fix(runtime): ToInt32/ToUint32 for dynamic bitwise helpers + Math.clz32#6122
Conversation
…th.clz32
The six dynamic bitwise/shift helpers (js_dynamic_{shr,shl,bitand,bitor,bitxor,
ushr}) and Math.clz32 converted their f64 operand with `x as i64 as i32/u32`,
which SATURATES for |x| >= 2^63 instead of ECMAScript ToInt32/ToUint32
(truncate toward zero, reduce modulo 2^32). So on the fully-dynamic (any-typed
operand) path, e.g. `(1e20 as any) | 0`, `(1e20 as any) >>> 0`, and
Math.clz32(1e20) produced wrong results (0/-1 instead of 1661992960 / 1).
Route all seven through shared dyn_to_int32/dyn_to_uint32 helpers using the
same rem_euclid form already proven correct in js_dynamic_bitnot and
js_math_to_int32. Adds test_gap_toint32_dynamic_bitops (byte-matched vs node).
📝 WalkthroughWalkthroughMath.clz32 and dynamic bitwise/shift operators (bitnot, shr, shl, bitand, bitor, bitxor, ushr) are updated to compute ToInt32/ToUint32 conversions via truncation modulo 2^32, replacing prior saturating i64/i32 cast-based logic. A new test file validates the corrected behavior for large-magnitude operands. ChangesToInt32/ToUint32 Wrapping Fix
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry-runtime/src/value/dynamic_arith.rs (1)
534-551: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: dedup the two conversion helpers and consider sharing one ToInt32/ToUint32 across modules.
dyn_to_uint32is byte-for-byte the unsigned reinterpretation ofdyn_to_int32, so it can delegate. More broadly, the same trunc-mod-2^32 logic now lives here, incrates/perry-runtime/src/math.rs(js_math_clz32), and incrates/perry-runtime/src/object/global_this/builtin_thunks.rs(math_to_int32/math_to_uint32) — three copies that can drift. Consider a single shared helper.♻️ Local dedup
#[inline] fn dyn_to_uint32(v: f64) -> u32 { - if !v.is_finite() { - 0 - } else { - v.trunc().rem_euclid(4_294_967_296.0) as u32 - } + dyn_to_int32(v) as u32 }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/perry-runtime/src/value/dynamic_arith.rs` around lines 534 - 551, The integer conversion logic is duplicated across dyn_to_int32, dyn_to_uint32, js_math_clz32, and the math_to_int32/math_to_uint32 helpers, so update dynamic_arith to make dyn_to_uint32 delegate to dyn_to_int32’s shared conversion path and extract a single shared ToInt32/ToUint32 helper that all modules can call. Keep the ES semantics centralized in one place and replace the repeated trunc/mod-2^32 logic in dynamic_arith.rs, math.rs, and builtin_thunks.rs with that common helper to prevent drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/perry-runtime/src/value/dynamic_arith.rs`:
- Around line 534-551: The integer conversion logic is duplicated across
dyn_to_int32, dyn_to_uint32, js_math_clz32, and the math_to_int32/math_to_uint32
helpers, so update dynamic_arith to make dyn_to_uint32 delegate to
dyn_to_int32’s shared conversion path and extract a single shared
ToInt32/ToUint32 helper that all modules can call. Keep the ES semantics
centralized in one place and replace the repeated trunc/mod-2^32 logic in
dynamic_arith.rs, math.rs, and builtin_thunks.rs with that common helper to
prevent drift.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e8c5c710-e5f0-4004-8a28-4b1e2415d7ec
📒 Files selected for processing (3)
crates/perry-runtime/src/math.rscrates/perry-runtime/src/value/dynamic_arith.rstest-files/test_gap_toint32_dynamic_bitops.ts
Partially addresses #6079 (the ToInt32 residuals).
Problem
The six dynamic bitwise/shift helpers —
js_dynamic_{shr,shl,bitand,bitor,bitxor,ushr}— andMath.clz32converted theirf64operand withx as i64 as i32(oras u32). Rust's float→int cast saturates at±2^63, whereas ECMAScript ToInt32/ToUint32 truncate toward zero and reduce modulo 2^32. So on the fully-dynamic (any-typed operand) path:The typed
| 0fast path (toint32_wrap, #5466) andjs_dynamic_bitnotwere already correct — these six + clz32 were missed.Fix
Add shared
dyn_to_int32/dyn_to_uint32helpers using the sametrunc().rem_euclid(2^32)form already proven injs_dynamic_bitnotandjs_math_to_int32, and route all seven sites through them (shift counts useToUint32(b) & 0x1F).Test
test-files/test_gap_toint32_dynamic_bitops.ts— clz32 + the six ops over large (1e20,5e9,3e9) and small operands, byte-matched vsnode --experimental-strip-types.Not in this PR (remaining #6079 items)
toFixed/toPrecision half-to-even rounding,
toString(radix)fraction truncation,Number("0x…" > u64), UTF-16 vs byte string-compare order, and thejs_jsvalue_loose_equalsassert drift — tracked separately in #6079.Summary by CodeRabbit
Bug Fixes
Math.clz32and related operations to handle very large magnitudes and non-finite values more predictably.~,&,|,^,<<,>>, and>>>results forany-typed operands to better match expected runtime behavior.Tests
Math.clz32.