perf(codegen): version stable packed array loops - #8755
Conversation
Lands #8719 with its soundness blocker fixed. The blocker was NOT where the emitted IR first suggested. The symptom was an unguarded `fptosi` + `xor i32` consuming a phi that merged a proven number (the string arm of a guarded `charCodeAt`) with an arbitrary user method's result (the generic arm) -- so an `any` receiver returning a BigInt yielded garbage where the spec requires BigInt xor or a TypeError. The cause is one lever in `expr/literals_vars.rs`. The PR's new `explicit_numeric_toint32` disjunct bypassed `can_lower_expr_as_i32_in_current_region`, which correctly answers false for `(h ^ recv.charCodeAt(i)) | 0`, and called into the `lower_expr_as_i32` family without its documented precondition. `lower_expr_native_i32`'s i32-chain arm then recursed structurally, `fptosi`-ing every operand it could not lower natively -- so `binary.rs`, which already applies `is_provably_not_bigint` per operand, was bypassed entirely rather than mishandling the join. `expr_produces_canonical_raw_f64` is a claim about the VALUE the ordinary lowering produces, not a licence to re-evaluate its operands natively. The fix lowers through `lower_expr` and applies `toint32_fast`, so an unproven tree keeps `js_dynamic_bitxor` while a proven one still gets the inline `xor i32`. The i32-slot store the lever exists for is preserved, so this remains an improvement over main, which took neither branch here. House precedent settles the alternative: `lower_guarded_numeric_add`'s doc block records that sinking arithmetic into the arms was already tried and is worse -- per-node diamonds make the outer add consume a phi that LLVM cannot prove canonical, and that shape went 86 ms -> 119 ms. The negative control passes UNMODIFIED (blob hash identical to main), and both positive tests still fire the fast path for proven receivers. No version bump.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (48)
📝 WalkthroughWalkthroughThis PR adds versioned lowering for indexed and stable packed arraylike loops, emits fast indexed-method clones, adds runtime packed-arraylike guards and numeric-proof invalidation, updates related guard masks and tests, and fixes ChangesLoop versioning and packed arraylike support
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant lower_for
participant stable_packed_loop
participant js_packed_arraylike_loop_guard
participant generic_loop
lower_for->>stable_packed_loop: try lower(...)
stable_packed_loop->>js_packed_arraylike_loop_guard: validate receiver and bound
js_packed_arraylike_loop_guard-->>stable_packed_loop: metadata or reject
stable_packed_loop->>stable_packed_loop: emit fast clone when admitted
stable_packed_loop->>generic_loop: enter generic path on reject or side exit
Possibly related PRs
Suggested reviewers: ✨ 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 |
…8771) Lands the follow-up commit from #8719, rebased onto current main. #8719's main body already landed via #8755. This is its remaining commit, cherry-picked so it carries only its own content: merging the branch would have reverted #8740, #8742, #8743 and #8763, whose changes its stale base predates. The lever now additionally requires `!ctx.stable_packed_loop_facts .is_empty()`, because other loop clones own narrower indexed-load contracts that their existing assignment lowering must continue to see. The conflict with the landed soundness fix was comment-only -- the code auto-merged -- and both halves survive: the unproven path still lowers through `lower_expr` + `toint32_fast` and so still reaches `js_dynamic_bitxor`, while the gate is narrowed. The negative control `char_code_at_on_an_unproven_receiver_keeps_the_ runtime_lowering` is byte-identical to main (blob 86b697d) and all three char_code_at probes pass. Known coverage note: those three probes bound their loops with `i < 64` rather than an array length, so under the narrowed gate they no longer enter the branch #8755 fixed -- they still pass, but via the generic tail. That branch's end-to-end coverage is `read_only_loops_have_preheader_ proofs_and_fallback_free_fast_blocks` in `issue_8690_loop_versioned_ arraylike.rs`, which this commit extends but which could not be executed here: it needs the compiler plus the -static wrappers, and the volume would not hold them. No version bump. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Lands #8719 with its soundness blocker fixed.
The bug was not where the IR first suggested
The symptom I reported was an unguarded
fptosi+xor i32consuming a phi that merged a proven number (the string arm of a guardedcharCodeAt) with an arbitrary user method's result (the generic arm) — so ananyreceiver returning a BigInt yields garbage where the spec requires BigInt xor or aTypeError. I attributed that tobinary.rsmishandling the join. That was wrong, and worth recording because the correct diagnosis is more useful:The cause is one lever in
expr/literals_vars.rs. The PR's newexplicit_numeric_toint32disjunct bypassescan_lower_expr_as_i32_in_current_region— which correctly answersfalsefor(h ^ recv.charCodeAt(i)) | 0, since aCallwith aPropertyGetcallee is not an i32-chain leaf. It then calls into thelower_expr_as_i32family without its documented precondition ("Must be called only aftercan_lower_expr_as_i32returned true for the same expression").lower_expr_native_i32's i32-chain arm recurses structurally andfptosis every operand it cannot lower natively.So
binary.rs— which already appliesis_provably_not_bigintper operand — was bypassed entirely rather than getting the join wrong.The predicate itself is fine and stays:
expr_produces_canonical_raw_f64is a claim about the value the ordinary lowering produces, not a licence to re-evaluate its operands natively. The bug was consuming a value-level claim as an evaluation-strategy licence.The fix, and why not the alternative
An unproven tree keeps
js_dynamic_bitxor; a proven one still gets the inlinexor i32. The conversion is free —x | 0always lowers tositofp i32 → double, so the round trip folds — and the i32-slot store the lever exists for is preserved, making this a strict improvement overmain, which took neither branch here.House precedent settles the alternative of sinking arithmetic into the arms:
lower_guarded_numeric_add's doc block (the #7773 fix) records that it was already tried and is worse — per-node diamonds make the outer add consume a phi LLVM cannot prove canonical, so the cold arm stays live in the loop; that shape went 86 ms → 119 ms.Emitted IR after the fix
A proven receiver still gets
cca.fast, the inlineload i8, andxor i32with no dynamic helper.Validation (on the merged result, against current
maine2eee4048)lint-job checkers passtype_analysis/numeric/tests.rsblob hash is byte-identical tomain(86b697d489c8), and all threechar_code_at_*tests pass, so the fast path still fires for proven receivers rather than the optimization being disabledperry-codegen --lib: 1229 passed, 0 failed (+3 = the PR's new tests)perry-runtime --lib(RUST_TEST_THREADS=1): 2671 passed, 0 failed (+2)perry-transform --lib93/0,perry-hir --lib334/0Two caveats, stated rather than smoothed over
--testssweep showedtyped_array_rmw_8692at 8/9. It then passed 9/9 six times standalone, once single-threaded, and the full sweep passed three consecutive times with zero failing suites — and it passes 9/9 on cleanmain. I believe it is flaky (that suite manipulates a process-global artifact env), not a regression here, but I am recording it rather than dropping it.crates/perry/tests/issue_8690_loop_versioned_arraylike.rsneeds the full compiler plus the-staticwrappers; disk was contended for much of this session. The IR-level evidence above is what backs the fix.Summary by CodeRabbit
Performance
Bug Fixes
TypeErrorbehavior when applying integer coercion to BigInt values.