jz 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +281 -142
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +461 -185
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +591 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +600 -205
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -2997
  77. package/src/jzify.js +0 -1553
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,984 @@
1
+ /**
2
+ * Literal-form heap elision & narrowing.
3
+ *
4
+ * Three related transforms over `let`/`const` literal bindings that the
5
+ * back-end would otherwise allocate on the heap:
6
+ *
7
+ * - `scalarizeFunctionTypedArrays` — fixed-size `new Int32Array(N)` etc.
8
+ * whose every use is statically indexed
9
+ * collapse to N WASM locals; element
10
+ * coercion stays inline.
11
+ * - `scalarizeFunctionArrayLiterals`/`scalarizeFunctionObjectLiterals` —
12
+ * fixed-size `[…]` and `{…}` literals
13
+ * with static accesses collapse to
14
+ * per-slot locals (heap → registers).
15
+ * - `promoteIntArrayLiterals` — `let xs = [1,2,3]` with every use
16
+ * typed-array-compatible promotes to
17
+ * `new Int32Array([1,2,3])`. Downstream
18
+ * narrows the carrier to a tight i32 vec.
19
+ *
20
+ * Each wrapper iterates to fixpoint and returns `boolean changed` so the
21
+ * orchestrator knows when to invalidate the program-facts cache.
22
+ *
23
+ * @module compile/plan/literals
24
+ */
25
+
26
+ import { ctx } from '../../ctx.js'
27
+ import {
28
+ some, T, stmtList, refsName, ASSIGN_OPS, isReassigned, hasControlTransfer,
29
+ } from '../../ast.js'
30
+ import {
31
+ intLiteralValue, constIntExpr, staticObjectProps, staticPropertyKey,
32
+ } from '../../static.js'
33
+ import {
34
+ smallConstForTripCount, containsDeclOf, cloneWithSubst,
35
+ } from '../../type.js'
36
+ import { VAL } from '../../reps.js'
37
+ import { includeModule } from '../../autoload.js'
38
+ import { analyzeBody, invalidateLocalsCache } from '../analyze.js'
39
+ import {
40
+ isSimpleArg, fixedScalarTypedArray, fixedTypedArraysInBody, maxScalarTypedArrayLen,
41
+ } from './common.js'
42
+
43
+ // === Loop unrolling & scalarization ===
44
+
45
+ // AST for the store coercion a typed-array element does on write (`arr[i] = v`).
46
+ // All expressible with operators jz already lowers post-plan (no module deps).
47
+ const coerceAST = (kind, expr) => {
48
+ if (kind === 'i32') return ['|', expr, [null, 0]]
49
+ if (kind === 'i16') return ['>>', ['<<', expr, [null, 16]], [null, 16]]
50
+ if (kind === 'u16') return ['&', expr, [null, 0xffff]]
51
+ if (kind === 'i8') return ['>>', ['<<', expr, [null, 24]], [null, 24]]
52
+ if (kind === 'u8') return ['&', expr, [null, 0xff]]
53
+ return expr
54
+ }
55
+ const maxScalarTypedLoopUnroll = () => ctx.transform.optimize?.scalarTypedLoopUnroll ?? 16
56
+ const maxScalarTypedNestedUnroll = () => ctx.transform.optimize?.scalarTypedNestedUnroll ?? 128
57
+
58
+ const scalarArrayElems = (expr) => {
59
+ if (!Array.isArray(expr) || expr[0] !== '[') return null
60
+ const elems = expr.slice(1)
61
+ if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...') || !isSimpleArg(e))) return null
62
+ return elems
63
+ }
64
+
65
+ const scalarObjectProps = (expr) => {
66
+ if (!Array.isArray(expr) || expr[0] !== '{}') return null
67
+ const props = staticObjectProps(expr.slice(1))
68
+ if (!props) return null
69
+ const seen = new Set()
70
+ for (let i = 0; i < props.names.length; i++) {
71
+ const name = props.names[i]
72
+ if (seen.has(name) || !isSimpleArg(props.values[i])) return null
73
+ seen.add(name)
74
+ }
75
+ return props
76
+ }
77
+
78
+ const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
79
+
80
+ // `name.length = n` (resize) / `name.prop op= v` / `++name.length`: a member
81
+ // write on the binding can't be modeled by fixed scalar slots — the fold would
82
+ // turn the assignment TARGET into a literal (`[null, len] = v`).
83
+ const isMemberWriteTarget = (op, node, name) =>
84
+ (ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
85
+ && Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') && node[1][1] === name
86
+
87
+ const safeScalarArrayUse = (node, name, len, parentOp = null) => {
88
+ if (typeof node === 'string') return node !== name
89
+ if (!Array.isArray(node)) return true
90
+ const op = node[0]
91
+ if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
92
+ if (isMemberWriteTarget(op, node, name)) return false
93
+ if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
94
+ if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
95
+ // Element write `name[idx] (op)= v` / `name[idx]++`: an out-of-bounds index
96
+ // grows the array (sparse-array semantics), which the fixed scalar slot set
97
+ // can't model — reject unless idx is a literal within the literal's bounds.
98
+ if ((ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
99
+ && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
100
+ const idx = constIntExpr(node[1][2])
101
+ if (idx == null || idx < 0 || idx >= len) return false
102
+ for (let i = 2; i < node.length; i++) if (!safeScalarArrayUse(node[i], name, len, op)) return false
103
+ return true
104
+ }
105
+ if (op === '[]' && node[1] === name) return constIntExpr(node[2]) != null
106
+ if (op === '...' && node[1] === name) return parentOp === '['
107
+ for (let i = 1; i < node.length; i++) {
108
+ if (!safeScalarArrayUse(node[i], name, len, op)) return false
109
+ }
110
+ return true
111
+ }
112
+
113
+ const rewriteScalarArrayUses = (node, arrays, parentOp = null) => {
114
+ if (!Array.isArray(node)) return node
115
+ const op = node[0]
116
+ if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') {
117
+ return [, arrays.get(node[1]).length]
118
+ }
119
+ if (op === '[]' && arrays.has(node[1])) {
120
+ const idx = constIntExpr(node[2])
121
+ const elems = arrays.get(node[1])
122
+ return idx != null && idx >= 0 && idx < elems.length ? elems[idx] : [, undefined]
123
+ }
124
+ if (op === '[') {
125
+ const out = ['[']
126
+ for (let i = 1; i < node.length; i++) {
127
+ const item = node[i]
128
+ if (Array.isArray(item) && item[0] === '...' && arrays.has(item[1])) {
129
+ out.push(...arrays.get(item[1]))
130
+ } else {
131
+ out.push(rewriteScalarArrayUses(item, arrays, op))
132
+ }
133
+ }
134
+ return out
135
+ }
136
+ return node.map((part, i) => i === 0 ? part : rewriteScalarArrayUses(part, arrays, op))
137
+ }
138
+
139
+ const safeScalarObjectUse = (node, name, keys) => {
140
+ if (typeof node === 'string') return node !== name
141
+ if (!Array.isArray(node)) return true
142
+ const op = node[0]
143
+ if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
144
+ if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
145
+ if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2])
146
+ if (op === '[]' && node[1] === name) {
147
+ const key = staticPropertyKey(node[2])
148
+ return key != null && keys.has(key)
149
+ }
150
+ if (op === '...' && node[1] === name) return false
151
+ for (let i = 1; i < node.length; i++) {
152
+ if (!safeScalarObjectUse(node[i], name, keys)) return false
153
+ }
154
+ return true
155
+ }
156
+
157
+ const rewriteScalarObjectUses = (node, objects) => {
158
+ if (!Array.isArray(node)) return node
159
+ const op = node[0]
160
+ if ((op === '.' || op === '?.') && objects.has(node[1])) {
161
+ const fields = objects.get(node[1])
162
+ return fields.get(node[2]) ?? [, undefined]
163
+ }
164
+ if (op === '[]' && objects.has(node[1])) {
165
+ const key = staticPropertyKey(node[2])
166
+ const fields = objects.get(node[1])
167
+ return key != null ? (fields.get(key) ?? [, undefined]) : node
168
+ }
169
+ return node.map((part, i) => i === 0 ? part : rewriteScalarObjectUses(part, objects))
170
+ }
171
+
172
+ const typedArraySlotIndex = (node, len) => {
173
+ const idx = constIntExpr(node)
174
+ return idx != null && idx >= 0 && idx < len ? idx : null
175
+ }
176
+
177
+ // `coerce` truthy ⇒ the array's element type truncates on store (Int*/Uint* views),
178
+ // so in-place updates (`arr[i]++`, `arr[i] += x`) can't be a plain `slot`-op rewrite —
179
+ // reject them and only scalarize plain `arr[i] = v` writes and `arr[i]` reads.
180
+ const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
181
+ if (typeof node === 'string') return node !== name
182
+ if (!Array.isArray(node)) return true
183
+ const op = node[0]
184
+ if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
185
+ if (isMemberWriteTarget(op, node, name)) return false
186
+ if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
187
+ if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
188
+ if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
189
+ return !coerce && typedArraySlotIndex(node[1][2], len) != null
190
+ if (ASSIGN_TARGET_OPS.has(op)) {
191
+ if (node[1] === name) return false
192
+ if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
193
+ if (coerce && op !== '=') return false
194
+ if (typedArraySlotIndex(node[1][2], len) == null) return false
195
+ for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
196
+ return true
197
+ }
198
+ }
199
+ if (op === '...' && node[1] === name) return false
200
+ for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
201
+ return true
202
+ }
203
+ const rewriteScalarTypedArrayUses = (node, arrays) => {
204
+ if (!Array.isArray(node)) return node
205
+ const op = node[0]
206
+ const slotFor = (idxNode, entry) => {
207
+ const idx = typedArraySlotIndex(idxNode, entry.len)
208
+ return idx == null ? null : entry.slots[idx]
209
+ }
210
+ if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') return [null, arrays.get(node[1]).len]
211
+ if (op === '[]' && arrays.has(node[1])) return slotFor(node[2], arrays.get(node[1])) ?? node
212
+ if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
213
+ const slot = slotFor(node[1][2], arrays.get(node[1][1]))
214
+ return slot ? [op, slot] : node
215
+ }
216
+ if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
217
+ const entry = arrays.get(node[1][1])
218
+ const slot = slotFor(node[1][2], entry)
219
+ if (!slot) return node
220
+ const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
221
+ return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
222
+ }
223
+ return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
224
+ }
225
+
226
+ const scalarTypedArrayStores = (name, entry) =>
227
+ entry.slots.map((slot, i) => ['=', ['[]', name, [null, i]], slot])
228
+
229
+ const scalarTypedArrayLoads = (name, entry) =>
230
+ entry.slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])
231
+
232
+ const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
233
+ if (!Array.isArray(node)) return out
234
+ const op = node[0]
235
+ const addSlot = target => {
236
+ if (Array.isArray(target) && target[0] === '[]' && target[1] === name) {
237
+ const idx = typedArraySlotIndex(target[2], len)
238
+ if (idx != null) out.add(idx)
239
+ return true
240
+ }
241
+ return false
242
+ }
243
+ if ((op === '++' || op === '--') && addSlot(node[1])) return out
244
+ if (ASSIGN_TARGET_OPS.has(op) && addSlot(node[1])) {
245
+ for (let i = 2; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
246
+ return out
247
+ }
248
+ if (op !== '=>') for (let i = 1; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
249
+ return out
250
+ }
251
+
252
+ const hasScalarTypedArrayRead = (node, name) => {
253
+ if (!Array.isArray(node)) return false
254
+ const op = node[0]
255
+ const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
256
+ if ((op === '++' || op === '--') && isTarget(node[1])) return true
257
+ if (ASSIGN_TARGET_OPS.has(op)) {
258
+ if (isTarget(node[1])) {
259
+ if (op !== '=') return true
260
+ for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
261
+ return false
262
+ }
263
+ }
264
+ if (op === '[]' && node[1] === name) return true
265
+ if (op === '=>') return false
266
+ for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
267
+ return false
268
+ }
269
+
270
+ const scalarizeTypedArrayLiteralSeq = (seq) => {
271
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
272
+ let changed = false
273
+ const stmts = seq.slice(1).map(stmt => {
274
+ const r = scalarizeTypedArrayLiterals(stmt)
275
+ changed ||= r.changed
276
+ return r.node
277
+ })
278
+
279
+ const candidates = new Map()
280
+ const mirrored = new Map()
281
+ for (let i = 0; i < stmts.length; i++) {
282
+ const stmt = stmts[i]
283
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
284
+ const decl = stmt[1]
285
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
286
+ const fixed = fixedScalarTypedArray(decl[2])
287
+ if (fixed == null) continue
288
+ const { len, coerce } = fixed
289
+ let hasSafeUse = false, hasUnsafeUse = false
290
+ for (let j = 0; j < stmts.length; j++) {
291
+ if (j === i) continue
292
+ if (!refsName(stmts[j], decl[1])) continue
293
+ const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
294
+ hasSafeUse ||= safe
295
+ hasUnsafeUse ||= !safe
296
+ }
297
+ if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
298
+ if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
299
+ else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
300
+ }
301
+ if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
302
+
303
+ const arrays = new Map()
304
+ for (const [name, c] of [...candidates, ...mirrored]) {
305
+ const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
306
+ arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
307
+ }
308
+
309
+ const out = []
310
+ for (let i = 0; i < stmts.length; i++) {
311
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
312
+ [...mirrored.entries()].find(([, c]) => c.index === i)
313
+ if (entry) {
314
+ const [name] = entry
315
+ const arr = arrays.get(name)
316
+ const { slots } = arr
317
+ if (arr.mirrored) {
318
+ out.push(stmts[i])
319
+ if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
320
+ } else if (slots.length) {
321
+ out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
322
+ }
323
+ changed = true
324
+ continue
325
+ }
326
+ const unsafe = []
327
+ for (const [name, arr] of arrays) {
328
+ if (arr.mirrored && refsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
329
+ }
330
+ if (unsafe.length) {
331
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
332
+ out.push(stmts[i])
333
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
334
+ changed = true
335
+ } else {
336
+ out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
337
+ }
338
+ }
339
+ return { node: [';', ...out], changed: true }
340
+ }
341
+
342
+ function scalarizeTypedArrayLiterals(node) {
343
+ if (!Array.isArray(node)) return { node, changed: false }
344
+ if (node[0] === '=>') return { node, changed: false }
345
+ if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
346
+ let changed = false
347
+ const out = [node[0]]
348
+ for (let i = 1; i < node.length; i++) {
349
+ const r = scalarizeTypedArrayLiterals(node[i])
350
+ changed ||= r.changed
351
+ out.push(r.node)
352
+ }
353
+ return changed ? { node: out, changed: true } : { node, changed: false }
354
+ }
355
+
356
+ const containsTypedArrayAccess = (body, names) => some(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
357
+
358
+ function smallScalarTypedForTrip(init, cond, step) {
359
+ const end = smallConstForTripCount(init, cond, step, maxScalarTypedLoopUnroll())
360
+ if (end == null) return null
361
+ const decl = init[1]
362
+ return { name: decl[1], end }
363
+ }
364
+
365
+ const scalarTypedLoopBudget = (body) => {
366
+ if (!Array.isArray(body) || body[0] === '=>') return 1
367
+ if (body[0] === 'for') {
368
+ const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
369
+ return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
370
+ }
371
+ let max = 1
372
+ for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
373
+ return max
374
+ }
375
+
376
+ const unrollTypedArrayLoops = (node, names) => {
377
+ if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
378
+ if (node[0] === ';') {
379
+ let changed = false
380
+ const out = [';']
381
+ for (const stmt of node.slice(1)) {
382
+ const r = unrollTypedArrayLoops(stmt, names)
383
+ changed ||= r.changed
384
+ if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
385
+ else out.push(r.node)
386
+ }
387
+ return changed ? { node: out, changed: true } : { node, changed: false }
388
+ }
389
+ if (node[0] === '{}') {
390
+ const r = unrollTypedArrayLoops(node[1], names)
391
+ return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
392
+ }
393
+ if (node[0] === 'for') {
394
+ const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
395
+ if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
396
+ !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
397
+ const out = [';']
398
+ for (let i = 0; i < trip.end; i++) {
399
+ const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
400
+ const r = unrollTypedArrayLoops(cloned, names)
401
+ out.push(...stmtList(r.node))
402
+ }
403
+ return { node: out, changed: true }
404
+ }
405
+ }
406
+ let changed = false
407
+ const out = [node[0]]
408
+ for (let i = 1; i < node.length; i++) {
409
+ const r = unrollTypedArrayLoops(node[i], names)
410
+ changed ||= r.changed
411
+ out.push(r.node)
412
+ }
413
+ return changed ? { node: out, changed: true } : { node, changed: false }
414
+ }
415
+
416
+ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
417
+ if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
418
+ if (some(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
419
+ const params = func.sig?.params || []
420
+ const cands = new Map()
421
+ for (let i = 0; i < params.length; i++) {
422
+ const pname = params[i].name
423
+ let len = null, coerce = null, ok = true
424
+ for (const site of sites) {
425
+ const arg = site.argList[i]
426
+ const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
427
+ if (!fixed) { ok = false; break }
428
+ if (len == null) { len = fixed.len; coerce = fixed.coerce }
429
+ else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
430
+ }
431
+ if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
432
+ }
433
+ if (!cands.size) return cands
434
+ for (const site of sites) {
435
+ const seen = new Set()
436
+ for (let i = 0; i < params.length; i++) {
437
+ if (!cands.has(params[i].name)) continue
438
+ const arg = site.argList[i]
439
+ if (typeof arg !== 'string' || seen.has(arg)) return new Map()
440
+ seen.add(arg)
441
+ }
442
+ }
443
+ return cands
444
+ }
445
+
446
+ const scalarizeTypedArrayParams = (func, paramCands) => {
447
+ for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
448
+ for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
449
+ if (!paramCands.size) return { body: func.body, changed: false }
450
+ const arrays = new Map()
451
+ for (const [name, c] of paramCands) {
452
+ arrays.set(name, {
453
+ len: c.len,
454
+ coerce: c.coerce,
455
+ slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
456
+ })
457
+ }
458
+ const prologue = []
459
+ const writeback = []
460
+ for (const [name, { len, slots }] of arrays) {
461
+ if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
462
+ for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
463
+ }
464
+ const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
465
+ return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
466
+ }
467
+
468
+ export const scalarizeFunctionTypedArrays = (programFacts) => {
469
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
470
+ const sitesByCallee = new Map()
471
+ for (const site of programFacts.callSites) {
472
+ if (!site.callerFunc) continue
473
+ const list = sitesByCallee.get(site.callee)
474
+ if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
475
+ }
476
+ let changed = false
477
+ for (const func of ctx.func.list) {
478
+ if (!func.body || func.raw) continue
479
+ const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
480
+ const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
481
+ if (names.size) {
482
+ let guard = 0
483
+ while (guard++ < 6) {
484
+ const r = unrollTypedArrayLoops(func.body, names)
485
+ if (!r.changed) break
486
+ func.body = r.node
487
+ changed = true
488
+ }
489
+ }
490
+ const p = scalarizeTypedArrayParams(func, paramCands)
491
+ if (p.changed) { func.body = p.body; changed = true }
492
+ const l = scalarizeTypedArrayLiterals(func.body)
493
+ if (l.changed) { func.body = l.node; changed = true }
494
+ if (changed) invalidateLocalsCache(func.body)
495
+ }
496
+ return changed
497
+ }
498
+
499
+ const scalarizeArrayLiteralSeq = (seq) => {
500
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
501
+ let changed = false
502
+ const stmts = seq.slice(1).map(stmt => {
503
+ const r = scalarizeArrayLiterals(stmt)
504
+ changed ||= r.changed
505
+ return r.node
506
+ })
507
+
508
+ const candidates = new Map()
509
+ for (let i = 0; i < stmts.length; i++) {
510
+ const stmt = stmts[i]
511
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
512
+ const decl = stmt[1]
513
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
514
+ const elems = scalarArrayElems(decl[2])
515
+ if (!elems) continue
516
+ let ok = true
517
+ for (let j = 0; j < stmts.length && ok; j++) {
518
+ if (j === i) continue
519
+ ok = safeScalarArrayUse(stmts[j], decl[1], elems.length)
520
+ }
521
+ if (!ok) continue
522
+ candidates.set(decl[1], { index: i, op: stmt[0], elems })
523
+ }
524
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
525
+
526
+ const arrays = new Map()
527
+ for (const [name, c] of candidates) {
528
+ const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
529
+ arrays.set(name, temps)
530
+ }
531
+
532
+ const out = []
533
+ for (let i = 0; i < stmts.length; i++) {
534
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
535
+ if (entry) {
536
+ const [name, c] = entry
537
+ const temps = arrays.get(name)
538
+ if (temps.length) {
539
+ out.push([c.op, ...temps.map((tmp, k) =>
540
+ ['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
541
+ }
542
+ changed = true
543
+ continue
544
+ }
545
+ out.push(rewriteScalarArrayUses(stmts[i], arrays))
546
+ }
547
+ return { node: [';', ...out], changed: true }
548
+ }
549
+
550
+ const scalarizeObjectLiteralSeq = (seq, escapes) => {
551
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
552
+ let changed = false
553
+ const stmts = seq.slice(1).map(stmt => {
554
+ const r = scalarizeObjectLiterals(stmt, escapes)
555
+ changed ||= r.changed
556
+ return r.node
557
+ })
558
+
559
+ const candidates = new Map()
560
+ for (let i = 0; i < stmts.length; i++) {
561
+ const stmt = stmts[i]
562
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
563
+ const decl = stmt[1]
564
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
565
+ if (escapes.get(decl[1]) !== false) continue
566
+ const props = scalarObjectProps(decl[2])
567
+ if (!props) continue
568
+ const keys = new Set(props.names)
569
+ let ok = true
570
+ for (let j = 0; j < stmts.length && ok; j++) {
571
+ if (j === i) continue
572
+ ok = safeScalarObjectUse(stmts[j], decl[1], keys)
573
+ }
574
+ if (!ok) continue
575
+ candidates.set(decl[1], { index: i, op: stmt[0], props })
576
+ }
577
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
578
+
579
+ const objects = new Map()
580
+ for (const [name, c] of candidates) {
581
+ const fields = new Map()
582
+ for (let i = 0; i < c.props.names.length; i++) {
583
+ fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
584
+ }
585
+ objects.set(name, fields)
586
+ }
587
+
588
+ const out = []
589
+ for (let i = 0; i < stmts.length; i++) {
590
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
591
+ if (entry) {
592
+ const [, c] = entry
593
+ const fields = objects.get(entry[0])
594
+ if (c.props.names.length) {
595
+ out.push([c.op, ...c.props.names.map((prop, k) =>
596
+ ['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
597
+ }
598
+ changed = true
599
+ continue
600
+ }
601
+ out.push(rewriteScalarObjectUses(stmts[i], objects))
602
+ }
603
+ return { node: [';', ...out], changed: true }
604
+ }
605
+
606
+ function scalarizeObjectLiterals(node, escapes) {
607
+ if (!Array.isArray(node)) return { node, changed: false }
608
+ if (node[0] === '=>') {
609
+ const r = scalarizeObjectLiterals(node[2], escapes)
610
+ if (!r.changed) return { node, changed: false }
611
+ return { node: [node[0], node[1], r.node], changed: true }
612
+ }
613
+ if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
614
+ let changed = false
615
+ const out = [node[0]]
616
+ for (let i = 1; i < node.length; i++) {
617
+ const r = scalarizeObjectLiterals(node[i], escapes)
618
+ changed ||= r.changed
619
+ out.push(r.node)
620
+ }
621
+ return changed ? { node: out, changed: true } : { node, changed: false }
622
+ }
623
+
624
+ function scalarizeArrayLiterals(node) {
625
+ if (!Array.isArray(node)) return { node, changed: false }
626
+ if (node[0] === '=>') {
627
+ const r = scalarizeArrayLiterals(node[2])
628
+ if (!r.changed) return { node, changed: false }
629
+ return { node: [node[0], node[1], r.node], changed: true }
630
+ }
631
+ if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
632
+ let changed = false
633
+ const out = [node[0]]
634
+ for (let i = 1; i < node.length; i++) {
635
+ const r = scalarizeArrayLiterals(node[i])
636
+ changed ||= r.changed
637
+ out.push(r.node)
638
+ }
639
+ return changed ? { node: out, changed: true } : { node, changed: false }
640
+ }
641
+
642
+ export const scalarizeFunctionArrayLiterals = () => {
643
+ let changed = false
644
+ for (const func of ctx.func.list) {
645
+ if (!func.body || func.raw) continue
646
+ let guard = 0
647
+ while (guard++ < 4) {
648
+ const r = scalarizeArrayLiterals(func.body)
649
+ if (!r.changed) break
650
+ func.body = r.node
651
+ changed = true
652
+ }
653
+ }
654
+ return changed
655
+ }
656
+
657
+
658
+
659
+ // === Int-array → Int32Array auto-promotion =================================
660
+ //
661
+ // A `let xs = [intLit, intLit, …]` binding whose every use is TYPED-compatible
662
+ // is rewritten to `let xs = new Int32Array([intLit, …])`. Downstream analysis
663
+ // then takes over: valTypeOf → VAL.TYPED, methods dispatch through `.typed:`,
664
+ // loops get auto-vectorized via i32x4 (SIMD pass), and the carrier shrinks
665
+ // from 8-byte f64 slots to packed i32. The promotion runs after literal
666
+ // scalarization (so arrays fully scalarized away are already gone) and before
667
+ // typed-array param scalarization (so a freshly-promoted array can still
668
+ // participate in subsequent loop unrolling).
669
+ //
670
+ // Safety: the binding must never appear in a pattern that TYPED can't honor.
671
+ // Disqualifiers (each fires per binding name):
672
+ // 1. reassignment `xs = …` / compound `xs +=` / `++xs` / `--xs` — TYPED has
673
+ // no value-replacement op; `xs = new TypedArray(…)` would also drop the
674
+ // promoted view's identity.
675
+ // 2. element write `xs[k] = v` — TYPED's i32-trunc store would lose
676
+ // fractional/NaN bits that VAL.ARRAY would have preserved.
677
+ // 3. method calls outside the read-safe whitelist (push, pop, shift, …) —
678
+ // TYPED arrays are fixed-length; mutators don't exist on the carrier.
679
+ // 4. `Array.isArray(xs)` — semantics flip true→false on promotion.
680
+ // 5. `…xs` spread / `xs` as call arg / `xs` as return / bare reference in
681
+ // any other position — escape; callee may rely on ARRAY layout.
682
+ // 6. captured by a closure / shadowed by an inner decl — same escape
683
+ // reasoning, plus the inner decl could rebind to a non-array.
684
+ //
685
+ // Elements at init must all be i32-range integer literals. A negative literal
686
+ // arrives as `[null, -n]` after prepare's constant folding; intLiteralValue
687
+ // recognizes that form. Float literals (`[1, 2.5]`) and out-of-range ints
688
+ // (`0x80000000` on the +ve side, etc.) disqualify the array as a whole.
689
+
690
+ // Methods we promote across. The bar: every entry must have a real `.typed:*`
691
+ // emitter in module/typedarray.js. Receiver-flow into chained methods is also
692
+ // typed-aware now — `.typed:map`/`.typed:filter`/`.typed:slice` all return
693
+ // TYPED carriers, and downstream `.filter`/`.slice`/`.map`/etc. on those
694
+ // re-dispatch via emit.js:2211's `.typed:<m>` lookup (VAL.TYPED ⇒ typed
695
+ // emitter). Methods missing here (.join, .sort, .reverse, .subarray, .fill,
696
+ // .toString, .copyWithin, …) lack a typed emitter — disqualify the candidate.
697
+ const _TYPED_SAFE_METHODS = new Set([
698
+ 'set',
699
+ 'map', 'filter', 'slice',
700
+ 'forEach', 'reduce',
701
+ 'indexOf', 'includes', 'find', 'findIndex', 'some', 'every',
702
+ ])
703
+
704
+ // `.length` is TYPED-aware via core.js:__len (shifts the byte header by
705
+ // __typed_shift on TAG=3). `.byteLength`/`.byteOffset`/`.buffer` are
706
+ // TYPED-only — a user reading those already expects a typed array, so a
707
+ // promotion candidate that hits them is a coincidence we'd rather not
708
+ // rely on; disqualify and let them write the TypedArray construction
709
+ // themselves.
710
+ const _TYPED_SAFE_PROPS = new Set(['length'])
711
+
712
+ // Returns the i32-range integer payload of an array-literal element, or null
713
+ // if the element isn't a literal integer that fits in i32. Mirrors the shape
714
+ // check used by `intLiteralValue` but without the rep-lookup (we're pre-
715
+ // analysis here, and want a pure syntactic gate).
716
+ const _intArrayLitElems = (expr) => {
717
+ if (!Array.isArray(expr) || expr[0] !== '[') return null
718
+ if (expr.length < 2) return null // empty literal — low value, skip
719
+ const out = []
720
+ for (let i = 1; i < expr.length; i++) {
721
+ const v = intLiteralValue(expr[i])
722
+ if (v == null) return null
723
+ out.push(v)
724
+ }
725
+ return out
726
+ }
727
+
728
+ // Walks `node` and disqualifies every candidate name that appears in an
729
+ // unsafe context. `initSet` holds the candidate's own init-decl AST nodes
730
+ // (their LHS reference is the binding being defined, not an escape).
731
+ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
732
+ if (initSet.has(node)) {
733
+ // The init decl itself: only walk the RHS (skip the LHS `name`).
734
+ return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
735
+ }
736
+ if (typeof node === 'string') {
737
+ // Bare identifier outside any handled parent context — escape.
738
+ if (candidates.has(node)) disqualified.add(node)
739
+ return
740
+ }
741
+ if (!Array.isArray(node)) return
742
+ const op = node[0]
743
+
744
+ // Closure body — any candidate referenced inside is captured. Bail without
745
+ // recursing further (we'd otherwise hit the bare-name leaf and disqualify
746
+ // anyway, but this is explicit and avoids walking the inner closure).
747
+ if (op === '=>') {
748
+ for (const n of candidates.keys()) {
749
+ if (!disqualified.has(n) && refsName(node, n, { skipArrow: false })) disqualified.add(n)
750
+ }
751
+ return
752
+ }
753
+
754
+ // Member write target `name.length = n` / `++name.length` — resize is an
755
+ // ARRAY-only op (TYPED is fixed-size); disqualify.
756
+ if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') &&
757
+ Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') &&
758
+ typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
759
+ disqualified.add(node[1][1])
760
+ for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
761
+ return
762
+ }
763
+
764
+ // Property access `name.prop` / `name?.prop`. Bare property reads only —
765
+ // method calls reach here via the `()` handler below (which intercepts
766
+ // before recursing into its callee).
767
+ if ((op === '.' || op === '?.') && typeof node[1] === 'string' && candidates.has(node[1])) {
768
+ if (!_TYPED_SAFE_PROPS.has(node[2])) disqualified.add(node[1])
769
+ return // node[2] is the property name (string), not an expression — done
770
+ }
771
+
772
+ // Method or function call. Two shapes carry the candidate:
773
+ // `name.method(args)` — receiver at node[1][1]; method whitelist gates.
774
+ // `f(…, name, …)` / `Array.isArray(name)` — name appears as a plain arg.
775
+ if (op === '()') {
776
+ const callee = node[1]
777
+ // Method call on a candidate receiver.
778
+ if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
779
+ typeof callee[1] === 'string' && candidates.has(callee[1])) {
780
+ if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
781
+ // Walk method args (skip the receiver — already validated above).
782
+ for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
783
+ return
784
+ }
785
+ // Array.isArray flips true→false under promotion.
786
+ if (callee === 'Array.isArray') {
787
+ const raw = node[2]
788
+ const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
789
+ for (const a of list) {
790
+ if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
791
+ else _disqualifyPromotion(a, candidates, disqualified, initSet)
792
+ }
793
+ return
794
+ }
795
+ // Fall through to generic recursion — `name` as a plain arg will hit the
796
+ // bare-name leaf above and disqualify.
797
+ }
798
+
799
+ // Index read `name[k]` — read access is TYPED-safe. Walk the key in case
800
+ // it contains references to other candidate names.
801
+ if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
802
+ _disqualifyPromotion(node[2], candidates, disqualified, initSet)
803
+ return
804
+ }
805
+
806
+ // Element write: `['=', ['[]', name, k], v]` (and compound forms).
807
+ // V1 stays read-only after init — element writes would silently truncate.
808
+ if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
809
+ typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
810
+ disqualified.add(node[1][1])
811
+ _disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
812
+ _disqualifyPromotion(node[2], candidates, disqualified, initSet)
813
+ return
814
+ }
815
+
816
+ // Whole-binding reassign: `name = …` / `name += …` / etc.
817
+ if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
818
+ disqualified.add(node[1])
819
+ _disqualifyPromotion(node[2], candidates, disqualified, initSet)
820
+ return
821
+ }
822
+
823
+ // Pre/post-increment on var or element.
824
+ if (op === '++' || op === '--') {
825
+ const t = node[1]
826
+ if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
827
+ if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
828
+ disqualified.add(t[1])
829
+ _disqualifyPromotion(t[2], candidates, disqualified, initSet)
830
+ return
831
+ }
832
+ }
833
+
834
+ // `let`/`const` declaration — the candidate's own init lands here via the
835
+ // initSet branch at the top. Any *other* decl that names a candidate is a
836
+ // shadow (impossible after jz hoists, but defensive) and disqualifies.
837
+ if (op === 'let' || op === 'const') {
838
+ for (let i = 1; i < node.length; i++) {
839
+ const d = node[i]
840
+ if (typeof d === 'string' && candidates.has(d)) disqualified.add(d)
841
+ else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
842
+ candidates.has(d[1]) && !initSet.has(d)) {
843
+ disqualified.add(d[1])
844
+ _disqualifyPromotion(d[2], candidates, disqualified, initSet)
845
+ } else {
846
+ _disqualifyPromotion(d, candidates, disqualified, initSet)
847
+ }
848
+ }
849
+ return
850
+ }
851
+
852
+ // Spread `…name` — could be in a call, a `[`, or a destructure target.
853
+ if (op === '...' && typeof node[1] === 'string' && candidates.has(node[1])) {
854
+ disqualified.add(node[1])
855
+ return
856
+ }
857
+
858
+ // for-of / for-in iteration: receiver position is `node[2]` (a bare name
859
+ // there would otherwise trigger escape). TYPED supports iteration, so
860
+ // allow the receiver but walk the body for other refs.
861
+ if (op === 'for-of' || op === 'for-in') {
862
+ // Walk decl (node[1]), iter (node[2]), body (node[3]); receiver as bare
863
+ // name is fine — only the body matters for further refs to the same name
864
+ // (but body refs would shadow or escape, which other rules catch).
865
+ for (let i = 1; i < node.length; i++) {
866
+ const child = node[i]
867
+ if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
868
+ _disqualifyPromotion(child, candidates, disqualified, initSet)
869
+ }
870
+ return
871
+ }
872
+
873
+ // Generic — recurse into children. Bare-name refs at unhandled positions
874
+ // hit the string-leaf branch above and disqualify on contact.
875
+ for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
876
+ }
877
+
878
+ // Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
879
+ // carries the exact init-decl AST node so the disqualifier can skip the
880
+ // binding's own LHS reference (which would otherwise look like a reassign).
881
+ const _collectIntArrayCandidates = (node, candidates) => {
882
+ if (!Array.isArray(node)) return
883
+ const op = node[0]
884
+ if (op === '=>') return
885
+ if (op === 'let' || op === 'const') {
886
+ for (let i = 1; i < node.length; i++) {
887
+ const d = node[i]
888
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
889
+ const elems = _intArrayLitElems(d[2])
890
+ if (elems == null) continue
891
+ // jz hoists `let` to function scope — duplicate candidate-name collisions
892
+ // shouldn't happen, but if they do the second wins (disqualifyPromotion
893
+ // will mark both via the shadow rule).
894
+ candidates.set(d[1], { initDecl: d, elems })
895
+ }
896
+ }
897
+ for (let i = 1; i < node.length; i++) _collectIntArrayCandidates(node[i], candidates)
898
+ }
899
+
900
+ // Rewrite `let name = [...]` → `let name = new Int32Array([...])` for every
901
+ // validated candidate. Preserves the original element AST nodes so the
902
+ // downstream `Int32Array.from` lowering picks up its existing static-data
903
+ // segment / per-element-store fast paths.
904
+ const _rewritePromoted = (node, validated, initSet) => {
905
+ if (!Array.isArray(node)) return { node, changed: false }
906
+ const op = node[0]
907
+ if (op === '=>') {
908
+ // Closures don't reach validated bindings (we disqualified on capture).
909
+ // Still recurse — a nested closure may itself hold a promotable.
910
+ let changed = false
911
+ const out = [op]
912
+ for (let i = 1; i < node.length; i++) {
913
+ const r = _rewritePromoted(node[i], validated, initSet)
914
+ if (r.changed) changed = true
915
+ out.push(r.node)
916
+ }
917
+ return changed ? { node: out, changed: true } : { node, changed: false }
918
+ }
919
+ let changed = false
920
+ const out = [op]
921
+ for (let i = 1; i < node.length; i++) {
922
+ const child = node[i]
923
+ if ((op === 'let' || op === 'const') && Array.isArray(child) && child[0] === '=' &&
924
+ typeof child[1] === 'string' && validated.has(child[1]) && initSet.has(child)) {
925
+ const newRhs = ['()', 'new.Int32Array', child[2]]
926
+ out.push(['=', child[1], newRhs])
927
+ changed = true
928
+ continue
929
+ }
930
+ const r = _rewritePromoted(child, validated, initSet)
931
+ if (r.changed) changed = true
932
+ out.push(r.node)
933
+ }
934
+ return changed ? { node: out, changed: true } : { node, changed: false }
935
+ }
936
+
937
+ const promoteIntArrayLiteralsInBody = (body) => {
938
+ const candidates = new Map()
939
+ _collectIntArrayCandidates(body, candidates)
940
+ if (!candidates.size) return { node: body, changed: false }
941
+ const initSet = new Set()
942
+ for (const { initDecl } of candidates.values()) initSet.add(initDecl)
943
+ const disqualified = new Set()
944
+ _disqualifyPromotion(body, candidates, disqualified, initSet)
945
+ const validated = new Set()
946
+ for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
947
+ if (!validated.size) return { node: body, changed: false }
948
+ return _rewritePromoted(body, validated, initSet)
949
+ }
950
+
951
+ // On the first successful promotion, pull in the typedarray module so the
952
+ // emitted `new.Int32Array` callee has a registered emitter. Calling
953
+ // `includeModule('typedarray')` on a no-op (already loaded) is cheap.
954
+ export const promoteIntArrayLiterals = () => {
955
+ let changed = false
956
+ for (const func of ctx.func.list) {
957
+ if (!func.body || func.raw) continue
958
+ const r = promoteIntArrayLiteralsInBody(func.body)
959
+ if (r.changed) {
960
+ func.body = r.node
961
+ invalidateLocalsCache(func.body)
962
+ if (!changed) includeModule('typedarray')
963
+ changed = true
964
+ }
965
+ }
966
+ return changed
967
+ }
968
+
969
+ export const scalarizeFunctionObjectLiterals = () => {
970
+ let changed = false
971
+ for (const func of ctx.func.list) {
972
+ if (!func.body || func.raw) continue
973
+ let guard = 0
974
+ while (guard++ < 4) {
975
+ const escapes = new Map(analyzeBody(func.body).escapes)
976
+ invalidateLocalsCache(func.body)
977
+ const r = scalarizeObjectLiterals(func.body, escapes)
978
+ if (!r.changed) break
979
+ func.body = r.node
980
+ changed = true
981
+ }
982
+ }
983
+ return changed
984
+ }