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

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions