jz 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,682 @@
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; cloth's relax: ~160 nodes × 2 sites, fired
363
+ // per link every relaxation pass). V8's pre-Turboshaft wasm tiers never
364
+ // inline cross-function, so an out-of-line leaf in a hot loop is a hard
365
+ // per-cell tax on Node ≤ 22 — and still saves call setup on newer tiers.
366
+ // The in-loop cap is generous because the gate above bounds non-tiny leaves
367
+ // to ≤2 sites, so the spliced duplication is at most ~2× a bounded body.
368
+ const allSitesInLoop = sites.every(site =>
369
+ site.callerFunc?.body && containsNode(site.callerFunc.body, site.node, false))
370
+ if (nodeSize(func.body) > (allSitesInLoop ? 200 : 30)) continue
371
+ }
372
+ if (some(func.body, n => n[0] === '()' && n[1] === func.name)) continue
373
+ // Kernels with nested loops (depth ≥ 2) are typically large and the inner
374
+ // loop carries most of the cost. Inlining them into a host that V8 can't
375
+ // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
376
+ // Keep them as standalone functions so V8 wasm tier-up can warm them.
377
+ if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
378
+ // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
379
+ // object/array literals returned) break downstream pointer-ABI specialization
380
+ // when inlined: narrow.js can't trace the post-inline alias chain back to a
381
+ // single ctor, so the typed-array param of a callee like processCascade(x, …)
382
+ // stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
383
+ // Keeping the factory as a callable function preserves the call-site type fact.
384
+ if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
385
+ if (paramNames.size && some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && paramNames.has(n[1])))
386
+ forwarders.add(func.name)
387
+ if (!hasLoop) leaves.add(func.name)
388
+ candidates.set(func.name, func)
389
+ }
390
+ if (!candidates.size) return false
391
+
392
+ // Trivial expr-bodied candidates can be substituted at any expression position
393
+ // (if-condition, ternary, etc.). Stmt-bodied ones go through inlineInStmt's
394
+ // statement-level path which preserves prefix ordering — EXCEPT flattenable
395
+ // block bodies (pure-arith let decls + trailing return, e.g. distance's
396
+ // dx/dy), which inlineInExpr turns into zero-prefix expressions per site.
397
+ const flattenableBody = (func) => {
398
+ const stmts = blockStmts(func.body)
399
+ if (!stmts) return false
400
+ return stmts.every((s, i) => i === stmts.length - 1
401
+ ? Array.isArray(s) && s[0] === 'return'
402
+ : Array.isArray(s) && (s[0] === 'let' || s[0] === 'const') && s.length === 2
403
+ && Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' && pureFlattenExpr(s[1][2]))
404
+ }
405
+ const exprOnlyCandidates = new Map()
406
+ for (const [name, func] of candidates) {
407
+ if (!Array.isArray(func.body) || func.body[0] !== '{}' || flattenableBody(func)) exprOnlyCandidates.set(name, func)
408
+ }
409
+
410
+ let changed = false
411
+ const exportedCandidates = new Map()
412
+ for (const [name, func] of candidates) {
413
+ const sites = sitesByCallee.get(name)
414
+ const fixedSiteExported = hasFixedTypedArraySites(func, sites) &&
415
+ !sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))
416
+ // Forwarders cross into an exported caller too: the tier-up rationale that
417
+ // keeps candidates out of exports concerns relocated loop kernels, not
418
+ // these tiny leaves — and inlining one devirtualizes a closure dispatch.
419
+ if (fixedSiteExported || forwarders.has(name) || leaves.has(name) || sites?.length === 1) exportedCandidates.set(name, func)
420
+ }
421
+ for (const func of ctx.func.list) {
422
+ if (!func.body || func.raw) continue
423
+ // Skip exports: they're entry points usually invoked once. Inlining a
424
+ // hot kernel here would put the loop into a function V8's wasm tier-up
425
+ // never warms (kernel stays in baseline). Keeping the kernel as its own
426
+ // callable function lets V8 promote it to TurboFan after a few calls.
427
+ // Exception: fixed-size typed-array callees should inline into the exported
428
+ // caller so scalar replacement can cross the call boundary and remove the
429
+ // caller's heap arrays.
430
+ const activeCandidates = func.exported ? exportedCandidates : candidates
431
+ if (func.exported && !activeCandidates.size) continue
432
+ // Expression-bodied arrows (`() => expr`) have func.body as the return
433
+ // value itself — never a `{}` block. inlineInStmt treats its argument as a
434
+ // statement (discards the return value of any top-level candidate call),
435
+ // which would turn `() => x()` into an empty block and lose the result.
436
+ // Route those through inlineInExpr so the call is replaced by the inlined
437
+ // value expression instead.
438
+ const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
439
+ const r = isExprBody
440
+ ? inlineInExpr(func.body, activeCandidates)
441
+ : inlineInStmt(func.body, activeCandidates)
442
+ let body = r.changed ? r.node : func.body
443
+ let bodyChanged = r.changed
444
+ // Expression-position pass. Exported callers take the leaf-safe subset —
445
+ // the same tier-up rationale as the statement path (leaves into exports
446
+ // are fine; relocated kernels are not).
447
+ const exprActive = func.exported
448
+ ? new Map([...exprOnlyCandidates].filter(([n]) => exportedCandidates.has(n)))
449
+ : exprOnlyCandidates
450
+ if (exprActive.size) {
451
+ const e = inlineInExpr(body, exprActive)
452
+ if (e.changed) { body = e.node; bodyChanged = true }
453
+ }
454
+ if (bodyChanged) { func.body = body; changed = true }
455
+ }
456
+ if (ast) {
457
+ const r = inlineInStmt(ast, candidates)
458
+ if (r.changed) changed = true
459
+ }
460
+ return changed
461
+ }
462
+
463
+ // === Inline non-escaping local lambdas ===
464
+ // `const f = (a) => …; … f(x) …` → the lambda body substituted at each call
465
+ // site. A non-escaping lambda's captured free vars are still in lexical scope at
466
+ // the call site, so splicing the body in place preserves capture-by-reference
467
+ // semantics while eliminating the closure object (no env pointer, no NaN-box, no
468
+ // call_indirect). Mirrors inlineHotInternalCalls, scoped to one function body.
469
+
470
+ // True iff every textual reference to `name` in `node` is the callee of a
471
+ // `name(...)` call (i.e. the binding never escapes — never read as a value,
472
+ // reassigned, captured by a nested lambda, or shadowed).
473
+ const onlyCalledNotReferenced = (node, name) => {
474
+ if (typeof node === 'string') return node !== name
475
+ if (!Array.isArray(node)) return true
476
+ const op = node[0]
477
+ if (op === 'str') return true
478
+ // A nested lambda touching `name` at all (capture or shadowing param) → bail.
479
+ if (op === '=>') return !refsName(node[1], name, REFS_IN_EXPR) && !refsName(node[2], name, REFS_IN_EXPR)
480
+ if (op === '()' && node[1] === name) {
481
+ for (let i = 2; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
482
+ return true
483
+ }
484
+ if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
485
+ if (op === ':') return onlyCalledNotReferenced(node[2], name)
486
+ for (let i = 1; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
487
+ return true
488
+ }
489
+
490
+ const bodyStmtList = body =>
491
+ Array.isArray(body) && body[0] === '{}' ? blockStmts(body)
492
+ : Array.isArray(body) && body[0] === ';' ? body.slice(1)
493
+ : body == null ? [] : [body]
494
+
495
+ const removeStmts = (body, set) => {
496
+ if (!Array.isArray(body)) return set.has(body) ? null : body
497
+ if (body[0] === '{}') return ['{}', removeStmts(body[1], set) ?? [';']]
498
+ if (body[0] === ';') {
499
+ const kept = body.slice(1).filter(s => !set.has(s))
500
+ return kept.length === 0 ? null : kept.length === 1 ? kept[0] : [';', ...kept]
501
+ }
502
+ return set.has(body) ? null : body
503
+ }
504
+
505
+ // Lambda body must be a guaranteed-return shape inlinedBody can splice: ≤1
506
+ // `return` (trailing, if a block), no throw/break/continue, no param mutation,
507
+ // no nested lambda.
508
+ const inlinableLambdaBody = (abody, params) => {
509
+ if (some(abody, n => n[0] === '=>')) return false
510
+ if (some(abody, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) return false
511
+ let returns = 0
512
+ some(abody, n => { if (n[0] === 'return') returns++; return false })
513
+ if (returns > 1) return false
514
+ if (returns === 1) {
515
+ const stmts = blockStmts(abody)
516
+ if (!stmts || !stmts.length) return false
517
+ const last = stmts[stmts.length - 1]
518
+ if (!Array.isArray(last) || last[0] !== 'return') return false
519
+ }
520
+ return !mutatesAny(abody, new Set(params))
521
+ }
522
+
523
+ const inlineLocalLambdasInBody = (getBody, setBody) => {
524
+ const body = getBody()
525
+ const stmts = bodyStmtList(body)
526
+ if (stmts.length < 2) return false
527
+
528
+ // Collect `const f = ARROW` (single-decl), all-plain params, inlinable body.
529
+ const decls = new Map()
530
+ for (const stmt of stmts) {
531
+ if (!Array.isArray(stmt) || stmt[0] !== 'const' || stmt.length !== 2) continue
532
+ const d = stmt[1]
533
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
534
+ const arrow = d[2]
535
+ if (!Array.isArray(arrow) || arrow[0] !== '=>') continue
536
+ const params = extractParams(arrow[1])
537
+ if (!params.every(p => typeof p === 'string')) continue
538
+ if (!inlinableLambdaBody(arrow[2], params)) continue
539
+ decls.set(d[1], { stmt, arrow, params })
540
+ }
541
+ if (!decls.size) return false
542
+
543
+ // Drop any candidate whose body references another (or its own) candidate —
544
+ // single-level inlining can't resolve such chains, and a still-referenced
545
+ // candidate's decl can't be removed.
546
+ for (let changed = true; changed;) {
547
+ changed = false
548
+ for (const [name, info] of decls) {
549
+ if ([...decls.keys()].some(c => refsName(info.arrow[2], c, REFS_IN_EXPR))) { decls.delete(name); changed = true }
550
+ }
551
+ }
552
+ // Every other reference to the name must be a `name(...)` call.
553
+ for (const [name, info] of [...decls]) {
554
+ if (!stmts.every(s => s === info.stmt || onlyCalledNotReferenced(s, name))) decls.delete(name)
555
+ }
556
+ if (!decls.size) return false
557
+
558
+ const asFunc = info => ({ sig: { params: info.params.map(name => ({ name })) }, body: info.arrow[2] })
559
+ const stmtCands = new Map(), exprCands = new Map()
560
+ for (const [name, info] of decls)
561
+ (Array.isArray(info.arrow[2]) && info.arrow[2][0] === '{}' ? stmtCands : exprCands).set(name, asFunc(info))
562
+
563
+ let out = body, didChange = false
564
+ if (stmtCands.size) { const r = inlineInStmt(out, stmtCands); if (r.changed) { out = r.node; didChange = true } }
565
+ if (exprCands.size) { const r = inlineInExpr(out, exprCands); if (r.changed) { out = r.node; didChange = true } }
566
+ if (!didChange) return false
567
+
568
+ // Remove decls of candidates that are now fully consumed.
569
+ const newStmts = bodyStmtList(out)
570
+ const dead = new Set()
571
+ for (const [name, info] of decls) {
572
+ if (!newStmts.some(s => s !== info.stmt && refsName(s, name, REFS_IN_EXPR))) dead.add(info.stmt)
573
+ }
574
+ if (dead.size) out = removeStmts(out, dead) ?? [';']
575
+
576
+ setBody(out)
577
+ return true
578
+ }
579
+
580
+ export const inlineLocalLambdas = () => {
581
+ let changed = false
582
+ for (const func of ctx.func.list) {
583
+ if (!func.body || func.raw) continue
584
+ if (inlineLocalLambdasInBody(() => func.body, b => { func.body = b })) changed = true
585
+ }
586
+ return changed
587
+ }
588
+
589
+ const restIndexExpr = (idx, restParams) => {
590
+ const k = constIntExpr(idx)
591
+ if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
592
+
593
+ let out = [, undefined]
594
+ for (let i = restParams.length - 1; i >= 0; i--) {
595
+ out = ['?:', ['==', clonePlain(idx), [, i]], restParams[i], out]
596
+ }
597
+ return out
598
+ }
599
+
600
+ const rewriteRestBody = (node, restName, restParams) => {
601
+ if (typeof node === 'string') return node === restName ? { ok: false } : { ok: true, node }
602
+ if (!Array.isArray(node)) return { ok: true, node }
603
+ if (node[0] === 'str') return { ok: true, node: node.slice() }
604
+
605
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === restName) {
606
+ return node[2] === 'length' ? { ok: true, node: [, restParams.length] } : { ok: false }
607
+ }
608
+
609
+ if (node[0] === '[]' && node[1] === restName) {
610
+ if (!isSimpleArg(node[2])) return { ok: false }
611
+ return { ok: true, node: restIndexExpr(node[2], restParams) }
612
+ }
613
+
614
+ const out = [node[0]]
615
+ for (let i = 1; i < node.length; i++) {
616
+ const r = rewriteRestBody(node[i], restName, restParams)
617
+ if (!r.ok) return r
618
+ out.push(r.node)
619
+ }
620
+ return { ok: true, node: out }
621
+ }
622
+
623
+ export const specializeFixedRestCalls = (programFacts) => {
624
+ const sitesByKey = new Map()
625
+ for (const site of programFacts.callSites) {
626
+ const func = ctx.func.map.get(site.callee)
627
+ if (!func?.rest || func.exported || func.raw || !func.body) continue
628
+ if (programFacts.valueUsed.has(func.name)) continue
629
+ if (func.defaults && Object.keys(func.defaults).length) continue
630
+ if (site.argList.some(a => Array.isArray(a) && a[0] === '...')) continue
631
+
632
+ const fixedN = func.sig.params.length - 1
633
+ const restN = Math.max(0, site.argList.length - fixedN)
634
+ const key = `${func.name}/${restN}`
635
+ const list = sitesByKey.get(key)
636
+ if (list) list.push(site); else sitesByKey.set(key, [site])
637
+ }
638
+
639
+ let changed = false
640
+ for (const [key, sites] of sitesByKey) {
641
+ const [name, restNText] = key.split('/')
642
+ const func = ctx.func.map.get(name)
643
+ const restN = Number(restNText)
644
+ const fixedParams = func.sig.params.slice(0, -1).map(p => ({ ...p }))
645
+ const restName = func.rest
646
+ const restParams = Array.from({ length: restN }, (_, i) => `${restName}${T}r${restN}_${i}`)
647
+ const rewritten = rewriteRestBody(func.body, restName, restParams)
648
+ if (!rewritten.ok) continue
649
+
650
+ const cloneName = `${name}${T}rest${restN}`
651
+ if (!ctx.func.map.has(cloneName)) {
652
+ const restSigParams = restParams.map(name => ({ name, type: 'f64' }))
653
+ const clone = {
654
+ ...func,
655
+ name: cloneName,
656
+ exported: false,
657
+ rest: null,
658
+ // Build the specialized sig fresh. `params`/`results` are sig's only
659
+ // fields and both are replaced here, so a `{ ...func.sig, … }` spread
660
+ // would be pure redundancy — and a full-override spread of a live sig
661
+ // object also trips the self-host codegen, so the explicit form is both
662
+ // simpler and the one that round-trips through jz.wasm.
663
+ sig: {
664
+ params: [...fixedParams, ...restSigParams],
665
+ results: [...func.sig.results],
666
+ },
667
+ body: rewritten.node,
668
+ }
669
+ ctx.func.list.push(clone)
670
+ ctx.func.names.add(cloneName)
671
+ ctx.func.map.set(cloneName, clone)
672
+ }
673
+
674
+ const fixedN = func.sig.params.length - 1
675
+ for (const site of sites) {
676
+ site.node[1] = cloneName
677
+ setCallArgs(site.node, site.argList.slice(0, fixedN + restN))
678
+ changed = true
679
+ }
680
+ }
681
+ return changed
682
+ }