fix(runtime): decode int32 operands in the dynamic arithmetic helpers - #5798
Conversation
The dynamic `+`/`-`/`*`/`/`/`%` runtime helpers performed the raw f64 op on
their operands. An int32 is NaN-boxed (INT32_TAG = 0x7FFE), so its f64 bits ARE
an IEEE NaN; doing `a + b` on it propagates the tag through the FPU (ARM64 keeps
the NaN payload) and returns the *boxed operand* instead of the result.
The most visible symptom: a better-sqlite3 integer column read through the
`any`-typed path made `n + 100 === n` (and the same for every integer column).
`-`/`*` happened to work only because codegen eagerly coerces operands for the
unambiguous numeric operators; `+` stays boxed (it may be string concat) and
routes the raw int32 into `js_dynamic_string_or_number_add`, whose numeric
fallback returned the operand's NaN-boxed bits for an int32 instead of its value.
Fix: decode int32 operands to their plain-double value before the f64 op, in
`js_dynamic_string_or_number_add` and the five `js_dynamic_{add,sub,mul,div,mod}`
helpers (shared `numify_arith_operand`). Plain doubles and real NaNs pass through
unchanged. Adds regression tests.
Same INT32_TAG-not-decoded class as the JSON `null` bug; surfaced on the
electron-compat stress test (math on sqlite integer columns silently wrong).
📝 WalkthroughWalkthroughAdds a Changesint32 Arithmetic Decoding Fix
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry-runtime/src/value/dynamic_arith.rs (1)
574-582: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd RHS and signed int32 regression cases.
The helper decodes both operands, but this test only exercises RHS int32 for
+; add RHS cases for-,/,%and at least one negative int32 payload to lock in signed decoding.Suggested test additions
assert_eq!(js_dynamic_sub(int32(42), 1.0), 41.0); + assert_eq!(js_dynamic_sub(100.0, int32(42)), 58.0); assert_eq!(js_dynamic_mul(int32(42), 2.0), 84.0); + assert_eq!(js_dynamic_mul(2.0, int32(42)), 84.0); + assert_eq!(js_dynamic_mul(int32(-7), 6.0), -42.0); assert_eq!(js_dynamic_div(int32(84), 2.0), 42.0); + assert_eq!(js_dynamic_div(84.0, int32(2)), 42.0); assert_eq!(js_dynamic_mod(int32(43), 10.0), 3.0); + assert_eq!(js_dynamic_mod(43.0, int32(10)), 3.0);🤖 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 574 - 582, The regression test in dynamic_arith_decodes_int32_operands only covers an int32 on the RHS for js_dynamic_add, so extend it to also assert RHS int32 decoding for js_dynamic_sub, js_dynamic_div, and js_dynamic_mod, using the existing helpers like int32, js_dynamic_sub, js_dynamic_div, and js_dynamic_mod. Also add at least one case with a negative int32 payload to verify signed decoding remains correct across the dynamic arithmetic path.
🤖 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 574-582: The regression test in
dynamic_arith_decodes_int32_operands only covers an int32 on the RHS for
js_dynamic_add, so extend it to also assert RHS int32 decoding for
js_dynamic_sub, js_dynamic_div, and js_dynamic_mod, using the existing helpers
like int32, js_dynamic_sub, js_dynamic_div, and js_dynamic_mod. Also add at
least one case with a negative int32 payload to verify signed decoding remains
correct across the dynamic arithmetic path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ac4e3349-d366-483b-9fc6-a34705e02550
📒 Files selected for processing (1)
crates/perry-runtime/src/value/dynamic_arith.rs
Problem
The dynamic arithmetic helpers (
js_dynamic_string_or_number_addandjs_dynamic_{add,sub,mul,div,mod}) performed the raw f64 op directly on their operands. An int32 is NaN-boxed (INT32_TAG = 0x7FFE), so its f64 bits are an IEEE NaN — doinga + bpropagates the tag through the FPU (ARM64 keeps the NaN payload) and hands back the boxed operand instead of the result.Most visible symptom: a
better-sqlite3integer column read through theany-typed path maden + 100 === n— silently wrong math on every integer DB column (timestamps + durations, sums, pagination offsets, …).-/*//happened to work because codegen eagerly coerces operands for the unambiguous numeric operators.+stays boxed (it might be string concat) and routes the raw int32 intojs_dynamic_string_or_number_add, whose numeric fallback returned the operand's NaN-boxed bits for an int32 instead of decoding it — but the other four helpers had the identical latent bug for any int32 that reaches them.Fix
Decode int32 operands to their plain-double value before the f64 op (shared
numify_arith_operand), injs_dynamic_string_or_number_addand the fivejs_dynamic_{add,sub,mul,div,mod}helpers. Plain doubles and real NaNs from arithmetic pass through unchanged; the hot all-doubles fast path injs_dynamic_string_or_number_addis untouched (int32 already falls out of it).This is the same
INT32_TAG-not-decoded class as the JSON-nullbug in #5796 (a different code path).Tests
dynamic_arithfor int32 operands across+ - * / %and the string-or-number+.Summary by CodeRabbit
+,-,*,/, and%calculations to return the expected results when integers are mixed with other numeric values.