jz 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/type.js ADDED
@@ -0,0 +1,651 @@
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] — integer typed arrays (Int8/Uint8/Int16/Uint16/Int32/Uint32, aux 0..5) read as i32:
405
+ // the element IS a 32-bit machine integer, so a binding used in integer/bitwise ops stays i32
406
+ // instead of round-tripping i32.load → f64 → trunc back (the deopt that made packed-pixel fade
407
+ // loops like lorenz slow). Uint32 reads carry the full 0..2^32-1 range as the i32 bit-pattern;
408
+ // ToInt32-coercing uses (& | ^ << >> >>>, i32.store) are bit-exact, and value uses that need the
409
+ // unsigned magnitude (compare, f64 convert) go through the elem-aux's unsigned path. Floats
410
+ // (Float32/Float64, aux 6/7) genuinely yield f64. typedElems: in-progress reads come from
411
+ // localTypedElemsOverlay during analyzeBody; post-analyze passes read ctx.types.typedElem.
412
+ if (op === '[]') {
413
+ if (typeof args[0] === 'string') {
414
+ const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
415
+ if (ctor) {
416
+ const aux = typedElemAux(ctor)
417
+ if (aux != null && (aux & 7) <= 5) return 'i32'
418
+ }
419
+ }
420
+ return 'f64'
421
+ }
422
+ // `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
423
+ // both return i32). Letting it stay i32 lets analyzeBody keep the counter
424
+ // local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
425
+ // the matching `i32.trunc_sat_f64_s` truncs at every `arr[i]` / `i*k` site.
426
+ // Only safe when receiver type is statically known to expose an integer length.
427
+ if (op === '.') {
428
+ if (args[1] === 'length' && typeof args[0] === 'string') {
429
+ const vt = lookupValType(args[0])
430
+ if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
431
+ }
432
+ if (args[1] === 'size' && typeof args[0] === 'string') {
433
+ const vt = lookupValType(args[0])
434
+ if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
435
+ }
436
+ if (args[1] === 'byteLength' && typeof args[0] === 'string') {
437
+ const vt = lookupValType(args[0])
438
+ if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
439
+ }
440
+ return 'f64'
441
+ }
442
+ // Comparisons, logical-not, and unsigned shift always yield an i32 — a boolean,
443
+ // or a ToUint32 result. True even on BigInt operands (`>>>` throws on bigint, so
444
+ // it never reaches here with one).
445
+ if (['>', '<', '>=', '<=', '==', '!=', '!', '>>>'].includes(op)) return 'i32'
446
+ // Bitwise & signed-shift: i32 on numbers, but f64 when operands are BigInt — the
447
+ // result is a bigint carried in the i64-bits-as-f64 ABI, not a 32-bit int.
448
+ if (['&', '|', '^', '~', '<<', '>>'].includes(op))
449
+ return valTypeOf(expr) === VAL.BIGINT ? 'f64' : 'i32'
450
+ // Preserve i32 if both operands i32
451
+ if (op === '+' || op === '-') {
452
+ const ta = exprType(args[0], locals)
453
+ const tb = args[1] != null ? exprType(args[1], locals) : ta // unary: inherit
454
+ if (ta !== 'i32' || tb !== 'i32') return 'f64'
455
+ // A uint32 operand ([0, 2^32)) makes the result exceed signed i32 range, so
456
+ // emit widens to f64 (see emit.js `+`/`-`). exprType must agree — else
457
+ // narrowing the result back to i32 would trunc_sat-saturate the f64 to INT32_MAX.
458
+ if (isUnsignedI32Expr(args[0]) || (args[1] != null && isUnsignedI32Expr(args[1]))) return 'f64'
459
+ return 'i32'
460
+ }
461
+ // `%` is i32 only when emit takes the i32.rem_s path: both operands i32, neither
462
+ // unsigned, AND the divisor is a nonzero integer constant. A 0 or runtime divisor
463
+ // yields NaN via f64rem (f64), so result-narrowing must NOT see i32 here — else a
464
+ // NaN remainder gets i32.trunc_sat'd to 0. Mirrors the emit.js `%` guard exactly.
465
+ if (op === '%') {
466
+ const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
467
+ if (ta !== 'i32' || tb !== 'i32') return 'f64'
468
+ if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
469
+ const dv = staticValue(args[1])
470
+ return (dv !== NO_VALUE && typeof dv === 'number' && dv !== 0 && Number.isInteger(dv)) ? 'i32' : 'f64'
471
+ }
472
+ // `*` — a JS multiply is an f64 operation; `i32.mul` reproduces it faithfully
473
+ // only while the exact product is f64-exact. Stay i32 when both operands are
474
+ // i32 *and* the product provably fits: a fully-static product checked
475
+ // directly, otherwise a literal operand small enough that |literal|·2^31 ≤
476
+ // 2^53 (mirrors emit.js `mulFitsI32` — keeps `i*4` i32, widens `h*16777619`).
477
+ if (op === '*') {
478
+ const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
479
+ if (ta !== 'i32' || tb !== 'i32') return 'f64'
480
+ // uint32 operand: product can exceed i32; emit widens to f64 (see emit.js `*`).
481
+ if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
482
+ if (sv !== NO_VALUE && typeof sv === 'number') return isI32(sv) ? 'i32' : 'f64'
483
+ const small = e => {
484
+ const v = staticValue(e)
485
+ return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <= 0x400000
486
+ }
487
+ return small(args[0]) || small(args[1]) ? 'i32' : 'f64'
488
+ }
489
+ // Unary preserves type
490
+ if (op === 'u-' || op === 'u+') return exprType(args[0], locals)
491
+ // Ternary / logical: conciliate
492
+ if (op === '?:' || op === '&&' || op === '||') {
493
+ const branches = op === '?:' ? [args[1], args[2]] : [args[0], args[1]]
494
+ const ta = exprType(branches[0], locals), tb = exprType(branches[1], locals)
495
+ return ta === 'i32' && tb === 'i32' ? 'i32' : 'f64'
496
+ }
497
+ if (op === '[') return 'f64'
498
+ // Builtin calls with known i32 result. Math.imul / Math.clz32 always produce
499
+ // a 32-bit integer; recognising this here keeps `let x = Math.imul(...)` (and
500
+ // chains like `x = Math.imul(x, k) + 12345`) on the i32 ABI all the way
501
+ // through, instead of widening the local to f64 because exprType defaulted.
502
+ if (op === '()') {
503
+ if (args[0] === 'math.imul' || args[0] === 'math.clz32') return 'i32'
504
+ // SIMD intrinsics → v128 lane vector, except lane-extract / reductions which
505
+ // hand a scalar back (i32x4.lane / v128.anyTrue / v128.allTrue → i32;
506
+ // f32x4.lane → f64). See module/simd.js.
507
+ if (typeof args[0] === 'string' && (args[0].startsWith('f32x4.') || args[0].startsWith('i32x4.') || args[0].startsWith('f64x2.') || args[0].startsWith('v128.'))) {
508
+ if (args[0] === 'f32x4.lane' || args[0] === 'f64x2.lane') return 'f64'
509
+ if (args[0] === 'i32x4.lane' || args[0] === 'v128.anyTrue' || args[0] === 'v128.allTrue') return 'i32'
510
+ return 'v128'
511
+ }
512
+ // charCodeAt: i32 when the index is provably in `[0, recv.length)` (an
513
+ // induction variable bounded by `recv.length` — OOB impossible). Otherwise
514
+ // f64: the JS-spec OOB result is NaN, which is not representable as i32.
515
+ if (Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'charCodeAt'
516
+ && inBoundsCharCodeAt(ctx).has(args[0])) return 'i32'
517
+ // User-function call: consult the callee's narrowed result type. By the time
518
+ // analyzeBody runs in emitFunc, narrowSignatures has set sig.results[0]='i32'
519
+ // on every body-i32-only func. Propagating this lets `let h = userFn(...)`
520
+ // (mix in callback bench: i32-FNV) keep h as an i32 local instead of widening
521
+ // to f64 and round-tripping i32↔f64 every iteration.
522
+ if (typeof args[0] === 'string') {
523
+ const f = ctx.func.map?.get(args[0])
524
+ if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'i32' && f.sig.ptrKind == null) return 'i32'
525
+ if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'v128') return 'v128' // SIMD helper
526
+ }
527
+ }
528
+ return 'f64'
529
+ }
530
+
531
+ // === Integer-certainty fixpoint (shared by analyzeIntCertain + program-facts) ===
532
+
533
+ const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
534
+ const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
535
+ const INT_CLOSED_OPS = new Set(['+', '-', '*']) // `%` handled separately — int only for nonzero divisor
536
+ const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
537
+
538
+ function collectIntDefs(body) {
539
+ const defs = new Map()
540
+ const pushDef = (name, rhs) => {
541
+ let list = defs.get(name)
542
+ if (!list) { list = []; defs.set(name, list) }
543
+ list.push(rhs)
544
+ }
545
+ const collect = (node) => {
546
+ if (!Array.isArray(node)) return
547
+ const [op, ...args] = node
548
+ if (op === '=>') return
549
+ if (op === 'let' || op === 'const') {
550
+ for (const a of args)
551
+ if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
552
+ } else if (op === '=' && typeof args[0] === 'string') {
553
+ pushDef(args[0], args[1])
554
+ } else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
555
+ !INT_CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
556
+ pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
557
+ } else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
558
+ pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
559
+ }
560
+ for (const a of args) collect(a)
561
+ }
562
+ collect(body)
563
+ return defs
564
+ }
565
+
566
+ function makeIsIntExpr(intCertain) {
567
+ return function isIntExpr(expr) {
568
+ if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
569
+ if (typeof expr === 'boolean') return true
570
+ if (typeof expr === 'string') return intCertain.get(expr) === true
571
+ if (!Array.isArray(expr)) return false
572
+ const sv = staticValue(expr)
573
+ if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
574
+ const [op, ...args] = expr
575
+ if (op == null) {
576
+ const v = args[0]
577
+ if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
578
+ if (typeof v === 'boolean') return true
579
+ return false
580
+ }
581
+ if (INT_BIT_OPS.has(op) || INT_CMP_OPS.has(op)) return true
582
+ if (op === '.') {
583
+ if ((args[1] === 'length' || args[1] === 'byteLength') && typeof args[0] === 'string') {
584
+ const vt = lookupValType(args[0])
585
+ return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
586
+ }
587
+ if (args[1] === 'size' && typeof args[0] === 'string') {
588
+ const vt = lookupValType(args[0])
589
+ return vt === VAL.SET || vt === VAL.MAP
590
+ }
591
+ return false
592
+ }
593
+ if (INT_CLOSED_OPS.has(op)) {
594
+ const a = isIntExpr(args[0])
595
+ const b = args[1] != null ? isIntExpr(args[1]) : a
596
+ return a && b
597
+ }
598
+ // `a % b` is integer-valued only when b is a provably-nonzero integer
599
+ // constant — `a % 0` is NaN, which is not an integer. A runtime or zero
600
+ // divisor leaves the expression non-int (f64), so result-narrowing won't
601
+ // truncate a NaN remainder to 0 and floor-elision won't drop a NaN.
602
+ if (op === '%') {
603
+ const bv = staticValue(args[1])
604
+ return bv !== NO_VALUE && typeof bv === 'number' && bv !== 0 && Number.isInteger(bv) && isIntExpr(args[0])
605
+ }
606
+ if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
607
+ if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
608
+ if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
609
+ if (op === '()') {
610
+ const c = args[0]
611
+ if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
612
+ if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
613
+ }
614
+ return false
615
+ }
616
+ }
617
+
618
+ /** Monotone fixpoint over binding defs in `body`. Map name → intCertain. */
619
+ export function intCertainMap(body) {
620
+ const defs = collectIntDefs(body)
621
+ if (defs.size === 0) return new Map()
622
+ const intCertain = new Map()
623
+ for (const name of defs.keys()) intCertain.set(name, true)
624
+ // A parameter has no def in `body` — its entry value is whatever the caller
625
+ // passed. For an f64 param (JS-number ABI) that is an arbitrary real, so a
626
+ // reassigned f64 param is NOT integer-certain: a self/int reassignment
627
+ // (`p = p`, `p = p + 1`) would otherwise vacuously satisfy the optimistic
628
+ // fixpoint, since `isIntExpr(p)` reads p's own provisional `true`. Seed f64
629
+ // params false so the unknown entry value grounds the lattice; i32-narrowed
630
+ // params (integer ABI) stay certain. Seeding false is always conservative —
631
+ // at worst it re-applies a floor/round that was a runtime no-op — so a
632
+ // mismatched ctx.func.current (whole-program intExprChecker callers) can only
633
+ // forgo an optimization, never miscompile.
634
+ for (const p of ctx.func.current?.params || [])
635
+ if (p.type !== 'i32' && intCertain.has(p.name)) intCertain.set(p.name, false)
636
+ const isIntExpr = makeIsIntExpr(intCertain)
637
+ let changed = true
638
+ while (changed) {
639
+ changed = false
640
+ for (const [name, rhsList] of defs) {
641
+ if (!intCertain.get(name)) continue
642
+ if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
643
+ }
644
+ }
645
+ return intCertain
646
+ }
647
+
648
+ /** Returns `expr => boolean` — integer-shaped expressions in `body`. */
649
+ export function intExprChecker(body) {
650
+ return makeIsIntExpr(intCertainMap(body))
651
+ }