jz 0.1.0 → 0.2.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.
package/src/plan.js ADDED
@@ -0,0 +1,1233 @@
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
+ */
25
+
26
+ import { ctx } from './ctx.js'
27
+ import { T, VAL, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts } from './analyze.js'
28
+ import { MAX_CLOSURE_ARITY } from './ir.js'
29
+ import narrowSignatures, { specializeBimorphicTyped, refineDynKeys } from './narrow.js'
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 scalarizeTypedArrayLiteralSeq = (seq) => {
333
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
334
+ let changed = false
335
+ const stmts = seq.slice(1).map(stmt => {
336
+ const r = scalarizeTypedArrayLiterals(stmt)
337
+ changed ||= r.changed
338
+ return r.node
339
+ })
340
+
341
+ const candidates = new Map()
342
+ const mirrored = new Map()
343
+ for (let i = 0; i < stmts.length; i++) {
344
+ const stmt = stmts[i]
345
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
346
+ const decl = stmt[1]
347
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
348
+ const len = fixedScalarTypedArrayLen(decl[2])
349
+ if (len == null) continue
350
+ let hasSafeUse = false, hasUnsafeUse = false
351
+ for (let j = 0; j < stmts.length; j++) {
352
+ if (j === i) continue
353
+ if (!mentionsName(stmts[j], decl[1])) continue
354
+ const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len)
355
+ hasSafeUse ||= safe
356
+ hasUnsafeUse ||= !safe
357
+ }
358
+ if (!hasSafeUse && hasUnsafeUse) continue
359
+ if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, mirrored: false })
360
+ else mirrored.set(decl[1], { index: i, len, mirrored: true })
361
+ }
362
+ if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
363
+
364
+ const arrays = new Map()
365
+ for (const [name, c] of [...candidates, ...mirrored]) {
366
+ const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
367
+ arrays.set(name, { len: c.len, slots, mirrored: c.mirrored })
368
+ }
369
+
370
+ const out = []
371
+ for (let i = 0; i < stmts.length; i++) {
372
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
373
+ [...mirrored.entries()].find(([, c]) => c.index === i)
374
+ if (entry) {
375
+ const [name] = entry
376
+ const arr = arrays.get(name)
377
+ const { slots } = arr
378
+ if (arr.mirrored) {
379
+ out.push(stmts[i])
380
+ if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
381
+ } else if (slots.length) {
382
+ out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
383
+ }
384
+ changed = true
385
+ continue
386
+ }
387
+ const unsafe = []
388
+ for (const [name, arr] of arrays) {
389
+ if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len)) unsafe.push([name, arr])
390
+ }
391
+ if (unsafe.length) {
392
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
393
+ out.push(stmts[i])
394
+ for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
395
+ changed = true
396
+ } else {
397
+ out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
398
+ }
399
+ }
400
+ return { node: [';', ...out], changed: true }
401
+ }
402
+
403
+ function scalarizeTypedArrayLiterals(node) {
404
+ if (!Array.isArray(node)) return { node, changed: false }
405
+ if (node[0] === '=>') return { node, changed: false }
406
+ if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
407
+ let changed = false
408
+ const out = [node[0]]
409
+ for (let i = 1; i < node.length; i++) {
410
+ const r = scalarizeTypedArrayLiterals(node[i])
411
+ changed ||= r.changed
412
+ out.push(r.node)
413
+ }
414
+ return changed ? { node: out, changed: true } : { node, changed: false }
415
+ }
416
+
417
+ const stmtList = (body) => {
418
+ if (!Array.isArray(body)) return body == null ? [] : [body]
419
+ if (body[0] === '{}') return stmtList(body[1])
420
+ if (body[0] === ';') return body.slice(1)
421
+ return [body]
422
+ }
423
+
424
+ const hasControlTransfer = node => scanBody(node, n => CONTROL_TRANSFER.has(n[0]))
425
+
426
+ const containsDeclOf = (body, name) => scanBody(body, n => {
427
+ if (n[0] !== 'let' && n[0] !== 'const') return false
428
+ for (let i = 1; i < n.length; i++) {
429
+ const d = n[i]
430
+ if (d === name) return true
431
+ if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
432
+ }
433
+ return false
434
+ })
435
+
436
+ const isReassigned = (body, name) => scanBody(body, n =>
437
+ (ASSIGN_OPS.has(n[0]) && n[1] === name) || ((n[0] === '++' || n[0] === '--') && n[1] === name))
438
+
439
+ const containsTypedArrayAccess = (body, names) => scanBody(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
440
+
441
+ function smallScalarTypedForTrip(init, cond, step) {
442
+ if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
443
+ const decl = init[1]
444
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
445
+ const name = decl[1]
446
+ if (constIntExpr(decl[2]) !== 0) return null
447
+ if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
448
+ const end = constIntExpr(cond[2])
449
+ if (end == null || end < 0 || end > MAX_SCALAR_TYPED_LOOP_UNROLL) return null
450
+ const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
451
+ (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
452
+ return stepOk ? { name, end } : null
453
+ }
454
+
455
+ const scalarTypedLoopBudget = (body) => {
456
+ if (!Array.isArray(body) || body[0] === '=>') return 1
457
+ if (body[0] === 'for') {
458
+ const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
459
+ return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
460
+ }
461
+ let max = 1
462
+ for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
463
+ return max
464
+ }
465
+
466
+ const unrollTypedArrayLoops = (node, names) => {
467
+ if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
468
+ if (node[0] === ';') {
469
+ let changed = false
470
+ const out = [';']
471
+ for (const stmt of node.slice(1)) {
472
+ const r = unrollTypedArrayLoops(stmt, names)
473
+ changed ||= r.changed
474
+ if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
475
+ else out.push(r.node)
476
+ }
477
+ return changed ? { node: out, changed: true } : { node, changed: false }
478
+ }
479
+ if (node[0] === '{}') {
480
+ const r = unrollTypedArrayLoops(node[1], names)
481
+ return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
482
+ }
483
+ if (node[0] === 'for') {
484
+ const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
485
+ if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= MAX_SCALAR_TYPED_NESTED_UNROLL &&
486
+ !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
487
+ const out = [';']
488
+ for (let i = 0; i < trip.end; i++) {
489
+ const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
490
+ const r = unrollTypedArrayLoops(cloned, names)
491
+ out.push(...stmtList(r.node))
492
+ }
493
+ return { node: out, changed: true }
494
+ }
495
+ }
496
+ let changed = false
497
+ const out = [node[0]]
498
+ for (let i = 1; i < node.length; i++) {
499
+ const r = unrollTypedArrayLoops(node[i], names)
500
+ changed ||= r.changed
501
+ out.push(r.node)
502
+ }
503
+ return changed ? { node: out, changed: true } : { node, changed: false }
504
+ }
505
+
506
+ const fixedTypedArraysInBody = (body) => {
507
+ const out = new Map()
508
+ const walk = node => {
509
+ if (!Array.isArray(node) || node[0] === '=>') return
510
+ if (node[0] === 'let' || node[0] === 'const') {
511
+ for (let i = 1; i < node.length; i++) {
512
+ const d = node[i]
513
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
514
+ const len = fixedScalarTypedArrayLen(d[2])
515
+ if (len != null) out.set(d[1], { len })
516
+ }
517
+ }
518
+ for (let i = 1; i < node.length; i++) walk(node[i])
519
+ }
520
+ walk(body)
521
+ return out
522
+ }
523
+
524
+ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
525
+ if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
526
+ if (scanBody(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
527
+ const params = func.sig?.params || []
528
+ const cands = new Map()
529
+ for (let i = 0; i < params.length; i++) {
530
+ const pname = params[i].name
531
+ let len = null, ok = true
532
+ for (const site of sites) {
533
+ const arg = site.argList[i]
534
+ const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
535
+ if (!fixed) { ok = false; break }
536
+ if (len == null) len = fixed.len
537
+ else if (len !== fixed.len) { ok = false; break }
538
+ }
539
+ if (ok && len != null) cands.set(pname, { len })
540
+ }
541
+ if (!cands.size) return cands
542
+ for (const site of sites) {
543
+ const seen = new Set()
544
+ for (let i = 0; i < params.length; i++) {
545
+ if (!cands.has(params[i].name)) continue
546
+ const arg = site.argList[i]
547
+ if (typeof arg !== 'string' || seen.has(arg)) return new Map()
548
+ seen.add(arg)
549
+ }
550
+ }
551
+ return cands
552
+ }
553
+
554
+ const scalarizeTypedArrayParams = (func, paramCands) => {
555
+ for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len)) paramCands.delete(name)
556
+ if (!paramCands.size) return { body: func.body, changed: false }
557
+ const arrays = new Map()
558
+ for (const [name, c] of paramCands) {
559
+ arrays.set(name, {
560
+ len: c.len,
561
+ slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
562
+ })
563
+ }
564
+ const prologue = []
565
+ const writeback = []
566
+ for (const [name, { len, slots }] of arrays) {
567
+ if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
568
+ for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
569
+ }
570
+ const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
571
+ return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
572
+ }
573
+
574
+ const scalarizeFunctionTypedArrays = (programFacts) => {
575
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
576
+ const sitesByCallee = new Map()
577
+ for (const site of programFacts.callSites) {
578
+ if (!site.callerFunc) continue
579
+ const list = sitesByCallee.get(site.callee)
580
+ if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
581
+ }
582
+ let changed = false
583
+ for (const func of ctx.func.list) {
584
+ if (!func.body || func.raw) continue
585
+ const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
586
+ const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
587
+ if (names.size) {
588
+ let guard = 0
589
+ while (guard++ < 6) {
590
+ const r = unrollTypedArrayLoops(func.body, names)
591
+ if (!r.changed) break
592
+ func.body = r.node
593
+ changed = true
594
+ }
595
+ }
596
+ const p = scalarizeTypedArrayParams(func, paramCands)
597
+ if (p.changed) { func.body = p.body; changed = true }
598
+ const l = scalarizeTypedArrayLiterals(func.body)
599
+ if (l.changed) { func.body = l.node; changed = true }
600
+ if (changed) invalidateLocalsCache(func.body)
601
+ }
602
+ return changed
603
+ }
604
+
605
+ const scalarizeArrayLiteralSeq = (seq) => {
606
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
607
+ let changed = false
608
+ const stmts = seq.slice(1).map(stmt => {
609
+ const r = scalarizeArrayLiterals(stmt)
610
+ changed ||= r.changed
611
+ return r.node
612
+ })
613
+
614
+ const candidates = new Map()
615
+ for (let i = 0; i < stmts.length; i++) {
616
+ const stmt = stmts[i]
617
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
618
+ const decl = stmt[1]
619
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
620
+ const elems = scalarArrayElems(decl[2])
621
+ if (!elems) continue
622
+ let ok = true
623
+ for (let j = 0; j < stmts.length && ok; j++) {
624
+ if (j === i) continue
625
+ ok = safeScalarArrayUse(stmts[j], decl[1])
626
+ }
627
+ if (!ok) continue
628
+ candidates.set(decl[1], { index: i, op: stmt[0], elems })
629
+ }
630
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
631
+
632
+ const arrays = new Map()
633
+ for (const [name, c] of candidates) {
634
+ const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
635
+ arrays.set(name, temps)
636
+ }
637
+
638
+ const out = []
639
+ for (let i = 0; i < stmts.length; i++) {
640
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
641
+ if (entry) {
642
+ const [name, c] = entry
643
+ const temps = arrays.get(name)
644
+ if (temps.length) {
645
+ out.push([c.op, ...temps.map((tmp, k) =>
646
+ ['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
647
+ }
648
+ changed = true
649
+ continue
650
+ }
651
+ out.push(rewriteScalarArrayUses(stmts[i], arrays))
652
+ }
653
+ return { node: [';', ...out], changed: true }
654
+ }
655
+
656
+ const scalarizeObjectLiteralSeq = (seq, escapes) => {
657
+ if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
658
+ let changed = false
659
+ const stmts = seq.slice(1).map(stmt => {
660
+ const r = scalarizeObjectLiterals(stmt, escapes)
661
+ changed ||= r.changed
662
+ return r.node
663
+ })
664
+
665
+ const candidates = new Map()
666
+ for (let i = 0; i < stmts.length; i++) {
667
+ const stmt = stmts[i]
668
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
669
+ const decl = stmt[1]
670
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
671
+ if (escapes.get(decl[1]) !== false) continue
672
+ const props = scalarObjectProps(decl[2])
673
+ if (!props) continue
674
+ const keys = new Set(props.names)
675
+ let ok = true
676
+ for (let j = 0; j < stmts.length && ok; j++) {
677
+ if (j === i) continue
678
+ ok = safeScalarObjectUse(stmts[j], decl[1], keys)
679
+ }
680
+ if (!ok) continue
681
+ candidates.set(decl[1], { index: i, op: stmt[0], props })
682
+ }
683
+ if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
684
+
685
+ const objects = new Map()
686
+ for (const [name, c] of candidates) {
687
+ const fields = new Map()
688
+ for (let i = 0; i < c.props.names.length; i++) {
689
+ fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
690
+ }
691
+ objects.set(name, fields)
692
+ }
693
+
694
+ const out = []
695
+ for (let i = 0; i < stmts.length; i++) {
696
+ const entry = [...candidates.entries()].find(([, c]) => c.index === i)
697
+ if (entry) {
698
+ const [, c] = entry
699
+ const fields = objects.get(entry[0])
700
+ if (c.props.names.length) {
701
+ out.push([c.op, ...c.props.names.map((prop, k) =>
702
+ ['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
703
+ }
704
+ changed = true
705
+ continue
706
+ }
707
+ out.push(rewriteScalarObjectUses(stmts[i], objects))
708
+ }
709
+ return { node: [';', ...out], changed: true }
710
+ }
711
+
712
+ function scalarizeObjectLiterals(node, escapes) {
713
+ if (!Array.isArray(node)) return { node, changed: false }
714
+ if (node[0] === '=>') return { node, changed: false }
715
+ if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
716
+ let changed = false
717
+ const out = [node[0]]
718
+ for (let i = 1; i < node.length; i++) {
719
+ const r = scalarizeObjectLiterals(node[i], escapes)
720
+ changed ||= r.changed
721
+ out.push(r.node)
722
+ }
723
+ return changed ? { node: out, changed: true } : { node, changed: false }
724
+ }
725
+
726
+ function scalarizeArrayLiterals(node) {
727
+ if (!Array.isArray(node)) return { node, changed: false }
728
+ if (node[0] === '=>') return { node, changed: false }
729
+ if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
730
+ let changed = false
731
+ const out = [node[0]]
732
+ for (let i = 1; i < node.length; i++) {
733
+ const r = scalarizeArrayLiterals(node[i])
734
+ changed ||= r.changed
735
+ out.push(r.node)
736
+ }
737
+ return changed ? { node: out, changed: true } : { node, changed: false }
738
+ }
739
+
740
+ const scalarizeFunctionArrayLiterals = () => {
741
+ let changed = false
742
+ for (const func of ctx.func.list) {
743
+ if (!func.body || func.raw) continue
744
+ let guard = 0
745
+ while (guard++ < 4) {
746
+ const r = scalarizeArrayLiterals(func.body)
747
+ if (!r.changed) break
748
+ func.body = r.node
749
+ changed = true
750
+ }
751
+ }
752
+ return changed
753
+ }
754
+
755
+ const scalarizeFunctionObjectLiterals = () => {
756
+ let changed = false
757
+ for (const func of ctx.func.list) {
758
+ if (!func.body || func.raw) continue
759
+ let guard = 0
760
+ while (guard++ < 4) {
761
+ const escapes = new Map(analyzeBody(func.body).escapes)
762
+ invalidateLocalsCache(func.body)
763
+ const r = scalarizeObjectLiterals(func.body, escapes)
764
+ if (!r.changed) break
765
+ func.body = r.node
766
+ changed = true
767
+ }
768
+ }
769
+ return changed
770
+ }
771
+
772
+ const cloneWithSubst = (node, subst, rename) => {
773
+ if (typeof node === 'string') {
774
+ if (subst.has(node)) return clonePlain(subst.get(node))
775
+ return rename.get(node) || node
776
+ }
777
+ if (!Array.isArray(node)) return node
778
+ const op = node[0]
779
+ if (op === 'str') return node.slice()
780
+ if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, rename), node[2]]
781
+ if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, rename)]
782
+ return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, rename))
783
+ }
784
+
785
+ // Returns { prefix, value } where prefix is the substituted body statements
786
+ // (excluding any trailing `return X`), and value is the substituted return
787
+ // expression — null if void or no trailing return value.
788
+ const inlinedBody = (func, args) => {
789
+ const params = func.sig.params
790
+ if (args.length !== params.length || !args.every(isSimpleArg)) return null
791
+ const paramNames = new Set(params.map(p => p.name))
792
+ if (mutatesAny(func.body, paramNames)) return null
793
+
794
+ const subst = new Map()
795
+ for (let i = 0; i < params.length; i++) subst.set(params[i].name, args[i])
796
+
797
+ const locals = new Set()
798
+ collectBindings(func.body, locals)
799
+ for (const p of params) locals.delete(p.name)
800
+
801
+ const rename = new Map()
802
+ for (const name of locals) rename.set(name, `${T}inl${ctx.func.uniq++}_${name}`)
803
+
804
+ const stmts = blockStmts(func.body)
805
+ // Expression-bodied arrow `(c) => expr`: no statement block; the whole body
806
+ // *is* the return value. Treat as zero-prefix + value.
807
+ if (!stmts) return { prefix: [], value: cloneWithSubst(func.body, subst, rename) }
808
+ const last = stmts.length ? stmts[stmts.length - 1] : null
809
+ const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
810
+ const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
811
+ const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
812
+ const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
813
+ return { prefix, value }
814
+ }
815
+
816
+ const isCandidateCall = (node, candidates) =>
817
+ Array.isArray(node) && node[0] === '()' && typeof node[1] === 'string' && candidates.has(node[1])
818
+
819
+ // Recursively substitute calls to expr-bodied candidates anywhere in `node`.
820
+ // Used for tiny pure-expression helpers (`isAlpha(c) => …`) that get called
821
+ // from expression contexts (if-conditions, ternary tests). For these the
822
+ // inlined body is value-only (zero prefix), so a pure substitution is safe.
823
+ const inlineInExpr = (node, candidates) => {
824
+ if (!Array.isArray(node)) return { node, changed: false }
825
+ if (node[0] === '=>') return { node, changed: false }
826
+ let changed = false
827
+ const next = [node[0]]
828
+ for (let i = 1; i < node.length; i++) {
829
+ const r = inlineInExpr(node[i], candidates)
830
+ if (r.changed) changed = true
831
+ next.push(r.node)
832
+ }
833
+ if (isCandidateCall(next, candidates)) {
834
+ const args = callArgs(next)
835
+ const shape = args && inlinedBody(candidates.get(next[1]), args)
836
+ if (shape && shape.value !== null && shape.prefix.length === 0) {
837
+ return { node: shape.value, changed: true }
838
+ }
839
+ }
840
+ return { node: changed ? next : node, changed }
841
+ }
842
+
843
+ const inlineInStmt = (stmt, candidates) => {
844
+ if (!Array.isArray(stmt)) return { node: stmt, changed: false }
845
+ // Statement-position call: discard return value, splice prefix in place.
846
+ if (isCandidateCall(stmt, candidates)) {
847
+ const args = callArgs(stmt)
848
+ const shape = args && inlinedBody(candidates.get(stmt[1]), args)
849
+ if (shape) return { node: ['{}', [';', ...shape.prefix]], changed: true, splice: shape.prefix }
850
+ }
851
+ // `let/const X = call(...)` with single decl: inline as prefix + decl(value).
852
+ if ((stmt[0] === 'let' || stmt[0] === 'const') && stmt.length === 2) {
853
+ const decl = stmt[1]
854
+ if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && isCandidateCall(decl[2], candidates)) {
855
+ const args = callArgs(decl[2])
856
+ const shape = args && inlinedBody(candidates.get(decl[2][1]), args)
857
+ if (shape && shape.value !== null) {
858
+ const splice = [...shape.prefix, [stmt[0], ['=', decl[1], shape.value]]]
859
+ return { node: ['{}', [';', ...splice]], changed: true, splice }
860
+ }
861
+ }
862
+ }
863
+ // `X = call(...)` at statement position: inline as prefix + assign(value).
864
+ if (stmt[0] === '=' && typeof stmt[1] === 'string' && isCandidateCall(stmt[2], candidates)) {
865
+ const args = callArgs(stmt[2])
866
+ const shape = args && inlinedBody(candidates.get(stmt[2][1]), args)
867
+ if (shape && shape.value !== null) {
868
+ const splice = [...shape.prefix, ['=', stmt[1], shape.value]]
869
+ return { node: ['{}', [';', ...splice]], changed: true, splice }
870
+ }
871
+ }
872
+ const op = stmt[0]
873
+ if (op === ';') {
874
+ let changed = false
875
+ const next = [';']
876
+ for (let i = 1; i < stmt.length; i++) {
877
+ const r = inlineInStmt(stmt[i], candidates)
878
+ changed ||= r.changed
879
+ if (r.splice) next.push(...r.splice)
880
+ else next.push(r.node)
881
+ }
882
+ return changed ? { node: next, changed } : { node: stmt, changed: false }
883
+ }
884
+ if (op === '{}') {
885
+ const r = inlineInStmt(stmt[1], candidates)
886
+ return r.changed ? { node: ['{}', r.node], changed: true } : { node: stmt, changed: false }
887
+ }
888
+ if (op === 'for') {
889
+ const r = inlineInStmt(stmt[4], candidates)
890
+ return r.changed ? { node: ['for', stmt[1], stmt[2], stmt[3], r.node], changed: true } : { node: stmt, changed: false }
891
+ }
892
+ if (op === 'while') {
893
+ const r = inlineInStmt(stmt[2], candidates)
894
+ return r.changed ? { node: ['while', stmt[1], r.node], changed: true } : { node: stmt, changed: false }
895
+ }
896
+ if (op === 'if') {
897
+ const thenR = inlineInStmt(stmt[2], candidates)
898
+ const elseR = stmt.length > 3 ? inlineInStmt(stmt[3], candidates) : null
899
+ if (thenR.changed || elseR?.changed) return {
900
+ node: stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node],
901
+ changed: true,
902
+ }
903
+ }
904
+ if (op === 'try' || op === 'catch' || op === 'finally') {
905
+ let changed = false
906
+ const next = [op]
907
+ for (let i = 1; i < stmt.length; i++) {
908
+ const part = stmt[i]
909
+ const r = Array.isArray(part) ? inlineInStmt(part, candidates) : { node: part, changed: false }
910
+ changed ||= r.changed
911
+ next.push(r.node)
912
+ }
913
+ return changed ? { node: next, changed: true } : { node: stmt, changed: false }
914
+ }
915
+ return { node: stmt, changed: false }
916
+ }
917
+
918
+ const inlineHotInternalCalls = (programFacts, ast) => {
919
+ const cfg = ctx.transform.optimize
920
+ if (cfg && cfg.sourceInline === false) return false
921
+
922
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
923
+ const sitesByCallee = new Map()
924
+ for (const cs of programFacts.callSites) {
925
+ const list = sitesByCallee.get(cs.callee)
926
+ if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
927
+ }
928
+
929
+ const containsNode = (root, needle, inLoop = false) => {
930
+ if (root === needle) return inLoop
931
+ if (!Array.isArray(root) || root[0] === '=>') return false
932
+ const nextInLoop = inLoop || LOOP_OPS.has(root[0])
933
+ for (let i = 1; i < root.length; i++) if (containsNode(root[i], needle, nextInLoop)) return true
934
+ return false
935
+ }
936
+
937
+ const hasFixedTypedArraySites = (func, sites) => {
938
+ const params = func.sig?.params || []
939
+ if (!sites?.length) return false
940
+ return sites.every(site => params.some((p, i) => {
941
+ const arg = site.argList[i]
942
+ return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
943
+ }))
944
+ }
945
+ const hasFixedTypedArrayHotSites = (func, sites) =>
946
+ hasFixedTypedArraySites(func, sites) && sites.some(site => site.callerFunc?.body && containsNode(site.callerFunc.body, site.node))
947
+
948
+ const candidates = new Map()
949
+ for (const func of ctx.func.list) {
950
+ if (func.exported || func.raw || !func.body || func.rest || programFacts.valueUsed.has(func.name)) continue
951
+ if (func.defaults && Object.keys(func.defaults).length) continue
952
+ const sites = sitesByCallee.get(func.name)
953
+ const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
954
+ if (!sites || sites.length < 1 || (!fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
955
+ const stmts = blockStmts(func.body)
956
+ // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
957
+ // return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
958
+ if (scanBody(func.body, n => n[0] === '=>')) continue
959
+ // throw/break/continue are unsupported; return is OK if it's a single
960
+ // trailing return (rewritten to a value at inlining time).
961
+ if (scanBody(func.body, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) continue
962
+ let returnCount = 0
963
+ scanBody(func.body, n => { if (n[0] === 'return') returnCount++; return false })
964
+ if (returnCount > 1) continue
965
+ if (returnCount === 1 && stmts) {
966
+ const last = stmts[stmts.length - 1]
967
+ if (!Array.isArray(last) || last[0] !== 'return') continue
968
+ }
969
+ // Either a kernel (has a loop) or a tiny leaf (no loop, no calls, small body).
970
+ // The leaf branch catches helpers like `isAlpha(c) => (c>=65 && c<=90) || …`
971
+ // that get hammered from a hot caller's loop — replacing the call with its
972
+ // body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
973
+ const hasLoop = scanBody(func.body, n => LOOP_OPS.has(n[0]))
974
+ if (!hasLoop) {
975
+ if (scanBody(func.body, n => n[0] === '()')) continue
976
+ if (nodeSize(func.body) > 30) continue
977
+ }
978
+ if (scanBody(func.body, n => n[0] === '()' && n[1] === func.name)) continue
979
+ // Kernels with nested loops (depth ≥ 2) are typically large and the inner
980
+ // loop carries most of the cost. Inlining them into a host that V8 can't
981
+ // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
982
+ // Keep them as standalone functions so V8 wasm tier-up can warm them.
983
+ if (loopDepth(func.body, 0) >= 2 && !fixedTypedArraySite) continue
984
+ // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
985
+ // object/array literals returned) break downstream pointer-ABI specialization
986
+ // when inlined: narrow.js can't trace the post-inline alias chain back to a
987
+ // single ctor, so the typed-array param of a callee like processCascade(x, …)
988
+ // stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
989
+ // Keeping the factory as a callable function preserves the call-site type fact.
990
+ if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
991
+ candidates.set(func.name, func)
992
+ }
993
+ if (!candidates.size) return false
994
+
995
+ // Trivial expr-bodied candidates can be substituted at any expression position
996
+ // (if-condition, ternary, etc.). Stmt-bodied ones go through inlineInStmt's
997
+ // statement-level path which preserves prefix ordering.
998
+ const exprOnlyCandidates = new Map()
999
+ for (const [name, func] of candidates) {
1000
+ if (!Array.isArray(func.body) || func.body[0] !== '{}') exprOnlyCandidates.set(name, func)
1001
+ }
1002
+
1003
+ let changed = false
1004
+ const exportedCandidates = new Map()
1005
+ for (const [name, func] of candidates) {
1006
+ const sites = sitesByCallee.get(name)
1007
+ if (hasFixedTypedArraySites(func, sites) &&
1008
+ !sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))) {
1009
+ exportedCandidates.set(name, func)
1010
+ }
1011
+ }
1012
+ for (const func of ctx.func.list) {
1013
+ if (!func.body || func.raw) continue
1014
+ // Skip exports: they're entry points usually invoked once. Inlining a
1015
+ // hot kernel here would put the loop into a function V8's wasm tier-up
1016
+ // never warms (kernel stays in baseline). Keeping the kernel as its own
1017
+ // callable function lets V8 promote it to TurboFan after a few calls.
1018
+ // Exception: fixed-size typed-array callees should inline into the exported
1019
+ // caller so scalar replacement can cross the call boundary and remove the
1020
+ // caller's heap arrays.
1021
+ const activeCandidates = func.exported ? exportedCandidates : candidates
1022
+ if (func.exported && !activeCandidates.size) continue
1023
+ const r = inlineInStmt(func.body, activeCandidates)
1024
+ let body = r.changed ? r.node : func.body
1025
+ let bodyChanged = r.changed
1026
+ if (!func.exported && exprOnlyCandidates.size) {
1027
+ const e = inlineInExpr(body, exprOnlyCandidates)
1028
+ if (e.changed) { body = e.node; bodyChanged = true }
1029
+ }
1030
+ if (bodyChanged) { func.body = body; changed = true }
1031
+ }
1032
+ if (ast) {
1033
+ const r = inlineInStmt(ast, candidates)
1034
+ if (r.changed) changed = true
1035
+ }
1036
+ return changed
1037
+ }
1038
+
1039
+ const restIndexExpr = (idx, restParams) => {
1040
+ const k = intLit(idx)
1041
+ if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
1042
+
1043
+ let out = [, undefined]
1044
+ for (let i = restParams.length - 1; i >= 0; i--) {
1045
+ out = ['?:', ['==', clonePlain(idx), [, i]], restParams[i], out]
1046
+ }
1047
+ return out
1048
+ }
1049
+
1050
+ const rewriteRestBody = (node, restName, restParams) => {
1051
+ if (typeof node === 'string') return node === restName ? { ok: false } : { ok: true, node }
1052
+ if (!Array.isArray(node)) return { ok: true, node }
1053
+ if (node[0] === 'str') return { ok: true, node: node.slice() }
1054
+
1055
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === restName) {
1056
+ return node[2] === 'length' ? { ok: true, node: [, restParams.length] } : { ok: false }
1057
+ }
1058
+
1059
+ if (node[0] === '[]' && node[1] === restName) {
1060
+ if (!isSimpleArg(node[2])) return { ok: false }
1061
+ return { ok: true, node: restIndexExpr(node[2], restParams) }
1062
+ }
1063
+
1064
+ const out = [node[0]]
1065
+ for (let i = 1; i < node.length; i++) {
1066
+ const r = rewriteRestBody(node[i], restName, restParams)
1067
+ if (!r.ok) return r
1068
+ out.push(r.node)
1069
+ }
1070
+ return { ok: true, node: out }
1071
+ }
1072
+
1073
+ const specializeFixedRestCalls = (programFacts) => {
1074
+ const sitesByKey = new Map()
1075
+ for (const site of programFacts.callSites) {
1076
+ const func = ctx.func.map.get(site.callee)
1077
+ if (!func?.rest || func.exported || func.raw || !func.body) continue
1078
+ if (programFacts.valueUsed.has(func.name)) continue
1079
+ if (func.defaults && Object.keys(func.defaults).length) continue
1080
+ if (site.argList.some(a => Array.isArray(a) && a[0] === '...')) continue
1081
+
1082
+ const fixedN = func.sig.params.length - 1
1083
+ const restN = Math.max(0, site.argList.length - fixedN)
1084
+ const key = `${func.name}/${restN}`
1085
+ const list = sitesByKey.get(key)
1086
+ if (list) list.push(site); else sitesByKey.set(key, [site])
1087
+ }
1088
+
1089
+ let changed = false
1090
+ for (const [key, sites] of sitesByKey) {
1091
+ const [name, restNText] = key.split('/')
1092
+ const func = ctx.func.map.get(name)
1093
+ const restN = Number(restNText)
1094
+ const fixedParams = func.sig.params.slice(0, -1).map(p => ({ ...p }))
1095
+ const restName = func.rest
1096
+ const restParams = Array.from({ length: restN }, (_, i) => `${restName}${T}r${restN}_${i}`)
1097
+ const rewritten = rewriteRestBody(func.body, restName, restParams)
1098
+ if (!rewritten.ok) continue
1099
+
1100
+ const cloneName = `${name}${T}rest${restN}`
1101
+ if (!ctx.func.map.has(cloneName)) {
1102
+ const restSigParams = restParams.map(name => ({ name, type: 'f64' }))
1103
+ const clone = {
1104
+ ...func,
1105
+ name: cloneName,
1106
+ exported: false,
1107
+ rest: null,
1108
+ sig: {
1109
+ ...func.sig,
1110
+ params: [...fixedParams, ...restSigParams],
1111
+ results: [...func.sig.results],
1112
+ },
1113
+ body: rewritten.node,
1114
+ }
1115
+ delete clone.defaults
1116
+ ctx.func.list.push(clone)
1117
+ ctx.func.names.add(cloneName)
1118
+ ctx.func.map.set(cloneName, clone)
1119
+ }
1120
+
1121
+ const fixedN = func.sig.params.length - 1
1122
+ for (const site of sites) {
1123
+ site.node[1] = cloneName
1124
+ setCallArgs(site.node, site.argList.slice(0, fixedN + restN))
1125
+ changed = true
1126
+ }
1127
+ }
1128
+ return changed
1129
+ }
1130
+
1131
+ const scanGlobalValueFacts = (root) => {
1132
+ if (!root) return
1133
+ const stmts = Array.isArray(root) && root[0] === ';' ? root.slice(1) : [root]
1134
+ for (const stmt of stmts) {
1135
+ if (!Array.isArray(stmt) || (stmt[0] !== 'const' && stmt[0] !== 'let')) continue
1136
+ for (const decl of stmt.slice(1)) {
1137
+ if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
1138
+ const vt = valTypeOf(decl[2])
1139
+ if (vt) {
1140
+ if (!ctx.scope.globalValTypes) ctx.scope.globalValTypes = new Map()
1141
+ ctx.scope.globalValTypes.set(decl[1], vt)
1142
+ if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(decl[1], decl[2])
1143
+ }
1144
+ const ctor = typedElemCtor(decl[2])
1145
+ if (ctor) {
1146
+ if (!ctx.scope.globalTypedElem) ctx.scope.globalTypedElem = new Map()
1147
+ ctx.scope.globalTypedElem.set(decl[1], ctor)
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ const unboxConstTypedGlobals = () => {
1154
+ if (!ctx.scope.globalTypedElem || !ctx.scope.consts) return
1155
+ for (const [name, ctor] of ctx.scope.globalTypedElem) {
1156
+ if (!ctx.scope.consts.has(name)) continue
1157
+ if (ctx.scope.globalValTypes?.get(name) !== VAL.TYPED) continue
1158
+ const aux = typedElemAux(ctor)
1159
+ if (aux == null) continue
1160
+ const decl = ctx.scope.globals.get(name)
1161
+ if (typeof decl !== 'string' || !decl.includes('mut f64')) continue
1162
+ ctx.scope.globals.set(name, `(global $${name} (mut i32) (i32.const 0))`)
1163
+ ctx.scope.globalTypes.set(name, 'i32')
1164
+ updateGlobalRep(name, { ptrKind: VAL.TYPED, ptrAux: aux })
1165
+ }
1166
+ }
1167
+
1168
+ const materializeAutoBoxSchemas = (programFacts) => {
1169
+ if (!ctx.schema.register) return
1170
+ for (const [name, props] of programFacts.propMap) {
1171
+ if (ctx.schema.vars.has(name)) {
1172
+ const existing = ctx.schema.resolve(name)
1173
+ const newProps = [...props].filter(prop => !existing.includes(prop))
1174
+ if (newProps.length) {
1175
+ const merged = [...existing, ...newProps]
1176
+ const mergedId = ctx.schema.register(merged)
1177
+ ctx.schema.vars.set(name, mergedId)
1178
+ }
1179
+ continue
1180
+ }
1181
+ const valueProps = [...props].filter(prop => !ctx.func.names.has(`${name}$${prop}`))
1182
+ if (!valueProps.length) continue
1183
+ const allProps = [...props]
1184
+ const schema = ['__inner__', ...allProps]
1185
+ const schemaId = ctx.schema.register(schema)
1186
+ ctx.schema.vars.set(name, schemaId)
1187
+ if (ctx.func.names.has(name) && !ctx.scope.globals.has(name))
1188
+ ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
1189
+ if (!ctx.schema.autoBox) ctx.schema.autoBox = new Map()
1190
+ ctx.schema.autoBox.set(name, { schemaId, schema })
1191
+ }
1192
+ }
1193
+
1194
+ const resolveClosureWidth = (programFacts) => {
1195
+ if (!ctx.closure.make) return
1196
+ const { hasSpread, hasRest, maxCall, maxDef } = programFacts
1197
+ const floor = ctx.closure.floor ?? 0
1198
+ ctx.closure.width = (hasSpread && hasRest)
1199
+ ? MAX_CLOSURE_ARITY
1200
+ : Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), floor))
1201
+ }
1202
+
1203
+ const canSkipWholeProgramNarrowing = (programFacts) =>
1204
+ programFacts.callSites.length === 0 &&
1205
+ programFacts.valueUsed.size === 0 &&
1206
+ !programFacts.anyDyn &&
1207
+ programFacts.propMap.size === 0 &&
1208
+ !programFacts.hasSchemaLiterals &&
1209
+ !ctx.closure.make
1210
+
1211
+ export default function plan(ast) {
1212
+ scanGlobalValueFacts(ast)
1213
+ unboxConstTypedGlobals()
1214
+
1215
+ let programFacts = collectProgramFacts(ast)
1216
+ if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
1217
+ if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
1218
+ if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
1219
+ if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
1220
+ if (scalarizeFunctionTypedArrays(programFacts)) programFacts = collectProgramFacts(ast)
1221
+ ctx.types.dynKeyVars = programFacts.dynVars
1222
+ ctx.types.anyDynKey = programFacts.anyDyn
1223
+
1224
+ materializeAutoBoxSchemas(programFacts)
1225
+ resolveClosureWidth(programFacts)
1226
+ if (canSkipWholeProgramNarrowing(programFacts)) return programFacts
1227
+
1228
+ narrowSignatures(programFacts, ast)
1229
+ specializeBimorphicTyped(programFacts)
1230
+ refineDynKeys(programFacts)
1231
+
1232
+ return programFacts
1233
+ }