Skip to content

gc: delete the explicit statepoint bridge — one native-root backend, not two - #7348

Merged
proggeramlug merged 2 commits into
mainfrom
chore/delete-statepoint-bridge
Aug 4, 2026
Merged

gc: delete the explicit statepoint bridge — one native-root backend, not two#7348
proggeramlug merged 2 commits into
mainfrom
chore/delete-statepoint-bridge

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Perry carried two statepoint backends. This deletes one, leaving a single native-root path.

  • Explicit bridge — Perry rewrote its own emitted IR text into gc.statepoint calls with hand-emitted relocations.
  • RS4GC — Perry retypes root allocas to ptr addrspace(1), tags the function gc "statepoint-example", and lets LLVM's RewriteStatepointsForGC insert every statepoint, relocation and downstream rewrite.

They were never peers

RS4GC does strictly more. The bridge cannot root an invoke, so since #7330 it refused try-carrying functions outright, and CI had to skip 09_try_catch_roots on that arm. Keeping a mode that cannot compile what its sibling compiles — along with its textual emitter, its call parser, and its knob — is the permanent hybrid this project keeps paying for.

Measured before removing, because it was also the fallback

The bridge wasn't only a peer: a bail in the RS4GC recognizer silently downgraded the whole function to it. Deleting it without checking would have converted a silent downgrade into a hard failure on real code.

So I measured the fallback rate using the per-function backend the report already records:

corpus functions rs4gc fell back to bridge
test-drizzle-pg 1,543 1,543 0
gc-ratchet probes 31 31 0

1,574 functions, zero fallbacks. A fallback nothing takes is an untested configuration — precisely what the GC knob kill-policy exists to prevent. A bail is now a hard failure naming the function, not a silent downgrade.

This is the same standard that made deleting the plain stack map safe (23,301 safepoints, 0 plain maps).

What went with it

Only the bridge used these:

  • the CFG-based root-liveness analysis (~140 lines) — RS4GC gets liveness from LLVM's SSA form
  • the direct-call parser, DirectCall, and the statepoint emitter
  • the PreciseRootBackend enum — there is one backend, so there is no enum
  • the PERRY_STATEPOINTS knob; PERRY_RS4GC=1 is the single switch and native_stack_roots_enabled() is now just rs4gc_enabled()

One fewer GC knob is one less kill-policy debt. Removed from both cache keys (PERRY_RS4GC remains keyed in each, so no vacuous-A/B hazard), CI, and docs.

Net −1,216 lines. The default shadow-stack path is untouched.

Verification

  • 10/10 gc-ratchet probes byte-match the pinned Node oracle on the sole backend, under PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_STACKMAP_WALKER=verify
  • 10/10 on the default (shadow-stack) arm — confirming the deletion didn't touch what ships
  • test-drizzle-pg still builds under the sole backend
  • 593 codegen unit tests pass

Note on #7344

#7344 fixes the bridge job's toolchain and adds an ELF arm. Its bridge half is moot now — that job is deleted here. The ELF arm is still wanted, repointed at RS4GC; I'd suggest closing #7344 and letting me re-land the ELF arm on top of this, rather than merging a fix for a job that no longer exists.

Summary by CodeRabbit

  • Breaking Changes

    • Removed the legacy explicit statepoint bridge and PERRY_STATEPOINTS configuration.
    • RS4GC is now the sole native-root backend; unsupported cases fail instead of falling back.
    • Updated --statepoint-report usage to require PERRY_RS4GC=1.
    • Changes to the removed configuration no longer affect build or object caching.
  • Documentation

    • Updated CLI help and documentation for RS4GC-only behavior.
    • Retained the default shadow-stack path unchanged.

Perry carried two statepoint backends. The explicit bridge rewrote Perry's own
IR text into gc.statepoint calls with hand-emitted relocations; RS4GC retypes
root allocas and lets LLVM's RewriteStatepointsForGC insert every statepoint
and relocation itself.

They were never peers. RS4GC does strictly more: the bridge cannot root an
invoke, so since #7330 it refused try-carrying functions and CI skipped
09_try_catch_roots on that arm. A mode that cannot compile what its sibling
compiles, kept beside it with its own emitter, parser and knob, is the
permanent hybrid this project keeps paying for.

The bridge was also RS4GC's fallback — a bail in the recognizer silently
downgraded the function to it. Measured first: 1,574 functions across
test-drizzle-pg (1,543) and the probes (31) all lowered as rs4gc, none fell
back. A fallback nothing takes is an untested configuration, so a bail is now a
hard failure naming the function rather than a silent downgrade.

Deleted with it, because only the bridge used them: the CFG root-liveness
analysis (RS4GC gets liveness from LLVM's SSA form), the direct-call parser and
statepoint emitter, PreciseRootBackend, and the PERRY_STATEPOINTS knob.
PERRY_RS4GC=1 is the single switch; native_stack_roots_enabled() is now just
rs4gc_enabled(). One fewer GC knob is one less kill-policy debt.

Net -1,216 lines. The default shadow-stack path is untouched.

Verified: 10/10 probes byte-match the oracle on the sole backend under forced
evacuation with the verifying walker, 10/10 on the default arm, drizzle still
builds, 593 codegen unit tests pass.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR removes the explicit statepoint bridge and PERRY_STATEPOINTS. RS4GC becomes the only native-root backend. Codegen, cache inputs, CLI documentation, changelog text, and native-root CI are updated.

Changes

RS4GC-only native-root pipeline

Layer / File(s) Summary
Remove explicit backend lowering
crates/perry-codegen/src/codegen/helpers.rs, crates/perry-codegen/src/function.rs, crates/perry-codegen/src/function/precise_roots.rs
Backend selection, statepoint lowering, fallback recursion, and PERRY_STATEPOINTS handling are removed. Precise-root lowering uses RS4GC and reports "rs4gc".
Align native-root code generation
crates/perry-codegen/src/linker.rs, crates/perry-codegen/src/module.rs
GC-map compaction and LLVM statepoint declarations use native stack-root enablement.
Remove statepoint configuration inputs
crates/perry/src/commands/compile/object_cache.rs, crates/perry/src/commands/compile/types.rs, docs/src/cli/flags.md, changelog.d/7345-delete-statepoint-bridge.md
Object-cache keys no longer include PERRY_STATEPOINTS. CLI text requires PERRY_RS4GC=1, and the changelog documents the removed bridge.
Update native-root CI
.github/workflows/gc-native-roots.yml
The x86 refusal probe uses PERRY_RS4GC=1. The fan-in job no longer declares the AArch64 job dependency.

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

Possibly related PRs

  • PerryTS/perry#7292: Directly changes the statepoint and RS4GC codegen layer removed by this PR.
  • PerryTS/perry#7314: Introduces related LLVM statepoint and RS4GC machinery consolidated by this PR.
  • PerryTS/perry#7330: Modifies the precise-root bridge and invoke handling removed here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes removal of the explicit statepoint bridge and the resulting single native-root backend.
Description check ✅ Passed The description clearly covers the motivation, concrete changes, related issue context, and detailed verification results, although it does not use all template headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 chore/delete-statepoint-bridge

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 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/gc-native-roots.yml (1)

293-296: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

The fan-in loop still reads the deleted native-roots-aarch64 job, so the gate fails on every run.

Line 284 removed native-roots-aarch64 from needs, and the job itself is deleted. Line 294 still expands ${{ needs.native-roots-aarch64.result }}. GitHub Actions resolves an unknown needs entry to an empty context, so that expression renders as an empty string and the loop entry becomes native-roots-aarch64=. That string does not match *=success), so failed=1 and the job exits 1.

gc-native-roots-complete is the single fan-in context for branch protection. It will now report failure on every run, including on main, and block merges regardless of whether the real arms passed. Remove the stale entry.

🐛 Proposed fix
           for arm in \
-            "native-roots-aarch64=${{ needs.native-roots-aarch64.result }}" \
             "native-roots-rs4gc-aarch64=${{ needs.native-roots-rs4gc-aarch64.result }}" \
             "statepoints-refuse-x86=${{ needs.statepoints-refuse-x86.result }}"; do

As per coding guidelines for .github/workflows/*.{yml,yaml}: a CI gate "must be included in required branch-protection contexts, must avoid unconditional cancellation of main runs, and must assert that the behavior it measures actually executed." A permanently failing fan-in context defeats that gate.

🤖 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 @.github/workflows/gc-native-roots.yml around lines 293 - 296, Remove the
stale "native-roots-aarch64=${{ needs.native-roots-aarch64.result }}" entry from
the fan-in loop in gc-native-roots-complete, leaving only currently defined
needs contexts such as native-roots-rs4gc-aarch64 and statepoints-refuse-x86 so
the gate evaluates their actual results.

Source: Coding guidelines

🧹 Nitpick comments (4)
docs/src/cli/flags.md (1)

106-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the --statepoint-report contract matching its current backend.

docs/src/cli/flags.md makes --statepoint-report a PERRY_RS4GC=1 requirement, but the CLI help in crates/perry/src/commands/compile/types.rs only says it is useful with that variable. Also keep plain-stack-map fallbacks terminology consistent: the report supports both plain stack maps and statepoint parser fallbacks, not only user-selectable plain stack-map mode.

🤖 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 `@docs/src/cli/flags.md` at line 106, Update the --statepoint-report entry in
docs/src/cli/flags.md to match the current CLI contract in Compile command
types: describe PERRY_RS4GC=1 as recommended/useful rather than required, and
revise the fallback wording to distinguish plain stack maps from statepoint
parser fallbacks without implying a selectable plain stack-map mode.
crates/perry-codegen/src/function.rs (1)

749-749: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The rs4gc_enabled() conjunct is now redundant.

native_stack_roots_enabled() delegates to rs4gc_enabled(). gc_strategy at Line 686 is already non-empty only when native_stack_roots_enabled() is true. The second conjunct can therefore never change the result. The comment above already states this condition is "the same fact as gc_strategy above", so drop the extra call to keep one source of truth.

♻️ Proposed simplification
-        let ir = if !gc_strategy.is_empty() && crate::codegen::helpers::rs4gc_enabled() {
+        let ir = if !gc_strategy.is_empty() {
             retype_landing_pads_for_statepoints(&ir)
         } else {
             ir
         };
🤖 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/function.rs` at line 749, Remove the redundant
crate::codegen::helpers::rs4gc_enabled() conjunct from the IR selection
condition, leaving gc_strategy.is_empty() as the sole check. Keep the
surrounding logic and the existing gc_strategy source of truth unchanged.
crates/perry-codegen/src/module.rs (1)

651-653: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated native_stack_roots_enabled() condition in both IR renderers. Removing PERRY_STATEPOINTS collapsed the second condition into the first, so each renderer now has two consecutive if blocks testing the same predicate. Merge each pair so the two paths stay easy to keep in lockstep.

  • crates/perry-codegen/src/module.rs#L651-L653: fold the push_statepoint_declarations(&mut ir) call into the if block that starts at Line 648 in to_ir.
  • crates/perry-codegen/src/module.rs#L923-L925: fold the push_statepoint_declarations(&mut pre) call into the if block that starts at Line 920 in codegen_unit_parts.
🤖 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/module.rs` around lines 651 - 653, Merge the
consecutive native_stack_roots_enabled() checks in to_ir at
crates/perry-codegen/src/module.rs:648-653 by moving
push_statepoint_declarations(&mut ir) into the first block. Apply the same
consolidation in codegen_unit_parts at
crates/perry-codegen/src/module.rs:920-925, placing
push_statepoint_declarations(&mut pre) in the existing predicate block.
crates/perry-codegen/src/function/precise_roots.rs (1)

240-247: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Return a compile error instead of panic! for the recognizer bail.

Failing hard is correct here. Emitting a plain stack map would leave GC-managed values in ordinary allocas with no relocation, which the collector cannot follow. The concern is only the failure mechanism.

panic! inside perry-codegen surfaces to the user as a Rust panic with a backtrace, not as a compiler diagnostic. It also unwinds through to_ir(), which runs per function and may run on worker threads during codegen-unit rendering; a panic there aborts or poisons that work rather than reporting one named function cleanly. Prefer the crate's existing compile-error path so the message reaches the user as a diagnostic. If to_ir() cannot return a Result without a wide signature change, an explicit eprintln! of this message followed by std::process::exit(1) is still a more predictable failure than an unwind.

Keep the message text as written. It names the function and the slot count, which is what a bug report needs.

As per coding guidelines for crates/perry-codegen/**/*.rs: "Ensure generated GC root stores dominate every later site that may collect, and do not leave GC-managed values only in ordinary allocas." — the hard failure is the right policy; only the panic mechanism needs changing.

🤖 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/function/precise_roots.rs` around lines 240 - 247,
Replace the panic in the None branch of the native-root recognizer with the
crate’s existing compile-error diagnostic path, preserving the exact message
text and function_name/root_ptrs.len() details. If to_ir() cannot propagate a
Result without a broad signature change, emit the same message with eprintln!
and terminate explicitly instead of unwinding through codegen.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In @.github/workflows/gc-native-roots.yml:
- Around line 293-296: Remove the stale "native-roots-aarch64=${{
needs.native-roots-aarch64.result }}" entry from the fan-in loop in
gc-native-roots-complete, leaving only currently defined needs contexts such as
native-roots-rs4gc-aarch64 and statepoints-refuse-x86 so the gate evaluates
their actual results.

---

Nitpick comments:
In `@crates/perry-codegen/src/function.rs`:
- Line 749: Remove the redundant crate::codegen::helpers::rs4gc_enabled()
conjunct from the IR selection condition, leaving gc_strategy.is_empty() as the
sole check. Keep the surrounding logic and the existing gc_strategy source of
truth unchanged.

In `@crates/perry-codegen/src/function/precise_roots.rs`:
- Around line 240-247: Replace the panic in the None branch of the native-root
recognizer with the crate’s existing compile-error diagnostic path, preserving
the exact message text and function_name/root_ptrs.len() details. If to_ir()
cannot propagate a Result without a broad signature change, emit the same
message with eprintln! and terminate explicitly instead of unwinding through
codegen.

In `@crates/perry-codegen/src/module.rs`:
- Around line 651-653: Merge the consecutive native_stack_roots_enabled() checks
in to_ir at crates/perry-codegen/src/module.rs:648-653 by moving
push_statepoint_declarations(&mut ir) into the first block. Apply the same
consolidation in codegen_unit_parts at
crates/perry-codegen/src/module.rs:920-925, placing
push_statepoint_declarations(&mut pre) in the existing predicate block.

In `@docs/src/cli/flags.md`:
- Line 106: Update the --statepoint-report entry in docs/src/cli/flags.md to
match the current CLI contract in Compile command types: describe PERRY_RS4GC=1
as recommended/useful rather than required, and revise the fallback wording to
distinguish plain stack maps from statepoint parser fallbacks without implying a
selectable plain stack-map mode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f5e6cf0-b970-40e6-8e17-4828330b2d40

📥 Commits

Reviewing files that changed from the base of the PR and between 494083f and ad7c9bd.

📒 Files selected for processing (11)
  • .github/workflows/gc-native-roots.yml
  • changelog.d/7345-delete-statepoint-bridge.md
  • crates/perry-codegen/src/codegen/helpers.rs
  • crates/perry-codegen/src/function.rs
  • crates/perry-codegen/src/function/precise_roots.rs
  • crates/perry-codegen/src/linker.rs
  • crates/perry-codegen/src/module.rs
  • crates/perry/src/commands/compile/build_cache.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/types.rs
  • docs/src/cli/flags.md
💤 Files with no reviewable changes (2)
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/build_cache.rs

…nt-bridge

# Conflicts:
#	crates/perry-codegen/src/function/precise_roots.rs
@proggeramlug
proggeramlug merged commit 54c0283 into main Aug 4, 2026
10 of 14 checks passed
@proggeramlug
proggeramlug deleted the chore/delete-statepoint-bridge branch August 4, 2026 06:36

@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.

🧹 Nitpick comments (1)
crates/perry-codegen/src/function/precise_roots.rs (1)

146-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for call-effect classification.

Line [146] makes CannotCollect the only unconditional gc-leaf-function case. Add tests for CannotCollect, AllocNoReentry with both contract states, and Unknown. This protects the RS4GC safepoint boundary from future changes.

🤖 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/function/precise_roots.rs` around lines 146 - 150,
Add regression tests around the call-effect classification logic in
precise-roots code generation, covering CannotCollect, AllocNoReentry when
gc_safepoint_only_contract_enabled is both enabled and disabled, and Unknown.
Assert that only CannotCollect is unconditionally treated as a gc-leaf-function
case and that AllocNoReentry follows the contract state, preserving the RS4GC
safepoint boundary.
🤖 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.

Nitpick comments:
In `@crates/perry-codegen/src/function/precise_roots.rs`:
- Around line 146-150: Add regression tests around the call-effect
classification logic in precise-roots code generation, covering CannotCollect,
AllocNoReentry when gc_safepoint_only_contract_enabled is both enabled and
disabled, and Unknown. Assert that only CannotCollect is unconditionally treated
as a gc-leaf-function case and that AllocNoReentry follows the contract state,
preserving the RS4GC safepoint boundary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dd0f5da7-0822-413c-b521-145209d42646

📥 Commits

Reviewing files that changed from the base of the PR and between ad7c9bd and d78cf97.

📒 Files selected for processing (1)
  • crates/perry-codegen/src/function/precise_roots.rs

proggeramlug added a commit that referenced this pull request Aug 4, 2026
…ly linked (#7353)

Perry now links LLVM 22 statically and ships self-contained. We own the
assumption rather than pushing it onto the user, and there is no "install
a compatible clang" step left to get wrong.

It is load-bearing, not a preference. The explicit statepoint bridge is
gone (#7348), so RS4GC is the only native-root backend, and RS4GC cannot
round-trip its IR through an external `opt` plus a different clang
(#7339). Keeping this opt-in meant the only working statepoint path was
behind a flag nobody sets.

Two defaults flip together, because either alone is half a feature:

  * `llvm-inprocess` becomes a default cargo feature.
  * `inprocess_requested()` defaults to ON -- but only iff the backend is
    actually compiled in. Defaulting to `true` unconditionally would route
    every compile in a `--no-default-features` build into the
    not-built-in stub and fail it outright. Verified both ways.

`PERRY_LLVM_INPROCESS=0` reverts to the clang subprocess for bisection,
and `--no-default-features` still builds the text path.

CI: a new `.github/actions/setup-llvm22` composite action, referenced from
all 44 toolchain steps across 18 workflows. One definition rather than 44
inline recipes, because the three platforms need three different sources
and only one is obvious -- Ubuntu 24.04's own llvm-dev is 18, and
chocolatey's `llvm` is the clang toolchain with no llvm-config.exe and
none of the static libs. Every arm asserts the major version.

Size: 98.9 MB, not the 185.9 MB this would have cost before #7350 --
`initialize_all()` was linking ~18 backends nothing can reach.

Also fixed, surfaced by the flip: PERRY_LLVM_KEEP_IR promises the whole
scratch dir including the .o. The clang path got that free because the
object is a file; in-process returns bytes and silently dropped it,
degrading a debugging aid exactly when someone is debugging.

Verified on the 81-module zod corpus with no env set: compiles, output
byte-identical to the clang path, and PERRY_RS4GC=1 now compiles a
try-carrying probe with no further flags. 605 codegen tests pass.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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.

1 participant