Found by #6977, which made the evacuating minor reachable for the first time. #6950's premise held: nothing had ever moved, so nothing had ever tested this.
Result
scripts/gc_repsel_matrix.sh --arms all --pressure 8, release build, macOS arm64, pinned Node 26.5.0, with the evacuating base from #6977:
arm liveness across the corpus:
evac_minor requires=move collected 19/20 moved-objects 19/20
Evacuation is now genuinely live on 19 of 20 corpus files — and 14 of 20 break.
The discriminator is precise roots, not relocation
Both columns collect and both relocate. The only difference is whether the conservative native-stack scan is running.
| corpus test |
PERRY_GC_INCREMENTAL=0 (conservative scan ON) |
+ PERRY_CONSERVATIVE_STACK_SCAN=off (precise roots) |
repsel_canonical_i32 |
OK (1 122 copied) |
SIGABRT (4 579 copied) |
int_valued_ta_locals |
OK (12) |
OK (12) |
specabi_polymorphic_coexist |
OK (1 099) |
mismatch (9 647) |
specabi_reassign |
OK (1 101) |
mismatch (9 671) |
specabi_recursion_escape |
OK (4) |
OK (4) |
specabi_view_detach |
OK (6) |
OK (6) |
ta_param_numeric_read |
OK (15) |
SIGSEGV (5 948) |
typedarray_param_read |
OK (15) |
SIGABRT (5 965) |
repsel_canonical_str_locals |
OK (0) |
OK (63) |
repsel_ptr_shape_locals |
OK (6) |
SIGABRT (4 640) |
repsel_ptr_shape_barriers |
OK (5 923) |
SIGSEGV (4 616) |
repsel_p4a_holes_axis |
OK (5 888) |
OK (4 602) |
repsel_p4a_inline_tiers |
OK (5 631) |
mismatch (10 621) |
repsel_p4a_logical_numeric |
OK (5 633) |
mismatch (10 636) |
repsel_p4a3_numarray_barriers |
OK (18) |
mismatch (15 297) |
repsel_p4a3_numarray_growth |
OK (0) |
OK (0) |
repsel_p4a3_ptr_numarray |
OK (5 637) |
mismatch (10 639) |
repsel_p4b_field_store_elision |
OK (0) |
mismatch (9 698) |
repsel_gc_stress |
SIGSEGV — separate defect, see below |
mismatch (1 573 353) |
repsel_proven_this_frozen |
OK (5 935) |
SIGSEGV (4 614) |
(N copied) is [gc-copy-minor] ran copied_objects=N summed over the run — the collector's own evidence that survivors were relocated.
Read that table carefully: relocation with a conservative scan is fine. 19/20 pass while copying thousands of objects. What breaks is relocation with precise roots — i.e. exactly the values that only the conservative scan was keeping alive or keeping correct, which is the class #6951/#6972 opened and #6968/#6969/#6970/#6971 enumerate. This is the same conclusion #6972's commit message reached ("the scan was doing load-bearing correctness work, not acting as a safety net"), now measured across the corpus instead of on one shape.
Minimal reproducer
test-files/test_gap_specabi_reassign.ts, 20 lines, no loop:
function first(a: any) { return a[0]; }
let P = new Int32Array(4);
P[0] = 42;
console.log("before:", first(P));
P = new Int32Array([7, 8]);
console.log("after:", first(P));
$ PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 ./test_gap_specabi_reassign
before: 42
after: 7 <- correct, 1 101 objects copied
$ PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \
PERRY_GC_TRACE=1 PERRY_GC_DIAG=1 ./test_gap_specabi_reassign
before: 42
after: undefined <- 9 671 objects copied
third: 99 1
before: and third: are correct; only the middle call is wrong, and only in the run where a collection lands inside it. The value reaching first is a typed array passed under the specialized ABI — a raw unboxed pointer, which is neither a shadow-stack root nor a slot an evacuating cycle rewrites. The pre-collection register is stale afterwards, and a[0] reads through it.
Note this is the SAME defect shape as #6951 (an SSA register holding the only reference across a collection point), one layer further in: #6972 rooted the argument accumulator; this is the argument value itself when the specialized ABI passes it raw. #6969 (ctor args), #6970 (native-method args) and #6971 (string-method receiver + args) are neighbouring cases; this one is an ordinary user-function call.
Why this matters more than the count
PERRY_CONSERVATIVE_STACK_SCAN resolves Auto -> SkipDisabled in shipped builds. The only reason precise roots are not already the shipped behaviour on the automatic paths is that gc_check_trigger forces ManualGcScanGuard::force_full_scan() on both arms. That forced scan is currently load-bearing for correctness on 14 of 20 representation-corpus files. Any change that removes it — which is the direction the moving-GC work is going — turns this table into production behaviour.
Reproducing
cargo build --release -p perry -p perry-runtime -p perry-stdlib \
-p perry-runtime-static -p perry-stdlib-static
scripts/gc_repsel_matrix.sh --arms evac_minor --pressure 8 --no-build
(needs #6977 for the evac_minor arm; before it, every one of these cells reported UNVERIFIED because nothing moved.)
Related: #6950, #6977, #6951, #6968, #6969, #6970, #6971, #6942, #6946.
Found by #6977, which made the evacuating minor reachable for the first time. #6950's premise held: nothing had ever moved, so nothing had ever tested this.
Result
scripts/gc_repsel_matrix.sh --arms all --pressure 8, release build, macOS arm64, pinned Node 26.5.0, with the evacuating base from #6977:Evacuation is now genuinely live on 19 of 20 corpus files — and 14 of 20 break.
The discriminator is precise roots, not relocation
Both columns collect and both relocate. The only difference is whether the conservative native-stack scan is running.
PERRY_GC_INCREMENTAL=0(conservative scan ON)+ PERRY_CONSERVATIVE_STACK_SCAN=off(precise roots)repsel_canonical_i32int_valued_ta_localsspecabi_polymorphic_coexistspecabi_reassignspecabi_recursion_escapespecabi_view_detachta_param_numeric_readtypedarray_param_readrepsel_canonical_str_localsrepsel_ptr_shape_localsrepsel_ptr_shape_barriersrepsel_p4a_holes_axisrepsel_p4a_inline_tiersrepsel_p4a_logical_numericrepsel_p4a3_numarray_barriersrepsel_p4a3_numarray_growthrepsel_p4a3_ptr_numarrayrepsel_p4b_field_store_elisionrepsel_gc_stressrepsel_proven_this_frozen(N copied)is[gc-copy-minor] ran copied_objects=Nsummed over the run — the collector's own evidence that survivors were relocated.Read that table carefully: relocation with a conservative scan is fine. 19/20 pass while copying thousands of objects. What breaks is relocation with precise roots — i.e. exactly the values that only the conservative scan was keeping alive or keeping correct, which is the class #6951/#6972 opened and #6968/#6969/#6970/#6971 enumerate. This is the same conclusion #6972's commit message reached ("the scan was doing load-bearing correctness work, not acting as a safety net"), now measured across the corpus instead of on one shape.
Minimal reproducer
test-files/test_gap_specabi_reassign.ts, 20 lines, no loop:before:andthird:are correct; only the middle call is wrong, and only in the run where a collection lands inside it. The value reachingfirstis a typed array passed under the specialized ABI — a raw unboxed pointer, which is neither a shadow-stack root nor a slot an evacuating cycle rewrites. The pre-collection register is stale afterwards, anda[0]reads through it.Note this is the SAME defect shape as #6951 (an SSA register holding the only reference across a collection point), one layer further in: #6972 rooted the argument accumulator; this is the argument value itself when the specialized ABI passes it raw. #6969 (ctor args), #6970 (native-method args) and #6971 (string-method receiver + args) are neighbouring cases; this one is an ordinary user-function call.
Why this matters more than the count
PERRY_CONSERVATIVE_STACK_SCANresolvesAuto -> SkipDisabledin shipped builds. The only reason precise roots are not already the shipped behaviour on the automatic paths is thatgc_check_triggerforcesManualGcScanGuard::force_full_scan()on both arms. That forced scan is currently load-bearing for correctness on 14 of 20 representation-corpus files. Any change that removes it — which is the direction the moving-GC work is going — turns this table into production behaviour.Reproducing
(needs #6977 for the
evac_minorarm; before it, every one of these cells reported UNVERIFIED because nothing moved.)Related: #6950, #6977, #6951, #6968, #6969, #6970, #6971, #6942, #6946.