jz 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
// literal tests use `== null`; created literals are bare numbers.
|
|
21
21
|
|
|
22
22
|
import { findMutations } from './analyze-scans.js'
|
|
23
|
+
import { ASSIGN_OPS } from '../ast.js'
|
|
24
|
+
import { litN, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks, freshLoopId } from './loop-model.js'
|
|
23
25
|
|
|
24
|
-
const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
25
26
|
const isVar = (n) => typeof n === 'string'
|
|
26
27
|
|
|
27
28
|
// `k = -r`: prepared as ['u-', r] (unary minus) or ['-', 0, r].
|
|
@@ -31,11 +32,10 @@ const negOf = (n) => Array.isArray(n) && n[0] === 'u-' ? n[1]
|
|
|
31
32
|
// Every write to `iv` in `node` is a strictly-positive step (++iv / iv+=1 / iv=iv+1),
|
|
32
33
|
// so iv advances monotonically — required so the three split loops partition [0,bound)
|
|
33
34
|
// and the clamp-free interior never re-runs at an edge index. Any other write → false.
|
|
34
|
-
const ASSIGN = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**=', '&&=', '||=', '??='])
|
|
35
35
|
function ivMonotonic(node, iv) {
|
|
36
36
|
if (!Array.isArray(node)) return true
|
|
37
37
|
if ((node[0] === '++' || node[0] === '--') && node[1] === iv) return node[0] === '++'
|
|
38
|
-
if (
|
|
38
|
+
if (ASSIGN_OPS.has(node[0]) && node[1] === iv) {
|
|
39
39
|
if (node[0] === '+=' && litN(node[2], 1)) return true
|
|
40
40
|
if (node[0] === '=' && Array.isArray(node[2]) && node[2][0] === '+'
|
|
41
41
|
&& ((node[2][1] === iv && litN(node[2][2], 1)) || (node[2][2] === iv && litN(node[2][1], 1)))) return true
|
|
@@ -44,22 +44,6 @@ function ivMonotonic(node, iv) {
|
|
|
44
44
|
return node.every(c => ivMonotonic(c, iv))
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// Vars assigned inside any closure in the function — a call in the loop can mutate
|
|
48
|
-
// them even though findMutations (direct writes only) misses it. iv/bound/r in this
|
|
49
|
-
// set must bail (same class as the loop-SR closure-mutation guard).
|
|
50
|
-
const collectAssigns = (n, out) => {
|
|
51
|
-
if (!Array.isArray(n)) return
|
|
52
|
-
if (typeof n[1] === 'string' && (ASSIGN.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
53
|
-
n.forEach(c => collectAssigns(c, out))
|
|
54
|
-
}
|
|
55
|
-
const closureMutated = (n, out) => {
|
|
56
|
-
if (!Array.isArray(n)) return out
|
|
57
|
-
if (n[0] === '=>') collectAssigns(n, out)
|
|
58
|
-
n.forEach(c => closureMutated(c, out))
|
|
59
|
-
return out
|
|
60
|
-
}
|
|
61
|
-
let _cm = new Set()
|
|
62
|
-
|
|
63
47
|
// Find, anywhere in `node`, a clamp `if (ci < 0) ci = 0; else if (ci >= B) ci = B-1`
|
|
64
48
|
// over a var `ci` and bound var `B`. Returns { ci, bound } or null (first match).
|
|
65
49
|
function findClamp(node) {
|
|
@@ -121,7 +105,7 @@ function countWrites(node, v) {
|
|
|
121
105
|
const visit = (x) => {
|
|
122
106
|
if (!Array.isArray(x)) return
|
|
123
107
|
if ((x[0] === '++' || x[0] === '--') && x[1] === v) n++
|
|
124
|
-
else if (
|
|
108
|
+
else if (ASSIGN_OPS.has(x[0]) && x[1] === v) n++
|
|
125
109
|
x.forEach(visit)
|
|
126
110
|
}
|
|
127
111
|
visit(node)
|
|
@@ -151,7 +135,7 @@ function ivWrittenInNestedLoop(body, iv) {
|
|
|
151
135
|
let found = false
|
|
152
136
|
const visit = (n, inLoop) => {
|
|
153
137
|
if (!Array.isArray(n)) return
|
|
154
|
-
if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (
|
|
138
|
+
if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (ASSIGN_OPS.has(n[0]) && n[1] === iv))) found = true
|
|
155
139
|
const deeper = inLoop || n[0] === 'while' || n[0] === 'for'
|
|
156
140
|
n.forEach(c => visit(c, deeper))
|
|
157
141
|
}
|
|
@@ -159,41 +143,24 @@ function ivWrittenInNestedLoop(body, iv) {
|
|
|
159
143
|
return found
|
|
160
144
|
}
|
|
161
145
|
|
|
162
|
-
let _uniq = 0
|
|
163
|
-
|
|
164
146
|
// Drop the clamp `if` node from a (cloned) body, leaving the bare `ci = iv + k`.
|
|
165
147
|
const dropClamp = (node, clampNode) =>
|
|
166
148
|
!Array.isArray(node) ? node
|
|
167
149
|
: node === clampNode ? ['{}', [';']] // empty block (the if is a statement)
|
|
168
150
|
: node.map(c => dropClamp(c, clampNode))
|
|
169
151
|
|
|
170
|
-
|
|
171
|
-
const stepIsPosInc = (s, iv) => {
|
|
172
|
-
if (!Array.isArray(s)) return false
|
|
173
|
-
let inc = s
|
|
174
|
-
if (s[0] === '-' && litN(s[2], 1) && Array.isArray(s[1]) && s[1][0] === '++') inc = s[1]
|
|
175
|
-
if (inc[0] === '++' && inc[1] === iv) return true
|
|
176
|
-
if (s[0] === '+=' && s[1] === iv && litN(s[2], 1)) return true
|
|
177
|
-
return s[0] === '=' && s[1] === iv && Array.isArray(s[2]) && s[2][0] === '+'
|
|
178
|
-
&& ((s[2][1] === iv && litN(s[2][2], 1)) || (s[2][2] === iv && litN(s[2][1], 1)))
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function tryPeel(stmt) {
|
|
182
|
-
if (!Array.isArray(stmt)) return null
|
|
152
|
+
function tryPeel(stmt, cm) {
|
|
183
153
|
// Normalize while / for into (iv, bound, body, init, step). For a `while`, the
|
|
184
154
|
// increment is inside the body; for a `for` it is the separate step clause, which
|
|
185
155
|
// we re-append to each split loop's body (converting the for into init + whiles).
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
iv = cond[1]; bound = cond[2]
|
|
195
|
-
if (!stepIsPosInc(step, iv)) return null
|
|
196
|
-
} else return null
|
|
156
|
+
const L = normalizeLoop(stmt)
|
|
157
|
+
if (!L) return null
|
|
158
|
+
let { init, cond, step, body } = L
|
|
159
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || !isVar(cond[1])) return null
|
|
160
|
+
const iv = cond[1], bound = cond[2]
|
|
161
|
+
// The `for` step must be the IV's strictly-positive +1 increment; a `while` increments in
|
|
162
|
+
// its body (validated below by ivMonotonic / the exactly-one-+1 checks).
|
|
163
|
+
if (L.kind === 'for' && unitIncVar(step) !== iv) return null
|
|
197
164
|
if (!isVar(bound) || !Array.isArray(body)) return null
|
|
198
165
|
// A loop whose body is a single statement (e.g. an outer row loop wrapping one
|
|
199
166
|
// inner loop — the vertical blur pass) isn't a `;` sequence; normalize it.
|
|
@@ -227,9 +194,9 @@ function tryPeel(stmt) {
|
|
|
227
194
|
if (!ivMonotonic(body, iv)) return null
|
|
228
195
|
const mut = new Set(); findMutations(body, new Set([bound, r]), mut)
|
|
229
196
|
if (mut.has(bound) || mut.has(r)) return null
|
|
230
|
-
if (
|
|
197
|
+
if (cm.has(iv) || cm.has(bound) || cm.has(r)) return null // closure-mutable → unsafe
|
|
231
198
|
|
|
232
|
-
const id =
|
|
199
|
+
const id = freshLoopId()
|
|
233
200
|
const xs = `__pks${id}`, xe = `__pke${id}`
|
|
234
201
|
// xs = r < bound ? r : bound ; xe = (bound - r) > xs ? bound - r : xs
|
|
235
202
|
const seed = ['let',
|
|
@@ -242,20 +209,7 @@ function tryPeel(stmt) {
|
|
|
242
209
|
return init ? [init, seed, ...loops] : [seed, ...loops]
|
|
243
210
|
}
|
|
244
211
|
|
|
245
|
-
function walk(node) {
|
|
246
|
-
if (!Array.isArray(node)) return node
|
|
247
|
-
const n = node.map(walk)
|
|
248
|
-
if (n[0] !== ';') return n
|
|
249
|
-
const out = [';']
|
|
250
|
-
for (let k = 1; k < n.length; k++) {
|
|
251
|
-
const r = tryPeel(n[k])
|
|
252
|
-
if (r) out.push(...r)
|
|
253
|
-
else out.push(n[k])
|
|
254
|
-
}
|
|
255
|
-
return out
|
|
256
|
-
}
|
|
257
|
-
|
|
258
212
|
export function peelClampedStencil(body) {
|
|
259
|
-
|
|
260
|
-
return
|
|
213
|
+
const cm = closureMutatedVars(body)
|
|
214
|
+
return rewriteBlocks(body, stmt => tryPeel(stmt, cm))
|
|
261
215
|
}
|
|
@@ -129,6 +129,35 @@ export const fixedScalarTypedArray = (expr) => {
|
|
|
129
129
|
? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** Does `expr` allocate a FRESH typed-array buffer of static numeric length —
|
|
133
|
+
* `new Float64Array(N)` with N a compile-time integer? Excludes view ctors
|
|
134
|
+
* (`new Float64Array(someBuffer)` — the arg is a buffer ref, not an int), so the
|
|
135
|
+
* result provably aliases no other buffer. Used by param-distinctness. */
|
|
136
|
+
export const isFreshTypedArrayAlloc = (expr) => {
|
|
137
|
+
if (typedElemCtor(expr) == null) return false
|
|
138
|
+
const args = callArgs(expr)
|
|
139
|
+
return !!args && args.length === 1 && constIntExpr(args[0]) != null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Local names const-bound to a fresh typed-array allocation (each a unique buffer, distinct from
|
|
143
|
+
* every other such binding and from any pre-existing value). `const` only — an immutable binding
|
|
144
|
+
* always holds that fresh buffer. Element writes (`a[i]=…`) don't reassign `a`, so they're fine. */
|
|
145
|
+
export const freshTypedArrayLocals = (body) => {
|
|
146
|
+
const fresh = new Set()
|
|
147
|
+
const walk = node => {
|
|
148
|
+
if (!Array.isArray(node) || node[0] === '=>') return
|
|
149
|
+
if (node[0] === 'const') {
|
|
150
|
+
for (let i = 1; i < node.length; i++) {
|
|
151
|
+
const d = node[i]
|
|
152
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' && isFreshTypedArrayAlloc(d[2])) fresh.add(d[1])
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
156
|
+
}
|
|
157
|
+
walk(body)
|
|
158
|
+
return fresh
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
/** Map of name → {len, coerce} for every fixed-size typed-array binding declared
|
|
133
162
|
* via `let`/`const` directly in `body` (no descent into nested arrows). */
|
|
134
163
|
export const fixedTypedArraysInBody = (body) => {
|
|
@@ -43,7 +43,7 @@ import { inlineHotInternalCalls, inlineLocalLambdas, specializeFixedRestCalls }
|
|
|
43
43
|
import { bindNestedRowLengths, unrollRowLenPadLoops, splitCharScanLoops } from './loops.js'
|
|
44
44
|
import {
|
|
45
45
|
scalarizeFunctionTypedArrays, scalarizeFunctionArrayLiterals,
|
|
46
|
-
promoteIntArrayLiterals, scalarizeFunctionObjectLiterals,
|
|
46
|
+
promoteIntArrayLiterals, scalarizeFunctionObjectLiterals, analyzeParamDistinctness,
|
|
47
47
|
} from './literals.js'
|
|
48
48
|
|
|
49
49
|
export default function plan(ast, profiler) {
|
|
@@ -113,6 +113,9 @@ export default function plan(ast, profiler) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
t('narrowSignatures', () => narrowSignatures(programFacts, ast))
|
|
116
|
+
// After narrowSignatures (params now carry ptrKind): mark typed-array params that every call
|
|
117
|
+
// site passes a distinct fresh buffer for → enables alias-aware LICM in the optimizer.
|
|
118
|
+
if (optimizing()) t('analyzeParamDistinctness', () => analyzeParamDistinctness(programFacts))
|
|
116
119
|
t('specializeBimorphicTyped', () => specializeBimorphicTyped(programFacts))
|
|
117
120
|
t('refineDynKeys', () => refineDynKeys(programFacts))
|
|
118
121
|
strictBoundaryTypeCheck(programFacts)
|
|
@@ -39,12 +39,25 @@ import {
|
|
|
39
39
|
// expression — null if void or no trailing return value.
|
|
40
40
|
const inlinedBody = (func, args) => {
|
|
41
41
|
const params = func.sig.params
|
|
42
|
-
if (args.length !== params.length
|
|
42
|
+
if (args.length !== params.length) return null
|
|
43
43
|
const paramNames = new Set(params.map(p => p.name))
|
|
44
44
|
if (mutatesAny(func.body, paramNames)) return null
|
|
45
45
|
|
|
46
|
+
// A simple arg (ident / literal / arithmetic) is cheap to substitute directly, even when its
|
|
47
|
+
// param is used several times. A NON-simple arg (a call, `?:`, indexed load) is bound to a fresh
|
|
48
|
+
// temp evaluated ONCE in call order — preserving evaluation count + left-to-right order, and
|
|
49
|
+
// never duplicating the expression. This lets nested calls inline: `lerp(grad(a), grad(b), u)`
|
|
50
|
+
// binds `t0 = grad(a); t1 = grad(b)` and substitutes the body with t0/t1; a later inliner pass
|
|
51
|
+
// then folds grad into those temp decls (the fixpoint in inlineHotInternalCalls).
|
|
46
52
|
const subst = new Map()
|
|
47
|
-
|
|
53
|
+
const argPrefix = []
|
|
54
|
+
for (let i = 0; i < params.length; i++) {
|
|
55
|
+
const arg = args[i]
|
|
56
|
+
if (isSimpleArg(arg)) { subst.set(params[i].name, arg); continue }
|
|
57
|
+
const tmp = `${T}inarg${ctx.func.uniq++}`
|
|
58
|
+
argPrefix.push(['const', ['=', tmp, arg]])
|
|
59
|
+
subst.set(params[i].name, tmp)
|
|
60
|
+
}
|
|
48
61
|
|
|
49
62
|
const locals = new Set()
|
|
50
63
|
collectBindings(func.body, locals)
|
|
@@ -56,13 +69,13 @@ const inlinedBody = (func, args) => {
|
|
|
56
69
|
const stmts = blockStmts(func.body)
|
|
57
70
|
// Expression-bodied arrow `(c) => expr`: no statement block; the whole body
|
|
58
71
|
// *is* the return value. Treat as zero-prefix + value.
|
|
59
|
-
if (!stmts) return { prefix:
|
|
72
|
+
if (!stmts) return { prefix: argPrefix, value: cloneWithSubst(func.body, subst, rename) }
|
|
60
73
|
const last = stmts.length ? stmts[stmts.length - 1] : null
|
|
61
74
|
const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
|
|
62
75
|
const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
|
|
63
76
|
const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
|
|
64
77
|
const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
|
|
65
|
-
return { prefix, value }
|
|
78
|
+
return { prefix: argPrefix.length ? [...argPrefix, ...prefix] : prefix, value }
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
const stmtDeclName = (stmt) => {
|
|
@@ -109,7 +122,14 @@ const isCandidateCall = (node, candidates) =>
|
|
|
109
122
|
// arithmetic>`, substituting the decls into the return value turns it into a
|
|
110
123
|
// zero-prefix expression. Duplicated subtrees (`dx` used twice → `x1-x2`
|
|
111
124
|
// twice) are pure, and the watr-layer CSE collapses the copies.
|
|
112
|
-
const PURE_FLATTEN_OPS = new Set([
|
|
125
|
+
const PURE_FLATTEN_OPS = new Set([
|
|
126
|
+
'+', '-', '*', '/', '%', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>',
|
|
127
|
+
// pure value-producing ops with no side effects — safe to duplicate (CSE collapses copies):
|
|
128
|
+
// comparisons, logical, bit-not, and the conditional. Lets a branchy leaf like noise's
|
|
129
|
+
// `grad(h,x,y) => { …; u = (h&1)===0 ? x : -x; … }` flatten to an expression so it inlines
|
|
130
|
+
// into its multi-call caller `perlin` — `lerp(grad(a), grad(b), u)` then collapses end to end.
|
|
131
|
+
'<', '<=', '>', '>=', '==', '!=', '===', '!==', '&&', '||', '!', '~', '?:',
|
|
132
|
+
])
|
|
113
133
|
const pureFlattenExpr = (n) => {
|
|
114
134
|
if (typeof n === 'number' || typeof n === 'string') return true // literal or ident
|
|
115
135
|
if (!Array.isArray(n)) return false
|
|
@@ -163,13 +183,19 @@ const inlineInExpr = (node, candidates) => {
|
|
|
163
183
|
|
|
164
184
|
const inlineInStmt = (stmt, candidates, loopVariantNames = null) => {
|
|
165
185
|
if (!Array.isArray(stmt)) return { node: stmt, changed: false }
|
|
166
|
-
// Statement-position call:
|
|
186
|
+
// Statement-position call: the result is unused, but the callee's return
|
|
187
|
+
// EXPRESSION may still carry side effects — an expression-bodied arrow whose body
|
|
188
|
+
// is itself effectful (`seek = n => idx = n` inlines to the assignment `idx = i`;
|
|
189
|
+
// a one-liner that calls another fn) puts the effect in `value`, not `prefix`.
|
|
190
|
+
// Emit it as a trailing statement so the effect runs; a pure value is dropped
|
|
191
|
+
// later by vacuum/DCE. (Dropping it lost the parser's seek() idx-advance → ∞ loop.)
|
|
167
192
|
if (isCandidateCall(stmt, candidates)) {
|
|
168
193
|
const args = callArgs(stmt)
|
|
169
194
|
const shape = args && inlinedBody(candidates.get(stmt[1]), args)
|
|
170
195
|
if (shape) {
|
|
171
196
|
const { hoisted, rest } = partitionInvariantPrefix(shape.prefix, loopVariantNames)
|
|
172
|
-
|
|
197
|
+
const splice = shape.value !== null ? [...rest, shape.value] : rest
|
|
198
|
+
return { node: ['{}', [';', ...splice]], changed: true, splice, hoisted }
|
|
173
199
|
}
|
|
174
200
|
}
|
|
175
201
|
// `let/const X = call(...)` with single decl: inline as prefix + decl(value).
|
|
@@ -259,9 +285,99 @@ const inlineInStmt = (stmt, candidates, loopVariantNames = null) => {
|
|
|
259
285
|
return { node: stmt, changed: false }
|
|
260
286
|
}
|
|
261
287
|
|
|
288
|
+
// Short-circuit operators: only the FIRST operand is unconditionally evaluated; a call in
|
|
289
|
+
// a later operand might not run, so it can't be hoisted.
|
|
290
|
+
const SHORT_CIRCUIT = new Set(['?:', '?', '&&', '||', '??'])
|
|
291
|
+
// Optional chaining: jz's own desugaring already tees the base to evaluate it once, and
|
|
292
|
+
// the key/args run conditionally — so the hoist treats the WHOLE expression as opaque (no
|
|
293
|
+
// operand, not even the base, is hoisted out) to avoid colliding with that desugaring.
|
|
294
|
+
const OPTIONAL_CHAIN = new Set(['?.', '?.[]', '?.()'])
|
|
295
|
+
// Mutating expression operators — evaluating one is an observable side effect.
|
|
296
|
+
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**=', '&&=', '||=', '??=', '++', '--'])
|
|
297
|
+
// Does evaluating this expression have an observable side effect (a call or assignment)?
|
|
298
|
+
const containsEffect = (n) => Array.isArray(n) && n[0] !== '=>' &&
|
|
299
|
+
(n[0] === '()' || n[0] === '?.()' || ASSIGN_OPS.has(n[0]) || n.slice(1).some(containsEffect))
|
|
300
|
+
|
|
301
|
+
// Hoist an unconditionally-evaluated NESTED call to a block-body candidate out to a
|
|
302
|
+
// preceding `const __h = call(...)` temp. inlineInStmt folds block-body candidates only at
|
|
303
|
+
// a DIRECT `const X = call` / `X = call`; a call buried in an expression (noise's
|
|
304
|
+
// `sum = sum + amp * perlin(x)`) is reached by neither that path nor inlineInExpr (which
|
|
305
|
+
// only substitutes zero-prefix expr-bodies). Hoisting normalizes it to the direct form.
|
|
306
|
+
// Only block-body candidates and only unconditional positions — preserving evaluation order
|
|
307
|
+
// + count. Statement HEADERS that are expression positions (for-init/update, while/if test)
|
|
308
|
+
// are left untouched: there's no sound place for a hoisted decl there, so those calls just
|
|
309
|
+
// stay outlined. Conservatively leaves unrecognized statement shapes alone.
|
|
310
|
+
const hoistNestedCalls = (body, blockNames) => {
|
|
311
|
+
if (!blockNames.size || !Array.isArray(body)) return { node: body, changed: false }
|
|
312
|
+
let changed = false
|
|
313
|
+
const seq = (stmts) => stmts.length === 1 ? stmts[0] : [';', ...stmts]
|
|
314
|
+
// Lifting a call to the pre-decl block moves its evaluation to the TOP of the statement.
|
|
315
|
+
// Sound only if no observable side effect is evaluated BEFORE it — else its effect jumps
|
|
316
|
+
// ahead of that one (`a() + helper(x)` must keep a()'s effect first). `eff.seen` threads
|
|
317
|
+
// left-to-right through evaluation order: a call or assignment LEFT IN PLACE marks every
|
|
318
|
+
// later position. A hoisted call moves as a unit — its args run in a fresh inner eff, and
|
|
319
|
+
// it does NOT advance the outer eff (the whole unit relocates together, order intact).
|
|
320
|
+
const hExpr = (n, pre, cond, eff) => {
|
|
321
|
+
if (!Array.isArray(n) || n[0] === '=>') return n
|
|
322
|
+
if (!cond && !eff.seen && n[0] === '()' && typeof n[1] === 'string' && blockNames.has(n[1])) {
|
|
323
|
+
const call = [n[0], n[1], ...n.slice(2).map(a => hExpr(a, pre, false, { seen: false }))]
|
|
324
|
+
const tmp = `${T}inl${ctx.func.uniq++}_h`
|
|
325
|
+
pre.push(['const', ['=', tmp, call]])
|
|
326
|
+
changed = true
|
|
327
|
+
return [null, tmp]
|
|
328
|
+
}
|
|
329
|
+
if (OPTIONAL_CHAIN.has(n[0])) {
|
|
330
|
+
const out = [n[0], ...n.slice(1).map(c => hExpr(c, pre, true, eff))]
|
|
331
|
+
if (n[0] === '?.()') eff.seen = true // optional CALL may run
|
|
332
|
+
return out
|
|
333
|
+
}
|
|
334
|
+
if (SHORT_CIRCUIT.has(n[0]))
|
|
335
|
+
return [n[0], hExpr(n[1], pre, cond, eff), ...n.slice(2).map(c => hExpr(c, pre, true, eff))]
|
|
336
|
+
const out = [n[0], ...n.slice(1).map(c => hExpr(c, pre, cond, eff))]
|
|
337
|
+
if (n[0] === '()' || ASSIGN_OPS.has(n[0])) eff.seen = true // a call/assign left in place is an effect
|
|
338
|
+
return out
|
|
339
|
+
}
|
|
340
|
+
// A RHS that is DIRECTLY a candidate call is already folded by inlineInStmt's
|
|
341
|
+
// `const X = call` / `X = call` paths — hoisting it would be redundant and (for an
|
|
342
|
+
// object/array-literal `{}`-bodied factory) would break the post-inline alias chain.
|
|
343
|
+
// Only hoist NESTED calls; leave a top-level direct call to those paths.
|
|
344
|
+
const directCall = (e) => Array.isArray(e) && e[0] === '()' && typeof e[1] === 'string' && blockNames.has(e[1])
|
|
345
|
+
const hStmt = (s) => { // → array of statements (hoisted decls prepended)
|
|
346
|
+
if (!Array.isArray(s)) return [s]
|
|
347
|
+
switch (s[0]) {
|
|
348
|
+
case ';': return [[';', ...s.slice(1).flatMap(hStmt)]]
|
|
349
|
+
case '{}': return [['{}', seq(hStmt(s[1]))]]
|
|
350
|
+
case 'if': return [s.length > 3
|
|
351
|
+
? ['if', s[1], seq(hStmt(s[2])), seq(hStmt(s[3]))]
|
|
352
|
+
: ['if', s[1], seq(hStmt(s[2]))]]
|
|
353
|
+
case 'for': { const i = forLoopBodyIndex(s); return [withForLoopBody(s, seq(hStmt(s[i])))] }
|
|
354
|
+
case 'while': return [['while', s[1], seq(hStmt(s[2]))]]
|
|
355
|
+
case 'let': case 'const': {
|
|
356
|
+
if (s.length === 2 && Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' && !directCall(s[1][2])) {
|
|
357
|
+
const pre = []; const rhs = hExpr(s[1][2], pre, false, { seen: false })
|
|
358
|
+
return pre.length ? [...pre, [s[0], ['=', s[1][1], rhs]]] : [s]
|
|
359
|
+
}
|
|
360
|
+
return [s]
|
|
361
|
+
}
|
|
362
|
+
// A computed assign target (`a[i]=…`) evaluates its index BEFORE the RHS, so an effect
|
|
363
|
+
// there (`a[j++]=…`) must block hoisting too — seed eff.seen from the LHS.
|
|
364
|
+
case '=': { if (directCall(s[2])) return [s]; const pre = []; const rhs = hExpr(s[2], pre, false, { seen: containsEffect(s[1]) }); return pre.length ? [...pre, ['=', s[1], rhs]] : [s] }
|
|
365
|
+
case 'return': { if (s.length < 2 || directCall(s[1])) return [s]; const pre = []; const v = hExpr(s[1], pre, false, { seen: false }); return pre.length ? [...pre, ['return', v]] : [s] }
|
|
366
|
+
default: return [s] // unrecognized shape (break/continue/throw/try/switch): leave alone
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const out = hStmt(body)
|
|
370
|
+
return { node: changed ? seq(out) : body, changed }
|
|
371
|
+
}
|
|
372
|
+
|
|
262
373
|
export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
263
374
|
const cfg = ctx.transform.optimize
|
|
264
375
|
if (cfg && cfg.sourceInline === false) return false
|
|
376
|
+
// Transitive candidacy + expression-position hoisting are a size↔speed trade (they
|
|
377
|
+
// pull a large multi-call leaf like noise's perlin fully into its hot caller, where
|
|
378
|
+
// the lower tiers prefer to keep multi-caller helpers outlined for V8 tier-up). Gate
|
|
379
|
+
// both on the speed tier so levels ≤2 keep their conservative inlining policy.
|
|
380
|
+
const speedTier = !!(cfg && cfg.inlineFns)
|
|
265
381
|
|
|
266
382
|
const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
|
|
267
383
|
const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
|
|
@@ -315,7 +431,14 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
315
431
|
// entry points, not pulling a leaf INTO an export's hot loop (game-of-life's
|
|
316
432
|
// step calls rot per cell; pre-Turboshaft wasm tiers never inline calls).
|
|
317
433
|
const leaves = new Set()
|
|
434
|
+
// Transitive candidacy via fixpoint: a function whose only user-callees are THEMSELVES
|
|
435
|
+
// candidates (so they inline away) can be inlined too. noise's `perlin` calls grad/fade/
|
|
436
|
+
// lerp (loop-free leaves) — once those are candidates, perlin clears the call-bearing-body
|
|
437
|
+
// gate and becomes a leaf candidate. Each pass adds ≥1 or stops, so it's bounded.
|
|
438
|
+
for (let recollect = true; recollect;) {
|
|
439
|
+
recollect = false
|
|
318
440
|
for (const func of ctx.func.list) {
|
|
441
|
+
if (candidates.has(func.name)) continue
|
|
319
442
|
const sites = sitesByCallee.get(func.name)
|
|
320
443
|
// Exported leaf/kernel with exactly one internal caller (e.g. fill→beat in
|
|
321
444
|
// floatbeat): inline into the caller's loop but keep the export for external
|
|
@@ -336,7 +459,13 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
336
459
|
const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
|
|
337
460
|
const hasLoop = some(func.body, n => LOOP_OPS.has(n[0]))
|
|
338
461
|
const isTinyLeaf = !hasLoop && nodeSize(func.body) <= 15
|
|
339
|
-
|
|
462
|
+
// A small leaf (no loop, ≤40 nodes) is cheap to splice even when called several times — its
|
|
463
|
+
// per-call overhead + lost cross-call fusion dwarfs the ≤8× duplication, and temp-binding +
|
|
464
|
+
// flattenPrefix keep the spliced body bounded (no arg re-evaluation, CSE collapses copies).
|
|
465
|
+
// The 2-site non-tiny-leaf cap would otherwise outline a hot helper like noise's `grad`
|
|
466
|
+
// (~30 nodes, called 4× from perlin) and freeze the call overhead per pixel.
|
|
467
|
+
const isSmallLeaf = !hasLoop && nodeSize(func.body) <= 48
|
|
468
|
+
if (!sites || sites.length < 1 || (!isTinyLeaf && !isSmallLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
|
|
340
469
|
const stmts = blockStmts(func.body)
|
|
341
470
|
// Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
|
|
342
471
|
// return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
|
|
@@ -356,7 +485,10 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
356
485
|
// that get hammered from a hot caller's loop — replacing the call with its
|
|
357
486
|
// body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
|
|
358
487
|
if (!hasLoop) {
|
|
359
|
-
|
|
488
|
+
// Calls to functions that are THEMSELVES candidates are fine — they inline away;
|
|
489
|
+
// only a call to a non-candidate user function blocks (a later fixpoint pass re-checks).
|
|
490
|
+
// Speed-tier only; lower tiers keep the strict "any user call ⇒ outline" rule.
|
|
491
|
+
if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]) && !(speedTier && candidates.has(n[1])))) continue
|
|
360
492
|
// Per-iteration call overhead dwarfs body-size bloat when EVERY site sits
|
|
361
493
|
// inside a caller's loop (game-of-life's rot: ~40 nodes × 2 sites, fired
|
|
362
494
|
// for most of 260k cells/frame; cloth's relax: ~160 nodes × 2 sites, fired
|
|
@@ -367,7 +499,10 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
367
499
|
// to ≤2 sites, so the spliced duplication is at most ~2× a bounded body.
|
|
368
500
|
const allSitesInLoop = sites.every(site =>
|
|
369
501
|
site.callerFunc?.body && containsNode(site.callerFunc.body, site.node, false))
|
|
370
|
-
|
|
502
|
+
// Non-in-loop cap is 40 (not 30) so a small leaf called from a straight-line but
|
|
503
|
+
// transitively-hot caller still inlines (noise's grad is called from perlin, which has
|
|
504
|
+
// no loop of its own but is itself the per-pixel kernel). Still tightly bounded.
|
|
505
|
+
if (nodeSize(func.body) > (allSitesInLoop ? 200 : 48)) continue
|
|
371
506
|
}
|
|
372
507
|
if (some(func.body, n => n[0] === '()' && n[1] === func.name)) continue
|
|
373
508
|
// Kernels with nested loops (depth ≥ 2) are typically large and the inner
|
|
@@ -386,6 +521,8 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
386
521
|
forwarders.add(func.name)
|
|
387
522
|
if (!hasLoop) leaves.add(func.name)
|
|
388
523
|
candidates.set(func.name, func)
|
|
524
|
+
if (speedTier) recollect = true // only the speed-tier transitive relaxation needs a re-pass
|
|
525
|
+
}
|
|
389
526
|
}
|
|
390
527
|
if (!candidates.size) return false
|
|
391
528
|
|
|
@@ -436,20 +573,38 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
436
573
|
// Route those through inlineInExpr so the call is replaced by the inlined
|
|
437
574
|
// value expression instead.
|
|
438
575
|
const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
|
|
439
|
-
|
|
440
|
-
|
|
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).
|
|
576
|
+
// Expression-position pass takes the leaf-safe subset for exports — the same tier-up
|
|
577
|
+
// rationale as the statement path (leaves into exports are fine; relocated kernels are not).
|
|
447
578
|
const exprActive = func.exported
|
|
448
579
|
? new Map([...exprOnlyCandidates].filter(([n]) => exportedCandidates.has(n)))
|
|
449
580
|
: exprOnlyCandidates
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
581
|
+
// Iterate to a (bounded) fixpoint: inlining a call whose args are themselves candidate calls
|
|
582
|
+
// binds those args to temps (`t0 = grad(a)`); the next pass folds the candidate into the temp
|
|
583
|
+
// decl. Depth is bounded by call nesting (a small constant), capped here so a pathological
|
|
584
|
+
// chain can't loop unbounded.
|
|
585
|
+
// Loop-free block-body LEAVES: the stmt path folds them only at a DIRECT `const X =
|
|
586
|
+
// call`, never nested in an expression. Hoisting such a call to a temp (in the iter
|
|
587
|
+
// fixpoint below) lets inlineInStmt then fold it — the noise `sum + amp*perlin(x)`
|
|
588
|
+
// shape. Restricted to LEAVES (no own loop): a loop kernel called in expression
|
|
589
|
+
// position (e.g. a 2-site `reduce`) was deliberately staying outlined for V8 tier-up,
|
|
590
|
+
// and hoisting it would pull the loop into a cold caller.
|
|
591
|
+
const blockNames = new Set()
|
|
592
|
+
for (const n of activeCandidates.keys()) if (leaves.has(n) && !exprActive.has(n)) blockNames.add(n)
|
|
593
|
+
let body = func.body, bodyChanged = false
|
|
594
|
+
for (let iter = 0; iter < 4; iter++) {
|
|
595
|
+
let iterChanged = false
|
|
596
|
+
if (speedTier && !isExprBody && blockNames.size) {
|
|
597
|
+
const h = hoistNestedCalls(body, blockNames)
|
|
598
|
+
if (h.changed) { body = h.node; iterChanged = true }
|
|
599
|
+
}
|
|
600
|
+
const r = isExprBody ? inlineInExpr(body, activeCandidates) : inlineInStmt(body, activeCandidates)
|
|
601
|
+
if (r.changed) { body = r.node; iterChanged = true }
|
|
602
|
+
if (exprActive.size) {
|
|
603
|
+
const e = inlineInExpr(body, exprActive)
|
|
604
|
+
if (e.changed) { body = e.node; iterChanged = true }
|
|
605
|
+
}
|
|
606
|
+
if (!iterChanged) break
|
|
607
|
+
bodyChanged = true
|
|
453
608
|
}
|
|
454
609
|
if (bodyChanged) { func.body = body; changed = true }
|
|
455
610
|
}
|