fix(gc): route RS4GC through the in-process LLVM backend - #7339
Conversation
PERRY_RS4GC=1 died with `unterminated attribute group` on any stock toolchain: the pass ran under an external LLVM 22 `opt`, and Apple clang 21 then could not parse the IR it produced. That made RS4GC reachable only with PERRY_LLVM_CLANG pointed at a version-matched LLVM 22 -- and RS4GC is the only backend that can root an `invoke`, which since #7302 is every call inside a `try`. Run the pass in-process instead, where LLVM 22 is already pinned and no IR ever crosses a toolchain boundary. Two gaps had to be closed: - inprocess.rs ignored `-S`, so the statepoint backends' request for assembly silently produced an object. #7314's compact-map rewriter works on assembly text, so it needs the real thing. - the returned assembly was written straight to a `.o` with nothing assembling it (`ld: unknown file type`). Mirror the external path: write it to plan.asm_path, run compact_and_assemble, return the object. The assembler is resolved via find_clang() because plan.clang is the literal `(in-process)` placeholder here.
…cts for assembly requests
Every existing RS4GC step pins PERRY_LLVM_OPT and PERRY_LLVM_CLANG to one brew install, which is the requirement the in-process route removes. An arm that keeps the pinning cannot observe that. This step is the only one that unsets both, and it runs probe 09 -- try-carrying, so every call in it is an invoke, which the explicit bridge refuses (#7330). It asserts four things, each of which has been a way a GC gate went green while measuring nothing: the map section exists, the compact rewrite ran, a copying minor actually copied, and RS4GC (not a per-function bail to the bridge) did the lowering.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe in-process LLVM backend now performs RS4GC rewriting, supports assembly emission, and integrates compact GC-map assembly. CI validates try/catch roots, evacuation, and RS4GC lowering on ARM64. ChangesIn-process LLVM RS4GC pipeline
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant gc_native_roots as gc-native-roots workflow
participant inprocess_rs as inprocess.rs
participant linker_rs as linker.rs
participant clang
gc_native_roots->>inprocess_rs: build with llvm-inprocess
inprocess_rs->>inprocess_rs: run RS4GC statepoint rewriting
inprocess_rs-->>linker_rs: emit assembly
linker_rs->>linker_rs: compact GC maps
linker_rs->>clang: assemble compacted assembly
clang-->>linker_rs: return object
linker_rs-->>gc_native_roots: run evacuation and backend checks
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Full gap suite, two arms, 479/479 — zero new regressions, zero refusalsRan every Zero refusals is the number that matters here, more than zero regressions. For context: the earlier soak that returned "13 regressions, do not flip" was Scope, stated plainlyThis makes RS4GC-in-process viable as a default on aarch64. It does not make
The plain in-process path ( |
…VM's generic CI hit `LLVM ERROR: Cannot select: intrinsic %llvm.aarch64.fjcvtzs` and aborted the compile. Codegen decides whether to emit `fjcvtzs` (FEAT_JSCVT, ARMv8.3+, the single-instruction ECMAScript ToInt32) from the TRIPLE ALONE -- `set_jscvt_for_target` opts in for every Apple arm64 triple -- and that is sound for clang, whose default CPU for arm64-apple-* is apple-m1 (ARMv8.5). But `create_target_machine` with an empty CPU string selects LLVM's `generic`, which on aarch64 is ARMv8.0 and has no FEAT_JSCVT. So the two halves disagreed: one decided what to EMIT from the triple, the other what the target could EXECUTE, and only the clang path had them aligned. The in-process backend now derives the same default. Local build never hit it because a host build gets `-mcpu=native`, which takes the host-features branch. Reproduced exactly with `PERRY_TARGET_CPU=generic` (the CI path): fails before, compiles after, 9/9 probes compile, native-tuning path unchanged. Also fixes the doc-comment placement that had left `#[allow(clippy::type_complexity)]` attached to the wrong function.
…#7345) The layer table said layer 3 was 'not started' when RuntimeHandleScope has 675 uses across 169 files; what is missing is the word 'non-optional'. It now says so, and carries #7341's 54-item worklist. Sequencing step 2 ('in-process LLVM -> statepoints') is complete, so the list said 'next' about work that had already landed. Replaced with what actually comes next, and with one ordering change that is a real finding rather than bookkeeping: reducing root density is now a PREREQUISITE for statepoint adoption, not a follow-up. Statepoints cost +18.95% binary size on root-dense code and +0% on root-free code, 97% of it __text. Making them the default today would regress the stated goal of minimal binary size. The plan already named this lever but flagged it 'expected, not measured -- layer 2 must prove it first'; layer 2 has landed, so it is now measurable. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
…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>
Closes #7327.
The problem
PERRY_RS4GC=1failed on any stock toolchain with:RS4GC ran as an external
optsubprocess and handed its output toclang. On aMac that pairing is Homebrew's LLVM 22
optfeeding Apple's clang 21, and thenewer
optemits attributes the olderclangcannot parse. So RS4GC wasreachable only with
PERRY_LLVM_CLANGpointed at a version-matched LLVM 22 —which the existing CI arm does, and which no user does.
That mattered more than a knob normally would, because RS4GC is the only
backend that can root an
invoke, and since #7302 every call inside atryisan invoke. The explicit bridge refuses them outright (#7330). 128 of 479 gap
tests (26%) contain
try {}. So there was no working statepoint path for aquarter of the suite on a default toolchain.
The fix
Run the pass in-process, where #7301 already pins LLVM 22 and no IR crosses a
toolchain boundary. The pass itself was already known to schedule there —
rs4gc_schedules_in_processhas been asserting it. Two gaps had to be closed toget from "schedules" to "produces a linkable object":
inprocess.rsignored-S. It fell into the catch-all that discards-c, so the statepoint backends asked for assembly and were handed anobject. Native-frame GC roots via LLVM statepoints, opt-in (#7173, #7174) #7314's compact-map rewriter rewrites
.llvm_stackmapsin assemblytext — that is where LLVM prints function addresses as symbol names — so it
needs the real thing.
Nothing assembled the result. The returned assembly went straight into a
.oand the link died withld: unknown file type. This now mirrors theexternal path: write to
plan.asm_path, runcompact_and_assemble, returnthe object. The assembler is resolved via
find_clang()becauseplan.clangis the literal
(in-process)placeholder on this path — and using the systemclang for it is sound, since the skew was an IR parse failure and by this
point the IR is gone.
Result
All 9 gc-ratchet probes compile under
PERRY_RS4GC=1 PERRY_LLVM_INPROCESS=1with no
PERRY_LLVM_*pinning, including probe 09, which the bridge cannotcompile at all:
__perry_gcmap9/9 byte-identical to the shadow-stack control, every one under
PERRY_CONSERVATIVE_STACK_SCAN=offso the native map is doing the rooting.--statepoint-reportreportsbackend rs4gc: 9 function(s)on probe 09 — noper-function bail to the bridge.
Gating
Existing RS4GC steps all pin
PERRY_LLVM_OPT+PERRY_LLVM_CLANG, so none ofthem can observe that the pinning is no longer needed. The new step is the only
arm that unsets both. It asserts four things, each of which is a way a GC gate
in this repo has previously gone green while measuring nothing:
__perry_gcmapexists (the map was emitted)__llvm_stackmapsis gone (the compact rewrite ran)gc_evacuation_liveness_assert.py(gc: the native-roots forced-evacuation arm is vacuous — nothing moves, and 5 of 8 probes match zero stack-map records #7336/ci(gc): make the forced-evacuation arm actually evacuate, and assert it (#7336) #7338 — the arm that justified statepoints was reporting
copied_objects = 0on all 8 probes)--only-backend rs4gc(the subject was RS4GC, not a silent bridge fallback)Job timeout goes 90 → 120 because the step builds a second time with the
llvm-inprocessfeature, which cargo cannot share with the build above it.No silent-fallback hole
The concern worth stating, since it is the #7332 shape: if the in-process
compile failed and fell back to external clang after
maybe_rs4gc_preprocesshad already skipped the external
opt, the result would be a binary with nostatepoints at all — correct-looking until a collection freed something live.
Checked: both the failure branch and the missing-feature stub
bail!, so thereis no path from "asked for in-process" to "served the text path".
Tests
dash_s_requests_assembly_and_dash_c_does_not— the parse gap directly.Verified it fails when
"-S" => emit_asm = trueis reverted to"-S" => {}.assembly_emission_is_text_not_an_object— asserts the twoFileTypes do notreturn identical bytes, so a future regression cannot silently re-swallow
-S.Scope
This does not change any default.
llvm-inprocessremains a non-default cargofeature, so
PERRY_RS4GC=1on a stock release build still takes the externalpath and still fails loudly there. Making RS4GC a default is a separate decision
that depends on #7301's feature becoming default, and on #7333 (the x86-64
walker) for non-aarch64 hosts.
Summary by CodeRabbit
New Features
invokeroots and compact GC-map generation work without external LLVM tool incompatibilities.Bug Fixes
Tests