perf(codegen): route % on i64-range integer locals to srem, not fmod - #7416
Merged
Conversation
added 2 commits
August 5, 2026 07:31
frem is not an aarch64 instruction -- it lowers to an fmod LIBRARY CALL.
bench_bitwise emitted four per iteration and ran 20.4x slower than Node.
It is now 1.39x: 55108ms -> 3763ms, a 14.6x improvement, with the
Node-verified CHECKSUM:525000000 unchanged.
A guarded srem fast path already existed. Every gate in front of it asked
the wrong question: is_integer_valued_expr resolves a LocalGet through
integer_locals, which is an i32-RANGE property that also gates i32 shadow
slots -- widening it would have put an i32-overflowing value into an i32
slot. The % path converts to i64 and only needs integer-valued-in-i64.
collect_int_valued_i64_locals supplies that weaker property as a
magnitude lattice, read only by the % gate. integer_locals is untouched.
Three holes closed while building it, each silently-wrong-arithmetic:
* Mul blowup: a pure integrality predicate let (a*b*c) % n reach
fptosi..to i64 with a 93-bit product -- poison. The lattice gates at
2^62, which also closes the same pre-existing hole for i32 locals.
* Zero divisor: an early version routed 1000 % d into srem where d
decrements through zero. srem(x,0) is UB; JS needs NaN. The divisor
is now a non-zero integer literal.
* The saturation argument was wrong for large deltas -- a = a + 1e18
escapes i64 in ~10 iterations, not 2^63. Replaced with an IEEE-754
bound: once ulp(v) >= 4D, v +/- d rounds back to v exactly.
The deciding gate is expr/mod.rs::lower_numeric_binary_value, which
intercepts numeric binary ops before binary::lower and only handed Mod
off for i32 counter slots. There are six frem emission sites; widening
the predicate alone changed nothing and the IR acceptance test caught it.
Verified: hot fn frem 4->0, srem 0->4; edge-case differential incl. both
-0 cases via Object.is is byte-identical to Node 26.5.1; cargo test
-p perry-codegen --lib 632 passed; 26/27 math/number/int gap tests, the
one failure pre-existing and unrelated.
|
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 (17)
📝 WalkthroughWalkthroughThe PR adds bounded i64 local analysis for safe integer ChangesInteger modulo lowering
GC evacuation liveness validation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant HIRFacts
participant TypeFacts
participant FnCtx
participant NumericAnalysis
participant ModuloLowering
HIRFacts->>TypeFacts: store bounded i64 local facts
TypeFacts->>FnCtx: initialize int_valued_i64_locals
FnCtx->>NumericAnalysis: analyze modulo operands
NumericAnalysis->>ModuloLowering: approve safe integer remainder
ModuloLowering->>ModuloLowering: emit srem i64
Possibly related issues
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7404.
bench_bitwise: 20.4× slower than Node → 1.39×. 55108ms → 3763ms, a 14.6× improvement, with the Node-verifiedCHECKSUM:525000000unchanged.The bug
fremis not an aarch64 instruction — it lowers to anfmodlibrary call. The hot loop emitted four per iteration (1754 profile samples infmod).A guarded
sremfast path already existed. Every gate in front of it asked the wrong question:is_integer_valued_exprresolves aLocalGetthroughinteger_locals, which is an i32-range property that also gates i32 shadow slots. Widening that would have put an i32-overflowing value into an i32 slot — a miscompile. The%path converts to i64 and only needs integer-valued-within-i64.collect_int_valued_i64_localssupplies that weaker property as a magnitude lattice, read only by the%gate.integer_localsis untouched.Three holes closed, each silently-wrong arithmetic
Mulblowup(a*b*c) % nreachfptosi … to i64with a 93-bit product — poison. The lattice gates at 2^62, which also closes the same pre-existing hole for i32 locals1000 % dwhereddecrements through zero.srem(x,0)is UB; JS needsNaN. Divisor now restricted to a non-zero integer literala = a + 1e18escapes in ~10 iterations. Replaced with an IEEE-754 bound: onceulp(v) ≥ 4D,v ± drounds back tovexactly, so|L| ≤ 2^(55+log₂D)foreverThat third one was my error in the task brief, caught and corrected during implementation.
The gate that actually decides this
Not
binary.rs:588. There are sixfrememission sites, and the owner here isexpr/mod.rs::lower_numeric_binary_value, which intercepts numeric binary ops beforebinary::lowerand only handedModoff when the dividend had an i32 counter slot. Widening the predicate alone changed nothing — the IR acceptance test caught that and stayed red.Verification
frem 4 → 0,srem 0 → 4(baseline had zerosrem, so the check could genuinely fail)-0cases viaObject.is(not===, which cannot distinguish them) — byte-identical to Node 26.5.1CHECKSUM:525000000on all runscargo test -p perry-codegen --lib632 passed / 0 failed; 26/27 math/number/int gap tests, the one failure (process.setTraceSigIntarg validation) pre-existing and byte-identical between armscargo fmt --checkclean; file-size cap and addr-class ratchet passNo env knob added, per CLAUDE.md's kill-policy.
Summary by CodeRabbit
Bug Fixes
%) calculations for integer-valued values outside the 32-bit range.Tests