jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/type.js ADDED
@@ -0,0 +1,647 @@
1
+ /**
2
+ * WASM local typing + typed-array metadata + integer proofs.
3
+ *
4
+ * - exprType: i32 vs f64 for locals/params
5
+ * - typedElemCtor / ternaryCtorOfRhs: detect typed-array ctor from an AST rhs
6
+ * (the pure PTR.TYPED aux codec lives in layout.js)
7
+ * - scanBoundedLoops / inBoundsCharCodeAt: charCodeAt i32 contract proof
8
+ * - loop unroll helpers: smallConstForTripCount, cloneWithSubst, …
9
+ * - intCertainMap / intExprChecker: integer-shaped binding analysis
10
+ *
11
+ * @module type
12
+ */
13
+ import { isI32, isReassigned } from './ast.js'
14
+ import { ctx } from './ctx.js'
15
+ import { VAL, lookupValType } from './reps.js'
16
+ import { valTypeOf } from './kind.js'
17
+ import { NO_VALUE, staticValue, intLiteralValue } from './static.js'
18
+ import { typedElemAux } from '../layout.js'
19
+
20
+ /** Byte-backed constructors whose `new X()` yields a PTR.TYPED / PTR.BUFFER value:
21
+ * the typed-array views + ArrayBuffer + DataView. Mirrors autoload's TYPED_CTORS
22
+ * (kept local to avoid a type↔module import cycle). Every other ctor — Map, Set,
23
+ * Date, Array, RegExp, user classes — has its own VAL kind via CALLEE_VAL and must
24
+ * NOT be mistaken for a typed-array construction (else its global misdispatches as
25
+ * a TypedArray, e.g. `map.set(k,v)` lowering to `arr.set(src,offset)`). */
26
+ const TYPED_FAMILY_CTORS = new Set([
27
+ 'Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
28
+ 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'ArrayBuffer', 'DataView',
29
+ ])
30
+
31
+ /** Extract typed-array ctor name ('new.Float32Array', 'new.Int8Array.view', etc) from RHS,
32
+ * or null if RHS isn't a typed-array/ArrayBuffer/DataView constructor. */
33
+ export function typedElemCtor(rhs) {
34
+ if (!Array.isArray(rhs) || rhs[0] !== '()' || typeof rhs[1] !== 'string' || !rhs[1].startsWith('new.')) return null
35
+ if (!TYPED_FAMILY_CTORS.has(rhs[1].slice(4))) return null
36
+ const args = rhs[2]
37
+ const isView = rhs[1].endsWith('Array') && rhs[1] !== 'new.ArrayBuffer'
38
+ && Array.isArray(args) && args[0] === ',' && args.length >= 4
39
+ return isView ? rhs[1] + '.view' : rhs[1]
40
+ }
41
+
42
+ /** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
43
+ * different typed-array ctors — caller should drop any cached entry rather
44
+ * than leave a stale ctor (which would lock the wrong store width). */
45
+ export const MIXED_CTORS = Symbol('MIXED_CTORS')
46
+
47
+
48
+ /** A `?:`/`&&`/`||` expression — value depends on a condition, so its ctor
49
+ * must be derived by walking branches (handled by `ternaryCtorOfRhs`). */
50
+ export const isCondExpr = e => Array.isArray(e) && (e[0] === '?:' || e[0] === '&&' || e[0] === '||')
51
+
52
+ /** Walk a `?:`/`&&`/`||` expression and return:
53
+ * - a single ctor string when every branch resolves to the same ctor,
54
+ * - MIXED_CTORS when branches resolve to different ctors,
55
+ * - null when no branch resolves (caller's behavior unchanged).
56
+ *
57
+ * `resolveName(name)` (optional) maps a *variable-name* branch to its known
58
+ * typed-array ctor — without it a branch like `cond ? bufA : bufB` (two typed
59
+ * bindings rather than two `new` literals) resolves to null and the binding
60
+ * falls back to the dynamic `$__typed_idx` read path. The classic ping-pong
61
+ * `let cur = flip ? a : b; cur[i]` needs this to keep fast typed loads. */
62
+ export function ternaryCtorOfRhs(rhs, resolveName) {
63
+ if (typeof rhs === 'string') return resolveName?.(rhs) ?? null
64
+ if (!Array.isArray(rhs)) return null
65
+ const op = rhs[0]
66
+ const lo = op === '?:' ? 2 : (op === '&&' || op === '||') ? 1 : 0
67
+ if (!lo) return null
68
+ const a = ternaryCtorOfRhs(rhs[lo], resolveName) ?? typedElemCtor(rhs[lo])
69
+ const b = ternaryCtorOfRhs(rhs[lo + 1], resolveName) ?? typedElemCtor(rhs[lo + 1])
70
+ return a && b ? (a === b ? a : MIXED_CTORS) : (a || b || null)
71
+ }
72
+
73
+ // =============================================================================
74
+ // charCodeAt in-bounds proof
75
+ // =============================================================================
76
+ // `String.prototype.charCodeAt` returns NaN for an out-of-range index, so the
77
+ // generic codegen contract is an f64 result (see module/string.js). When the
78
+ // index is the induction variable of a `for (let i = C; i < recv.length; i++)`
79
+ // loop, every `recv.charCodeAt(i)` in the loop body is statically inside
80
+ // `[0, recv.length)` — OOB is impossible — so the call may use the cheaper i32
81
+ // (raw-byte) contract instead. This is a static guarantee, not a guess.
82
+
83
+ /** Step expression of a `for` that increments `name` by exactly 1. */
84
+ export function isUnitIncrement(step, name) {
85
+ if (!Array.isArray(step)) return false
86
+ if (step[0] === '++' && step[1] === name) return true
87
+ // postfix `i++` in value position lowers to `(++i) - 1`
88
+ if (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++'
89
+ && step[1][1] === name && intLiteralValue(step[2]) === 1) return true
90
+ return false
91
+ }
92
+
93
+ /** `let`/`const` re-declaration of `name` within `node` — does not cross `=>`
94
+ * (a closure has its own scope; collection already stops at closure boundaries). */
95
+ function redeclaresName(node, name) {
96
+ if (!Array.isArray(node) || node[0] === '=>') return false
97
+ if (node[0] === 'let' || node[0] === 'const') {
98
+ for (let k = 1; k < node.length; k++) {
99
+ const d = node[k]
100
+ if (d === name) return true
101
+ if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
102
+ }
103
+ }
104
+ for (let k = 1; k < node.length; k++) if (redeclaresName(node[k], name)) return true
105
+ return false
106
+ }
107
+
108
+ /** Collect `recv.charCodeAt(idxVar)` callee nodes within `node`. Stops at `=>`:
109
+ * a closure may run after the loop, when `idxVar` has reached `recv.length`. */
110
+ function collectBoundedCC(node, recv, idxVar, set) {
111
+ if (!Array.isArray(node) || node[0] === '=>') return
112
+ if (node[0] === '()' && node.length === 3 && node[2] === idxVar
113
+ && Array.isArray(node[1]) && node[1][0] === '.'
114
+ && node[1][1] === recv && node[1][2] === 'charCodeAt')
115
+ set.add(node[1])
116
+ for (let k = 1; k < node.length; k++) collectBoundedCC(node[k], recv, idxVar, set)
117
+ }
118
+
119
+ /** Receiver of a `.length` expression, possibly wrapped in `(… | 0)` — the
120
+ * shape `prepare` produces when it hoists a for-cond bound. */
121
+ function lengthRecv(expr) {
122
+ if (Array.isArray(expr) && expr[0] === '|' && intLiteralValue(expr[2]) === 0) expr = expr[1]
123
+ if (Array.isArray(expr) && expr[0] === '.' && expr[2] === 'length'
124
+ && typeof expr[1] === 'string') return expr[1]
125
+ // `Math.min(X, recv.length)` (either arg order): min ≤ recv.length regardless
126
+ // of X, so the bound proof carries through. This is the shape
127
+ // splitCharScanLoops plants for the in-bounds main loop of a split scan.
128
+ if (Array.isArray(expr) && expr[0] === '()' && expr[1] === 'math.min') {
129
+ const argsNode = expr[2]
130
+ const args = Array.isArray(argsNode) && argsNode[0] === ',' ? argsNode.slice(1) : [argsNode]
131
+ for (const a of args) { const r = lengthRecv(a); if (r) return r }
132
+ }
133
+ return null
134
+ }
135
+
136
+ /** Flatten `let`/`const` declarations (incl. `;`-joined groups) into `out`,
137
+ * mapping each declared name to its initializer expression. */
138
+ function collectDecls(node, out) {
139
+ if (!Array.isArray(node)) return
140
+ if (node[0] === ';') { for (let k = 1; k < node.length; k++) collectDecls(node[k], out); return }
141
+ if (node[0] === 'let' || node[0] === 'const') {
142
+ for (let k = 1; k < node.length; k++) {
143
+ const d = node[k]
144
+ if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') out.set(d[1], d[2])
145
+ }
146
+ }
147
+ }
148
+
149
+ /** Walk `node`, recording in `set` the `charCodeAt` callee nodes proven in-bounds
150
+ * by an enclosing canonical induction loop `for (let i = C; i < recv.length; i++)`.
151
+ * Matches the post-`prepare` shape, where the `.length` bound is hoisted into a
152
+ * temp (`cond` becomes `i < lenTmp`, `lenTmp` declared in `init`). */
153
+ export function scanBoundedLoops(node, set) {
154
+ if (!Array.isArray(node)) return
155
+ if (node[0] === 'for' && node.length === 5) {
156
+ const [, init, cond, step, body] = node
157
+ let idx = null, recv = null, boundVar = null
158
+ if (Array.isArray(cond) && cond[0] === '<' && typeof cond[1] === 'string') {
159
+ const decls = new Map()
160
+ collectDecls(init, decls)
161
+ idx = cond[1]
162
+ // index must be declared in `init` as `let i = C`, C an integer literal ≥ 0
163
+ const start = decls.has(idx) ? intLiteralValue(decls.get(idx)) : null
164
+ if (start == null || start < 0) idx = null
165
+ // bound is `recv.length`, directly or via a hoisted temp declared in `init`
166
+ let bound = cond[2]
167
+ if (typeof bound === 'string') { boundVar = bound; bound = decls.get(bound) }
168
+ recv = lengthRecv(bound)
169
+ }
170
+ // step `i++`; body never writes `i`/`recv`/the bound temp (incl. via
171
+ // closures) and never re-declares `i`. Then every bare `i` in the body
172
+ // satisfies `0 ≤ C ≤ i < recv.length`.
173
+ if (idx && recv && idx !== recv && isUnitIncrement(step, idx)
174
+ && !isReassigned(body, idx) && !isReassigned(body, recv)
175
+ && (boundVar == null || !isReassigned(body, boundVar))
176
+ && !redeclaresName(body, idx))
177
+ collectBoundedCC(body, recv, idx, set)
178
+ }
179
+ for (let k = 1; k < node.length; k++) scanBoundedLoops(node[k], set)
180
+ }
181
+
182
+ const NO_BOUNDED_CC = new Set() // shared immutable empty result
183
+
184
+ /** Set of `['.', recv, 'charCodeAt']` callee nodes in the current function whose
185
+ * index argument is provably within `[0, recv.length)`. Memoised per body. */
186
+ export function inBoundsCharCodeAt(ctx) {
187
+ const body = ctx.func?.body
188
+ if (!Array.isArray(body)) return NO_BOUNDED_CC
189
+ if (ctx.func._ccBody === body) return ctx.func.ccInBounds
190
+ const set = new Set()
191
+ scanBoundedLoops(body, set)
192
+ ctx.func.ccInBounds = set
193
+ ctx.func._ccBody = body
194
+ return set
195
+ }
196
+
197
+ /** Collect proven-in-bounds `recv[idxVar]` accesses within a canonical induction
198
+ * loop. Stores `"recv\x00idxVar"` keys — `\x00` isn't a valid identifier char so
199
+ * the pair is unambiguous. Stops at `=>` (a closure may run after the loop, when
200
+ * `idxVar` has reached `recv.length`). */
201
+ function collectBoundedArrIdx(node, recv, idxVar, set) {
202
+ if (!Array.isArray(node) || node[0] === '=>') return
203
+ if (node[0] === '[]' && node.length === 3 && node[1] === recv && node[2] === idxVar)
204
+ set.add(recv + '\x00' + idxVar)
205
+ for (let k = 1; k < node.length; k++) collectBoundedArrIdx(node[k], recv, idxVar, set)
206
+ }
207
+
208
+ /** Walk `node`, recording `"recv\x00idx"` pairs for `recv[idx]` reads proven within
209
+ * `[0, recv.length)` by an enclosing canonical loop `for (let i = C; i < recv.length;
210
+ * i++)`. Same loop contract as `scanBoundedLoops` (charCodeAt) — sibling proof for
211
+ * the ARRAY indexed-read fast path in `module/array.js`. */
212
+ export function scanBoundedArrIdx(node, set) {
213
+ if (!Array.isArray(node)) return
214
+ if (node[0] === 'for' && node.length === 5) {
215
+ const [, init, cond, step, body] = node
216
+ let idx = null, recv = null, boundVar = null
217
+ if (Array.isArray(cond) && cond[0] === '<' && typeof cond[1] === 'string') {
218
+ const decls = new Map()
219
+ collectDecls(init, decls)
220
+ idx = cond[1]
221
+ const start = decls.has(idx) ? intLiteralValue(decls.get(idx)) : null
222
+ if (start == null || start < 0) idx = null
223
+ let bound = cond[2]
224
+ if (typeof bound === 'string') { boundVar = bound; bound = decls.get(bound) }
225
+ recv = lengthRecv(bound)
226
+ }
227
+ if (idx && recv && idx !== recv && isUnitIncrement(step, idx)
228
+ && !isReassigned(body, idx) && !isReassigned(body, recv)
229
+ && (boundVar == null || !isReassigned(body, boundVar))
230
+ && !redeclaresName(body, idx))
231
+ collectBoundedArrIdx(body, recv, idx, set)
232
+ }
233
+ for (let k = 1; k < node.length; k++) scanBoundedArrIdx(node[k], set)
234
+ }
235
+
236
+ /** Set of `"recv\x00idx"` keys for `recv[idx]` reads in the current function proven
237
+ * in-bounds. Memoised per body (separate slot from the charCodeAt proof). */
238
+ export function inBoundsArrIdx(ctx) {
239
+ const body = ctx.func?.body
240
+ if (!Array.isArray(body)) return NO_BOUNDED_CC
241
+ if (ctx.func._aiBody === body) return ctx.func.aiInBounds
242
+ const set = new Set()
243
+ scanBoundedArrIdx(body, set)
244
+ ctx.func.aiInBounds = set
245
+ ctx.func._aiBody = body
246
+ return set
247
+ }
248
+
249
+ // === Loop unroll / AST transforms (emit + plan) ===
250
+
251
+ export const MAX_SMALL_FOR_UNROLL = 8
252
+ export const MAX_NESTED_FOR_UNROLL = 64
253
+
254
+ export function containsNestedClosure(body) {
255
+ if (!Array.isArray(body)) return false
256
+ if (body[0] === '=>') return true
257
+ for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
258
+ return false
259
+ }
260
+
261
+ export function containsNestedLoop(body) {
262
+ if (!Array.isArray(body)) return false
263
+ const op = body[0]
264
+ if (op === 'for' || op === 'while' || op === 'do') return true
265
+ if (op === '=>') return false
266
+ for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
267
+ return false
268
+ }
269
+
270
+ export function nestedSmallLoopBudget(body) {
271
+ if (!Array.isArray(body)) return 1
272
+ if (body[0] === '=>') return 1
273
+ if (body[0] === 'for') {
274
+ const [, init, cond, step, loopBody] = body
275
+ const n = smallConstForTripCount(init, cond, step)
276
+ return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
277
+ }
278
+ let max = 1
279
+ for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
280
+ return max
281
+ }
282
+
283
+ export function containsDeclOf(body, name) {
284
+ if (!Array.isArray(body)) return false
285
+ const op = body[0]
286
+ if (op === '=>') return false
287
+ if (op === 'let' || op === 'const') {
288
+ for (let i = 1; i < body.length; i++) {
289
+ const d = body[i]
290
+ if (d === name) return true
291
+ if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
292
+ }
293
+ }
294
+ for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
295
+ return false
296
+ }
297
+
298
+ /** Clone AST with substitutions/renames. Skips into `=>` bodies. */
299
+ export function cloneWithSubst(node, subst, rename = null) {
300
+ if (!(subst instanceof Map)) {
301
+ const name = subst, value = rename
302
+ if (node === name) return [null, value]
303
+ if (!Array.isArray(node)) return node
304
+ if (node[0] === '=>') return node
305
+ return node.map(x => cloneWithSubst(x, name, value))
306
+ }
307
+ const ren = rename instanceof Map ? rename : new Map()
308
+ if (typeof node === 'string') {
309
+ if (subst.has(node)) return clonePlain(subst.get(node))
310
+ return ren.get(node) || node
311
+ }
312
+ if (!Array.isArray(node)) return node
313
+ const op = node[0]
314
+ if (op === 'str') return node.slice()
315
+ if (op === '=>') return node
316
+ if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, ren), node[2]]
317
+ if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, ren)]
318
+ return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, ren))
319
+ }
320
+
321
+ const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
322
+
323
+ export function containsKnownTypedArrayIndex(body) {
324
+ if (!Array.isArray(body)) return false
325
+ if (body[0] === '=>') return false
326
+ if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
327
+ for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
328
+ return false
329
+ }
330
+
331
+ /** Trip count for `for (let i=0; i<N; i++)` when structurally obvious, else null. */
332
+ export function smallConstForTripCount(init, cond, step, maxEnd = MAX_SMALL_FOR_UNROLL) {
333
+ if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
334
+ const decl = init[1]
335
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
336
+ const name = decl[1]
337
+ const start = intLiteralValue(decl[2])
338
+ if (start !== 0) return null
339
+ if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
340
+ const end = intLiteralValue(cond[2])
341
+ if (end == null || end < 0 || end > maxEnd) return null
342
+ const stepOk = Array.isArray(step) && (
343
+ (step[0] === '++' && step[1] === name) ||
344
+ (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
345
+ )
346
+ return stepOk ? end : null
347
+ }
348
+
349
+ /** Does `body` always exit via return/throw/break/continue? */
350
+ export function isTerminator(body) {
351
+ if (!Array.isArray(body)) return false
352
+ const op = body[0]
353
+ if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
354
+ if (op === '{}' || op === ';') {
355
+ for (let i = body.length - 1; i >= 1; i--) {
356
+ const s = body[i]
357
+ if (s == null) continue
358
+ return isTerminator(s)
359
+ }
360
+ return false
361
+ }
362
+ return false
363
+ }
364
+
365
+ const isUnsignedI32Expr = (e) => Array.isArray(e) && (
366
+ e[0] === '>>>' ||
367
+ (e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
368
+ )
369
+
370
+ /**
371
+ * Infer expression result type from AST (without emitting).
372
+ * Used to determine local variable types before compilation.
373
+ * Looks up `locals` first, then current-function params (for i32-specialized params).
374
+ */
375
+ export function exprType(expr, locals) {
376
+ if (expr == null) return 'f64'
377
+ if (typeof expr === 'number')
378
+ return isI32(expr) ? 'i32' : 'f64'
379
+ if (typeof expr === 'string') {
380
+ if (locals?.has?.(expr)) return locals.get(expr)
381
+ const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
382
+ if (paramType) return paramType
383
+ // Module-level numeric consts (top-level `const N = 128`) are emitted as
384
+ // wasm globals with a known wasm type. Without this lookup, references to
385
+ // them inside functions fall back to f64, widening counters bounded by the
386
+ // const (`for (let r = 0; r < N_ROUNDS; r++)`) to f64 via the comparison
387
+ // pass. Only propagate primitive numeric kinds — i64 globals are reserved
388
+ // for the NaN-box carrier ABI and shouldn't influence local typing.
389
+ const gt = ctx.scope?.globalTypes?.get?.(expr)
390
+ if (gt === 'i32' || gt === 'f64') return gt
391
+ return 'f64'
392
+ }
393
+ if (!Array.isArray(expr)) return 'f64'
394
+
395
+ const [op, ...args] = expr
396
+ if (op == null) return exprType(args[0], locals) // literal [, value]
397
+
398
+ // Statically evaluable to -0 (e.g. -1 * 0) — i32 would lose the sign.
399
+ const sv = staticValue(expr)
400
+ if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return 'f64'
401
+
402
+ // Always f64
403
+ if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
404
+ // arr[i] — typed integer arrays return i32. Only Int8/Uint8/Int16/Uint16/Int32
405
+ // (every value fits in signed i32). Skip Uint32: 0..2^32-1 overflows signed.
406
+ // During analyzeBody the in-progress typedElems is in localTypedElemsOverlay;
407
+ // post-analyze passes read from ctx.types.typedElem.
408
+ if (op === '[]') {
409
+ if (typeof args[0] === 'string') {
410
+ const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
411
+ if (ctor) {
412
+ const aux = typedElemAux(ctor)
413
+ if (aux != null && (aux & 7) <= 4) return 'i32'
414
+ }
415
+ }
416
+ return 'f64'
417
+ }
418
+ // `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
419
+ // both return i32). Letting it stay i32 lets analyzeBody keep the counter
420
+ // local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
421
+ // the matching `i32.trunc_sat_f64_s` truncs at every `arr[i]` / `i*k` site.
422
+ // Only safe when receiver type is statically known to expose an integer length.
423
+ if (op === '.') {
424
+ if (args[1] === 'length' && typeof args[0] === 'string') {
425
+ const vt = lookupValType(args[0])
426
+ if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
427
+ }
428
+ if (args[1] === 'size' && typeof args[0] === 'string') {
429
+ const vt = lookupValType(args[0])
430
+ if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
431
+ }
432
+ if (args[1] === 'byteLength' && typeof args[0] === 'string') {
433
+ const vt = lookupValType(args[0])
434
+ if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
435
+ }
436
+ return 'f64'
437
+ }
438
+ // Comparisons, logical-not, and unsigned shift always yield an i32 — a boolean,
439
+ // or a ToUint32 result. True even on BigInt operands (`>>>` throws on bigint, so
440
+ // it never reaches here with one).
441
+ if (['>', '<', '>=', '<=', '==', '!=', '!', '>>>'].includes(op)) return 'i32'
442
+ // Bitwise & signed-shift: i32 on numbers, but f64 when operands are BigInt — the
443
+ // result is a bigint carried in the i64-bits-as-f64 ABI, not a 32-bit int.
444
+ if (['&', '|', '^', '~', '<<', '>>'].includes(op))
445
+ return valTypeOf(expr) === VAL.BIGINT ? 'f64' : 'i32'
446
+ // Preserve i32 if both operands i32
447
+ if (op === '+' || op === '-') {
448
+ const ta = exprType(args[0], locals)
449
+ const tb = args[1] != null ? exprType(args[1], locals) : ta // unary: inherit
450
+ if (ta !== 'i32' || tb !== 'i32') return 'f64'
451
+ // A uint32 operand ([0, 2^32)) makes the result exceed signed i32 range, so
452
+ // emit widens to f64 (see emit.js `+`/`-`). exprType must agree — else
453
+ // narrowing the result back to i32 would trunc_sat-saturate the f64 to INT32_MAX.
454
+ if (isUnsignedI32Expr(args[0]) || (args[1] != null && isUnsignedI32Expr(args[1]))) return 'f64'
455
+ return 'i32'
456
+ }
457
+ // `%` is i32 only when emit takes the i32.rem_s path: both operands i32, neither
458
+ // unsigned, AND the divisor is a nonzero integer constant. A 0 or runtime divisor
459
+ // yields NaN via f64rem (f64), so result-narrowing must NOT see i32 here — else a
460
+ // NaN remainder gets i32.trunc_sat'd to 0. Mirrors the emit.js `%` guard exactly.
461
+ if (op === '%') {
462
+ const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
463
+ if (ta !== 'i32' || tb !== 'i32') return 'f64'
464
+ if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
465
+ const dv = staticValue(args[1])
466
+ return (dv !== NO_VALUE && typeof dv === 'number' && dv !== 0 && Number.isInteger(dv)) ? 'i32' : 'f64'
467
+ }
468
+ // `*` — a JS multiply is an f64 operation; `i32.mul` reproduces it faithfully
469
+ // only while the exact product is f64-exact. Stay i32 when both operands are
470
+ // i32 *and* the product provably fits: a fully-static product checked
471
+ // directly, otherwise a literal operand small enough that |literal|·2^31 ≤
472
+ // 2^53 (mirrors emit.js `mulFitsI32` — keeps `i*4` i32, widens `h*16777619`).
473
+ if (op === '*') {
474
+ const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
475
+ if (ta !== 'i32' || tb !== 'i32') return 'f64'
476
+ // uint32 operand: product can exceed i32; emit widens to f64 (see emit.js `*`).
477
+ if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
478
+ if (sv !== NO_VALUE && typeof sv === 'number') return isI32(sv) ? 'i32' : 'f64'
479
+ const small = e => {
480
+ const v = staticValue(e)
481
+ return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <= 0x400000
482
+ }
483
+ return small(args[0]) || small(args[1]) ? 'i32' : 'f64'
484
+ }
485
+ // Unary preserves type
486
+ if (op === 'u-' || op === 'u+') return exprType(args[0], locals)
487
+ // Ternary / logical: conciliate
488
+ if (op === '?:' || op === '&&' || op === '||') {
489
+ const branches = op === '?:' ? [args[1], args[2]] : [args[0], args[1]]
490
+ const ta = exprType(branches[0], locals), tb = exprType(branches[1], locals)
491
+ return ta === 'i32' && tb === 'i32' ? 'i32' : 'f64'
492
+ }
493
+ if (op === '[') return 'f64'
494
+ // Builtin calls with known i32 result. Math.imul / Math.clz32 always produce
495
+ // a 32-bit integer; recognising this here keeps `let x = Math.imul(...)` (and
496
+ // chains like `x = Math.imul(x, k) + 12345`) on the i32 ABI all the way
497
+ // through, instead of widening the local to f64 because exprType defaulted.
498
+ if (op === '()') {
499
+ if (args[0] === 'math.imul' || args[0] === 'math.clz32') return 'i32'
500
+ // SIMD intrinsics → v128 lane vector, except lane-extract / reductions which
501
+ // hand a scalar back (i32x4.lane / v128.anyTrue / v128.allTrue → i32;
502
+ // f32x4.lane → f64). See module/simd.js.
503
+ if (typeof args[0] === 'string' && (args[0].startsWith('f32x4.') || args[0].startsWith('i32x4.') || args[0].startsWith('f64x2.') || args[0].startsWith('v128.'))) {
504
+ if (args[0] === 'f32x4.lane') return 'f64'
505
+ if (args[0] === 'i32x4.lane' || args[0] === 'v128.anyTrue' || args[0] === 'v128.allTrue') return 'i32'
506
+ return 'v128'
507
+ }
508
+ // charCodeAt: i32 when the index is provably in `[0, recv.length)` (an
509
+ // induction variable bounded by `recv.length` — OOB impossible). Otherwise
510
+ // f64: the JS-spec OOB result is NaN, which is not representable as i32.
511
+ if (Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'charCodeAt'
512
+ && inBoundsCharCodeAt(ctx).has(args[0])) return 'i32'
513
+ // User-function call: consult the callee's narrowed result type. By the time
514
+ // analyzeBody runs in emitFunc, narrowSignatures has set sig.results[0]='i32'
515
+ // on every body-i32-only func. Propagating this lets `let h = userFn(...)`
516
+ // (mix in callback bench: i32-FNV) keep h as an i32 local instead of widening
517
+ // to f64 and round-tripping i32↔f64 every iteration.
518
+ if (typeof args[0] === 'string') {
519
+ const f = ctx.func.map?.get(args[0])
520
+ if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'i32' && f.sig.ptrKind == null) return 'i32'
521
+ if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'v128') return 'v128' // SIMD helper
522
+ }
523
+ }
524
+ return 'f64'
525
+ }
526
+
527
+ // === Integer-certainty fixpoint (shared by analyzeIntCertain + program-facts) ===
528
+
529
+ const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
530
+ const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
531
+ const INT_CLOSED_OPS = new Set(['+', '-', '*']) // `%` handled separately — int only for nonzero divisor
532
+ const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
533
+
534
+ function collectIntDefs(body) {
535
+ const defs = new Map()
536
+ const pushDef = (name, rhs) => {
537
+ let list = defs.get(name)
538
+ if (!list) { list = []; defs.set(name, list) }
539
+ list.push(rhs)
540
+ }
541
+ const collect = (node) => {
542
+ if (!Array.isArray(node)) return
543
+ const [op, ...args] = node
544
+ if (op === '=>') return
545
+ if (op === 'let' || op === 'const') {
546
+ for (const a of args)
547
+ if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
548
+ } else if (op === '=' && typeof args[0] === 'string') {
549
+ pushDef(args[0], args[1])
550
+ } else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
551
+ !INT_CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
552
+ pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
553
+ } else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
554
+ pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
555
+ }
556
+ for (const a of args) collect(a)
557
+ }
558
+ collect(body)
559
+ return defs
560
+ }
561
+
562
+ function makeIsIntExpr(intCertain) {
563
+ return function isIntExpr(expr) {
564
+ if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
565
+ if (typeof expr === 'boolean') return true
566
+ if (typeof expr === 'string') return intCertain.get(expr) === true
567
+ if (!Array.isArray(expr)) return false
568
+ const sv = staticValue(expr)
569
+ if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
570
+ const [op, ...args] = expr
571
+ if (op == null) {
572
+ const v = args[0]
573
+ if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
574
+ if (typeof v === 'boolean') return true
575
+ return false
576
+ }
577
+ if (INT_BIT_OPS.has(op) || INT_CMP_OPS.has(op)) return true
578
+ if (op === '.') {
579
+ if ((args[1] === 'length' || args[1] === 'byteLength') && typeof args[0] === 'string') {
580
+ const vt = lookupValType(args[0])
581
+ return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
582
+ }
583
+ if (args[1] === 'size' && typeof args[0] === 'string') {
584
+ const vt = lookupValType(args[0])
585
+ return vt === VAL.SET || vt === VAL.MAP
586
+ }
587
+ return false
588
+ }
589
+ if (INT_CLOSED_OPS.has(op)) {
590
+ const a = isIntExpr(args[0])
591
+ const b = args[1] != null ? isIntExpr(args[1]) : a
592
+ return a && b
593
+ }
594
+ // `a % b` is integer-valued only when b is a provably-nonzero integer
595
+ // constant — `a % 0` is NaN, which is not an integer. A runtime or zero
596
+ // divisor leaves the expression non-int (f64), so result-narrowing won't
597
+ // truncate a NaN remainder to 0 and floor-elision won't drop a NaN.
598
+ if (op === '%') {
599
+ const bv = staticValue(args[1])
600
+ return bv !== NO_VALUE && typeof bv === 'number' && bv !== 0 && Number.isInteger(bv) && isIntExpr(args[0])
601
+ }
602
+ if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
603
+ if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
604
+ if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
605
+ if (op === '()') {
606
+ const c = args[0]
607
+ if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
608
+ if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
609
+ }
610
+ return false
611
+ }
612
+ }
613
+
614
+ /** Monotone fixpoint over binding defs in `body`. Map name → intCertain. */
615
+ export function intCertainMap(body) {
616
+ const defs = collectIntDefs(body)
617
+ if (defs.size === 0) return new Map()
618
+ const intCertain = new Map()
619
+ for (const name of defs.keys()) intCertain.set(name, true)
620
+ // A parameter has no def in `body` — its entry value is whatever the caller
621
+ // passed. For an f64 param (JS-number ABI) that is an arbitrary real, so a
622
+ // reassigned f64 param is NOT integer-certain: a self/int reassignment
623
+ // (`p = p`, `p = p + 1`) would otherwise vacuously satisfy the optimistic
624
+ // fixpoint, since `isIntExpr(p)` reads p's own provisional `true`. Seed f64
625
+ // params false so the unknown entry value grounds the lattice; i32-narrowed
626
+ // params (integer ABI) stay certain. Seeding false is always conservative —
627
+ // at worst it re-applies a floor/round that was a runtime no-op — so a
628
+ // mismatched ctx.func.current (whole-program intExprChecker callers) can only
629
+ // forgo an optimization, never miscompile.
630
+ for (const p of ctx.func.current?.params || [])
631
+ if (p.type !== 'i32' && intCertain.has(p.name)) intCertain.set(p.name, false)
632
+ const isIntExpr = makeIsIntExpr(intCertain)
633
+ let changed = true
634
+ while (changed) {
635
+ changed = false
636
+ for (const [name, rhsList] of defs) {
637
+ if (!intCertain.get(name)) continue
638
+ if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
639
+ }
640
+ }
641
+ return intCertain
642
+ }
643
+
644
+ /** Returns `expr => boolean` — integer-shaped expressions in `body`. */
645
+ export function intExprChecker(body) {
646
+ return makeIsIntExpr(intCertainMap(body))
647
+ }