jz 0.9.0 → 0.9.2

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.
package/src/type.js CHANGED
@@ -26,8 +26,8 @@ import { typedElemAux } from '../layout.js'
26
26
  * NOT be mistaken for a typed-array construction (else its global misdispatches as
27
27
  * a TypedArray, e.g. `map.set(k,v)` lowering to `arr.set(src,offset)`). */
28
28
  const TYPED_FAMILY_CTORS = new Set([
29
- 'Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
30
- 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'ArrayBuffer', 'DataView',
29
+ 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
30
+ 'Float16Array', 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'ArrayBuffer', 'DataView',
31
31
  ])
32
32
 
33
33
  /** Extract typed-array ctor name ('new.Float32Array', 'new.Int8Array.view', etc) from RHS,
@@ -208,7 +208,10 @@ export function affineIdxOfIV(idx, iv, body, env) {
208
208
  return invariantIdxExpr(e, iv, body, env) ? { a: 0, slots: [{ k: 1, e }], bConst: 0 } : null
209
209
  }
210
210
  const r = aff(idx)
211
- return r && r.a >= 0 && Number.isInteger(r.a) && Number.isInteger(r.bConst)
211
+ // Negative iv-coefficients are admitted (the mirror index `N−k` of symmetric
212
+ // fills): the guard emitter picks extremes by the SIGN of a — a·iv is maximal
213
+ // at maxIv for a ≥ 0 but at ENTRY for a < 0, and minimal at the other end.
214
+ return r && Number.isInteger(r.a) && Number.isInteger(r.bConst)
212
215
  && r.slots.every(t => Number.isInteger(t.k)) ? r : null
213
216
  }
214
217
 
@@ -268,9 +271,24 @@ export function bodyAffineEnv(body, iv) {
268
271
  * DROPPED (its first iterations are genuinely OOB — the checked form is the
269
272
  * semantics, a guard would just always fail). */
270
273
  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
274
+ // `&&`-cond whiles (`while (len < max && src[j+len] === src[ip+len]) len++`
275
+ // — the LZ match scan): the countable bound must be the LEFTMOST conjunct.
276
+ // Every later conjunct short-circuits AFTER it, so its accesses run only at
277
+ // iv < bound (exactly the pre-increment extent), and a false conjunct only
278
+ // exits the loop EARLY — the iv range never grows. The rest conjuncts ride
279
+ // into both arms verbatim; their typed accesses are candidates (scanned
280
+ // after the body so a same-key BODY access — possibly post-increment, wider
281
+ // — registers its extent first).
282
+ let condRest = null
283
+ let c = cond
284
+ while (Array.isArray(c) && c[0] === '&&' && Array.isArray(c[1])) {
285
+ condRest = condRest == null ? c[2] : ['&&', c[2], condRest] // scan-only bag
286
+ c = c[1]
287
+ }
288
+ if (!Array.isArray(c) || (c[0] !== '<' && c[0] !== '<=') || typeof c[1] !== 'string') return null
272
289
  if (containsNestedClosure(body)) return null
273
- const iv = cond[1], incl = cond[0] === '<='
290
+ if (condRest != null && containsNestedClosure(condRest)) return null
291
+ const iv = c[1], incl = c[0] === '<='
274
292
  if (redeclaresName(body, iv)) return null
275
293
  // iv start: a static init decl (`for (let i = 0; …)`) folds the lo conjunct;
276
294
  // otherwise (while-shapes: `let i = 0; while (i < n) …`) the guard reads the
@@ -348,7 +366,7 @@ export function versionableTypedFor(init, cond, step, body, locals, entryHint =
348
366
  if (L == null || L < 1 || !Number.isInteger(L)) return null
349
367
  bump = L
350
368
  } else return null
351
- const bound = cond[2]
369
+ const bound = c[2]
352
370
  // bKind drives the guard's conversion to a max-iv i64:
353
371
  // 'i32' — literal, i32-machine name, or a typed receiver's .length: exact extend;
354
372
  // 'f64' — any other stable name (an untyped param, a NaN-boxed unknown): the
@@ -370,6 +388,24 @@ export function versionableTypedFor(init, cond, step, body, locals, entryHint =
370
388
  if (inds) for (const nm of inds.keys()) env.set(nm, null)
371
389
  const cands = []
372
390
  const seen = new Set()
391
+ // A body-advanced iv (bump > 0) exceeds bound−1 only AFTER its increment
392
+ // runs — accesses in top-level statements strictly BEFORE the write see
393
+ // iv ≤ bound−1 and need no widening. The canonical tail-increment while
394
+ // (`…reads…; k++`) then guards exactly; only genuinely post-increment
395
+ // accesses widen. Nested/mid-expression writes keep everything `post`.
396
+ let ivWriteAt = -1
397
+ const seqBody = Array.isArray(body) && (body[0] === '{}' || body[0] === ';')
398
+ if (bump > 0 && seqBody) {
399
+ for (let s = 1; s < body.length; s++) {
400
+ const st = body[s]
401
+ if (Array.isArray(st) && ((st[0] === '=' || st[0] === '+=') && st[1] === iv || (st[0] === '++' || st[0] === '--') && st[1] === iv)) { ivWriteAt = s; break }
402
+ }
403
+ }
404
+ let scanTop = -1 // current top-level statement index during scan
405
+ // cond-rest accesses are exactly pre-increment: short-circuit order proves
406
+ // they evaluate only when `iv < bound` already held this iteration
407
+ let forcePre = false
408
+ const isPost = () => !forcePre && bump > 0 && (ivWriteAt === -1 || scanTop === -1 || scanTop >= ivWriteAt)
373
409
  const scan = (n) => {
374
410
  if (!Array.isArray(n)) return
375
411
  if (n[0] === '[]' && n.length === 3 && typeof n[1] === 'string' && n[1] !== iv
@@ -390,7 +426,7 @@ export function versionableTypedFor(init, cond, step, body, locals, entryHint =
390
426
  && !(aff.slots.length === 0 && startC != null && aff.a * startC + aff.bConst < 0)) {
391
427
  seen.add(key)
392
428
  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 })
429
+ cands.push({ recv: n[1], idx: n[2], a: aff.a, slots, bConst: aff.bConst, post: isPost() })
394
430
  } else {
395
431
  // LAST resort — beyond the affine model (masked ring cursors, wrap
396
432
  // idioms): an interval HULL the static walk bounded but couldn't
@@ -407,8 +443,16 @@ export function versionableTypedFor(init, cond, step, body, locals, entryHint =
407
443
  }
408
444
  for (let k = 1; k < n.length; k++) scan(n[k])
409
445
  }
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(' '))
446
+ if (seqBody) {
447
+ for (let s = 1; s < body.length; s++) { scanTop = s; scan(body[s]) }
448
+ scanTop = -1
449
+ } else scan(body)
450
+ // `&&`-cond rest conjuncts — scanned AFTER the body so a shared-key body
451
+ // access (potentially post-increment, wider extent) wins the seen-set
452
+ if (condRest != null) { forcePre = true; scan(condRest); forcePre = false }
453
+ // typeof-process guard, not globalThis.process — a bare `globalThis` read
454
+ // compiles to an env.globalThis import in the self-host build; typeof folds dead
455
+ if (typeof process !== 'undefined' && process.env.JZ_DBG_VS) console.error('VS', iv, 'cands', cands.length, 'bump', bump, 'ivWriteAt', ivWriteAt, 'body0', Array.isArray(body) ? body[0] : typeof body, cands.slice(0,4).map(c => c.recv + (c.range ? ':hull' : c.ind ? ':ind' : ':aff') + (c.post ? ':POST' : '')).join(' '))
412
456
  return cands.length
413
457
  ? { iv, ivKind: exprType(iv, locals) === 'i32' ? 'i32' : 'f64', startC, bump, bound, bKind, incl, stepBy, cands }
414
458
  : null
@@ -426,6 +470,14 @@ export function versionableTypedFor(init, cond, step, body, locals, entryHint =
426
470
  * top body (redeclaresName catches inner decls — a per-row offset slot must
427
471
  * not be read before its row exists). Unliftable levels simply keep their own
428
472
  * inner versioning during arm emission — graceful degradation, not a bail. */
473
+ /** The countable-iv name of a (possibly `&&`-chained) loop cond — the leftmost
474
+ * conjunct's lhs. Feeds the sibling-decl entryHint lookup for while-shapes. */
475
+ const condIvName = (cnd) => {
476
+ let c = cnd
477
+ while (Array.isArray(c) && c[0] === '&&' && Array.isArray(c[1])) c = c[1]
478
+ return Array.isArray(c) && (c[0] === '<' || c[0] === '<=') && typeof c[1] === 'string' ? c[1] : null
479
+ }
480
+
429
481
  export function versionableTypedNest(init, cond, step, body, locals) {
430
482
  if (containsNestedClosure(body)) return null
431
483
  const levels = []
@@ -478,8 +530,8 @@ export function versionableTypedNest(init, cond, step, body, locals) {
478
530
  continue
479
531
  }
480
532
  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)
533
+ && Array.isArray(st[1]) && condIvName(st[1]) != null) {
534
+ walkLoop(null, st[1], null, st[2], lastDecls.get(condIvName(st[1])) ?? null, false)
483
535
  } else if (Array.isArray(st) && st[0] === 'for' && st.length === 5) {
484
536
  walkLoop(st[1], st[2], st[3], st[4], null, false)
485
537
  } else scanStmts(st)
@@ -652,6 +704,15 @@ export function isUnitIncrement(step, name) {
652
704
  return false
653
705
  }
654
706
 
707
+ export function isUnitDecrement(step, name) {
708
+ if (!Array.isArray(step)) return false
709
+ if (step[0] === '--' && step[1] === name) return true
710
+ // postfix `i--` in value position lowers to `(--i) + 1`
711
+ if (step[0] === '+' && Array.isArray(step[1]) && step[1][0] === '--'
712
+ && step[1][1] === name && intLiteralValue(step[2]) === 1) return true
713
+ return false
714
+ }
715
+
655
716
  /** `let`/`const` re-declaration of `name` within `node` — does not cross `=>`
656
717
  * (a closure has its own scope; collection already stops at closure boundaries). */
657
718
  function redeclaresName(node, name) {
@@ -873,6 +934,9 @@ const ipOk = (v) => v != null && v[0] >= -IP_LIM && v[1] <= IP_LIM
873
934
  * assignments embedded in expressions). */
874
935
  function scanIntervalIdx(body, out, lens, ranges) {
875
936
  const env = new Map() // name → [lo, hi] | null (unknown)
937
+ // While-body fixpoint passes walk EXPLORATORILY — env may be transiently too
938
+ // narrow, so proof/hull recording is suppressed until the stable final pass.
939
+ let recording = true
876
940
  const symEnv = new Map() // name → { h: symbolic hull, incNode } — wrap cursors vs mutable bounds
877
941
  // names written inside ANY closure in this body: a later call can change them at
878
942
  // any point — they never hold a trusted interval
@@ -924,6 +988,14 @@ function scanIntervalIdx(body, out, lens, ranges) {
924
988
  const r = NARROW_ELEM_RANGE[ctx.types.typedElem?.get(x)]
925
989
  return r ?? null
926
990
  }
991
+ // `X.length` of a typed receiver with a known static length — the length-
992
+ // identity atom: `const n = a.length` binds a singleton, `(a.length-1)>>1`
993
+ // style index math evaluates exactly. Typed lengths are fixed for the
994
+ // binding's lifetime (the tracker drops the entry on any rebinding).
995
+ if ((op === '.' || op === '?.') && e.length === 3 && typeof x === 'string' && e[2] === 'length') {
996
+ const L = lens(x)
997
+ if (L != null) return [L, L]
998
+ }
927
999
  if (e.length === 2 && op === '()') return ev(x) // grouping, not a call
928
1000
  if (e.length === 2 && (op === '-' || op === 'u-')) { const v = ev(x); return ipOk(v) && v ? [-v[1], -v[0]] : null }
929
1001
  if (op === '?:' && e.length === 4) { // join of both arms, each under its refinement
@@ -967,29 +1039,55 @@ function scanIntervalIdx(body, out, lens, ranges) {
967
1039
  else if (op === '%' && B[0] === B[1] && B[0] > 0 && A[0] >= 0) r = [0, Math.min(A[1], B[0] - 1)]
968
1040
  return ipOk(r) ? r : null
969
1041
  }
970
- // condition refinement for if-arms: `name < K` / `name >= K` … over a known name
1042
+ // condition refinement for if-arms: `name < K` / `name >= K` … over a known name.
1043
+ // The lhs also admits the AFFINE form `name ± c` (`inl_i + 3 <= N` — the strided
1044
+ // codec cursors): the comparison re-biases to `name OP K∓c`. The rhs admits any
1045
+ // ACCESS-FREE expression the evaluator folds to a singleton (`src.length | 0`).
1046
+ const pureExpr = (e) => {
1047
+ if (!Array.isArray(e)) return true
1048
+ if (e[0] === '[]' || e[0] === '()' || e[0] === 'new' || e[0] === '?:' || e[0] === '=' || ASSIGN_OPS.has(e[0])) return false
1049
+ for (let k = 1; k < e.length; k++) if (!pureExpr(e[k])) return false
1050
+ return true
1051
+ }
971
1052
  const refine = (c, negate) => {
972
1053
  if (!Array.isArray(c) || c.length !== 3) return null
973
1054
  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)
1055
+ // rhs: an int literal/module const, a body-known interval (`xi >= ww`, or a
1056
+ // RANGE-valued name — `child < end` inside the extract loop, where end is
1057
+ // the enclosing downward iv: the sound bound is the range's op-side
1058
+ // endpoint, hi for </<=, lo for >/>=), or a folded access-free expression
976
1059
  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)
1060
+ let rLo = constInt(r), rHi = rLo
1061
+ if (rLo == null && rE) { rLo = rE[0]; rHi = rE[1] }
1062
+ if (rLo == null && Array.isArray(r) && pureExpr(r)) {
1063
+ const rr = ev(r)
1064
+ if (rr) { rLo = rr[0]; rHi = rr[1] }
1065
+ }
1066
+ if (rLo == null) return null
1067
+ if (Array.isArray(l) && l.length === 3 && (l[0] === '+' || l[0] === '-')) {
1068
+ const cR = intLiteralValue(l[2]), cL = intLiteralValue(l[1])
1069
+ if (typeof l[1] === 'string' && cR != null) { rLo = l[0] === '+' ? rLo - cR : rLo + cR; rHi = l[0] === '+' ? rHi - cR : rHi + cR; l = l[1] }
1070
+ else if (l[0] === '+' && typeof l[2] === 'string' && cL != null) { rLo = rLo - cL; rHi = rHi - cL; l = l[2] }
1071
+ }
1072
+ if (typeof l !== 'string') return null
1073
+ const v = env.get(l)
980
1074
  if (!v) return null
981
1075
  if (negate) op = op === '<' ? '>=' : op === '<=' ? '>' : op === '>' ? '<=' : op === '>=' ? '<'
982
1076
  : 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)]]
1077
+ if (op === '<') return [l, [v[0], Math.min(v[1], rHi - 1)]]
1078
+ if (op === '<=') return [l, [v[0], Math.min(v[1], rHi)]]
1079
+ if (op === '>') return [l, [Math.max(v[0], rLo + 1), v[1]]]
1080
+ if (op === '>=') return [l, [Math.max(v[0], rLo), v[1]]]
1081
+ if (op === '===') return [l, [Math.max(v[0], rLo), Math.min(v[1], rHi)]]
988
1082
  // ≠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]]]
1083
+ // exactly the toroidal-wrap ternary (`y === 0 ? h-1 : y-1`); singleton rhs only
1084
+ if (op === '!==' && rLo === rHi) return [l, [v[0] === rLo ? rLo + 1 : v[0], v[1] === rLo ? rLo - 1 : v[1]]]
991
1085
  return null
992
1086
  }
1087
+ // every conjunct of an `&&` chain holds where the whole condition held
1088
+ const refineAll = (c2) => Array.isArray(c2) && c2[0] === '&&'
1089
+ ? [...refineAll(c2[1]), ...refineAll(c2[2])]
1090
+ : (() => { const r = refine(c2, false); return r ? [r] : [] })()
993
1091
  const killAssigned = (n) => {
994
1092
  if (!Array.isArray(n)) return // descend into closures too — capture-writes stay dead
995
1093
  if (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--') {
@@ -1000,14 +1098,134 @@ function scanIntervalIdx(body, out, lens, ranges) {
1000
1098
  }
1001
1099
  for (let k = 1; k < n.length; k++) killAssigned(n[k])
1002
1100
  }
1101
+ // A canonical-iv range `iv ∈ [entry, B−1]` is a body-independent THEOREM only
1102
+ // while B is invariant: a body-written bound (`while (i < n) { …; n = 12 }`)
1103
+ // admits iv past the entry bound — the seed then "proved" raw OOB reads
1104
+ // (dist-reproduced on every canonical loop form). Every name the bound reads
1105
+ // must be unwritten AND undeclared in the body.
1106
+ const boundInvariant = (bexpr, body) => {
1107
+ if (bexpr == null) return false
1108
+ const s = new Set(); collectNames(bexpr, s)
1109
+ for (const bn of s) if (isReassigned(body, bn) || redeclaresName(body, bn)) return false
1110
+ return true
1111
+ }
1112
+ // ABRUPT EDGES. A `break` reaches the loop's exit — and a `continue` its back
1113
+ // edge — carrying the flow state AT the statement, which the fall-through walk
1114
+ // never sees (`if (c) { x = BIG; break } x = 0` exits with x = BIG). Loop walks
1115
+ // push a frame; break/continue snapshot env into it; exits/joins hull the
1116
+ // snapshots in. Bare break binds to the innermost frame (a `switch` frame
1117
+ // swallows it); bare continue to the innermost LOOP frame; labeled forms can
1118
+ // cross any number of frames, so they conservatively feed every open one.
1119
+ const loopStack = [] // { kind: 'loop' | 'switch', breaks: [], continues: [] }
1120
+ const hullInto = (snap) => {
1121
+ for (const k2 of new Set([...env.keys(), ...snap.keys()])) {
1122
+ const a = env.get(k2), b = snap.get(k2)
1123
+ env.set(k2, a && b ? [Math.min(a[0], b[0]), Math.max(a[1], b[1])] : null)
1124
+ }
1125
+ }
1126
+ // LOOP BODY FIXPOINT (2-round widening). Pass A walks from the ENTRY state
1127
+ // (∩ cond) and yields the back-edge state; the JOIN hulls entry with it (a
1128
+ // name known on only one edge → null); pass B re-walks from join∩cond and
1129
+ // any name whose back-edge escapes its join widens to unknown; the FINAL
1130
+ // pass walks the stable env with proof recording ON, leaving env at the
1131
+ // loop invariant. `seedFn` re-applies body-independent theorems (canonical
1132
+ // iv ranges, wrap cursors) each pass; `condNode` refines at body top,
1133
+ // descending `&&` (both conjuncts hold when the loop is entered).
1134
+ const loopFixpoint = (seedFn, walkFn, condNode, exitBodyEnd = false) => {
1135
+ const applyCond = () => { if (condNode != null) for (const r of refineAll(condNode)) if (!closureWrites.has(r[0])) env.set(r[0], r[1]) }
1136
+ const restore = (m) => { env.clear(); for (const [k2, v2] of m) env.set(k2, v2) }
1137
+ // every pass walks under a loop frame: continue edges are back-edges too,
1138
+ // so their snapshots hull into the pass-end state before any join/verify
1139
+ const walkPass = () => {
1140
+ const lc = { kind: 'loop', breaks: [], continues: [] }
1141
+ loopStack.push(lc); walkFn(); loopStack.pop()
1142
+ for (const s of lc.continues) hullInto(s)
1143
+ return lc
1144
+ }
1145
+ const entryEnv = new Map(env)
1146
+ const prevRec = recording
1147
+ recording = false
1148
+ seedFn(); applyCond(); walkPass() // pass A: discovery
1149
+ // WIDENING JOIN: an escaping bound widens to the i32 extreme instead of the
1150
+ // one-step hull — the pass-B seed's cond refinement then clamps it to the
1151
+ // loop bound. This is what turns `for (; x + 3 <= N; x += 3)` into the
1152
+ // invariant x ∈ [0, N−3] (the strided-accumulator class) rather than
1153
+ // null: hull(entry, one step) can never contain step №2, so without the
1154
+ // widen every advancing cursor escapes to unknown.
1155
+ const joined = new Map()
1156
+ for (const k2 of new Set([...entryEnv.keys(), ...env.keys()])) {
1157
+ const a = entryEnv.get(k2), b = env.get(k2)
1158
+ joined.set(k2, a && b
1159
+ ? [b[0] < a[0] ? -IP_LIM : Math.min(a[0], b[0]), b[1] > a[1] ? IP_LIM : Math.max(a[1], b[1])]
1160
+ : null)
1161
+ }
1162
+ restore(joined); seedFn(); applyCond(); walkPass() // pass B: verify
1163
+ // the back edge re-evaluates the condition before re-entering the body, so
1164
+ // the state to verify against the invariant is walk-end ∩ cond
1165
+ applyCond()
1166
+ for (const [k2, v2] of env) {
1167
+ const j = joined.get(k2)
1168
+ if (!(v2 && j && v2[0] >= j[0] && v2[1] <= j[1])) joined.set(k2, null)
1169
+ }
1170
+ // NARROWING (≤2 decreasing passes): the widened invariant is sound but
1171
+ // loose — a name with no cond conjunct to re-clamp it sits at ±IP_LIM even
1172
+ // when the loop's true range is finite (`i = child` copy chains: i only
1173
+ // ever receives root- or cond-clamped child-values, so hull(entry,
1174
+ // end-state) is the real invariant). Each pass recomputes the hull from
1175
+ // the stable state — every reachable back-edge state ⊆ F(joined ∩ cond),
1176
+ // so hull(entry, F(joined ∩ cond)) contains them all and only TIGHTENS
1177
+ // (meet with the previous invariant keeps the sequence decreasing).
1178
+ //
1179
+ // GATE (exact, not heuristic — a raw compile-time win jz.wasm pays for):
1180
+ // ONLY a name at an ±IP_LIM endpoint can narrow. The join gave every
1181
+ // non-escaping name its exact one-step hull (min/max of entry ∪ back-edge),
1182
+ // which is already the tightest interval containing both edges — a fresh
1183
+ // walk reproduces the same stable end-state, so the meet is a no-op there.
1184
+ // The escaped names (widened to the sentinel) are the sole candidates. If
1185
+ // NONE widened, skip both extra body walks (the common case: cond-clamped
1186
+ // ivs never escape). Turns the heapsort-class cost into zero on every
1187
+ // ordinary loop.
1188
+ const widened = () => {
1189
+ for (const [, j] of joined) if (j && (j[0] === -IP_LIM || j[1] === IP_LIM)) return true
1190
+ return false
1191
+ }
1192
+ for (let np = 0; np < 2 && widened(); np++) {
1193
+ restore(joined); seedFn(); applyCond(); walkPass(); applyCond()
1194
+ let changed = false
1195
+ for (const [k2, j] of joined) {
1196
+ const a = entryEnv.get(k2), b = env.get(k2)
1197
+ if (!j || !a || !b) continue
1198
+ const nl = Math.max(j[0], Math.min(a[0], b[0])), nh = Math.min(j[1], Math.max(a[1], b[1]))
1199
+ if (nl > j[0] || nh < j[1]) { joined.set(k2, [nl, nh]); changed = true }
1200
+ }
1201
+ if (!changed) break
1202
+ }
1203
+ recording = prevRec
1204
+ restore(joined); seedFn(); applyCond()
1205
+ const lcF = walkPass() // FINAL: record on the stable env
1206
+ // exit state:
1207
+ // - default: the invariant (joined) — sound for any trip count.
1208
+ // - exitBodyEnd (caller proved ≥1 trip): the final walk's BODY-END state —
1209
+ // tighter for defined-every-iteration names (an inlined preamble's
1210
+ // `inl_i = 0` keeps [0,0] where the join would null it), and sound
1211
+ // because the walk ran from the verified invariant, so its end state
1212
+ // covers every real last-iteration state. Zero-trip loops must NOT use
1213
+ // it: their real exit is the ENTRY state, which body-end doesn't cover.
1214
+ if (!exitBodyEnd) restore(joined)
1215
+ // ∪ break-edge states (a break bypasses the loop condition and reaches the
1216
+ // exit mid-body; the caller's tighter iv/wrap exit forms stay sound — each
1217
+ // is an every-point invariant that covers break states)
1218
+ for (const s of lcF.breaks) hullInto(s)
1219
+ }
1003
1220
  const visit = (n) => {
1004
1221
  if (!Array.isArray(n) || n[0] === '=>') return
1005
1222
  if (n._rangeFacts) return visitWithFacts(n)
1006
1223
  const op = n[0]
1007
1224
  if (op === '[]' && n.length === 3 && typeof n[1] === 'string') {
1008
1225
  const idxV = ev(n[2])
1226
+ if (!recording) return // exploratory fixpoint pass: env effects only
1009
1227
  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)
1228
+ if (typeof process !== 'undefined' && process.env.JZ_DBG_IP) console.error('IPW', n[1], JSON.stringify(n[2]).slice(0,50), JSON.stringify(idxV), 'len', L)
1011
1229
  if (L != null && idxV && idxV[0] >= 0 && idxV[1] < L) out.add(idxKey(n[1], n[2]))
1012
1230
  // a bounded idx against an UNKNOWN length is half a proof — export the hull
1013
1231
  // (joined over every sighting of this key) for the versioning guard to close
@@ -1043,7 +1261,22 @@ function scanIntervalIdx(body, out, lens, ranges) {
1043
1261
  if (ASSIGN_OPS.has(op) || op === '++' || op === '--') {
1044
1262
  if (typeof n[1] === 'string' && symEnv.get(n[1])?.incNode === n) symEnv.delete(n[1])
1045
1263
  for (let k = 2; k < n.length; k++) visit(n[k])
1046
- if (typeof n[1] === 'string') env.set(n[1], null)
1264
+ if (typeof n[1] === 'string') {
1265
+ // `x += K` / `x -= K` / `x++` / `x--` transfer exactly — a strided
1266
+ // accumulator keeps a computable back-edge for the loop fixpoint
1267
+ // (cond-clamped by the widening join); anything else is unknown
1268
+ const cur = env.get(n[1])
1269
+ let nv = null
1270
+ if (cur) {
1271
+ if (op === '++') nv = [cur[0] + 1, cur[1] + 1]
1272
+ else if (op === '--') nv = [cur[0] - 1, cur[1] - 1]
1273
+ else if (op === '+=' || op === '-=') {
1274
+ const d = ev(n[2])
1275
+ if (d) nv = op === '+=' ? [cur[0] + d[0], cur[1] + d[1]] : [cur[0] - d[1], cur[1] - d[0]]
1276
+ }
1277
+ }
1278
+ setEnv(n[1], nv)
1279
+ }
1047
1280
  else {
1048
1281
  visit(n[1]) // records the member-write access proof (`out[idx] = …`)
1049
1282
  if (Array.isArray(n[1]) && n[1][0] !== '[]' && n[1][0] !== '.' && n[1][0] !== '?.') {
@@ -1052,12 +1285,29 @@ function scanIntervalIdx(body, out, lens, ranges) {
1052
1285
  }
1053
1286
  return
1054
1287
  }
1288
+ if (op === 'break' || op === 'continue') {
1289
+ if (typeof n[1] === 'string') { // labeled: may cross frames — feed every open one
1290
+ for (const fr of loopStack) if (fr.kind === 'loop') { fr.breaks.push(new Map(env)); fr.continues.push(new Map(env)) }
1291
+ }
1292
+ else if (op === 'break') {
1293
+ const fr = loopStack[loopStack.length - 1]
1294
+ if (fr && fr.kind === 'loop') fr.breaks.push(new Map(env))
1295
+ }
1296
+ else {
1297
+ const fr = loopStack.findLast(f => f.kind === 'loop')
1298
+ if (fr) fr.continues.push(new Map(env))
1299
+ }
1300
+ return
1301
+ }
1055
1302
  if (op === 'for' && n.length === 5) {
1056
1303
  const [, init, cond, step, lbody] = n
1057
1304
  visit(init)
1058
- // canonical literal-interval iv: `for (iv = A; iv </<= B; iv++)`, A/B const
1305
+ // canonical literal-interval iv: `for (iv = A; iv </<= B; iv++)` or the
1306
+ // DOWNWARD twin `for (iv = A; iv >/>= B; iv--)` (heapify roots, reverse
1307
+ // scans) — A/B singleton through the full evaluator
1059
1308
  let iv = null, range = null
1060
- if (Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=') && typeof cond[1] === 'string') {
1309
+ const down = Array.isArray(cond) && (cond[0] === '>' || cond[0] === '>=')
1310
+ if (Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || down) && typeof cond[1] === 'string') {
1061
1311
  const decls = new Map(); collectDecls(init, decls)
1062
1312
  // start/bound through the full evaluator: function-local consts (`x < ww`)
1063
1313
  // and computed starts (`k = -rr`) resolve as singleton intervals
@@ -1066,15 +1316,29 @@ function scanIntervalIdx(body, out, lens, ranges) {
1066
1316
  const Bs = cond[2] != null ? ev(cond[2]) : null
1067
1317
  const A = As && As[0] === As[1] ? As[0] : null
1068
1318
  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]
1319
+ if (A != null && B != null && !isReassigned(lbody, cond[1]) && !redeclaresName(lbody, cond[1])
1320
+ && (cond[2] == null || boundInvariant(cond[2], lbody))
1321
+ && (down ? isUnitDecrement(step, cond[1]) : isUnitIncrement(step, cond[1]))) {
1322
+ iv = cond[1]
1323
+ range = down ? [cond[0] === '>' ? B + 1 : B, A] : [A, cond[0] === '<' ? B - 1 : B]
1072
1324
  }
1073
1325
  }
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)
1326
+ // Body fixpoint (same engine as `while` below): the canonical-iv range is
1327
+ // a body-independent theorem re-seeded each pass; everything else
1328
+ // discovers its invariant. This is what proves heapsort's `child` chains
1329
+ // (`child = 2*i+1; while-ish descend`) and medianUs's `samples[mid]`.
1330
+ // Exit state: a canonical iv with a non-empty LITERAL range proves ≥1
1331
+ // trip, so the tighter body-end exit is sound (post-loop peel tails read
1332
+ // `src[inl_i+…]` off exactly that state); anything else takes the joined
1333
+ // invariant (zero-trip exit = entry state ⊆ joined).
1334
+ const seeded = iv && range[0] <= range[1] && !closureWrites.has(iv)
1335
+ const seeds = () => {
1336
+ if (seeded) env.set(iv, range)
1337
+ else if (iv) env.set(iv, null)
1338
+ }
1339
+ loopFixpoint(seeds,
1340
+ () => { if (cond != null) visit(cond); visit(lbody); if (step != null) visit(step) },
1341
+ cond, seeded)
1078
1342
  if (iv) env.set(iv, null) // iv holds the exit value after the loop
1079
1343
  return
1080
1344
  }
@@ -1087,7 +1351,8 @@ function scanIntervalIdx(body, out, lens, ranges) {
1087
1351
  let iv = null, entry = null, brange = null
1088
1352
  if (Array.isArray(c) && c[0] === '<' && typeof c[1] === 'string' && wbody != null) {
1089
1353
  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]
1354
+ if (entry && brange && ivMonotoneInc(wbody, c[1]) && !redeclaresName(wbody, c[1])
1355
+ && boundInvariant(c[2], wbody)) iv = c[1]
1091
1356
  }
1092
1357
  // WRAPPING-CURSOR invariant (`si = si + K; if (si >= C) si = 0` — the ring
1093
1358
  // index of table-driven maps): the pair is self-closing on [0, C-1], so an
@@ -1148,29 +1413,53 @@ function scanIntervalIdx(body, out, lens, ranges) {
1148
1413
  else symWraps.push([nm, { lo: 0, hiName: Cname, hiBias: -1, entryHi: e0[1] }, a2])
1149
1414
  }
1150
1415
  }
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])
1416
+ // Body fixpoint (loopFixpoint below): the monotone-iv/wrap/symWrap seeds
1417
+ // are theorems independent of the body, re-applied each pass; everything
1418
+ // else discovers its invariant. Bounds heapsort's `while (child < n)`
1419
+ // chains, medianUs's downward insertion scan, and interpreter
1420
+ // `while (pc < N)` dispatch — shapes the single-kill walk lost entirely.
1421
+ const seeds = () => {
1422
+ if (iv) env.set(iv, [entry[0], brange[1] - 1])
1423
+ for (const [nm, r] of wraps) if (!closureWrites.has(nm)) env.set(nm, r)
1424
+ for (const [nm, h, incNode] of symWraps) if (!closureWrites.has(nm)) symEnv.set(nm, { h, incNode })
1425
+ }
1426
+ loopFixpoint(seeds, () => { visit(c); for (let k = 2; k < n.length; k++) visit(n[k]) }, c)
1427
+ // exit state: the invariant (already in env) hulls entry ∪ back-edges;
1428
+ // iv/wraps publish their tighter exit forms
1157
1429
  if (iv) env.set(iv, [Math.min(entry[0], brange[0]), Math.max(entry[1], brange[1])])
1158
1430
  for (const [nm, r] of wraps) if (!closureWrites.has(nm)) env.set(nm, r) // holds at exit too
1159
1431
  for (const [nm] of symWraps) symEnv.delete(nm)
1160
1432
  return
1161
1433
  }
1162
1434
  if (op === 'do' || op === 'for-of' || op === 'for-in' || op === 'label'
1163
- || op === 'switch' || op === 'try') {
1435
+ || op === 'switch' || op === 'try' || op === 'catch' || op === 'finally') {
1436
+ // ('try' is the parser shape; prepare lowers it to 'catch'/'finally' nodes,
1437
+ // which is what this walk actually receives)
1164
1438
  killAssigned(n) // unknown trip count / branch selection: no interval survives entry
1165
- for (let k = 1; k < n.length; k++) visit(n[k])
1439
+ // Each child walks from the killed entry state and the construct EXITS at
1440
+ // it: case selection enters any child directly, an exception can leave a
1441
+ // `try` child mid-statement, a `do` body can break out — so neither a
1442
+ // sibling's nor the last child's flow state is the construct's. In-child
1443
+ // straight-line proofs (defined-before-use chains) still record.
1444
+ const killed = new Map(env)
1445
+ const fr = op === 'switch' ? { kind: 'switch', breaks: [], continues: [] }
1446
+ : op === 'do' || op === 'for-of' || op === 'for-in' ? { kind: 'loop', breaks: [], continues: [] }
1447
+ : null // label/try: transparent — abrupt edges bind to enclosing frames
1448
+ if (fr) loopStack.push(fr)
1449
+ for (let k = 1; k < n.length; k++) {
1450
+ visit(n[k])
1451
+ env.clear(); for (const [k2, v2] of killed) env.set(k2, v2)
1452
+ }
1453
+ if (fr) loopStack.pop()
1166
1454
  return
1167
1455
  }
1168
1456
  if (op === 'if') {
1169
1457
  const [, c, thenB, elseB] = n
1170
1458
  visit(c)
1171
1459
  const save = new Map(env)
1172
- const rT = refine(c, false)
1173
- if (rT && !closureWrites.has(rT[0])) env.set(rT[0], rT[1])
1460
+ // every `&&` conjunct holds on the then path (`if (child+1 < n && a[child] <
1461
+ // a[child+1]) child++` — the ++ under BOTH bounds)
1462
+ for (const rT of refineAll(c)) if (!closureWrites.has(rT[0])) env.set(rT[0], rT[1])
1174
1463
  visit(thenB)
1175
1464
  const afterThen = new Map(env)
1176
1465
  env.clear(); for (const [k2, v2] of save) env.set(k2, v2)
@@ -1187,6 +1476,25 @@ function scanIntervalIdx(body, out, lens, ranges) {
1187
1476
  }
1188
1477
  return
1189
1478
  }
1479
+ // Short-circuit operands evaluate under the left side's verdict: `&&`'s rhs
1480
+ // runs only where lhs HELD (`child + 1 < n && a[child] < a[child + 1]` — the
1481
+ // lookahead read is bounds-guarded by its sibling conjunct), `||`'s rhs only
1482
+ // where lhs FAILED. Reads inside the rhs prove under that refinement; writes
1483
+ // there ran conditionally, so the exit state joins both possibilities.
1484
+ if ((op === '&&' || op === '||') && n.length === 3) {
1485
+ visit(n[1])
1486
+ const save = new Map(env)
1487
+ if (op === '&&') { for (const r of refineAll(n[1])) if (!closureWrites.has(r[0])) env.set(r[0], r[1]) }
1488
+ else { const r = refine(n[1], true); if (r && !closureWrites.has(r[0])) env.set(r[0], r[1]) }
1489
+ visit(n[2])
1490
+ const after = new Map(env)
1491
+ env.clear(); for (const [k2, v2] of save) env.set(k2, v2)
1492
+ for (const k2 of new Set([...after.keys(), ...env.keys()])) {
1493
+ const a = after.get(k2), b = env.get(k2)
1494
+ env.set(k2, a && b ? [Math.min(a[0], b[0]), Math.max(a[1], b[1])] : null)
1495
+ }
1496
+ return
1497
+ }
1190
1498
  if (op === '()' && n.length === 2) { visit(n[1]); return } // grouping, not a call
1191
1499
  if (op === '()' || op === 'new') { // a call may reassign module globals
1192
1500
  for (let k = 1; k < n.length; k++) visit(n[k])
@@ -1472,7 +1780,8 @@ export function exprType(expr, locals) {
1472
1780
  const ctor = typedElemCtorOf(args[0], locals)
1473
1781
  if (ctor) {
1474
1782
  const aux = typedElemAux(ctor)
1475
- if (aux != null && (aux & 7) <= 5) return 'i32'
1783
+ // int family only Float16Array shares code 3 with a flag; its elements are floats
1784
+ if (aux != null && (aux & 7) <= 5 && !(aux & 32)) return 'i32'
1476
1785
  }
1477
1786
  }
1478
1787
  return 'f64'