Found while landing #7024 (PR #7057), which made the alloc-point deferral reachable under an explicit PERRY_GC_HEAP_LIMIT. It is a coverage loss, not a correctness bug, and it is worth a number of its own because it is the one place the matrix got less live.
The observable
scripts/gc_repsel_matrix.sh --pressure 8, test_gap_specabi_reassign:
| arm |
before #7024 |
after |
default |
PASS (cycles=1 scavenged=0) |
UNVER (cycles=0) |
verify_evac |
PASS (cycles=1) |
UNVER (cycles=0) |
cons_scan_off |
PASS (cycles=1 scavenged=3568) |
UNVER (cycles=0) |
gen_gc_off, wb_off, gen_off_verify, wb_off_force, all_four |
PASS |
UNVER |
The row does not collect at all any more on any arm that used to take the allocation-point fallback. It stays byte-exact against the oracle; the harness correctly reports UNVER rather than green.
Mechanism
test-files/test_gap_specabi_reassign.ts is a 20-line program with no loop. Codegen only emits the back-edge poll (js_gc_loop_safepoint) for allocating loop bodies, so this program contains no poll at all, and it does not run long enough to reach the microtask-pump safepoint.
Before #7024 the deferral was unreachable under the pressure knob (the defer cap and the trigger ceiling shared a formula), so a due nursery trigger fell through to the alloc-point non-moving minor and the row collected once. Now the trigger is correctly deferred to the next precise-root safepoint — and the program exits before reaching one. The safety valve (one gc_moving_defer_slack_dyn_bytes() of further growth) never fires either, because the program never allocates that much.
Nothing leaks: growth is bounded by the slack, and the process is exiting.
Why file it
- One corpus row went inert, and the matrix's whole premise is that an inert arm is not a green one. Nine cells that used to be PASS are now UNVER for this reason.
- It is a real property of the shipped collector, not just of the corpus: a loop-free program that reaches its first nursery trigger near exit now performs no collection at all where it previously performed one. For a short-lived process that is free; for anything holding a finalizer-bearing resource it is a behavioural difference worth knowing about.
Candidate directions (not prescriptive)
- Extend poll coverage so the deferral has a drain in more shapes — the
emit_gc_loop_safepoint doc already lists the specialized/versioned for lowerings, for-of and for-in as not yet emitting it (Phase 2 codegen work). That does not help a genuinely loop-free program.
- Drain a pending deferral at a function-return safepoint, not only at loop back-edges and the microtask pump.
- Or accept it and give the corpus row a shape that reaches a safepoint, so the matrix keeps its liveness on this representation. That is the cheapest, and the least honest, so it should be a deliberate choice rather than a drive-by edit.
Environment: release build, macOS arm64, pinned Node 26.5.0, --pressure 8.
Related: #7024, #6993, #7019, #6950.
Found while landing #7024 (PR #7057), which made the alloc-point deferral reachable under an explicit
PERRY_GC_HEAP_LIMIT. It is a coverage loss, not a correctness bug, and it is worth a number of its own because it is the one place the matrix got less live.The observable
scripts/gc_repsel_matrix.sh --pressure 8,test_gap_specabi_reassign:defaultcycles=1 scavenged=0)cycles=0)verify_evaccycles=1)cycles=0)cons_scan_offcycles=1 scavenged=3568)cycles=0)gen_gc_off,wb_off,gen_off_verify,wb_off_force,all_fourThe row does not collect at all any more on any arm that used to take the allocation-point fallback. It stays byte-exact against the oracle; the harness correctly reports UNVER rather than green.
Mechanism
test-files/test_gap_specabi_reassign.tsis a 20-line program with no loop. Codegen only emits the back-edge poll (js_gc_loop_safepoint) for allocating loop bodies, so this program contains no poll at all, and it does not run long enough to reach the microtask-pump safepoint.Before #7024 the deferral was unreachable under the pressure knob (the defer cap and the trigger ceiling shared a formula), so a due nursery trigger fell through to the alloc-point non-moving minor and the row collected once. Now the trigger is correctly deferred to the next precise-root safepoint — and the program exits before reaching one. The safety valve (one
gc_moving_defer_slack_dyn_bytes()of further growth) never fires either, because the program never allocates that much.Nothing leaks: growth is bounded by the slack, and the process is exiting.
Why file it
Candidate directions (not prescriptive)
emit_gc_loop_safepointdoc already lists the specialized/versionedforlowerings,for-ofandfor-inas not yet emitting it (Phase 2 codegen work). That does not help a genuinely loop-free program.Environment: release build, macOS arm64, pinned Node 26.5.0,
--pressure 8.Related: #7024, #6993, #7019, #6950.