Skip to content

fix(runtime): ToInt32/ToUint32 for dynamic bitwise helpers + Math.clz32#6122

Merged
proggeramlug merged 1 commit into
mainfrom
fix/6079-toint32-dynamic-bitops
Jul 8, 2026
Merged

fix(runtime): ToInt32/ToUint32 for dynamic bitwise helpers + Math.clz32#6122
proggeramlug merged 1 commit into
mainfrom
fix/6079-toint32-dynamic-bitops

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Partially addresses #6079 (the ToInt32 residuals).

Problem

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 (or as 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:

const x: any = 1e20;
x | 0;        // node 1661992960   perry -1  (or 0)
x >>> 0;      // node 1661992960   perry wrong
Math.clz32(1e20); // node 1        perry 0

The typed | 0 fast path (toint32_wrap, #5466) and js_dynamic_bitnot were already correct — these six + clz32 were missed.

Fix

Add shared dyn_to_int32 / dyn_to_uint32 helpers using the same trunc().rem_euclid(2^32) form already proven in js_dynamic_bitnot and js_math_to_int32, and route all seven sites through them (shift counts use ToUint32(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 vs node --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 the js_jsvalue_loose_equals assert drift — tracked separately in #6079.

Summary by CodeRabbit

  • Bug Fixes

    • Improved bitwise and shift behavior for numeric values so large and fractional inputs now follow JavaScript-style conversion rules more consistently.
    • Fixed Math.clz32 and related operations to handle very large magnitudes and non-finite values more predictably.
    • Updated dynamic ~, &, |, ^, <<, >>, and >>> results for any-typed operands to better match expected runtime behavior.
  • Tests

    • Added coverage for large-number, boundary, and small-value cases across bitwise operations and Math.clz32.

…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).
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Math.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.

Changes

ToInt32/ToUint32 Wrapping Fix

Layer / File(s) Summary
Math.clz32 truncate-mod-2^32 fix
crates/perry-runtime/src/math.rs
js_math_clz32 computes its intermediate u32 via trunc().rem_euclid(2^32) and returns 0 for non-finite inputs, replacing the prior i64 cast that could saturate.
Dynamic bitwise/shift helper and operator updates
crates/perry-runtime/src/value/dynamic_arith.rs
New dyn_to_int32/dyn_to_uint32 helpers implement ToInt32/ToUint32 via truncation modulo 2^32 with non-finite mapped to 0; js_dynamic_bitnot, shr, shl, bitand, bitor, bitxor, and ushr are rewritten to use them instead of cast-based conversions.
Regression test for large operands
test-files/test_gap_toint32_dynamic_bitops.ts
New test verifies clz32, bitwise, and shift operations produce correct truncation-modulo-2^32 results for any-typed operands with `

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • PerryTS/perry#5822: Both PRs modify the non-BigInt js_dynamic_bitnot conversion pipeline in dynamic_arith.rs for ToInt32/NaN/Infinity handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is informative, but it does not follow the repository's required template sections and checklist. Rewrite the PR description to use the required template with Summary, Changes, Related issue, Test plan, Screenshots/output, and Checklist sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the runtime ToInt32/ToUint32 fix for dynamic bitwise helpers and Math.clz32.
Linked Issues check ✅ Passed The PR matches the linked issue's ToInt32/ToUint32 and Math.clz32 requirements for dynamic bitwise/shift operands. [#6079]
Out of Scope Changes check ✅ Passed The changes are confined to the requested runtime conversion fixes and added test coverage, with no unrelated scope visible.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/6079-toint32-dynamic-bitops

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry-runtime/src/value/dynamic_arith.rs (1)

534-551: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: dedup the two conversion helpers and consider sharing one ToInt32/ToUint32 across modules.

dyn_to_uint32 is byte-for-byte the unsigned reinterpretation of dyn_to_int32, so it can delegate. More broadly, the same trunc-mod-2^32 logic now lives here, in crates/perry-runtime/src/math.rs (js_math_clz32), and in crates/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

📥 Commits

Reviewing files that changed from the base of the PR and between 21aad6d and 26ba39e.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/math.rs
  • crates/perry-runtime/src/value/dynamic_arith.rs
  • test-files/test_gap_toint32_dynamic_bitops.ts

@proggeramlug proggeramlug merged commit c78c20b into main Jul 8, 2026
23 of 24 checks passed
@proggeramlug proggeramlug deleted the fix/6079-toint32-dynamic-bitops branch July 8, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant