jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,679 @@
1
+ /**
2
+ * Call-graph reshaping — three related transforms that fold dynamic dispatch
3
+ * into static control flow:
4
+ *
5
+ * - `inlineHotInternalCalls` — non-exported call sites whose callee is a
6
+ * small, simple-arg function get the body
7
+ * spliced in. Threshold-gated by callsite
8
+ * count and loop depth (`programFacts`).
9
+ * - `inlineLocalLambdas` — `const f = (a) => …; … f(x) …` inside one
10
+ * function body, where `f` is non-escaping,
11
+ * splices the lambda body at each call.
12
+ * - `specializeFixedRestCalls` — `f(...args)` with statically-known argc
13
+ * produces a clone of `f` whose rest param
14
+ * is destructured at fixed indices; the
15
+ * spread call collapses to a fixed-arity one.
16
+ *
17
+ * Each transform mutates `ctx.func.list` / `func.body` and returns `boolean`
18
+ * indicating whether anything changed (so `plan()` knows to invalidate the
19
+ * program-facts cache).
20
+ *
21
+ * @module compile/plan/inline
22
+ */
23
+
24
+ import { ctx } from '../../ctx.js'
25
+ import {
26
+ callArgs, setCallArgs, some, blockStmts, T, refsName, refsAny, REFS_IN_EXPR,
27
+ extractParams,
28
+ } from '../../ast.js'
29
+ import { cloneWithSubst } from '../../type.js'
30
+ import { constIntExpr } from '../../static.js'
31
+ import { analyzeBody } from '../analyze.js'
32
+ import {
33
+ LOOP_OPS, isSimpleArg, mutatesAny, loopDepth, nodeSize, clonePlain, collectBindings,
34
+ fixedTypedArraysInBody, forLoopBodyIndex, withForLoopBody,
35
+ } from './common.js'
36
+
37
+ // Returns { prefix, value } where prefix is the substituted body statements
38
+ // (excluding any trailing `return X`), and value is the substituted return
39
+ // expression — null if void or no trailing return value.
40
+ const inlinedBody = (func, args) => {
41
+ const params = func.sig.params
42
+ if (args.length !== params.length || !args.every(isSimpleArg)) return null
43
+ const paramNames = new Set(params.map(p => p.name))
44
+ if (mutatesAny(func.body, paramNames)) return null
45
+
46
+ const subst = new Map()
47
+ for (let i = 0; i < params.length; i++) subst.set(params[i].name, args[i])
48
+
49
+ const locals = new Set()
50
+ collectBindings(func.body, locals)
51
+ for (const p of params) locals.delete(p.name)
52
+
53
+ const rename = new Map()
54
+ for (const name of locals) rename.set(name, `${T}inl${ctx.func.uniq++}_${name}`)
55
+
56
+ const stmts = blockStmts(func.body)
57
+ // Expression-bodied arrow `(c) => expr`: no statement block; the whole body
58
+ // *is* the return value. Treat as zero-prefix + value.
59
+ if (!stmts) return { prefix: [], value: cloneWithSubst(func.body, subst, rename) }
60
+ const last = stmts.length ? stmts[stmts.length - 1] : null
61
+ const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
62
+ const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
63
+ const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
64
+ const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
65
+ return { prefix, value }
66
+ }
67
+
68
+ const stmtDeclName = (stmt) => {
69
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) return null
70
+ const decl = stmt[1]
71
+ return Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' ? decl[1] : null
72
+ }
73
+
74
+ const whileInductionVar = (cond) => {
75
+ if (typeof cond === 'string') return cond
76
+ if (!Array.isArray(cond)) return null
77
+ const op = cond[0]
78
+ if ((op === '<' || op === '<=' || op === '>' || op === '>=') && typeof cond[1] === 'string') return cond[1]
79
+ return null
80
+ }
81
+
82
+ // When splicing an inlined kernel into a loop, hoist leading decls that do not
83
+ // reference the loop induction var (e.g. floatbeat chord tables).
84
+ const partitionInvariantPrefix = (prefix, variantNames) => {
85
+ if (!prefix.length || !variantNames?.size) return { hoisted: [], rest: prefix }
86
+ const hoisted = []
87
+ let i = 0
88
+ for (; i < prefix.length; i++) {
89
+ const s = prefix[i]
90
+ if (!stmtDeclName(s) || refsAny(s, variantNames, REFS_IN_EXPR)) break
91
+ hoisted.push(s)
92
+ }
93
+ return { hoisted, rest: prefix.slice(i) }
94
+ }
95
+
96
+ const spliceInlinedShape = (prefix, valueStmt, loopVariantNames) => {
97
+ const { hoisted, rest } = partitionInvariantPrefix(prefix, loopVariantNames)
98
+ const splice = [...rest, valueStmt]
99
+ return { node: ['{}', [';', ...splice]], splice, hoisted, changed: true }
100
+ }
101
+
102
+ const isCandidateCall = (node, candidates) =>
103
+ Array.isArray(node) && node[0] === '()' && typeof node[1] === 'string' && candidates.has(node[1])
104
+
105
+ // Prefix flattening for expression-position inlining. A helper like
106
+ // let distance = (x1,y1,x2,y2) => { let dx=x1-x2; let dy=y1-y2; return Math.sqrt(dx*dx+dy*dy) }
107
+ // called as `Math.sin(distance(...) * res)` can't splice statements into an
108
+ // expression context — but when every prefix stmt is `let name = <pure
109
+ // arithmetic>`, substituting the decls into the return value turns it into a
110
+ // zero-prefix expression. Duplicated subtrees (`dx` used twice → `x1-x2`
111
+ // twice) are pure, and the watr-layer CSE collapses the copies.
112
+ const PURE_FLATTEN_OPS = new Set(['+', '-', '*', '/', '%', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>'])
113
+ const pureFlattenExpr = (n) => {
114
+ if (typeof n === 'number' || typeof n === 'string') return true // literal or ident
115
+ if (!Array.isArray(n)) return false
116
+ const op = n[0]
117
+ if (op == null) return true // boxed literal [null, v]
118
+ if (op === '()' && n.length === 2) return pureFlattenExpr(n[1]) // grouping parens
119
+ return PURE_FLATTEN_OPS.has(op) && n.slice(1).every(pureFlattenExpr)
120
+ }
121
+ const substIdents = (n, subst) => {
122
+ if (typeof n === 'string') return subst.get(n) ?? n
123
+ if (!Array.isArray(n)) return n
124
+ return n.map((c, i) => i === 0 ? c : substIdents(c, subst))
125
+ }
126
+ const flattenPrefix = (shape) => {
127
+ if (!shape || shape.value === null || !shape.prefix.length) return shape
128
+ const subst = new Map()
129
+ for (const stmt of shape.prefix) {
130
+ if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) return null
131
+ const d = stmt[1]
132
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string' || !pureFlattenExpr(d[2])) return null
133
+ subst.set(d[1], substIdents(d[2], subst)) // earlier decls feed later RHSs
134
+ }
135
+ const value = substIdents(shape.value, subst)
136
+ if (nodeSize(value) > 100) return null // duplication blow-up guard
137
+ return { prefix: [], value }
138
+ }
139
+
140
+ // Recursively substitute calls to expr-bodied candidates anywhere in `node`.
141
+ // Used for tiny pure-expression helpers (`isAlpha(c) => …`) that get called
142
+ // from expression contexts (if-conditions, ternary tests). For these the
143
+ // inlined body is value-only (zero prefix), so a pure substitution is safe.
144
+ const inlineInExpr = (node, candidates) => {
145
+ if (!Array.isArray(node)) return { node, changed: false }
146
+ if (node[0] === '=>') return { node, changed: false }
147
+ let changed = false
148
+ const next = [node[0]]
149
+ for (let i = 1; i < node.length; i++) {
150
+ const r = inlineInExpr(node[i], candidates)
151
+ if (r.changed) changed = true
152
+ next.push(r.node)
153
+ }
154
+ if (isCandidateCall(next, candidates)) {
155
+ const args = callArgs(next)
156
+ const shape = flattenPrefix(args && inlinedBody(candidates.get(next[1]), args))
157
+ if (shape && shape.value !== null && shape.prefix.length === 0) {
158
+ return { node: shape.value, changed: true }
159
+ }
160
+ }
161
+ return { node: changed ? next : node, changed }
162
+ }
163
+
164
+ const inlineInStmt = (stmt, candidates, loopVariantNames = null) => {
165
+ if (!Array.isArray(stmt)) return { node: stmt, changed: false }
166
+ // Statement-position call: discard return value, splice prefix in place.
167
+ if (isCandidateCall(stmt, candidates)) {
168
+ const args = callArgs(stmt)
169
+ const shape = args && inlinedBody(candidates.get(stmt[1]), args)
170
+ if (shape) {
171
+ const { hoisted, rest } = partitionInvariantPrefix(shape.prefix, loopVariantNames)
172
+ return { node: ['{}', [';', ...rest]], changed: true, splice: rest, hoisted }
173
+ }
174
+ }
175
+ // `let/const X = call(...)` with single decl: inline as prefix + decl(value).
176
+ if ((stmt[0] === 'let' || stmt[0] === 'const') && stmt.length === 2) {
177
+ const decl = stmt[1]
178
+ if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && isCandidateCall(decl[2], candidates)) {
179
+ const args = callArgs(decl[2])
180
+ const shape = args && inlinedBody(candidates.get(decl[2][1]), args)
181
+ if (shape && shape.value !== null) {
182
+ const { hoisted, rest } = partitionInvariantPrefix(shape.prefix, loopVariantNames)
183
+ const splice = [...rest, [stmt[0], ['=', decl[1], shape.value]]]
184
+ return { node: ['{}', [';', ...splice]], changed: true, splice, hoisted }
185
+ }
186
+ }
187
+ }
188
+ // `X = call(...)` at statement position: inline as prefix + assign(value).
189
+ // LHS may be a name or an indexed lvalue (`out[i] = beat(...)` in fill loops).
190
+ if (stmt[0] === '=' && isCandidateCall(stmt[2], candidates)) {
191
+ const args = callArgs(stmt[2])
192
+ const shape = args && inlinedBody(candidates.get(stmt[2][1]), args)
193
+ if (shape && shape.value !== null) {
194
+ return spliceInlinedShape(shape.prefix, ['=', stmt[1], shape.value], loopVariantNames)
195
+ }
196
+ }
197
+ const op = stmt[0]
198
+ if (op === ';') {
199
+ let changed = false
200
+ const next = [';']
201
+ for (let i = 1; i < stmt.length; i++) {
202
+ const r = inlineInStmt(stmt[i], candidates, loopVariantNames)
203
+ changed ||= r.changed
204
+ if (r.hoisted?.length) {
205
+ next.push(...r.hoisted)
206
+ changed = true
207
+ }
208
+ if (r.splice) next.push(...r.splice)
209
+ else next.push(r.node)
210
+ }
211
+ return changed ? { node: next, changed: true } : { node: stmt, changed: false }
212
+ }
213
+ if (op === '{}') {
214
+ const r = inlineInStmt(stmt[1], candidates, loopVariantNames)
215
+ if (!r.changed) return { node: stmt, changed: false }
216
+ // If the child was itself a candidate call (or a let/assign-of-call), it
217
+ // already returned a `['{}', [';', ...prefix]]` shape. Re-wrapping here
218
+ // would yield `['{}', ['{}', …]]`, which codegen rejects ("Unknown op: {}").
219
+ if (Array.isArray(r.node) && r.node[0] === '{}') return { node: r.node, changed: true, hoisted: r.hoisted }
220
+ return { node: ['{}', r.node], changed: true, hoisted: r.hoisted }
221
+ }
222
+ if (op === 'for') {
223
+ const idx = forLoopBodyIndex(stmt)
224
+ const vars = loopVariantNames ? new Set(loopVariantNames) : new Set()
225
+ const r = inlineInStmt(stmt[idx], candidates, vars.size ? vars : null)
226
+ if (!r.changed) return { node: stmt, changed: false }
227
+ return { node: withForLoopBody(stmt, r.node), changed: true, hoisted: r.hoisted }
228
+ }
229
+ if (op === 'while') {
230
+ const vars = loopVariantNames ? new Set(loopVariantNames) : new Set()
231
+ const ind = whileInductionVar(stmt[1])
232
+ if (ind) vars.add(ind)
233
+ const r = inlineInStmt(stmt[2], candidates, vars.size ? vars : null)
234
+ if (!r.changed) return { node: stmt, changed: false }
235
+ return { node: ['while', stmt[1], r.node], changed: true, hoisted: r.hoisted }
236
+ }
237
+ if (op === 'if') {
238
+ const thenR = inlineInStmt(stmt[2], candidates, loopVariantNames)
239
+ const elseR = stmt.length > 3 ? inlineInStmt(stmt[3], candidates, loopVariantNames) : null
240
+ if (thenR.changed || elseR?.changed) return {
241
+ node: stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node],
242
+ changed: true,
243
+ hoisted: [...(thenR.hoisted || []), ...(elseR?.hoisted || [])],
244
+ }
245
+ }
246
+ if (op === 'try' || op === 'catch' || op === 'finally') {
247
+ let changed = false
248
+ const next = [op]
249
+ let hoisted = []
250
+ for (let i = 1; i < stmt.length; i++) {
251
+ const part = stmt[i]
252
+ const r = Array.isArray(part) ? inlineInStmt(part, candidates, loopVariantNames) : { node: part, changed: false }
253
+ changed ||= r.changed
254
+ if (r.hoisted?.length) hoisted = hoisted.concat(r.hoisted)
255
+ next.push(r.node)
256
+ }
257
+ return changed ? { node: next, changed: true, hoisted } : { node: stmt, changed: false }
258
+ }
259
+ return { node: stmt, changed: false }
260
+ }
261
+
262
+ export const inlineHotInternalCalls = (programFacts, ast) => {
263
+ const cfg = ctx.transform.optimize
264
+ if (cfg && cfg.sourceInline === false) return false
265
+
266
+ const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
267
+ const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
268
+ const sitesByCallee = new Map()
269
+ for (const cs of programFacts.callSites) {
270
+ const list = sitesByCallee.get(cs.callee)
271
+ if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
272
+ }
273
+
274
+ const containsNode = (root, needle, inLoop = false) => {
275
+ if (root === needle) return inLoop
276
+ if (!Array.isArray(root) || root[0] === '=>') return false
277
+ const nextInLoop = inLoop || LOOP_OPS.has(root[0])
278
+ for (let i = 1; i < root.length; i++) if (containsNode(root[i], needle, nextInLoop)) return true
279
+ return false
280
+ }
281
+
282
+ const hasFixedTypedArraySites = (func, sites) => {
283
+ const params = func.sig?.params || []
284
+ if (!sites?.length) return false
285
+ return sites.every(site => params.some((p, i) => {
286
+ const arg = site.argList[i]
287
+ return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
288
+ }))
289
+ }
290
+ const hasFullyFixedTypedArraySites = (func, sites) => {
291
+ const params = func.sig?.params || []
292
+ if (!sites?.length) return false
293
+ let sawTypedArg = false
294
+ for (const site of sites) {
295
+ const typed = typedByFunc.get(site.callerFunc)
296
+ const fixed = fixedByFunc.get(site.callerFunc)
297
+ for (let i = 0; i < params.length; i++) {
298
+ const arg = site.argList[i]
299
+ if (typeof arg !== 'string' || !typed?.has(arg)) continue
300
+ sawTypedArg = true
301
+ if (!fixed?.has(arg)) return false
302
+ }
303
+ }
304
+ return sawTypedArg
305
+ }
306
+
307
+ const candidates = new Map()
308
+ // Forwarders — a candidate whose body calls one of its own parameters.
309
+ // Inlining one replaces that parameter with the call-site argument; when the
310
+ // argument is a known function name the resulting indirect call collapses to
311
+ // a direct `call` (devirtualization).
312
+ const forwarders = new Set()
313
+ // Loop-free leaves — safe to splice into EXPORTED callers too: the tier-up
314
+ // rationale for skipping exports concerns relocating loop KERNELS into cold
315
+ // entry points, not pulling a leaf INTO an export's hot loop (game-of-life's
316
+ // step calls rot per cell; pre-Turboshaft wasm tiers never inline calls).
317
+ const leaves = new Set()
318
+ for (const func of ctx.func.list) {
319
+ const sites = sitesByCallee.get(func.name)
320
+ // Exported leaf/kernel with exactly one internal caller (e.g. fill→beat in
321
+ // floatbeat): inline into the caller's loop but keep the export for external
322
+ // one-off calls (bench beat()). Multi-caller exports stay outlined so V8 can
323
+ // tier-up shared kernels.
324
+ const soleCallerExport = func.exported && sites?.length === 1
325
+ if (func.raw || !func.body || func.rest) continue
326
+ if (func.exported && !soleCallerExport) continue
327
+ if (programFacts.valueUsed.has(func.name) && !soleCallerExport) continue
328
+ if (func.defaults && Object.keys(func.defaults).length) continue
329
+ const paramNames = new Set((func.sig?.params || []).map(p => p.name))
330
+ if (paramNames.size && some(func.body, n => {
331
+ if (n[0] !== '()' || !Array.isArray(n[1]) || n[1][0] !== '.') return false
332
+ const [, obj, prop] = n[1]
333
+ return prop === 'push' && typeof obj === 'string' && paramNames.has(obj)
334
+ })) continue
335
+ const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
336
+ const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
337
+ const hasLoop = some(func.body, n => LOOP_OPS.has(n[0]))
338
+ const isTinyLeaf = !hasLoop && nodeSize(func.body) <= 15
339
+ if (!sites || sites.length < 1 || (!isTinyLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
340
+ const stmts = blockStmts(func.body)
341
+ // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
342
+ // return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
343
+ if (some(func.body, n => n[0] === '=>')) continue
344
+ // throw/break/continue are unsupported; return is OK if it's a single
345
+ // trailing return (rewritten to a value at inlining time).
346
+ if (some(func.body, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) continue
347
+ let returnCount = 0
348
+ some(func.body, n => { if (n[0] === 'return') returnCount++; return false })
349
+ if (returnCount > 1) continue
350
+ if (returnCount === 1 && stmts) {
351
+ const last = stmts[stmts.length - 1]
352
+ if (!Array.isArray(last) || last[0] !== 'return') continue
353
+ }
354
+ // Either a kernel (has a loop) or a tiny leaf (no loop, no calls, small body).
355
+ // The leaf branch catches helpers like `isAlpha(c) => (c>=65 && c<=90) || …`
356
+ // that get hammered from a hot caller's loop — replacing the call with its
357
+ // body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
358
+ if (!hasLoop) {
359
+ if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]))) continue
360
+ // Per-iteration call overhead dwarfs body-size bloat when EVERY site sits
361
+ // inside a caller's loop (game-of-life's rot: ~40 nodes × 2 sites, fired
362
+ // for most of 260k cells/frame). V8's pre-Turboshaft wasm tiers never
363
+ // inline cross-function, so an out-of-line leaf in a hot loop is a hard
364
+ // per-cell tax on Node ≤ 22 — and still saves call setup on newer tiers.
365
+ const allSitesInLoop = sites.every(site =>
366
+ site.callerFunc?.body && containsNode(site.callerFunc.body, site.node, false))
367
+ if (nodeSize(func.body) > (allSitesInLoop ? 80 : 30)) continue
368
+ }
369
+ if (some(func.body, n => n[0] === '()' && n[1] === func.name)) continue
370
+ // Kernels with nested loops (depth ≥ 2) are typically large and the inner
371
+ // loop carries most of the cost. Inlining them into a host that V8 can't
372
+ // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
373
+ // Keep them as standalone functions so V8 wasm tier-up can warm them.
374
+ if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
375
+ // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
376
+ // object/array literals returned) break downstream pointer-ABI specialization
377
+ // when inlined: narrow.js can't trace the post-inline alias chain back to a
378
+ // single ctor, so the typed-array param of a callee like processCascade(x, …)
379
+ // stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
380
+ // Keeping the factory as a callable function preserves the call-site type fact.
381
+ if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
382
+ if (paramNames.size && some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && paramNames.has(n[1])))
383
+ forwarders.add(func.name)
384
+ if (!hasLoop) leaves.add(func.name)
385
+ candidates.set(func.name, func)
386
+ }
387
+ if (!candidates.size) return false
388
+
389
+ // Trivial expr-bodied candidates can be substituted at any expression position
390
+ // (if-condition, ternary, etc.). Stmt-bodied ones go through inlineInStmt's
391
+ // statement-level path which preserves prefix ordering — EXCEPT flattenable
392
+ // block bodies (pure-arith let decls + trailing return, e.g. distance's
393
+ // dx/dy), which inlineInExpr turns into zero-prefix expressions per site.
394
+ const flattenableBody = (func) => {
395
+ const stmts = blockStmts(func.body)
396
+ if (!stmts) return false
397
+ return stmts.every((s, i) => i === stmts.length - 1
398
+ ? Array.isArray(s) && s[0] === 'return'
399
+ : Array.isArray(s) && (s[0] === 'let' || s[0] === 'const') && s.length === 2
400
+ && Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' && pureFlattenExpr(s[1][2]))
401
+ }
402
+ const exprOnlyCandidates = new Map()
403
+ for (const [name, func] of candidates) {
404
+ if (!Array.isArray(func.body) || func.body[0] !== '{}' || flattenableBody(func)) exprOnlyCandidates.set(name, func)
405
+ }
406
+
407
+ let changed = false
408
+ const exportedCandidates = new Map()
409
+ for (const [name, func] of candidates) {
410
+ const sites = sitesByCallee.get(name)
411
+ const fixedSiteExported = hasFixedTypedArraySites(func, sites) &&
412
+ !sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))
413
+ // Forwarders cross into an exported caller too: the tier-up rationale that
414
+ // keeps candidates out of exports concerns relocated loop kernels, not
415
+ // these tiny leaves — and inlining one devirtualizes a closure dispatch.
416
+ if (fixedSiteExported || forwarders.has(name) || leaves.has(name) || sites?.length === 1) exportedCandidates.set(name, func)
417
+ }
418
+ for (const func of ctx.func.list) {
419
+ if (!func.body || func.raw) continue
420
+ // Skip exports: they're entry points usually invoked once. Inlining a
421
+ // hot kernel here would put the loop into a function V8's wasm tier-up
422
+ // never warms (kernel stays in baseline). Keeping the kernel as its own
423
+ // callable function lets V8 promote it to TurboFan after a few calls.
424
+ // Exception: fixed-size typed-array callees should inline into the exported
425
+ // caller so scalar replacement can cross the call boundary and remove the
426
+ // caller's heap arrays.
427
+ const activeCandidates = func.exported ? exportedCandidates : candidates
428
+ if (func.exported && !activeCandidates.size) continue
429
+ // Expression-bodied arrows (`() => expr`) have func.body as the return
430
+ // value itself — never a `{}` block. inlineInStmt treats its argument as a
431
+ // statement (discards the return value of any top-level candidate call),
432
+ // which would turn `() => x()` into an empty block and lose the result.
433
+ // Route those through inlineInExpr so the call is replaced by the inlined
434
+ // value expression instead.
435
+ const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
436
+ const r = isExprBody
437
+ ? inlineInExpr(func.body, activeCandidates)
438
+ : inlineInStmt(func.body, activeCandidates)
439
+ let body = r.changed ? r.node : func.body
440
+ let bodyChanged = r.changed
441
+ // Expression-position pass. Exported callers take the leaf-safe subset —
442
+ // the same tier-up rationale as the statement path (leaves into exports
443
+ // are fine; relocated kernels are not).
444
+ const exprActive = func.exported
445
+ ? new Map([...exprOnlyCandidates].filter(([n]) => exportedCandidates.has(n)))
446
+ : exprOnlyCandidates
447
+ if (exprActive.size) {
448
+ const e = inlineInExpr(body, exprActive)
449
+ if (e.changed) { body = e.node; bodyChanged = true }
450
+ }
451
+ if (bodyChanged) { func.body = body; changed = true }
452
+ }
453
+ if (ast) {
454
+ const r = inlineInStmt(ast, candidates)
455
+ if (r.changed) changed = true
456
+ }
457
+ return changed
458
+ }
459
+
460
+ // === Inline non-escaping local lambdas ===
461
+ // `const f = (a) => …; … f(x) …` → the lambda body substituted at each call
462
+ // site. A non-escaping lambda's captured free vars are still in lexical scope at
463
+ // the call site, so splicing the body in place preserves capture-by-reference
464
+ // semantics while eliminating the closure object (no env pointer, no NaN-box, no
465
+ // call_indirect). Mirrors inlineHotInternalCalls, scoped to one function body.
466
+
467
+ // True iff every textual reference to `name` in `node` is the callee of a
468
+ // `name(...)` call (i.e. the binding never escapes — never read as a value,
469
+ // reassigned, captured by a nested lambda, or shadowed).
470
+ const onlyCalledNotReferenced = (node, name) => {
471
+ if (typeof node === 'string') return node !== name
472
+ if (!Array.isArray(node)) return true
473
+ const op = node[0]
474
+ if (op === 'str') return true
475
+ // A nested lambda touching `name` at all (capture or shadowing param) → bail.
476
+ if (op === '=>') return !refsName(node[1], name, REFS_IN_EXPR) && !refsName(node[2], name, REFS_IN_EXPR)
477
+ if (op === '()' && node[1] === name) {
478
+ for (let i = 2; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
479
+ return true
480
+ }
481
+ if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
482
+ if (op === ':') return onlyCalledNotReferenced(node[2], name)
483
+ for (let i = 1; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
484
+ return true
485
+ }
486
+
487
+ const bodyStmtList = body =>
488
+ Array.isArray(body) && body[0] === '{}' ? blockStmts(body)
489
+ : Array.isArray(body) && body[0] === ';' ? body.slice(1)
490
+ : body == null ? [] : [body]
491
+
492
+ const removeStmts = (body, set) => {
493
+ if (!Array.isArray(body)) return set.has(body) ? null : body
494
+ if (body[0] === '{}') return ['{}', removeStmts(body[1], set) ?? [';']]
495
+ if (body[0] === ';') {
496
+ const kept = body.slice(1).filter(s => !set.has(s))
497
+ return kept.length === 0 ? null : kept.length === 1 ? kept[0] : [';', ...kept]
498
+ }
499
+ return set.has(body) ? null : body
500
+ }
501
+
502
+ // Lambda body must be a guaranteed-return shape inlinedBody can splice: ≤1
503
+ // `return` (trailing, if a block), no throw/break/continue, no param mutation,
504
+ // no nested lambda.
505
+ const inlinableLambdaBody = (abody, params) => {
506
+ if (some(abody, n => n[0] === '=>')) return false
507
+ if (some(abody, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) return false
508
+ let returns = 0
509
+ some(abody, n => { if (n[0] === 'return') returns++; return false })
510
+ if (returns > 1) return false
511
+ if (returns === 1) {
512
+ const stmts = blockStmts(abody)
513
+ if (!stmts || !stmts.length) return false
514
+ const last = stmts[stmts.length - 1]
515
+ if (!Array.isArray(last) || last[0] !== 'return') return false
516
+ }
517
+ return !mutatesAny(abody, new Set(params))
518
+ }
519
+
520
+ const inlineLocalLambdasInBody = (getBody, setBody) => {
521
+ const body = getBody()
522
+ const stmts = bodyStmtList(body)
523
+ if (stmts.length < 2) return false
524
+
525
+ // Collect `const f = ARROW` (single-decl), all-plain params, inlinable body.
526
+ const decls = new Map()
527
+ for (const stmt of stmts) {
528
+ if (!Array.isArray(stmt) || stmt[0] !== 'const' || stmt.length !== 2) continue
529
+ const d = stmt[1]
530
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
531
+ const arrow = d[2]
532
+ if (!Array.isArray(arrow) || arrow[0] !== '=>') continue
533
+ const params = extractParams(arrow[1])
534
+ if (!params.every(p => typeof p === 'string')) continue
535
+ if (!inlinableLambdaBody(arrow[2], params)) continue
536
+ decls.set(d[1], { stmt, arrow, params })
537
+ }
538
+ if (!decls.size) return false
539
+
540
+ // Drop any candidate whose body references another (or its own) candidate —
541
+ // single-level inlining can't resolve such chains, and a still-referenced
542
+ // candidate's decl can't be removed.
543
+ for (let changed = true; changed;) {
544
+ changed = false
545
+ for (const [name, info] of decls) {
546
+ if ([...decls.keys()].some(c => refsName(info.arrow[2], c, REFS_IN_EXPR))) { decls.delete(name); changed = true }
547
+ }
548
+ }
549
+ // Every other reference to the name must be a `name(...)` call.
550
+ for (const [name, info] of [...decls]) {
551
+ if (!stmts.every(s => s === info.stmt || onlyCalledNotReferenced(s, name))) decls.delete(name)
552
+ }
553
+ if (!decls.size) return false
554
+
555
+ const asFunc = info => ({ sig: { params: info.params.map(name => ({ name })) }, body: info.arrow[2] })
556
+ const stmtCands = new Map(), exprCands = new Map()
557
+ for (const [name, info] of decls)
558
+ (Array.isArray(info.arrow[2]) && info.arrow[2][0] === '{}' ? stmtCands : exprCands).set(name, asFunc(info))
559
+
560
+ let out = body, didChange = false
561
+ if (stmtCands.size) { const r = inlineInStmt(out, stmtCands); if (r.changed) { out = r.node; didChange = true } }
562
+ if (exprCands.size) { const r = inlineInExpr(out, exprCands); if (r.changed) { out = r.node; didChange = true } }
563
+ if (!didChange) return false
564
+
565
+ // Remove decls of candidates that are now fully consumed.
566
+ const newStmts = bodyStmtList(out)
567
+ const dead = new Set()
568
+ for (const [name, info] of decls) {
569
+ if (!newStmts.some(s => s !== info.stmt && refsName(s, name, REFS_IN_EXPR))) dead.add(info.stmt)
570
+ }
571
+ if (dead.size) out = removeStmts(out, dead) ?? [';']
572
+
573
+ setBody(out)
574
+ return true
575
+ }
576
+
577
+ export const inlineLocalLambdas = () => {
578
+ let changed = false
579
+ for (const func of ctx.func.list) {
580
+ if (!func.body || func.raw) continue
581
+ if (inlineLocalLambdasInBody(() => func.body, b => { func.body = b })) changed = true
582
+ }
583
+ return changed
584
+ }
585
+
586
+ const restIndexExpr = (idx, restParams) => {
587
+ const k = constIntExpr(idx)
588
+ if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
589
+
590
+ let out = [, undefined]
591
+ for (let i = restParams.length - 1; i >= 0; i--) {
592
+ out = ['?:', ['==', clonePlain(idx), [, i]], restParams[i], out]
593
+ }
594
+ return out
595
+ }
596
+
597
+ const rewriteRestBody = (node, restName, restParams) => {
598
+ if (typeof node === 'string') return node === restName ? { ok: false } : { ok: true, node }
599
+ if (!Array.isArray(node)) return { ok: true, node }
600
+ if (node[0] === 'str') return { ok: true, node: node.slice() }
601
+
602
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === restName) {
603
+ return node[2] === 'length' ? { ok: true, node: [, restParams.length] } : { ok: false }
604
+ }
605
+
606
+ if (node[0] === '[]' && node[1] === restName) {
607
+ if (!isSimpleArg(node[2])) return { ok: false }
608
+ return { ok: true, node: restIndexExpr(node[2], restParams) }
609
+ }
610
+
611
+ const out = [node[0]]
612
+ for (let i = 1; i < node.length; i++) {
613
+ const r = rewriteRestBody(node[i], restName, restParams)
614
+ if (!r.ok) return r
615
+ out.push(r.node)
616
+ }
617
+ return { ok: true, node: out }
618
+ }
619
+
620
+ export const specializeFixedRestCalls = (programFacts) => {
621
+ const sitesByKey = new Map()
622
+ for (const site of programFacts.callSites) {
623
+ const func = ctx.func.map.get(site.callee)
624
+ if (!func?.rest || func.exported || func.raw || !func.body) continue
625
+ if (programFacts.valueUsed.has(func.name)) continue
626
+ if (func.defaults && Object.keys(func.defaults).length) continue
627
+ if (site.argList.some(a => Array.isArray(a) && a[0] === '...')) continue
628
+
629
+ const fixedN = func.sig.params.length - 1
630
+ const restN = Math.max(0, site.argList.length - fixedN)
631
+ const key = `${func.name}/${restN}`
632
+ const list = sitesByKey.get(key)
633
+ if (list) list.push(site); else sitesByKey.set(key, [site])
634
+ }
635
+
636
+ let changed = false
637
+ for (const [key, sites] of sitesByKey) {
638
+ const [name, restNText] = key.split('/')
639
+ const func = ctx.func.map.get(name)
640
+ const restN = Number(restNText)
641
+ const fixedParams = func.sig.params.slice(0, -1).map(p => ({ ...p }))
642
+ const restName = func.rest
643
+ const restParams = Array.from({ length: restN }, (_, i) => `${restName}${T}r${restN}_${i}`)
644
+ const rewritten = rewriteRestBody(func.body, restName, restParams)
645
+ if (!rewritten.ok) continue
646
+
647
+ const cloneName = `${name}${T}rest${restN}`
648
+ if (!ctx.func.map.has(cloneName)) {
649
+ const restSigParams = restParams.map(name => ({ name, type: 'f64' }))
650
+ const clone = {
651
+ ...func,
652
+ name: cloneName,
653
+ exported: false,
654
+ rest: null,
655
+ // Build the specialized sig fresh. `params`/`results` are sig's only
656
+ // fields and both are replaced here, so a `{ ...func.sig, … }` spread
657
+ // would be pure redundancy — and a full-override spread of a live sig
658
+ // object also trips the self-host codegen, so the explicit form is both
659
+ // simpler and the one that round-trips through jz.wasm.
660
+ sig: {
661
+ params: [...fixedParams, ...restSigParams],
662
+ results: [...func.sig.results],
663
+ },
664
+ body: rewritten.node,
665
+ }
666
+ ctx.func.list.push(clone)
667
+ ctx.func.names.add(cloneName)
668
+ ctx.func.map.set(cloneName, clone)
669
+ }
670
+
671
+ const fixedN = func.sig.params.length - 1
672
+ for (const site of sites) {
673
+ site.node[1] = cloneName
674
+ setCallArgs(site.node, site.argList.slice(0, fixedN + restN))
675
+ changed = true
676
+ }
677
+ }
678
+ return changed
679
+ }