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
@@ -0,0 +1,1199 @@
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, REFS_IN_EXPR, 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 (isDeclOp(op) && 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 (isDeclOp(op) && 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 (isDeclOp(op) && 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
+
204
+ // `name`'s reference used as a bare VALUE — anywhere except as the base of `name[i]`,
205
+ // `name.prop`, or `name.method(...)`. That captures a second handle to its backing memory.
206
+ const refsAsValue = (node, name) => {
207
+ if (node === name) return true
208
+ if (!Array.isArray(node)) return false
209
+ if (node[0] === '[]' && node[1] === name) return refsAsValue(node[2], name) // name[i]: vet the index only
210
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === name) return false // name.prop / name.method()
211
+ if (node[0] === '()') return false // a call RESULT, not name itself
212
+ return node.slice(1).some(e => refsAsValue(e, name))
213
+ }
214
+
215
+ // True when `name`'s backing memory ESCAPES into a persistent alias: bound to another
216
+ // variable (`let b = name`), stored into a field or literal (`o.x = name`, `[name]`), or
217
+ // captured as a `.subarray(...)` view. Mirrored scalarization syncs scalars↔memory only
218
+ // AROUND each unsafe statement, so a write through the captured alias in a LATER statement
219
+ // never reaches `name`'s scalar slots (and vice-versa) — the array must stay memory-backed.
220
+ // A bare `name` passed only as a call ARGUMENT is transient (the callee touches it during
221
+ // the call, already covered by the surrounding sync), so it is NOT a capture.
222
+ const createsTypedArrayAlias = (node, name) => {
223
+ if (!Array.isArray(node)) return false
224
+ if (node[0] === '()' && Array.isArray(node[1]) && node[1][0] === '.'
225
+ && node[1][1] === name && node[1][2] === 'subarray') return true // zero-copy view
226
+ if (node[0] === '=' && refsAsValue(node[2], name)) return true // let b = name / x = name
227
+ if ((node[0] === '[' || node[0] === '{}') && node.slice(1).some(e => refsAsValue(e, name))) return true // [name] / {k:name}
228
+ for (let i = 1; i < node.length; i++) if (createsTypedArrayAlias(node[i], name)) return true
229
+ return false
230
+ }
231
+ const rewriteScalarTypedArrayUses = (node, arrays) => {
232
+ if (!Array.isArray(node)) return node
233
+ const op = node[0]
234
+ const slotFor = (idxNode, entry) => {
235
+ const idx = typedArraySlotIndex(idxNode, entry.len)
236
+ return idx == null ? null : entry.slots[idx]
237
+ }
238
+ if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') return [null, arrays.get(node[1]).len]
239
+ if (op === '[]' && arrays.has(node[1])) return slotFor(node[2], arrays.get(node[1])) ?? node
240
+ if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
241
+ const slot = slotFor(node[1][2], arrays.get(node[1][1]))
242
+ return slot ? [op, slot] : node
243
+ }
244
+ if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
245
+ const entry = arrays.get(node[1][1])
246
+ const slot = slotFor(node[1][2], entry)
247
+ if (!slot) return node
248
+ const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
249
+ return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
250
+ }
251
+ return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
252
+ }
253
+
254
+ const scalarTypedArrayStores = (name, entry) =>
255
+ entry.slots.map((slot, i) => ['=', ['[]', name, [null, i]], slot])
256
+
257
+ const scalarTypedArrayLoads = (name, entry) =>
258
+ entry.slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])
259
+
260
+ const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
261
+ if (!Array.isArray(node)) return out
262
+ const op = node[0]
263
+ const addSlot = target => {
264
+ if (Array.isArray(target) && target[0] === '[]' && target[1] === name) {
265
+ const idx = typedArraySlotIndex(target[2], len)
266
+ if (idx != null) out.add(idx)
267
+ return true
268
+ }
269
+ return false
270
+ }
271
+ if ((op === '++' || op === '--') && addSlot(node[1])) return out
272
+ if (ASSIGN_TARGET_OPS.has(op) && addSlot(node[1])) {
273
+ for (let i = 2; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
274
+ return out
275
+ }
276
+ if (op !== '=>') for (let i = 1; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
277
+ return out
278
+ }
279
+
280
+ const hasScalarTypedArrayRead = (node, name) => {
281
+ if (!Array.isArray(node)) return false
282
+ const op = node[0]
283
+ const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
284
+ if ((op === '++' || op === '--') && isTarget(node[1])) return true
285
+ if (ASSIGN_TARGET_OPS.has(op)) {
286
+ if (isTarget(node[1])) {
287
+ if (op !== '=') return true
288
+ for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
289
+ return false
290
+ }
291
+ }
292
+ if (op === '[]' && node[1] === name) return true
293
+ if (op === '=>') return false
294
+ for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
295
+ return false
296
+ }
297
+
298
+ const scalarizeTypedArrayLiteralSeq = (seq) => {
299
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
300
+ let changed = false
301
+ const stmts = seq.slice(1).map(stmt => {
302
+ const r = scalarizeTypedArrayLiterals(stmt)
303
+ changed ||= r.changed
304
+ return r.node
305
+ })
306
+
307
+ const candidates = new Map()
308
+ const mirrored = new Map()
309
+ for (let i = 0; i < stmts.length; i++) {
310
+ const stmt = stmts[i]
311
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
312
+ const decl = stmt[1]
313
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
314
+ const fixed = fixedScalarTypedArray(decl[2])
315
+ if (fixed == null) continue
316
+ const { len, coerce } = fixed
317
+ let hasSafeUse = false, hasUnsafeUse = false, hasAliasUse = false
318
+ for (let j = 0; j < stmts.length; j++) {
319
+ if (j === i) continue
320
+ if (!refsName(stmts[j], decl[1])) continue
321
+ const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
322
+ hasSafeUse ||= safe
323
+ hasUnsafeUse ||= !safe
324
+ hasAliasUse ||= createsTypedArrayAlias(stmts[j], decl[1])
325
+ }
326
+ if (hasAliasUse) continue // persistent aliasing view (subarray) — keep memory-backed
327
+ if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
328
+ if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
329
+ else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
330
+ }
331
+ if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
332
+
333
+ const arrays = new Map()
334
+ for (const [name, c] of [...candidates, ...mirrored]) {
335
+ const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
336
+ arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
337
+ }
338
+
339
+ const out = []
340
+ for (let i = 0; i < stmts.length; i++) {
341
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
342
+ [...mirrored.entries()].find(([, c]) => c.index === i)
343
+ if (entry) {
344
+ const [name] = entry
345
+ const arr = arrays.get(name)
346
+ const { slots } = arr
347
+ if (arr.mirrored) {
348
+ out.push(stmts[i])
349
+ if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
350
+ } else if (slots.length) {
351
+ out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
352
+ }
353
+ changed = true
354
+ continue
355
+ }
356
+ const unsafe = []
357
+ for (const [name, arr] of arrays) {
358
+ if (arr.mirrored && refsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
359
+ }
360
+ if (unsafe.length) {
361
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
362
+ out.push(stmts[i])
363
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
364
+ changed = true
365
+ } else {
366
+ out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
367
+ }
368
+ }
369
+ return { node: [';', ...out], changed: true }
370
+ }
371
+
372
+ function scalarizeTypedArrayLiterals(node) {
373
+ if (!Array.isArray(node)) return { node, changed: false }
374
+ if (node[0] === '=>') return { node, changed: false }
375
+ if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
376
+ let changed = false
377
+ const out = [node[0]]
378
+ for (let i = 1; i < node.length; i++) {
379
+ const r = scalarizeTypedArrayLiterals(node[i])
380
+ changed ||= r.changed
381
+ out.push(r.node)
382
+ }
383
+ return changed ? { node: out, changed: true } : { node, changed: false }
384
+ }
385
+
386
+ const containsTypedArrayAccess = (body, names) => some(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
387
+
388
+ function smallScalarTypedForTrip(init, cond, step) {
389
+ const end = smallConstForTripCount(init, cond, step, maxScalarTypedLoopUnroll())
390
+ if (end == null) return null
391
+ const decl = init[1]
392
+ return { name: decl[1], end }
393
+ }
394
+
395
+ const scalarTypedLoopBudget = (body) => {
396
+ if (!Array.isArray(body) || body[0] === '=>') return 1
397
+ if (body[0] === 'for') {
398
+ const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
399
+ return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
400
+ }
401
+ let max = 1
402
+ for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
403
+ return max
404
+ }
405
+
406
+ const unrollTypedArrayLoops = (node, names) => {
407
+ if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
408
+ if (node[0] === ';') {
409
+ let changed = false
410
+ const out = [';']
411
+ for (const stmt of node.slice(1)) {
412
+ const r = unrollTypedArrayLoops(stmt, names)
413
+ changed ||= r.changed
414
+ if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
415
+ else out.push(r.node)
416
+ }
417
+ return changed ? { node: out, changed: true } : { node, changed: false }
418
+ }
419
+ if (node[0] === '{}') {
420
+ const r = unrollTypedArrayLoops(node[1], names)
421
+ return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
422
+ }
423
+ if (node[0] === 'for') {
424
+ const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
425
+ if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
426
+ !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
427
+ const out = [';']
428
+ for (let i = 0; i < trip.end; i++) {
429
+ const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
430
+ const r = unrollTypedArrayLoops(cloned, names)
431
+ out.push(...stmtList(r.node))
432
+ }
433
+ return { node: out, changed: true }
434
+ }
435
+ }
436
+ let changed = false
437
+ const out = [node[0]]
438
+ for (let i = 1; i < node.length; i++) {
439
+ const r = unrollTypedArrayLoops(node[i], names)
440
+ changed ||= r.changed
441
+ out.push(r.node)
442
+ }
443
+ return changed ? { node: out, changed: true } : { node, changed: false }
444
+ }
445
+
446
+ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
447
+ if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
448
+ if (some(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
449
+ const params = func.sig?.params || []
450
+ const cands = new Map()
451
+ for (let i = 0; i < params.length; i++) {
452
+ const pname = params[i].name
453
+ let len = null, coerce = null, ok = true
454
+ for (const site of sites) {
455
+ const arg = site.argList[i]
456
+ const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
457
+ if (!fixed) { ok = false; break }
458
+ if (len == null) { len = fixed.len; coerce = fixed.coerce }
459
+ else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
460
+ }
461
+ if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
462
+ }
463
+ if (!cands.size) return cands
464
+ for (const site of sites) {
465
+ const seen = new Set()
466
+ for (let i = 0; i < params.length; i++) {
467
+ if (!cands.has(params[i].name)) continue
468
+ const arg = site.argList[i]
469
+ if (typeof arg !== 'string' || seen.has(arg)) return new Map()
470
+ seen.add(arg)
471
+ }
472
+ }
473
+ return cands
474
+ }
475
+
476
+ const scalarizeTypedArrayParams = (func, paramCands) => {
477
+ for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
478
+ for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
479
+ if (!paramCands.size) return { body: func.body, changed: false }
480
+ const arrays = new Map()
481
+ for (const [name, c] of paramCands) {
482
+ arrays.set(name, {
483
+ len: c.len,
484
+ coerce: c.coerce,
485
+ slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
486
+ })
487
+ }
488
+ const prologue = []
489
+ const writeback = []
490
+ for (const [name, { len, slots }] of arrays) {
491
+ if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
492
+ for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
493
+ }
494
+ const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
495
+ return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
496
+ }
497
+
498
+ export const scalarizeFunctionTypedArrays = (programFacts) => {
499
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
500
+ const sitesByCallee = new Map()
501
+ for (const site of programFacts.callSites) {
502
+ if (!site.callerFunc) continue
503
+ const list = sitesByCallee.get(site.callee)
504
+ if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
505
+ }
506
+ let changed = false
507
+ for (const func of ctx.func.list) {
508
+ if (!func.body || func.raw) continue
509
+ const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
510
+ const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
511
+ if (names.size) {
512
+ let guard = 0
513
+ while (guard++ < 6) {
514
+ const r = unrollTypedArrayLoops(func.body, names)
515
+ if (!r.changed) break
516
+ func.body = r.node
517
+ changed = true
518
+ }
519
+ }
520
+ const p = scalarizeTypedArrayParams(func, paramCands)
521
+ if (p.changed) { func.body = p.body; changed = true }
522
+ const l = scalarizeTypedArrayLiterals(func.body)
523
+ if (l.changed) { func.body = l.node; changed = true }
524
+ if (changed) invalidateLocalsCache(func.body)
525
+ }
526
+ return changed
527
+ }
528
+
529
+ const scalarizeArrayLiteralSeq = (seq) => {
530
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
531
+ let changed = false
532
+ const stmts = seq.slice(1).map(stmt => {
533
+ const r = scalarizeArrayLiterals(stmt)
534
+ changed ||= r.changed
535
+ return r.node
536
+ })
537
+
538
+ const candidates = new Map()
539
+ for (let i = 0; i < stmts.length; i++) {
540
+ const stmt = stmts[i]
541
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
542
+ const decl = stmt[1]
543
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
544
+ const elems = scalarArrayElems(decl[2])
545
+ if (!elems) continue
546
+ let ok = true
547
+ for (let j = 0; j < stmts.length && ok; j++) {
548
+ if (j === i) continue
549
+ ok = safeScalarArrayUse(stmts[j], decl[1], elems.length)
550
+ }
551
+ if (!ok) continue
552
+ candidates.set(decl[1], { index: i, op: stmt[0], elems })
553
+ }
554
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
555
+
556
+ const arrays = new Map()
557
+ for (const [name, c] of candidates) {
558
+ const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
559
+ arrays.set(name, temps)
560
+ }
561
+
562
+ const out = []
563
+ for (let i = 0; i < stmts.length; i++) {
564
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
565
+ if (entry) {
566
+ const [name, c] = entry
567
+ const temps = arrays.get(name)
568
+ if (temps.length) {
569
+ out.push([c.op, ...temps.map((tmp, k) =>
570
+ ['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
571
+ }
572
+ changed = true
573
+ continue
574
+ }
575
+ out.push(rewriteScalarArrayUses(stmts[i], arrays))
576
+ }
577
+ return { node: [';', ...out], changed: true }
578
+ }
579
+
580
+ const scalarizeObjectLiteralSeq = (seq, escapes) => {
581
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
582
+ let changed = false
583
+ const stmts = seq.slice(1).map(stmt => {
584
+ const r = scalarizeObjectLiterals(stmt, escapes)
585
+ changed ||= r.changed
586
+ return r.node
587
+ })
588
+
589
+ const candidates = new Map()
590
+ for (let i = 0; i < stmts.length; i++) {
591
+ const stmt = stmts[i]
592
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
593
+ const decl = stmt[1]
594
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
595
+ if (escapes.get(decl[1]) !== false) continue
596
+ const props = scalarObjectProps(decl[2])
597
+ if (!props) continue
598
+ const keys = new Set(props.names)
599
+ let ok = true
600
+ for (let j = 0; j < stmts.length && ok; j++) {
601
+ if (j === i) continue
602
+ ok = safeScalarObjectUse(stmts[j], decl[1], keys)
603
+ }
604
+ if (!ok) continue
605
+ candidates.set(decl[1], { index: i, op: stmt[0], props })
606
+ }
607
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
608
+
609
+ const objects = new Map()
610
+ for (const [name, c] of candidates) {
611
+ const fields = new Map()
612
+ for (let i = 0; i < c.props.names.length; i++) {
613
+ fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
614
+ }
615
+ objects.set(name, fields)
616
+ }
617
+
618
+ const out = []
619
+ for (let i = 0; i < stmts.length; i++) {
620
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
621
+ if (entry) {
622
+ const [, c] = entry
623
+ const fields = objects.get(entry[0])
624
+ if (c.props.names.length) {
625
+ out.push([c.op, ...c.props.names.map((prop, k) =>
626
+ ['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
627
+ }
628
+ changed = true
629
+ continue
630
+ }
631
+ out.push(rewriteScalarObjectUses(stmts[i], objects))
632
+ }
633
+ return { node: [';', ...out], changed: true }
634
+ }
635
+
636
+ function scalarizeObjectLiterals(node, escapes) {
637
+ if (!Array.isArray(node)) return { node, changed: false }
638
+ if (node[0] === '=>') {
639
+ const r = scalarizeObjectLiterals(node[2], escapes)
640
+ if (!r.changed) return { node, changed: false }
641
+ return { node: [node[0], node[1], r.node], changed: true }
642
+ }
643
+ if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
644
+ let changed = false
645
+ const out = [node[0]]
646
+ for (let i = 1; i < node.length; i++) {
647
+ const r = scalarizeObjectLiterals(node[i], escapes)
648
+ changed ||= r.changed
649
+ out.push(r.node)
650
+ }
651
+ return changed ? { node: out, changed: true } : { node, changed: false }
652
+ }
653
+
654
+ // === Whole-program constant fold of module-scope aggregate literals ===
655
+ //
656
+ // `var x = [1,2,3]; export const y = x[0]` materializes a data-segment array and
657
+ // indexes it through __arr_idx_known (bounds + grow-forwarding) — all dead weight
658
+ // for a never-grown constant. When EVERY reference to a module-scope const aggregate
659
+ // is a static READ, replace each `x[k]` / `x.length` / `o.key` by its literal value
660
+ // program-wide; the now-unused decl is dropped, so no array is built (no data, no
661
+ // memory, no index helper) and the global is never declared. The per-function
662
+ // scalarizers can't do this: the decl lives at module scope and may be read from
663
+ // several function bodies, so the check must span the whole program.
664
+
665
+ const ASSIGN_OR_UPDATE = (op) => ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--'
666
+ // Module-scope binding ops. `var` survives to compile at module scope (jzify only
667
+ // lowers it to `let` inside functions), so fold it too — the reassignment guard
668
+ // below keeps a re-bound `var` heap-backed.
669
+ const isDeclOp = (op) => op === 'let' || op === 'const' || op === 'var'
670
+
671
+ // Reject `delete x`, `delete x.k`, `delete x[k]` — a deletion mutates the aggregate.
672
+ const isDeleteOf = (node, name) =>
673
+ node[0] === 'delete' && (node[1] === name || (Array.isArray(node[1]) && node[1][1] === name))
674
+
675
+ // A reference is fold-safe only if it READS `name` via a static index/key (or
676
+ // `.length` for arrays). Any write/update/delete, reassignment, second declaration,
677
+ // bare value use (escape), spread, dynamic key, or non-own member (method / proto
678
+ // chain) escapes the literal model and disqualifies the binding.
679
+ const foldSafeArrayUse = (node, name, len) => {
680
+ if (typeof node === 'string') return node !== name
681
+ if (!Array.isArray(node)) return true
682
+ const op = node[0]
683
+ if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
684
+ if (isDeleteOf(node, name)) return false
685
+ if (ASSIGN_OR_UPDATE(op)) {
686
+ const t = node[1]
687
+ if (t === name) return false
688
+ if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
689
+ }
690
+ if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
691
+ if (op === '[]' && node[1] === name) return constIntExpr(node[2]) != null
692
+ if (op === '...' && node[1] === name) return false
693
+ for (let i = 1; i < node.length; i++) if (!foldSafeArrayUse(node[i], name, len)) return false
694
+ return true
695
+ }
696
+
697
+ const foldSafeObjectUse = (node, name, keys) => {
698
+ if (typeof node === 'string') return node !== name
699
+ if (!Array.isArray(node)) return true
700
+ const op = node[0]
701
+ if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
702
+ if (isDeleteOf(node, name)) return false
703
+ if (ASSIGN_OR_UPDATE(op)) {
704
+ const t = node[1]
705
+ if (t === name) return false
706
+ if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
707
+ }
708
+ if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2]) // own key only — proto-safe
709
+ if (op === '[]' && node[1] === name) { const k = staticPropertyKey(node[2]); return k != null && keys.has(k) }
710
+ if (op === '...' && node[1] === name) return false
711
+ for (let i = 1; i < node.length; i++) if (!foldSafeObjectUse(node[i], name, keys)) return false
712
+ return true
713
+ }
714
+
715
+ const moduleStmtsOf = (seq) =>
716
+ Array.isArray(seq) && seq[0] === ';' ? seq.slice(1)
717
+ : Array.isArray(seq) && isDeclOp(seq[0]) ? [seq]
718
+ : []
719
+
720
+ export function foldStaticConstAggregates(ast) {
721
+ // Span the main module AST and every bundled sub-module init — a const declared in
722
+ // one can be read from another or from a function body (mirrors the constInts fold).
723
+ const seqs = [ast, ...(ctx.module.moduleInits || [])]
724
+ const moduleStmts = seqs.flatMap(moduleStmtsOf)
725
+ const funcs = ctx.func.list.filter(f => f.body && !f.raw)
726
+ // A function parameter named `x` rebinds `x` for the whole body — its `x[…]` reads
727
+ // the param, not the module binding (params live on `f.sig`, separate from `.body`,
728
+ // so the body scan can't see them). Such a function is skipped (scan) / excluded
729
+ // (rewrite) for that name.
730
+ const paramNames = (f) => (f.sig?.params || []).map(p => p.name)
731
+ // Every AST a function can reference an outer binding from: its body PLUS each
732
+ // default-parameter expression (`(v = x[0]) => …`), which prepare extracts to
733
+ // `f.defaults` — separate from the body, so the body scan/rewrite would miss it.
734
+ const funcNodes = (f) => f.defaults ? [f.body, ...Object.values(f.defaults)] : [f.body]
735
+
736
+ // Classify module statements. A binding's value comes from an inline decl
737
+ // (`const x = […]`) OR — for `var`, which jzify lowers to `let x; x = […]` — a
738
+ // lone module-scope assignment after an uninitialized decl. The init statement(s)
739
+ // are excluded from the read-only scan and dropped on fold.
740
+ const inlineInit = new Map() // name -> {value, stmt} | null (poisoned: >1 decl)
741
+ const assigns = new Map() // name -> [assignStmt…]
742
+ const uninitDecl = new Map() // name -> declStmt (`let x` / `var x`, string declarator)
743
+ for (const stmt of moduleStmts) {
744
+ if (!Array.isArray(stmt)) continue
745
+ const op = stmt[0]
746
+ if (isDeclOp(op) && stmt.length === 2 && Array.isArray(stmt[1]) && stmt[1][0] === '=' && typeof stmt[1][1] === 'string') {
747
+ const name = stmt[1][1]
748
+ inlineInit.set(name, inlineInit.has(name) ? null : { value: stmt[1][2], stmt })
749
+ } else if (isDeclOp(op) && stmt.length === 2 && typeof stmt[1] === 'string') {
750
+ uninitDecl.set(stmt[1], stmt)
751
+ } else if (op === '=' && typeof stmt[1] === 'string') {
752
+ (assigns.get(stmt[1]) ?? assigns.set(stmt[1], []).get(stmt[1])).push(stmt)
753
+ }
754
+ }
755
+
756
+ // index of each statement in module-execution order — used to prove a `var`'s
757
+ // assignment dominates its reads.
758
+ const pos = new Map()
759
+ moduleStmts.forEach((s, i) => pos.set(s, i))
760
+
761
+ const arr = new Map(), obj = new Map(), initStmts = new Map()
762
+ const consider = (name, value, init) => {
763
+ if (ctx.func.exports?.[name]) return // exported → escapes to JS
764
+ const elems = scalarArrayElems(value)
765
+ if (elems) { arr.set(name, elems); initStmts.set(name, init); return }
766
+ const props = scalarObjectProps(value)
767
+ if (props) { obj.set(name, props); initStmts.set(name, init) }
768
+ }
769
+ for (const [name, info] of inlineInit) {
770
+ // a decl-initialized binding that is ALSO assigned is reassigned → not constant.
771
+ if (info && !assigns.has(name)) consider(name, info.value, new Set([info.stmt]))
772
+ }
773
+ for (const [name, list] of assigns) {
774
+ // `var` lowering: exactly one assignment, a matching uninit decl, and no
775
+ // competing inline decl. The assignment must dominate every read — conservatively,
776
+ // it precedes all other module references and the name is unused in any function
777
+ // body (a function could run before the assignment). `let`/`const` need no such
778
+ // guard (TDZ forbids use-before-init).
779
+ if (inlineInit.has(name) || list.length !== 1 || !uninitDecl.has(name)) continue
780
+ const assign = list[0], at = pos.get(assign)
781
+ const refsBefore = moduleStmts.some((s, i) => i < at && s !== uninitDecl.get(name) && refsName(s, name, REFS_IN_EXPR))
782
+ const refsInFn = funcs.some(f => !paramNames(f).includes(name) && funcNodes(f).some(n => refsName(n, name, REFS_IN_EXPR)))
783
+ if (refsBefore || refsInFn) continue
784
+ consider(name, assign[2], new Set([assign, uninitDecl.get(name)]))
785
+ }
786
+ if (!arr.size && !obj.size) return false
787
+
788
+ // Every mention outside the binding's init statement(s) — across all module
789
+ // statements and all function bodies — must be a fold-safe static read.
790
+ const checkAll = (name, pred) => {
791
+ const skip = initStmts.get(name)
792
+ return moduleStmts.every(s => skip.has(s) || pred(s))
793
+ && funcs.every(f => paramNames(f).includes(name) || funcNodes(f).every(pred))
794
+ }
795
+ for (const [name, elems] of [...arr]) if (!checkAll(name, n => foldSafeArrayUse(n, name, elems.length))) arr.delete(name)
796
+ for (const [name, props] of [...obj]) {
797
+ const keys = new Set(props.names)
798
+ if (!checkAll(name, n => foldSafeObjectUse(n, name, keys))) obj.delete(name)
799
+ }
800
+ if (!arr.size && !obj.size) return false
801
+
802
+ const objects = new Map()
803
+ for (const [name, props] of obj) {
804
+ const fields = new Map()
805
+ props.names.forEach((k, i) => fields.set(k, props.values[i]))
806
+ objects.set(name, fields)
807
+ }
808
+ const rewrite = (n) => rewriteScalarObjectUses(rewriteScalarArrayUses(n, arr), objects)
809
+ // Every init statement (the decl and, for `var`, its lone assignment) is dropped —
810
+ // nothing reads the binding anymore, so no aggregate is built.
811
+ const dropped = new Set()
812
+ for (const name of [...arr.keys(), ...obj.keys()]) for (const s of initStmts.get(name)) dropped.add(s)
813
+
814
+ // Rewrite + drop init statements in each module sequence (mutate in place so
815
+ // callers holding `ast`/moduleInits references see the result).
816
+ for (const seq of seqs) {
817
+ if (!Array.isArray(seq) || seq[0] !== ';') continue
818
+ const kept = []
819
+ for (const stmt of seq.slice(1)) {
820
+ if (dropped.has(stmt)) continue
821
+ kept.push(rewrite(stmt))
822
+ }
823
+ seq.splice(1, seq.length - 1, ...kept)
824
+ }
825
+ // Rewrite each function body AND its default-parameter expressions, excluding the
826
+ // folded names its params shadow.
827
+ for (const f of funcs) {
828
+ const pn = paramNames(f)
829
+ const shadows = pn.some(p => arr.has(p) || objects.has(p))
830
+ const rw = shadows
831
+ ? (n) => rewriteScalarObjectUses(rewriteScalarArrayUses(n, new Map([...arr].filter(([k]) => !pn.includes(k)))), new Map([...objects].filter(([k]) => !pn.includes(k))))
832
+ : rewrite
833
+ f.body = rw(f.body)
834
+ if (f.defaults) for (const k of Object.keys(f.defaults)) f.defaults[k] = rw(f.defaults[k])
835
+ }
836
+ return true
837
+ }
838
+
839
+ function scalarizeArrayLiterals(node) {
840
+ if (!Array.isArray(node)) return { node, changed: false }
841
+ if (node[0] === '=>') {
842
+ const r = scalarizeArrayLiterals(node[2])
843
+ if (!r.changed) return { node, changed: false }
844
+ return { node: [node[0], node[1], r.node], changed: true }
845
+ }
846
+ if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
847
+ let changed = false
848
+ const out = [node[0]]
849
+ for (let i = 1; i < node.length; i++) {
850
+ const r = scalarizeArrayLiterals(node[i])
851
+ changed ||= r.changed
852
+ out.push(r.node)
853
+ }
854
+ return changed ? { node: out, changed: true } : { node, changed: false }
855
+ }
856
+
857
+ export const scalarizeFunctionArrayLiterals = () => {
858
+ let changed = false
859
+ for (const func of ctx.func.list) {
860
+ if (!func.body || func.raw) continue
861
+ let guard = 0
862
+ while (guard++ < 4) {
863
+ const r = scalarizeArrayLiterals(func.body)
864
+ if (!r.changed) break
865
+ func.body = r.node
866
+ changed = true
867
+ }
868
+ }
869
+ return changed
870
+ }
871
+
872
+
873
+
874
+ // === Int-array → Int32Array auto-promotion =================================
875
+ //
876
+ // A `let xs = [intLit, intLit, …]` binding whose every use is TYPED-compatible
877
+ // is rewritten to `let xs = new Int32Array([intLit, …])`. Downstream analysis
878
+ // then takes over: valTypeOf → VAL.TYPED, methods dispatch through `.typed:`,
879
+ // loops get auto-vectorized via i32x4 (SIMD pass), and the carrier shrinks
880
+ // from 8-byte f64 slots to packed i32. The promotion runs after literal
881
+ // scalarization (so arrays fully scalarized away are already gone) and before
882
+ // typed-array param scalarization (so a freshly-promoted array can still
883
+ // participate in subsequent loop unrolling).
884
+ //
885
+ // Safety: the binding must never appear in a pattern that TYPED can't honor.
886
+ // Disqualifiers (each fires per binding name):
887
+ // 1. reassignment `xs = …` / compound `xs +=` / `++xs` / `--xs` — TYPED has
888
+ // no value-replacement op; `xs = new TypedArray(…)` would also drop the
889
+ // promoted view's identity.
890
+ // 2. element write `xs[k] = v` — TYPED's i32-trunc store would lose
891
+ // fractional/NaN bits that VAL.ARRAY would have preserved.
892
+ // 3. method calls outside the read-safe whitelist (push, pop, shift, …) —
893
+ // TYPED arrays are fixed-length; mutators don't exist on the carrier.
894
+ // 4. `Array.isArray(xs)` — semantics flip true→false on promotion.
895
+ // 5. `…xs` spread / `xs` as call arg / `xs` as return / bare reference in
896
+ // any other position — escape; callee may rely on ARRAY layout.
897
+ // 6. captured by a closure / shadowed by an inner decl — same escape
898
+ // reasoning, plus the inner decl could rebind to a non-array.
899
+ //
900
+ // Elements at init must all be i32-range integer literals. A negative literal
901
+ // arrives as `[null, -n]` after prepare's constant folding; intLiteralValue
902
+ // recognizes that form. Float literals (`[1, 2.5]`) and out-of-range ints
903
+ // (`0x80000000` on the +ve side, etc.) disqualify the array as a whole.
904
+
905
+ // Methods we promote across. The bar: every entry must have a real `.typed:*`
906
+ // emitter in module/typedarray.js. Receiver-flow into chained methods is also
907
+ // typed-aware now — `.typed:map`/`.typed:filter`/`.typed:slice` all return
908
+ // TYPED carriers, and downstream `.filter`/`.slice`/`.map`/etc. on those
909
+ // re-dispatch via emit.js:2211's `.typed:<m>` lookup (VAL.TYPED ⇒ typed
910
+ // emitter). Methods missing here (.join, .sort, .reverse, .subarray, .fill,
911
+ // .toString, .copyWithin, …) lack a typed emitter — disqualify the candidate.
912
+ const _TYPED_SAFE_METHODS = new Set([
913
+ 'set',
914
+ 'map', 'filter', 'slice',
915
+ 'forEach', 'reduce',
916
+ 'indexOf', 'includes', 'find', 'findIndex', 'some', 'every',
917
+ ])
918
+
919
+ // `.length` is TYPED-aware via core.js:__len (shifts the byte header by
920
+ // __typed_shift on TAG=3). `.byteLength`/`.byteOffset`/`.buffer` are
921
+ // TYPED-only — a user reading those already expects a typed array, so a
922
+ // promotion candidate that hits them is a coincidence we'd rather not
923
+ // rely on; disqualify and let them write the TypedArray construction
924
+ // themselves.
925
+ const _TYPED_SAFE_PROPS = new Set(['length'])
926
+
927
+ // Returns the i32-range integer payload of an array-literal element, or null
928
+ // if the element isn't a literal integer that fits in i32. Mirrors the shape
929
+ // check used by `intLiteralValue` but without the rep-lookup (we're pre-
930
+ // analysis here, and want a pure syntactic gate).
931
+ const _intArrayLitElems = (expr) => {
932
+ if (!Array.isArray(expr) || expr[0] !== '[') return null
933
+ if (expr.length < 2) return null // empty literal — low value, skip
934
+ const out = []
935
+ for (let i = 1; i < expr.length; i++) {
936
+ const v = intLiteralValue(expr[i])
937
+ if (v == null) return null
938
+ out.push(v)
939
+ }
940
+ return out
941
+ }
942
+
943
+ // Walks `node` and disqualifies every candidate name that appears in an
944
+ // unsafe context. `initSet` holds the candidate's own init-decl AST nodes
945
+ // (their LHS reference is the binding being defined, not an escape).
946
+ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
947
+ if (initSet.has(node)) {
948
+ // The init decl itself: only walk the RHS (skip the LHS `name`).
949
+ return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
950
+ }
951
+ if (typeof node === 'string') {
952
+ // Bare identifier outside any handled parent context — escape.
953
+ if (candidates.has(node)) disqualified.add(node)
954
+ return
955
+ }
956
+ if (!Array.isArray(node)) return
957
+ const op = node[0]
958
+
959
+ // Closure body — any candidate referenced inside is captured. Bail without
960
+ // recursing further (we'd otherwise hit the bare-name leaf and disqualify
961
+ // anyway, but this is explicit and avoids walking the inner closure).
962
+ if (op === '=>') {
963
+ for (const n of candidates.keys()) {
964
+ if (!disqualified.has(n) && refsName(node, n, { skipArrow: false })) disqualified.add(n)
965
+ }
966
+ return
967
+ }
968
+
969
+ // Member write target `name.length = n` / `++name.length` — resize is an
970
+ // ARRAY-only op (TYPED is fixed-size); disqualify.
971
+ if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') &&
972
+ Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') &&
973
+ typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
974
+ disqualified.add(node[1][1])
975
+ for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
976
+ return
977
+ }
978
+
979
+ // Property access `name.prop` / `name?.prop`. Bare property reads only —
980
+ // method calls reach here via the `()` handler below (which intercepts
981
+ // before recursing into its callee).
982
+ if ((op === '.' || op === '?.') && typeof node[1] === 'string' && candidates.has(node[1])) {
983
+ if (!_TYPED_SAFE_PROPS.has(node[2])) disqualified.add(node[1])
984
+ return // node[2] is the property name (string), not an expression — done
985
+ }
986
+
987
+ // Method or function call. Two shapes carry the candidate:
988
+ // `name.method(args)` — receiver at node[1][1]; method whitelist gates.
989
+ // `f(…, name, …)` / `Array.isArray(name)` — name appears as a plain arg.
990
+ if (op === '()') {
991
+ const callee = node[1]
992
+ // Method call on a candidate receiver.
993
+ if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
994
+ typeof callee[1] === 'string' && candidates.has(callee[1])) {
995
+ if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
996
+ // Walk method args (skip the receiver — already validated above).
997
+ for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
998
+ return
999
+ }
1000
+ // Array.isArray flips true→false under promotion.
1001
+ if (callee === 'Array.isArray') {
1002
+ const raw = node[2]
1003
+ const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
1004
+ for (const a of list) {
1005
+ if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
1006
+ else _disqualifyPromotion(a, candidates, disqualified, initSet)
1007
+ }
1008
+ return
1009
+ }
1010
+ // Fall through to generic recursion — `name` as a plain arg will hit the
1011
+ // bare-name leaf above and disqualify.
1012
+ }
1013
+
1014
+ // Index read `name[k]` — read access is TYPED-safe. Walk the key in case
1015
+ // it contains references to other candidate names.
1016
+ if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
1017
+ _disqualifyPromotion(node[2], candidates, disqualified, initSet)
1018
+ return
1019
+ }
1020
+
1021
+ // Element write: `['=', ['[]', name, k], v]` (and compound forms).
1022
+ // V1 stays read-only after init — element writes would silently truncate.
1023
+ if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
1024
+ typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
1025
+ disqualified.add(node[1][1])
1026
+ _disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
1027
+ _disqualifyPromotion(node[2], candidates, disqualified, initSet)
1028
+ return
1029
+ }
1030
+
1031
+ // Whole-binding reassign: `name = …` / `name += …` / etc.
1032
+ if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
1033
+ disqualified.add(node[1])
1034
+ _disqualifyPromotion(node[2], candidates, disqualified, initSet)
1035
+ return
1036
+ }
1037
+
1038
+ // Pre/post-increment on var or element.
1039
+ if (op === '++' || op === '--') {
1040
+ const t = node[1]
1041
+ if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
1042
+ if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
1043
+ disqualified.add(t[1])
1044
+ _disqualifyPromotion(t[2], candidates, disqualified, initSet)
1045
+ return
1046
+ }
1047
+ }
1048
+
1049
+ // `let`/`const` declaration — the candidate's own init lands here via the
1050
+ // initSet branch at the top. Any *other* decl that names a candidate is a
1051
+ // shadow (impossible after jz hoists, but defensive) and disqualifies.
1052
+ if (op === 'let' || op === 'const') {
1053
+ for (let i = 1; i < node.length; i++) {
1054
+ const d = node[i]
1055
+ if (typeof d === 'string' && candidates.has(d)) disqualified.add(d)
1056
+ else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
1057
+ candidates.has(d[1]) && !initSet.has(d)) {
1058
+ disqualified.add(d[1])
1059
+ _disqualifyPromotion(d[2], candidates, disqualified, initSet)
1060
+ } else {
1061
+ _disqualifyPromotion(d, candidates, disqualified, initSet)
1062
+ }
1063
+ }
1064
+ return
1065
+ }
1066
+
1067
+ // Spread `…name` — could be in a call, a `[`, or a destructure target.
1068
+ if (op === '...' && typeof node[1] === 'string' && candidates.has(node[1])) {
1069
+ disqualified.add(node[1])
1070
+ return
1071
+ }
1072
+
1073
+ // for-of / for-in iteration: receiver position is `node[2]` (a bare name
1074
+ // there would otherwise trigger escape). TYPED supports iteration, so
1075
+ // allow the receiver but walk the body for other refs.
1076
+ if (op === 'for-of' || op === 'for-in') {
1077
+ // Walk decl (node[1]), iter (node[2]), body (node[3]); receiver as bare
1078
+ // name is fine — only the body matters for further refs to the same name
1079
+ // (but body refs would shadow or escape, which other rules catch).
1080
+ for (let i = 1; i < node.length; i++) {
1081
+ const child = node[i]
1082
+ if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
1083
+ _disqualifyPromotion(child, candidates, disqualified, initSet)
1084
+ }
1085
+ return
1086
+ }
1087
+
1088
+ // Generic — recurse into children. Bare-name refs at unhandled positions
1089
+ // hit the string-leaf branch above and disqualify on contact.
1090
+ for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
1091
+ }
1092
+
1093
+ // Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
1094
+ // carries the exact init-decl AST node so the disqualifier can skip the
1095
+ // binding's own LHS reference (which would otherwise look like a reassign).
1096
+ const _collectIntArrayCandidates = (node, candidates) => {
1097
+ if (!Array.isArray(node)) return
1098
+ const op = node[0]
1099
+ if (op === '=>') return
1100
+ if (op === 'let' || op === 'const') {
1101
+ for (let i = 1; i < node.length; i++) {
1102
+ const d = node[i]
1103
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
1104
+ const elems = _intArrayLitElems(d[2])
1105
+ if (elems == null) continue
1106
+ // jz hoists `let` to function scope — duplicate candidate-name collisions
1107
+ // shouldn't happen, but if they do the second wins (disqualifyPromotion
1108
+ // will mark both via the shadow rule).
1109
+ candidates.set(d[1], { initDecl: d, elems })
1110
+ }
1111
+ }
1112
+ for (let i = 1; i < node.length; i++) _collectIntArrayCandidates(node[i], candidates)
1113
+ }
1114
+
1115
+ // Rewrite `let name = [...]` → `let name = new Int32Array([...])` for every
1116
+ // validated candidate. Preserves the original element AST nodes so the
1117
+ // downstream `Int32Array.from` lowering picks up its existing static-data
1118
+ // segment / per-element-store fast paths.
1119
+ const _rewritePromoted = (node, validated, initSet) => {
1120
+ if (!Array.isArray(node)) return { node, changed: false }
1121
+ const op = node[0]
1122
+ if (op === '=>') {
1123
+ // Closures don't reach validated bindings (we disqualified on capture).
1124
+ // Still recurse — a nested closure may itself hold a promotable.
1125
+ let changed = false
1126
+ const out = [op]
1127
+ for (let i = 1; i < node.length; i++) {
1128
+ const r = _rewritePromoted(node[i], validated, initSet)
1129
+ if (r.changed) changed = true
1130
+ out.push(r.node)
1131
+ }
1132
+ return changed ? { node: out, changed: true } : { node, changed: false }
1133
+ }
1134
+ let changed = false
1135
+ const out = [op]
1136
+ for (let i = 1; i < node.length; i++) {
1137
+ const child = node[i]
1138
+ if ((op === 'let' || op === 'const') && Array.isArray(child) && child[0] === '=' &&
1139
+ typeof child[1] === 'string' && validated.has(child[1]) && initSet.has(child)) {
1140
+ const newRhs = ['()', 'new.Int32Array', child[2]]
1141
+ out.push(['=', child[1], newRhs])
1142
+ changed = true
1143
+ continue
1144
+ }
1145
+ const r = _rewritePromoted(child, validated, initSet)
1146
+ if (r.changed) changed = true
1147
+ out.push(r.node)
1148
+ }
1149
+ return changed ? { node: out, changed: true } : { node, changed: false }
1150
+ }
1151
+
1152
+ const promoteIntArrayLiteralsInBody = (body) => {
1153
+ const candidates = new Map()
1154
+ _collectIntArrayCandidates(body, candidates)
1155
+ if (!candidates.size) return { node: body, changed: false }
1156
+ const initSet = new Set()
1157
+ for (const { initDecl } of candidates.values()) initSet.add(initDecl)
1158
+ const disqualified = new Set()
1159
+ _disqualifyPromotion(body, candidates, disqualified, initSet)
1160
+ const validated = new Set()
1161
+ for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
1162
+ if (!validated.size) return { node: body, changed: false }
1163
+ return _rewritePromoted(body, validated, initSet)
1164
+ }
1165
+
1166
+ // On the first successful promotion, pull in the typedarray module so the
1167
+ // emitted `new.Int32Array` callee has a registered emitter. Calling
1168
+ // `includeModule('typedarray')` on a no-op (already loaded) is cheap.
1169
+ export const promoteIntArrayLiterals = () => {
1170
+ let changed = false
1171
+ for (const func of ctx.func.list) {
1172
+ if (!func.body || func.raw) continue
1173
+ const r = promoteIntArrayLiteralsInBody(func.body)
1174
+ if (r.changed) {
1175
+ func.body = r.node
1176
+ invalidateLocalsCache(func.body)
1177
+ if (!changed) includeModule('typedarray')
1178
+ changed = true
1179
+ }
1180
+ }
1181
+ return changed
1182
+ }
1183
+
1184
+ export const scalarizeFunctionObjectLiterals = () => {
1185
+ let changed = false
1186
+ for (const func of ctx.func.list) {
1187
+ if (!func.body || func.raw) continue
1188
+ let guard = 0
1189
+ while (guard++ < 4) {
1190
+ const escapes = new Map(analyzeBody(func.body).escapes)
1191
+ invalidateLocalsCache(func.body)
1192
+ const r = scalarizeObjectLiterals(func.body, escapes)
1193
+ if (!r.changed) break
1194
+ func.body = r.node
1195
+ changed = true
1196
+ }
1197
+ }
1198
+ return changed
1199
+ }