Skip to content

fix(codegen): teach the in-process native backend today's RS4GC IR; unfreeze the dialect corpora - #7998

Merged
proggeramlug merged 3 commits into
mainfrom
fix/7982-native-addrspace-corpora
Aug 12, 2026
Merged

fix(codegen): teach the in-process native backend today's RS4GC IR; unfreeze the dialect corpora#7998
proggeramlug merged 3 commits into
mainfrom
fix/7982-native-addrspace-corpora

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Refs #7982.

The reported defect, and the family behind it

PERRY_LLVM_INPROCESS=native could not build any function containing an RS4GC root slot — effectively every function. The issue warned this was not a one-liner, and it was not; each fix exposed the next shape:

# shape what was wrong
1 alloca ptr addrspace(1) basic_type had no addrspace(N) arm (the reported failure)
2 store ptr addrspace(1) null, ptr %r ty_and_val split it into type ptr + value addrspace(1) null. The qualifier belongs to the type but sits after a space, so operand position needed the same treatment as type position
3 null in an addrspace operand constant always returned an addrspace(0) null; the verifier rejects that stored into an alloca ptr addrspace(1)
4 define … "frame-pointer"="non-leaf" gc "statepoint-example" no string-attribute arm; and gc "…" contains a space, so the whitespace-split attribute loop saw two junk tokens
5 call … ) "gc-leaf-function" no string-attribute arm on callsites
6 landingpad token cleanup only {ptr, i32} catch was supported. token is not an inkwell BasicType, so this pad is built through llvm-sys; the older shape keeps its branch, both occur in one module today

Two defects that were hiding behind it — both worse

The native path returned assembly and called it an object. The statepoint backends compact the stack map at assembly time, so the plan carries -S. The textual in-process path has always run the rewrite-and-assemble step; the native and diff paths returned the bytes straight to the object cache and the link died with ld: unknown file type. linker.rs even carries a comment saying exactly that would happen — for the path that already handled it. Factored into linker::finish_native_emission, wired into all four emit sites.

A natively-constructed module had NO GC strategy, so RS4GC never ran on it. native_emit::synth_define_header was a second, independent copy of LlFunction::to_ir's header renderer, written before "frame-pointer"="non-leaf" and gc "statepoint-example" existed and never updated. The result verifies, links and runs correctly on any program that does not collect — while having no precise roots at all. That is #7332's shape, and it is invisible to a behaviour-parity smoke arm by construction: identical output on a non-collecting program is precisely what it produces. Its only symptom was the diff arm's byte mismatch (149,105 text vs 50,995 native), and the diff arm was never reached because the native arm failed first.

Fixed structurally rather than by testing for agreement: one renderer, LlFunction::define_header. A copy that can drift eventually does, and this one took two attributes and an unknown number of releases to notice. The pin lives in function.rs, which compiles without the llvm-inprocess feature, so it runs in per-PR cargo-test rather than only in the feature job.

The corpora — arguably the more important half

All three tracked .ll files froze on 2026-08-03, 151 codegen commits earlier, with zero addrspace(1). corpus_spike ... ok proved the tests ran, not that they test today's IR. And it had already happened once, nine days before (#7310, stale setjmp calls) — which is the argument against refreshing by hand a third time.

  1. scripts/refresh_llvm_inprocess_corpora.sh regenerates all three from the built compiler; --check diffs instead of writing.
  2. scripts/check_llvm_corpus_currency.py (added to lint) asserts every IR form the reader carries a dedicated branch for is present in the corpora. A form that disappears fails, and the fix is a refresh or deleting the now-untested branch — the kill policy applied to fixtures.
    Sabotage-verified against the corpora this PR replaces: pointed at the origin/main files it names all 10 forms they lacked, then passes on the refreshed ones. Its --self-test rejects the 2026-08-03 corpus shape and accepts today's.
    Stated limit, because it matters: this catches a form that vanishes, not one codegen newly invents — nothing static can. The honest closure for that direction is the end-to-end native arm, which compiles today's IR by construction.
  3. llvm-inprocess.yml's age diagnostic was itself vacuous. git log -- <path> on the default depth-1 checkout has one commit to answer from, so "IR-affecting commits since then" printed a confident 0 however stale the files were — a diagnostic reassuring readers about the exact thing it existed to detect. Now fetch-depth: 0, and it fails loudly rather than printing zero if history is ever missing again.

Validation

  • 934 perry-codegen lib tests green under --features llvm-inprocess (all three corpus round-trips included), LLVM 22.1.4.
  • PERRY_LLVM_INPROCESS=native compiles and runs spike.ts and test_gap_7302_invoke_eh_paths.ts with output byte-identical to the textual backend, banner asserted.
  • Corpora refreshed: 497 / 1082 / 420 addrspace(1) sites; the EH corpus keeps 84 invoke edges and its personality clause.
  • dialect/mod.rs crossed the 2000-line cap, so type/constant parsing moved to dialect/types.rs.

Not closed here (separate defects, neither ever reached in CI)

  • =diff still reports a byte mismatch on the spike: 149,105 text vs 163,902 native. It was 50,995 before the GC-strategy fix, i.e. the gap narrowed from "RS4GC never ran on the native arm" to a real but far smaller divergence. The pre-opt prints differ by design (the C-API builder constant-folds at construction), so this needs its own investigation.
  • The unit-split diff arm fails with call to undeclared @js_shadow_slot_set — the per-unit skeleton does not declare it.

Both are worth their own issues; this PR takes native from "cannot build any GC-rooted function" to "builds and runs correctly", and stops the corpora from silently rotting again.

Summary by CodeRabbit

  • New Features

    • Improved in-process LLVM support for current RS4GC IR, including address spaces, constants, attributes, callsites, and landing pads.
    • Native and differential compilation now reliably produce assembled object output when required.
    • Preserved garbage-collection strategies and personality information in generated native modules.
    • Improved handling of pointer types and token-based cleanup landing pads.
  • Bug Fixes

    • Fixed inconsistencies between native and textual LLVM output.
    • Added clearer failures for unsupported or malformed LLVM constructs.
  • Chores

    • Added automated LLVM corpus coverage and freshness checks.

…efresh the frozen corpora

`PERRY_LLVM_INPROCESS=native` could not build any function containing an
RS4GC root slot — which is effectively every function. Every `main`
execution of `llvm-inprocess`'s native-backend job failed at

    in line: %r5 = alloca ptr addrspace(1)

The issue warned this was not a one-liner, and it was not: fixing each
shape only exposed the next. The family, in the order it surfaced:

  1. `basic_type` had no `addrspace(N)` arm — the reported failure.
  2. `ty_and_val` split `ptr addrspace(1) null` into type `ptr` and value
     `addrspace(1) null`. The qualifier belongs to the type but sits after
     a space, so OPERAND position needed the same treatment as type
     position.
  3. `constant` returned an addrspace(0) `null` regardless of the operand
     type, which the verifier rejects when stored into an
     `alloca ptr addrspace(1)`.
  4. Define lines carry `"frame-pointer"="non-leaf"` (a string attribute)
     and `gc "statepoint-example"` (which contains a space, so the
     whitespace-split attribute loop saw two junk tokens).
  5. Callsites carry `"gc-leaf-function"`.
  6. Landing pads are now `landingpad token cleanup`. `token` is not an
     inkwell `BasicType`, so that pad is built through llvm-sys. The
     `{ptr, i32} catch` shape still occurs and keeps its branch.

Two further defects were hiding behind the reader failure, both worse
than it:

**The native path returned assembly and called it an object.** The
statepoint backends compact the stack map at assembly time, so the plan
carries `-S`. The textual in-process path has always run the
rewrite-and-assemble step afterwards; the native and diff paths returned
the bytes straight to the object cache, and the link died with
`ld: unknown file type`. Factored into `linker::finish_native_emission`
and wired into all four native/diff emit sites.

**A natively-constructed module had NO GC strategy, so RS4GC never ran on
it.** `native_emit::synth_define_header` was a second, independent copy of
`LlFunction::to_ir`'s header renderer, written before
`"frame-pointer"="non-leaf"` and `gc "statepoint-example"` existed and
never updated. The result verifies, links and executes correctly on any
program that does not collect, while having no precise roots at all —
#7332's shape, and invisible to a behaviour-parity smoke arm by
construction. The two callers now share `LlFunction::define_header`, so
the next attribute reaches both; `function.rs` (compiled without the
`llvm-inprocess` feature, i.e. visible to per-PR CI) pins `to_ir`'s first
line against it.

**The corpora, which is the half worth more than the fix.** All three
tracked `.ll` files froze on 2026-08-03, 151 codegen commits earlier, and
contained zero `addrspace(1)`. `corpus_spike ... ok` proved the tests ran,
not that they test today's IR — CLAUDE.md's fourth way a gate cannot fail,
inside the liveness assert written to prevent it. The same thing had
happened nine days earlier (#7310, stale setjmp calls), which is the
argument against fixing it by hand again:

  * `scripts/refresh_llvm_inprocess_corpora.sh` regenerates all three from
    the built compiler (`--check` diffs instead of writing).
  * `scripts/check_llvm_corpus_currency.py` (added to `lint`) asserts every
    IR form the reader carries a dedicated branch for is PRESENT in the
    corpora, so a form that disappears fails and must be either refreshed
    or deleted per the kill policy. Sabotage-verified against the corpora
    this commit replaces: it names all 10 forms they lacked.
  * `llvm-inprocess.yml`'s existing age diagnostic was itself vacuous —
    `git log` on a depth-1 checkout printed `0` commits behind however
    stale the files were. Now `fetch-depth: 0`, and it fails loudly rather
    than printing a reassuring zero if history is ever missing again.

Corpora refreshed: 497 / 1082 / 420 `addrspace(1)` sites, EH corpus keeps
84 invoke edges and its personality clause. `dialect/mod.rs` crossed the
2000-line cap, so type/constant parsing moved to `dialect/types.rs`.

Validated locally against LLVM 22.1.4: 934 `perry-codegen` lib tests green
(all three corpus round-trips included); `PERRY_LLVM_INPROCESS=native`
compiles and runs the spike and the try/catch program with output
byte-identical to the textual backend.

Known remaining, NOT closed here and separate defects: `=diff` still
reports a byte mismatch on the spike (149,105 text vs 163,902 native — it
was 50,995 before the GC-strategy fix, i.e. the gap narrowed from "RS4GC
never ran" to a real but much smaller divergence), and the unit-split diff
arm fails with `call to undeclared @js_shadow_slot_set`. Neither was ever
reached in CI, because the native arm failed first.

Refs #7982.
@coderabbitai

coderabbitai Bot commented Aug 12, 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: c9899f72-601e-4cb9-8e6f-c99187eca6b3

📥 Commits

Reviewing files that changed from the base of the PR and between 667b028 and 3b23693.

📒 Files selected for processing (15)
  • .github/workflows/llvm-inprocess.yml
  • .github/workflows/test.yml
  • changelog.d/7998-native-addrspace-and-corpus-currency.md
  • crates/perry-codegen/src/dialect/eh.rs
  • crates/perry-codegen/src/dialect/mod.rs
  • crates/perry-codegen/src/dialect/types.rs
  • crates/perry-codegen/src/function.rs
  • crates/perry-codegen/src/linker.rs
  • crates/perry-codegen/src/native_emit.rs
  • experiments/llvm-inprocess-spike/batch_kernel.ll
  • experiments/llvm-inprocess-spike/eh_text.ll
  • experiments/llvm-inprocess-spike/spike_text.ll
  • gc-handoff/KNOBS-NOTES.md
  • scripts/check_llvm_corpus_currency.py
  • scripts/refresh_llvm_inprocess_corpora.sh

📝 Walkthrough

Walkthrough

The PR expands LLVM IR parsing for address spaces, attributes, constants, GC metadata, and cleanup landing pads. It unifies native function headers and assembly finalization. It adds corpus refresh and currency tooling, CI checks, and related documentation.

Changes

LLVM backend updates

Layer / File(s) Summary
LLVM dialect parsing and landing pads
crates/perry-codegen/src/dialect/types.rs, crates/perry-codegen/src/dialect/mod.rs, crates/perry-codegen/src/dialect/eh.rs
The reader now parses address-space-qualified types, constants, function signatures, GC strategies, string attributes, typed memory operands, and token cleanup landing pads.
Canonical native function headers
crates/perry-codegen/src/function.rs, crates/perry-codegen/src/native_emit.rs
LlFunction::define_header centralizes function-header rendering for textual and native construction. Tests verify header consistency and linkage handling.
Native emission finalization
crates/perry-codegen/src/linker.rs, crates/perry-codegen/src/native_emit.rs
Native, split-unit, and differential paths convert assembly output into objects through finish_native_emission.
Corpus refresh and CI validation
scripts/*llvm*, .github/workflows/*, changelog.d/7998-native-addrspace-and-corpus-currency.md, gc-handoff/KNOBS-NOTES.md
The PR adds corpus regeneration and coverage checks. CI uses full Git history and runs validator self-tests and corpus validation. The changelog and handoff notes record the backend changes and validation status.

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

Sequence Diagram(s)

sequenceDiagram
  participant LLVMIR
  participant DialectReader
  participant LlFunction
  participant NativeEmitter
  participant Linker
  participant Clang
  LLVMIR->>DialectReader: parse supported IR forms
  DialectReader->>LlFunction: apply GC metadata and attributes
  NativeEmitter->>LlFunction: render function header
  NativeEmitter->>Linker: finalize emitted bytes
  Linker->>Clang: assemble output when -S is enabled
  Clang-->>NativeEmitter: return object bytes
Loading

Possibly related issues

Possibly related PRs

✨ 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/7982-native-addrspace-corpora

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.

@proggeramlug
proggeramlug marked this pull request as ready for review August 12, 2026 21:59
The knob-parse audit inventory (every GC-family env knob, how it parsed
before and after), the corpus-currency findings, and the #7737 triage —
including two things worth more than either fix:

  * `llvm-inprocess.yml`'s corpus-age diagnostic was itself vacuous. It asks
    `git log` how many IR-affecting commits landed since the corpora changed,
    on a depth-1 checkout — one commit to answer from, so it printed a
    confident 0 however stale the files were.
  * #7737 item 4's stated prerequisite has gone stale in the opposite
    direction: `gc-ratchet` has zero successes in its last 30 `main` runs and
    `gc-stress` failed the last three, so promoting either to required today
    would block every open PR.
@proggeramlug
proggeramlug merged commit 845ebd3 into main Aug 12, 2026
0 of 19 checks passed
@proggeramlug
proggeramlug deleted the fix/7982-native-addrspace-corpora branch August 12, 2026 22:06
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Follow-up commit 61d0f57da: the define_header pin took three attempts, and the first two failed the same way the bug did — worth recording because it is this PR's own bug class occurring inside its own test.

  1. The to_ir == define_header agreement test cannot see a dropped gc "statepoint-example": with one shared renderer both sides change identically. Sabotage passed.
  2. A dedicated strategy test that branched on native_stack_roots_enabled() never ran its ON arm under cargo test — no module has called set_native_roots_for_target, so the predicate is false in the test process. Sabotage passed again.
  3. Only pinning both lowerings with NativeRootsPin::{native,shadow}, and asserting stack_map_slot_count in each arm so neither can pass vacuously, goes red on the sabotage.

Sabotage-verified with the fix committed first, then restored and rebuilt. 935 perry-codegen lib tests green under --features llvm-inprocess; the three define_header_tests also pass without the feature, i.e. in per-PR cargo-test.

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