With the conservative native-stack scan disabled — i.e. with precise (shadow-stack) roots only — a collection that lands during argument evaluation drops console.log's string-literal argument. The literal is simply gone from the output; no crash, no diagnostic.
This is the same signature #6942 recorded in item 3 ("the console.log string literal arguments are gone too — the labels vanish"), but reduced to a case that needs no evacuation and no manual minor(): a plain allocation-driven, non-moving full mark-sweep is enough. That makes it easier to fix and easier to regression-test than the #6942 form.
Repro
let sink: unknown[] = [];
function churn(n: number): number {
let acc = 0;
for (let i = 0; i < n; i++) {
sink.push({ i: i, s: "x" + (i & 255), a: [i, i + 1] });
if (sink.length > 4096) { acc = (acc + sink.length) | 0; sink = []; }
}
return acc | 0;
}
console.log("alpha", churn(420000));
console.log("bravo", churn(420000));
console.log("charlie", churn(420000));
console.log("delta", churn(420000));
console.log("echo", churn(420000));
$ perry repro.ts -o repro && ./repro # and node --experimental-strip-types
alpha 417894
bravo 421991
charlie 417894
delta 421991
echo 417894
$ PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 ./repro
alpha 417894
bravo 421991
charlie 417894
421991 <- "delta" gone
417894 <- "echo" gone
Deterministic across runs. Measured on origin/main @ 83a6767ff, macOS arm64, --release, pinned Node 26.5.0. PERRY_GC_TRACE=1 confirms real collections occur (3 cycles).
It is not a representation-selection defect
Reproduced identically with all representation gates off (PERRY_CANONICAL_I32_LOCALS=0 PERRY_CANONICAL_STR_LOCALS=0 PERRY_PTR_SHAPE_LOCALS=0 PERRY_PTR_NUMARRAY_LOCALS=0 PERRY_SPECIALIZED_ABI=0 PERRY_INT_VALUED_LOCALS=0), on test-files/test_gap_repsel_gc_stress.ts — byte-identical corrupted output with the representations on and off. The evaluated-but-not-yet-consumed argument temporary is not a precise root, independently of how the surrounding locals are represented.
Why it is invisible today, and why that is the problem
Production is not currently exposed: every automatic collection takes ManualGcScanGuard::force_full_scan() (gc/policy.rs::gc_check_trigger, both the OldReclaim and the nursery arms), so the conservative scan pins the literal. But that means the conservative scan is doing load-bearing correctness work, not acting as a safety net — and gc/roots.rs's nominal production default is Auto → SkipDisabled. Any change that narrows the forced scan (which is what #6942 asks for so the #6655/#6935 rooting bug class becomes testable at all) turns this into a live miscompile.
It also blocks the one GC arm with real detection power: PERRY_CONSERVATIVE_STACK_SCAN=off is the only configuration that can observe a missing shadow-slot binding on a representation-selected pointer local. Until argument temporaries are precise roots, that arm is red for an unrelated reason and cannot discriminate. It is carried as a triaged expected-red in test-parity/gc_repsel_triage.txt against this issue.
Related: #6942, #6946, #6655, #6935.
With the conservative native-stack scan disabled — i.e. with precise (shadow-stack) roots only — a collection that lands during argument evaluation drops
console.log's string-literal argument. The literal is simply gone from the output; no crash, no diagnostic.This is the same signature #6942 recorded in item 3 ("the
console.logstring literal arguments are gone too — the labels vanish"), but reduced to a case that needs no evacuation and no manualminor(): a plain allocation-driven, non-moving full mark-sweep is enough. That makes it easier to fix and easier to regression-test than the #6942 form.Repro
Deterministic across runs. Measured on
origin/main@83a6767ff, macOS arm64,--release, pinned Node 26.5.0.PERRY_GC_TRACE=1confirms real collections occur (3 cycles).It is not a representation-selection defect
Reproduced identically with all representation gates off (
PERRY_CANONICAL_I32_LOCALS=0 PERRY_CANONICAL_STR_LOCALS=0 PERRY_PTR_SHAPE_LOCALS=0 PERRY_PTR_NUMARRAY_LOCALS=0 PERRY_SPECIALIZED_ABI=0 PERRY_INT_VALUED_LOCALS=0), ontest-files/test_gap_repsel_gc_stress.ts— byte-identical corrupted output with the representations on and off. The evaluated-but-not-yet-consumed argument temporary is not a precise root, independently of how the surrounding locals are represented.Why it is invisible today, and why that is the problem
Production is not currently exposed: every automatic collection takes
ManualGcScanGuard::force_full_scan()(gc/policy.rs::gc_check_trigger, both theOldReclaimand the nursery arms), so the conservative scan pins the literal. But that means the conservative scan is doing load-bearing correctness work, not acting as a safety net — andgc/roots.rs's nominal production default isAuto → SkipDisabled. Any change that narrows the forced scan (which is what #6942 asks for so the #6655/#6935 rooting bug class becomes testable at all) turns this into a live miscompile.It also blocks the one GC arm with real detection power:
PERRY_CONSERVATIVE_STACK_SCAN=offis the only configuration that can observe a missing shadow-slot binding on a representation-selected pointer local. Until argument temporaries are precise roots, that arm is red for an unrelated reason and cannot discriminate. It is carried as a triaged expected-red intest-parity/gc_repsel_triage.txtagainst this issue.Related: #6942, #6946, #6655, #6935.