jz 0.1.1 → 0.2.1

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.
package/src/plan.js CHANGED
@@ -1,10 +1,1168 @@
1
- /** Pre-emit compile planning: collect facts, resolve ABIs, and run narrowing. */
1
+ /**
2
+ * Pre-emit compile planning: bridges prepare (AST shape) and emit (wasm bytes).
3
+ *
4
+ * # Stage contract
5
+ * IN: populated `ctx` from prepare.js (functions, schemas, scopes, modules)
6
+ * plus the prepared AST.
7
+ * OUT: returns a `programFacts` object; mutates `ctx` so each function has
8
+ * narrowed signatures, finalized global reps, and per-call decisions.
9
+ *
10
+ * # Pipeline (top-level `plan(ast)`)
11
+ * 1. scanGlobalValueFacts / unboxConstTypedGlobals — finalize global storage.
12
+ * 2. collectProgramFacts — sweep arrow bodies for typed-elem usage, key sets,
13
+ * loop depth, control-transfer shapes; rerun if hot inlining changes the AST.
14
+ * 3. materializeAutoBoxSchemas / resolveClosureWidth — settle layout decisions.
15
+ * 4. Whole-program narrowing (skipped on simple programs):
16
+ * - narrowSignatures — pick a specialization per function from call sites
17
+ * - specializeBimorphicTyped — split typed-elem hot paths into two variants
18
+ * when callers diverge between two ctors
19
+ * - refineDynKeys — tighten dynamic property-key sets
20
+ *
21
+ * No bytes are emitted here; emit.js consumes the planned ctx + programFacts.
22
+ *
23
+ * @module plan
24
+ */
2
25
 
3
26
  import { ctx } from './ctx.js'
4
- import { VAL, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts } from './analyze.js'
27
+ import { T, VAL, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts } from './analyze.js'
5
28
  import { MAX_CLOSURE_ARITY } from './ir.js'
6
29
  import narrowSignatures, { specializeBimorphicTyped, refineDynKeys } from './narrow.js'
7
30
 
31
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
32
+ const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
33
+ const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
34
+ const SCALAR_TYPED_CTOR = 'new.Float64Array'
35
+ const MAX_SCALAR_TYPED_ARRAY_LEN = 32
36
+ const MAX_SCALAR_TYPED_LOOP_UNROLL = 16
37
+ const MAX_SCALAR_TYPED_NESTED_UNROLL = 128
38
+
39
+ const isSeq = node => Array.isArray(node) && node[0] === ';'
40
+ const blockStmts = body => {
41
+ if (!Array.isArray(body) || body[0] !== '{}') return null
42
+ const inner = body[1]
43
+ if (!Array.isArray(inner)) return inner == null ? [] : [inner]
44
+ return inner[0] === ';' ? inner.slice(1) : [inner]
45
+ }
46
+
47
+ const callArgs = node => {
48
+ if (!Array.isArray(node) || node[0] !== '()') return null
49
+ const raw = node[2]
50
+ return raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
51
+ }
52
+
53
+ const isSimpleArg = node => {
54
+ if (typeof node === 'string' || typeof node === 'number') return true
55
+ if (!Array.isArray(node)) return false
56
+ if (node[0] == null) return typeof node[1] === 'number'
57
+ if (node[0] === 'str') return typeof node[1] === 'string'
58
+ if (node[0] === 'u-' || (node[0] === '-' && node.length === 2)) return isSimpleArg(node[1])
59
+ if (['+', '-', '*', '%', '&', '|', '^', '<<', '>>', '>>>'].includes(node[0]))
60
+ return isSimpleArg(node[1]) && isSimpleArg(node[2])
61
+ return false
62
+ }
63
+
64
+ const scanBody = (node, fn) => {
65
+ if (!Array.isArray(node)) return false
66
+ if (fn(node)) return true
67
+ if (node[0] === '=>') return false
68
+ for (let i = 1; i < node.length; i++) if (scanBody(node[i], fn)) return true
69
+ return false
70
+ }
71
+
72
+ const loopDepth = (node, depth) => {
73
+ if (!Array.isArray(node)) return depth
74
+ if (node[0] === '=>') return depth
75
+ const here = LOOP_OPS.has(node[0]) ? depth + 1 : depth
76
+ let max = here
77
+ for (let i = 1; i < node.length; i++) {
78
+ const d = loopDepth(node[i], here)
79
+ if (d > max) max = d
80
+ }
81
+ return max
82
+ }
83
+
84
+ const nodeSize = (node) => {
85
+ if (!Array.isArray(node)) return 1
86
+ let n = 1
87
+ for (let i = 1; i < node.length; i++) n += nodeSize(node[i])
88
+ return n
89
+ }
90
+
91
+ const collectBindings = (node, out) => {
92
+ if (!Array.isArray(node)) return
93
+ const op = node[0]
94
+ if (op === '=>') return
95
+ if (op === 'let' || op === 'const') {
96
+ for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
97
+ }
98
+ for (let i = 1; i < node.length; i++) collectBindings(node[i], out)
99
+ }
100
+
101
+ const collectBindingTarget = (node, out) => {
102
+ if (typeof node === 'string') { out.add(node); return }
103
+ if (!Array.isArray(node)) return
104
+ if (node[0] === '=') collectBindingTarget(node[1], out)
105
+ else if (node[0] === '...' && typeof node[1] === 'string') out.add(node[1])
106
+ else if (node[0] === ',' || node[0] === '[]' || node[0] === '{}')
107
+ for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
108
+ }
109
+
110
+ const mutatesAny = (node, names) => scanBody(node, n => {
111
+ const op = n[0]
112
+ if ((op === '++' || op === '--') && typeof n[1] === 'string') return names.has(n[1])
113
+ return ASSIGN_OPS.has(op) && typeof n[1] === 'string' && names.has(n[1])
114
+ })
115
+
116
+ const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
117
+
118
+ const intLit = node => {
119
+ if (typeof node === 'number' && Number.isInteger(node)) return node
120
+ if (Array.isArray(node) && node[0] == null && Number.isInteger(node[1])) return node[1]
121
+ return null
122
+ }
123
+
124
+ const constIntExpr = (node) => {
125
+ const lit = intLit(node)
126
+ if (lit != null) return lit
127
+ if (typeof node === 'string') return ctx.scope.constInts?.get(node) ?? null
128
+ if (!Array.isArray(node)) return null
129
+ const op = node[0]
130
+ if (op === 'u-') {
131
+ const v = constIntExpr(node[1])
132
+ return v == null ? null : -v
133
+ }
134
+ if (node.length !== 3) return null
135
+ const a = constIntExpr(node[1]), b = constIntExpr(node[2])
136
+ if (a == null || b == null) return null
137
+ if (op === '+') return a + b
138
+ if (op === '-') return a - b
139
+ if (op === '*') return a * b
140
+ if (op === '<<') return a << b
141
+ return null
142
+ }
143
+
144
+ const setCallArgs = (node, args) => {
145
+ node[2] = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
146
+ }
147
+
148
+ const scalarArrayElems = (expr) => {
149
+ if (!Array.isArray(expr) || expr[0] !== '[') return null
150
+ const elems = expr.slice(1)
151
+ if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...') || !isSimpleArg(e))) return null
152
+ return elems
153
+ }
154
+
155
+ const scalarObjectProps = (expr) => {
156
+ if (!Array.isArray(expr) || expr[0] !== '{}') return null
157
+ const props = staticObjectProps(expr.slice(1))
158
+ if (!props) return null
159
+ const seen = new Set()
160
+ for (let i = 0; i < props.names.length; i++) {
161
+ const name = props.names[i]
162
+ if (seen.has(name) || !isSimpleArg(props.values[i])) return null
163
+ seen.add(name)
164
+ }
165
+ return props
166
+ }
167
+
168
+ const fixedScalarTypedArrayLen = (expr) => {
169
+ if (typedElemCtor(expr) !== SCALAR_TYPED_CTOR) return null
170
+ const args = callArgs(expr)
171
+ if (!args || args.length !== 1) return null
172
+ const len = constIntExpr(args[0])
173
+ return len != null && len >= 0 && len <= MAX_SCALAR_TYPED_ARRAY_LEN ? len : null
174
+ }
175
+
176
+ const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
177
+
178
+ const safeScalarArrayUse = (node, name, parentOp = null) => {
179
+ if (typeof node === 'string') return node !== name
180
+ if (!Array.isArray(node)) return true
181
+ const op = node[0]
182
+ if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
183
+ if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
184
+ if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
185
+ if (op === '[]' && node[1] === name) return intLit(node[2]) != null
186
+ if (op === '...' && node[1] === name) return parentOp === '['
187
+ for (let i = 1; i < node.length; i++) {
188
+ if (!safeScalarArrayUse(node[i], name, op)) return false
189
+ }
190
+ return true
191
+ }
192
+
193
+ const rewriteScalarArrayUses = (node, arrays, parentOp = null) => {
194
+ if (!Array.isArray(node)) return node
195
+ const op = node[0]
196
+ if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') {
197
+ return [, arrays.get(node[1]).length]
198
+ }
199
+ if (op === '[]' && arrays.has(node[1])) {
200
+ const idx = intLit(node[2])
201
+ const elems = arrays.get(node[1])
202
+ return idx != null && idx >= 0 && idx < elems.length ? elems[idx] : [, undefined]
203
+ }
204
+ if (op === '[') {
205
+ const out = ['[']
206
+ for (let i = 1; i < node.length; i++) {
207
+ const item = node[i]
208
+ if (Array.isArray(item) && item[0] === '...' && arrays.has(item[1])) {
209
+ out.push(...arrays.get(item[1]))
210
+ } else {
211
+ out.push(rewriteScalarArrayUses(item, arrays, op))
212
+ }
213
+ }
214
+ return out
215
+ }
216
+ return node.map((part, i) => i === 0 ? part : rewriteScalarArrayUses(part, arrays, op))
217
+ }
218
+
219
+ const safeScalarObjectUse = (node, name, keys) => {
220
+ if (typeof node === 'string') return node !== name
221
+ if (!Array.isArray(node)) return true
222
+ const op = node[0]
223
+ if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
224
+ if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
225
+ if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2])
226
+ if (op === '[]' && node[1] === name) {
227
+ const key = staticPropertyKey(node[2])
228
+ return key != null && keys.has(key)
229
+ }
230
+ if (op === '...' && node[1] === name) return false
231
+ for (let i = 1; i < node.length; i++) {
232
+ if (!safeScalarObjectUse(node[i], name, keys)) return false
233
+ }
234
+ return true
235
+ }
236
+
237
+ const rewriteScalarObjectUses = (node, objects) => {
238
+ if (!Array.isArray(node)) return node
239
+ const op = node[0]
240
+ if ((op === '.' || op === '?.') && objects.has(node[1])) {
241
+ const fields = objects.get(node[1])
242
+ return fields.get(node[2]) ?? [, undefined]
243
+ }
244
+ if (op === '[]' && objects.has(node[1])) {
245
+ const key = staticPropertyKey(node[2])
246
+ const fields = objects.get(node[1])
247
+ return key != null ? (fields.get(key) ?? [, undefined]) : node
248
+ }
249
+ return node.map((part, i) => i === 0 ? part : rewriteScalarObjectUses(part, objects))
250
+ }
251
+
252
+ const typedArraySlotIndex = (node, len) => {
253
+ const idx = constIntExpr(node)
254
+ return idx != null && idx >= 0 && idx < len ? idx : null
255
+ }
256
+
257
+ const safeScalarTypedArrayUse = (node, name, len) => {
258
+ if (typeof node === 'string') return node !== name
259
+ if (!Array.isArray(node)) return true
260
+ const op = node[0]
261
+ if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
262
+ if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
263
+ if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
264
+ if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
265
+ return typedArraySlotIndex(node[1][2], len) != null
266
+ if (ASSIGN_TARGET_OPS.has(op)) {
267
+ if (node[1] === name) return false
268
+ if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
269
+ if (typedArraySlotIndex(node[1][2], len) == null) return false
270
+ for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len)) return false
271
+ return true
272
+ }
273
+ }
274
+ if (op === '...' && node[1] === name) return false
275
+ for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len)) return false
276
+ return true
277
+ }
278
+
279
+ const mentionsName = (node, name) => {
280
+ if (typeof node === 'string') return node === name
281
+ if (!Array.isArray(node) || node[0] === '=>') return false
282
+ for (let i = 1; i < node.length; i++) if (mentionsName(node[i], name)) return true
283
+ return false
284
+ }
285
+
286
+ const rewriteScalarTypedArrayUses = (node, arrays) => {
287
+ if (!Array.isArray(node)) return node
288
+ const op = node[0]
289
+ const slotFor = (idxNode, entry) => {
290
+ const idx = typedArraySlotIndex(idxNode, entry.len)
291
+ return idx == null ? null : entry.slots[idx]
292
+ }
293
+ if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') return [null, arrays.get(node[1]).len]
294
+ if (op === '[]' && arrays.has(node[1])) return slotFor(node[2], arrays.get(node[1])) ?? node
295
+ if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
296
+ const slot = slotFor(node[1][2], arrays.get(node[1][1]))
297
+ return slot ? [op, slot] : node
298
+ }
299
+ if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
300
+ const slot = slotFor(node[1][2], arrays.get(node[1][1]))
301
+ return slot ? [op, slot, ...node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))] : node
302
+ }
303
+ return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
304
+ }
305
+
306
+ const scalarTypedArrayStores = (name, entry) =>
307
+ entry.slots.map((slot, i) => ['=', ['[]', name, [null, i]], slot])
308
+
309
+ const scalarTypedArrayLoads = (name, entry) =>
310
+ entry.slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])
311
+
312
+ const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
313
+ if (!Array.isArray(node)) return out
314
+ const op = node[0]
315
+ const addSlot = target => {
316
+ if (Array.isArray(target) && target[0] === '[]' && target[1] === name) {
317
+ const idx = typedArraySlotIndex(target[2], len)
318
+ if (idx != null) out.add(idx)
319
+ return true
320
+ }
321
+ return false
322
+ }
323
+ if ((op === '++' || op === '--') && addSlot(node[1])) return out
324
+ if (ASSIGN_TARGET_OPS.has(op) && addSlot(node[1])) {
325
+ for (let i = 2; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
326
+ return out
327
+ }
328
+ if (op !== '=>') for (let i = 1; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
329
+ return out
330
+ }
331
+
332
+ const hasScalarTypedArrayRead = (node, name) => {
333
+ if (!Array.isArray(node)) return false
334
+ const op = node[0]
335
+ const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
336
+ if ((op === '++' || op === '--') && isTarget(node[1])) return true
337
+ if (ASSIGN_TARGET_OPS.has(op)) {
338
+ if (isTarget(node[1])) {
339
+ if (op !== '=') return true
340
+ for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
341
+ return false
342
+ }
343
+ }
344
+ if (op === '[]' && node[1] === name) return true
345
+ if (op === '=>') return false
346
+ for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
347
+ return false
348
+ }
349
+
350
+ const scalarizeTypedArrayLiteralSeq = (seq) => {
351
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
352
+ let changed = false
353
+ const stmts = seq.slice(1).map(stmt => {
354
+ const r = scalarizeTypedArrayLiterals(stmt)
355
+ changed ||= r.changed
356
+ return r.node
357
+ })
358
+
359
+ const candidates = new Map()
360
+ const mirrored = new Map()
361
+ for (let i = 0; i < stmts.length; i++) {
362
+ const stmt = stmts[i]
363
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
364
+ const decl = stmt[1]
365
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
366
+ const len = fixedScalarTypedArrayLen(decl[2])
367
+ if (len == null) continue
368
+ let hasSafeUse = false, hasUnsafeUse = false
369
+ for (let j = 0; j < stmts.length; j++) {
370
+ if (j === i) continue
371
+ if (!mentionsName(stmts[j], decl[1])) continue
372
+ const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len)
373
+ hasSafeUse ||= safe
374
+ hasUnsafeUse ||= !safe
375
+ }
376
+ if (!hasSafeUse && hasUnsafeUse) continue
377
+ if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, mirrored: false })
378
+ else mirrored.set(decl[1], { index: i, len, mirrored: true })
379
+ }
380
+ if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
381
+
382
+ const arrays = new Map()
383
+ for (const [name, c] of [...candidates, ...mirrored]) {
384
+ const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
385
+ arrays.set(name, { len: c.len, slots, mirrored: c.mirrored })
386
+ }
387
+
388
+ const out = []
389
+ for (let i = 0; i < stmts.length; i++) {
390
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
391
+ [...mirrored.entries()].find(([, c]) => c.index === i)
392
+ if (entry) {
393
+ const [name] = entry
394
+ const arr = arrays.get(name)
395
+ const { slots } = arr
396
+ if (arr.mirrored) {
397
+ out.push(stmts[i])
398
+ if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
399
+ } else if (slots.length) {
400
+ out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
401
+ }
402
+ changed = true
403
+ continue
404
+ }
405
+ const unsafe = []
406
+ for (const [name, arr] of arrays) {
407
+ if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len)) unsafe.push([name, arr])
408
+ }
409
+ if (unsafe.length) {
410
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
411
+ out.push(stmts[i])
412
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
413
+ changed = true
414
+ } else {
415
+ out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
416
+ }
417
+ }
418
+ return { node: [';', ...out], changed: true }
419
+ }
420
+
421
+ function scalarizeTypedArrayLiterals(node) {
422
+ if (!Array.isArray(node)) return { node, changed: false }
423
+ if (node[0] === '=>') return { node, changed: false }
424
+ if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
425
+ let changed = false
426
+ const out = [node[0]]
427
+ for (let i = 1; i < node.length; i++) {
428
+ const r = scalarizeTypedArrayLiterals(node[i])
429
+ changed ||= r.changed
430
+ out.push(r.node)
431
+ }
432
+ return changed ? { node: out, changed: true } : { node, changed: false }
433
+ }
434
+
435
+ const stmtList = (body) => {
436
+ if (!Array.isArray(body)) return body == null ? [] : [body]
437
+ if (body[0] === '{}') return stmtList(body[1])
438
+ if (body[0] === ';') return body.slice(1)
439
+ return [body]
440
+ }
441
+
442
+ const hasControlTransfer = node => scanBody(node, n => CONTROL_TRANSFER.has(n[0]))
443
+
444
+ const containsDeclOf = (body, name) => scanBody(body, n => {
445
+ if (n[0] !== 'let' && n[0] !== 'const') return false
446
+ for (let i = 1; i < n.length; i++) {
447
+ const d = n[i]
448
+ if (d === name) return true
449
+ if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
450
+ }
451
+ return false
452
+ })
453
+
454
+ const isReassigned = (body, name) => scanBody(body, n =>
455
+ (ASSIGN_OPS.has(n[0]) && n[1] === name) || ((n[0] === '++' || n[0] === '--') && n[1] === name))
456
+
457
+ const containsTypedArrayAccess = (body, names) => scanBody(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
458
+
459
+ function smallScalarTypedForTrip(init, cond, step) {
460
+ if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
461
+ const decl = init[1]
462
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
463
+ const name = decl[1]
464
+ if (constIntExpr(decl[2]) !== 0) return null
465
+ if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
466
+ const end = constIntExpr(cond[2])
467
+ if (end == null || end < 0 || end > MAX_SCALAR_TYPED_LOOP_UNROLL) return null
468
+ const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
469
+ (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
470
+ return stepOk ? { name, end } : null
471
+ }
472
+
473
+ const scalarTypedLoopBudget = (body) => {
474
+ if (!Array.isArray(body) || body[0] === '=>') return 1
475
+ if (body[0] === 'for') {
476
+ const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
477
+ return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
478
+ }
479
+ let max = 1
480
+ for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
481
+ return max
482
+ }
483
+
484
+ const unrollTypedArrayLoops = (node, names) => {
485
+ if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
486
+ if (node[0] === ';') {
487
+ let changed = false
488
+ const out = [';']
489
+ for (const stmt of node.slice(1)) {
490
+ const r = unrollTypedArrayLoops(stmt, names)
491
+ changed ||= r.changed
492
+ if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
493
+ else out.push(r.node)
494
+ }
495
+ return changed ? { node: out, changed: true } : { node, changed: false }
496
+ }
497
+ if (node[0] === '{}') {
498
+ const r = unrollTypedArrayLoops(node[1], names)
499
+ return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
500
+ }
501
+ if (node[0] === 'for') {
502
+ const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
503
+ if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= MAX_SCALAR_TYPED_NESTED_UNROLL &&
504
+ !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
505
+ const out = [';']
506
+ for (let i = 0; i < trip.end; i++) {
507
+ const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
508
+ const r = unrollTypedArrayLoops(cloned, names)
509
+ out.push(...stmtList(r.node))
510
+ }
511
+ return { node: out, changed: true }
512
+ }
513
+ }
514
+ let changed = false
515
+ const out = [node[0]]
516
+ for (let i = 1; i < node.length; i++) {
517
+ const r = unrollTypedArrayLoops(node[i], names)
518
+ changed ||= r.changed
519
+ out.push(r.node)
520
+ }
521
+ return changed ? { node: out, changed: true } : { node, changed: false }
522
+ }
523
+
524
+ const fixedTypedArraysInBody = (body) => {
525
+ const out = new Map()
526
+ const walk = node => {
527
+ if (!Array.isArray(node) || node[0] === '=>') return
528
+ if (node[0] === 'let' || node[0] === 'const') {
529
+ for (let i = 1; i < node.length; i++) {
530
+ const d = node[i]
531
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
532
+ const len = fixedScalarTypedArrayLen(d[2])
533
+ if (len != null) out.set(d[1], { len })
534
+ }
535
+ }
536
+ for (let i = 1; i < node.length; i++) walk(node[i])
537
+ }
538
+ walk(body)
539
+ return out
540
+ }
541
+
542
+ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
543
+ if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
544
+ if (scanBody(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
545
+ const params = func.sig?.params || []
546
+ const cands = new Map()
547
+ for (let i = 0; i < params.length; i++) {
548
+ const pname = params[i].name
549
+ let len = null, ok = true
550
+ for (const site of sites) {
551
+ const arg = site.argList[i]
552
+ const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
553
+ if (!fixed) { ok = false; break }
554
+ if (len == null) len = fixed.len
555
+ else if (len !== fixed.len) { ok = false; break }
556
+ }
557
+ if (ok && len != null) cands.set(pname, { len })
558
+ }
559
+ if (!cands.size) return cands
560
+ for (const site of sites) {
561
+ const seen = new Set()
562
+ for (let i = 0; i < params.length; i++) {
563
+ if (!cands.has(params[i].name)) continue
564
+ const arg = site.argList[i]
565
+ if (typeof arg !== 'string' || seen.has(arg)) return new Map()
566
+ seen.add(arg)
567
+ }
568
+ }
569
+ return cands
570
+ }
571
+
572
+ const scalarizeTypedArrayParams = (func, paramCands) => {
573
+ for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len)) paramCands.delete(name)
574
+ for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
575
+ if (!paramCands.size) return { body: func.body, changed: false }
576
+ const arrays = new Map()
577
+ for (const [name, c] of paramCands) {
578
+ arrays.set(name, {
579
+ len: c.len,
580
+ slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
581
+ })
582
+ }
583
+ const prologue = []
584
+ const writeback = []
585
+ for (const [name, { len, slots }] of arrays) {
586
+ if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
587
+ for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
588
+ }
589
+ const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
590
+ return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
591
+ }
592
+
593
+ const scalarizeFunctionTypedArrays = (programFacts) => {
594
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
595
+ const sitesByCallee = new Map()
596
+ for (const site of programFacts.callSites) {
597
+ if (!site.callerFunc) continue
598
+ const list = sitesByCallee.get(site.callee)
599
+ if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
600
+ }
601
+ let changed = false
602
+ for (const func of ctx.func.list) {
603
+ if (!func.body || func.raw) continue
604
+ const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
605
+ const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
606
+ if (names.size) {
607
+ let guard = 0
608
+ while (guard++ < 6) {
609
+ const r = unrollTypedArrayLoops(func.body, names)
610
+ if (!r.changed) break
611
+ func.body = r.node
612
+ changed = true
613
+ }
614
+ }
615
+ const p = scalarizeTypedArrayParams(func, paramCands)
616
+ if (p.changed) { func.body = p.body; changed = true }
617
+ const l = scalarizeTypedArrayLiterals(func.body)
618
+ if (l.changed) { func.body = l.node; changed = true }
619
+ if (changed) invalidateLocalsCache(func.body)
620
+ }
621
+ return changed
622
+ }
623
+
624
+ const scalarizeArrayLiteralSeq = (seq) => {
625
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
626
+ let changed = false
627
+ const stmts = seq.slice(1).map(stmt => {
628
+ const r = scalarizeArrayLiterals(stmt)
629
+ changed ||= r.changed
630
+ return r.node
631
+ })
632
+
633
+ const candidates = new Map()
634
+ for (let i = 0; i < stmts.length; i++) {
635
+ const stmt = stmts[i]
636
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
637
+ const decl = stmt[1]
638
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
639
+ const elems = scalarArrayElems(decl[2])
640
+ if (!elems) continue
641
+ let ok = true
642
+ for (let j = 0; j < stmts.length && ok; j++) {
643
+ if (j === i) continue
644
+ ok = safeScalarArrayUse(stmts[j], decl[1])
645
+ }
646
+ if (!ok) continue
647
+ candidates.set(decl[1], { index: i, op: stmt[0], elems })
648
+ }
649
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
650
+
651
+ const arrays = new Map()
652
+ for (const [name, c] of candidates) {
653
+ const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
654
+ arrays.set(name, temps)
655
+ }
656
+
657
+ const out = []
658
+ for (let i = 0; i < stmts.length; i++) {
659
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
660
+ if (entry) {
661
+ const [name, c] = entry
662
+ const temps = arrays.get(name)
663
+ if (temps.length) {
664
+ out.push([c.op, ...temps.map((tmp, k) =>
665
+ ['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
666
+ }
667
+ changed = true
668
+ continue
669
+ }
670
+ out.push(rewriteScalarArrayUses(stmts[i], arrays))
671
+ }
672
+ return { node: [';', ...out], changed: true }
673
+ }
674
+
675
+ const scalarizeObjectLiteralSeq = (seq, escapes) => {
676
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
677
+ let changed = false
678
+ const stmts = seq.slice(1).map(stmt => {
679
+ const r = scalarizeObjectLiterals(stmt, escapes)
680
+ changed ||= r.changed
681
+ return r.node
682
+ })
683
+
684
+ const candidates = new Map()
685
+ for (let i = 0; i < stmts.length; i++) {
686
+ const stmt = stmts[i]
687
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
688
+ const decl = stmt[1]
689
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
690
+ if (escapes.get(decl[1]) !== false) continue
691
+ const props = scalarObjectProps(decl[2])
692
+ if (!props) continue
693
+ const keys = new Set(props.names)
694
+ let ok = true
695
+ for (let j = 0; j < stmts.length && ok; j++) {
696
+ if (j === i) continue
697
+ ok = safeScalarObjectUse(stmts[j], decl[1], keys)
698
+ }
699
+ if (!ok) continue
700
+ candidates.set(decl[1], { index: i, op: stmt[0], props })
701
+ }
702
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
703
+
704
+ const objects = new Map()
705
+ for (const [name, c] of candidates) {
706
+ const fields = new Map()
707
+ for (let i = 0; i < c.props.names.length; i++) {
708
+ fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
709
+ }
710
+ objects.set(name, fields)
711
+ }
712
+
713
+ const out = []
714
+ for (let i = 0; i < stmts.length; i++) {
715
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
716
+ if (entry) {
717
+ const [, c] = entry
718
+ const fields = objects.get(entry[0])
719
+ if (c.props.names.length) {
720
+ out.push([c.op, ...c.props.names.map((prop, k) =>
721
+ ['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
722
+ }
723
+ changed = true
724
+ continue
725
+ }
726
+ out.push(rewriteScalarObjectUses(stmts[i], objects))
727
+ }
728
+ return { node: [';', ...out], changed: true }
729
+ }
730
+
731
+ function scalarizeObjectLiterals(node, escapes) {
732
+ if (!Array.isArray(node)) return { node, changed: false }
733
+ if (node[0] === '=>') return { node, changed: false }
734
+ if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
735
+ let changed = false
736
+ const out = [node[0]]
737
+ for (let i = 1; i < node.length; i++) {
738
+ const r = scalarizeObjectLiterals(node[i], escapes)
739
+ changed ||= r.changed
740
+ out.push(r.node)
741
+ }
742
+ return changed ? { node: out, changed: true } : { node, changed: false }
743
+ }
744
+
745
+ function scalarizeArrayLiterals(node) {
746
+ if (!Array.isArray(node)) return { node, changed: false }
747
+ if (node[0] === '=>') return { node, changed: false }
748
+ if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
749
+ let changed = false
750
+ const out = [node[0]]
751
+ for (let i = 1; i < node.length; i++) {
752
+ const r = scalarizeArrayLiterals(node[i])
753
+ changed ||= r.changed
754
+ out.push(r.node)
755
+ }
756
+ return changed ? { node: out, changed: true } : { node, changed: false }
757
+ }
758
+
759
+ const scalarizeFunctionArrayLiterals = () => {
760
+ let changed = false
761
+ for (const func of ctx.func.list) {
762
+ if (!func.body || func.raw) continue
763
+ let guard = 0
764
+ while (guard++ < 4) {
765
+ const r = scalarizeArrayLiterals(func.body)
766
+ if (!r.changed) break
767
+ func.body = r.node
768
+ changed = true
769
+ }
770
+ }
771
+ return changed
772
+ }
773
+
774
+ const scalarizeFunctionObjectLiterals = () => {
775
+ let changed = false
776
+ for (const func of ctx.func.list) {
777
+ if (!func.body || func.raw) continue
778
+ let guard = 0
779
+ while (guard++ < 4) {
780
+ const escapes = new Map(analyzeBody(func.body).escapes)
781
+ invalidateLocalsCache(func.body)
782
+ const r = scalarizeObjectLiterals(func.body, escapes)
783
+ if (!r.changed) break
784
+ func.body = r.node
785
+ changed = true
786
+ }
787
+ }
788
+ return changed
789
+ }
790
+
791
+ const cloneWithSubst = (node, subst, rename) => {
792
+ if (typeof node === 'string') {
793
+ if (subst.has(node)) return clonePlain(subst.get(node))
794
+ return rename.get(node) || node
795
+ }
796
+ if (!Array.isArray(node)) return node
797
+ const op = node[0]
798
+ if (op === 'str') return node.slice()
799
+ if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, rename), node[2]]
800
+ if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, rename)]
801
+ return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, rename))
802
+ }
803
+
804
+ // Returns { prefix, value } where prefix is the substituted body statements
805
+ // (excluding any trailing `return X`), and value is the substituted return
806
+ // expression — null if void or no trailing return value.
807
+ const inlinedBody = (func, args) => {
808
+ const params = func.sig.params
809
+ if (args.length !== params.length || !args.every(isSimpleArg)) return null
810
+ const paramNames = new Set(params.map(p => p.name))
811
+ if (mutatesAny(func.body, paramNames)) return null
812
+
813
+ const subst = new Map()
814
+ for (let i = 0; i < params.length; i++) subst.set(params[i].name, args[i])
815
+
816
+ const locals = new Set()
817
+ collectBindings(func.body, locals)
818
+ for (const p of params) locals.delete(p.name)
819
+
820
+ const rename = new Map()
821
+ for (const name of locals) rename.set(name, `${T}inl${ctx.func.uniq++}_${name}`)
822
+
823
+ const stmts = blockStmts(func.body)
824
+ // Expression-bodied arrow `(c) => expr`: no statement block; the whole body
825
+ // *is* the return value. Treat as zero-prefix + value.
826
+ if (!stmts) return { prefix: [], value: cloneWithSubst(func.body, subst, rename) }
827
+ const last = stmts.length ? stmts[stmts.length - 1] : null
828
+ const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
829
+ const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
830
+ const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
831
+ const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
832
+ return { prefix, value }
833
+ }
834
+
835
+ const isCandidateCall = (node, candidates) =>
836
+ Array.isArray(node) && node[0] === '()' && typeof node[1] === 'string' && candidates.has(node[1])
837
+
838
+ // Recursively substitute calls to expr-bodied candidates anywhere in `node`.
839
+ // Used for tiny pure-expression helpers (`isAlpha(c) => …`) that get called
840
+ // from expression contexts (if-conditions, ternary tests). For these the
841
+ // inlined body is value-only (zero prefix), so a pure substitution is safe.
842
+ const inlineInExpr = (node, candidates) => {
843
+ if (!Array.isArray(node)) return { node, changed: false }
844
+ if (node[0] === '=>') return { node, changed: false }
845
+ let changed = false
846
+ const next = [node[0]]
847
+ for (let i = 1; i < node.length; i++) {
848
+ const r = inlineInExpr(node[i], candidates)
849
+ if (r.changed) changed = true
850
+ next.push(r.node)
851
+ }
852
+ if (isCandidateCall(next, candidates)) {
853
+ const args = callArgs(next)
854
+ const shape = args && inlinedBody(candidates.get(next[1]), args)
855
+ if (shape && shape.value !== null && shape.prefix.length === 0) {
856
+ return { node: shape.value, changed: true }
857
+ }
858
+ }
859
+ return { node: changed ? next : node, changed }
860
+ }
861
+
862
+ const inlineInStmt = (stmt, candidates) => {
863
+ if (!Array.isArray(stmt)) return { node: stmt, changed: false }
864
+ // Statement-position call: discard return value, splice prefix in place.
865
+ if (isCandidateCall(stmt, candidates)) {
866
+ const args = callArgs(stmt)
867
+ const shape = args && inlinedBody(candidates.get(stmt[1]), args)
868
+ if (shape) return { node: ['{}', [';', ...shape.prefix]], changed: true, splice: shape.prefix }
869
+ }
870
+ // `let/const X = call(...)` with single decl: inline as prefix + decl(value).
871
+ if ((stmt[0] === 'let' || stmt[0] === 'const') && stmt.length === 2) {
872
+ const decl = stmt[1]
873
+ if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && isCandidateCall(decl[2], candidates)) {
874
+ const args = callArgs(decl[2])
875
+ const shape = args && inlinedBody(candidates.get(decl[2][1]), args)
876
+ if (shape && shape.value !== null) {
877
+ const splice = [...shape.prefix, [stmt[0], ['=', decl[1], shape.value]]]
878
+ return { node: ['{}', [';', ...splice]], changed: true, splice }
879
+ }
880
+ }
881
+ }
882
+ // `X = call(...)` at statement position: inline as prefix + assign(value).
883
+ if (stmt[0] === '=' && typeof stmt[1] === 'string' && isCandidateCall(stmt[2], candidates)) {
884
+ const args = callArgs(stmt[2])
885
+ const shape = args && inlinedBody(candidates.get(stmt[2][1]), args)
886
+ if (shape && shape.value !== null) {
887
+ const splice = [...shape.prefix, ['=', stmt[1], shape.value]]
888
+ return { node: ['{}', [';', ...splice]], changed: true, splice }
889
+ }
890
+ }
891
+ const op = stmt[0]
892
+ if (op === ';') {
893
+ let changed = false
894
+ const next = [';']
895
+ for (let i = 1; i < stmt.length; i++) {
896
+ const r = inlineInStmt(stmt[i], candidates)
897
+ changed ||= r.changed
898
+ if (r.splice) next.push(...r.splice)
899
+ else next.push(r.node)
900
+ }
901
+ return changed ? { node: next, changed } : { node: stmt, changed: false }
902
+ }
903
+ if (op === '{}') {
904
+ const r = inlineInStmt(stmt[1], candidates)
905
+ return r.changed ? { node: ['{}', r.node], changed: true } : { node: stmt, changed: false }
906
+ }
907
+ if (op === 'for') {
908
+ const r = inlineInStmt(stmt[4], candidates)
909
+ return r.changed ? { node: ['for', stmt[1], stmt[2], stmt[3], r.node], changed: true } : { node: stmt, changed: false }
910
+ }
911
+ if (op === 'while') {
912
+ const r = inlineInStmt(stmt[2], candidates)
913
+ return r.changed ? { node: ['while', stmt[1], r.node], changed: true } : { node: stmt, changed: false }
914
+ }
915
+ if (op === 'if') {
916
+ const thenR = inlineInStmt(stmt[2], candidates)
917
+ const elseR = stmt.length > 3 ? inlineInStmt(stmt[3], candidates) : null
918
+ if (thenR.changed || elseR?.changed) return {
919
+ node: stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node],
920
+ changed: true,
921
+ }
922
+ }
923
+ if (op === 'try' || op === 'catch' || op === 'finally') {
924
+ let changed = false
925
+ const next = [op]
926
+ for (let i = 1; i < stmt.length; i++) {
927
+ const part = stmt[i]
928
+ const r = Array.isArray(part) ? inlineInStmt(part, candidates) : { node: part, changed: false }
929
+ changed ||= r.changed
930
+ next.push(r.node)
931
+ }
932
+ return changed ? { node: next, changed: true } : { node: stmt, changed: false }
933
+ }
934
+ return { node: stmt, changed: false }
935
+ }
936
+
937
+ const inlineHotInternalCalls = (programFacts, ast) => {
938
+ const cfg = ctx.transform.optimize
939
+ if (cfg && cfg.sourceInline === false) return false
940
+
941
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
942
+ const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
943
+ const sitesByCallee = new Map()
944
+ for (const cs of programFacts.callSites) {
945
+ const list = sitesByCallee.get(cs.callee)
946
+ if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
947
+ }
948
+
949
+ const containsNode = (root, needle, inLoop = false) => {
950
+ if (root === needle) return inLoop
951
+ if (!Array.isArray(root) || root[0] === '=>') return false
952
+ const nextInLoop = inLoop || LOOP_OPS.has(root[0])
953
+ for (let i = 1; i < root.length; i++) if (containsNode(root[i], needle, nextInLoop)) return true
954
+ return false
955
+ }
956
+
957
+ const hasFixedTypedArraySites = (func, sites) => {
958
+ const params = func.sig?.params || []
959
+ if (!sites?.length) return false
960
+ return sites.every(site => params.some((p, i) => {
961
+ const arg = site.argList[i]
962
+ return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
963
+ }))
964
+ }
965
+ const hasFullyFixedTypedArraySites = (func, sites) => {
966
+ const params = func.sig?.params || []
967
+ if (!sites?.length) return false
968
+ let sawTypedArg = false
969
+ for (const site of sites) {
970
+ const typed = typedByFunc.get(site.callerFunc)
971
+ const fixed = fixedByFunc.get(site.callerFunc)
972
+ for (let i = 0; i < params.length; i++) {
973
+ const arg = site.argList[i]
974
+ if (typeof arg !== 'string' || !typed?.has(arg)) continue
975
+ sawTypedArg = true
976
+ if (!fixed?.has(arg)) return false
977
+ }
978
+ }
979
+ return sawTypedArg
980
+ }
981
+
982
+ const candidates = new Map()
983
+ for (const func of ctx.func.list) {
984
+ if (func.exported || func.raw || !func.body || func.rest || programFacts.valueUsed.has(func.name)) continue
985
+ if (func.defaults && Object.keys(func.defaults).length) continue
986
+ const sites = sitesByCallee.get(func.name)
987
+ const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
988
+ const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
989
+ if (!sites || sites.length < 1 || (!fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
990
+ const stmts = blockStmts(func.body)
991
+ // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
992
+ // return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
993
+ if (scanBody(func.body, n => n[0] === '=>')) continue
994
+ // throw/break/continue are unsupported; return is OK if it's a single
995
+ // trailing return (rewritten to a value at inlining time).
996
+ if (scanBody(func.body, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) continue
997
+ let returnCount = 0
998
+ scanBody(func.body, n => { if (n[0] === 'return') returnCount++; return false })
999
+ if (returnCount > 1) continue
1000
+ if (returnCount === 1 && stmts) {
1001
+ const last = stmts[stmts.length - 1]
1002
+ if (!Array.isArray(last) || last[0] !== 'return') continue
1003
+ }
1004
+ // Either a kernel (has a loop) or a tiny leaf (no loop, no calls, small body).
1005
+ // The leaf branch catches helpers like `isAlpha(c) => (c>=65 && c<=90) || …`
1006
+ // that get hammered from a hot caller's loop — replacing the call with its
1007
+ // body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
1008
+ const hasLoop = scanBody(func.body, n => LOOP_OPS.has(n[0]))
1009
+ if (!hasLoop) {
1010
+ if (scanBody(func.body, n => n[0] === '()')) continue
1011
+ if (nodeSize(func.body) > 30) continue
1012
+ }
1013
+ if (scanBody(func.body, n => n[0] === '()' && n[1] === func.name)) continue
1014
+ // Kernels with nested loops (depth ≥ 2) are typically large and the inner
1015
+ // loop carries most of the cost. Inlining them into a host that V8 can't
1016
+ // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
1017
+ // Keep them as standalone functions so V8 wasm tier-up can warm them.
1018
+ if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
1019
+ // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
1020
+ // object/array literals returned) break downstream pointer-ABI specialization
1021
+ // when inlined: narrow.js can't trace the post-inline alias chain back to a
1022
+ // single ctor, so the typed-array param of a callee like processCascade(x, …)
1023
+ // stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
1024
+ // Keeping the factory as a callable function preserves the call-site type fact.
1025
+ if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
1026
+ candidates.set(func.name, func)
1027
+ }
1028
+ if (!candidates.size) return false
1029
+
1030
+ // Trivial expr-bodied candidates can be substituted at any expression position
1031
+ // (if-condition, ternary, etc.). Stmt-bodied ones go through inlineInStmt's
1032
+ // statement-level path which preserves prefix ordering.
1033
+ const exprOnlyCandidates = new Map()
1034
+ for (const [name, func] of candidates) {
1035
+ if (!Array.isArray(func.body) || func.body[0] !== '{}') exprOnlyCandidates.set(name, func)
1036
+ }
1037
+
1038
+ let changed = false
1039
+ const exportedCandidates = new Map()
1040
+ for (const [name, func] of candidates) {
1041
+ const sites = sitesByCallee.get(name)
1042
+ if (hasFixedTypedArraySites(func, sites) &&
1043
+ !sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))) {
1044
+ exportedCandidates.set(name, func)
1045
+ }
1046
+ }
1047
+ for (const func of ctx.func.list) {
1048
+ if (!func.body || func.raw) continue
1049
+ // Skip exports: they're entry points usually invoked once. Inlining a
1050
+ // hot kernel here would put the loop into a function V8's wasm tier-up
1051
+ // never warms (kernel stays in baseline). Keeping the kernel as its own
1052
+ // callable function lets V8 promote it to TurboFan after a few calls.
1053
+ // Exception: fixed-size typed-array callees should inline into the exported
1054
+ // caller so scalar replacement can cross the call boundary and remove the
1055
+ // caller's heap arrays.
1056
+ const activeCandidates = func.exported ? exportedCandidates : candidates
1057
+ if (func.exported && !activeCandidates.size) continue
1058
+ const r = inlineInStmt(func.body, activeCandidates)
1059
+ let body = r.changed ? r.node : func.body
1060
+ let bodyChanged = r.changed
1061
+ if (!func.exported && exprOnlyCandidates.size) {
1062
+ const e = inlineInExpr(body, exprOnlyCandidates)
1063
+ if (e.changed) { body = e.node; bodyChanged = true }
1064
+ }
1065
+ if (bodyChanged) { func.body = body; changed = true }
1066
+ }
1067
+ if (ast) {
1068
+ const r = inlineInStmt(ast, candidates)
1069
+ if (r.changed) changed = true
1070
+ }
1071
+ return changed
1072
+ }
1073
+
1074
+ const restIndexExpr = (idx, restParams) => {
1075
+ const k = intLit(idx)
1076
+ if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
1077
+
1078
+ let out = [, undefined]
1079
+ for (let i = restParams.length - 1; i >= 0; i--) {
1080
+ out = ['?:', ['==', clonePlain(idx), [, i]], restParams[i], out]
1081
+ }
1082
+ return out
1083
+ }
1084
+
1085
+ const rewriteRestBody = (node, restName, restParams) => {
1086
+ if (typeof node === 'string') return node === restName ? { ok: false } : { ok: true, node }
1087
+ if (!Array.isArray(node)) return { ok: true, node }
1088
+ if (node[0] === 'str') return { ok: true, node: node.slice() }
1089
+
1090
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === restName) {
1091
+ return node[2] === 'length' ? { ok: true, node: [, restParams.length] } : { ok: false }
1092
+ }
1093
+
1094
+ if (node[0] === '[]' && node[1] === restName) {
1095
+ if (!isSimpleArg(node[2])) return { ok: false }
1096
+ return { ok: true, node: restIndexExpr(node[2], restParams) }
1097
+ }
1098
+
1099
+ const out = [node[0]]
1100
+ for (let i = 1; i < node.length; i++) {
1101
+ const r = rewriteRestBody(node[i], restName, restParams)
1102
+ if (!r.ok) return r
1103
+ out.push(r.node)
1104
+ }
1105
+ return { ok: true, node: out }
1106
+ }
1107
+
1108
+ const specializeFixedRestCalls = (programFacts) => {
1109
+ const sitesByKey = new Map()
1110
+ for (const site of programFacts.callSites) {
1111
+ const func = ctx.func.map.get(site.callee)
1112
+ if (!func?.rest || func.exported || func.raw || !func.body) continue
1113
+ if (programFacts.valueUsed.has(func.name)) continue
1114
+ if (func.defaults && Object.keys(func.defaults).length) continue
1115
+ if (site.argList.some(a => Array.isArray(a) && a[0] === '...')) continue
1116
+
1117
+ const fixedN = func.sig.params.length - 1
1118
+ const restN = Math.max(0, site.argList.length - fixedN)
1119
+ const key = `${func.name}/${restN}`
1120
+ const list = sitesByKey.get(key)
1121
+ if (list) list.push(site); else sitesByKey.set(key, [site])
1122
+ }
1123
+
1124
+ let changed = false
1125
+ for (const [key, sites] of sitesByKey) {
1126
+ const [name, restNText] = key.split('/')
1127
+ const func = ctx.func.map.get(name)
1128
+ const restN = Number(restNText)
1129
+ const fixedParams = func.sig.params.slice(0, -1).map(p => ({ ...p }))
1130
+ const restName = func.rest
1131
+ const restParams = Array.from({ length: restN }, (_, i) => `${restName}${T}r${restN}_${i}`)
1132
+ const rewritten = rewriteRestBody(func.body, restName, restParams)
1133
+ if (!rewritten.ok) continue
1134
+
1135
+ const cloneName = `${name}${T}rest${restN}`
1136
+ if (!ctx.func.map.has(cloneName)) {
1137
+ const restSigParams = restParams.map(name => ({ name, type: 'f64' }))
1138
+ const clone = {
1139
+ ...func,
1140
+ name: cloneName,
1141
+ exported: false,
1142
+ rest: null,
1143
+ sig: {
1144
+ ...func.sig,
1145
+ params: [...fixedParams, ...restSigParams],
1146
+ results: [...func.sig.results],
1147
+ },
1148
+ body: rewritten.node,
1149
+ }
1150
+ delete clone.defaults
1151
+ ctx.func.list.push(clone)
1152
+ ctx.func.names.add(cloneName)
1153
+ ctx.func.map.set(cloneName, clone)
1154
+ }
1155
+
1156
+ const fixedN = func.sig.params.length - 1
1157
+ for (const site of sites) {
1158
+ site.node[1] = cloneName
1159
+ setCallArgs(site.node, site.argList.slice(0, fixedN + restN))
1160
+ changed = true
1161
+ }
1162
+ }
1163
+ return changed
1164
+ }
1165
+
8
1166
  const scanGlobalValueFacts = (root) => {
9
1167
  if (!root) return
10
1168
  const stmts = Array.isArray(root) && root[0] === ';' ? root.slice(1) : [root]
@@ -89,7 +1247,12 @@ export default function plan(ast) {
89
1247
  scanGlobalValueFacts(ast)
90
1248
  unboxConstTypedGlobals()
91
1249
 
92
- const programFacts = collectProgramFacts(ast)
1250
+ let programFacts = collectProgramFacts(ast)
1251
+ if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
1252
+ if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
1253
+ if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
1254
+ if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
1255
+ if (scalarizeFunctionTypedArrays(programFacts)) programFacts = collectProgramFacts(ast)
93
1256
  ctx.types.dynKeyVars = programFacts.dynVars
94
1257
  ctx.types.anyDynKey = programFacts.anyDyn
95
1258
 
@@ -102,4 +1265,4 @@ export default function plan(ast) {
102
1265
  refineDynKeys(programFacts)
103
1266
 
104
1267
  return programFacts
105
- }
1268
+ }