Skip to content

fix(test): make the root-lowering pin reachable from integration suites, and say which lowering each asserts (#7493) - #7509

Merged
proggeramlug merged 4 commits into
mainfrom
fix/7493-integration-suites-native-roots
Aug 6, 2026
Merged

fix(test): make the root-lowering pin reachable from integration suites, and say which lowering each asserts (#7493)#7509
proggeramlug merged 4 commits into
mainfrom
fix/7493-integration-suites-native-roots

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7493.

What was wrong

#7370 made native roots (RS4GC statepoints) the default lowering. NativeRootsPin was added so a test asserting on shadow-stack IR could say so; the in-crate unit tests were repaired with it. Five integration suites assert the same mechanics and had no pin to reach for — NativeRootsPin is #[cfg(test)], and tests/*.rs link the crate as an ordinary external consumer, so the item does not exist for them. Those suites run nightly/at-tag only, so nothing turned red at merge time and shadow_slot_hygiene sat at 0/12 on main.

1. The pin mechanism, and why it cannot leak

perry_codegen::testing::NativeRootsPin, behind a testing cargo feature that only this crate's own [dev-dependencies] entry enables.

Three shapes were available; the other two were rejected:

  • #[doc(hidden)] pub, unconditional — the pin would ship in every perry binary, and the thread-local read it adds at the top of rs4gc_enabled() would be compiled into the production decision path. A knob that is merely undocumented is still a knob, and this repo has a written policy about modes nobody decided to have (CLAUDE.md, "GC knob kill-policy").
  • Move the suite bodies into in-crate #[cfg(test)] modules (CI: PR cargo-test never executes crates/perry integration suites; main-push full run starves under merge trains (documented near-miss) #5960's suggestion, and the issue's). Correct in principle — it would also put them in the per-PR cargo-test gate — but native_proof_regressions.rs alone is 14k lines against a 2000-line file cap. Not a mechanical move.

The feature's safety is structural, not a promise:

  • With testing off, NativeRootsPin, its backing thread-local, and the if let Some(pinned) = … arm in rs4gc_enabled() are #[cfg]-ed out of the artifact — absent, not private-and-unreachable.

  • Cargo resolves dev-dependency features only when a test/bench target is built. Verified on this branch:

    $ cargo tree -p perry-codegen -e features,no-dev -i perry-codegen
    perry-codegen v0.5.1285
    ├── perry-codegen feature "default" (command-line)
    └── perry-codegen feature "llvm-inprocess"
    

    No testing. With dev-dependencies included it appears; that is the only edge.

  • A production edge added later would be a silent regression, so it is gated: codegen::testing_feature_gate_tests scans every workspace manifest and fails if any [dependencies] / [build-dependencies] / [target.*.dependencies] table enables perry-codegen's testing. It lives in src/, so it runs in cargo-test — a required context. It asserts its own subject was live (must find >10 manifests, must find this crate's, must find the legitimate dev edge) and is sabotage-tested against a planted production edge.

NativeRootsPin::native() joins shadow(). Native roots being today's default does not make it redundant: a pin also outranks PERRY_RS4GC, so a pinned assertion means the same thing during the PERRY_RS4GC=0 sweep a GC engineer actually runs.

2. Per-test classification

Per test, not per file — several files disagree internally, and one of the findings below is exactly a case where a blanket pin would have been wrong.

suite shadow native unpinned before → after (single-threaded)
shadow_slot_hygiene 12 0/12 → 11/12
scalar_replaced_slot_roots 11 2/11 → 5/11
temp_root_operand_temporaries 2 17 12/19 → 13/19
temp_root_argument_temporaries 7 3/7 → 3/7
native_proof_regressions 2 15 238 249/253 → 253/255
native_proof_buffer_views 1 31 28/30 → 30/32
typed_feedback (#7492, untouched) 16 16/16 → 16/16

Reasoning:

  • shadow_slot_hygiene — 12/12 shadow. The file's subject is the shadow frame: slot reservation, bind/clear ordering, slot indices, the post-init frame region. Native roots have no frame, no index and no bind — nothing to translate.
  • scalar_replaced_slot_roots — 11/11 shadow. Every test measures js_shadow_slot_bind / js_shadow_frame_enter call sites.
  • temp_root_operand_temporaries — 2 shadow. the_inline_ctor_this_slot_is_bound_as_a_shadow_slot and a_collection_free_construction_emits_no_this_slot_root assert the shadow spelling of the this-slot root. The other 17 are lowering-independent and stay unpinned.
  • temp_root_argument_temporaries — none. PERRY_RS4GC=0 moves it not at all (3/7 either way). Its failures are perf(codegen): lower temp roots onto pooled frame allocas — three FFI calls per temporary become a store and a load #7487's, not feat(gc): make native roots (statepoints) the default #7370's.
  • native_proof_regressions — 2 shadow, 15 native. The two assert the shadow spelling of a box-pointer slot (store i64 <bits>, ptr %slot vs store ptr addrspace(1) %rs4gc.sN). The fifteen in invalidation are pinned native for a different reason, given below.
  • native_proof_buffer_views — 1 native, same reason as invalidation.

Two corrections to the issue's own data, both from running the tests alone:

Two tests were pinned even though they were passing

numeric_only_scalar_replaced_{object,array}_emits_no_rooting assert bind_calls(&ir) == 0, and a_collection_free_construction_emits_no_this_slot_root asserts !contains("@js_shadow_slot_bind"). Under the post-#7370 default those hold for every program, rooted or not — they were green without their subject ever running (CLAUDE.md hazard 4). Pinning them makes them assert their subject again. The first two now fail for a real reason (#7504); the third passes and is now meaningful.

That is a deliberate green→red: a red test that is measuring something beats a green one that is not.

Why the invalidation fifteen are pinned native

assert_buffer_store_uses_dynamic_fallback proves the absence of a native buffer GEP with a module-wide !ir.contains("getelementptr inbounds i8"). The shadow lowering's inline slot addressing (#7088) emits exactly that instruction for reasons unrelated to any buffer, so under PERRY_RS4GC=0 fifteen tests report a stale proof that was never there. The pin stops the false alarm; the assertion is still wrong and is filed as #7505 with the data-flow fix it should have.

3. Which shipped-lowering mechanics are now uncovered — #7502

This is the part "the tests are green again" hides. 23 tests now pin the shadow stack, which is not what ships on aarch64 or x86_64. Nine distinct mechanics have no native-roots assertion anywhere:

mechanic native equivalent covered?
a pointer-typed local reserves a frame slot the alloca is addrspace(1) and is in the stack map no
a dead value's slot is cleared before the next allocation not in that statepoint's live set no
a numeric local reserves NO slot its alloca stays non-addrspace(1) no
the entry-module frame starts after the init prelude first statepoint is after js_gc_init no
loop-body slots clear each iteration per-back-edge live sets no
a scalar-replaced field holding a heap value is a precise root (#6968) that field's alloca is relocated no
a scalar-replaced numeric-only literal pays no rooting (#6997) its allocas stay non-addrspace(1) no
the this slot of an inline ctor is rooted (#7202/#7207) ditto no
slot indices not shifted by an interleaved numeric local n/a (map is offset-keyed) n/a
duplicate var decls stay inside the frame (#7184's shape) n/a (no frame bound exists) n/a

Six of the seven "no" rows are shapes docs/src/internals/gc-rooting-invariant.md records as having already shipped broken. gc-root-dominance.yml is not a substitute: it checks store ordering in emitted IR, not whether a value is a root at all — a value the lowering declines to put in the map has no store for it to check.

Three of those tests were found passing vacuously in this PR. There is no reason to think they are the only three; they are the three that happened to sit next to a lowering pin.

#7502 carries the full table, the three candidate assertion surfaces (pre-opt IR / post-RS4GC live sets / the emitted __perry_gcmap section) and a sabotage requirement for each new test, including for negative assertions specifically.

4. Also fixed: the poison cascade that hid this suite's signal

native_proof_regressions reported 55 failures at default parallelism and 4 under --test-threads=1. 51 of the 55 were PoisonError at ARTIFACT_ENV_LOCK.lock().unwrap()#7490's shape again, and the scheduler picked the victims, which reads as order-dependent codegen state.

Root cause, two defects in one hand-rolled helper (present in identical copy-pasted form in both native_proof_* suites):

  1. The PERRY_NATIVE_REPS* env vars are read by compile_module from the process environment and the restore was written out by hand after the compile — so a panic inside compile_module left them installed for the rest of the binary. Every later compile, including the many compile_ir ones that never take this lock, then wrote artifact JSON into a directory another test was reading.
  2. That torn read (serde_jsonEOF while parsing a value) panicked inside the lock, poisoning it.

Both copies are replaced by one shared tests/native_proof_support/mod.rs: a poison-tolerant accessor, an RAII env guard, and an artifact reader that treats a foreign or half-written neighbour as noise while still naming everything it skipped if the subject turns up missing. Two sabotage tests plant each failure shape and assert the fix is what prevents it — they fail against the pre-fix code, which is what makes a green run evidence.

Result: 198/253 → 253/255 at default parallelism, identical to single-threaded.

(Extracting the helper also brought native_proof_buffer_views.rs from 2059 back to 1963 lines, under the file-size gate.)

5. CI visibility — what was decided and why

ci_e2e_scope.py already runs the suites a diff names, so all six run on this PR. What it deliberately refuses is the source→suite direction, and that is the hole #7370 fell through: it changed crates/perry-codegen/src/, named no suite, and the per-PR cargo-test gate is --lib --bins.

Cost is not the objection. These are in-process compiles of hand-built HIR with emit_ir_only: true — no perry compile subprocess, no link. Measured, --test-threads=1: 0.17s, 0.18s, 0.08s, 0.39s, 0.50s, 5.13s — ~6.5s for all six.

The mapping is not added here, on purpose. Fifteen tests across these suites are red for three causes this PR does not own (#7494, #7503, #7504). e2e-scoped is not in branch protection's required contexts, so wiring crates/perry-codegen/src/** → these six today would produce a job that is red on most perry-codegen PRs and cannot block anything — CLAUDE.md hazard 2 with extra steps. Reviewers learn to ignore it, and it can then never be promoted. A new gate has never been green; this one would start red by construction.

Filed as #7507 with the concrete SOURCE_SUITE_MAP change, the --self-test extension, and the "an entry that matches nothing FAILS" cross-check — blocked on the residue issues.

What IS added, and is green today, is the tripwire that catches the exact #7370 shape in a required context: host_target_lowering_default_is_native_roots, a unit test in src/ (so it runs in cargo-test). It fails the moment the root-lowering default flips again, with the follow-on work named in the failure message. It asserts its subject is live — the two pins must give different answers, and the unsupported-target arm (arm64_32-apple-watchos) must give the opposite default — so an rs4gc_enabled() folded to a constant fails it rather than passing it.

Verification

Three consecutive runs at default parallelism and three at --test-threads=1, all seven suites. Results identical in all six sweeps — no order dependence, no parallelism dependence:

shadow_slot_hygiene              11 passed;  1 failed
scalar_replaced_slot_roots        5 passed;  6 failed
temp_root_operand_temporaries    13 passed;  6 failed
temp_root_argument_temporaries    3 passed;  4 failed
native_proof_regressions        253 passed;  2 failed
native_proof_buffer_views        30 passed;  2 failed
typed_feedback                   16 passed;  0 failed   <- #7492, not regressed
perry-codegen --lib             644 passed;  0 failed

cargo fmt --all -- --check clean; scripts/check_file_size.sh clean; scripts/check_test_registration.py clean (162 files / 4 registries); scripts/ci_e2e_scope.py --self-test ok; cargo check -p perry clean.

The 15 remaining failures, every one filed

No test was deleted, skipped or weakened.

tests cause issue
integer_modulo::…keep_frem, proven_buffer_and_typed_array_reads…, reassigned_typed_array_store… lowering-independent proof/record drift #7494 (pre-existing)
10 in temp_root_{argument,operand}_temporaries assert the pre-#7487 FFI temp-root spelling; the 8 that still pass are vacuous #7503
6 in scalar_replaced_slot_roots + flat_const_row_aliases… bind_calls counts module-wide and #7487's pooled temp roots emit js_shadow_slot_bind too #7504
typed_f64_receiver_method_clone… guard-failure edge no longer calls $generic #7506

Two of those (#7503, #7504) are themselves coverage findings, not just red tests: between them, the #6951/#6969/#6970/#6971/#7114/#7154/#7200 temp-root contract currently has no working assertion in either direction, and #6997's "numeric literals pay no rooting" is unmeasurable.

Follow-ups filed

Summary by CodeRabbit

  • New Features

    • Added opt-in testing controls for selecting native-root or shadow-stack lowering.
    • Added shared support for safely managing test environments and compiled artifacts.
  • Bug Fixes

    • Improved reliability of native-root integration and regression tests, including cleanup after failures.
  • Documentation

    • Clarified root-lowering expectations, test coverage, and known limitations.
  • Chores

    • Incremented the workspace version to 0.5.1291.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72e91af0-5671-464e-86ba-49571f56f3c8

📥 Commits

Reviewing files that changed from the base of the PR and between d3472a5 and 13fd833.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • Cargo.toml
  • changelog.d/7509-integration-suites-native-roots.md
  • crates/perry-codegen/Cargo.toml
  • crates/perry-codegen/src/codegen/helpers.rs
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/codegen/testing_feature_gate_tests.rs
  • crates/perry-codegen/src/lib.rs
  • crates/perry-codegen/src/testing.rs
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/native_proof_regressions/invalidation.rs
  • crates/perry-codegen/tests/native_proof_support/mod.rs
  • crates/perry-codegen/tests/scalar_replaced_slot_roots.rs
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
  • crates/perry-codegen/tests/temp_root_argument_temporaries.rs
  • crates/perry-codegen/tests/temp_root_operand_temporaries.rs

📝 Walkthrough

Walkthrough

The change exposes NativeRootsPin through a testing feature, adds feature and lowering-default tripwires, centralizes native-proof test environment handling, and pins integration tests to the lowering they assert.

Changes

Native-root testing support

Layer / File(s) Summary
Testing feature and lowering API
crates/perry-codegen/Cargo.toml, crates/perry-codegen/src/codegen/helpers.rs, crates/perry-codegen/src/lib.rs, crates/perry-codegen/src/testing.rs
The testing feature exposes NativeRootsPin. The API selects native-root or shadow-stack lowering and restores the prior thread-local state when dropped.
Feature gate and default tripwires
crates/perry-codegen/src/codegen/mod.rs, crates/perry-codegen/src/codegen/testing_feature_gate_tests.rs
Tests scan workspace manifests for invalid production feature edges and verify native defaults, platform fallback behavior, explicit overrides, and state restoration.
Shared native-proof harness
crates/perry-codegen/tests/native_proof_support/*, crates/perry-codegen/tests/native_proof_buffer_views.rs, crates/perry-codegen/tests/native_proof_regressions.rs
Shared utilities serialize artifact-environment access, restore variables during unwinding, tolerate poisoned mutexes, and locate valid module artifacts.
Lowering-specific proof suites
crates/perry-codegen/tests/native_proof_buffer_views.rs, crates/perry-codegen/tests/native_proof_regressions*, crates/perry-codegen/tests/scalar_replaced_slot_roots.rs, crates/perry-codegen/tests/shadow_slot_hygiene.rs, crates/perry-codegen/tests/temp_root_*
Tests explicitly select native-root or shadow-stack lowering. Documentation identifies stale or intentionally unpinned assertions.
Release metadata
Cargo.toml, changelog.d/7509-integration-suites-native-roots.md
The workspace version changes to 0.5.1291. The changelog records the testing controls, harness, tripwires, and tracked failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IntegrationTest
  participant NativeRootsPin
  participant Codegen
  participant ArtifactHarness
  IntegrationTest->>NativeRootsPin: select native or shadow lowering
  IntegrationTest->>Codegen: compile module with selected lowering
  Codegen->>ArtifactHarness: produce and locate module artifact
  ArtifactHarness-->>IntegrationTest: return matching JSON artifact
  NativeRootsPin-->>IntegrationTest: restore prior lowering state
Loading

Possibly related issues

Possibly related PRs

  • PerryTS/perry#7370 — Introduced native-root defaulting that requires explicit lowering selection in these tests.
  • PerryTS/perry#7492 — Relates to poison-tolerant mutex handling and panic-isolation tests.
  • PerryTS/perry#7322 — Overlaps with native-root lowering controls and testing.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7493-integration-suites-native-roots

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/codegen/helpers.rs`:
- Around line 173-203: Make NativeRootsPin non-Send so it cannot be moved across
threads and dropped on a different thread. Update the NativeRootsPin definition
using an existing thread-affinity marker pattern, while preserving its shadow(),
native(), and Drop behavior on the creating thread.

In `@crates/perry-codegen/src/codegen/testing_feature_gate_tests.rs`:
- Around line 109-129: Update the dependency scanning logic around
names_this_crate and the section-processing flow to resolve Cargo dependency
aliases via each table’s package field before accepting the testing-feature
gate. Recognize tables such as dependencies.codegen with package =
"perry-codegen", preserve existing direct-name handling, and add a planted
renamed-dependency test case covering features = ["testing"].
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9be38679-88fc-4abe-8a0d-e7de8e423326

📥 Commits

Reviewing files that changed from the base of the PR and between 40214c5 and 10d8f3d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • changelog.d/7509-integration-suites-native-roots.md
  • crates/perry-codegen/Cargo.toml
  • crates/perry-codegen/src/codegen/helpers.rs
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/codegen/testing_feature_gate_tests.rs
  • crates/perry-codegen/src/lib.rs
  • crates/perry-codegen/src/testing.rs
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/native_proof_regressions/invalidation.rs
  • crates/perry-codegen/tests/native_proof_support/mod.rs
  • crates/perry-codegen/tests/scalar_replaced_slot_roots.rs
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
  • crates/perry-codegen/tests/temp_root_argument_temporaries.rs
  • crates/perry-codegen/tests/temp_root_operand_temporaries.rs

Comment on lines +173 to 203
#[cfg(any(test, feature = "testing"))]
pub struct NativeRootsPin(Option<bool>);

#[cfg(test)]
#[cfg(any(test, feature = "testing"))]
impl NativeRootsPin {
/// Pin this thread to the shadow-stack lowering for the guard's lifetime.
pub(crate) fn shadow() -> Self {
/// Pin this thread to the **shadow-stack** lowering for the guard's
/// lifetime — Perry's heap-backed shadow frame, `js_shadow_frame_enter` +
/// per-slot binds.
pub fn shadow() -> Self {
NativeRootsPin(NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(false))))
}

/// Pin this thread to the **native-roots** (RS4GC statepoint) lowering:
/// `ptr addrspace(1)` root allocas, `gc "statepoint-example"`, relocations
/// inserted by LLVM.
///
/// This is today's default on every target the runtime can walk, so a test
/// that wants it does not strictly *need* the pin — but a pin is not
/// redundant: it also overrides `PERRY_RS4GC` from the environment, so the
/// assertion means the same thing during a `PERRY_RS4GC=0` bisection run as
/// it does in CI. Without it, a whole-suite sweep under the process-global
/// env knob silently retargets every unpinned test at the other lowering.
pub fn native() -> Self {
NativeRootsPin(NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(true))))
}
}

#[cfg(test)]
#[cfg(any(test, feature = "testing"))]
impl Drop for NativeRootsPin {
fn drop(&mut self) {
NATIVE_ROOTS_OVERRIDE.with(|c| c.set(self.0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'struct NativeRootsPin|impl (Send|Sync) for NativeRootsPin|NATIVE_ROOTS_OVERRIDE|thread::spawn' \
  crates/perry-codegen/src crates/perry-codegen/tests

Repository: PerryTS/perry

Length of output: 5148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re

p = pathlib.Path("crates/perry-codegen/src/codegen/helpers.rs")
text = p.read_text()

struct = re.search(r'#[^\n]*\npub struct NativeRootsPin\(([^)]+)\)', text, re.M)
drop  = re.search(r'impl Drop for NativeRootsPin\s*\{[^}]+fn drop\(&mut self\)\s*\{([^}]+)\}\s*\}', text, re.S)

print("struct:", struct.group(0) if struct else "NOT FOUND")
fields = struct.group(1) if struct else None
print("field_type:", fields)
print("drop_body:", drop.group(1).strip() if drop else "NOT FOUND")
print("mentions_NATIVE_ROOTS_OVERRIDE_in_drop:", "NATIVE_ROOTS_OVERRIDE" in (drop.group(1).strip() if drop else ""))
print("has_phantom_Rc :", "PhantomData<&'static (); PhantomData<std" in text or "PhantomData<std::rc::Rc" in text)

# Deterministic Rust marker model: a struct is Send/Sync iff all fields are Send/Sync, etc.
python3 - <<'PY'
fields = ("Option<bool>",)
sentinel = ("Rc<()>",)
def has_send(t):
    return t in ("Option<bool>",)
def has_sync(t):
    return t in ("Option<bool>",)
print("NativeRootsPin currently Send/Sync?", all(has_send(fields[0])), all(has_sync(fields[0])))
print("NativeRootsPin with PhantomData<Rc<()>> Send/Sync?", all(f in sentinel or f in ("Option<bool>",) for f in fields))
PY
PY

Repository: PerryTS/perry

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re

p = pathlib.Path("crates/perry-codegen/src/codegen/helpers.rs")
text = p.read_text()

struct = re.search(r'#[^\n]*\npub struct NativeRootsPin\(([^)]+)\)', text, re.M)
drop = re.search(r'impl Drop for NativeRootsPin\s*\{[^}]+fn drop\(&mut self\)\s*\{([^}]+)\}\s*\}', text, re.S)

print("struct:", struct.group(0) if struct else "NOT FOUND")
fields = struct.group(1) if struct else None
print("field_type:", fields)
print("drop_body:", drop.group(1).strip() if drop else "NOT FOUND")
if drop:
    print("has_NATIVE_ROOTS_OVERRIDE_in_drop:", "NATIVE_ROOTS_OVERRIDE" in drop.group(1).strip())
print("has_phantom_Rc :", "PhantomData<std::rc::Rc" in text)

print("NativeRootsPin currently Send/Sync?", (fields and fields.strip().startswith("Option<bool>")))
PY

Repository: PerryTS/perry

Length of output: 425


Make NativeRootsPin thread-affine.

NativeRootsPin(Option<bool>) is Send, so a caller can move the guard to another thread. Drop restores NATIVE_ROOTS_OVERRIDE on that thread instead, while the original thread remains pinned and can use the wrong lowering.

Proposed fix
-pub struct NativeRootsPin(Option<bool>);
+pub struct NativeRootsPin(
+    Option<bool>,
+    std::marker::PhantomData<std::rc::Rc<()>>,
+);
@@
-        NativeRootsPin(NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(false))))
+        NativeRootsPin(
+            NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(false))),
+            std::marker::PhantomData,
+        )
@@
-        NativeRootsPin(NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(true))))
+        NativeRootsPin(
+            NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(true))),
+            std::marker::PhantomData,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[cfg(any(test, feature = "testing"))]
pub struct NativeRootsPin(Option<bool>);
#[cfg(test)]
#[cfg(any(test, feature = "testing"))]
impl NativeRootsPin {
/// Pin this thread to the shadow-stack lowering for the guard's lifetime.
pub(crate) fn shadow() -> Self {
/// Pin this thread to the **shadow-stack** lowering for the guard's
/// lifetime — Perry's heap-backed shadow frame, `js_shadow_frame_enter` +
/// per-slot binds.
pub fn shadow() -> Self {
NativeRootsPin(NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(false))))
}
/// Pin this thread to the **native-roots** (RS4GC statepoint) lowering:
/// `ptr addrspace(1)` root allocas, `gc "statepoint-example"`, relocations
/// inserted by LLVM.
///
/// This is today's default on every target the runtime can walk, so a test
/// that wants it does not strictly *need* the pin — but a pin is not
/// redundant: it also overrides `PERRY_RS4GC` from the environment, so the
/// assertion means the same thing during a `PERRY_RS4GC=0` bisection run as
/// it does in CI. Without it, a whole-suite sweep under the process-global
/// env knob silently retargets every unpinned test at the other lowering.
pub fn native() -> Self {
NativeRootsPin(NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(true))))
}
}
#[cfg(test)]
#[cfg(any(test, feature = "testing"))]
impl Drop for NativeRootsPin {
fn drop(&mut self) {
NATIVE_ROOTS_OVERRIDE.with(|c| c.set(self.0));
#[cfg(any(test, feature = "testing"))]
pub struct NativeRootsPin(
Option<bool>,
std::marker::PhantomData<std::rc::Rc<()>>,
);
#[cfg(any(test, feature = "testing"))]
impl NativeRootsPin {
/// Pin this thread to the **shadow-stack** lowering for the guard's
/// lifetime — Perry's heap-backed shadow frame, `js_shadow_frame_enter` +
/// per-slot binds.
pub fn shadow() -> Self {
NativeRootsPin(
NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(false))),
std::marker::PhantomData,
)
}
/// Pin this thread to the **native-roots** (RS4GC statepoint) lowering:
/// `ptr addrspace(1)` root allocas, `gc "statepoint-example"`, relocations
/// inserted by LLVM.
///
/// This is today's default on every target the runtime can walk, so a test
/// that wants it does not strictly *need* the pin — but a pin is not
/// redundant: it also overrides `PERRY_RS4GC` from the environment, so the
/// assertion means the same thing during a `PERRY_RS4GC=0` bisection run as
/// it does in CI. Without it, a whole-suite sweep under the process-global
/// env knob silently retargets every unpinned test at the other lowering.
pub fn native() -> Self {
NativeRootsPin(
NATIVE_ROOTS_OVERRIDE.with(|c| c.replace(Some(true))),
std::marker::PhantomData,
)
}
}
#[cfg(any(test, feature = "testing"))]
impl Drop for NativeRootsPin {
fn drop(&mut self) {
NATIVE_ROOTS_OVERRIDE.with(|c| c.set(self.0));
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/codegen/helpers.rs` around lines 173 - 203, Make
NativeRootsPin non-Send so it cannot be moved across threads and dropped on a
different thread. Update the NativeRootsPin definition using an existing
thread-affinity marker pattern, while preserving its shadow(), native(), and
Drop behavior on the creating thread.

Comment on lines +109 to +129
// Only dependency-table lines matter, and only ones naming this crate.
let names_this_crate =
line.contains("perry-codegen") || section.iter().any(|s| s == "perry-codegen");
if !names_this_crate {
continue;
}
// `features = [..., "testing", ...]` — quoted, so a `testing = []`
// feature DEFINITION in `[features]` is not mistaken for an edge.
if !line.contains("\"testing\"") {
continue;
}
if is_production_dependency_section(&section) {
offenders.push(format!(
"{label}:{}: [{}] enables perry-codegen's `testing` feature: {}",
lineno + 1,
section.join("."),
line.trim()
));
} else if section.iter().any(|seg| seg == "dev-dependencies") {
saw_dev_edge = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve Cargo dependency aliases before accepting this gate.

Line 111 does not identify a renamed dependency table. Cargo accepts [dependencies.codegen], package = "perry-codegen", and features = ["testing"]. The feature line contains neither perry-codegen nor a perry-codegen section segment, so this production edge passes the gate.

Parse dependency tables with TOML semantics, or track each table’s package field before processing its features field. Add a planted renamed-dependency case to this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/codegen/testing_feature_gate_tests.rs` around lines
109 - 129, Update the dependency scanning logic around names_this_crate and the
section-processing flow to resolve Cargo dependency aliases via each table’s
package field before accepting the testing-feature gate. Recognize tables such
as dependencies.codegen with package = "perry-codegen", preserve existing
direct-name handling, and add a planted renamed-dependency test case covering
features = ["testing"].

Ralph Küpper added 4 commits August 6, 2026 11:30
…es (#7493)

#7370 made native roots (RS4GC statepoints) the default lowering.
`NativeRootsPin` was added so a test asserting on shadow-stack IR could say
so, the in-crate unit tests were repaired with it — and the five integration
suites that assert the same mechanics had no pin to reach for, because
`NativeRootsPin` is `#[cfg(test)]` and `tests/*.rs` link this crate as an
external consumer. They run nightly/at-tag only, so nothing went red at merge
time and `shadow_slot_hygiene` sat at 0/12 on `main`.

The pin is now reachable as `perry_codegen::testing::NativeRootsPin`, behind a
`testing` cargo feature that only this crate's own `[dev-dependencies]` entry
enables. With the feature off the pin, its thread-local and the branch it adds
to `rs4gc_enabled()` are `#[cfg]`-ed out of the artifact — not merely private,
absent — and cargo builds dev-dependencies for test/bench targets only, so no
production profile can reach it. `NativeRootsPin::native()` joins `shadow()`,
because a pin also outranks `PERRY_RS4GC` and that is what keeps an assertion
meaning the same thing during a `PERRY_RS4GC=0` sweep.

Per-test classification (not per file — the files disagree internally):

* `shadow_slot_hygiene` — 12/12 shadow. The file's subject IS the shadow
  frame; 0/12 -> 11/12.
* `scalar_replaced_slot_roots` — 11/11 shadow (every test counts
  `js_shadow_slot_bind` sites). 2/11 -> 5/11.
* `temp_root_operand_temporaries` — 2 shadow, the rest unpinned and
  lowering-independent. 12/19 -> 13/19.
* `temp_root_argument_temporaries` — none. `PERRY_RS4GC=0` moves it not at
  all; its failures are #7487's, not #7370's.
* `native_proof_regressions` — 2 shadow, 15 native in `invalidation`.
  249/253 -> 253/255 single-threaded, 198/253 -> 253/255 in parallel.
* `native_proof_buffer_views` — 1 native. 28/30 -> 30/32.

Two tests were pinned though they were PASSING:
`numeric_only_scalar_replaced_{object,array}_emits_no_rooting` and
`a_collection_free_construction_emits_no_this_slot_root` assert
`bind_calls(&ir) == 0` / `!contains("@js_shadow_slot_bind")`, which under the
native default is true of every program. They were green without their subject
running (CLAUDE.md hazard 4). Pinned, the first two now fail for a real reason
(#7497).

Also fixed here because it hid this suite's real signal: `native_proof_
regressions` reported 55 failures under default parallelism and 4 under
`--test-threads=1`. 51 of the 55 were `PoisonError` — #7490's shape again. The
`PERRY_NATIVE_REPS*` env vars are process-global and the restore was
hand-written after the compile, so a panic inside `compile_module` left them
installed and every later unlocked compile wrote artifact JSON into a directory
another test was reading; the torn read panicked inside the lock and poisoned
it. The harness is now one shared `tests/native_proof_support/mod.rs`: a
poison-tolerant accessor, an RAII env guard, and an artifact reader that treats
a foreign or half-written neighbour as noise. Two sabotage tests plant each
failure shape and assert the fix is what prevents it.

Finally, a tripwire in `src/` — so it runs in the REQUIRED `cargo-test` job,
which is the tier this whole issue is about not being in:
`host_target_lowering_default_is_native_roots` fails the moment the default
flips again, naming the suites that then need re-pinning. It asserts its subject
is live (both pins must give different answers; the unsupported-target arm must
give the opposite default), so a constant-folded `rs4gc_enabled()` fails it
rather than passing it. A second gate scans every workspace manifest and fails
if a non-dev dependency edge ever enables the `testing` feature.

Refs #7493.
@proggeramlug
proggeramlug force-pushed the fix/7493-integration-suites-native-roots branch from 10d8f3d to 13fd833 Compare August 6, 2026 09:30
@proggeramlug
proggeramlug merged commit e4ab722 into main Aug 6, 2026
7 of 11 checks passed
@proggeramlug
proggeramlug deleted the fix/7493-integration-suites-native-roots branch August 6, 2026 09:30
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integration suites assert the shadow-stack lowering but NativeRootsPin is #[cfg(test)] — shadow_slot_hygiene 0/12 red since #7370

1 participant