Summary
is_int32_producing_expr (crates/perry-codegen/src/collectors.rs:1645-1661) treats Add | Sub | Mul as int-stable when both operands are int-stable. This is wrong: signed-i32 closure under +/−/× only holds when each step's mathematical result fits in i32. needs_i32_slot then installs an i32 shadow that silently truncates 64-bit results from runtime updates.
The recent >>> 0 exclusion (commit 817c4b56) patched one instance of this class. The underlying defect — "int-int closure under +/−/× ⇒ i32-safe" — is intact and produces wrong output in idiomatic JS/TS shapes.
Concrete repros (all diff against Bun)
| # |
Pattern |
Bun |
Perry |
Severity |
| 1 |
let sum=0; for(...) sum += compute(i) (benchmarks/suite/14_closure.ts) |
2,500,000,000,000,000 |
-1,678,753,792 |
HIGH |
| 2 |
const big = a*b both int-stable, product > i32 |
10,000,000,000 |
1,410,065,408 |
HIGH |
| 3 |
const big = 100000*100000 (literal × literal) |
10,000,000,000 |
1,410,065,408 |
HIGH |
| 4 |
let acc=0; for(...) acc -= 100 past -2³¹ |
-5,000,000,000 |
-705,032,704 |
HIGH |
| 5 |
let prod=1; for(i<=15) prod *= i (factorial) |
1,307,674,368,000 |
2,004,310,016 |
HIGH |
| 6 |
const big = a+b both int-stable, sum > i32 |
4,000,000,000 |
-294,967,296 |
HIGH |
| 7 |
let sum: number = 0; ... += (TS-typed number ignored) |
2,500,000,000,000,000 |
-1,678,753,792 |
HIGH |
| 8 |
const c = b + b via forward-closure pass |
4,000,000,000 |
-294,967,296 |
HIGH |
| 9 |
i = i + 100 strided counter past i32 |
4,000,000,000 |
-294,967,296 |
HIGH |
| 10 |
i++ past i32 max |
2,147,483,740 |
-2,147,483,556 |
MED |
Repro for Bug #1
bun benchmarks/suite/14_closure.ts
# sum:2500000000000000
target/release/perry compile benchmarks/suite/14_closure.ts -o /tmp/c14 && /tmp/c14
# sum:-1678753792 ← WRONG
Confirmed safe paths (no fix needed)
Math.imul — JS spec mandates i32 truncation; perry matches Bun.
(expr) | 0 — JS ToInt32 produces signed i32.
- Single Integer literal init guarded by
i32::try_from(*n).is_ok().
(expr) >>> 0 — excluded after 817c4b56.
returns_int_expr post-817c4b56 — only accepts Integer + signed-spec bitwise + Math.imul.
Affected sites
crates/perry-codegen/src/collectors.rs:1645-1661 — Add/Sub/Mul arm of is_int32_producing_expr (root cause).
crates/perry-codegen/src/collectors.rs:1762 — Integer-literal seed feeding mutable accumulators.
crates/perry-codegen/src/collectors.rs:1494-1525 — collect_extra_integer_let_ids forward-closure pass.
crates/perry-codegen/src/stmt.rs:532-541 — needs_i32_slot should consult HIR Number type for explicit annotations.
Proposed fix shape
One of:
- Reject Add/Sub/Mul in
is_int32_producing_expr unless operands are statically bounded.
- Maintain f64 as the read source whenever any Add/Sub/Mul write could overflow.
- Honor
: number type annotations as opting OUT of int-shadow at the seed site.
Found during the May-2026 release-readiness sweep, after fixing the related >>> 0 bug class (817c4b56). 14_closure is the canonical case; the same shape appears anywhere TS code computes a large total via +=.
Summary
is_int32_producing_expr(crates/perry-codegen/src/collectors.rs:1645-1661) treatsAdd | Sub | Mulas int-stable when both operands are int-stable. This is wrong: signed-i32 closure under+/−/×only holds when each step's mathematical result fits in i32.needs_i32_slotthen installs an i32 shadow that silently truncates 64-bit results from runtime updates.The recent
>>> 0exclusion (commit817c4b56) patched one instance of this class. The underlying defect — "int-int closure under +/−/× ⇒ i32-safe" — is intact and produces wrong output in idiomatic JS/TS shapes.Concrete repros (all diff against Bun)
let sum=0; for(...) sum += compute(i)(benchmarks/suite/14_closure.ts)const big = a*bboth int-stable, product > i32const big = 100000*100000(literal × literal)let acc=0; for(...) acc -= 100past -2³¹let prod=1; for(i<=15) prod *= i(factorial)const big = a+bboth int-stable, sum > i32let sum: number = 0; ... +=(TS-typednumberignored)const c = b + bvia forward-closure passi = i + 100strided counter past i32i++past i32 maxRepro for Bug #1
Confirmed safe paths (no fix needed)
Math.imul— JS spec mandates i32 truncation; perry matches Bun.(expr) | 0— JSToInt32produces signed i32.i32::try_from(*n).is_ok().(expr) >>> 0— excluded after817c4b56.returns_int_exprpost-817c4b56— only accepts Integer + signed-spec bitwise + Math.imul.Affected sites
crates/perry-codegen/src/collectors.rs:1645-1661— Add/Sub/Mul arm ofis_int32_producing_expr(root cause).crates/perry-codegen/src/collectors.rs:1762— Integer-literal seed feeding mutable accumulators.crates/perry-codegen/src/collectors.rs:1494-1525—collect_extra_integer_let_idsforward-closure pass.crates/perry-codegen/src/stmt.rs:532-541—needs_i32_slotshould consult HIRNumbertype for explicit annotations.Proposed fix shape
One of:
is_int32_producing_exprunless operands are statically bounded.: numbertype annotations as opting OUT of int-shadow at the seed site.Found during the May-2026 release-readiness sweep, after fixing the related
>>> 0bug class (817c4b56). 14_closure is the canonical case; the same shape appears anywhere TS code computes a large total via+=.