jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -43,12 +43,15 @@
43
43
  * @module src/infer
44
44
  */
45
45
 
46
- import { ctx } from './ctx.js'
47
- import {
48
- VAL, collectParamNames, valTypeOf,
49
- updateRep, updateGlobalRep, analyzeValTypes, analyzeIntCertain,
50
- staticObjectProps, typedElemCtor, ctorFromElemAux, shapeOfObjectLiteralAst,
51
- } from './analyze.js'
46
+ import { ctx } from '../ctx.js'
47
+ import { collectParamNames, ASSIGN_OPS } from '../ast.js'
48
+ import { analyzeValTypes, analyzeIntCertain } from './analyze.js'
49
+ import { staticObjectProps, staticArrayElems } from '../static.js'
50
+ import { typedElemCtor } from '../type.js'
51
+ import { ctorFromElemAux } from '../../layout.js'
52
+ import { shapeOfObjectLiteralAst, valTypeOf } from '../kind.js'
53
+ import { includeForStringValue } from '../autoload.js'
54
+ import { VAL, updateRep, updateGlobalRep } from '../reps.js'
52
55
 
53
56
  // === typeof predicate helper ==============================================
54
57
  //
@@ -63,7 +66,7 @@ import {
63
66
  /** Match a `typeof name <op> lit` predicate. Returns `{ name, code, eq }` —
64
67
  * `name` is the typeof's operand binding, `code` is either the raw type
65
68
  * string ('string'|'number'|'function'|…) or the prepare-normalized typeof
66
- * code (-1|-2|-3|-4|-5|-6, see TYPEOF_MAP in prepare.js), and `eq` is true
69
+ * code (TYPEOF in ast.js), and `eq` is true
67
70
  * for `==`/`===` (false for `!=`/`!==`). Returns null when the node isn't a
68
71
  * typeof predicate. */
69
72
  export function typeofPredicate(node) {
@@ -80,91 +83,10 @@ export function typeofPredicate(node) {
80
83
  return { name: typeofSide[1], code, eq: op === '==' || op === '===' }
81
84
  }
82
85
 
83
- // === paramReps lattice primitives ==========================================
86
+ // === paramReps lattice =====================================================
84
87
  //
85
- // `paramReps: Map<funcName, Map<paramIdx, ValueRep>>` is the cross-call fact
86
- // lattice. Evidence sources (body-walk and call-site `infer*`) produce
87
- // raw observations; these primitives apply them with sticky-null poison
88
- // semantics: undefined → observe (set); equal → stay; disagreement → null
89
- // (sticky). null is "no consensus" — readers treat it as missing.
90
- //
91
- // Lifecycle phases (chronological — readers should know which fields are
92
- // valid at which point):
93
- //
94
- // 1. prepare ─ no paramReps yet. AST walk collects callSites with raw
95
- // arg lists; programFacts.paramReps is allocated empty.
96
- //
97
- // 2. collectProgramFacts ─ unchanged paramReps; aggregates module-global
98
- // callSites, valueUsed, hasSchemaLiterals into
99
- // programFacts. paramReps still empty here.
100
- //
101
- // 3. narrowSignatures phase D (call-site lattice) ─
102
- // runCallsiteLattice merges raw call-site facts via
103
- // mergeParamFact for fields: val, schemaId, intConst,
104
- // arrayElemSchema, arrayElemValType, typedCtor, wasm.
105
- // After D, these may be undefined/null/value.
106
- // Sticky-null reachable on any field whose call sites
107
- // disagreed.
108
- //
109
- // 4. validateIntConstParams ─ clears intConst on any param whose body
110
- // contains a write to it (intConst's contract is "no
111
- // writes after the call").
112
- //
113
- // 5. phase E / E2 / E3 (param ABI narrowing) ─
114
- // applyI32ParamSpecialization sets sig.params[k].type
115
- // to 'i32' based on rep.wasm consensus. Then
116
- // applyPointerParamAbi sets rep.ptrKind on
117
- // consistently-OBJECT/ARRAY/etc params; this prepares
118
- // the i32-offset ABI for unboxed pointer params.
119
- //
120
- // 6. phase F (signature fixpoint) ─ clearStickyNull on val + schemaId,
121
- // then re-runs D until stable. New evidence from
122
- // newly-narrowed return types (valResult) can unstick.
123
- //
124
- // 7. phase G (narrowReturnArrayElems) ─ propagates Array<T> element
125
- // facts back through return paths into caller param
126
- // reps. After G, arrayElemSchema/arrayElemValType
127
- // reflect the transitive closure.
128
- //
129
- // 8. phase H (applyTypedPointerParamAbi) ─ sets ptrKind=TYPED + ptrAux
130
- // (elem code) for params whose val converged to TYPED.
131
- // Depends on F having seeded val first.
132
- //
133
- // 9. phase I (resetParamWasmFacts + final i32 spec) ─ last WASM-level
134
- // pass. After H/I, sig.params[k].type and rep.ptrKind
135
- // are frozen for emit.
136
- //
137
- // 10. per-function compile (emit start) ─ each func reads its
138
- // paramReps[name][k] and folds the consensus facts
139
- // into ctx.func.localReps via updateRep. Locals and
140
- // params then share one ValueRep store for the
141
- // remainder of emit.
142
-
143
- /** Per-call-site fact merge into a param's ValueRep field. */
144
- export const mergeParamFact = (rep, key, observed) => {
145
- if (rep[key] === null) return // sticky poison
146
- if (observed == null) { rep[key] = null; return } // unknown → poison
147
- if (rep[key] === undefined) rep[key] = observed
148
- else if (rep[key] !== observed) rep[key] = null
149
- }
150
-
151
- /** Get-or-create per-param rep at (funcName, paramIdx) on a paramReps map. */
152
- export const ensureParamRep = (paramReps, funcName, k) => {
153
- let m = paramReps.get(funcName)
154
- if (!m) { m = new Map(); paramReps.set(funcName, m) }
155
- let r = m.get(k)
156
- if (!r) { r = {}; m.set(k, r) }
157
- return r
158
- }
159
-
160
- /** Reset sticky-null on a single field across all params program-wide.
161
- * Used between fixpoint phases when newly-narrowed facts unblock previously-
162
- * poisoned observations (e.g. valResult set after first pass). */
163
- export const clearStickyNull = (paramReps, key) => {
164
- for (const m of paramReps.values()) for (const r of m.values()) {
165
- if (r[key] === null) r[key] = undefined
166
- }
167
- }
88
+ // Primitives live in src/param-reps.js (cycle-free leaf). Lifecycle phases
89
+ // below document when each field is valid during narrowSignatures.
168
90
 
169
91
  // === Source registry =======================================================
170
92
 
@@ -212,7 +134,7 @@ const STRING_ONLY_METHODS = new Set([
212
134
  'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
213
135
  'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
214
136
  'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
215
- 'matchAll', 'match', 'replace', 'replaceAll', 'split',
137
+ 'matchAll', 'match', 'replace', 'replaceAll', 'split', 'lastIndexOf',
216
138
  ])
217
139
  const ARRAY_ONLY_POISON = new Set([
218
140
  'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
@@ -312,9 +234,6 @@ const isLengthAccess = (n) =>
312
234
  const isIndexAccess = (n) =>
313
235
  Array.isArray(n) && n[0] === '[]' && typeof n[1] === 'string'
314
236
 
315
- const isAssignOp = (op) =>
316
- typeof op === 'string' && (op === '=' || (op.length > 1 && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
317
-
318
237
  const isStringLiteralRhs = (rhs) =>
319
238
  Array.isArray(rhs) && (rhs[0] === 'str' || (rhs[0] == null && typeof rhs[1] === 'string'))
320
239
 
@@ -354,11 +273,11 @@ const notStringEvidence = (body, names) => {
354
273
  markStringy(node[1])
355
274
  }
356
275
  // Index write: `xs[i] = v` or compound `xs[i] op= v`.
357
- if (isAssignOp(op) && isIndexAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
276
+ if (ASSIGN_OPS.has(op) && isIndexAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
358
277
  writes.add(node[1][1])
359
278
  }
360
279
  // Length mutation: `xs.length = n`, `xs.length += k`, `xs.length++`.
361
- if (isAssignOp(op) && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
280
+ if (ASSIGN_OPS.has(op) && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
362
281
  writes.add(node[1][1])
363
282
  }
364
283
  if ((op === '++' || op === '--') && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
@@ -422,11 +341,56 @@ export function recordGlobalRep(name, expr) {
422
341
  }
423
342
  const ctor = typedElemCtor(expr)
424
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
+ }
425
379
  // Static-shape capture for module-level object literals — lets `{ ...G.path }`
426
380
  // resolve its source schema by walking the global rep's shape tree at the
427
381
  // spread site (see shape walk in analyze.js / resolveSchema in object.js).
428
382
  const sh = shapeOfObjectLiteralAst(expr)
429
- if (sh) updateGlobalRep(name, { jsonShape: sh })
383
+ if (sh) {
384
+ updateGlobalRep(name, { jsonShape: sh })
385
+ // A module-global object is read via __dyn_get (its props live behind a
386
+ // runtime key, not a function-local schema slot), which emits a `['str', key]`
387
+ // node at compile time. Ensure the string module's `str` emitter is bound now
388
+ // — a string-literal-free program (`let g = {l:{a:1}}; export let f = () => g.l`)
389
+ // would otherwise abort late with "Unknown op: str". Scoped to globals that
390
+ // actually capture an object shape, so string-free local-object programs keep
391
+ // their minimal bundle (and golden-size pins).
392
+ includeForStringValue()
393
+ }
430
394
  }
431
395
 
432
396
  // === Call-site argument inference =========================================
@@ -0,0 +1,155 @@
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
+
24
+ const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
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
+ // IV a statement increments by exactly +1, or null. Covers `i++` (post-inc desugars
31
+ // to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1`.
32
+ function incVarOf(stmt) {
33
+ if (!Array.isArray(stmt)) return null
34
+ let inc = stmt
35
+ if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
36
+ if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
37
+ if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
38
+ if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
39
+ const [, a, b] = stmt[2]
40
+ if (a === stmt[1] && litN(b, 1)) return stmt[1]
41
+ if (b === stmt[1] && litN(a, 1)) return stmt[1]
42
+ }
43
+ return null
44
+ }
45
+
46
+ const usesPattern = (n, i, w, pred) => Array.isArray(n) && (pred(n, i, w) || n.some(c => usesPattern(c, i, w, pred)))
47
+ const replace = (n, i, w, cx, cy) =>
48
+ !Array.isArray(n) ? n : isMod(n, i, w) ? cx : isFloorDiv(n, i, w) ? cy : n.map(c => replace(c, i, w, cx, cy))
49
+ // a `continue` that targets THIS loop (not one nested inside) — would skip the increment
50
+ const hasOuterContinue = (n) => Array.isArray(n) &&
51
+ (n[0] === 'continue' || (n[0] !== 'while' && n[0] !== 'for' && n[0] !== 'do' && n[0] !== '=>' && n.some(hasOuterContinue)))
52
+ // Vars assigned anywhere inside a closure in the function. Such a var can be
53
+ // mutated by a call in the loop body (the closure may be defined outside the loop,
54
+ // so a body-local findMutations misses it), so it is not safe as the IV or divisor.
55
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??='])
56
+ const collectAssigns = (n, out) => {
57
+ if (!Array.isArray(n)) return
58
+ if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
59
+ n.forEach(c => collectAssigns(c, out))
60
+ }
61
+ const closureMutated = (n, out) => {
62
+ if (!Array.isArray(n)) return out
63
+ if (n[0] === '=>') collectAssigns(n, out)
64
+ n.forEach(c => closureMutated(c, out))
65
+ return out
66
+ }
67
+
68
+ let _uniq = 0
69
+ let _cm = new Set() // closure-mutated vars for the function currently being transformed
70
+
71
+ // Try to strength-reduce one `while` statement. Returns [seed, loop] or null.
72
+ function tryReduce(stmt) {
73
+ if (!Array.isArray(stmt) || stmt[0] !== 'while') return null
74
+ const cond = stmt[1], lbody = stmt[2]
75
+ if (!Array.isArray(lbody) || lbody[0] !== ';') return null
76
+
77
+ // exactly one IV incremented by +1
78
+ let iv = null, ivIdx = -1
79
+ for (let k = 1; k < lbody.length; k++) {
80
+ const v = incVarOf(lbody[k])
81
+ if (v) { if (iv) return null; iv = v; ivIdx = k }
82
+ }
83
+ if (!iv) return null
84
+
85
+ // a single invariant divisor `w` (`i % w` / `(i/w)|0` all share it)
86
+ let w = null
87
+ const findW = (n) => {
88
+ if (!Array.isArray(n)) return
89
+ const d = n[0] === '%' && n[1] === iv ? n[2]
90
+ : (n[0] === '|' && litN(n[2], 0) && Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === iv) ? n[1][2] : null
91
+ if (typeof d === 'string') { if (w == null) w = d; else if (w !== d) w = false }
92
+ n.forEach(findW)
93
+ }
94
+ findW(lbody)
95
+ if (!w || w === iv) return null
96
+
97
+ const usesMod = usesPattern(lbody, iv, w, isMod)
98
+ const usesDiv = usesPattern(lbody, iv, w, isFloorDiv)
99
+ if (!usesMod && !usesDiv) return null
100
+
101
+ // soundness: w invariant, iv written ONLY by the increment, no continue/closure capture
102
+ const wMut = new Set(); findMutations(lbody, new Set([w]), wMut)
103
+ if (wMut.has(w)) return null
104
+ const ivMut = new Set()
105
+ findMutations([';', ...lbody.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
106
+ if (ivMut.has(iv)) return null
107
+ if (hasOuterContinue(lbody)) return null
108
+ if (_cm.has(iv) || _cm.has(w)) return null // IV/divisor mutable via a closure call
109
+
110
+ const id = _uniq++
111
+ const cx = `__lsrx${id}`, cy = `__lsry${id}`
112
+ // seed (inside the w>0 branch): cx = (i%w)|0, cy = (i/w)|0 — one-time, i32 via |0
113
+ const seedDecls = [['=', cx, ['|', ['%', iv, w], 0]]]
114
+ if (usesDiv) seedDecls.push(['=', cy, ['|', ['/', iv, w], 0]])
115
+ const seed = ['let', ...seedDecls]
116
+
117
+ // transformed body: replace patterns, inject `cx++; if(cx>=w){cx=0; cy++}` after the increment
118
+ const newBody = [';']
119
+ for (let k = 1; k < lbody.length; k++) {
120
+ newBody.push(replace(lbody[k], iv, w, cx, cy))
121
+ if (k === ivIdx) {
122
+ newBody.push(['=', cx, ['+', cx, 1]])
123
+ if (usesDiv) newBody.push(['=', cy, ['+', cy, ['?:', ['>=', cx, w], 1, 0]]])
124
+ newBody.push(['=', cx, ['?:', ['>=', cx, w], 0, cx]])
125
+ }
126
+ }
127
+ const fast = ['while', replace(cond, iv, w, cx, cy), newBody]
128
+ // The counters track i%w only when w>0 AND i>=0: w<=0 gives NaN / a negative-divisor
129
+ // modulo, and i<0 makes i%w negative (JS modulo takes the dividend's sign) — neither
130
+ // follows the 0..w-1 +1-wrap the counters model. A one-time `w>0 && i>=0` guard (i is
131
+ // the IV's entry value; it only increments, so it stays ≥0) keeps the fast i32 path
132
+ // for the universal positive-dimension, forward-counting case and falls back to the
133
+ // unmodified loop otherwise — sound for any w, any start.
134
+ return [['if', ['&&', ['>', w, 0], ['>=', iv, 0]], ['{}', [';', seed, fast]], stmt]]
135
+ }
136
+
137
+ // Walk the body; transform `while` loops inside every block (post-order so a nested
138
+ // loop is reduced before its enclosing one is examined).
139
+ function walk(node) {
140
+ if (!Array.isArray(node)) return node
141
+ const n = node.map(walk)
142
+ if (n[0] !== ';') return n
143
+ const out = [';']
144
+ for (let k = 1; k < n.length; k++) {
145
+ const r = tryReduce(n[k])
146
+ if (r) out.push(...r)
147
+ else out.push(n[k])
148
+ }
149
+ return out
150
+ }
151
+
152
+ export function strengthReduceLoopDivMod(body) {
153
+ _cm = closureMutated(body, new Set())
154
+ return walk(body)
155
+ }