refactor(codegen): Layer 1 slice 1 — migrate lower_array_method.rs onto the rooting API (#7615) - #7618
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe migration ledger now includes ChangesArray method rooting migration
Estimated code review effort: 1 (Trivial) | ~3 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
…ting API Every operand — receiver included — is now lowered through rooting::with_operands_rooted; no arm calls lower_expr. The module lands in the migration ledger. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
…_method Eighteen copies of the same three lines become arg_or_undefined(). Verified IR-neutral: all 8 probe modules byte-identical against the pre-refactor arm. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
88b0316 to
392feae
Compare
Audit before merge — verified, merged as v0.5.1354Structural claims verified on the branch: The genuinely-new coverage verified behaviourally: the The honest scope correction is the report's best content. The map said "40 Re-run here: root-dominance both modes (129/129, 0 violations, 40/40 Process note folded into this merge cycle: the slice agent's flag of the |
Layer 1 campaign slice 1 (#7615):
crates/perry-codegen/src/lower_array_method.rsmigrated end to end ontocrate::rooting, following the template #7617 established. 1 module, 1199 → 1346 lines, 37 raw sites, 40 hazard sites — the map's highest density. The whole module lands in one PR; the ledger notes no outstanding boundary.The hazard was structural, not incidental
lower_array_methodlowered the receiver above thematch:So every one of the ~30 arms held a heap pointer in an SSA register across the lowering of its arguments. For any argument that runs user code — a callback literal (
js_closure_newallocates), a call, an array literal — that is #7453's window, inarr.map(cb),arr.filter(cb),arr.sort(cmp),arr.concat(f()),arr.splice(i, n, mk()). The module had zero rooting references before this.What is actually new, stated against #7280
This is the part worth reading, because the module was partially protected already and the PR should not claim otherwise.
root_reload.rs(#7280) is a post-pass that re-reads a shadow slot below every collection point that can run under it. Where the receiver is a shadow-slotted local, it already fired — visible in the baseline IR as an out-of-sequence register (%r309 = load double, ptr %r7emitted after%r250). So forconst a = [...]; a.sort(cmp)the old code was not stale.What #7280 structurally cannot cover, and what this migration closes:
A receiver reassigned by its own argument. GC: #7154's residual is NOT fixed — the loop-polls config is red 0/30, and stock zod alone fails 5/40 #7280 deliberately bails when a store to the slot can run in the window, because re-loading would observe the assignment —
operand_is_reloadable's documented miscompile. It leaves the register alone, and the register is stale. Baseline IR, verbatim:After:
A temp root is the only strategy that gives both the call-time value and a rewritten address. Same shape on
indexOf.Argument-to-argument windows.
arr.splice(mk(), mk(), mk(), mk())held each evaluated argument in a register across the next one's lowering; no slot, so nothing to reload.A dead duplicate unbox in
sort. The comparator path unboxed the receiver above theif, lowered the comparator, then unboxed again.%r243 = and i64 %r242, 281474976710655in the baseline is referenced exactly once — its own definition. Removed; it is the only instruction this PR deletes.How it is migrated
One
rooting::with_operands_rootedaround the wholematch, over the receiver plus the arguments the arm consumes. Those are declared in one place,lowered_arg_count, and the arms then only emit —lower_exprdoes not appear in this file, which is what makes "no operand register crosses a collection point" a property of the module rather than of each arm's author.The asymmetry that makes the table safe is stated in its doc: under-counting is loud (an arm indexes a value that is not there and panics during codegen, on the first program that reaches it), over-counting is benign (JS evaluates every argument anyway, so lowering one the arm ignores is a spec fix at worst). Counts are exactly what each arm lowered before, so nothing changes in this slice. Two unit tests state the table independently and assert it never claims more than it was given.
IR-identity evidence
Program set: 7 purpose-built probes reaching all 46 of this module's distinctive callees — every arm including the
thisArgarray-like family, the runtime-dispatch family, the typed-arrayset/subarrayarms, and the two receiver shapes #7280 cannot reach. CompiledPERRY_RS4GC=0 PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_INLINE_SHADOW_SLOT=0 PERRY_NO_AUTO_OPTIMIZE=1 --trace llvm,PERRY_RUNTIME_DIRpinned, both arms.95 functions compared; 90 identical, 5 differ — and all 5 are
main, the only function in each probe containing an array-method call. 2 of 7 modules are identical outright.Whole-corpus net instruction delta, every line of it:
store i64 0, ptr %·load i64·store i64·bitcast i64→doublejs_shadow_slot_bind/js_shadow_slot_setat slots 1–4bitcast double→i64·alloca i64·js_shadow_frame_enter(i32 5)load double, ptr %— GC: #7154's residual is NOT fixed — the loop-polls config is red 0/30, and stock zod alone fails 5/40 #7280's reloads, replaced by root readssortunbox (bitcast+and)js_shadow_frame_enter(i32 3)(frame grew to 5 slots)Non-root-plumbing added: 0. Non-plumbing removed: 1 kind, the dead
sortunbox, verified dead by reference count.The comparison masks register names and compares each function as a multiset, because
mainhere is a long run of near-identicalconsole.log(a.method(…))statements anddifflibaligns statement N's inserted plumbing against statement N+1's body — it reported 1426 spurious additions for a 171-instruction delta. What the multiset gives up is ORDER, which is the property a rooting change is about, so order is checked separately: by reading the unified diff of the small cases (shown above) and by the dominance checker over the whole corpus.Behavioural A/B: all 7 probes, both arms — identical stdout and exit code; and the new arm is byte-identical again under
PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=64.Ledger sabotage — result recorded
Reintroduced the escape hatch into the migrated module (a real, compiling
temp_root_push_double+temp_root_truncatepair in thereversearm):expr::temp_rootlower_array_method.rs:375and:376Same answer #7617 measured, now confirmed for a second module: the API does not make the bug fail to compile, and the ledger is what denies the escape hatch. Reverted; the file names
temp_rootonly in one prose line of its module doc, whichescape_hatch_usesstrips as a comment.Verification (local — the CI backlog is deep, so this is the evidence)
gc-root-dominance, both gated modes, on the post-change compiler: corpus 129/129 sources → 149 modules, 2452 functions, 9810 root stores → 0 violations;--seeded-violations 40→ 40 planted, 40 caught, 0 missed;--unrooted-allocas --moving-only→ 0. Baseline arm: same corpus, 9803 root stores, 7860 gc-capable allocas, also 0/0. The +7 / +1 delta is how you can tell the gate's subject was live rather than absent.--self-test,--audit-alloc-re,--audit-poll-capable,--audit-immovable-sources.cargo test -p perry-codegen --lib— 691 pass, including both newlowered_arg_counttests and all four ledger tests.--doc— bothcompile_fail,E0499arms still reject.cargo test -p perry-runtime --no-fail-fast— 1886 pass, 0 fail../run_parity_tests.sh --filter test_gap_arrayagainst the pinned oracle (node 26.5.1), both arms: 13/13 PASS, 0 parity fail, 0 compile fail, 0 crashed, 0 skipped — identical failure sets, because the set is empty. Full-suite result noted below.cargo fmt --all -- --check,workspace_architecture.py(+self-test),check_file_size.sh(1346 lines, under the cap),gc_store_site_inventory.py(+self-test),addr_class_inventory.py(+self-test),class_id_collisions.py,raw_handle_debt.py(+self-test, 998 = baseline),gc_gate_wiring_check.py(+self-test),binding_pins.mjs --check.Not run locally: the dependency-scale (
zod) dominance corpus, which needsnpm ci.Unrelated finding:
lintis already red onmainpython3 scripts/check_test_registration.pyfails on a pristineorigin/main:That file landed in #7612 (
96e034809). Neither it nortest-parity/gc_repsel_corpus.txtis in this PR's diff, and registering a test changes whatgc-stress/gc-moving-witnessesexecute — a gate change with its own measurement, not something to smuggle into a refactor. Flagging it rather than fixing it: a required-and-red gate means every merge is bypassing it, which is hazard 2 in CLAUDE.md.Not in this PR
No version bump. No behaviour change beyond the itemised rooting fixes and the one dead instruction. No new combinator — the
toStringarm'sunbox_str_handlewindow (#7213) is deliberately left, with its reasoning recorded in the module header: closing it needs a combinator that roots across a collection point this module emits rather than one it infers from an operand list, and that should arrive with the slice that needs it. No gate widening. No further modules.Advances #7615. Slice 1b (
expr/arrays_finds.rs,expr/array_methods.rs) is next.Summary by CodeRabbit
Documentation
Chores
Bug Fixes