@@ -12818,6 +12818,76 @@ test "parallel_js heap_limit_bytes fails closed with deferred generator tracing"
1281812818 try std.testing.expect(ctx.gc_par_deferred_rounds.load(.monotonic) > before_deferred_rounds);
1281912819}
1282012820
12821+ test "parallel_js heap_limit_bytes fails closed with deferred iterator helper tracing" {
12822+ // #36 sibling policy witness for Iterator Helper cells. The parallel tracer
12823+ // defers helper state for the same reason as generators: mutable helper
12824+ // fields change around callback/source-iterator execution. No-GIL
12825+ // allocation-failure recovery must therefore abort rather than reporting a
12826+ // safe sweep while a rooted helper remains deferred.
12827+ if (builtin.single_threaded) return error.SkipZigTest;
12828+ const ctx = try Context.createWithTestingOptions(std.testing.allocator, .{
12829+ .enable_threads = true,
12830+ .enable_gc = true,
12831+ .parallel_gc = true,
12832+ .parallel_js = true,
12833+ .heap_limit_bytes = 8 * 1024 * 1024,
12834+ });
12835+ defer ctx.destroy();
12836+
12837+ const prepared = try ctx.evaluate(
12838+ \\(() => {
12839+ \\ const source = Iterator.from([1, 2, 3, 4, 5]);
12840+ \\ globalThis.__deferredRecoveryHelper =
12841+ \\ source.map(x => ({ value: x + 1 })).filter(o => o.value % 2 === 0);
12842+ \\ return globalThis.__deferredRecoveryHelper.next().value.value;
12843+ \\})();
12844+ );
12845+ try std.testing.expectEqual(@as(f64, 2), prepared.asNum());
12846+
12847+ var sibling: jsthread.ThreadRecord = .{ .id = 999, .gil = ctx.gil.?, .ctx = ctx };
12848+ {
12849+ ctx.gil.?.lockApi();
12850+ defer ctx.gil.?.unlockApi();
12851+ try ctx.reserveJsThreadsLocked(1);
12852+ ctx.js_threads.appendAssumeCapacity(&sibling);
12853+ }
12854+ defer {
12855+ ctx.gil.?.lockApi();
12856+ var i: usize = ctx.js_threads.items.len;
12857+ while (i > 0) {
12858+ i -= 1;
12859+ if (ctx.js_threads.items[i] == &sibling) {
12860+ _ = ctx.js_threads.swapRemove(i);
12861+ break;
12862+ }
12863+ }
12864+ ctx.gil.?.unlockApi();
12865+ }
12866+
12867+ const before_attempts = ctx.gc_par_attempts.load(.monotonic);
12868+ const before_collections = ctx.gc_par_collections.load(.monotonic);
12869+ const before_aborts = ctx.gc_par_aborts.load(.monotonic);
12870+ const before_round_aborts = ctx.gc_par_round_limit_aborts.load(.monotonic);
12871+ const before_deferred_rounds = ctx.gc_par_deferred_rounds.load(.monotonic);
12872+
12873+ const gc_saved = gc_mod.setActiveHeap(ctx.gc);
12874+ defer _ = gc_mod.setActiveHeap(gc_saved);
12875+ const ss_saved = stack_scan.enter(@frameAddress());
12876+ defer stack_scan.leave(ss_saved);
12877+ var machine = ctx.interpreter();
12878+ try ctx.pushActiveInterpreter(&machine);
12879+ defer ctx.popActiveInterpreter(&machine);
12880+ const ai_saved = gc_mod.setActiveInterpreter(&machine);
12881+ defer _ = gc_mod.setActiveInterpreter(ai_saved);
12882+
12883+ try std.testing.expect(!ctx.collectForAllocationFailure(&machine));
12884+ try std.testing.expect(ctx.gc_par_attempts.load(.monotonic) > before_attempts);
12885+ try std.testing.expectEqual(before_collections, ctx.gc_par_collections.load(.monotonic));
12886+ try std.testing.expect(ctx.gc_par_aborts.load(.monotonic) > before_aborts);
12887+ try std.testing.expect(ctx.gc_par_round_limit_aborts.load(.monotonic) > before_round_aborts);
12888+ try std.testing.expect(ctx.gc_par_deferred_rounds.load(.monotonic) > before_deferred_rounds);
12889+ }
12890+
1282112891test "parallel_js (M3): sync wait peers publish roots for mid-script parallel GC" {
1282212892 // Sync property waits, Condition waits, and contended Lock acquisition are
1282312893 // not frozen `gc_parked` peers: they periodically pump tasks between short
0 commit comments