Skip to content

i32 fast path: copying a local whose value came from an arithmetic chain past 2^31 wraps (fourth face of #6072) #6339

Description

@proggeramlug

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.

let big1 = 2000000000;
let big2 = big1 + big1;   // 4e9 — correct in its own slot
let t = 0;
t = big2;
console.log(t);           // node: 4000000000   perry: -294967296

let sum = 0;
for (let i = 0; i < 5; i++) sum = sum + 1000000000;
let sumCopy = 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:

Expr::LocalGet(id) => known_int_locals.contains(id),

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions