@@ -12888,6 +12888,86 @@ test "parallel_js heap_limit_bytes fails closed with deferred iterator helper tr
1288812888 try std.testing.expect(ctx.gc_par_deferred_rounds.load(.monotonic) > before_deferred_rounds);
1288912889}
1289012890
12891+ test "parallel_js heap_limit_bytes fails closed with deferred async-generator requests" {
12892+ // #36 request-buffer witness: an async generator suspended on an unresolved
12893+ // await keeps later next/return requests in its GC-backed request side
12894+ // store. That cell is still a Generator cell and is deferred by the parallel
12895+ // tracer, so no-GIL allocation-failure recovery must fail closed rather than
12896+ // sweep while a peer is live.
12897+ if (builtin.single_threaded) return error.SkipZigTest;
12898+ const ctx = try Context.createWithTestingOptions(std.testing.allocator, .{
12899+ .enable_threads = true,
12900+ .enable_gc = true,
12901+ .parallel_gc = true,
12902+ .parallel_js = true,
12903+ .heap_limit_bytes = 8 * 1024 * 1024,
12904+ });
12905+ defer ctx.destroy();
12906+
12907+ const baseline = ctx.gc_generator_backing_stores_live;
12908+ const prepared = try ctx.evaluate(
12909+ \\(() => {
12910+ \\ async function* ag() {
12911+ \\ await new Promise(() => {});
12912+ \\ yield 1;
12913+ \\ }
12914+ \\ const it = ag();
12915+ \\ const p1 = it.next();
12916+ \\ const p2 = it.next();
12917+ \\ const p3 = it.return(7);
12918+ \\ globalThis.__deferredRecoveryAsyncGenerator = { it, p1, p2, p3 };
12919+ \\ return typeof p1.then === 'function' &&
12920+ \\ typeof p2.then === 'function' &&
12921+ \\ typeof p3.then === 'function';
12922+ \\})();
12923+ );
12924+ try std.testing.expect(prepared.asBool());
12925+ try std.testing.expect(ctx.gc_generator_backing_stores_live > baseline);
12926+
12927+ var sibling: jsthread.ThreadRecord = .{ .id = 999, .gil = ctx.gil.?, .ctx = ctx };
12928+ {
12929+ ctx.gil.?.lockApi();
12930+ defer ctx.gil.?.unlockApi();
12931+ try ctx.reserveJsThreadsLocked(1);
12932+ ctx.js_threads.appendAssumeCapacity(&sibling);
12933+ }
12934+ defer {
12935+ ctx.gil.?.lockApi();
12936+ var i: usize = ctx.js_threads.items.len;
12937+ while (i > 0) {
12938+ i -= 1;
12939+ if (ctx.js_threads.items[i] == &sibling) {
12940+ _ = ctx.js_threads.swapRemove(i);
12941+ break;
12942+ }
12943+ }
12944+ ctx.gil.?.unlockApi();
12945+ }
12946+
12947+ const before_attempts = ctx.gc_par_attempts.load(.monotonic);
12948+ const before_collections = ctx.gc_par_collections.load(.monotonic);
12949+ const before_aborts = ctx.gc_par_aborts.load(.monotonic);
12950+ const before_round_aborts = ctx.gc_par_round_limit_aborts.load(.monotonic);
12951+ const before_deferred_rounds = ctx.gc_par_deferred_rounds.load(.monotonic);
12952+
12953+ const gc_saved = gc_mod.setActiveHeap(ctx.gc);
12954+ defer _ = gc_mod.setActiveHeap(gc_saved);
12955+ const ss_saved = stack_scan.enter(@frameAddress());
12956+ defer stack_scan.leave(ss_saved);
12957+ var machine = ctx.interpreter();
12958+ try ctx.pushActiveInterpreter(&machine);
12959+ defer ctx.popActiveInterpreter(&machine);
12960+ const ai_saved = gc_mod.setActiveInterpreter(&machine);
12961+ defer _ = gc_mod.setActiveInterpreter(ai_saved);
12962+
12963+ try std.testing.expect(!ctx.collectForAllocationFailure(&machine));
12964+ try std.testing.expect(ctx.gc_par_attempts.load(.monotonic) > before_attempts);
12965+ try std.testing.expectEqual(before_collections, ctx.gc_par_collections.load(.monotonic));
12966+ try std.testing.expect(ctx.gc_par_aborts.load(.monotonic) > before_aborts);
12967+ try std.testing.expect(ctx.gc_par_round_limit_aborts.load(.monotonic) > before_round_aborts);
12968+ try std.testing.expect(ctx.gc_par_deferred_rounds.load(.monotonic) > before_deferred_rounds);
12969+ }
12970+
1289112971test "parallel_js (M3): sync wait peers publish roots for mid-script parallel GC" {
1289212972 // Sync property waits, Condition waits, and contended Lock acquisition are
1289312973 // not frozen `gc_parked` peers: they periodically pump tasks between short
0 commit comments