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
i32 fast path: copying a local whose value came from an arithmetic chain past 2^31 wraps (fourth face of #6072) #6339
Severity: P1 (silent data corruption) Found while fixing:#6319
What
Copying a local whose value came from an arithmetic chain (rather than a literal) past 2^31 wraps. This survives #6258, #6318 and #6319 — it is a fourth, distinct face of #6072's title.
letbig1=2000000000;letbig2=big1+big1;// 4e9 — correct in its own slotlett=0;t=big2;console.log(t);// node: 4000000000 perry: -294967296letsum=0;for(leti=0;i<5;i++)sum=sum+1000000000;letsumCopy=0;sumCopy=sum;console.log(sumCopy);// node: 5000000000 perry: 705032704
big2 / sum themselves stay correct — they never get an i32 shadow, because an Add write is not strictly-i32-bounded. Only the locals that copy them wrap.
Why
is_strictly_i32_bounded_expr (crates/perry-codegen/src/collectors/i32_locals.rs) answers its LocalGet arm from integer_locals:
That conflates two different properties. integer_locals means integer-valued (no fraction, no NaN) — it deliberately admits overflowing arithmetic; is_int32_producing_expr's own doc says so:
Add / Sub / Mul when both operands are int-producing. The sum/product may overflow i32, but the existing i32-slot machinery already accepts this risk […]
So t = big2 is judged a strictly-i32-bounded write, t gets an i32 shadow at its Let site, and the store truncates. Reads prefer the shadow (issue #48), so the wrapped value is what console.log sees.
#6319 closed the literal-magnitude hole (let x = 3000000000) by narrowing every Expr::Integer(_) judgment to literals that actually fit in i32. It does not close this one: the value here is produced by Add, not by a literal.
Fix sketch
Make the LocalGet / Update arms consult a set of locals proven i32-ranged, not merely integer-valued. is_strictly_i32_bounded_expr is a shallow match (it never recurses) and is monotone in its oracle, so a greatest fixpoint over collect_strictly_i32_bounded_locals is exact and terminating: start the oracle at integer_locals, disqualify any local with a non-strict write, and propagate through the copy edges until stable.
Locals that lose the shadow fall back to the f64 slot — correct, marginally slower. Index-used locals keep the shadow through index_used_locals, which is an independent term in needs_i32_slot, so the numeric array-index fast path (#6299/#6312) and the loop counter (#6318) are unaffected.
Wants its own PR with the compiler-output-regression gate run: strictly_i32_bounded_locals is what recovers image_convolution's FNV-1a h accumulator, and that suite carries runtime budgets.
Confidence: high (probe + source). Reproduced against origin/main and against the #6319 branch.
Repro confirmed against fix/6319-i32-literal-magnitude-wrap (#6340): #6319's literal-magnitude fix closes the let x = 3000000000 face but leaves this one — add-chain copy still prints -294967296 and acc copy still prints 705032704 with that branch applied. Distinct root cause, as described above.
Activity
proggeramlug commentedon Jul 13, 2026
Repro confirmed against
fix/6319-i32-literal-magnitude-wrap(#6340): #6319's literal-magnitude fix closes thelet x = 3000000000face but leaves this one —add-chain copystill prints-294967296andacc copystill prints705032704with that branch applied. Distinct root cause, as described above.fix(codegen): i32 fast path must prove a copied local is i32-RANGED, …
fix(codegen): i32 fast path must prove a copied local is i32-RANGED, …