You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
number arithmetic in a local is evaluated past double precision — (x * 1103515245 + 12345) & 0x7fffffff diverges from Node #7232
Found incidentally while validating a string-sort microbenchmark for #7229 (the
benchmark's LCG key generator produced different keys under Perry than under Node).
Reproduces on main @ 65a64f428; not introduced by #7229 — both arms of that
A/B behave identically.
Repro
// lcg2.ts// straight-line, no looplets0=12345;lets1=(s0*1103515245+12345)&0x7fffffff;lets2=(s1*1103515245+12345)&0x7fffffff;console.log("straight s1="+s1+" s2="+s2);// same thing in a looplett=12345;for(leti=0;i<2;i++){t=(t*1103515245+12345)&0x7fffffff;}console.log("loop t="+t);// forced through a function boundaryfunctionstep(x: number): number{return(x*1103515245+12345)&0x7fffffff;}console.log("fn "+step(step(12345)));
s1 * 1103515245 is 1406932606 * 1103515245 ≈ 1.55e18, which is past 2^53, so
in IEEE-754 double the product is rounded — and + 12345 then falls partly
inside the ulp (256 at that magnitude). ECMAScript numbers are doubles, so the
rounding is observable and Node's & 0x7fffffff sees the rounded value.
Perry gets the right answer only when the intermediate crosses a function
boundary (where it is NaN-boxed as an f64 and therefore rounded). When the
product stays in an SSA local — straight-line or loop-carried — the low bits
survive, i.e. the multiply is being evaluated at wider-than-double precision
(i64-shaped), and ToInt32 then reads bits that JS has already discarded.
654583808 - 654583775 = 33, i.e. the two answers differ only in the low bits
that double rounding is supposed to have erased.
Note this is not a ToInt32-of-a-large-double bug on its own — a literal
double round-trips correctly:
consta=1552571579408578600;console.log((a+12345)|0);// node 654583808, perry 654583808 ✅
It needs the arithmetic to happen in a local.
Why it matters
Any JS that relies on double rounding past 2^53 silently diverges: LCG/PRNG
seeds (the case that surfaced this), hash mixing, checksum accumulators, ID
arithmetic. It is quiet — no throw, no warning, just different numbers — and it
is exactly the failure mode the representation-selection work has to stay clear
of, so it is worth a regression test regardless of which pass turns out to own it.
Suggested acceptance
A parity case asserting (x * 1103515245 + 12345) & 0x7fffffff agrees with Node
for a loop-carried x, plus the straight-line and through-a-call variants so a
future fix can't pass by accident on only one of the three shapes.
Activity
docs: changelog fragment for #7232 (PR #7237)
fix(codegen): round JS arithmetic to f64 at every i32-chain step (#7232…