Severity: P1 (silent data corruption; potential infinite loop)
Found by: audit fable-audit-perry-2.md, confirmed by probe.
What
Locals and loop counters that codegen classifies into i32 slots do a plain add i32 with no overflow handling, so they wrap at 2³¹ instead of promoting to double.
let big = 2147483640;
for (let k = 0; k < 10; k++) big++;
console.log(big); // node: 2147483650 perry: -2147483646
// runtime-bound loop crossing 2^31:
for (let i = 2147483640; i < lim; i++) { ... } // lim = 2147483653
// node: 13 iterations perry: i wraps negative past 2^31 → spins (only a safety break stopped the probe)
Why
crates/perry-codegen/src/expr/literals_vars.rs:796 (let new_i32 = blk.add(I32, &old_i32, delta);) and the raw add(I32, …) sites in expr/i32_fast_path.rs:635,994 and stmt/loops.rs:912,1980 emit unchecked i32 adds. The range guards validate the initializer, not that increments stay below 2³¹ when the bound is a runtime value.
Fix
Emit llvm.sadd.with.overflow.i32 on the update path and bail to the double representation on overflow, or gate the i32 slot on a range_facts proof that the loop/accumulator bound is provably < 2^31.
Confidence: high (probe + source).
Severity: P1 (silent data corruption; potential infinite loop)
Found by: audit
fable-audit-perry-2.md, confirmed by probe.What
Locals and loop counters that codegen classifies into i32 slots do a plain
add i32with no overflow handling, so they wrap at 2³¹ instead of promoting to double.Why
crates/perry-codegen/src/expr/literals_vars.rs:796(let new_i32 = blk.add(I32, &old_i32, delta);) and the rawadd(I32, …)sites inexpr/i32_fast_path.rs:635,994andstmt/loops.rs:912,1980emit unchecked i32 adds. The range guards validate the initializer, not that increments stay below 2³¹ when the bound is a runtime value.Fix
Emit
llvm.sadd.with.overflow.i32on the update path and bail to the double representation on overflow, or gate the i32 slot on arange_factsproof that the loop/accumulator bound is provably< 2^31.Confidence: high (probe + source).