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.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === '=>')
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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 =
|
|
603
|
-
const b = args[1] != null ?
|
|
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) &&
|
|
1691
|
+
return bv !== NO_VALUE && typeof bv === 'number' && bv !== 0 && Number.isInteger(bv) && levelOf(args[0]) ? 1 : 0
|
|
613
1692
|
}
|
|
614
|
-
if (op === 'u-'
|
|
615
|
-
if (op === '
|
|
616
|
-
if (op === '
|
|
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
|
-
|
|
620
|
-
|
|
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
|
|
1703
|
+
return 0
|
|
623
1704
|
}
|
|
624
1705
|
}
|
|
625
1706
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
|
631
|
-
for (const name of defs.keys())
|
|
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 `
|
|
637
|
-
// params
|
|
638
|
-
// params (integer ABI) stay
|
|
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' &&
|
|
644
|
-
const
|
|
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
|
-
|
|
650
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
}
|