jz 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -46,7 +46,7 @@
46
46
  import { ctx } from '../ctx.js'
47
47
  import { collectParamNames, ASSIGN_OPS } from '../ast.js'
48
48
  import { analyzeValTypes, analyzeIntCertain } from './analyze.js'
49
- import { staticObjectProps } from '../static.js'
49
+ import { staticObjectProps, staticArrayElems } from '../static.js'
50
50
  import { typedElemCtor } from '../type.js'
51
51
  import { ctorFromElemAux } from '../../layout.js'
52
52
  import { shapeOfObjectLiteralAst, valTypeOf } from '../kind.js'
@@ -341,6 +341,41 @@ export function recordGlobalRep(name, expr) {
341
341
  }
342
342
  const ctor = typedElemCtor(expr)
343
343
  if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
344
+ // Module-level const array literal with a uniform element val-type (e.g. a numeric
345
+ // table `const FREQS = [261.63, …]`): record it so `FREQS[i]` reads in any using
346
+ // function are typed (NUMBER) rather than untyped. Without this an untyped element
347
+ // read makes `s += FREQS[i]` take the polymorphic +/ToString path — dragging the
348
+ // entire string runtime (~5 kB) into a kernel that uses no strings. A function-local
349
+ // array gets this from analyzeValTypes; a module-level one is invisible to the using
350
+ // function's body walk, so capture it here. Soundness for a later `FREQS[i]=…` is the
351
+ // read-site dynWriteVars guard in valTypeOf (kind.js) — this is just the literal fact.
352
+ if (vt === VAL.ARRAY) {
353
+ const elems = staticArrayElems(expr)
354
+ if (elems && elems.length && elems.every(e => e != null)) {
355
+ let common = valTypeOf(elems[0])
356
+ for (let k = 1; k < elems.length && common != null; k++)
357
+ if (valTypeOf(elems[k]) !== common) common = null
358
+ if (common != null) updateGlobalRep(name, { arrayElemValType: common })
359
+ // Array-of-arrays numeric table (`const C = [[0,4,7], …]`): also record the
360
+ // nested element kind so `C[i][j]` (and `ch = C[i]; ch[j]`) reads stay typed —
361
+ // the same string-runtime drop as the flat case, one level down. Single-level
362
+ // (mirrors analyzeValTypes' local arrElemElemValTypes); deeper nesting falls back.
363
+ if (common === VAL.ARRAY) {
364
+ let nested = null, seen = false, ok = true
365
+ for (const el of elems) {
366
+ const inner = staticArrayElems(el)
367
+ if (!inner || !inner.length || !inner.every(e => e != null)) { ok = false; break }
368
+ for (const ie of inner) {
369
+ const ivt = valTypeOf(ie)
370
+ if (!seen) { nested = ivt; seen = true }
371
+ else if (ivt !== nested) { ok = false; break }
372
+ }
373
+ if (!ok) break
374
+ }
375
+ if (ok && nested != null) updateGlobalRep(name, { arrayElemElemValType: nested })
376
+ }
377
+ }
378
+ }
344
379
  // Static-shape capture for module-level object literals — lets `{ ...G.path }`
345
380
  // resolve its source schema by walking the global rep's shape tree at the
346
381
  // spread site (see shape walk in analyze.js / resolveSchema in object.js).
@@ -0,0 +1,109 @@
1
+ // Loop induction strength-reduction for `i % w` and `(i / w) | 0`.
2
+ //
3
+ // JS `%` and `/` are float ops; `i % w` with a RUNTIME divisor lowers to f64
4
+ // (i % 0 is NaN, so it can't be soundly typed i32 — see type.js exprType `%`).
5
+ // That makes the column `x = i % w` an f64 local, which cascades: every dependent
6
+ // neighbour index becomes f64 and each typed-array access pays an f64→i32 convert.
7
+ // Measured ~5× slower than V8, which speculates the divisor non-zero and uses i32.
8
+ //
9
+ // This pass replaces the per-iteration division with incremental i32 counters in a
10
+ // unit-stride loop: the column `cx` increments by 1 and wraps to 0 at `w`, bumping
11
+ // the row `cy`. No division survives in the body, so the whole index chain stays
12
+ // i32. Fully sound — counters are pure increment (the divisor-zero question never
13
+ // arises), and if `w == 0` the loop's `i < w*h` guard never admits an iteration, so
14
+ // the one-time seed `(i%w)|0` (NaN→0) is unused.
15
+ //
16
+ // Recognized (post-prepare AST): a `while` whose body increments one IV `i` by +1
17
+ // exactly once, reads `['%', i, w]` and/or `['|', ['/', i, w], LIT0]` with `w`
18
+ // loop-invariant, and has no `continue` / closure-capture of i,w (which could
19
+ // desync the counters). Number literals are sparse-array holes `[, v]` (n[0] is the
20
+ // hole = undefined), so literal tests use `== null`; created literals are bare numbers.
21
+
22
+ import { findMutations } from './analyze-scans.js'
23
+ import { litN, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks, freshLoopId } from './loop-model.js'
24
+
25
+ const isMod = (n, i, w) => Array.isArray(n) && n[0] === '%' && n[1] === i && n[2] === w
26
+ const isFloorDiv = (n, i, w) =>
27
+ Array.isArray(n) && n[0] === '|' && litN(n[2], 0) &&
28
+ Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === i && n[1][2] === w
29
+
30
+ const usesPattern = (n, i, w, pred) => Array.isArray(n) && (pred(n, i, w) || n.some(c => usesPattern(c, i, w, pred)))
31
+ const replace = (n, i, w, cx, cy) =>
32
+ !Array.isArray(n) ? n : isMod(n, i, w) ? cx : isFloorDiv(n, i, w) ? cy : n.map(c => replace(c, i, w, cx, cy))
33
+ // a `continue` that targets THIS loop (not one nested inside) — would skip the increment
34
+ const hasOuterContinue = (n) => Array.isArray(n) &&
35
+ (n[0] === 'continue' || (n[0] !== 'while' && n[0] !== 'for' && n[0] !== 'do' && n[0] !== '=>' && n.some(hasOuterContinue)))
36
+
37
+
38
+ // Try to strength-reduce one `while` statement. Returns [seed, loop] or null. `cm` is
39
+ // the function's closure-mutated-vars set (an IV/divisor in it is unsafe).
40
+ function tryReduce(stmt, cm) {
41
+ const L = normalizeLoop(stmt)
42
+ if (!L || L.kind !== 'while') return null
43
+ const cond = L.cond, lbody = L.body
44
+ if (!Array.isArray(lbody) || lbody[0] !== ';') return null
45
+
46
+ // exactly one IV incremented by +1
47
+ let iv = null, ivIdx = -1
48
+ for (let k = 1; k < lbody.length; k++) {
49
+ const v = unitIncVar(lbody[k])
50
+ if (v) { if (iv) return null; iv = v; ivIdx = k }
51
+ }
52
+ if (!iv) return null
53
+
54
+ // a single invariant divisor `w` (`i % w` / `(i/w)|0` all share it)
55
+ let w = null
56
+ const findW = (n) => {
57
+ if (!Array.isArray(n)) return
58
+ const d = n[0] === '%' && n[1] === iv ? n[2]
59
+ : (n[0] === '|' && litN(n[2], 0) && Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === iv) ? n[1][2] : null
60
+ if (typeof d === 'string') { if (w == null) w = d; else if (w !== d) w = false }
61
+ n.forEach(findW)
62
+ }
63
+ findW(lbody)
64
+ if (!w || w === iv) return null
65
+
66
+ const usesMod = usesPattern(lbody, iv, w, isMod)
67
+ const usesDiv = usesPattern(lbody, iv, w, isFloorDiv)
68
+ if (!usesMod && !usesDiv) return null
69
+
70
+ // soundness: w invariant, iv written ONLY by the increment, no continue/closure capture
71
+ const wMut = new Set(); findMutations(lbody, new Set([w]), wMut)
72
+ if (wMut.has(w)) return null
73
+ const ivMut = new Set()
74
+ findMutations([';', ...lbody.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
75
+ if (ivMut.has(iv)) return null
76
+ if (hasOuterContinue(lbody)) return null
77
+ if (cm.has(iv) || cm.has(w)) return null // IV/divisor mutable via a closure call
78
+
79
+ const id = freshLoopId()
80
+ const cx = `__lsrx${id}`, cy = `__lsry${id}`
81
+ // seed (inside the w>0 branch): cx = (i%w)|0, cy = (i/w)|0 — one-time, i32 via |0
82
+ const seedDecls = [['=', cx, ['|', ['%', iv, w], 0]]]
83
+ if (usesDiv) seedDecls.push(['=', cy, ['|', ['/', iv, w], 0]])
84
+ const seed = ['let', ...seedDecls]
85
+
86
+ // transformed body: replace patterns, inject `cx++; if(cx>=w){cx=0; cy++}` after the increment
87
+ const newBody = [';']
88
+ for (let k = 1; k < lbody.length; k++) {
89
+ newBody.push(replace(lbody[k], iv, w, cx, cy))
90
+ if (k === ivIdx) {
91
+ newBody.push(['=', cx, ['+', cx, 1]])
92
+ if (usesDiv) newBody.push(['=', cy, ['+', cy, ['?:', ['>=', cx, w], 1, 0]]])
93
+ newBody.push(['=', cx, ['?:', ['>=', cx, w], 0, cx]])
94
+ }
95
+ }
96
+ const fast = ['while', replace(cond, iv, w, cx, cy), newBody]
97
+ // The counters track i%w only when w>0 AND i>=0: w<=0 gives NaN / a negative-divisor
98
+ // modulo, and i<0 makes i%w negative (JS modulo takes the dividend's sign) — neither
99
+ // follows the 0..w-1 +1-wrap the counters model. A one-time `w>0 && i>=0` guard (i is
100
+ // the IV's entry value; it only increments, so it stays ≥0) keeps the fast i32 path
101
+ // for the universal positive-dimension, forward-counting case and falls back to the
102
+ // unmodified loop otherwise — sound for any w, any start.
103
+ return [['if', ['&&', ['>', w, 0], ['>=', iv, 0]], ['{}', [';', seed, fast]], stmt]]
104
+ }
105
+
106
+ export function strengthReduceLoopDivMod(body) {
107
+ const cm = closureMutatedVars(body)
108
+ return rewriteBlocks(body, stmt => tryReduce(stmt, cm))
109
+ }
@@ -0,0 +1,91 @@
1
+ // Shared AST-level loop primitives for the per-function loop transforms
2
+ // (loop-divmod, loop-square, peel-stencil). Operates on the post-prepare AST.
3
+ //
4
+ // Each of those passes recognizes one narrow loop idiom and rewrites it, but they had
5
+ // re-derived the same building blocks verbatim: the `[, v]` number-literal-hole
6
+ // recognizer, the "+1 induction variable" matcher, the closure-mutated-variable
7
+ // analysis (a var assigned inside any `=>` may be mutated by a call in the loop, so it
8
+ // is unsafe as an IV/bound/divisor), the while/for structural normalizer, and the
9
+ // post-order block walk that applies a per-statement rewrite. One home for all of them
10
+ // — adding the next loop transform is then one recognizer over these, not a fourth copy.
11
+
12
+ import { ASSIGN_OPS } from '../ast.js'
13
+ import { ctx } from '../ctx.js'
14
+
15
+ // Fresh id for a loop transform's generated locals (`__lsrx<id>`, `__pks<id>`, …). Backed by
16
+ // a per-compile counter (reset in ctx.reset), NOT a module-global: a module-`let` counter
17
+ // grows unbounded across a long-lived host and makes compile(P) non-deterministic — its output
18
+ // names depend on how many programs were compiled before it. Distinct prefixes keep the shared
19
+ // id space collision-free across transforms.
20
+ export const freshLoopId = () => ctx.transform.loopXformId++
21
+
22
+ // Post-prepare number literals are sparse-array holes `[<hole>, v]` (length 2, the op
23
+ // slot `n[0]` is the elided hole == null). `litVal` returns the numeric value or null;
24
+ // `litN(n, k)` tests for the exact literal `k`.
25
+ export const litVal = (n) => Array.isArray(n) && n.length === 2 && n[0] == null && typeof n[1] === 'number' ? n[1] : null
26
+ export const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
27
+
28
+ // The induction variable a statement increments by exactly +1, else null. Covers
29
+ // `i++` (post-inc desugars to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1` / `i = 1 + i`.
30
+ export function unitIncVar(stmt) {
31
+ if (!Array.isArray(stmt)) return null
32
+ let inc = stmt
33
+ if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
34
+ if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
35
+ if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
36
+ if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
37
+ const [, a, b] = stmt[2]
38
+ if (a === stmt[1] && litN(b, 1)) return stmt[1]
39
+ if (b === stmt[1] && litN(a, 1)) return stmt[1]
40
+ }
41
+ return null
42
+ }
43
+
44
+ // Normalize a `while` / flat `for` (`['for', init, cond, update, body]`) into a common
45
+ // shape, else null. `init`/`step` are null for a while; a `for` with fewer than the 5
46
+ // flat slots (no body) is not a loop here.
47
+ export function normalizeLoop(stmt) {
48
+ if (!Array.isArray(stmt)) return null
49
+ if (stmt[0] === 'while') return { kind: 'while', init: null, cond: stmt[1], step: null, body: stmt[2] }
50
+ if (stmt[0] === 'for' && stmt.length >= 5) return { kind: 'for', init: stmt[1], cond: stmt[2], step: stmt[3], body: stmt[4] }
51
+ return null
52
+ }
53
+
54
+ // Variables assigned anywhere inside a closure (`=>`) in `body`. A call in the loop can
55
+ // mutate such a var even though a direct-write scan (findMutations) misses it (the
56
+ // closure may be defined outside the loop), so an IV / bound / divisor in this set is
57
+ // unsafe to strength-reduce. (ASSIGN_OPS plus the `++`/`--` updates.)
58
+ export function closureMutatedVars(body) {
59
+ const out = new Set()
60
+ const collect = (n) => {
61
+ if (!Array.isArray(n)) return
62
+ if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
63
+ n.forEach(collect)
64
+ }
65
+ const walk = (n) => {
66
+ if (!Array.isArray(n)) return
67
+ if (n[0] === '=>') collect(n)
68
+ n.forEach(walk)
69
+ }
70
+ walk(body)
71
+ return out
72
+ }
73
+
74
+ // Post-order block rewriter: walk `body`; for every `;`-sequence, apply `tryStmt` to
75
+ // each statement. A truthy result is an ARRAY of replacement statements spliced in
76
+ // place; a falsy result keeps the statement unchanged. Children are rewritten first, so
77
+ // a nested loop is transformed before the block that encloses it.
78
+ export function rewriteBlocks(body, tryStmt) {
79
+ const walk = (node) => {
80
+ if (!Array.isArray(node)) return node
81
+ const n = node.map(walk)
82
+ if (n[0] !== ';') return n
83
+ const out = [';']
84
+ for (let k = 1; k < n.length; k++) {
85
+ const r = tryStmt(n[k])
86
+ if (r) out.push(...r); else out.push(n[k])
87
+ }
88
+ return out
89
+ }
90
+ return walk(body)
91
+ }
@@ -0,0 +1,102 @@
1
+ // Bounded-square narrowing: carry `i*i` as i32 inside a loop guarded by `i*i < CONST`.
2
+ //
3
+ // A PRODUCT `i*i` is f64 in unhinted JS — the integer-overflow contract: a product can
4
+ // exceed 2³¹, where JS keeps a Number, so jz can't blindly use i32.mul (it would wrap).
5
+ // In a Sieve-of-Eratosthenes `for(i=2; i*i<LIMIT; i++) for(j=i*i; j<LIMIT; j+=i) …`, that
6
+ // makes the outer bound, the inner counter `j`'s init, and the whole index chain f64 —
7
+ // each typed-array access then pays an f64→i32 convert.
8
+ //
9
+ // But when the loop GUARD is `i*i < CONST` with CONST a compile-time constant ≤ 2³⁰, the
10
+ // counter is i ≤ ⌈√CONST⌉ ≤ 2¹⁵ throughout the body (the loop is still running, and i is
11
+ // only incremented by +1), so EVERY `i*i` there is < 2³⁰ < 2³¹ and `Math.imul(i,i) == i*i`
12
+ // exactly. The exit overshoot (the first i with i*i ≥ CONST, evaluated in the condition)
13
+ // is ≤ CONST + 2√CONST+1 < 2³¹ for CONST ≤ 2³⁰, so the condition narrows soundly too. The
14
+ // hard cap is 2³⁰ — well under the 2³¹ overflow point, with margin for the overshoot.
15
+ //
16
+ // So we rewrite those `i*i` to `Math.imul(i,i)` (jz's i32 multiply): semantically identical
17
+ // in the proven range, and it lets the EXISTING i32 machinery carry the index chain as i32
18
+ // — the inner `j` (init now i32, step `j+i` i32, bound `j<CONST` i32) cascades on its own.
19
+ //
20
+ // Sound iff: the loop condition is `(i*i) </≤ CONST` (CONST ≤ 2³⁰), and the IV `i` is
21
+ // incremented by +1 and NOT otherwise mutated (so within a body iteration i ∈ {entry, +1},
22
+ // both squares < 2³¹). Mirrors strength-reduce-divmod's structure (post-prepare `while`).
23
+
24
+ import { findMutations } from './analyze-scans.js'
25
+ import { includeMods } from '../autoload.js'
26
+ import { ctx } from '../ctx.js'
27
+ import { litVal, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks } from './loop-model.js'
28
+
29
+ const SQUARE_BOUND_MAX = 2 ** 30
30
+ // The constant numeric value of a bound: a literal, OR a module const folded to an int
31
+ // (`const LIMIT = 1<<20` → ctx.scope.constInts.get('LIMIT') = 1048576 — the bench form).
32
+ const boundVal = (n) => {
33
+ const lit = litVal(n)
34
+ if (lit != null) return lit
35
+ if (typeof n === 'string') { const v = ctx.scope.constInts?.get(n); return typeof v === 'number' ? v : null }
36
+ return null
37
+ }
38
+ // `i * i` — the IV squared.
39
+ const isSquare = (n, iv) => Array.isArray(n) && n[0] === '*' && n[1] === iv && n[2] === iv && typeof iv === 'string'
40
+ // Math.imul(i, i) in CANONICAL post-prepare form: prepare resolves `Math.imul` → the string
41
+ // ref `'math.imul'`, so the call is `['()', 'math.imul', [',', i, i]]`. math.imul emits a
42
+ // primitive `i32.mul` (no stdlib helper / module include needed).
43
+ const imulOf = (iv) => ['()', 'math.imul', [',', iv, iv]]
44
+
45
+ // The IV of a `(i*i) </≤ CONST` (or mirrored `CONST >/≥ (i*i)`) guard, CONST ≤ 2³⁰, else null.
46
+ function boundedSquareIV(cond) {
47
+ if (!Array.isArray(cond)) return null
48
+ let prod, bound
49
+ if (cond[0] === '<' || cond[0] === '<=') { prod = cond[1]; bound = cond[2] }
50
+ else if (cond[0] === '>' || cond[0] === '>=') { prod = cond[2]; bound = cond[1] }
51
+ else return null
52
+ if (!isSquare(prod, prod && prod[1])) return null
53
+ const b = boundVal(bound)
54
+ if (b == null || b < 0 || b > SQUARE_BOUND_MAX) return null
55
+ return prod[1]
56
+ }
57
+
58
+ // Narrow a `for`/`while` whose guard is `(i*i) </≤ CONST` (CONST ≤ 2³⁰) and whose IV `i` is
59
+ // incremented by +1 and not otherwise mutated — then within any body iteration i ∈ {entry,
60
+ // entry+1}, entry ≤ ⌈√CONST⌉ ≤ 2¹⁵, so every `i*i` (and the exit overshoot) is < 2³¹ and
61
+ // Math.imul(i,i) == i*i. Rewrites those products; the dependent counter chain cascades to i32.
62
+ // `cm` is the function's closure-mutated-vars set (an IV in it has an unprovable entry value).
63
+ function tryNarrow(stmt, cm) {
64
+ const L = normalizeLoop(stmt)
65
+ if (!L) return null
66
+ const { kind, cond, step, body } = L, isFor = kind === 'for'
67
+
68
+ const iv = boundedSquareIV(cond)
69
+ if (!iv) return null
70
+ if (cm.has(iv)) return null // mutable via a closure call — entry value unprovable
71
+
72
+ if (isFor) {
73
+ // The `for` update is the IV's sole, +1 mutation; the body must not reassign i.
74
+ if (unitIncVar(step) !== iv) return null
75
+ const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
76
+ if (ivMut.has(iv)) return null
77
+ } else {
78
+ // `while`: the increment lives in the body (exactly one +1; nothing else mutates i).
79
+ if (!Array.isArray(body) || body[0] !== ';') return null
80
+ let ivIdx = -1
81
+ for (let k = 1; k < body.length; k++) if (unitIncVar(body[k]) === iv) { if (ivIdx >= 0) return null; ivIdx = k }
82
+ if (ivIdx < 0) return null
83
+ const ivMut = new Set()
84
+ findMutations([';', ...body.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
85
+ if (ivMut.has(iv)) return null
86
+ }
87
+
88
+ // We're injecting `math.imul` after prepare's auto-import step, so ensure the math module
89
+ // (which registers the `math.imul` → i32.mul primitive emitter) is included.
90
+ includeMods('math')
91
+ // Rewrite every `i*i` in the condition + body to Math.imul(i,i) (NOT init/update — they're
92
+ // `i=2` / `i++`). The inner counter whose init this feeds cascades to i32 on its own.
93
+ const rw = (n) => !Array.isArray(n) ? n : isSquare(n, iv) ? imulOf(iv) : n.map(rw)
94
+ return [isFor
95
+ ? ['for', L.init, rw(cond), step, rw(body)]
96
+ : ['while', rw(cond), rw(body)]]
97
+ }
98
+
99
+ export function narrowBoundedSquare(body) {
100
+ const cm = closureMutatedVars(body)
101
+ return rewriteBlocks(body, stmt => tryNarrow(stmt, cm))
102
+ }