Severity: P1 (silent data corruption)
Found while fixing: #6072 repro 2 (PR #6318)
What
An integer literal beyond i32 range still seeds integer_locals, so a plain copy of such a local is judged "strictly i32-bounded" and gets a wrapping i32 shadow:
let x = 3000000000;
let y = 0;
y = x;
console.log("y:", y); // node: 3000000000 perry: -1294967296
let z = 0;
z = x + 4;
console.log("z:", z); // node: 3000000004 perry: 3000000004 (correct — Add isn't strict)
Reproduces on main (v0.5.1258, 0960415ad) and is unaffected by #6258 and #6318 — it is a third, distinct face of #6072's title ("i32 fast-path locals silently wrap at 2^31"), reached through the copy path rather than through x++ (#6258) or a dynamic loop bound (#6318).
Why
collect_integer_let_ids (crates/perry-codegen/src/collectors/i32_locals.rs:615) seeds any Expr::Integer(_) init into integer_locals, regardless of magnitude. is_strictly_i32_bounded_expr's Expr::LocalGet(id) arm then answers known_int_locals.contains(id) — so y = x counts as a strictly-i32-bounded write, y gets an i32 shadow at its Let site, and the write is lowered via lower_expr_as_i32, which ToInt32-wraps the 3e9 double. Reads then prefer the shadow (issue #48), so the wrapped value is what console.log sees.
x itself stays correct: its own Let-site slot is gated on init_in_i32_range. Only locals that copy it inherit the bogus "i32-bounded" judgement.
Fix sketch
Narrow the Expr::Integer seed in collect_integer_let_ids to literals that fit in i32 (i32::try_from(*n).is_ok()). That also removes a latent poison fptosi in the arr.length-hoist and static-i < n loop paths, which seed a counter's i32 shadow from integer_locals membership alone.
Not a drive-by: integer_locals also feeds can_lower_expr_as_i32 / lower_expr_as_i32, so a const SEED = 0x9E3779B9 (2654435769 > INT32_MAX) operand would drop out of the i32 arithmetic chain that the compiler_output_regression image-convolution / FNV-1a gate watches. Wants its own PR with that gate run.
Confidence: high (probe + source).
Severity: P1 (silent data corruption)
Found while fixing: #6072 repro 2 (PR #6318)
What
An integer literal beyond i32 range still seeds
integer_locals, so a plain copy of such a local is judged "strictly i32-bounded" and gets a wrapping i32 shadow:Reproduces on
main(v0.5.1258,0960415ad) and is unaffected by #6258 and #6318 — it is a third, distinct face of #6072's title ("i32 fast-path locals silently wrap at 2^31"), reached through the copy path rather than throughx++(#6258) or a dynamic loop bound (#6318).Why
collect_integer_let_ids(crates/perry-codegen/src/collectors/i32_locals.rs:615) seeds anyExpr::Integer(_)init intointeger_locals, regardless of magnitude.is_strictly_i32_bounded_expr'sExpr::LocalGet(id)arm then answersknown_int_locals.contains(id)— soy = xcounts as a strictly-i32-bounded write,ygets an i32 shadow at itsLetsite, and the write is lowered vialower_expr_as_i32, whichToInt32-wraps the 3e9 double. Reads then prefer the shadow (issue #48), so the wrapped value is whatconsole.logsees.xitself stays correct: its ownLet-site slot is gated oninit_in_i32_range. Only locals that copy it inherit the bogus "i32-bounded" judgement.Fix sketch
Narrow the
Expr::Integerseed incollect_integer_let_idsto literals that fit in i32 (i32::try_from(*n).is_ok()). That also removes a latent poisonfptosiin thearr.length-hoist and static-i < nloop paths, which seed a counter's i32 shadow frominteger_localsmembership alone.Not a drive-by:
integer_localsalso feedscan_lower_expr_as_i32/lower_expr_as_i32, so aconst SEED = 0x9E3779B9(2654435769 > INT32_MAX) operand would drop out of the i32 arithmetic chain that thecompiler_output_regressionimage-convolution / FNV-1a gate watches. Wants its own PR with that gate run.Confidence: high (probe + source).