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/plan.js DELETED
@@ -1,2132 +0,0 @@
1
- /**
2
- * Pre-emit compile planning: bridges prepare (AST shape) and emit (wasm bytes).
3
- *
4
- * # Stage contract
5
- * IN: populated `ctx` from prepare.js (functions, schemas, scopes, modules)
6
- * plus the prepared AST.
7
- * OUT: returns a `programFacts` object; mutates `ctx` so each function has
8
- * narrowed signatures, finalized global reps, and per-call decisions.
9
- *
10
- * # Pipeline (top-level `plan(ast)`)
11
- * 1. unboxConstTypedGlobals — finalize global storage. (Global value facts
12
- * themselves are seeded by prepare via `infer.recordGlobalRep`.)
13
- * 2. collectProgramFacts — sweep arrow bodies for typed-elem usage, key sets,
14
- * loop depth, control-transfer shapes; rerun if hot inlining changes the AST.
15
- * 3. materializeAutoBoxSchemas / resolveClosureWidth — settle layout decisions.
16
- * 4. Whole-program narrowing (skipped on simple programs):
17
- * - narrowSignatures — pick a specialization per function from call sites
18
- * - specializeBimorphicTyped — split typed-elem hot paths into two variants
19
- * when callers diverge between two ctors
20
- * - refineDynKeys — tighten dynamic property-key sets
21
- *
22
- * No bytes are emitted here; emit.js consumes the planned ctx + programFacts.
23
- *
24
- * @module plan
25
- */
26
-
27
- import { ctx } from './ctx.js'
28
- import { T, VAL, ASSIGN_OPS, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts, analyzeFuncNamespaces, extractParams, intLiteralValue } from './analyze.js'
29
- import { includeModule } from './autoload.js'
30
- import { MAX_CLOSURE_ARITY, UNDEF_WAT } from './ir.js'
31
- import narrowSignatures, { specializeBimorphicTyped, refineDynKeys, applyJsstringBoundaryCarrierStandalone, narrowBoolResults } from './narrow.js'
32
-
33
- const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
34
- const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
35
- // Fixed-size typed arrays eligible for scalar replacement, mapped to the element
36
- // store-coercion kind ('' = none, i.e. Float64Array's f64-identity). Excluded:
37
- // Float32Array — store coercion is `Math.fround`, needs the `math` module pulled at plan time
38
- // Uint32Array — element range [0, 2^32) exceeds what jz keeps as f64 after `x >>> 0` (i32-narrowed)
39
- // Uint8ClampedArray — round-half-to-even clamp
40
- // Coerced (truthy) types are scalarized only when fully local — any escape (passed
41
- // to a call, `.buffer`/view aliasing, etc.) keeps the real allocation, since the
42
- // mirror/fence path can't track writes through an alias that outlives the fence.
43
- const SCALAR_TYPED_COERCE = {
44
- 'new.Float64Array': '',
45
- 'new.Int32Array': 'i32',
46
- 'new.Int16Array': 'i16', 'new.Uint16Array': 'u16',
47
- 'new.Int8Array': 'i8', 'new.Uint8Array': 'u8',
48
- }
49
- // AST for the store coercion a typed-array element does on write (`arr[i] = v`).
50
- // All expressible with operators jz already lowers post-plan (no module deps).
51
- const coerceAST = (kind, expr) => {
52
- if (kind === 'i32') return ['|', expr, [null, 0]]
53
- if (kind === 'i16') return ['>>', ['<<', expr, [null, 16]], [null, 16]]
54
- if (kind === 'u16') return ['&', expr, [null, 0xffff]]
55
- if (kind === 'i8') return ['>>', ['<<', expr, [null, 24]], [null, 24]]
56
- if (kind === 'u8') return ['&', expr, [null, 0xff]]
57
- return expr
58
- }
59
- const maxScalarTypedArrayLen = () => ctx.transform.optimize?.scalarTypedArrayLen ?? 32
60
- const maxScalarTypedLoopUnroll = () => ctx.transform.optimize?.scalarTypedLoopUnroll ?? 16
61
- const maxScalarTypedNestedUnroll = () => ctx.transform.optimize?.scalarTypedNestedUnroll ?? 128
62
-
63
- const isSeq = node => Array.isArray(node) && node[0] === ';'
64
- const blockStmts = body => {
65
- if (!Array.isArray(body) || body[0] !== '{}') return null
66
- const inner = body[1]
67
- if (!Array.isArray(inner)) return inner == null ? [] : [inner]
68
- return inner[0] === ';' ? inner.slice(1) : [inner]
69
- }
70
-
71
- const callArgs = node => {
72
- if (!Array.isArray(node) || node[0] !== '()') return null
73
- const raw = node[2]
74
- return raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
75
- }
76
-
77
- const isSimpleArg = node => {
78
- if (typeof node === 'string' || typeof node === 'number') return true
79
- if (!Array.isArray(node)) return false
80
- if (node[0] == null) return typeof node[1] === 'number'
81
- if (node[0] === 'str') return typeof node[1] === 'string'
82
- if (node[0] === 'u-' || (node[0] === '-' && node.length === 2)) return isSimpleArg(node[1])
83
- if (['+', '-', '*', '%', '&', '|', '^', '<<', '>>', '>>>'].includes(node[0]))
84
- return isSimpleArg(node[1]) && isSimpleArg(node[2])
85
- return false
86
- }
87
-
88
- const scanBody = (node, fn) => {
89
- if (!Array.isArray(node)) return false
90
- if (fn(node)) return true
91
- if (node[0] === '=>') return false
92
- for (let i = 1; i < node.length; i++) if (scanBody(node[i], fn)) return true
93
- return false
94
- }
95
-
96
- const loopDepth = (node, depth) => {
97
- if (!Array.isArray(node)) return depth
98
- if (node[0] === '=>') return depth
99
- const here = LOOP_OPS.has(node[0]) ? depth + 1 : depth
100
- let max = here
101
- for (let i = 1; i < node.length; i++) {
102
- const d = loopDepth(node[i], here)
103
- if (d > max) max = d
104
- }
105
- return max
106
- }
107
-
108
- const nodeSize = (node) => {
109
- if (!Array.isArray(node)) return 1
110
- let n = 1
111
- for (let i = 1; i < node.length; i++) n += nodeSize(node[i])
112
- return n
113
- }
114
-
115
- const collectBindings = (node, out) => {
116
- if (!Array.isArray(node)) return
117
- const op = node[0]
118
- if (op === '=>') return
119
- if (op === 'let' || op === 'const') {
120
- for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
121
- }
122
- for (let i = 1; i < node.length; i++) collectBindings(node[i], out)
123
- }
124
-
125
- const collectBindingTarget = (node, out) => {
126
- if (typeof node === 'string') { out.add(node); return }
127
- if (!Array.isArray(node)) return
128
- if (node[0] === '=') collectBindingTarget(node[1], out)
129
- else if (node[0] === '...' && typeof node[1] === 'string') out.add(node[1])
130
- else if (node[0] === ',' || node[0] === '[]' || node[0] === '{}')
131
- for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
132
- }
133
-
134
- const mutatesAny = (node, names) => scanBody(node, n => {
135
- const op = n[0]
136
- if ((op === '++' || op === '--') && typeof n[1] === 'string') return names.has(n[1])
137
- return ASSIGN_OPS.has(op) && typeof n[1] === 'string' && names.has(n[1])
138
- })
139
-
140
- const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
141
-
142
- const intLit = node => {
143
- if (typeof node === 'number' && Number.isInteger(node)) return node
144
- if (Array.isArray(node) && node[0] == null && Number.isInteger(node[1])) return node[1]
145
- return null
146
- }
147
-
148
- const constIntExpr = (node) => {
149
- const lit = intLit(node)
150
- if (lit != null) return lit
151
- if (typeof node === 'string') return ctx.scope.constInts?.get(node) ?? null
152
- if (!Array.isArray(node)) return null
153
- const op = node[0]
154
- if (op === 'u-') {
155
- const v = constIntExpr(node[1])
156
- return v == null ? null : -v
157
- }
158
- if (node.length !== 3) return null
159
- const a = constIntExpr(node[1]), b = constIntExpr(node[2])
160
- if (a == null || b == null) return null
161
- if (op === '+') return a + b
162
- if (op === '-') return a - b
163
- if (op === '*') return a * b
164
- if (op === '<<') return a << b
165
- return null
166
- }
167
-
168
- const setCallArgs = (node, args) => {
169
- node[2] = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
170
- }
171
-
172
- const scalarArrayElems = (expr) => {
173
- if (!Array.isArray(expr) || expr[0] !== '[') return null
174
- const elems = expr.slice(1)
175
- if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...') || !isSimpleArg(e))) return null
176
- return elems
177
- }
178
-
179
- const scalarObjectProps = (expr) => {
180
- if (!Array.isArray(expr) || expr[0] !== '{}') return null
181
- const props = staticObjectProps(expr.slice(1))
182
- if (!props) return null
183
- const seen = new Set()
184
- for (let i = 0; i < props.names.length; i++) {
185
- const name = props.names[i]
186
- if (seen.has(name) || !isSimpleArg(props.values[i])) return null
187
- seen.add(name)
188
- }
189
- return props
190
- }
191
-
192
- const fixedScalarTypedArray = (expr) => {
193
- const ctor = typedElemCtor(expr)
194
- if (ctor == null || !(ctor in SCALAR_TYPED_COERCE)) return null
195
- const args = callArgs(expr)
196
- if (!args || args.length !== 1) return null
197
- const len = constIntExpr(args[0])
198
- return len != null && len >= 0 && len <= maxScalarTypedArrayLen()
199
- ? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
200
- }
201
-
202
- const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
203
-
204
- const safeScalarArrayUse = (node, name, len, parentOp = null) => {
205
- if (typeof node === 'string') return node !== name
206
- if (!Array.isArray(node)) return true
207
- const op = node[0]
208
- if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
209
- if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
210
- if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
211
- // Element write `name[idx] (op)= v` / `name[idx]++`: an out-of-bounds index
212
- // grows the array (sparse-array semantics), which the fixed scalar slot set
213
- // can't model — reject unless idx is a literal within the literal's bounds.
214
- if ((ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
215
- && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
216
- const idx = intLit(node[1][2])
217
- if (idx == null || idx < 0 || idx >= len) return false
218
- for (let i = 2; i < node.length; i++) if (!safeScalarArrayUse(node[i], name, len, op)) return false
219
- return true
220
- }
221
- if (op === '[]' && node[1] === name) return intLit(node[2]) != null
222
- if (op === '...' && node[1] === name) return parentOp === '['
223
- for (let i = 1; i < node.length; i++) {
224
- if (!safeScalarArrayUse(node[i], name, len, op)) return false
225
- }
226
- return true
227
- }
228
-
229
- const rewriteScalarArrayUses = (node, arrays, parentOp = null) => {
230
- if (!Array.isArray(node)) return node
231
- const op = node[0]
232
- if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') {
233
- return [, arrays.get(node[1]).length]
234
- }
235
- if (op === '[]' && arrays.has(node[1])) {
236
- const idx = intLit(node[2])
237
- const elems = arrays.get(node[1])
238
- return idx != null && idx >= 0 && idx < elems.length ? elems[idx] : [, undefined]
239
- }
240
- if (op === '[') {
241
- const out = ['[']
242
- for (let i = 1; i < node.length; i++) {
243
- const item = node[i]
244
- if (Array.isArray(item) && item[0] === '...' && arrays.has(item[1])) {
245
- out.push(...arrays.get(item[1]))
246
- } else {
247
- out.push(rewriteScalarArrayUses(item, arrays, op))
248
- }
249
- }
250
- return out
251
- }
252
- return node.map((part, i) => i === 0 ? part : rewriteScalarArrayUses(part, arrays, op))
253
- }
254
-
255
- const safeScalarObjectUse = (node, name, keys) => {
256
- if (typeof node === 'string') return node !== name
257
- if (!Array.isArray(node)) return true
258
- const op = node[0]
259
- if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
260
- if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
261
- if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2])
262
- if (op === '[]' && node[1] === name) {
263
- const key = staticPropertyKey(node[2])
264
- return key != null && keys.has(key)
265
- }
266
- if (op === '...' && node[1] === name) return false
267
- for (let i = 1; i < node.length; i++) {
268
- if (!safeScalarObjectUse(node[i], name, keys)) return false
269
- }
270
- return true
271
- }
272
-
273
- const rewriteScalarObjectUses = (node, objects) => {
274
- if (!Array.isArray(node)) return node
275
- const op = node[0]
276
- if ((op === '.' || op === '?.') && objects.has(node[1])) {
277
- const fields = objects.get(node[1])
278
- return fields.get(node[2]) ?? [, undefined]
279
- }
280
- if (op === '[]' && objects.has(node[1])) {
281
- const key = staticPropertyKey(node[2])
282
- const fields = objects.get(node[1])
283
- return key != null ? (fields.get(key) ?? [, undefined]) : node
284
- }
285
- return node.map((part, i) => i === 0 ? part : rewriteScalarObjectUses(part, objects))
286
- }
287
-
288
- const typedArraySlotIndex = (node, len) => {
289
- const idx = constIntExpr(node)
290
- return idx != null && idx >= 0 && idx < len ? idx : null
291
- }
292
-
293
- // `coerce` truthy ⇒ the array's element type truncates on store (Int*/Uint* views),
294
- // so in-place updates (`arr[i]++`, `arr[i] += x`) can't be a plain `slot`-op rewrite —
295
- // reject them and only scalarize plain `arr[i] = v` writes and `arr[i]` reads.
296
- const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
297
- if (typeof node === 'string') return node !== name
298
- if (!Array.isArray(node)) return true
299
- const op = node[0]
300
- if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
301
- if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
302
- if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
303
- if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
304
- return !coerce && typedArraySlotIndex(node[1][2], len) != null
305
- if (ASSIGN_TARGET_OPS.has(op)) {
306
- if (node[1] === name) return false
307
- if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
308
- if (coerce && op !== '=') return false
309
- if (typedArraySlotIndex(node[1][2], len) == null) return false
310
- for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
311
- return true
312
- }
313
- }
314
- if (op === '...' && node[1] === name) return false
315
- for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
316
- return true
317
- }
318
-
319
- const mentionsName = (node, name) => {
320
- if (typeof node === 'string') return node === name
321
- if (!Array.isArray(node) || node[0] === '=>') return false
322
- for (let i = 1; i < node.length; i++) if (mentionsName(node[i], name)) return true
323
- return false
324
- }
325
-
326
- const rewriteScalarTypedArrayUses = (node, arrays) => {
327
- if (!Array.isArray(node)) return node
328
- const op = node[0]
329
- const slotFor = (idxNode, entry) => {
330
- const idx = typedArraySlotIndex(idxNode, entry.len)
331
- return idx == null ? null : entry.slots[idx]
332
- }
333
- if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') return [null, arrays.get(node[1]).len]
334
- if (op === '[]' && arrays.has(node[1])) return slotFor(node[2], arrays.get(node[1])) ?? node
335
- if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
336
- const slot = slotFor(node[1][2], arrays.get(node[1][1]))
337
- return slot ? [op, slot] : node
338
- }
339
- if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
340
- const entry = arrays.get(node[1][1])
341
- const slot = slotFor(node[1][2], entry)
342
- if (!slot) return node
343
- const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
344
- return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
345
- }
346
- return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
347
- }
348
-
349
- const scalarTypedArrayStores = (name, entry) =>
350
- entry.slots.map((slot, i) => ['=', ['[]', name, [null, i]], slot])
351
-
352
- const scalarTypedArrayLoads = (name, entry) =>
353
- entry.slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])
354
-
355
- const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
356
- if (!Array.isArray(node)) return out
357
- const op = node[0]
358
- const addSlot = target => {
359
- if (Array.isArray(target) && target[0] === '[]' && target[1] === name) {
360
- const idx = typedArraySlotIndex(target[2], len)
361
- if (idx != null) out.add(idx)
362
- return true
363
- }
364
- return false
365
- }
366
- if ((op === '++' || op === '--') && addSlot(node[1])) return out
367
- if (ASSIGN_TARGET_OPS.has(op) && addSlot(node[1])) {
368
- for (let i = 2; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
369
- return out
370
- }
371
- if (op !== '=>') for (let i = 1; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
372
- return out
373
- }
374
-
375
- const hasScalarTypedArrayRead = (node, name) => {
376
- if (!Array.isArray(node)) return false
377
- const op = node[0]
378
- const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
379
- if ((op === '++' || op === '--') && isTarget(node[1])) return true
380
- if (ASSIGN_TARGET_OPS.has(op)) {
381
- if (isTarget(node[1])) {
382
- if (op !== '=') return true
383
- for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
384
- return false
385
- }
386
- }
387
- if (op === '[]' && node[1] === name) return true
388
- if (op === '=>') return false
389
- for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
390
- return false
391
- }
392
-
393
- const scalarizeTypedArrayLiteralSeq = (seq) => {
394
- if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
395
- let changed = false
396
- const stmts = seq.slice(1).map(stmt => {
397
- const r = scalarizeTypedArrayLiterals(stmt)
398
- changed ||= r.changed
399
- return r.node
400
- })
401
-
402
- const candidates = new Map()
403
- const mirrored = new Map()
404
- for (let i = 0; i < stmts.length; i++) {
405
- const stmt = stmts[i]
406
- if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
407
- const decl = stmt[1]
408
- if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
409
- const fixed = fixedScalarTypedArray(decl[2])
410
- if (fixed == null) continue
411
- const { len, coerce } = fixed
412
- let hasSafeUse = false, hasUnsafeUse = false
413
- for (let j = 0; j < stmts.length; j++) {
414
- if (j === i) continue
415
- if (!mentionsName(stmts[j], decl[1])) continue
416
- const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
417
- hasSafeUse ||= safe
418
- hasUnsafeUse ||= !safe
419
- }
420
- if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
421
- if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
422
- else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
423
- }
424
- if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
425
-
426
- const arrays = new Map()
427
- for (const [name, c] of [...candidates, ...mirrored]) {
428
- const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
429
- arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
430
- }
431
-
432
- const out = []
433
- for (let i = 0; i < stmts.length; i++) {
434
- const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
435
- [...mirrored.entries()].find(([, c]) => c.index === i)
436
- if (entry) {
437
- const [name] = entry
438
- const arr = arrays.get(name)
439
- const { slots } = arr
440
- if (arr.mirrored) {
441
- out.push(stmts[i])
442
- if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
443
- } else if (slots.length) {
444
- out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
445
- }
446
- changed = true
447
- continue
448
- }
449
- const unsafe = []
450
- for (const [name, arr] of arrays) {
451
- if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
452
- }
453
- if (unsafe.length) {
454
- for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
455
- out.push(stmts[i])
456
- for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
457
- changed = true
458
- } else {
459
- out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
460
- }
461
- }
462
- return { node: [';', ...out], changed: true }
463
- }
464
-
465
- function scalarizeTypedArrayLiterals(node) {
466
- if (!Array.isArray(node)) return { node, changed: false }
467
- if (node[0] === '=>') return { node, changed: false }
468
- if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
469
- let changed = false
470
- const out = [node[0]]
471
- for (let i = 1; i < node.length; i++) {
472
- const r = scalarizeTypedArrayLiterals(node[i])
473
- changed ||= r.changed
474
- out.push(r.node)
475
- }
476
- return changed ? { node: out, changed: true } : { node, changed: false }
477
- }
478
-
479
- const stmtList = (body) => {
480
- if (!Array.isArray(body)) return body == null ? [] : [body]
481
- if (body[0] === '{}') return stmtList(body[1])
482
- if (body[0] === ';') return body.slice(1)
483
- return [body]
484
- }
485
-
486
- const hasControlTransfer = node => scanBody(node, n => CONTROL_TRANSFER.has(n[0]))
487
-
488
- const containsDeclOf = (body, name) => scanBody(body, n => {
489
- if (n[0] !== 'let' && n[0] !== 'const') return false
490
- for (let i = 1; i < n.length; i++) {
491
- const d = n[i]
492
- if (d === name) return true
493
- if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
494
- }
495
- return false
496
- })
497
-
498
- const isReassigned = (body, name) => scanBody(body, n =>
499
- (ASSIGN_OPS.has(n[0]) && n[1] === name) || ((n[0] === '++' || n[0] === '--') && n[1] === name))
500
-
501
- const containsTypedArrayAccess = (body, names) => scanBody(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
502
-
503
- function smallScalarTypedForTrip(init, cond, step) {
504
- if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
505
- const decl = init[1]
506
- if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
507
- const name = decl[1]
508
- if (constIntExpr(decl[2]) !== 0) return null
509
- if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
510
- const end = constIntExpr(cond[2])
511
- if (end == null || end < 0 || end > maxScalarTypedLoopUnroll()) return null
512
- const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
513
- (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
514
- return stepOk ? { name, end } : null
515
- }
516
-
517
- const scalarTypedLoopBudget = (body) => {
518
- if (!Array.isArray(body) || body[0] === '=>') return 1
519
- if (body[0] === 'for') {
520
- const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
521
- return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
522
- }
523
- let max = 1
524
- for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
525
- return max
526
- }
527
-
528
- const unrollTypedArrayLoops = (node, names) => {
529
- if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
530
- if (node[0] === ';') {
531
- let changed = false
532
- const out = [';']
533
- for (const stmt of node.slice(1)) {
534
- const r = unrollTypedArrayLoops(stmt, names)
535
- changed ||= r.changed
536
- if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
537
- else out.push(r.node)
538
- }
539
- return changed ? { node: out, changed: true } : { node, changed: false }
540
- }
541
- if (node[0] === '{}') {
542
- const r = unrollTypedArrayLoops(node[1], names)
543
- return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
544
- }
545
- if (node[0] === 'for') {
546
- const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
547
- if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
548
- !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
549
- const out = [';']
550
- for (let i = 0; i < trip.end; i++) {
551
- const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
552
- const r = unrollTypedArrayLoops(cloned, names)
553
- out.push(...stmtList(r.node))
554
- }
555
- return { node: out, changed: true }
556
- }
557
- }
558
- let changed = false
559
- const out = [node[0]]
560
- for (let i = 1; i < node.length; i++) {
561
- const r = unrollTypedArrayLoops(node[i], names)
562
- changed ||= r.changed
563
- out.push(r.node)
564
- }
565
- return changed ? { node: out, changed: true } : { node, changed: false }
566
- }
567
-
568
- const fixedTypedArraysInBody = (body) => {
569
- const out = new Map()
570
- const walk = node => {
571
- if (!Array.isArray(node) || node[0] === '=>') return
572
- if (node[0] === 'let' || node[0] === 'const') {
573
- for (let i = 1; i < node.length; i++) {
574
- const d = node[i]
575
- if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
576
- const fixed = fixedScalarTypedArray(d[2])
577
- if (fixed != null) out.set(d[1], fixed)
578
- }
579
- }
580
- for (let i = 1; i < node.length; i++) walk(node[i])
581
- }
582
- walk(body)
583
- return out
584
- }
585
-
586
- const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
587
- if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
588
- if (scanBody(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
589
- const params = func.sig?.params || []
590
- const cands = new Map()
591
- for (let i = 0; i < params.length; i++) {
592
- const pname = params[i].name
593
- let len = null, coerce = null, ok = true
594
- for (const site of sites) {
595
- const arg = site.argList[i]
596
- const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
597
- if (!fixed) { ok = false; break }
598
- if (len == null) { len = fixed.len; coerce = fixed.coerce }
599
- else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
600
- }
601
- if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
602
- }
603
- if (!cands.size) return cands
604
- for (const site of sites) {
605
- const seen = new Set()
606
- for (let i = 0; i < params.length; i++) {
607
- if (!cands.has(params[i].name)) continue
608
- const arg = site.argList[i]
609
- if (typeof arg !== 'string' || seen.has(arg)) return new Map()
610
- seen.add(arg)
611
- }
612
- }
613
- return cands
614
- }
615
-
616
- const scalarizeTypedArrayParams = (func, paramCands) => {
617
- for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
618
- for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
619
- if (!paramCands.size) return { body: func.body, changed: false }
620
- const arrays = new Map()
621
- for (const [name, c] of paramCands) {
622
- arrays.set(name, {
623
- len: c.len,
624
- coerce: c.coerce,
625
- slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
626
- })
627
- }
628
- const prologue = []
629
- const writeback = []
630
- for (const [name, { len, slots }] of arrays) {
631
- if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
632
- for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
633
- }
634
- const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
635
- return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
636
- }
637
-
638
- const scalarizeFunctionTypedArrays = (programFacts) => {
639
- const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
640
- const sitesByCallee = new Map()
641
- for (const site of programFacts.callSites) {
642
- if (!site.callerFunc) continue
643
- const list = sitesByCallee.get(site.callee)
644
- if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
645
- }
646
- let changed = false
647
- for (const func of ctx.func.list) {
648
- if (!func.body || func.raw) continue
649
- const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
650
- const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
651
- if (names.size) {
652
- let guard = 0
653
- while (guard++ < 6) {
654
- const r = unrollTypedArrayLoops(func.body, names)
655
- if (!r.changed) break
656
- func.body = r.node
657
- changed = true
658
- }
659
- }
660
- const p = scalarizeTypedArrayParams(func, paramCands)
661
- if (p.changed) { func.body = p.body; changed = true }
662
- const l = scalarizeTypedArrayLiterals(func.body)
663
- if (l.changed) { func.body = l.node; changed = true }
664
- if (changed) invalidateLocalsCache(func.body)
665
- }
666
- return changed
667
- }
668
-
669
- const scalarizeArrayLiteralSeq = (seq) => {
670
- if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
671
- let changed = false
672
- const stmts = seq.slice(1).map(stmt => {
673
- const r = scalarizeArrayLiterals(stmt)
674
- changed ||= r.changed
675
- return r.node
676
- })
677
-
678
- const candidates = new Map()
679
- for (let i = 0; i < stmts.length; i++) {
680
- const stmt = stmts[i]
681
- if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
682
- const decl = stmt[1]
683
- if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
684
- const elems = scalarArrayElems(decl[2])
685
- if (!elems) continue
686
- let ok = true
687
- for (let j = 0; j < stmts.length && ok; j++) {
688
- if (j === i) continue
689
- ok = safeScalarArrayUse(stmts[j], decl[1], elems.length)
690
- }
691
- if (!ok) continue
692
- candidates.set(decl[1], { index: i, op: stmt[0], elems })
693
- }
694
- if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
695
-
696
- const arrays = new Map()
697
- for (const [name, c] of candidates) {
698
- const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
699
- arrays.set(name, temps)
700
- }
701
-
702
- const out = []
703
- for (let i = 0; i < stmts.length; i++) {
704
- const entry = [...candidates.entries()].find(([, c]) => c.index === i)
705
- if (entry) {
706
- const [name, c] = entry
707
- const temps = arrays.get(name)
708
- if (temps.length) {
709
- out.push([c.op, ...temps.map((tmp, k) =>
710
- ['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
711
- }
712
- changed = true
713
- continue
714
- }
715
- out.push(rewriteScalarArrayUses(stmts[i], arrays))
716
- }
717
- return { node: [';', ...out], changed: true }
718
- }
719
-
720
- const scalarizeObjectLiteralSeq = (seq, escapes) => {
721
- if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
722
- let changed = false
723
- const stmts = seq.slice(1).map(stmt => {
724
- const r = scalarizeObjectLiterals(stmt, escapes)
725
- changed ||= r.changed
726
- return r.node
727
- })
728
-
729
- const candidates = new Map()
730
- for (let i = 0; i < stmts.length; i++) {
731
- const stmt = stmts[i]
732
- if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
733
- const decl = stmt[1]
734
- if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
735
- if (escapes.get(decl[1]) !== false) continue
736
- const props = scalarObjectProps(decl[2])
737
- if (!props) continue
738
- const keys = new Set(props.names)
739
- let ok = true
740
- for (let j = 0; j < stmts.length && ok; j++) {
741
- if (j === i) continue
742
- ok = safeScalarObjectUse(stmts[j], decl[1], keys)
743
- }
744
- if (!ok) continue
745
- candidates.set(decl[1], { index: i, op: stmt[0], props })
746
- }
747
- if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
748
-
749
- const objects = new Map()
750
- for (const [name, c] of candidates) {
751
- const fields = new Map()
752
- for (let i = 0; i < c.props.names.length; i++) {
753
- fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
754
- }
755
- objects.set(name, fields)
756
- }
757
-
758
- const out = []
759
- for (let i = 0; i < stmts.length; i++) {
760
- const entry = [...candidates.entries()].find(([, c]) => c.index === i)
761
- if (entry) {
762
- const [, c] = entry
763
- const fields = objects.get(entry[0])
764
- if (c.props.names.length) {
765
- out.push([c.op, ...c.props.names.map((prop, k) =>
766
- ['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
767
- }
768
- changed = true
769
- continue
770
- }
771
- out.push(rewriteScalarObjectUses(stmts[i], objects))
772
- }
773
- return { node: [';', ...out], changed: true }
774
- }
775
-
776
- function scalarizeObjectLiterals(node, escapes) {
777
- if (!Array.isArray(node)) return { node, changed: false }
778
- if (node[0] === '=>') return { node, changed: false }
779
- if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
780
- let changed = false
781
- const out = [node[0]]
782
- for (let i = 1; i < node.length; i++) {
783
- const r = scalarizeObjectLiterals(node[i], escapes)
784
- changed ||= r.changed
785
- out.push(r.node)
786
- }
787
- return changed ? { node: out, changed: true } : { node, changed: false }
788
- }
789
-
790
- function scalarizeArrayLiterals(node) {
791
- if (!Array.isArray(node)) return { node, changed: false }
792
- if (node[0] === '=>') return { node, changed: false }
793
- if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
794
- let changed = false
795
- const out = [node[0]]
796
- for (let i = 1; i < node.length; i++) {
797
- const r = scalarizeArrayLiterals(node[i])
798
- changed ||= r.changed
799
- out.push(r.node)
800
- }
801
- return changed ? { node: out, changed: true } : { node, changed: false }
802
- }
803
-
804
- const scalarizeFunctionArrayLiterals = () => {
805
- let changed = false
806
- for (const func of ctx.func.list) {
807
- if (!func.body || func.raw) continue
808
- let guard = 0
809
- while (guard++ < 4) {
810
- const r = scalarizeArrayLiterals(func.body)
811
- if (!r.changed) break
812
- func.body = r.node
813
- changed = true
814
- }
815
- }
816
- return changed
817
- }
818
-
819
- // === Int-array → Int32Array auto-promotion =================================
820
- //
821
- // A `let xs = [intLit, intLit, …]` binding whose every use is TYPED-compatible
822
- // is rewritten to `let xs = new Int32Array([intLit, …])`. Downstream analysis
823
- // then takes over: valTypeOf → VAL.TYPED, methods dispatch through `.typed:`,
824
- // loops get auto-vectorized via i32x4 (SIMD pass), and the carrier shrinks
825
- // from 8-byte f64 slots to packed i32. The promotion runs after literal
826
- // scalarization (so arrays fully scalarized away are already gone) and before
827
- // typed-array param scalarization (so a freshly-promoted array can still
828
- // participate in subsequent loop unrolling).
829
- //
830
- // Safety: the binding must never appear in a pattern that TYPED can't honor.
831
- // Disqualifiers (each fires per binding name):
832
- // 1. reassignment `xs = …` / compound `xs +=` / `++xs` / `--xs` — TYPED has
833
- // no value-replacement op; `xs = new TypedArray(…)` would also drop the
834
- // promoted view's identity.
835
- // 2. element write `xs[k] = v` — TYPED's i32-trunc store would lose
836
- // fractional/NaN bits that VAL.ARRAY would have preserved.
837
- // 3. method calls outside the read-safe whitelist (push, pop, shift, …) —
838
- // TYPED arrays are fixed-length; mutators don't exist on the carrier.
839
- // 4. `Array.isArray(xs)` — semantics flip true→false on promotion.
840
- // 5. `…xs` spread / `xs` as call arg / `xs` as return / bare reference in
841
- // any other position — escape; callee may rely on ARRAY layout.
842
- // 6. captured by a closure / shadowed by an inner decl — same escape
843
- // reasoning, plus the inner decl could rebind to a non-array.
844
- //
845
- // Elements at init must all be i32-range integer literals. A negative literal
846
- // arrives as `[null, -n]` after prepare's constant folding; intLiteralValue
847
- // recognizes that form. Float literals (`[1, 2.5]`) and out-of-range ints
848
- // (`0x80000000` on the +ve side, etc.) disqualify the array as a whole.
849
-
850
- // Methods we promote across. The bar: every entry must have a real `.typed:*`
851
- // emitter in module/typedarray.js. Receiver-flow into chained methods is also
852
- // typed-aware now — `.typed:map`/`.typed:filter`/`.typed:slice` all return
853
- // TYPED carriers, and downstream `.filter`/`.slice`/`.map`/etc. on those
854
- // re-dispatch via emit.js:2211's `.typed:<m>` lookup (VAL.TYPED ⇒ typed
855
- // emitter). Methods missing here (.join, .sort, .reverse, .subarray, .fill,
856
- // .toString, .copyWithin, …) lack a typed emitter — disqualify the candidate.
857
- const _TYPED_SAFE_METHODS = new Set([
858
- 'set',
859
- 'map', 'filter', 'slice',
860
- 'forEach', 'reduce',
861
- 'indexOf', 'includes', 'find', 'findIndex', 'some', 'every',
862
- ])
863
-
864
- // `.length` is TYPED-aware via core.js:__len (shifts the byte header by
865
- // __typed_shift on TAG=3). `.byteLength`/`.byteOffset`/`.buffer` are
866
- // TYPED-only — a user reading those already expects a typed array, so a
867
- // promotion candidate that hits them is a coincidence we'd rather not
868
- // rely on; disqualify and let them write the TypedArray construction
869
- // themselves.
870
- const _TYPED_SAFE_PROPS = new Set(['length'])
871
-
872
- // Returns the i32-range integer payload of an array-literal element, or null
873
- // if the element isn't a literal integer that fits in i32. Mirrors the shape
874
- // check used by `intLiteralValue` but without the rep-lookup (we're pre-
875
- // analysis here, and want a pure syntactic gate).
876
- const _intArrayLitElems = (expr) => {
877
- if (!Array.isArray(expr) || expr[0] !== '[') return null
878
- if (expr.length < 2) return null // empty literal — low value, skip
879
- const out = []
880
- for (let i = 1; i < expr.length; i++) {
881
- const v = intLiteralValue(expr[i])
882
- if (v == null) return null
883
- out.push(v)
884
- }
885
- return out
886
- }
887
-
888
- // True iff `name` appears anywhere within `node` as a bare identifier or
889
- // inside any expression position. Used to detect escape across a closure
890
- // boundary (where we can't trace the use sites locally).
891
- const _refsName = (node, name) => {
892
- if (typeof node === 'string') return node === name
893
- if (!Array.isArray(node)) return false
894
- for (let i = 1; i < node.length; i++) if (_refsName(node[i], name)) return true
895
- return false
896
- }
897
-
898
- // Walks `node` and disqualifies every candidate name that appears in an
899
- // unsafe context. `initSet` holds the candidate's own init-decl AST nodes
900
- // (their LHS reference is the binding being defined, not an escape).
901
- const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
902
- if (initSet.has(node)) {
903
- // The init decl itself: only walk the RHS (skip the LHS `name`).
904
- return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
905
- }
906
- if (typeof node === 'string') {
907
- // Bare identifier outside any handled parent context — escape.
908
- if (candidates.has(node)) disqualified.add(node)
909
- return
910
- }
911
- if (!Array.isArray(node)) return
912
- const op = node[0]
913
-
914
- // Closure body — any candidate referenced inside is captured. Bail without
915
- // recursing further (we'd otherwise hit the bare-name leaf and disqualify
916
- // anyway, but this is explicit and avoids walking the inner closure).
917
- if (op === '=>') {
918
- for (const n of candidates.keys()) {
919
- if (!disqualified.has(n) && _refsName(node, n)) disqualified.add(n)
920
- }
921
- return
922
- }
923
-
924
- // Property access `name.prop` / `name?.prop`. Bare property reads only —
925
- // method calls reach here via the `()` handler below (which intercepts
926
- // before recursing into its callee).
927
- if ((op === '.' || op === '?.') && typeof node[1] === 'string' && candidates.has(node[1])) {
928
- if (!_TYPED_SAFE_PROPS.has(node[2])) disqualified.add(node[1])
929
- return // node[2] is the property name (string), not an expression — done
930
- }
931
-
932
- // Method or function call. Two shapes carry the candidate:
933
- // `name.method(args)` — receiver at node[1][1]; method whitelist gates.
934
- // `f(…, name, …)` / `Array.isArray(name)` — name appears as a plain arg.
935
- if (op === '()') {
936
- const callee = node[1]
937
- // Method call on a candidate receiver.
938
- if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
939
- typeof callee[1] === 'string' && candidates.has(callee[1])) {
940
- if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
941
- // Walk method args (skip the receiver — already validated above).
942
- for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
943
- return
944
- }
945
- // Array.isArray flips true→false under promotion.
946
- if (callee === 'Array.isArray') {
947
- const raw = node[2]
948
- const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
949
- for (const a of list) {
950
- if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
951
- else _disqualifyPromotion(a, candidates, disqualified, initSet)
952
- }
953
- return
954
- }
955
- // Fall through to generic recursion — `name` as a plain arg will hit the
956
- // bare-name leaf above and disqualify.
957
- }
958
-
959
- // Index read `name[k]` — read access is TYPED-safe. Walk the key in case
960
- // it contains references to other candidate names.
961
- if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
962
- _disqualifyPromotion(node[2], candidates, disqualified, initSet)
963
- return
964
- }
965
-
966
- // Element write: `['=', ['[]', name, k], v]` (and compound forms).
967
- // V1 stays read-only after init — element writes would silently truncate.
968
- if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
969
- typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
970
- disqualified.add(node[1][1])
971
- _disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
972
- _disqualifyPromotion(node[2], candidates, disqualified, initSet)
973
- return
974
- }
975
-
976
- // Whole-binding reassign: `name = …` / `name += …` / etc.
977
- if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
978
- disqualified.add(node[1])
979
- _disqualifyPromotion(node[2], candidates, disqualified, initSet)
980
- return
981
- }
982
-
983
- // Pre/post-increment on var or element.
984
- if (op === '++' || op === '--') {
985
- const t = node[1]
986
- if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
987
- if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
988
- disqualified.add(t[1])
989
- _disqualifyPromotion(t[2], candidates, disqualified, initSet)
990
- return
991
- }
992
- }
993
-
994
- // `let`/`const` declaration — the candidate's own init lands here via the
995
- // initSet branch at the top. Any *other* decl that names a candidate is a
996
- // shadow (impossible after jz hoists, but defensive) and disqualifies.
997
- if (op === 'let' || op === 'const') {
998
- for (let i = 1; i < node.length; i++) {
999
- const d = node[i]
1000
- if (typeof d === 'string' && candidates.has(d)) disqualified.add(d)
1001
- else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
1002
- candidates.has(d[1]) && !initSet.has(d)) {
1003
- disqualified.add(d[1])
1004
- _disqualifyPromotion(d[2], candidates, disqualified, initSet)
1005
- } else {
1006
- _disqualifyPromotion(d, candidates, disqualified, initSet)
1007
- }
1008
- }
1009
- return
1010
- }
1011
-
1012
- // Spread `…name` — could be in a call, a `[`, or a destructure target.
1013
- if (op === '...' && typeof node[1] === 'string' && candidates.has(node[1])) {
1014
- disqualified.add(node[1])
1015
- return
1016
- }
1017
-
1018
- // for-of / for-in iteration: receiver position is `node[2]` (a bare name
1019
- // there would otherwise trigger escape). TYPED supports iteration, so
1020
- // allow the receiver but walk the body for other refs.
1021
- if (op === 'for-of' || op === 'for-in') {
1022
- // Walk decl (node[1]), iter (node[2]), body (node[3]); receiver as bare
1023
- // name is fine — only the body matters for further refs to the same name
1024
- // (but body refs would shadow or escape, which other rules catch).
1025
- for (let i = 1; i < node.length; i++) {
1026
- const child = node[i]
1027
- if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
1028
- _disqualifyPromotion(child, candidates, disqualified, initSet)
1029
- }
1030
- return
1031
- }
1032
-
1033
- // Generic — recurse into children. Bare-name refs at unhandled positions
1034
- // hit the string-leaf branch above and disqualify on contact.
1035
- for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
1036
- }
1037
-
1038
- // Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
1039
- // carries the exact init-decl AST node so the disqualifier can skip the
1040
- // binding's own LHS reference (which would otherwise look like a reassign).
1041
- const _collectIntArrayCandidates = (node, candidates) => {
1042
- if (!Array.isArray(node)) return
1043
- const op = node[0]
1044
- if (op === '=>') return
1045
- if (op === 'let' || op === 'const') {
1046
- for (let i = 1; i < node.length; i++) {
1047
- const d = node[i]
1048
- if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
1049
- const elems = _intArrayLitElems(d[2])
1050
- if (elems == null) continue
1051
- // jz hoists `let` to function scope — duplicate candidate-name collisions
1052
- // shouldn't happen, but if they do the second wins (disqualifyPromotion
1053
- // will mark both via the shadow rule).
1054
- candidates.set(d[1], { initDecl: d, elems })
1055
- }
1056
- }
1057
- for (let i = 1; i < node.length; i++) _collectIntArrayCandidates(node[i], candidates)
1058
- }
1059
-
1060
- // Rewrite `let name = [...]` → `let name = new Int32Array([...])` for every
1061
- // validated candidate. Preserves the original element AST nodes so the
1062
- // downstream `Int32Array.from` lowering picks up its existing static-data
1063
- // segment / per-element-store fast paths.
1064
- const _rewritePromoted = (node, validated, initSet) => {
1065
- if (!Array.isArray(node)) return { node, changed: false }
1066
- const op = node[0]
1067
- if (op === '=>') {
1068
- // Closures don't reach validated bindings (we disqualified on capture).
1069
- // Still recurse — a nested closure may itself hold a promotable.
1070
- let changed = false
1071
- const out = [op]
1072
- for (let i = 1; i < node.length; i++) {
1073
- const r = _rewritePromoted(node[i], validated, initSet)
1074
- if (r.changed) changed = true
1075
- out.push(r.node)
1076
- }
1077
- return changed ? { node: out, changed: true } : { node, changed: false }
1078
- }
1079
- let changed = false
1080
- const out = [op]
1081
- for (let i = 1; i < node.length; i++) {
1082
- const child = node[i]
1083
- if ((op === 'let' || op === 'const') && Array.isArray(child) && child[0] === '=' &&
1084
- typeof child[1] === 'string' && validated.has(child[1]) && initSet.has(child)) {
1085
- const newRhs = ['()', 'new.Int32Array', child[2]]
1086
- out.push(['=', child[1], newRhs])
1087
- changed = true
1088
- continue
1089
- }
1090
- const r = _rewritePromoted(child, validated, initSet)
1091
- if (r.changed) changed = true
1092
- out.push(r.node)
1093
- }
1094
- return changed ? { node: out, changed: true } : { node, changed: false }
1095
- }
1096
-
1097
- const promoteIntArrayLiteralsInBody = (body) => {
1098
- const candidates = new Map()
1099
- _collectIntArrayCandidates(body, candidates)
1100
- if (!candidates.size) return { node: body, changed: false }
1101
- const initSet = new Set()
1102
- for (const { initDecl } of candidates.values()) initSet.add(initDecl)
1103
- const disqualified = new Set()
1104
- _disqualifyPromotion(body, candidates, disqualified, initSet)
1105
- const validated = new Set()
1106
- for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
1107
- if (!validated.size) return { node: body, changed: false }
1108
- return _rewritePromoted(body, validated, initSet)
1109
- }
1110
-
1111
- // On the first successful promotion, pull in the typedarray module so the
1112
- // emitted `new.Int32Array` callee has a registered emitter. Calling
1113
- // `includeModule('typedarray')` on a no-op (already loaded) is cheap.
1114
- const promoteIntArrayLiterals = () => {
1115
- let changed = false
1116
- for (const func of ctx.func.list) {
1117
- if (!func.body || func.raw) continue
1118
- const r = promoteIntArrayLiteralsInBody(func.body)
1119
- if (r.changed) {
1120
- func.body = r.node
1121
- invalidateLocalsCache(func.body)
1122
- if (!changed) includeModule('typedarray')
1123
- changed = true
1124
- }
1125
- }
1126
- return changed
1127
- }
1128
-
1129
- const scalarizeFunctionObjectLiterals = () => {
1130
- let changed = false
1131
- for (const func of ctx.func.list) {
1132
- if (!func.body || func.raw) continue
1133
- let guard = 0
1134
- while (guard++ < 4) {
1135
- const escapes = new Map(analyzeBody(func.body).escapes)
1136
- invalidateLocalsCache(func.body)
1137
- const r = scalarizeObjectLiterals(func.body, escapes)
1138
- if (!r.changed) break
1139
- func.body = r.node
1140
- changed = true
1141
- }
1142
- }
1143
- return changed
1144
- }
1145
-
1146
- const cloneWithSubst = (node, subst, rename) => {
1147
- if (typeof node === 'string') {
1148
- if (subst.has(node)) return clonePlain(subst.get(node))
1149
- return rename.get(node) || node
1150
- }
1151
- if (!Array.isArray(node)) return node
1152
- const op = node[0]
1153
- if (op === 'str') return node.slice()
1154
- if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, rename), node[2]]
1155
- if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, rename)]
1156
- return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, rename))
1157
- }
1158
-
1159
- // Returns { prefix, value } where prefix is the substituted body statements
1160
- // (excluding any trailing `return X`), and value is the substituted return
1161
- // expression — null if void or no trailing return value.
1162
- const inlinedBody = (func, args) => {
1163
- const params = func.sig.params
1164
- if (args.length !== params.length || !args.every(isSimpleArg)) return null
1165
- const paramNames = new Set(params.map(p => p.name))
1166
- if (mutatesAny(func.body, paramNames)) return null
1167
-
1168
- const subst = new Map()
1169
- for (let i = 0; i < params.length; i++) subst.set(params[i].name, args[i])
1170
-
1171
- const locals = new Set()
1172
- collectBindings(func.body, locals)
1173
- for (const p of params) locals.delete(p.name)
1174
-
1175
- const rename = new Map()
1176
- for (const name of locals) rename.set(name, `${T}inl${ctx.func.uniq++}_${name}`)
1177
-
1178
- const stmts = blockStmts(func.body)
1179
- // Expression-bodied arrow `(c) => expr`: no statement block; the whole body
1180
- // *is* the return value. Treat as zero-prefix + value.
1181
- if (!stmts) return { prefix: [], value: cloneWithSubst(func.body, subst, rename) }
1182
- const last = stmts.length ? stmts[stmts.length - 1] : null
1183
- const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
1184
- const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
1185
- const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
1186
- const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
1187
- return { prefix, value }
1188
- }
1189
-
1190
- const isCandidateCall = (node, candidates) =>
1191
- Array.isArray(node) && node[0] === '()' && typeof node[1] === 'string' && candidates.has(node[1])
1192
-
1193
- // Recursively substitute calls to expr-bodied candidates anywhere in `node`.
1194
- // Used for tiny pure-expression helpers (`isAlpha(c) => …`) that get called
1195
- // from expression contexts (if-conditions, ternary tests). For these the
1196
- // inlined body is value-only (zero prefix), so a pure substitution is safe.
1197
- const inlineInExpr = (node, candidates) => {
1198
- if (!Array.isArray(node)) return { node, changed: false }
1199
- if (node[0] === '=>') return { node, changed: false }
1200
- let changed = false
1201
- const next = [node[0]]
1202
- for (let i = 1; i < node.length; i++) {
1203
- const r = inlineInExpr(node[i], candidates)
1204
- if (r.changed) changed = true
1205
- next.push(r.node)
1206
- }
1207
- if (isCandidateCall(next, candidates)) {
1208
- const args = callArgs(next)
1209
- const shape = args && inlinedBody(candidates.get(next[1]), args)
1210
- if (shape && shape.value !== null && shape.prefix.length === 0) {
1211
- return { node: shape.value, changed: true }
1212
- }
1213
- }
1214
- return { node: changed ? next : node, changed }
1215
- }
1216
-
1217
- const inlineInStmt = (stmt, candidates) => {
1218
- if (!Array.isArray(stmt)) return { node: stmt, changed: false }
1219
- // Statement-position call: discard return value, splice prefix in place.
1220
- if (isCandidateCall(stmt, candidates)) {
1221
- const args = callArgs(stmt)
1222
- const shape = args && inlinedBody(candidates.get(stmt[1]), args)
1223
- if (shape) return { node: ['{}', [';', ...shape.prefix]], changed: true, splice: shape.prefix }
1224
- }
1225
- // `let/const X = call(...)` with single decl: inline as prefix + decl(value).
1226
- if ((stmt[0] === 'let' || stmt[0] === 'const') && stmt.length === 2) {
1227
- const decl = stmt[1]
1228
- if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && isCandidateCall(decl[2], candidates)) {
1229
- const args = callArgs(decl[2])
1230
- const shape = args && inlinedBody(candidates.get(decl[2][1]), args)
1231
- if (shape && shape.value !== null) {
1232
- const splice = [...shape.prefix, [stmt[0], ['=', decl[1], shape.value]]]
1233
- return { node: ['{}', [';', ...splice]], changed: true, splice }
1234
- }
1235
- }
1236
- }
1237
- // `X = call(...)` at statement position: inline as prefix + assign(value).
1238
- if (stmt[0] === '=' && typeof stmt[1] === 'string' && isCandidateCall(stmt[2], candidates)) {
1239
- const args = callArgs(stmt[2])
1240
- const shape = args && inlinedBody(candidates.get(stmt[2][1]), args)
1241
- if (shape && shape.value !== null) {
1242
- const splice = [...shape.prefix, ['=', stmt[1], shape.value]]
1243
- return { node: ['{}', [';', ...splice]], changed: true, splice }
1244
- }
1245
- }
1246
- const op = stmt[0]
1247
- if (op === ';') {
1248
- let changed = false
1249
- const next = [';']
1250
- for (let i = 1; i < stmt.length; i++) {
1251
- const r = inlineInStmt(stmt[i], candidates)
1252
- changed ||= r.changed
1253
- if (r.splice) next.push(...r.splice)
1254
- else next.push(r.node)
1255
- }
1256
- return changed ? { node: next, changed } : { node: stmt, changed: false }
1257
- }
1258
- if (op === '{}') {
1259
- const r = inlineInStmt(stmt[1], candidates)
1260
- if (!r.changed) return { node: stmt, changed: false }
1261
- // If the child was itself a candidate call (or a let/assign-of-call), it
1262
- // already returned a `['{}', [';', ...prefix]]` shape. Re-wrapping here
1263
- // would yield `['{}', ['{}', …]]`, which codegen rejects ("Unknown op: {}").
1264
- if (Array.isArray(r.node) && r.node[0] === '{}') return { node: r.node, changed: true }
1265
- return { node: ['{}', r.node], changed: true }
1266
- }
1267
- if (op === 'for') {
1268
- const r = inlineInStmt(stmt[4], candidates)
1269
- return r.changed ? { node: ['for', stmt[1], stmt[2], stmt[3], r.node], changed: true } : { node: stmt, changed: false }
1270
- }
1271
- if (op === 'while') {
1272
- const r = inlineInStmt(stmt[2], candidates)
1273
- return r.changed ? { node: ['while', stmt[1], r.node], changed: true } : { node: stmt, changed: false }
1274
- }
1275
- if (op === 'if') {
1276
- const thenR = inlineInStmt(stmt[2], candidates)
1277
- const elseR = stmt.length > 3 ? inlineInStmt(stmt[3], candidates) : null
1278
- if (thenR.changed || elseR?.changed) return {
1279
- node: stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node],
1280
- changed: true,
1281
- }
1282
- }
1283
- if (op === 'try' || op === 'catch' || op === 'finally') {
1284
- let changed = false
1285
- const next = [op]
1286
- for (let i = 1; i < stmt.length; i++) {
1287
- const part = stmt[i]
1288
- const r = Array.isArray(part) ? inlineInStmt(part, candidates) : { node: part, changed: false }
1289
- changed ||= r.changed
1290
- next.push(r.node)
1291
- }
1292
- return changed ? { node: next, changed: true } : { node: stmt, changed: false }
1293
- }
1294
- return { node: stmt, changed: false }
1295
- }
1296
-
1297
- const inlineHotInternalCalls = (programFacts, ast) => {
1298
- const cfg = ctx.transform.optimize
1299
- if (cfg && cfg.sourceInline === false) return false
1300
-
1301
- const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
1302
- const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
1303
- const sitesByCallee = new Map()
1304
- for (const cs of programFacts.callSites) {
1305
- const list = sitesByCallee.get(cs.callee)
1306
- if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
1307
- }
1308
-
1309
- const containsNode = (root, needle, inLoop = false) => {
1310
- if (root === needle) return inLoop
1311
- if (!Array.isArray(root) || root[0] === '=>') return false
1312
- const nextInLoop = inLoop || LOOP_OPS.has(root[0])
1313
- for (let i = 1; i < root.length; i++) if (containsNode(root[i], needle, nextInLoop)) return true
1314
- return false
1315
- }
1316
-
1317
- const hasFixedTypedArraySites = (func, sites) => {
1318
- const params = func.sig?.params || []
1319
- if (!sites?.length) return false
1320
- return sites.every(site => params.some((p, i) => {
1321
- const arg = site.argList[i]
1322
- return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
1323
- }))
1324
- }
1325
- const hasFullyFixedTypedArraySites = (func, sites) => {
1326
- const params = func.sig?.params || []
1327
- if (!sites?.length) return false
1328
- let sawTypedArg = false
1329
- for (const site of sites) {
1330
- const typed = typedByFunc.get(site.callerFunc)
1331
- const fixed = fixedByFunc.get(site.callerFunc)
1332
- for (let i = 0; i < params.length; i++) {
1333
- const arg = site.argList[i]
1334
- if (typeof arg !== 'string' || !typed?.has(arg)) continue
1335
- sawTypedArg = true
1336
- if (!fixed?.has(arg)) return false
1337
- }
1338
- }
1339
- return sawTypedArg
1340
- }
1341
-
1342
- const candidates = new Map()
1343
- // Forwarders — a candidate whose body calls one of its own parameters.
1344
- // Inlining one replaces that parameter with the call-site argument; when the
1345
- // argument is a known function name the resulting indirect call collapses to
1346
- // a direct `call` (devirtualization).
1347
- const forwarders = new Set()
1348
- for (const func of ctx.func.list) {
1349
- if (func.exported || func.raw || !func.body || func.rest || programFacts.valueUsed.has(func.name)) continue
1350
- if (func.defaults && Object.keys(func.defaults).length) continue
1351
- const sites = sitesByCallee.get(func.name)
1352
- const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
1353
- const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
1354
- const hasLoop = scanBody(func.body, n => LOOP_OPS.has(n[0]))
1355
- const isTinyLeaf = !hasLoop && nodeSize(func.body) <= 15
1356
- if (!sites || sites.length < 1 || (!isTinyLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
1357
- const stmts = blockStmts(func.body)
1358
- // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
1359
- // return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
1360
- if (scanBody(func.body, n => n[0] === '=>')) continue
1361
- // throw/break/continue are unsupported; return is OK if it's a single
1362
- // trailing return (rewritten to a value at inlining time).
1363
- if (scanBody(func.body, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) continue
1364
- let returnCount = 0
1365
- scanBody(func.body, n => { if (n[0] === 'return') returnCount++; return false })
1366
- if (returnCount > 1) continue
1367
- if (returnCount === 1 && stmts) {
1368
- const last = stmts[stmts.length - 1]
1369
- if (!Array.isArray(last) || last[0] !== 'return') continue
1370
- }
1371
- // Either a kernel (has a loop) or a tiny leaf (no loop, no calls, small body).
1372
- // The leaf branch catches helpers like `isAlpha(c) => (c>=65 && c<=90) || …`
1373
- // that get hammered from a hot caller's loop — replacing the call with its
1374
- // body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
1375
- if (!hasLoop) {
1376
- if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]))) continue
1377
- if (nodeSize(func.body) > 30) continue
1378
- }
1379
- if (scanBody(func.body, n => n[0] === '()' && n[1] === func.name)) continue
1380
- // Kernels with nested loops (depth ≥ 2) are typically large and the inner
1381
- // loop carries most of the cost. Inlining them into a host that V8 can't
1382
- // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
1383
- // Keep them as standalone functions so V8 wasm tier-up can warm them.
1384
- if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
1385
- // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
1386
- // object/array literals returned) break downstream pointer-ABI specialization
1387
- // when inlined: narrow.js can't trace the post-inline alias chain back to a
1388
- // single ctor, so the typed-array param of a callee like processCascade(x, …)
1389
- // stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
1390
- // Keeping the factory as a callable function preserves the call-site type fact.
1391
- if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
1392
- const paramNames = new Set((func.sig?.params || []).map(p => p.name))
1393
- if (paramNames.size && scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && paramNames.has(n[1])))
1394
- forwarders.add(func.name)
1395
- candidates.set(func.name, func)
1396
- }
1397
- if (!candidates.size) return false
1398
-
1399
- // Trivial expr-bodied candidates can be substituted at any expression position
1400
- // (if-condition, ternary, etc.). Stmt-bodied ones go through inlineInStmt's
1401
- // statement-level path which preserves prefix ordering.
1402
- const exprOnlyCandidates = new Map()
1403
- for (const [name, func] of candidates) {
1404
- if (!Array.isArray(func.body) || func.body[0] !== '{}') exprOnlyCandidates.set(name, func)
1405
- }
1406
-
1407
- let changed = false
1408
- const exportedCandidates = new Map()
1409
- for (const [name, func] of candidates) {
1410
- const sites = sitesByCallee.get(name)
1411
- const fixedSiteExported = hasFixedTypedArraySites(func, sites) &&
1412
- !sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))
1413
- // Forwarders cross into an exported caller too: the tier-up rationale that
1414
- // keeps candidates out of exports concerns relocated loop kernels, not
1415
- // these tiny leaves — and inlining one devirtualizes a closure dispatch.
1416
- if (fixedSiteExported || forwarders.has(name)) exportedCandidates.set(name, func)
1417
- }
1418
- for (const func of ctx.func.list) {
1419
- if (!func.body || func.raw) continue
1420
- // Skip exports: they're entry points usually invoked once. Inlining a
1421
- // hot kernel here would put the loop into a function V8's wasm tier-up
1422
- // never warms (kernel stays in baseline). Keeping the kernel as its own
1423
- // callable function lets V8 promote it to TurboFan after a few calls.
1424
- // Exception: fixed-size typed-array callees should inline into the exported
1425
- // caller so scalar replacement can cross the call boundary and remove the
1426
- // caller's heap arrays.
1427
- const activeCandidates = func.exported ? exportedCandidates : candidates
1428
- if (func.exported && !activeCandidates.size) continue
1429
- // Expression-bodied arrows (`() => expr`) have func.body as the return
1430
- // value itself — never a `{}` block. inlineInStmt treats its argument as a
1431
- // statement (discards the return value of any top-level candidate call),
1432
- // which would turn `() => x()` into an empty block and lose the result.
1433
- // Route those through inlineInExpr so the call is replaced by the inlined
1434
- // value expression instead.
1435
- const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
1436
- const r = isExprBody
1437
- ? inlineInExpr(func.body, activeCandidates)
1438
- : inlineInStmt(func.body, activeCandidates)
1439
- let body = r.changed ? r.node : func.body
1440
- let bodyChanged = r.changed
1441
- if (!func.exported && exprOnlyCandidates.size) {
1442
- const e = inlineInExpr(body, exprOnlyCandidates)
1443
- if (e.changed) { body = e.node; bodyChanged = true }
1444
- }
1445
- if (bodyChanged) { func.body = body; changed = true }
1446
- }
1447
- if (ast) {
1448
- const r = inlineInStmt(ast, candidates)
1449
- if (r.changed) changed = true
1450
- }
1451
- return changed
1452
- }
1453
-
1454
- // === Inline non-escaping local lambdas ===
1455
- // `const f = (a) => …; … f(x) …` → the lambda body substituted at each call
1456
- // site. A non-escaping lambda's captured free vars are still in lexical scope at
1457
- // the call site, so splicing the body in place preserves capture-by-reference
1458
- // semantics while eliminating the closure object (no env pointer, no NaN-box, no
1459
- // call_indirect). Mirrors inlineHotInternalCalls, scoped to one function body.
1460
-
1461
- // True iff `name` appears textually anywhere in `node` (descending into nested
1462
- // arrows; `.prop` / `:key` positions are literal names, not refs — skipped to
1463
- // match cloneWithSubst's structure).
1464
- const referencesName = (node, name) => {
1465
- if (typeof node === 'string') return node === name
1466
- if (!Array.isArray(node)) return false
1467
- const op = node[0]
1468
- if (op === 'str') return false
1469
- if (op === '.' || op === '?.') return referencesName(node[1], name)
1470
- if (op === ':') return referencesName(node[2], name)
1471
- for (let i = 1; i < node.length; i++) if (referencesName(node[i], name)) return true
1472
- return false
1473
- }
1474
-
1475
- // True iff every textual reference to `name` in `node` is the callee of a
1476
- // `name(...)` call (i.e. the binding never escapes — never read as a value,
1477
- // reassigned, captured by a nested lambda, or shadowed).
1478
- const onlyCalledNotReferenced = (node, name) => {
1479
- if (typeof node === 'string') return node !== name
1480
- if (!Array.isArray(node)) return true
1481
- const op = node[0]
1482
- if (op === 'str') return true
1483
- // A nested lambda touching `name` at all (capture or shadowing param) → bail.
1484
- if (op === '=>') return !referencesName(node[1], name) && !referencesName(node[2], name)
1485
- if (op === '()' && node[1] === name) {
1486
- for (let i = 2; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
1487
- return true
1488
- }
1489
- if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
1490
- if (op === ':') return onlyCalledNotReferenced(node[2], name)
1491
- for (let i = 1; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
1492
- return true
1493
- }
1494
-
1495
- const bodyStmtList = body =>
1496
- Array.isArray(body) && body[0] === '{}' ? blockStmts(body)
1497
- : Array.isArray(body) && body[0] === ';' ? body.slice(1)
1498
- : body == null ? [] : [body]
1499
-
1500
- const removeStmts = (body, set) => {
1501
- if (!Array.isArray(body)) return set.has(body) ? null : body
1502
- if (body[0] === '{}') return ['{}', removeStmts(body[1], set) ?? [';']]
1503
- if (body[0] === ';') {
1504
- const kept = body.slice(1).filter(s => !set.has(s))
1505
- return kept.length === 0 ? null : kept.length === 1 ? kept[0] : [';', ...kept]
1506
- }
1507
- return set.has(body) ? null : body
1508
- }
1509
-
1510
- // Lambda body must be a guaranteed-return shape inlinedBody can splice: ≤1
1511
- // `return` (trailing, if a block), no throw/break/continue, no param mutation,
1512
- // no nested lambda.
1513
- const inlinableLambdaBody = (abody, params) => {
1514
- if (scanBody(abody, n => n[0] === '=>')) return false
1515
- if (scanBody(abody, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) return false
1516
- let returns = 0
1517
- scanBody(abody, n => { if (n[0] === 'return') returns++; return false })
1518
- if (returns > 1) return false
1519
- if (returns === 1) {
1520
- const stmts = blockStmts(abody)
1521
- if (!stmts || !stmts.length) return false
1522
- const last = stmts[stmts.length - 1]
1523
- if (!Array.isArray(last) || last[0] !== 'return') return false
1524
- }
1525
- return !mutatesAny(abody, new Set(params))
1526
- }
1527
-
1528
- const inlineLocalLambdasInBody = (getBody, setBody) => {
1529
- const body = getBody()
1530
- const stmts = bodyStmtList(body)
1531
- if (stmts.length < 2) return false
1532
-
1533
- // Collect `const f = ARROW` (single-decl), all-plain params, inlinable body.
1534
- const decls = new Map()
1535
- for (const stmt of stmts) {
1536
- if (!Array.isArray(stmt) || stmt[0] !== 'const' || stmt.length !== 2) continue
1537
- const d = stmt[1]
1538
- if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
1539
- const arrow = d[2]
1540
- if (!Array.isArray(arrow) || arrow[0] !== '=>') continue
1541
- const params = extractParams(arrow[1])
1542
- if (!params.every(p => typeof p === 'string')) continue
1543
- if (!inlinableLambdaBody(arrow[2], params)) continue
1544
- decls.set(d[1], { stmt, arrow, params })
1545
- }
1546
- if (!decls.size) return false
1547
-
1548
- // Drop any candidate whose body references another (or its own) candidate —
1549
- // single-level inlining can't resolve such chains, and a still-referenced
1550
- // candidate's decl can't be removed.
1551
- for (let changed = true; changed;) {
1552
- changed = false
1553
- for (const [name, info] of decls) {
1554
- if ([...decls.keys()].some(c => referencesName(info.arrow[2], c))) { decls.delete(name); changed = true }
1555
- }
1556
- }
1557
- // Every other reference to the name must be a `name(...)` call.
1558
- for (const [name, info] of [...decls]) {
1559
- if (!stmts.every(s => s === info.stmt || onlyCalledNotReferenced(s, name))) decls.delete(name)
1560
- }
1561
- if (!decls.size) return false
1562
-
1563
- const asFunc = info => ({ sig: { params: info.params.map(name => ({ name })) }, body: info.arrow[2] })
1564
- const stmtCands = new Map(), exprCands = new Map()
1565
- for (const [name, info] of decls)
1566
- (Array.isArray(info.arrow[2]) && info.arrow[2][0] === '{}' ? stmtCands : exprCands).set(name, asFunc(info))
1567
-
1568
- let out = body, didChange = false
1569
- if (stmtCands.size) { const r = inlineInStmt(out, stmtCands); if (r.changed) { out = r.node; didChange = true } }
1570
- if (exprCands.size) { const r = inlineInExpr(out, exprCands); if (r.changed) { out = r.node; didChange = true } }
1571
- if (!didChange) return false
1572
-
1573
- // Remove decls of candidates that are now fully consumed.
1574
- const newStmts = bodyStmtList(out)
1575
- const dead = new Set()
1576
- for (const [name, info] of decls) {
1577
- if (!newStmts.some(s => s !== info.stmt && referencesName(s, name))) dead.add(info.stmt)
1578
- }
1579
- if (dead.size) out = removeStmts(out, dead) ?? [';']
1580
-
1581
- setBody(out)
1582
- return true
1583
- }
1584
-
1585
- const inlineLocalLambdas = () => {
1586
- let changed = false
1587
- for (const func of ctx.func.list) {
1588
- if (!func.body || func.raw) continue
1589
- if (inlineLocalLambdasInBody(() => func.body, b => { func.body = b })) changed = true
1590
- }
1591
- return changed
1592
- }
1593
-
1594
- const restIndexExpr = (idx, restParams) => {
1595
- const k = intLit(idx)
1596
- if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
1597
-
1598
- let out = [, undefined]
1599
- for (let i = restParams.length - 1; i >= 0; i--) {
1600
- out = ['?:', ['==', clonePlain(idx), [, i]], restParams[i], out]
1601
- }
1602
- return out
1603
- }
1604
-
1605
- const rewriteRestBody = (node, restName, restParams) => {
1606
- if (typeof node === 'string') return node === restName ? { ok: false } : { ok: true, node }
1607
- if (!Array.isArray(node)) return { ok: true, node }
1608
- if (node[0] === 'str') return { ok: true, node: node.slice() }
1609
-
1610
- if ((node[0] === '.' || node[0] === '?.') && node[1] === restName) {
1611
- return node[2] === 'length' ? { ok: true, node: [, restParams.length] } : { ok: false }
1612
- }
1613
-
1614
- if (node[0] === '[]' && node[1] === restName) {
1615
- if (!isSimpleArg(node[2])) return { ok: false }
1616
- return { ok: true, node: restIndexExpr(node[2], restParams) }
1617
- }
1618
-
1619
- const out = [node[0]]
1620
- for (let i = 1; i < node.length; i++) {
1621
- const r = rewriteRestBody(node[i], restName, restParams)
1622
- if (!r.ok) return r
1623
- out.push(r.node)
1624
- }
1625
- return { ok: true, node: out }
1626
- }
1627
-
1628
- const specializeFixedRestCalls = (programFacts) => {
1629
- const sitesByKey = new Map()
1630
- for (const site of programFacts.callSites) {
1631
- const func = ctx.func.map.get(site.callee)
1632
- if (!func?.rest || func.exported || func.raw || !func.body) continue
1633
- if (programFacts.valueUsed.has(func.name)) continue
1634
- if (func.defaults && Object.keys(func.defaults).length) continue
1635
- if (site.argList.some(a => Array.isArray(a) && a[0] === '...')) continue
1636
-
1637
- const fixedN = func.sig.params.length - 1
1638
- const restN = Math.max(0, site.argList.length - fixedN)
1639
- const key = `${func.name}/${restN}`
1640
- const list = sitesByKey.get(key)
1641
- if (list) list.push(site); else sitesByKey.set(key, [site])
1642
- }
1643
-
1644
- let changed = false
1645
- for (const [key, sites] of sitesByKey) {
1646
- const [name, restNText] = key.split('/')
1647
- const func = ctx.func.map.get(name)
1648
- const restN = Number(restNText)
1649
- const fixedParams = func.sig.params.slice(0, -1).map(p => ({ ...p }))
1650
- const restName = func.rest
1651
- const restParams = Array.from({ length: restN }, (_, i) => `${restName}${T}r${restN}_${i}`)
1652
- const rewritten = rewriteRestBody(func.body, restName, restParams)
1653
- if (!rewritten.ok) continue
1654
-
1655
- const cloneName = `${name}${T}rest${restN}`
1656
- if (!ctx.func.map.has(cloneName)) {
1657
- const restSigParams = restParams.map(name => ({ name, type: 'f64' }))
1658
- const clone = {
1659
- ...func,
1660
- name: cloneName,
1661
- exported: false,
1662
- rest: null,
1663
- sig: {
1664
- ...func.sig,
1665
- params: [...fixedParams, ...restSigParams],
1666
- results: [...func.sig.results],
1667
- },
1668
- body: rewritten.node,
1669
- }
1670
- delete clone.defaults
1671
- ctx.func.list.push(clone)
1672
- ctx.func.names.add(cloneName)
1673
- ctx.func.map.set(cloneName, clone)
1674
- }
1675
-
1676
- const fixedN = func.sig.params.length - 1
1677
- for (const site of sites) {
1678
- site.node[1] = cloneName
1679
- setCallArgs(site.node, site.argList.slice(0, fixedN + restN))
1680
- changed = true
1681
- }
1682
- }
1683
- return changed
1684
- }
1685
-
1686
- // `scanGlobalValueFacts` was deleted — prepare's depth-0 catch (calling
1687
- // `recordGlobalRep` from src/infer.js) is the authoritative pass and a
1688
- // strict superset of what this top-level walker observed.
1689
-
1690
- // Flow-insensitive type inference for module-level `let` bindings whose
1691
- // initial RHS doesn't pin a type (most often `let mem;` followed later by
1692
- // `mem = new TypedArray(...)` inside an init function). Without this the
1693
- // read site has to runtime-check the NaN-box tag on every access — game-of-life's
1694
- // inner step does that 9× per cell, blowing up the hot loop. We union RHS types
1695
- // across every assignment (initial decl + every `name = …` in any function);
1696
- // if every observed RHS is either a typed-array ctor of the same kind, a known
1697
- // VAL.TYPED binding of the same ctor, or null/undefined, the binding is
1698
- // monomorphically VAL.TYPED. Anything else (literal number, non-typed call,
1699
- // mixed ctors) clears the candidacy, keeping the read site polymorphic.
1700
- const inferModuleLetTypes = (ast) => {
1701
- if (!ctx.scope.userGlobals) return
1702
- // candidates: name → { ctor: string|null, valid: true } | { valid: false }
1703
- // valid=true with ctor=null means "still no positive evidence"; we promote
1704
- // only when ctor is non-null at the end. Assignments to nullish (undef/null)
1705
- // don't change ctor — they're consistent with any typed-array value.
1706
- const seen = new Map()
1707
- for (const name of ctx.scope.userGlobals) seen.set(name, { ctor: null, valid: true })
1708
-
1709
- const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
1710
- || (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
1711
-
1712
- const observe = (name, rhs) => {
1713
- const c = seen.get(name)
1714
- if (!c || !c.valid) return
1715
- if (isNullishLit(rhs)) return
1716
- // Resolve typed-array ctor from `new TypedArrayCtor(...)`, ternary of typed,
1717
- // or a reference to a name we already know is typed.
1718
- let ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
1719
- if (ctor === MIXED_CTORS) { c.valid = false; return }
1720
- if (!ctor && typeof rhs === 'string') {
1721
- if (ctx.scope.globalValTypes?.get(rhs) === VAL.TYPED)
1722
- ctor = ctx.scope.globalTypedElem?.get(rhs) ?? null
1723
- }
1724
- if (!ctor) { c.valid = false; return }
1725
- if (c.ctor && c.ctor !== ctor) { c.valid = false; return }
1726
- c.ctor = ctor
1727
- }
1728
-
1729
- const walk = (node) => {
1730
- if (!Array.isArray(node)) return
1731
- const op = node[0]
1732
- if (op === '=' && typeof node[1] === 'string' && seen.has(node[1])) observe(node[1], node[2])
1733
- if ((op === 'let' || op === 'const') && node.length > 1) {
1734
- for (let i = 1; i < node.length; i++) {
1735
- const d = node[i]
1736
- if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' && seen.has(d[1]))
1737
- observe(d[1], d[2])
1738
- }
1739
- }
1740
- // Compound-assigns (`+=`, etc.) to a typed-array binding can't preserve
1741
- // the typed-array kind — invalidate.
1742
- if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && seen.has(node[1])) {
1743
- const c = seen.get(node[1])
1744
- if (c) c.valid = false
1745
- }
1746
- for (let i = 1; i < node.length; i++) walk(node[i])
1747
- }
1748
- walk(ast)
1749
- for (const f of ctx.func.list) if (f.body && !f.raw) walk(f.body)
1750
-
1751
- for (const [name, c] of seen) {
1752
- if (!c.valid || !c.ctor) continue
1753
- if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
1754
- ;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
1755
- ;(ctx.scope.globalTypedElem ||= new Map()).set(name, c.ctor)
1756
- }
1757
- }
1758
-
1759
- // `MIXED_CTORS` and `ternaryCtorOfRhs` are not exported from analyze — re-derive
1760
- // the sentinel locally and look up the ctor through `typedElemCtor` alone.
1761
- // `ternaryCtorOfRhs` is purely for ternary RHSs (e.g. `cond ? new Int32Array(N) : new Int32Array(M)`),
1762
- // which game-of-life-style sources don't use; falling back to typedElemCtor is fine.
1763
- const MIXED_CTORS = Symbol('MIXED_CTORS')
1764
- const ternaryCtorOfRhs = () => null
1765
-
1766
- const unboxConstTypedGlobals = () => {
1767
- if (!ctx.scope.globalTypedElem || !ctx.scope.consts) return
1768
- for (const [name, ctor] of ctx.scope.globalTypedElem) {
1769
- if (!ctx.scope.consts.has(name)) continue
1770
- if (ctx.scope.globalValTypes?.get(name) !== VAL.TYPED) continue
1771
- const aux = typedElemAux(ctor)
1772
- if (aux == null) continue
1773
- const decl = ctx.scope.globals.get(name)
1774
- if (typeof decl !== 'string' || !decl.includes('mut f64')) continue
1775
- ctx.scope.globals.set(name, `(global $${name} (mut i32) (i32.const 0))`)
1776
- ctx.scope.globalTypes.set(name, 'i32')
1777
- updateGlobalRep(name, { ptrKind: VAL.TYPED, ptrAux: aux })
1778
- }
1779
- }
1780
-
1781
- /**
1782
- * Function-namespace scalar replacement + devirtualization.
1783
- *
1784
- * A property of a user function compiles, by default, as a dynamic object: each
1785
- * `f.prop` write is a `__dyn_set` into a closure-keyed hash side-table, each
1786
- * read a `__dyn_get`. But a function's property table can never be observed by
1787
- * the host (the host receives only the callable; the table lives in jz linear
1788
- * memory), so jz sees every `f.prop` site — the slot is a closed, fully-known
1789
- * cell. Per property of a non-escaping namespace:
1790
- *
1791
- * - reassigned (`multiProp`) slot → dissolve into a plain f64 module global:
1792
- * `__dyn_get/__dyn_set` → `global.get/global.set`. The indirect call stays
1793
- * (a genuinely reassigned function pointer needs `call_indirect`). Pure
1794
- * storage relocation: the global inits to `UNDEF_WAT`, exactly mirroring
1795
- * "key never set → __dyn_get yields undefined".
1796
- * - written once to its lifted `$f$prop` function and only ever *called*
1797
- * (never read as a value) → the `__dyn_set` is dead: emit already lowers
1798
- * `f.prop()` to a direct `call $f$prop`. Drop the write entirely.
1799
- *
1800
- * Disqualified namespaces (`f` escapes as a bare value / is computed-indexed —
1801
- * an alias could reach the table) keep the dynamic path. Together these can
1802
- * eliminate the `__dyn_*` machinery from a namespace-only program outright.
1803
- */
1804
- const flattenFuncNamespaces = (ast) => {
1805
- const names = ctx.func.names
1806
- if (!names?.size) return false
1807
- // Cheap structural gate: a flattenable namespace exists only if some lifted
1808
- // `f$prop` name's `f` is itself a function (prepare lifts every `f.prop =
1809
- // arrow` — multiProp slots included). The base `f` may itself carry a module
1810
- // prefix (`mod$f`), so scan every `$` boundary, not just the first; a
1811
- // populated `multiProp` registry is itself a direct namespace witness.
1812
- let hasNs = ctx.func.multiProp.size > 0
1813
- if (!hasNs) outer: for (const n of names) {
1814
- for (let i = n.indexOf('$'); i > 0; i = n.indexOf('$', i + 1))
1815
- if (names.has(n.slice(0, i))) { hasNs = true; break outer }
1816
- }
1817
- if (!hasNs) return false
1818
- const ns = analyzeFuncNamespaces(ast)
1819
- if (!ns.size) return false
1820
- // f → Map<prop, decision>; decision is { global } (SROA) or { drop } (dead
1821
- // write to an only-called single-write slot).
1822
- const flat = new Map()
1823
- for (const [f, info] of ns) {
1824
- if (info.disq) continue
1825
- let decide
1826
- const plan = (prop, d) => { if (!decide) flat.set(f, decide = new Map()); decide.set(prop, d) }
1827
- for (const prop of info.props) {
1828
- if (ctx.func.multiProp.has(`${f}.${prop}`)) { plan(prop, { global: `${f}${T}${prop}` }); continue }
1829
- const w = info.writes.get(prop)
1830
- // Single write of the lifted `$f$prop`, never read as a value → drop it.
1831
- if (w && w.length === 1 && w[0].atInit && w[0].rhs === `${f}$${prop}` && !info.valRead.has(prop))
1832
- plan(prop, { drop: true })
1833
- }
1834
- }
1835
- if (!flat.size) return false
1836
- for (const decide of flat.values())
1837
- for (const d of decide.values())
1838
- if (d.global && !ctx.scope.globals.has(d.global)) {
1839
- ctx.scope.globals.set(d.global, `(global $${d.global} (mut f64) ${UNDEF_WAT})`)
1840
- ctx.scope.globalTypes.set(d.global, 'f64')
1841
- }
1842
- const decisionFor = (obj, prop) =>
1843
- typeof obj === 'string' && typeof prop === 'string' && flat.has(obj)
1844
- ? flat.get(obj).get(prop) : undefined
1845
- const isEmptySeq = (n) => Array.isArray(n) && n.length === 1 && n[0] === ';'
1846
- const rewrite = (node) => {
1847
- if (!Array.isArray(node)) return node
1848
- const op = node[0]
1849
- if (op === '.' || op === '?.') {
1850
- const d = decisionFor(node[1], node[2])
1851
- if (d?.global) return d.global // drop-decisions leave reads/calls alone
1852
- }
1853
- if (op === '=' && Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.')) {
1854
- const d = decisionFor(node[1][1], node[1][2])
1855
- if (d?.global) return ['=', d.global, rewrite(node[2])]
1856
- if (d?.drop) return [';'] // dead write — emit nothing
1857
- }
1858
- const out = [op]
1859
- // Filter dropped writes out of statement sequences (an empty `[';']` left in
1860
- // a body would lower to an unrenderable node).
1861
- for (let i = 1; i < node.length; i++) {
1862
- const c = rewrite(node[i])
1863
- if (op === ';' && isEmptySeq(c)) continue
1864
- out.push(c)
1865
- }
1866
- return out
1867
- }
1868
- const newAst = rewrite(ast)
1869
- ast.length = 0
1870
- for (let i = 0; i < newAst.length; i++) ast.push(newAst[i])
1871
- for (const fn of ctx.func.list)
1872
- if (fn.body && !fn.raw) fn.body = rewrite(fn.body)
1873
- // The defining `f.prop = …` writes live in moduleInits for bundled programs —
1874
- // rewrite them too, or reads would resolve to an unwritten global.
1875
- if (ctx.module.moduleInits)
1876
- for (let i = 0; i < ctx.module.moduleInits.length; i++)
1877
- ctx.module.moduleInits[i] = rewrite(ctx.module.moduleInits[i])
1878
- return true
1879
- }
1880
-
1881
- /**
1882
- * Closure devirtualization.
1883
- *
1884
- * `flattenFuncNamespaces` dissolves a reassigned `f.prop` function slot into a
1885
- * module global, but the call through it stays a `call_indirect` on a
1886
- * `global.get`, dispatched via an ABI-adapting trampoline. When that global is
1887
- * written *only* by unconditional module-init assignments it holds, for the
1888
- * entire post-init program, one statically-known function — so every call
1889
- * through it collapses to a direct `call`: no table lookup, no trampoline, no
1890
- * 8-wide padding ABI, no closure type guard.
1891
- *
1892
- * A global G qualifies iff:
1893
- * 1. every assignment to G is an unconditional module-init statement — none in
1894
- * a function body, none nested inside init control flow;
1895
- * 2. G's final init value resolves (through global aliases) to a top-level
1896
- * function F;
1897
- * 3. G is never *called* by module-init code, nor by any function reachable
1898
- * from it — so every call site runs strictly post-init, where G ≡ F.
1899
- * Devirt then only swaps an indirect call for a direct call to the very same
1900
- * callee: it cannot change behavior, only drop dispatch overhead. The result is
1901
- * recorded in `ctx.func.globalDevirt` (`Map<global, fn>`) and consumed by emit.
1902
- */
1903
- const devirtGlobalCalls = (ast) => {
1904
- const fnNames = ctx.func.names
1905
- if (!fnNames?.size || !ctx.scope.globals?.size) return
1906
-
1907
- // Module-init statement stream, in execution order: moduleInits run first in
1908
- // `$__start`, then the main module's top-level.
1909
- const initStmts = []
1910
- const flatten = (n) => {
1911
- if (Array.isArray(n) && n[0] === ';') for (let i = 1; i < n.length; i++) flatten(n[i])
1912
- else if (n != null) initStmts.push(n)
1913
- }
1914
- for (const mi of ctx.module.moduleInits || []) flatten(mi)
1915
- flatten(ast)
1916
-
1917
- const isGlobal = (s) => typeof s === 'string' && ctx.scope.globals.has(s)
1918
- // `[target, rhs]` pairs for a `=` / `let` / `const` node assigning a global.
1919
- const writesOf = (node) => {
1920
- if (!Array.isArray(node)) return []
1921
- if (node[0] === '=' && isGlobal(node[1])) return [[node[1], node[2]]]
1922
- if (node[0] === 'let' || node[0] === 'const') {
1923
- const out = []
1924
- for (let i = 1; i < node.length; i++) {
1925
- const d = node[i]
1926
- if (Array.isArray(d) && d[0] === '=' && isGlobal(d[1])) out.push([d[1], d[2]])
1927
- }
1928
- return out
1929
- }
1930
- return []
1931
- }
1932
-
1933
- // Poison a global assigned anywhere but an unconditional init statement — in a
1934
- // function body, or nested in init control flow. Its value is then not a
1935
- // fixed post-init constant.
1936
- const poison = new Set()
1937
- const scanWrites = (node, topInit) => {
1938
- if (!Array.isArray(node)) return
1939
- const op = node[0]
1940
- if (op === 'let' || op === 'const') {
1941
- // A declarator `=` is part of the declaration, not a nested assignment —
1942
- // poison only when the declaration itself is non-top-level.
1943
- for (let i = 1; i < node.length; i++) {
1944
- const d = node[i]
1945
- if (Array.isArray(d) && d[0] === '=') {
1946
- if (!topInit && isGlobal(d[1])) poison.add(d[1])
1947
- scanWrites(d[2], false)
1948
- } else scanWrites(d, false)
1949
- }
1950
- return
1951
- }
1952
- if (op === '=') {
1953
- if (!topInit && isGlobal(node[1])) poison.add(node[1])
1954
- scanWrites(node[1], false)
1955
- scanWrites(node[2], false)
1956
- return
1957
- }
1958
- for (let i = 1; i < node.length; i++) scanWrites(node[i], false)
1959
- }
1960
- for (const stmt of initStmts) scanWrites(stmt, true)
1961
- for (const fn of ctx.func.map.values())
1962
- if (fn.body && !fn.raw) scanWrites(fn.body, false)
1963
-
1964
- // Resolve each global's value by a linear pass over init in execution order.
1965
- const env = new Map()
1966
- const evalFn = (rhs) =>
1967
- typeof rhs !== 'string' ? null
1968
- : fnNames.has(rhs) ? rhs
1969
- : env.has(rhs) ? env.get(rhs)
1970
- : null
1971
- for (const stmt of initStmts)
1972
- for (const [g, rhs] of writesOf(stmt)) env.set(g, evalFn(rhs))
1973
-
1974
- const devirt = new Map()
1975
- for (const [g, fn] of env)
1976
- if (fn && fnNames.has(fn) && !poison.has(g)) devirt.set(g, fn)
1977
- if (!devirt.size) return
1978
-
1979
- // Condition 3: a call through G that runs *during* init would see an
1980
- // intermediate value. Drop any candidate G called by init code, or by a
1981
- // function reachable from it.
1982
- //
1983
- // `walkStraightLine` follows only straight-line execution: a nested `=>`
1984
- // literal is a closure *constructed* here, not run here, so its body is
1985
- // skipped — an IIFE callee `(=> …)()` is the one exception, its body does
1986
- // run. This is what keeps operator-registration init (`binary('+', 11)`
1987
- // builds, but does not invoke, a parselet closure) from dragging the parser
1988
- // into the init-reachable set, and keeps a wrapper body's `space()` call —
1989
- // which fires at parse time — from counting as an init call. (Soundness
1990
- // rests on a closure constructed during init not also being invoked during
1991
- // init: true of function-slot wrappers, which are registered then called at
1992
- // use time.)
1993
- const walkStraightLine = (node, onCall) => {
1994
- if (!Array.isArray(node)) return
1995
- const op = node[0]
1996
- if (op === '()') {
1997
- onCall(node[1])
1998
- if (Array.isArray(node[1]) && node[1][0] === '=>') walkStraightLine(node[1][2], onCall)
1999
- for (let i = 2; i < node.length; i++) walkStraightLine(node[i], onCall)
2000
- return
2001
- }
2002
- if (op === '=>' || op === 'function') return
2003
- for (let i = 1; i < node.length; i++) walkStraightLine(node[i], onCall)
2004
- }
2005
- const reachable = new Set()
2006
- const queue = []
2007
- const seedCalls = (node) => walkStraightLine(node, (c) => {
2008
- if (typeof c === 'string' && fnNames.has(c)) queue.push(c)
2009
- })
2010
- for (const s of initStmts) seedCalls(s)
2011
- while (queue.length) {
2012
- const f = queue.pop()
2013
- if (reachable.has(f)) continue
2014
- reachable.add(f)
2015
- const fn = ctx.func.map.get(f)
2016
- if (fn?.body && !fn.raw) seedCalls(fn.body)
2017
- }
2018
- const calledInInit = new Set()
2019
- const collectCalled = (node) => walkStraightLine(node, (c) => {
2020
- if (devirt.has(c)) calledInInit.add(c)
2021
- })
2022
- for (const s of initStmts) collectCalled(s)
2023
- for (const f of reachable) { const fn = ctx.func.map.get(f); if (fn?.body) collectCalled(fn.body) }
2024
- for (const g of calledInInit) devirt.delete(g)
2025
-
2026
- if (devirt.size) ctx.func.globalDevirt = devirt
2027
- }
2028
-
2029
- const materializeAutoBoxSchemas = (programFacts) => {
2030
- if (!ctx.schema.register) return
2031
- for (const [name, props] of programFacts.propMap) {
2032
- if (ctx.schema.vars.has(name)) {
2033
- const existing = ctx.schema.resolve(name)
2034
- const newProps = [...props].filter(prop => !existing.includes(prop))
2035
- if (newProps.length) {
2036
- const merged = [...existing, ...newProps]
2037
- const mergedId = ctx.schema.register(merged)
2038
- ctx.schema.vars.set(name, mergedId)
2039
- }
2040
- continue
2041
- }
2042
- const valueProps = [...props].filter(prop => !ctx.func.names.has(`${name}$${prop}`))
2043
- if (!valueProps.length) continue
2044
- const allProps = [...props]
2045
- const schema = ['__inner__', ...allProps]
2046
- const schemaId = ctx.schema.register(schema)
2047
- ctx.schema.vars.set(name, schemaId)
2048
- if (ctx.func.names.has(name) && !ctx.scope.globals.has(name))
2049
- ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
2050
- if (!ctx.schema.autoBox) ctx.schema.autoBox = new Map()
2051
- ctx.schema.autoBox.set(name, { schemaId, schema })
2052
- }
2053
- }
2054
-
2055
- const resolveClosureWidth = (programFacts) => {
2056
- if (!ctx.closure.make) return
2057
- const { hasSpread, hasRest, maxCall, maxDef, valueUsed } = programFacts
2058
- const floor = ctx.closure.floor ?? 0
2059
- // A top-level function used as a first-class value gets a boundary trampoline
2060
- // that forwards $__a0..$__a{arity-1} into it (emit.js). The uniform closure
2061
- // ABI must therefore be at least as wide as any table-resident function's
2062
- // fixed arity — maxDef only counts surviving `=>` literals, so lifted/hoisted
2063
- // function definitions slip past it (their bodies are walked, their param
2064
- // lists aren't). Without this, e.g. an arity-3 function used only via a
2065
- // 1-arg indirect call emits `(local.get $__a2)` against a 2-param trampoline.
2066
- let maxValueArity = 0
2067
- if (valueUsed) for (const name of valueUsed) {
2068
- const n = ctx.func.map.get(name)?.sig?.params?.length ?? 0
2069
- if (n > maxValueArity) maxValueArity = n
2070
- }
2071
- ctx.closure.width = (hasSpread && hasRest)
2072
- ? MAX_CLOSURE_ARITY
2073
- : Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), maxValueArity, floor))
2074
- }
2075
-
2076
- const canSkipWholeProgramNarrowing = (programFacts) =>
2077
- programFacts.callSites.length === 0 &&
2078
- programFacts.valueUsed.size === 0 &&
2079
- !programFacts.anyDyn &&
2080
- programFacts.propMap.size === 0 &&
2081
- !programFacts.hasSchemaLiterals &&
2082
- !ctx.closure.make
2083
-
2084
- export default function plan(ast) {
2085
- inferModuleLetTypes(ast)
2086
- unboxConstTypedGlobals()
2087
-
2088
- let programFacts = collectProgramFacts(ast)
2089
- // Function-namespace SROA — dissolve reassigned `f.prop` slots into module
2090
- // globals before inlining/narrowing, so all downstream passes see plain
2091
- // globals instead of the dynamic property machinery.
2092
- if (flattenFuncNamespaces(ast)) programFacts = collectProgramFacts(ast)
2093
- // Devirtualize calls through init-constant function globals (closure
2094
- // devirtualization) — must follow the SROA above, which creates the globals.
2095
- devirtGlobalCalls(ast)
2096
- // The call-inlining family (`inlineHotInternalCalls` self-gates on `sourceInline`)
2097
- // is a pure speed optimization — the un-inlined calls emit correctly. Scalar
2098
- // replacement (`scalarize*`) is *not* gated on `sourceInline`: callers turn it on
2099
- // independently via `optimize: { sourceInline: false }` to test heap elision alone.
2100
- if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
2101
- if (inlineLocalLambdas()) programFacts = collectProgramFacts(ast)
2102
- if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
2103
- if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
2104
- if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
2105
- // Promotion runs AFTER literal scalarization (those that fully reduce to scalars
2106
- // are gone) and BEFORE typed-array scalarization (so a freshly-promoted array's
2107
- // fixed-length-typed-of-known-size variant could still participate in loop
2108
- // unrolling — currently it can't, since promotion produces the `[...]`-arg
2109
- // form rather than `new Int32Array(N)`, but the ordering keeps the door open).
2110
- if (promoteIntArrayLiterals()) programFacts = collectProgramFacts(ast)
2111
- if (scalarizeFunctionTypedArrays(programFacts)) programFacts = collectProgramFacts(ast)
2112
- ctx.types.dynKeyVars = programFacts.dynVars
2113
- ctx.types.anyDynKey = programFacts.anyDyn
2114
-
2115
- materializeAutoBoxSchemas(programFacts)
2116
- resolveClosureWidth(programFacts)
2117
- if (canSkipWholeProgramNarrowing(programFacts)) {
2118
- // Phase J (jsstring boundary opt-in) is body-local and call-site-independent;
2119
- // run it even when the rest of narrowing is skipped so simple `export let
2120
- // f = (s) => s.length` still flips to externref. Likewise the boolean-result
2121
- // fact, so `export let f = (a) => a > 2` boxes its boundary atom.
2122
- applyJsstringBoundaryCarrierStandalone(programFacts)
2123
- narrowBoolResults()
2124
- return programFacts
2125
- }
2126
-
2127
- narrowSignatures(programFacts, ast)
2128
- specializeBimorphicTyped(programFacts)
2129
- refineDynKeys(programFacts)
2130
-
2131
- return programFacts
2132
- }