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.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /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
|
+
}
|