Repository navigation
Description
Found while root-causing #7232. Same invariant, different pass — and this one is
worse, because it also truncates fractional arguments.
emit_i64_specializations (crates/perry-codegen/src/codegen/i64_spec.rs)
re-emits a whole function body in i64 arithmetic whenever
is_integer_specializable (crates/perry-codegen/src/collectors/clamp_detect.rs:154)
accepts it, and wraps it in an f64 shim that fptosis every argument and
sitofps the result. The admission rule is:
return_type == Numberand every paramNumber, and- the body is Add/Sub/Mul/Compare/Conditional over locals, params, integer
literals, exactly-representable number literals, and self-calls
(i64s_expr, same file, line 278).
Nothing in that rule proves the arguments are integers, and nothing bounds any
intermediate. So both halves of the contract are unchecked.
Repro
// probe.ts
function grow(n: number, acc: number): number {
return n === 0 ? acc : grow(n - 1, acc * 3 + 1);
}
console.log("grow:", grow(40, 1));
function frac(n: number, acc: number): number {
return n === 0 ? acc : frac(n - 1, acc * 2);
}
console.log("frac:", frac(3, 0.5));$ node --experimental-strip-types probe.ts # 26.5.1
grow: 18236498188585394000
frac: 4
$ perry probe.ts -o probe && ./probe # main @ c9cd73ba5, aarch64 macOS
grow: -210245885124158400 # WRONG — i64 overflow wraparound
frac: 0 # WRONG — fptosi truncated 0.5 to 0
Two distinct failures:
grow— the accumulator passes 2^53 and then 2^63. JS rounds each*
and+to the nearest double; the i64 body computes exactly and then wraps
at 2^63, so the sign flips. (Below 2^63 but above 2^53 the answer is merely
wrong rather than negative — the number arithmetic in a local is evaluated past double precision —(x * 1103515245 + 12345) & 0x7fffffffdiverges from Node #7232 divergence, in i64 form.)frac—frac(3, 0.5)is0.5 * 2 * 2 * 2 = 4in JS. The wrapper's
fptosi double %arg to i64truncates0.5to0on entry, so the whole
computation runs on the wrong value. Anumberparam is a double; nothing
here proves otherwise.
The emitted IR shows both directly:
define double @perry_fn_probe_ts__frac(double %arg0, double %arg1) alwaysinline {
%r1 = fptosi double %arg0 to i64 ; 0.5 -> 0
%r2 = fptosi double %arg1 to i64
%r3 = call i64 @perry_fn_probe_ts__frac_i64(i64 %r1, i64 %r2)
%r4 = sitofp i64 %r3 to double
ret double %r4
}Why it has stayed quiet
The specialization is alwaysinline and non-recursive call sites get inlined at
HIR level before it matters — function mulAdd(x, y) { return x * y + 1 } called
straight-line emits an fmul and the _i64 body is dead-stripped. Self-recursion
is what makes the specialized body actually reachable, which is also the case the
pass exists for (the doc comment says "pure numeric recursive fn ... like
fibonacci"). Fibonacci's values are small and integral, so the motivating example
never exercises either hole.
Pre-dates #7232's fix — reproduced identically at c9cd73ba5 and on the #7232
branch, which touches a different pass (expr/i32_fast_path.rs).
The invariant it needs
The same one #7232's fix states, one width up:
Integer arithmetic may stand in for a JS
numbercomputation only while every
intermediate is exactly representable as a double (|v| <= 2^53) — and only
when every input is provably an integer to begin with.
Neither is currently proven. A fix has to either (a) prove both — integer-valued
params (there is already collectors/integer_locals.rs and #7122's canonical-i32
machinery to build on) plus a magnitude bound on the body, reusing
i32_chain_magnitude_bits's composition at the i64/2^53 ceiling — or (b) delete
the pass, since per CLAUDE.md's kill-policy an unproven mode that nothing
exercises is a decision that has not been made. Worth measuring what it actually
buys on benchmarks/suite/05_fibonacci.ts before choosing.
Suggested acceptance
A parity case with a self-recursive accumulator that crosses 2^53, one that
crosses 2^63, and one called with a fractional argument — so a fix cannot pass
by handling only the overflow half.
Activity
docs: changelog fragment for #7238
fix(codegen): remove the unproven i64 function specialization (PerryT…