Repository navigation
The shipped root lowering (native/statepoints) has no coverage in the suites #7493 pinned to shadow — 9 mechanics, 3 of them found passing vacuously #7502
Description
Split out of #7493, which restored these suites' assertions by pinning them to the lowering they were written against. That was the right repair — but it leaves a hole that "the tests are green again" hides, and this issue is that hole.
The problem
Since #7370, native roots (RS4GC statepoints) are the lowering that ships on aarch64 and x86_64 — every target whose frames the runtime can walk. The shadow stack survives only as the fallback for arm64_32 watchOS and ARM64 Windows.
After #7493, shadow_slot_hygiene (12 tests) and scalar_replaced_slot_roots (11 tests) pin NativeRootsPin::shadow(). Every assertion in both files is about a mechanism the shipped configuration does not use:
| mechanic asserted (shadow) | native-roots equivalent | covered today? |
|---|---|---|
| a pointer-typed local reserves a frame slot | the local's alloca is ptr addrspace(1) and appears in the stack map |
no |
| a dead value's slot is cleared before the next allocation | the value is not live at the statepoint, so it is not in that statepoint's live set | no |
| a numeric local reserves NO slot | a numeric local's alloca stays ptr / double, out of the map |
no |
| slot indices are not shifted by an interleaved numeric local | (no indices exist; the map is keyed by frame offset) | n/a |
| the entry-module frame starts AFTER the init prelude | the entry function's first statepoint is after js_gc_init |
no |
| a loop body's slots are cleared each iteration | per-back-edge statepoint live sets do not carry the previous iteration's value | no |
| a scalar-replaced field holding a heap value is a precise root (#6968) | that field's alloca is addrspace(1) and relocated |
no |
| a scalar-replaced numeric-only literal pays NO rooting (#6997) | its allocas stay non-addrspace(1) |
no |
duplicate var declarations keep every slot inside the frame (#7184's shape) |
(no frame bound exists; the #7184 shape is unrepresentable) | n/a |
So: nine distinct root-lowering mechanics have zero assertions against the lowering Perry actually emits. Six of the nine are the exact shapes docs/src/internals/gc-rooting-invariant.md lists as having already shipped broken.
gc-root-dominance.yml is not a substitute. It reads emitted IR for store dominance — it answers "is this root store ordered correctly", not "is this value a root at all". A value the statepoint lowering silently declines to put in the map has no store for it to check.
Why it matters more than the usual "add tests" issue
Both #7493's own findings are instances of this:
numeric_only_scalar_replaced_{object,array}_emits_no_rootingassertedbind_calls(&ir) == 0. Under the native default that count is zero for every program — the tests were passing vacuously, i.e. CLAUDE.md hazard 4.a_collection_free_construction_emits_no_this_slot_rootasserted!contains("@js_shadow_slot_bind"), likewise true of every program.
Three tests that read as coverage were measuring nothing. There is no reason to think those are the only three; they are the three that happened to be adjacent to a lowering pin.
What to build
A native-roots counterpart for each row above. The load-bearing design question is what to assert against, since the statepoint form has no bind call to count:
- Pre-
optIR — theptr addrspace(1)alloca set and thegc "statepoint-example"attribute are visible in whatcompile_modulereturns today. Cheapest; proves the request, not the result. - Post-RS4GC IR —
gc.statepoint/gc.relocateand each statepoint's live-value list. Proves the result. Needs the in-process pipeline (In-process LLVM backend: native construction via the C API, opt-in (#7241, engine-plan layer 0) #7301) to be reachable from a test. - The emitted
__perry_gcmapsection —crates/perry-codegen/src/gc_map.rsalready builds it; assert on the parsed map. Strongest, and it is what the runtime actually reads.
(3) is the one that can catch "the map says nothing lives here". Prefer it where the mechanic is about which values are roots; (1) is fine for the negative direction (a numeric local must never become addrspace(1)).
Acceptance
- Each no row above has at least one native-roots assertion.
- Each new test is sabotage-checked: with the rooting deliberately dropped it must fail. A negative assertion in particular must be shown to be non-vacuous under the lowering it runs on — that is the specific mistake this issue exists to stop repeating.
- The shadow-pinned originals stay. Both lowerings are supported; both need coverage. The point is not to replace them.
Activity
docs(test): point the lowering notes at the filed follow-ups (#7502, #…
fix(test): make the root-lowering pin reachable from integration suit…
test(gc): cover the root lowering that actually ships (#7502)
test(gc): cover the root lowering that actually ships — native-roots …