fix(codegen): teach the in-process IR reader the vector instructions codegen emits (#8228) - #8241
Conversation
…codegen emits The dialect reader had no `insertelement` case, so `#8204`'s object-header image compose fell through to the binary-op arm and failed every module large enough to split across native codegen units. Adds `insertelement`, `extractelement`, `shufflevector`, vector-typed `add`/`mul`, constant vector literals, and vector `poison`/`undef`/ `zeroinitializer` — the closed set perry-codegen actually emits. Also replaces the reader's snapshot-only gate with a live emit -> re-parse test, so the next new emission form fails in `cargo-test` rather than in a user build of a multi-unit module.
|
Warning Review limit reached
Next review available in: 1 minute Limit details: You’ve used all 8 included reviews currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
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 |
…8274) * test(next): stop the App Route dylib gate pinning the text backend The gate pinned PERRY_LLVM_INPROCESS="${PERRY_LLVM_INPROCESS:-0}", so it compiled the fixture through the text transport rather than the native in-process path that is the default. `${VAR:-0}` cannot express "unset", so no invocation of this gate could exercise the shipped configuration — the job ran, but not on its subject. The pin was correct while #8228 made the native path unable to compile five of this fixture's modules (insertelement had no case in the dialect reader). #8241 fixed that, so the pin now only hides the backend under test. Forward the variable only when the caller sets it: an explicit backend remains selectable for bisection, and the unset default reaches the compiler unchanged. Verified on 183d30c: native backend compiles 104/104 modules and the gate passes 100/100 verifier repetitions twice, with `freeze/LLVM pipeline started` present 5x per compile against 0x on the text path. Refs #8040, #8228. * docs(changelog): renumber the fragment to its own PR #8259 is a different, already-closed issue; fragment filenames are PR-keyed. Claude-Session: https://claude.ai/code/session_01AHvBYz7E6wWKv8kmvLLGpj --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Fixes #8228.
What was broken
Perry's in-process LLVM backend round-trips its own textual IR through a
hand-written dialect reader (
crates/perry-codegen/src/dialect/). That readeraccepts a closed set of instruction forms by design — anything else is
supposed to fail loudly rather than diverge silently.
#8204(the #8122 header-shrink instruction-cost recovery) started emitting theper-class object-header image as a
<2 x i64>composed at module init:The reader has no
insertelementcase, so the line fell through to thebinary-op arm, which sees three operands where it wants two:
The reader is only the default for split (multi-unit) modules
(
native_emit::native_units_mode), so this hit exactly the modules big enough tosplit — the five biggest of the Next App Route fixture, including
next-server/app-route.runtime.prod.jsitself.What this covers, and what it deliberately does not
git grepovercrates/perry-codegen/srcfor every vector/aggregate form givestwo emitters, and both reach the reader as
Rawtext:insertelement <2 x i64> <i64 W, i64 0>, i64 %hdr, i32 1function.rs:586,codegen/string_pool.rs:514insertelement <4 x i32> {<i32 0,…>|%reg|poison}, i32 %v, i32 Nexpr/channel.rs:401,410,440shufflevector <4 x i32> %a, <4 x i32> poison, <4 x i32> zeroinitializerexpr/channel.rs:415extractelement <4 x i32> %v, i32 Nexpr/channel.rs:469mul/addover<4 x i32>expr/channel.rs:421,449All of it is now accepted:
dialect/vector.rs(new; split out for the 2000-line cap, followingeh.rs/types.rs) —insertelement,extractelement,shufflevector, andthe vector arm of binary arithmetic. The last one is not cosmetic: inkwell's
integer builders are generic over
VectorValue, but the scalar arm reachesits operands through
into_int_value(), which panics on a vector insteadof returning an error, so
mul <4 x i32>would have aborted the compilerrather than reported anything.
dialect/types.rs— constant vector literals (<i64 W, i64 0>,<i32 0, i32 0, i32 0, i32 0>) pluspoison/undef/zeroinitializerof vector type.
channel.rsseeds a splat frompoisonand its shuffle maskis
zeroinitializer, so those are emitted forms, not corners.channel.rs's<4 x i32>family was the same latent bug — it has simplynever been reached, because a byte-channel reduction has not yet shown up in a
module large enough to split. It is covered by this fix.
Deliberately not implemented:
insertvalueandextractvalue. Perry emitsneither — the tree's only
extractvalueis an input fixture toretype_landing_pads_for_statepoints, not emitted IR. Adding them would be armsno test can reach, which is the opposite of what a closed reader is for. Vector
binary ops other than
add/mulbail with a message naming the emitter, forthe same reason.
Why did this reach
main?Because the reader's only gate is a set of frozen snapshots. All three
dialect::tests::corpus_*tests round-trip.llfiles checked in underexperiments/llvm-inprocess-spike/. A form the emitters started producingafter those files were captured is invisible to them by construction.
Nothing else covered it either:
gap/parity fixture is small enough to be a single unit, which keeps the
mature text transport;
PERRY_LLVM_INPROCESS=0(external clang/opt) compiles all 104 modules cleanat the same commit, and that is the configuration
tests/test_next_app_route_dylib.shpins;which is tag-gated.
So the failure needed a large in-process build to appear, and no per-PR job
performs one.
The test added
dialect::tests::compiled_module_ir_round_trips_through_the_readerreplaces thesnapshot with a live emit → re-parse: it compiles a real HIR module (a class
new'd in a loop, which admits the inline bump allocator and therefore theheader image) through the real emitters, then feeds every function of the
resulting IR back through the reader and verifies the module. It asserts the
insertelement <2 x i64> <i64compose is present before round-tripping, so afixture that stopped exercising the allocator fails loudly instead of passing
vacuously. This closes the class, not the instance: the next new emission form
goes red in
cargo-test.dialect::tests::channel_reduction_vector_forms_round_tripcovers the<4 x i32>family, which no HIR fixture reaches cheaply. Its IR is built fromthe emitter's own
format!templates rather than from duplicated literals, andeach template is asserted to still be present verbatim in
expr/channel.rs— soif the emitter's text changes, this fixture changes with it or the test fails.
Validation
All on the bench mini (M1, macOS 26.5.1 arm64, LLVM 22.1.8),
perry-devprofile.Sabotage A/B — the new tests are the detector
Reverting only the fix (
dialect/{mod,types}.rsback tomain,dialect/vector.rsremoved) while keeping the new tests, on the same tree:207232172546is the same packed header word the production failure reports, sothe unit test reproduces the field failure exactly. And the three frozen-corpus tests
pass in both arms — the demonstration that they could never have caught this.
Test counts
cargo build --profile perry-dev -p perry— clean, no new warnings.cargo test --profile perry-dev -p perry-codegen --lib— 1057 passed, 0 failed(1055 before; the two new tests are the delta).
cargo fmt --all -- --check,scripts/check_file_size.sh,scripts/addr_class_inventory.py— clean.crates/perry-codegen/tests/*.rswere not run: this change touches onlysrc/dialect/, which no integration suite covers.Acceptance: the App Route fixture's app dylib
Same tree, same fixture, same runtime archives (the diff is codegen-only, so both
arms share one
PERRY_RUNTIME_DIR); only the compiler binary differs.PERRY_MODULE_JOBS=2 PERRY_CODEGEN_UNIT_JOBS=2, in-process backend (default),perry compile perry-host.js --output-type dylib --no-auto-optimize --no-cache.Before — exit 1 after 14m19s, all five modules from #8228, one signature:
After — exit 0 after 15m24s,
Error compiling modulecount 0, all 104modules compiled,
Wrote shared library: next-app-after.dylib(87,490,488 bytes),and
nm -ustill showsjs_gc_initundefined (the fixture's own check that the appdylib does not embed the Perry ABI).
Third arm: the defect is isolated to the native path
Same unfixed
maincompiler, same fixture, onlyPERRY_LLVM_INPROCESS=offadded(that selects the mature text transport,
native_emit::native_units_mode):exit 0 in 9m15s,
Error compiling modulecount 0, dylib written. So the threearms are
mainbad binary op \insertelement``mainPERRY_LLVM_INPROCESS=offmain+ this fixArm 2 is a usable interim workaround and is now noted on #8228, but it is not free:
the text path materialized 222.6 MB of IR for one module and produced a 206 MB
dylib against the native path's 87 MB (the giant modules drop to
-O0there).It is a way to keep moving, not a reason to leave the native default broken.
Running the App Route end to end is out of scope here and owned separately; this PR
is done when the dylib links.