jz 0.8.0 → 0.9.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 (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
package/src/type.js CHANGED
@@ -41,6 +41,566 @@ export function typedElemCtor(rhs) {
41
41
  return isView ? rhs[1] + '.view' : rhs[1]
42
42
  }
43
43
 
44
+ /** Static element count for `new T(<int literal>)` / `new T([literals…])`, or null
45
+ * for views (buffer, off, len), buffer/array copies, ternaries and computed sizes.
46
+ * Typed arrays are FIXED-LENGTH, so a binding's length is exactly as stable as its
47
+ * ctor — the tracker applies the same multi-def invalidation to both. */
48
+ export function typedStaticLen(rhs) {
49
+ if (!Array.isArray(rhs) || rhs[0] !== '()' || typeof rhs[1] !== 'string' || !rhs[1].startsWith('new.')) return null
50
+ if (!rhs[1].endsWith('Array') || rhs[1] === 'new.ArrayBuffer') return null
51
+ const args = rhs[2]
52
+ if (args === undefined) return 0
53
+ if (Array.isArray(args) && args[0] === ',') return null // view form
54
+ const n = constIntExpr(args)
55
+ if (n != null) return n >= 0 ? n : null
56
+ // `new T([a,b,c])` — BOTH literal shapes: parse-time `['[]', [',', …]]` (the
57
+ // module-scope infer site) and post-prepare `['[', …elems]` (the analyze tracker).
58
+ if (Array.isArray(args) && args[0] === '[]' && args.length === 2) {
59
+ const inner = args[1]
60
+ return inner === undefined ? 0
61
+ : Array.isArray(inner) && inner[0] === ',' ? inner.length - 1 : 1
62
+ }
63
+ if (Array.isArray(args) && args[0] === '['
64
+ && !args.slice(1).some(e => Array.isArray(e) && e[0] === '...')) return args.length - 1
65
+ return null
66
+ }
67
+
68
+ /** Fold an int-const expression: literals, module int consts (`const N = 3`), and
69
+ * +,-,*,<< over them — `new Int8Array(CIN*H*W)` sizes are static facts. */
70
+ export function constIntExpr(e) {
71
+ const n = intLiteralValue(e)
72
+ if (n != null) return n
73
+ if (typeof e === 'string') {
74
+ const ci = ctx.scope?.constInts?.get?.(e)
75
+ return ci != null && isI32(ci) ? ci : null
76
+ }
77
+ if (!Array.isArray(e) || e.length !== 3) return null
78
+ const [op, x, y] = e
79
+ if (op !== '+' && op !== '-' && op !== '*' && op !== '<<') return null
80
+ const A = constIntExpr(x), B = constIntExpr(y)
81
+ if (A == null || B == null) return null
82
+ const r = op === '+' ? A + B : op === '-' ? A - B : op === '*' ? A * B : A * 2 ** B
83
+ return Number.isSafeInteger(r) && isI32(r) ? r : null
84
+ }
85
+
86
+ /** `recv[idx]` provably within [0, recv.length) for a typed receiver — the gate the
87
+ * checked `.typed:[]` forms and the identity folds share. Proof classes:
88
+ * 1. the canonical-loop structural pair (inBoundsArrIdx);
89
+ * 2. a literal index against the binding's STATIC length (ctx.types.typedLen /
90
+ * ctx.scope.globalTypedLen — `new T(<n>)`, tracker-invalidated on redef);
91
+ * 3. the masked form `x & m` / `m & x` (ToInt32 & clears the sign for m ≥ 0, so the
92
+ * result is in [0, m]) with m < that static length;
93
+ * 4. a versioned-loop assumption (ctx.types.assumedBounds) — the emitter is inside
94
+ * the guarded arm of a loop whose runtime extent check covers exactly this
95
+ * (recv, idx) pair (see versionableTypedFor / the 'for' emitter);
96
+ * 5. the static interval walk (intervalProvenIdx) — const-bound nests whose index
97
+ * chains (incl. the clamp idiom) provably fit a static receiver length. */
98
+ export function typedIdxProven(recv, idx) {
99
+ if (typeof recv !== 'string') return false
100
+ // a versioned assumption is scoped to its OWNING loop: honored only while that
101
+ // loop's frame is on the emission stack (a textual twin of the access OUTSIDE
102
+ // the loop sees the cursor past its bound and must stay checked)
103
+ const owner = ctx.types.assumedBounds?.get(idxKey(recv, idx))
104
+ if (owner != null && ctx.func.stack?.some(f => f.bodyNode === owner)) return true
105
+ if (intervalProvenIdx(ctx).has(idxKey(recv, idx))) return true
106
+ if (typeof idx === 'string' && inBoundsArrIdx(ctx).has(recv + '\x00' + idx)) return true
107
+ const len = ctx.types.typedLen?.get(recv) ?? ctx.scope?.globalTypedLen?.get(recv)
108
+ if (len == null) return false
109
+ const k = intLiteralValue(idx)
110
+ if (k != null) return k >= 0 && k < len
111
+ if (Array.isArray(idx) && idx[0] === '&' && idx.length === 3) {
112
+ const m = intLiteralValue(idx[1]) ?? intLiteralValue(idx[2])
113
+ if (m != null) return m >= 0 && m < len
114
+ }
115
+ if (typeof idx === 'string') {
116
+ const B = litBoundArrIdx(ctx).get(recv + '\x00' + idx)
117
+ if (B != null) return B <= len
118
+ }
119
+ return false
120
+ }
121
+
122
+ /** Structural key for a `recv[idx]` site — the assumedBounds channel between the
123
+ * versioning scan and typedIdxProven. JSON is structural, so the key matches even
124
+ * when the prover sees a clone of the scanned node. */
125
+ export const idxKey = (recv, idx) => recv + '\x00' + (typeof idx === 'string' ? idx : JSON.stringify(idx))
126
+
127
+ /** Decompose `idx` as `a*iv + bName + bConst`: literal iv-coefficient a ≥ 0, at most
128
+ * one symbolic body-invariant name (coefficient 1), an int constant. `env` maps
129
+ * single-def body-let names to their own affine forms (`const j = 3*i` → uses of
130
+ * `j`, `j+1` resolve through it). Additive combine over `+`/`-`/literal-`*`; two
131
+ * symbolic names, a scaled name, or a negative final iv-coefficient reject. The
132
+ * kernel shapes: `i`, `3*i+1` (AoS), `j+half` via env (butterfly), `irow+kx`
133
+ * (conv), plain invariant `base` (a = 0). */
134
+ export function affineIdxOfIV(idx, iv, body, env) {
135
+ const slotEq = (p, q) => p === q || (typeof p !== 'string' && typeof q !== 'string'
136
+ && JSON.stringify(p) === JSON.stringify(q))
137
+ const MAX_SLOTS = 2 // butterfly's `b = i + j + half` carries two symbolic terms
138
+ const addSlots = (A, B, s) => {
139
+ const out = A.map(t => ({ k: t.k, e: t.e }))
140
+ for (const t of B) {
141
+ const hit = out.find(o => slotEq(o.e, t.e))
142
+ if (hit) hit.k += s * t.k
143
+ else out.push({ k: s * t.k, e: t.e })
144
+ }
145
+ return out.filter(t => t.k !== 0)
146
+ }
147
+ const aff = (e) => {
148
+ if (e === iv) return { a: 1, slots: [], bConst: 0 }
149
+ const n = intLiteralValue(e)
150
+ if (n != null) return { a: 0, slots: [], bConst: n }
151
+ if (typeof e === 'string') {
152
+ // a name the env KNOWS (declared in this body) must resolve through it — a
153
+ // null entry is a body-varying non-affine value the guard cannot pre-read
154
+ if (env?.has(e)) return env.get(e)
155
+ return isReassigned(body, e) ? null : { a: 0, slots: [{ k: 1, e }], bConst: 0 }
156
+ }
157
+ if (!Array.isArray(e)) return null
158
+ const [op, x, y] = e
159
+ // TOROIDAL WRAP of this loop's OWN iv — `iv === 0 ? B-1 : iv-1` (backward) or
160
+ // `iv === B-1 ? 0 : iv+1` (forward): a bounded ATOM ∈ [0, B-1]. Asymmetric by
161
+ // nature — it contributes B-1 to the HI extent and 0 to the LO — carried as a
162
+ // wrap-flagged slot the emitter accounts one-sidedly.
163
+ if (op === '?:' && e.length === 4 && Array.isArray(x) && x.length === 3) {
164
+ const [cop, cl, cr] = x
165
+ const isMinus1 = (n, B) => Array.isArray(n) && n[0] === '-' && n.length === 3
166
+ && slotEq(n[1], B) && intLiteralValue(n[2]) === 1
167
+ let B = null
168
+ if (cop === '===' && cl === iv && intLiteralValue(cr) === 0
169
+ && Array.isArray(e[3]) && e[3][0] === '-' && e[3][1] === iv && intLiteralValue(e[3][2]) === 1
170
+ && Array.isArray(e[2]) && e[2][0] === '-' && intLiteralValue(e[2][2]) === 1)
171
+ B = e[2][1] // iv===0 ? B-1 : iv-1
172
+ else if (cop === '===' && cl === iv && isMinus1(cr, Array.isArray(cr) ? cr[1] : cr)
173
+ && intLiteralValue(e[2]) === 0
174
+ && Array.isArray(e[3]) && e[3][0] === '+' && e[3][1] === iv && intLiteralValue(e[3][2]) === 1)
175
+ B = cr[1] // iv===B-1 ? 0 : iv+1
176
+ else if (cop === '>' && cl === iv && intLiteralValue(cr) === 0
177
+ && Array.isArray(e[2]) && e[2][0] === '-' && e[2][1] === iv && intLiteralValue(e[2][2]) === 1
178
+ && Array.isArray(e[3]) && e[3][0] === '-' && intLiteralValue(e[3][2]) === 1)
179
+ B = e[3][1] // iv>0 ? iv-1 : B-1
180
+ else if (cop === '<' && cl === iv && isMinus1(cr, Array.isArray(cr) ? cr[1] : cr)
181
+ && Array.isArray(e[2]) && e[2][0] === '+' && e[2][1] === iv && intLiteralValue(e[2][2]) === 1
182
+ && intLiteralValue(e[3]) === 0)
183
+ B = cr[1] // iv<B-1 ? iv+1 : 0
184
+ if (B != null && invariantIdxExpr(B, iv, body, env))
185
+ return { a: 0, slots: [{ k: 1, e: B, wrap: true }], bConst: 0 }
186
+ }
187
+ if (e.length === 3 && op === '*') {
188
+ const L = intLiteralValue(x) ?? intLiteralValue(y)
189
+ if (L != null) {
190
+ const t = aff(intLiteralValue(x) != null ? y : x)
191
+ if (t) return { a: t.a * L, slots: t.slots.map(u => ({ ...u, k: u.k * L })), bConst: t.bConst * L }
192
+ }
193
+ // fall through: a non-literal product (`y*w`) may still be an invariant slot
194
+ }
195
+ if (e.length === 3 && (op === '+' || op === '-')) {
196
+ const l = aff(x), r = aff(y)
197
+ if (l && r) {
198
+ const s = op === '+' ? 1 : -1
199
+ const slots = addSlots(l.slots, r.slots, s)
200
+ if (slots.length <= MAX_SLOTS)
201
+ return { a: l.a + s * r.a, slots, bConst: l.bConst + s * r.bConst }
202
+ }
203
+ // fall through to the whole-expr slot
204
+ }
205
+ // WHOLE-EXPR SLOT: an iv-free pure arithmetic expression over stable outer names
206
+ // (`y*w`, `(oy+ky)*IW`) — the guard evaluates it once at loop entry; runtime
207
+ // `integral ∧ |v| ≤ 2^31` conjuncts (the 'f64' slot kind) make the int model exact.
208
+ return invariantIdxExpr(e, iv, body, env) ? { a: 0, slots: [{ k: 1, e }], bConst: 0 } : null
209
+ }
210
+ const r = aff(idx)
211
+ return r && r.a >= 0 && Number.isInteger(r.a) && Number.isInteger(r.bConst)
212
+ && r.slots.every(t => Number.isInteger(t.k)) ? r : null
213
+ }
214
+
215
+ /** `e` is a pure arithmetic expression whose value cannot change across the loop:
216
+ * literals and stable outer names under numeric operators — no calls, no indexing,
217
+ * no property reads, no assignments, no iv, no body-declared names (they don't
218
+ * exist at guard time). The slot whitelist matches what the guard can safely
219
+ * re-evaluate before the loop. */
220
+ const SLOT_OPS = new Set(['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'])
221
+ function invariantIdxExpr(e, iv, body, env) {
222
+ if (intLiteralValue(e) != null) return true
223
+ if (typeof e === 'string')
224
+ return e !== iv && !env?.has(e) && !isReassigned(body, e) && !redeclaresName(body, e)
225
+ if (!Array.isArray(e) || !SLOT_OPS.has(e[0]) || e.length > 3) return false
226
+ for (let k = 1; k < e.length; k++) if (!invariantIdxExpr(e[k], iv, body, env)) return false
227
+ return true
228
+ }
229
+
230
+ /** Single-def body-let affine environment for `affineIdxOfIV`: names declared
231
+ * EXACTLY once in `body`, never written, whose rhs is itself iv-affine (through
232
+ * earlier env entries — decls resolve in walk order: `const j = 3*i; const k = j+1`).
233
+ * A second decl of the same name (block shadowing) evicts it permanently. */
234
+ export function bodyAffineEnv(body, iv) {
235
+ const env = new Map() // name → affine, or null = body-declared but unresolvable
236
+ const walk = (n) => {
237
+ if (!Array.isArray(n) || n[0] === '=>') return
238
+ if (n[0] === 'let' || n[0] === 'const') {
239
+ for (let k = 1; k < n.length; k++) {
240
+ const d = n[k]
241
+ const name = typeof d === 'string' ? d : Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' ? d[1] : null
242
+ if (name == null) continue
243
+ if (env.has(name)) { env.set(name, null); continue } // shadowing second decl — evict
244
+ env.set(name, typeof d === 'string' || isReassigned(body, name) ? null
245
+ : affineIdxOfIV(d[2], iv, body, env))
246
+ }
247
+ }
248
+ for (let k = 1; k < n.length; k++) walk(n[k])
249
+ }
250
+ walk(body)
251
+ return env
252
+ }
253
+
254
+ /** Loop-versioning scan for the 'for' emitter: a countable loop
255
+ * `for (let iv = C≥0; iv < BOUND; iv++)` whose body indexes TYPED receivers with
256
+ * iv-affine indices that no static class proves. Returns null or
257
+ * `{ iv, startC, bound, cands }` — each cand `{ recv, idx, a, bName, bConst }`.
258
+ * The caller emits `if (∀ extents in bounds) fast-arm else checked-arm`, assuming
259
+ * exactly `cands`' keys inside the fast arm, so every judgment here is
260
+ * load-bearing for memory safety:
261
+ * - BOUND re-evaluates in the guard → must be pure AND i32-machine-typed (an f64
262
+ * bound like `i < 5.5` admits iv = trunc-extent + 1 — the guard would under-
263
+ * estimate); literal int, an unwritten i32 name, or an unwritten typed
264
+ * receiver's `.length`;
265
+ * - bName terms must be i32-typed for the same reason;
266
+ * - closures in the body would be cloned per arm (two instances) — bail;
267
+ * - a candidate whose static low extent `a*C + bConst` is provably negative is
268
+ * DROPPED (its first iterations are genuinely OOB — the checked form is the
269
+ * semantics, a guard would just always fail). */
270
+ export function versionableTypedFor(init, cond, step, body, locals, entryHint = null) {
271
+ if (!Array.isArray(cond) || (cond[0] !== '<' && cond[0] !== '<=') || typeof cond[1] !== 'string') return null
272
+ if (containsNestedClosure(body)) return null
273
+ const iv = cond[1], incl = cond[0] === '<='
274
+ if (redeclaresName(body, iv)) return null
275
+ // iv start: a static init decl (`for (let i = 0; …)`) folds the lo conjunct;
276
+ // otherwise (while-shapes: `let i = 0; while (i < n) …`) the guard reads the
277
+ // ENTRY value of iv at runtime — the extent math is entry-relative either way.
278
+ const decls = new Map()
279
+ collectDecls(init, decls)
280
+ // entryHint: a sibling `let b = 0` right before a while — the nest scan's decl
281
+ // tracking supplies the static entry the empty init slot can't
282
+ const startC = decls.has(iv) ? intLiteralValue(decls.get(iv)) : entryHint
283
+ if (startC != null && startC < 0) return null // statically-negative start: guard is dead weight
284
+ // iv advance: a unit-increment step slot (for-loops), or — when the step slot is
285
+ // empty — a SINGLE body write of shape `i = (i+LIT)|0` / `i = i+LIT` / `i += LIT` /
286
+ // `i++` with int LIT ≥ 1 (while-loops). A body-advanced iv is visible PAST the
287
+ // bound inside its final iteration (cond passes at B-1, the increment runs mid-
288
+ // body), so the max-iv widens by LIT (`bump`). Any other write shape rejects.
289
+ // a name the guard re-reads must denote the same binding for the whole loop
290
+ const stable = (name) => !isReassigned(body, name) && !redeclaresName(body, name)
291
+ let bump = 0, inds = null, stepBy = null
292
+ if (isUnitIncrement(step, iv)) {
293
+ if (isReassigned(body, iv)) return null
294
+ } else if (Array.isArray(step) && (step[0] === '+=' && step[1] === iv
295
+ || (step[0] === '=' && step[1] === iv && Array.isArray(step[2]) && step[2][0] === '+'
296
+ && (step[2][1] === iv || step[2][2] === iv)))) {
297
+ // MONOTONE non-unit advance (`i += len` — the fft block stride): extents only
298
+ // need iv ⊆ [start, maxIv], which any positive stride preserves. A literal
299
+ // stride proves positivity statically; a stable-name stride adds a runtime
300
+ // `stride ≥ 1` conjunct (zero/negative falls to the checked arm).
301
+ const x = step[0] === '+=' ? step[2] : step[2][1] === iv ? step[2][2] : step[2][1]
302
+ const lit = intLiteralValue(x)
303
+ if (lit != null ? lit < 1 : !(typeof x === 'string' && x !== iv
304
+ && !isReassigned(body, x) && !redeclaresName(body, x))) return null
305
+ if (isReassigned(body, iv)) return null
306
+ stepBy = lit != null ? { lit } : { name: x, kind: exprType(x, locals) === 'i32' ? 'i32' : 'f64' }
307
+ } else if (Array.isArray(step) && step[0] === ',') {
308
+ // comma step (`j++, k += step`): exactly one unit-inc of iv; every other part
309
+ // `cursor += slope` (int literal or invariant name, cursor unwritten in body)
310
+ // declares an INDUCTION — cursor value at iteration t is entry + slope*t, so a
311
+ // plain `arr[cursor]` access guards by its two endpoints (either slope sign).
312
+ let unit = 0
313
+ inds = new Map()
314
+ for (const p of step.slice(1)) {
315
+ if (isUnitIncrement(p, iv)) { unit++; continue }
316
+ if (Array.isArray(p) && p[0] === '+=' && typeof p[1] === 'string' && p[1] !== iv
317
+ && stable(p[1])
318
+ && (intLiteralValue(p[2]) != null || (typeof p[2] === 'string' && p[2] !== iv && stable(p[2])))) {
319
+ inds.set(p[1], p[2]); continue
320
+ }
321
+ inds = null; break
322
+ }
323
+ if (!inds || unit !== 1 || !inds.size || isReassigned(body, iv)) return null
324
+ } else if (step == null && isReassigned(body, iv)) {
325
+ const writes = []
326
+ const collectW = (n) => {
327
+ if (!Array.isArray(n)) return
328
+ if (((n[0] === '=' || n[0] === '+=') && n[1] === iv) || ((n[0] === '++' || n[0] === '--') && n[1] === iv))
329
+ writes.push(n)
330
+ for (let k = 1; k < n.length; k++) collectW(n[k])
331
+ }
332
+ collectW(body)
333
+ if (writes.length !== 1) return null
334
+ const w = writes[0]
335
+ const incOf = (n) => {
336
+ if (n[0] === '++') return 1
337
+ if (n[0] === '+=') return intLiteralValue(n[2])
338
+ if (n[0] !== '=') return null
339
+ let rhs = n[2]
340
+ if (Array.isArray(rhs) && rhs[0] === '|' && intLiteralValue(rhs[2]) === 0) rhs = rhs[1] // (i+LIT)|0
341
+ if (Array.isArray(rhs) && rhs[0] === '+' && rhs.length === 3) {
342
+ if (rhs[1] === iv) return intLiteralValue(rhs[2])
343
+ if (rhs[2] === iv) return intLiteralValue(rhs[1])
344
+ }
345
+ return null
346
+ }
347
+ const L = incOf(w)
348
+ if (L == null || L < 1 || !Number.isInteger(L)) return null
349
+ bump = L
350
+ } else return null
351
+ const bound = cond[2]
352
+ // bKind drives the guard's conversion to a max-iv i64:
353
+ // 'i32' — literal, i32-machine name, or a typed receiver's .length: exact extend;
354
+ // 'f64' — any other stable name (an untyped param, a NaN-boxed unknown): the
355
+ // emitter adds a runtime `|B| ≤ 2^31` conjunct — box bit patterns are NaN, so
356
+ // abs-compare fails and the checked arm takes over; a genuine number converts
357
+ // exactly via ceil/floor + trunc_sat (never traps, saturation is conjunct-dead).
358
+ const bKind = intLiteralValue(bound) != null ? 'i32'
359
+ : (() => { const r = lengthRecv(bound); return r != null && ctx.types.typedElem?.has(r) && stable(r) })() ? 'i32'
360
+ : typeof bound === 'string' && stable(bound) ? (exprType(bound, locals) === 'i32' ? 'i32' : 'f64')
361
+ // an invariant pure EXPRESSION bound (`x < w - 1` — the stencil interior) re-
362
+ // evaluates safely in the guard; machine-f64 rides the runtime-conjunct path
363
+ : invariantIdxExpr(bound, iv, body, null) ? (exprType(bound, locals) === 'i32' ? 'i32' : 'f64')
364
+ : null
365
+ if (bKind == null) return null
366
+ const env = bodyAffineEnv(body, iv)
367
+ // induction cursors vary per iteration — they must not leak into slot terms
368
+ // (the affine env blocks them); their PLAIN `arr[cursor]` reads are their own
369
+ // candidate class below
370
+ if (inds) for (const nm of inds.keys()) env.set(nm, null)
371
+ const cands = []
372
+ const seen = new Set()
373
+ const scan = (n) => {
374
+ if (!Array.isArray(n)) return
375
+ if (n[0] === '[]' && n.length === 3 && typeof n[1] === 'string' && n[1] !== iv
376
+ && ctx.types.typedElem?.has(n[1]) && stable(n[1])) {
377
+ const key = idxKey(n[1], n[2])
378
+ if (!seen.has(key) && !typedIdxProven(n[1], n[2])) {
379
+ if (typeof n[2] === 'string' && inds?.has(n[2])) {
380
+ seen.add(key)
381
+ cands.push({ recv: n[1], idx: n[2], ind: n[2], slope: inds.get(n[2]),
382
+ entryC: decls.has(n[2]) ? intLiteralValue(decls.get(n[2])) : null })
383
+ } else {
384
+ const aff = affineIdxOfIV(n[2], iv, body, env)
385
+ // symbolic slots: i32-machine exprs are exact; any other rides the f64
386
+ // path with runtime `integral ∧ |v| ≤ 2^31` conjuncts (kind 'f64')
387
+ if (aff
388
+ // statically-negative low extent: the checked form IS the semantics, a
389
+ // guard would always fail (runtime-entry loops keep the runtime lo check)
390
+ && !(aff.slots.length === 0 && startC != null && aff.a * startC + aff.bConst < 0)) {
391
+ seen.add(key)
392
+ const slots = aff.slots.map(t => ({ ...t, kind: exprType(t.e, locals) === 'i32' ? 'i32' : 'f64' }))
393
+ cands.push({ recv: n[1], idx: n[2], a: aff.a, slots, bConst: aff.bConst })
394
+ } else {
395
+ // LAST resort — beyond the affine model (masked ring cursors, wrap
396
+ // idioms): an interval HULL the static walk bounded but couldn't
397
+ // discharge (dynamic receiver length) closes with one runtime
398
+ // `hull.hi < len` conjunct. Strictly a fallback: it must never steal
399
+ // an affine candidate (whose per-iv extents are tighter).
400
+ const rng = intervalIdxRanges(ctx).get(key)
401
+ if (rng && (rng.hiName == null || stable(rng.hiName))) {
402
+ seen.add(key); cands.push({ recv: n[1], idx: n[2], range: rng })
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ for (let k = 1; k < n.length; k++) scan(n[k])
409
+ }
410
+ scan(body)
411
+ if (globalThis.process?.env?.JZ_DBG_VS) console.error('VS', iv, 'cands', cands.length, cands.slice(0,3).map(c => c.recv + (c.range ? ':hull' : c.ind ? ':ind' : ':aff')).join(' '))
412
+ return cands.length
413
+ ? { iv, ivKind: exprType(iv, locals) === 'i32' ? 'i32' : 'f64', startC, bump, bound, bKind, incl, stepBy, cands }
414
+ : null
415
+ }
416
+
417
+ /** Nest-level versioning scan: the intercepted loop PLUS every nested loop whose
418
+ * guard is evaluable at the TOP entry — one guard for the whole nest, so the
419
+ * outer-strip / per-pixel / iterated-reduce recognizers see a BARE nest in the
420
+ * fast arm (an inner-loop guard would blind them, and per-row guards are dearer
421
+ * than one per nest anyway). A nested level lifts only when
422
+ * - its iv entry is STATIC (init literal or the `let b = 0; while (b < n)`
423
+ * sibling-decl pattern — a runtime entry read at top-entry would be stale),
424
+ * - it carries no induction cursors (their entry values are per-inner-entry),
425
+ * - every name its guard reads is neither written NOR DECLARED anywhere in the
426
+ * top body (redeclaresName catches inner decls — a per-row offset slot must
427
+ * not be read before its row exists). Unliftable levels simply keep their own
428
+ * inner versioning during arm emission — graceful degradation, not a bail. */
429
+ export function versionableTypedNest(init, cond, step, body, locals) {
430
+ if (containsNestedClosure(body)) return null
431
+ const levels = []
432
+ // RANGE-ONLY level: a loop the canonical-iv analysis rejects (`while (keys[h]
433
+ // !== k)` — no `<` cond, no countable iv) can still guard its hull-bounded
434
+ // accesses: hull conjuncts need no iv at all. The masked ring cursor over a
435
+ // dynamic-length param table is exactly this shape.
436
+ const rangeOnly = (c2, b2) => {
437
+ const cands = [], seen = new Set()
438
+ const stable2 = (nm) => !isReassigned(b2, nm) && !redeclaresName(b2, nm)
439
+ const scan = (n) => {
440
+ if (!Array.isArray(n) || n[0] === '=>') return
441
+ if (n[0] === '[]' && n.length === 3 && typeof n[1] === 'string'
442
+ && ctx.types.typedElem?.has(n[1]) && stable2(n[1])) {
443
+ const key = idxKey(n[1], n[2])
444
+ if (!seen.has(key) && !typedIdxProven(n[1], n[2])) {
445
+ const rng = intervalIdxRanges(ctx).get(key)
446
+ if (rng && (rng.hiName == null || stable2(rng.hiName))) {
447
+ seen.add(key); cands.push({ recv: n[1], idx: n[2], range: rng })
448
+ }
449
+ }
450
+ }
451
+ for (let k = 1; k < n.length; k++) scan(n[k])
452
+ }
453
+ scan(c2) // `while (keys[h] !== k)` — the accesses live in the COND
454
+ scan(b2)
455
+ return cands.length ? { rangeOnly: true, cands } : null
456
+ }
457
+ const walkLoop = (i2, c2, s2, b2, hint, isTop) => {
458
+ const spec = versionableTypedFor(i2, c2, s2, b2, locals, hint) ?? rangeOnly(c2, b2)
459
+ if (spec) { spec.top = isTop; spec.bodyNode = b2; levels.push(spec) }
460
+ scanStmts(b2)
461
+ }
462
+ const scanStmts = (n) => {
463
+ if (!Array.isArray(n) || n[0] === '=>') return
464
+ if (n[0] === 'while' && n.length === 3 && Array.isArray(n[1])) { walkLoop(null, n[1], null, n[2], null, false); return }
465
+ if (n[0] === 'for' && n.length === 5) { walkLoop(n[1], n[2], n[3], n[4], null, false); return }
466
+ if (n[0] === ';' || n[0] === '{}') {
467
+ let lastDecls = new Map()
468
+ for (let k = 1; k < n.length; k++) {
469
+ const st = n[k]
470
+ if (Array.isArray(st) && (st[0] === 'let' || st[0] === 'const')) {
471
+ for (let j = 1; j < st.length; j++) {
472
+ const d = st[j]
473
+ if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
474
+ const v = intLiteralValue(d[2])
475
+ if (v != null && v >= 0) lastDecls.set(d[1], v); else lastDecls.delete(d[1])
476
+ } else if (typeof d === 'string') lastDecls.delete(d)
477
+ }
478
+ continue
479
+ }
480
+ if (Array.isArray(st) && st[0] === 'while' && st.length === 3
481
+ && Array.isArray(st[1]) && typeof st[1][1] === 'string') {
482
+ walkLoop(null, st[1], null, st[2], lastDecls.get(st[1][1]) ?? null, false)
483
+ } else if (Array.isArray(st) && st[0] === 'for' && st.length === 5) {
484
+ walkLoop(st[1], st[2], st[3], st[4], null, false)
485
+ } else scanStmts(st)
486
+ lastDecls = new Map() // any other statement may disturb tracked entries
487
+ }
488
+ return
489
+ }
490
+ for (let k = 1; k < n.length; k++) scanStmts(n[k])
491
+ }
492
+ walkLoop(init, cond, step, body, null, true)
493
+ const stableTop = (name) => typeof name !== 'string'
494
+ || (!isReassigned(body, name) && !redeclaresName(body, name))
495
+ const exprNames = (e, out) => {
496
+ if (typeof e === 'string') out.push(e)
497
+ else if (Array.isArray(e)) for (let k = 1; k < e.length; k++) exprNames(e[k], out)
498
+ }
499
+ const keepPre = levels.filter((L) => {
500
+ if (!L.top) {
501
+ if (!L.rangeOnly && L.startC == null) return false
502
+ const n0 = L.cands.length
503
+ // an induction whose ENTRY is a static init literal (`for (let j=0, k=0; …)`)
504
+ // lifts like any extent — only runtime-entry cursors are per-inner-entry
505
+ L.cands = L.cands.filter(c => c.ind == null || c.entryC != null)
506
+ if (!L.cands.length) return false
507
+ // runtime-entry inductions dropped: the level still needs ITS OWN intercept
508
+ // for them — the top guard must not brake it
509
+ if (L.cands.length !== n0) L.partial = true
510
+ }
511
+ // names the lifted guard READS at top entry (iv itself is NOT read — inner
512
+ // entries are static by the filter above, and only the top may read its iv)
513
+ const names = []
514
+ exprNames(L.bound, names)
515
+ if (typeof L.bound === 'string') names.push(L.bound)
516
+ for (const c of L.cands) {
517
+ names.push(c.recv)
518
+ if (c.range != null) { if (c.range.hiName != null) names.push(c.range.hiName); continue }
519
+ if (c.ind != null) { names.push(c.ind); if (typeof c.slope === 'string') names.push(c.slope) }
520
+ else for (const t of c.slots) { if (typeof t.e === 'string') names.push(t.e); else exprNames(t.e, names) }
521
+ }
522
+ // the top level's own iv/bound legitimately live in the top body — only names
523
+ // read by LIFTED (inner) guards need top-stability; the top spec re-checks
524
+ // nothing new here beyond its own scan
525
+ return L.top || names.every(stableTop)
526
+ })
527
+ const keep = keepPre
528
+ if (!keep.length) return null
529
+ // FLAT-CURSOR inductions: `j++` exactly once in the whole nest (the universal
530
+ // image-kernel pixel cursor `px[j] = …; j++`). Its value spans
531
+ // [j0, j0 + slope·(Π level-trips − 1 or − 0)] — every containing loop must be a
532
+ // LIFTED level (trip = maxIv − entry + 1 known at the guard); a pre-increment
533
+ // read tops out one slope earlier than a post-increment one, so each access
534
+ // carries its position. Entry j0 reads at the nest top (the cursor lives in an
535
+ // enclosing scope by construction — a body-declared cursor is rejected by
536
+ // redeclaresName).
537
+ const cursorWrites = new Map() // name → { node, slope } | null (disqualified)
538
+ const collectCW = (n) => {
539
+ if (!Array.isArray(n)) return
540
+ if ((ASSIGN_OPS_V.has(n[0]) || n[0] === '++' || n[0] === '--') && typeof n[1] === 'string') {
541
+ const name = n[1]
542
+ const L = n[0] === '++' ? 1
543
+ : n[0] === '--' ? null
544
+ : n[0] === '+=' ? intLiteralValue(n[2])
545
+ : n[0] === '=' ? (() => {
546
+ let rhs = n[2]
547
+ if (Array.isArray(rhs) && rhs[0] === '|' && intLiteralValue(rhs[2]) === 0) rhs = rhs[1]
548
+ if (Array.isArray(rhs) && rhs[0] === '+' && rhs.length === 3) {
549
+ if (rhs[1] === name) return intLiteralValue(rhs[2])
550
+ if (rhs[2] === name) return intLiteralValue(rhs[1])
551
+ }
552
+ return null
553
+ })()
554
+ : null
555
+ cursorWrites.set(name, cursorWrites.has(name) || L == null || L < 1 || !Number.isInteger(L)
556
+ ? null : { node: n, slope: L })
557
+ }
558
+ for (let k = 1; k < n.length; k++) collectCW(n[k])
559
+ }
560
+ collectCW(body)
561
+ const contains = (hay, needle) => hay === needle
562
+ || (Array.isArray(hay) && hay.some((x, i) => i > 0 && contains(x, needle)))
563
+ const allLoopBodies = []
564
+ const collectLoops = (n) => {
565
+ if (!Array.isArray(n) || n[0] === '=>') return
566
+ if (n[0] === 'while' && n.length === 3) allLoopBodies.push(n[2])
567
+ else if (n[0] === 'for' && n.length === 5) allLoopBodies.push(n[4])
568
+ for (let k = 1; k < n.length; k++) collectLoops(n[k])
569
+ }
570
+ collectLoops(body)
571
+ const keptBodies = new Set(keep.map(L => L.bodyNode))
572
+ const cursors = []
573
+ for (const [name, w] of cursorWrites) {
574
+ if (w == null) continue
575
+ if (redeclaresName(body, name)) continue
576
+ // every loop containing the write must be a lifted level (trips known);
577
+ // the top level's own body contains it by construction
578
+ const containing = allLoopBodies.filter(b => contains(b, w.node))
579
+ if (!containing.every(b => keptBodies.has(b) || b === body)) continue
580
+ if (!keptBodies.has(body) && containing.length === 0) continue
581
+ const chain = keep.filter(L => contains(L.bodyNode, w.node) || L.bodyNode === body)
582
+ if (!chain.length) continue
583
+ // accesses arr[name]: position vs the write decides the endpoint
584
+ const cands = []
585
+ let seenWrite = false
586
+ const scanC = (n) => {
587
+ if (!Array.isArray(n)) return
588
+ if (n === w.node) { seenWrite = true }
589
+ if (n[0] === '[]' && n.length === 3 && typeof n[1] === 'string' && n[2] === name
590
+ && ctx.types.typedElem?.has(n[1])
591
+ && !isReassigned(body, n[1]) && !redeclaresName(body, n[1]))
592
+ cands.push({ recv: n[1], idx: n[2], post: seenWrite })
593
+ for (let k = 1; k < n.length; k++) scanC(n[k])
594
+ }
595
+ scanC(body)
596
+ if (cands.length) cursors.push({ name, slope: w.slope, chain, cands,
597
+ kind: exprType(name, locals) === 'i32' ? 'i32' : 'f64' })
598
+ }
599
+ keep.cursors = cursors
600
+ return keep
601
+ }
602
+ const ASSIGN_OPS_V = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**='])
603
+
44
604
  /** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
45
605
  * different typed-array ctors — caller should drop any cached entry rather
46
606
  * than leave a stale ctor (which would lock the wrong store width). */
@@ -211,7 +771,7 @@ function collectBoundedArrIdx(node, recv, idxVar, set) {
211
771
  * `[0, recv.length)` by an enclosing canonical loop `for (let i = C; i < recv.length;
212
772
  * i++)`. Same loop contract as `scanBoundedLoops` (charCodeAt) — sibling proof for
213
773
  * the ARRAY indexed-read fast path in `module/array.js`. */
214
- export function scanBoundedArrIdx(node, set) {
774
+ export function scanBoundedArrIdx(node, set, litSet) {
215
775
  if (!Array.isArray(node)) return
216
776
  if (node[0] === 'for' && node.length === 5) {
217
777
  const [, init, cond, step, body] = node
@@ -231,8 +791,43 @@ export function scanBoundedArrIdx(node, set) {
231
791
  && (boundVar == null || !isReassigned(body, boundVar))
232
792
  && !redeclaresName(body, idx))
233
793
  collectBoundedArrIdx(body, recv, idx, set)
794
+ // LITERAL-bound loop `for (let i = C≥0; i < B; i++)`: every `X[i]` read is in
795
+ // [C, B) — provable against a receiver whose STATIC length ≥ B (typedIdxProven
796
+ // consults litSet's recorded bound vs ctx.types.typedLen). Collected for every
797
+ // receiver name in the body; per-receiver reassignment guarded like the
798
+ // .length form. Two loops sharing (recv, i) names keep the MAX bound —
799
+ // conservative for the proof.
800
+ if (litSet && !(idx && recv)) {
801
+ // re-derive idx with the same start guard (the .length branch nulled it only
802
+ // when recv didn't resolve — recompute cleanly for the literal branch)
803
+ if (Array.isArray(cond) && cond[0] === '<' && typeof cond[1] === 'string') {
804
+ const decls = new Map()
805
+ collectDecls(init, decls)
806
+ const idx2 = cond[1]
807
+ const start = decls.has(idx2) ? intLiteralValue(decls.get(idx2)) : null
808
+ let bound = cond[2]
809
+ if (typeof bound === 'string' && decls.has(bound)) bound = decls.get(bound)
810
+ const B = intLiteralValue(bound)
811
+ if (start != null && start >= 0 && B != null && B >= 0
812
+ && isUnitIncrement(step, idx2) && !isReassigned(body, idx2) && !redeclaresName(body, idx2)) {
813
+ const recvs = new Set()
814
+ const collectRecvs = (n) => {
815
+ if (!Array.isArray(n) || n[0] === '=>') return
816
+ if (n[0] === '[]' && n.length === 3 && typeof n[1] === 'string' && n[2] === idx2) recvs.add(n[1])
817
+ for (let k = 1; k < n.length; k++) collectRecvs(n[k])
818
+ }
819
+ collectRecvs(body)
820
+ for (const r of recvs) {
821
+ if (r === idx2 || isReassigned(body, r)) continue
822
+ const key = r + '\x00' + idx2
823
+ const prev = litSet.get(key)
824
+ litSet.set(key, prev == null ? B : Math.max(prev, B))
825
+ }
826
+ }
827
+ }
828
+ }
234
829
  }
235
- for (let k = 1; k < node.length; k++) scanBoundedArrIdx(node[k], set)
830
+ for (let k = 1; k < node.length; k++) scanBoundedArrIdx(node[k], set, litSet)
236
831
  }
237
832
 
238
833
  /** Set of `"recv\x00idx"` keys for `recv[idx]` reads in the current function proven
@@ -242,12 +837,432 @@ export function inBoundsArrIdx(ctx) {
242
837
  if (!Array.isArray(body)) return NO_BOUNDED_CC
243
838
  if (ctx.func._aiBody === body) return ctx.func.aiInBounds
244
839
  const set = new Set()
245
- scanBoundedArrIdx(body, set)
840
+ const litSet = new Map()
841
+ scanBoundedArrIdx(body, set, litSet)
246
842
  ctx.func.aiInBounds = set
843
+ ctx.func.aiLitBounds = litSet
247
844
  ctx.func._aiBody = body
248
845
  return set
249
846
  }
250
847
 
848
+ /** Map of `"recv\x00idx"` → max literal loop bound for `recv[idx]` reads under
849
+ * `for (let i = C≥0; i < LIT; i++)` — proven in-bounds iff LIT ≤ the receiver's
850
+ * static length (typedIdxProven). Memoised with inBoundsArrIdx. */
851
+ export function litBoundArrIdx(ctx) {
852
+ inBoundsArrIdx(ctx)
853
+ return ctx.func?.aiLitBounds || NO_LIT_BOUNDS
854
+ }
855
+ const NO_LIT_BOUNDS = new Map()
856
+
857
+ // === Static interval proof (typedIdxProven class 5) ===
858
+ // A tiny abstract interpreter over integer INTERVALS for const-bound loop nests —
859
+ // the conv2d/blur shape class: every dimension folds to a literal, every index is a
860
+ // chain of decls over ivs (`const irow = inCh+(oy+ky)*W+ox`), and the clamp idiom
861
+ // (`if(xi<0)xi=0; else if(xi>=w)xi=w-1`) bounds the tap. No runtime guard can help
862
+ // there (nest-level recognizers must see the BARE nest), and none is needed — the
863
+ // whole computation is static. Accesses whose idx interval fits a STATIC receiver
864
+ // length are recorded proven; everything else stays checked/versioned.
865
+
866
+ const IP_LIM = 0x40000000 // endpoints beyond ±2^30 widen to unknown (i32 headroom)
867
+ const ipOk = (v) => v != null && v[0] >= -IP_LIM && v[1] <= IP_LIM
868
+
869
+ /** Walk one function body, recording proven `recv[idx]` keys into `out`.
870
+ * `lens(name)` → static element count or null. Soundness posture: `out` only ever
871
+ * ADDS proofs, so every unknown/bail direction is safe — the sharp edges are all
872
+ * in keeping `env` honest (kills before loops/switch, closure-captured writes,
873
+ * assignments embedded in expressions). */
874
+ function scanIntervalIdx(body, out, lens, ranges) {
875
+ const env = new Map() // name → [lo, hi] | null (unknown)
876
+ const symEnv = new Map() // name → { h: symbolic hull, incNode } — wrap cursors vs mutable bounds
877
+ // names written inside ANY closure in this body: a later call can change them at
878
+ // any point — they never hold a trusted interval
879
+ const closureWrites = new Set()
880
+ const collectClosureWrites = (n, inClosure) => {
881
+ if (!Array.isArray(n)) return
882
+ const into = inClosure || n[0] === '=>'
883
+ if (into && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) {
884
+ if (typeof n[1] === 'string') closureWrites.add(n[1])
885
+ // member writes (`o[i]=…`, `o.p=…`) rebind no name; only PATTERN targets do
886
+ else if (Array.isArray(n[1]) && n[1][0] !== '[]' && n[1][0] !== '.' && n[1][0] !== '?.')
887
+ collectNames(n[1], closureWrites)
888
+ }
889
+ for (let k = 1; k < n.length; k++) collectClosureWrites(n[k], into)
890
+ }
891
+ const collectNames = (n, set) => {
892
+ if (typeof n === 'string') { set.add(n); return }
893
+ if (Array.isArray(n)) for (let k = 1; k < n.length; k++) collectNames(n[k], set)
894
+ }
895
+ collectClosureWrites(body, false)
896
+ const activeFacts = new Map() // name → [lo, hi] theorem stamped by a rewrite pass (peel)
897
+ const setEnv = (name, v) => {
898
+ if (closureWrites.has(name) || !ipOk(v)) v = null
899
+ const f = activeFacts.get(name)
900
+ if (f) v = v ? [Math.max(v[0], f[0]), Math.min(v[1], f[1])] : f
901
+ env.set(name, v)
902
+ }
903
+ const constInt = (e) => {
904
+ const n = intLiteralValue(e)
905
+ if (n != null) return n
906
+ if (typeof e === 'string' && !closureWrites.has(e)) {
907
+ const ci = ctx.scope?.constInts?.get?.(e)
908
+ if (ci != null && isI32(ci)) return ci
909
+ }
910
+ return null
911
+ }
912
+ const ARITH = new Set(['+', '-', '*', '<<', '>>', '>>>', '&', '%', '|'])
913
+ const ev = (e) => {
914
+ const n = constInt(e)
915
+ if (n != null) return [n, n]
916
+ if (typeof e === 'string') return closureWrites.has(e) ? null : env.get(e) ?? null
917
+ if (!Array.isArray(e)) return null
918
+ const [op, x, y] = e
919
+ // a NARROW typed load is range-bound by its element width (`table[in[j]]` — a
920
+ // Uint8Array read is [0,255] wherever j lands; even an unproven-idx read's
921
+ // undefined coerces through ToInt32 to 0, inside every narrow range)
922
+ if (op === '[]' && e.length === 3 && typeof x === 'string') {
923
+ visit(e) // record the access's own proof attempt
924
+ const r = NARROW_ELEM_RANGE[ctx.types.typedElem?.get(x)]
925
+ return r ?? null
926
+ }
927
+ if (e.length === 2 && op === '()') return ev(x) // grouping, not a call
928
+ if (e.length === 2 && (op === '-' || op === 'u-')) { const v = ev(x); return ipOk(v) && v ? [-v[1], -v[0]] : null }
929
+ if (op === '?:' && e.length === 4) { // join of both arms, each under its refinement
930
+ visit(x)
931
+ const rT = refine(x, false), rE = refine(x, true)
932
+ const sT = rT ? env.get(rT[0]) : null
933
+ if (rT) env.set(rT[0], rT[1])
934
+ const a = ev(e[2])
935
+ if (rT) env.set(rT[0], sT)
936
+ const sE = rE ? env.get(rE[0]) : null
937
+ if (rE) env.set(rE[0], rE[1])
938
+ const b = ev(e[3])
939
+ if (rE) env.set(rE[0], sE)
940
+ return a && b ? [Math.min(a[0], b[0]), Math.max(a[1], b[1])] : null
941
+ }
942
+ // any non-arithmetic node (call, assignment, ternary, indexing…) routes through
943
+ // visit so its env effects and access proofs are processed, value unknown
944
+ if (e.length !== 3 || !ARITH.has(op)) { visit(e); return null }
945
+ // `x|0` — ToInt32 is identity on an in-range interval
946
+ if (op === '|' && intLiteralValue(y) === 0) { const v = ev(x); return ipOk(v) ? v : null }
947
+ const A = ev(x), B = ev(y)
948
+ if (!A || !B) {
949
+ // a const mask bounds one-sidedly even when the other side is unknown
950
+ if (op === '&') {
951
+ const m = intLiteralValue(x) ?? intLiteralValue(y)
952
+ if (m != null && m >= 0) return [0, m]
953
+ }
954
+ return null
955
+ }
956
+ let r = null
957
+ if (op === '+') r = [A[0] + B[0], A[1] + B[1]]
958
+ else if (op === '-') r = [A[0] - B[1], A[1] - B[0]]
959
+ else if (op === '*') {
960
+ const p = [A[0] * B[0], A[0] * B[1], A[1] * B[0], A[1] * B[1]]
961
+ r = [Math.min(...p), Math.max(...p)]
962
+ }
963
+ else if (op === '<<' && B[0] === B[1] && B[0] >= 0 && B[0] <= 20) r = [A[0] * 2 ** B[0], A[1] * 2 ** B[0]]
964
+ else if (op === '>>' && B[0] === B[1] && B[0] >= 0 && B[0] <= 31) r = [A[0] >> B[0], A[1] >> B[0]]
965
+ else if (op === '>>>' && B[0] === B[1] && B[0] >= 0 && A[0] >= 0) r = [A[0] >>> B[0], A[1] >>> B[0]]
966
+ else if (op === '&' && B[0] === B[1] && B[0] >= 0) r = [0, B[0]]
967
+ else if (op === '%' && B[0] === B[1] && B[0] > 0 && A[0] >= 0) r = [0, Math.min(A[1], B[0] - 1)]
968
+ return ipOk(r) ? r : null
969
+ }
970
+ // condition refinement for if-arms: `name < K` / `name >= K` … over a known name
971
+ const refine = (c, negate) => {
972
+ if (!Array.isArray(c) || c.length !== 3) return null
973
+ let [op, l, r] = c
974
+ // rhs: an int literal/module const, or a body-known SINGLETON interval (`xi >= ww`
975
+ // where `const ww = 64|0` is function-local)
976
+ const rE = typeof r === 'string' ? env.get(r) : null
977
+ const rv = constInt(r) ?? (rE && rE[0] === rE[1] ? rE[0] : null)
978
+ if (typeof l !== 'string' || rv == null) return null
979
+ const K = rv, v = env.get(l)
980
+ if (!v) return null
981
+ if (negate) op = op === '<' ? '>=' : op === '<=' ? '>' : op === '>' ? '<=' : op === '>=' ? '<'
982
+ : op === '===' ? '!==' : op === '!==' ? '===' : null
983
+ if (op === '<') return [l, [v[0], Math.min(v[1], K - 1)]]
984
+ if (op === '<=') return [l, [v[0], Math.min(v[1], K)]]
985
+ if (op === '>') return [l, [Math.max(v[0], K + 1), v[1]]]
986
+ if (op === '>=') return [l, [Math.max(v[0], K), v[1]]]
987
+ if (op === '===') return [l, [Math.max(v[0], K), Math.min(v[1], K)]]
988
+ // ≠K tightens only at an ENDPOINT (interior point removal keeps the hull) —
989
+ // exactly the toroidal-wrap ternary (`y === 0 ? h-1 : y-1`)
990
+ if (op === '!==') return [l, [v[0] === K ? K + 1 : v[0], v[1] === K ? K - 1 : v[1]]]
991
+ return null
992
+ }
993
+ const killAssigned = (n) => {
994
+ if (!Array.isArray(n)) return // descend into closures too — capture-writes stay dead
995
+ if (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--') {
996
+ if (typeof n[1] === 'string') env.set(n[1], null)
997
+ else if (Array.isArray(n[1]) && n[1][0] !== '[]' && n[1][0] !== '.' && n[1][0] !== '?.') {
998
+ const s = new Set(); collectNames(n[1], s); for (const x of s) env.set(x, null)
999
+ }
1000
+ }
1001
+ for (let k = 1; k < n.length; k++) killAssigned(n[k])
1002
+ }
1003
+ const visit = (n) => {
1004
+ if (!Array.isArray(n) || n[0] === '=>') return
1005
+ if (n._rangeFacts) return visitWithFacts(n)
1006
+ const op = n[0]
1007
+ if (op === '[]' && n.length === 3 && typeof n[1] === 'string') {
1008
+ const idxV = ev(n[2])
1009
+ const L = lens(n[1])
1010
+ if (globalThis.process?.env?.JZ_DBG_IP) console.error('IPW', n[1], JSON.stringify(n[2]).slice(0,50), JSON.stringify(idxV), 'len', L)
1011
+ if (L != null && idxV && idxV[0] >= 0 && idxV[1] < L) out.add(idxKey(n[1], n[2]))
1012
+ // a bounded idx against an UNKNOWN length is half a proof — export the hull
1013
+ // (joined over every sighting of this key) for the versioning guard to close
1014
+ // with a runtime `hi < len` conjunct (the wrap-cursor + dynamic-table class)
1015
+ else if (idxV && idxV[0] >= 0 && ranges) {
1016
+ const k = idxKey(n[1], n[2]), prev = ranges.get(k)
1017
+ ranges.set(k, prev ? [Math.min(prev[0], idxV[0]), Math.max(prev[1], idxV[1])] : idxV)
1018
+ }
1019
+ // symbolic wrap hull (`seq[si]` with si ∈ [0, SEQLEN-1], SEQLEN mutable):
1020
+ // exported only while the cursor's pre-increment window is open; a numeric
1021
+ // or conflicting prior sighting voids the key (one symbolic form per key)
1022
+ else if (idxV == null && typeof n[2] === 'string' && symEnv.has(n[2]) && ranges) {
1023
+ const k = idxKey(n[1], n[2]), h = symEnv.get(n[2]).h, prev = ranges.get(k)
1024
+ if (prev == null) ranges.set(k, h)
1025
+ else if (prev.hiName !== h.hiName || prev.hiBias !== h.hiBias) ranges.set(k, null)
1026
+ }
1027
+ return
1028
+ }
1029
+ if (op === 'let' || op === 'const') {
1030
+ for (let k = 1; k < n.length; k++) {
1031
+ const d = n[k]
1032
+ if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') setEnv(d[1], ev(d[2]))
1033
+ else if (typeof d === 'string') env.set(d, null)
1034
+ else if (Array.isArray(d)) { visit(d); const s = new Set(); collectNames(d[0] === '=' ? d[1] : d, s); for (const x of s) env.set(x, null) }
1035
+ }
1036
+ return
1037
+ }
1038
+ if (op === '=' && typeof n[1] === 'string') {
1039
+ if (symEnv.get(n[1])?.incNode === n) symEnv.delete(n[1]) // past the increment: window closed
1040
+ setEnv(n[1], ev(n[2]))
1041
+ return
1042
+ }
1043
+ if (ASSIGN_OPS.has(op) || op === '++' || op === '--') {
1044
+ if (typeof n[1] === 'string' && symEnv.get(n[1])?.incNode === n) symEnv.delete(n[1])
1045
+ for (let k = 2; k < n.length; k++) visit(n[k])
1046
+ if (typeof n[1] === 'string') env.set(n[1], null)
1047
+ else {
1048
+ visit(n[1]) // records the member-write access proof (`out[idx] = …`)
1049
+ if (Array.isArray(n[1]) && n[1][0] !== '[]' && n[1][0] !== '.' && n[1][0] !== '?.') {
1050
+ const s = new Set(); collectNames(n[1], s); for (const x of s) env.set(x, null)
1051
+ }
1052
+ }
1053
+ return
1054
+ }
1055
+ if (op === 'for' && n.length === 5) {
1056
+ const [, init, cond, step, lbody] = n
1057
+ visit(init)
1058
+ // canonical literal-interval iv: `for (iv = A; iv </<= B; iv++)`, A/B const
1059
+ let iv = null, range = null
1060
+ if (Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=') && typeof cond[1] === 'string') {
1061
+ const decls = new Map(); collectDecls(init, decls)
1062
+ // start/bound through the full evaluator: function-local consts (`x < ww`)
1063
+ // and computed starts (`k = -rr`) resolve as singleton intervals
1064
+ const dv = decls.get(cond[1])
1065
+ const As = dv != null ? ev(dv) : null
1066
+ const Bs = cond[2] != null ? ev(cond[2]) : null
1067
+ const A = As && As[0] === As[1] ? As[0] : null
1068
+ const B = Bs && Bs[0] === Bs[1] ? Bs[0] : null
1069
+ if (A != null && B != null && isUnitIncrement(step, cond[1])
1070
+ && !isReassigned(lbody, cond[1]) && !redeclaresName(lbody, cond[1])) {
1071
+ iv = cond[1]; range = [A, cond[0] === '<' ? B - 1 : B]
1072
+ }
1073
+ }
1074
+ killAssigned(lbody)
1075
+ if (iv && range[0] <= range[1] && !closureWrites.has(iv)) env.set(iv, range)
1076
+ else if (iv) env.set(iv, null)
1077
+ visit(lbody)
1078
+ if (iv) env.set(iv, null) // iv holds the exit value after the loop
1079
+ return
1080
+ }
1081
+ if (op === 'while') {
1082
+ // `while (iv < B)` with a known iv at entry, monotone +1 advances, and a
1083
+ // bounded B: inside the body iv ∈ [entryLo, B_hi-1] (cond holds at body top);
1084
+ // at exit iv ∈ [min(entryLo, B_lo), max(entryHi, B_hi)] — the peel's split
1085
+ // loops chain through this. Anything else: kill and walk.
1086
+ const [, c, wbody] = n
1087
+ let iv = null, entry = null, brange = null
1088
+ if (Array.isArray(c) && c[0] === '<' && typeof c[1] === 'string' && wbody != null) {
1089
+ entry = env.get(c[1]); brange = c[2] != null ? ev(c[2]) : null
1090
+ if (entry && brange && ivMonotoneInc(wbody, c[1]) && !redeclaresName(wbody, c[1])) iv = c[1]
1091
+ }
1092
+ // WRAPPING-CURSOR invariant (`si = si + K; if (si >= C) si = 0` — the ring
1093
+ // index of table-driven maps): the pair is self-closing on [0, C-1], so an
1094
+ // entry inside that range keeps the name there for the WHOLE loop. Seeded
1095
+ // before the kill; the pair must be the name's only writes in this loop.
1096
+ const wraps = [], symWraps = []
1097
+ if (wbody != null) {
1098
+ const stmts = Array.isArray(wbody) && (wbody[0] === ';' || wbody[0] === '{}') ? wbody : [';', wbody]
1099
+ // one-statement MASK cursor `nm = (nm + K) & M` (the ulam direction ring):
1100
+ // self-closing on [0, M] for any entry inside it — no reset pair needed
1101
+ for (let k = 1; k < stmts.length; k++) {
1102
+ const a2 = stmts[k]
1103
+ if (!(Array.isArray(a2) && a2[0] === '=' && typeof a2[1] === 'string')) continue
1104
+ let rhs = a2[2]
1105
+ if (!(Array.isArray(rhs) && rhs[0] === '&' && rhs.length === 3)) continue
1106
+ const M = intLiteralValue(rhs[1]) ?? intLiteralValue(rhs[2])
1107
+ const inner = intLiteralValue(rhs[1]) != null ? rhs[2] : rhs[1]
1108
+ if (M == null || M < 0) continue
1109
+ const grp = Array.isArray(inner) && inner[0] === '()' && inner.length === 2 ? inner[1] : inner
1110
+ if (!(Array.isArray(grp) && grp[0] === '+' && (grp[1] === a2[1] || grp[2] === a2[1]))) continue
1111
+ const e0 = env.get(a2[1])
1112
+ if (!e0 || e0[0] < 0 || e0[1] > M) continue
1113
+ let writes = 0
1114
+ const cw = (x) => { if (!Array.isArray(x)) return
1115
+ if ((ASSIGN_OPS.has(x[0]) || x[0] === '++' || x[0] === '--') && x[1] === a2[1]) writes++
1116
+ for (let j = 1; j < x.length; j++) cw(x[j]) }
1117
+ cw(wbody)
1118
+ if (writes === 1) wraps.push([a2[1], [0, M]])
1119
+ }
1120
+ for (let k = 1; k < stmts.length - 1; k++) {
1121
+ const a2 = stmts[k], b2 = stmts[k + 1]
1122
+ let nm = null, K = null
1123
+ if (Array.isArray(a2) && a2[0] === '=' && typeof a2[1] === 'string'
1124
+ && Array.isArray(a2[2]) && a2[2][0] === '+' && a2[2][1] === a2[1]) { nm = a2[1]; K = intLiteralValue(a2[2][2]) }
1125
+ else if (Array.isArray(a2) && a2[0] === '+=' && typeof a2[1] === 'string') { nm = a2[1]; K = intLiteralValue(a2[2]) }
1126
+ else if (Array.isArray(a2) && a2[0] === '++' && typeof a2[1] === 'string') { nm = a2[1]; K = 1 }
1127
+ if (nm == null || K == null || K < 1) continue
1128
+ if (!(Array.isArray(b2) && b2[0] === 'if' && b2.length === 3
1129
+ && Array.isArray(b2[1]) && b2[1][0] === '>=' && b2[1][1] === nm
1130
+ && Array.isArray(b2[2]) && b2[2][0] === '=' && b2[2][1] === nm && intLiteralValue(b2[2][2]) === 0)) continue
1131
+ const C = constInt(b2[1][2])
1132
+ const Cname = C == null && typeof b2[1][2] === 'string' ? b2[1][2] : null
1133
+ if ((C == null || C < 1) && Cname == null) continue
1134
+ const e0 = env.get(nm)
1135
+ if (!e0 || e0[0] < 0 || (C != null && e0[1] > C - 1)) continue
1136
+ // the pair must be the only writes (2 exact: the add and the reset)
1137
+ let writes = 0
1138
+ const cw = (x) => { if (!Array.isArray(x)) return
1139
+ if ((ASSIGN_OPS.has(x[0]) || x[0] === '++' || x[0] === '--') && x[1] === nm) writes++
1140
+ for (let j = 1; j < x.length; j++) cw(x[j]) }
1141
+ cw(wbody)
1142
+ if (writes !== 2) continue
1143
+ if (C != null) wraps.push([nm, [0, C - 1]])
1144
+ // symbolic bound (`let SEQLEN = 5` — mutable): the invariant is
1145
+ // si ∈ [0, C-1] RELATIVE to C's runtime value — recorded as a symbolic
1146
+ // hull for reads BEFORE the increment (the versioning guard closes it
1147
+ // with `C ≥ entryHi+1 ∧ C ≤ len`); no numeric env seeding is possible
1148
+ else symWraps.push([nm, { lo: 0, hiName: Cname, hiBias: -1, entryHi: e0[1] }, a2])
1149
+ }
1150
+ }
1151
+ killAssigned(n)
1152
+ if (iv) env.set(iv, [entry[0], brange[1] - 1])
1153
+ for (const [nm, r] of wraps) if (!closureWrites.has(nm)) env.set(nm, r)
1154
+ for (const [nm, h, incNode] of symWraps) if (!closureWrites.has(nm)) symEnv.set(nm, { h, incNode })
1155
+ visit(c) // cond accesses (`while (keys[h] !== k)`) see the seeded ranges too
1156
+ for (let k = 2; k < n.length; k++) visit(n[k])
1157
+ if (iv) env.set(iv, [Math.min(entry[0], brange[0]), Math.max(entry[1], brange[1])])
1158
+ for (const [nm, r] of wraps) if (!closureWrites.has(nm)) env.set(nm, r) // holds at exit too
1159
+ for (const [nm] of symWraps) symEnv.delete(nm)
1160
+ return
1161
+ }
1162
+ if (op === 'do' || op === 'for-of' || op === 'for-in' || op === 'label'
1163
+ || op === 'switch' || op === 'try') {
1164
+ killAssigned(n) // unknown trip count / branch selection: no interval survives entry
1165
+ for (let k = 1; k < n.length; k++) visit(n[k])
1166
+ return
1167
+ }
1168
+ if (op === 'if') {
1169
+ const [, c, thenB, elseB] = n
1170
+ visit(c)
1171
+ const save = new Map(env)
1172
+ const rT = refine(c, false)
1173
+ if (rT && !closureWrites.has(rT[0])) env.set(rT[0], rT[1])
1174
+ visit(thenB)
1175
+ const afterThen = new Map(env)
1176
+ env.clear(); for (const [k2, v2] of save) env.set(k2, v2)
1177
+ // the fall-through state refines by ¬cond whether or not an else arm exists
1178
+ // (`if (xi >= 64) xi = 63` leaves xi < 64 on the other path)
1179
+ const rE = refine(c, true)
1180
+ if (rE && !closureWrites.has(rE[0])) env.set(rE[0], rE[1])
1181
+ if (elseB !== undefined) visit(elseB)
1182
+ // join: both arms merge (min lo, max hi); known-in-one-arm-only joins unknown
1183
+ const keys = new Set([...afterThen.keys(), ...env.keys()])
1184
+ for (const k2 of keys) {
1185
+ const a = afterThen.get(k2), b = env.get(k2)
1186
+ env.set(k2, a && b ? [Math.min(a[0], b[0]), Math.max(a[1], b[1])] : null)
1187
+ }
1188
+ return
1189
+ }
1190
+ if (op === '()' && n.length === 2) { visit(n[1]); return } // grouping, not a call
1191
+ if (op === '()' || op === 'new') { // a call may reassign module globals
1192
+ for (let k = 1; k < n.length; k++) visit(n[k])
1193
+ for (const [k2] of env) if (!closureWrites.has(k2) && (ctx.scope?.globalTypes?.has?.(k2) || ctx.types?.typedElem?.has?.(k2))) env.set(k2, null)
1194
+ return
1195
+ }
1196
+ for (let k = 1; k < n.length; k++) visit(n[k])
1197
+ }
1198
+ // the function root is itself an `=>` node — enter its body; only NESTED closures skip
1199
+ // A rewrite pass (peelClampedStencil) stamps `_rangeFacts` — theorems about names
1200
+ // inside the stamped subtree (`ci ∈ [0, bound-1]`, established by ITS soundness
1201
+ // argument). They intersect every env write of that name while the subtree walks.
1202
+ const visitWithFacts = (n) => {
1203
+ const popped = []
1204
+ for (const [name, boundName] of n._rangeFacts) {
1205
+ const B = boundName != null ? ev(boundName) : null
1206
+ if (B && !activeFacts.has(name)) { activeFacts.set(name, [0, B[1] - 1]); popped.push(name) }
1207
+ }
1208
+ const facts = n._rangeFacts
1209
+ n._rangeFacts = null // re-entry brake (the self-host subset has no delete)
1210
+ visit(n)
1211
+ n._rangeFacts = facts
1212
+ for (const name of popped) activeFacts.delete(name)
1213
+ }
1214
+ visit(Array.isArray(body) && body[0] === '=>' ? body[body.length - 1] : body)
1215
+ }
1216
+
1217
+ /** Every write to `iv` in `node` is a strictly-positive unit step (++iv / iv+=1 /
1218
+ * iv=(iv+1)|0 / iv=iv+1) — the while-iv interval model requires monotone advance. */
1219
+ function ivMonotoneInc(node, iv) {
1220
+ if (!Array.isArray(node)) return true
1221
+ if ((node[0] === '++' || node[0] === '--') && node[1] === iv) return node[0] === '++'
1222
+ if (ASSIGN_OPS.has(node[0]) && node[1] === iv) {
1223
+ if (node[0] === '+=' && intLiteralValue(node[2]) >= 1) return true
1224
+ if (node[0] === '=') {
1225
+ let rhs = node[2]
1226
+ if (Array.isArray(rhs) && rhs[0] === '|' && intLiteralValue(rhs[2]) === 0) rhs = rhs[1]
1227
+ if (Array.isArray(rhs) && rhs[0] === '+' && rhs.length === 3
1228
+ && ((rhs[1] === iv && intLiteralValue(rhs[2]) >= 1) || (rhs[2] === iv && intLiteralValue(rhs[1]) >= 1))) return true
1229
+ }
1230
+ return false
1231
+ }
1232
+ for (let k = 1; k < node.length; k++) if (!ivMonotoneInc(node[k], iv)) return false
1233
+ return true
1234
+ }
1235
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**='])
1236
+ const NARROW_ELEM_RANGE = {
1237
+ 'new.Int8Array': [-128, 127], 'new.Uint8Array': [0, 255], 'new.Uint8ClampedArray': [0, 255],
1238
+ 'new.Int16Array': [-32768, 32767], 'new.Uint16Array': [0, 65535],
1239
+ 'new.Int8Array.view': [-128, 127], 'new.Uint8Array.view': [0, 255], 'new.Uint8ClampedArray.view': [0, 255],
1240
+ 'new.Int16Array.view': [-32768, 32767], 'new.Uint16Array.view': [0, 65535],
1241
+ }
1242
+
1243
+ /** Memoized per-function set of interval-proven `recv[idx]` keys. */
1244
+ export function intervalProvenIdx(ctx) {
1245
+ const body = ctx.func?.body
1246
+ if (!Array.isArray(body)) return NO_INTERVAL_PROVEN
1247
+ if (ctx.func._ipBody === body) return ctx.func.ipProven
1248
+ const out = new Set(), ranges = new Map()
1249
+ const lens = (name) => ctx.types.typedLen?.get(name) ?? ctx.scope?.globalTypedLen?.get(name) ?? null
1250
+ scanIntervalIdx(body, out, lens, ranges)
1251
+ ctx.func.ipProven = out
1252
+ ctx.func.ipRanges = ranges
1253
+ ctx.func._ipBody = body
1254
+ return out
1255
+ }
1256
+
1257
+ /** Idx-interval hulls the walk computed but could not discharge (receiver length
1258
+ * unknown) — the versioning guard closes them with a runtime `hi < len`. */
1259
+ export function intervalIdxRanges(ctx) {
1260
+ intervalProvenIdx(ctx)
1261
+ return ctx.func?.ipRanges || NO_INTERVAL_RANGES
1262
+ }
1263
+ const NO_INTERVAL_RANGES = new Map()
1264
+ const NO_INTERVAL_PROVEN = new Set()
1265
+
251
1266
  // === Loop unroll / AST transforms (emit + plan) ===
252
1267
 
253
1268
  export const MAX_SMALL_FOR_UNROLL = 8
@@ -304,7 +1319,9 @@ export function cloneWithSubst(node, subst, rename = null) {
304
1319
  if (node === name) return [null, value]
305
1320
  if (!Array.isArray(node)) return node
306
1321
  if (node[0] === '=>') return node
307
- return node.map(x => cloneWithSubst(x, name, value))
1322
+ const out = node.map(x => cloneWithSubst(x, name, value))
1323
+ stampClonedIdxProof(node, out)
1324
+ return out
308
1325
  }
309
1326
  const ren = rename instanceof Map ? rename : new Map()
310
1327
  if (typeof node === 'string') {
@@ -317,7 +1334,24 @@ export function cloneWithSubst(node, subst, rename = null) {
317
1334
  if (op === '=>') return node
318
1335
  if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, ren), node[2]]
319
1336
  if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, ren)]
320
- return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, ren))
1337
+ const out = node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, ren))
1338
+ stampClonedIdxProof(node, out)
1339
+ return out
1340
+ }
1341
+
1342
+ /** Proof carry-over for clones: substitution only SHRINKS an index's value set (an
1343
+ * unrolled iv becomes one literal from its proven range), so a proven typed access
1344
+ * stays proven under its post-substitution key — without this, loop unrolling
1345
+ * silently re-checks every access the interval walk or a versioned guard covered. */
1346
+ function stampClonedIdxProof(node, out) {
1347
+ if (node[0] !== '[]' || node.length !== 3 || typeof node[1] !== 'string' || out[1] !== node[1]) return
1348
+ const k = idxKey(node[1], node[2])
1349
+ const ip = intervalProvenIdx(ctx) // memoized; NO_INTERVAL_PROVEN when no function ctx
1350
+ if (ip.has(k)) ip.add(idxKey(out[1], out[2]))
1351
+ const rng = ctx.func?.ipRanges?.get(k)
1352
+ if (rng != null) ctx.func.ipRanges.set(idxKey(out[1], out[2]), rng) // hulls survive substitution too
1353
+ const owner = ctx.types?.assumedBounds?.get(k)
1354
+ if (owner != null) ctx.types.assumedBounds.set(idxKey(out[1], out[2]), owner)
321
1355
  }
322
1356
 
323
1357
  const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
@@ -451,6 +1485,11 @@ export function exprType(expr, locals) {
451
1485
  // The membership lives in one place — `propValType` (src/kind-traits.js).
452
1486
  if (op === '.') {
453
1487
  if (typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER) return 'i32'
1488
+ // Strict-int32 schema slot (write census): the read emits as a raw i32
1489
+ // (emitSchemaSlotRead's trunc route), so the static local-slot classifier
1490
+ // must agree — `const x = hitX ? p.x : nx` then declares x i32 instead of
1491
+ // f64, and the whole ternary/arith chain stays in int registers.
1492
+ if (typeof args[0] === 'string' && ctx.schema?.slotI32CertainAt?.(args[0], args[1])) return 'i32'
454
1493
  return 'f64'
455
1494
  }
456
1495
  // Comparisons, logical-not, and unsigned shift always yield an i32 — a boolean,
@@ -552,108 +1591,183 @@ const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
552
1591
  const INT_CLOSED_OPS = new Set(['+', '-', '*']) // `%` handled separately — int only for nonzero divisor
553
1592
  const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
554
1593
 
555
- function collectIntDefs(body) {
1594
+ // `capturedNames`, when given, additionally folds in defs found INSIDE nested
1595
+ // arrow bodies — but ONLY for names in that set, and only when found there;
1596
+ // the top-level (own-scope) collection below is completely unaffected either
1597
+ // way. Default callers (no `capturedNames`) get byte-identical behavior to
1598
+ // before: an ordinary local can't be assigned from inside a nested arrow
1599
+ // without becoming a closure capture, so stopping at `=>` is exact there. A
1600
+ // captured (boxed) variable is exactly the case where it CAN — its cell-type
1601
+ // decision (src/compile/index.js's closure-capture narrowing) needs those
1602
+ // writes too, wherever in the closure tree they live. Doesn't track arrow-body
1603
+ // shadowing (a same-named nested param/`let` re-declaring `name`) — same
1604
+ // direction of imprecision `boxedCaptures`' own `findMutations` already
1605
+ // accepts for the boxing decision itself: at worst this forgoes the i32 cell
1606
+ // fast path (falls back to the always-safe f64 cell), it can never mis-widen
1607
+ // an actually-non-integer write to i32.
1608
+ function collectIntDefs(body, capturedNames) {
556
1609
  const defs = new Map()
557
- const pushDef = (name, rhs) => {
1610
+ const pushDef = (name, rhs, inArrow) => {
1611
+ if (inArrow && !capturedNames.has(name)) return
558
1612
  let list = defs.get(name)
559
1613
  if (!list) { list = []; defs.set(name, list) }
560
1614
  list.push(rhs)
561
1615
  }
562
- const collect = (node) => {
1616
+ const collect = (node, inArrow) => {
563
1617
  if (!Array.isArray(node)) return
564
1618
  const [op, ...args] = node
565
- if (op === '=>') return
1619
+ if (op === '=>') {
1620
+ if (capturedNames && capturedNames.size) collect(args[1], true)
1621
+ return
1622
+ }
566
1623
  if (op === 'let' || op === 'const') {
567
1624
  for (const a of args)
568
- if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
1625
+ if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2], inArrow)
569
1626
  } else if (op === '=' && typeof args[0] === 'string') {
570
- pushDef(args[0], args[1])
1627
+ pushDef(args[0], args[1], inArrow)
571
1628
  } else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
572
1629
  !CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
573
- pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
1630
+ pushDef(args[0], [op.slice(0, -1), args[0], args[1]], inArrow)
574
1631
  } else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
575
- pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
1632
+ pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]], inArrow)
576
1633
  }
577
- for (const a of args) collect(a)
1634
+ for (const a of args) collect(a, inArrow)
578
1635
  }
579
- collect(body)
1636
+ collect(body, false)
580
1637
  return defs
581
1638
  }
582
1639
 
583
- function makeIsIntExpr(intCertain) {
584
- return function isIntExpr(expr) {
585
- if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
586
- if (typeof expr === 'boolean') return true
587
- if (typeof expr === 'string') return intCertain.get(expr) === true
588
- if (!Array.isArray(expr)) return false
1640
+ // The integer lattice is 3-level:
1641
+ // 0 not provably integer-valued
1642
+ // 1 integral, but unbounded magnitude and/or -0-capable (`+ - *` closure,
1643
+ // floor/ceil/round/trunc, `>>>` a uint32 can exceed int32, `%`/unary
1644
+ // minus -0 producers)
1645
+ // 2 STRICT int32: the value is exactly representable as a signed 32-bit
1646
+ // int and is never -0 — i.e. `i32.trunc_sat_f64_s` of its f64 form is
1647
+ // an exact round-trip. Producers: int32-range literals, booleans,
1648
+ // comparisons, the signed bitwise ops (`| & ^ ~ << >>`), Math.imul /
1649
+ // clz32, and meets of those through ?:/&&/||.
1650
+ // Level ≥1 is the historical `isIntExpr` (ToNumber-skip / floor-elision
1651
+ // consumers); level 2 feeds raw-i32 slot loads and i32 local typing, where
1652
+ // saturation or a lost -0 would be a WRONG VALUE, not a lost optimization.
1653
+ const INT_MATH_FNS_I32 = new Set(['imul', 'clz32'])
1654
+ const _numLevel = (v) => typeof v === 'boolean' ? 2
1655
+ : typeof v !== 'number' || !Number.isInteger(v) || Object.is(v, -0) ? 0
1656
+ : v >= -2147483648 && v <= 2147483647 ? 2 : 1
1657
+
1658
+ function makeIntLevelExpr(intLevels, slotLevelOf) {
1659
+ return function levelOf(expr) {
1660
+ if (typeof expr === 'number' || typeof expr === 'boolean') return _numLevel(expr)
1661
+ if (typeof expr === 'string') return intLevels.get(expr) ?? 0
1662
+ if (!Array.isArray(expr)) return 0
589
1663
  const sv = staticValue(expr)
590
- if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
1664
+ if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return 0
591
1665
  const [op, ...args] = expr
592
- if (op == null) {
593
- const v = args[0]
594
- if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
595
- if (typeof v === 'boolean') return true
596
- return false
597
- }
598
- if (INT_BIT_OPS.has(op) || CMP_OPS.has(op)) return true
599
- if (op === '.')
600
- return typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER
1666
+ if (op == null) return _numLevel(args[0])
1667
+ if (op === '>>>') return 1 // uint32: up to 2^32-1, exceeds int32
1668
+ if (INT_BIT_OPS.has(op) || CMP_OPS.has(op)) return 2
1669
+ if (op === '.') {
1670
+ // Slot-census resolver (analyzeSchemaSlotIntCertain's optimistic
1671
+ // fixpoint): a censused slot answers definitively — including 0
1672
+ // (a known non-int write beats the val-kind fallback below).
1673
+ if (slotLevelOf && typeof args[0] === 'string') {
1674
+ const r = slotLevelOf(args[0], args[1])
1675
+ if (r != null) return r
1676
+ }
1677
+ return typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER ? 1 : 0
1678
+ }
601
1679
  if (INT_CLOSED_OPS.has(op)) {
602
- const a = isIntExpr(args[0])
603
- const b = args[1] != null ? isIntExpr(args[1]) : a
604
- return a && b
1680
+ const a = levelOf(args[0])
1681
+ const b = args[1] != null ? levelOf(args[1]) : a
1682
+ return a && b ? 1 : 0 // integral-closed, range-open
605
1683
  }
606
1684
  // `a % b` is integer-valued only when b is a provably-nonzero integer
607
1685
  // constant — `a % 0` is NaN, which is not an integer. A runtime or zero
608
1686
  // divisor leaves the expression non-int (f64), so result-narrowing won't
609
1687
  // truncate a NaN remainder to 0 and floor-elision won't drop a NaN.
1688
+ // Never strict: `-5 % 5` is -0.
610
1689
  if (op === '%') {
611
1690
  const bv = staticValue(args[1])
612
- return bv !== NO_VALUE && typeof bv === 'number' && bv !== 0 && Number.isInteger(bv) && isIntExpr(args[0])
1691
+ return bv !== NO_VALUE && typeof bv === 'number' && bv !== 0 && Number.isInteger(bv) && levelOf(args[0]) ? 1 : 0
613
1692
  }
614
- if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
615
- if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
616
- if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
1693
+ if (op === 'u-') return levelOf(args[0]) ? 1 : 0 // -(0) is -0; -(-2^31) exceeds int32
1694
+ if (op === 'u+') return levelOf(args[0]) // ToNumber identity on an int
1695
+ if (op === '?:') return Math.min(levelOf(args[1]), levelOf(args[2]))
1696
+ if (op === '&&' || op === '||') return Math.min(levelOf(args[0]), levelOf(args[1]))
617
1697
  if (op === '()') {
618
1698
  const c = args[0]
619
- if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
620
- if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
1699
+ const fn = typeof c === 'string' && c.startsWith('math.') ? c.slice(5)
1700
+ : Array.isArray(c) && c[0] === '.' && c[1] === 'Math' ? c[2] : null
1701
+ if (fn && INT_MATH_FNS.has(fn)) return INT_MATH_FNS_I32.has(fn) ? 2 : 1
621
1702
  }
622
- return false
1703
+ return 0
623
1704
  }
624
1705
  }
625
1706
 
626
- /** Monotone fixpoint over binding defs in `body`. Map name intCertain. */
627
- export function intCertainMap(body) {
628
- const defs = collectIntDefs(body)
1707
+ // Adapt a boolean-or-level slot resolver to the level contract (a boolean
1708
+ // `true` caps at level 1 — weak evidence stays weak).
1709
+ const _slotLevelAdapter = (slotIntOf) => slotIntOf
1710
+ ? (obj, prop) => { const r = slotIntOf(obj, prop); return r == null ? null : r === true ? 1 : r === false ? 0 : r }
1711
+ : null
1712
+
1713
+ /** Monotone fixpoint over binding defs in `body`. Map name → intCertain.
1714
+ * `capturedNames` (optional): also fold in defs of these specific names found
1715
+ * inside nested arrow bodies — see collectIntDefs. Only src/compile/index.js's
1716
+ * boxed-cell narrowing passes this; every other caller keeps the default
1717
+ * own-scope-only behavior unchanged. */
1718
+ /** Monotone-down level fixpoint over binding defs in `body`:
1719
+ * Map name → 0|1|2 (see the lattice above `makeIntLevelExpr`).
1720
+ * `slotLevelOf(obj, prop)` → 0|1|2|null resolves `.prop` reads. */
1721
+ export function intLevelMap(body, capturedNames, slotLevelOf) {
1722
+ const defs = collectIntDefs(body, capturedNames)
629
1723
  if (defs.size === 0) return new Map()
630
- const intCertain = new Map()
631
- for (const name of defs.keys()) intCertain.set(name, true)
1724
+ const levels = new Map()
1725
+ for (const name of defs.keys()) levels.set(name, 2)
632
1726
  // A parameter has no def in `body` — its entry value is whatever the caller
633
1727
  // passed. For an f64 param (JS-number ABI) that is an arbitrary real, so a
634
1728
  // reassigned f64 param is NOT integer-certain: a self/int reassignment
635
1729
  // (`p = p`, `p = p + 1`) would otherwise vacuously satisfy the optimistic
636
- // fixpoint, since `isIntExpr(p)` reads p's own provisional `true`. Seed f64
637
- // params false so the unknown entry value grounds the lattice; i32-narrowed
638
- // params (integer ABI) stay certain. Seeding false is always conservative —
1730
+ // fixpoint, since `levelOf(p)` reads p's own provisional 2. Seed f64
1731
+ // params 0 so the unknown entry value grounds the lattice; i32-narrowed
1732
+ // params (integer ABI) stay strict. Seeding 0 is always conservative —
639
1733
  // at worst it re-applies a floor/round that was a runtime no-op — so a
640
1734
  // mismatched ctx.func.current (whole-program intExprChecker callers) can only
641
1735
  // forgo an optimization, never miscompile.
642
1736
  for (const p of ctx.func.current?.params || [])
643
- if (p.type !== 'i32' && intCertain.has(p.name)) intCertain.set(p.name, false)
644
- const isIntExpr = makeIsIntExpr(intCertain)
1737
+ if (p.type !== 'i32' && levels.has(p.name)) levels.set(p.name, 0)
1738
+ const levelOf = makeIntLevelExpr(levels, slotLevelOf)
645
1739
  let changed = true
646
1740
  while (changed) {
647
1741
  changed = false
648
1742
  for (const [name, rhsList] of defs) {
649
- if (!intCertain.get(name)) continue
650
- if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
1743
+ const cur = levels.get(name)
1744
+ if (!cur) continue
1745
+ let next = cur
1746
+ for (const rhs of rhsList) { const l = levelOf(rhs); if (l < next) next = l; if (!next) break }
1747
+ if (next !== cur) { levels.set(name, next); changed = true }
651
1748
  }
652
1749
  }
653
- return intCertain
1750
+ return levels
1751
+ }
1752
+
1753
+ /** Monotone fixpoint over binding defs in `body`. Map name → intCertain
1754
+ * (boolean — the level ≥1 projection; see `intLevelMap` for the raw levels). */
1755
+ export function intCertainMap(body, capturedNames, slotIntOf) {
1756
+ const levels = intLevelMap(body, capturedNames, _slotLevelAdapter(slotIntOf))
1757
+ const out = new Map()
1758
+ for (const [name, l] of levels) out.set(name, l >= 1)
1759
+ return out
654
1760
  }
655
1761
 
656
1762
  /** Returns `expr => boolean` — integer-shaped expressions in `body`. */
657
- export function intExprChecker(body) {
658
- return makeIsIntExpr(intCertainMap(body))
1763
+ export function intExprChecker(body, slotIntOf) {
1764
+ const slotLevelOf = _slotLevelAdapter(slotIntOf)
1765
+ const levelOf = makeIntLevelExpr(intLevelMap(body, undefined, slotLevelOf), slotLevelOf)
1766
+ return (expr) => levelOf(expr) >= 1
1767
+ }
1768
+
1769
+ /** Returns `expr => 0|1|2` over `body`'s level fixpoint — the strict-i32
1770
+ * sibling of `intExprChecker` (slot census / raw-i32 consumers). */
1771
+ export function intLevelChecker(body, slotLevelOf) {
1772
+ return makeIntLevelExpr(intLevelMap(body, undefined, slotLevelOf), slotLevelOf)
659
1773
  }