Skip to content

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

@proggeramlug

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_rooting asserted bind_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_root asserted !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:

  1. Pre-opt IR — the ptr addrspace(1) alloca set and the gc "statepoint-example" attribute are visible in what compile_module returns today. Cheapest; proves the request, not the result.
  2. Post-RS4GC IRgc.statepoint / gc.relocate and 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.
  3. The emitted __perry_gcmap sectioncrates/perry-codegen/src/gc_map.rs already 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions