jz 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- 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 +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +266 -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 +414 -186
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +586 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +589 -202
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -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
package/src/plan.js
DELETED
|
@@ -1,2132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-emit compile planning: bridges prepare (AST shape) and emit (wasm bytes).
|
|
3
|
-
*
|
|
4
|
-
* # Stage contract
|
|
5
|
-
* IN: populated `ctx` from prepare.js (functions, schemas, scopes, modules)
|
|
6
|
-
* plus the prepared AST.
|
|
7
|
-
* OUT: returns a `programFacts` object; mutates `ctx` so each function has
|
|
8
|
-
* narrowed signatures, finalized global reps, and per-call decisions.
|
|
9
|
-
*
|
|
10
|
-
* # Pipeline (top-level `plan(ast)`)
|
|
11
|
-
* 1. unboxConstTypedGlobals — finalize global storage. (Global value facts
|
|
12
|
-
* themselves are seeded by prepare via `infer.recordGlobalRep`.)
|
|
13
|
-
* 2. collectProgramFacts — sweep arrow bodies for typed-elem usage, key sets,
|
|
14
|
-
* loop depth, control-transfer shapes; rerun if hot inlining changes the AST.
|
|
15
|
-
* 3. materializeAutoBoxSchemas / resolveClosureWidth — settle layout decisions.
|
|
16
|
-
* 4. Whole-program narrowing (skipped on simple programs):
|
|
17
|
-
* - narrowSignatures — pick a specialization per function from call sites
|
|
18
|
-
* - specializeBimorphicTyped — split typed-elem hot paths into two variants
|
|
19
|
-
* when callers diverge between two ctors
|
|
20
|
-
* - refineDynKeys — tighten dynamic property-key sets
|
|
21
|
-
*
|
|
22
|
-
* No bytes are emitted here; emit.js consumes the planned ctx + programFacts.
|
|
23
|
-
*
|
|
24
|
-
* @module plan
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { ctx } from './ctx.js'
|
|
28
|
-
import { T, VAL, ASSIGN_OPS, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts, analyzeFuncNamespaces, extractParams, intLiteralValue } from './analyze.js'
|
|
29
|
-
import { includeModule } from './autoload.js'
|
|
30
|
-
import { MAX_CLOSURE_ARITY, UNDEF_WAT } from './ir.js'
|
|
31
|
-
import narrowSignatures, { specializeBimorphicTyped, refineDynKeys, applyJsstringBoundaryCarrierStandalone, narrowBoolResults } from './narrow.js'
|
|
32
|
-
|
|
33
|
-
const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
|
|
34
|
-
const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
|
|
35
|
-
// Fixed-size typed arrays eligible for scalar replacement, mapped to the element
|
|
36
|
-
// store-coercion kind ('' = none, i.e. Float64Array's f64-identity). Excluded:
|
|
37
|
-
// Float32Array — store coercion is `Math.fround`, needs the `math` module pulled at plan time
|
|
38
|
-
// Uint32Array — element range [0, 2^32) exceeds what jz keeps as f64 after `x >>> 0` (i32-narrowed)
|
|
39
|
-
// Uint8ClampedArray — round-half-to-even clamp
|
|
40
|
-
// Coerced (truthy) types are scalarized only when fully local — any escape (passed
|
|
41
|
-
// to a call, `.buffer`/view aliasing, etc.) keeps the real allocation, since the
|
|
42
|
-
// mirror/fence path can't track writes through an alias that outlives the fence.
|
|
43
|
-
const SCALAR_TYPED_COERCE = {
|
|
44
|
-
'new.Float64Array': '',
|
|
45
|
-
'new.Int32Array': 'i32',
|
|
46
|
-
'new.Int16Array': 'i16', 'new.Uint16Array': 'u16',
|
|
47
|
-
'new.Int8Array': 'i8', 'new.Uint8Array': 'u8',
|
|
48
|
-
}
|
|
49
|
-
// AST for the store coercion a typed-array element does on write (`arr[i] = v`).
|
|
50
|
-
// All expressible with operators jz already lowers post-plan (no module deps).
|
|
51
|
-
const coerceAST = (kind, expr) => {
|
|
52
|
-
if (kind === 'i32') return ['|', expr, [null, 0]]
|
|
53
|
-
if (kind === 'i16') return ['>>', ['<<', expr, [null, 16]], [null, 16]]
|
|
54
|
-
if (kind === 'u16') return ['&', expr, [null, 0xffff]]
|
|
55
|
-
if (kind === 'i8') return ['>>', ['<<', expr, [null, 24]], [null, 24]]
|
|
56
|
-
if (kind === 'u8') return ['&', expr, [null, 0xff]]
|
|
57
|
-
return expr
|
|
58
|
-
}
|
|
59
|
-
const maxScalarTypedArrayLen = () => ctx.transform.optimize?.scalarTypedArrayLen ?? 32
|
|
60
|
-
const maxScalarTypedLoopUnroll = () => ctx.transform.optimize?.scalarTypedLoopUnroll ?? 16
|
|
61
|
-
const maxScalarTypedNestedUnroll = () => ctx.transform.optimize?.scalarTypedNestedUnroll ?? 128
|
|
62
|
-
|
|
63
|
-
const isSeq = node => Array.isArray(node) && node[0] === ';'
|
|
64
|
-
const blockStmts = body => {
|
|
65
|
-
if (!Array.isArray(body) || body[0] !== '{}') return null
|
|
66
|
-
const inner = body[1]
|
|
67
|
-
if (!Array.isArray(inner)) return inner == null ? [] : [inner]
|
|
68
|
-
return inner[0] === ';' ? inner.slice(1) : [inner]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const callArgs = node => {
|
|
72
|
-
if (!Array.isArray(node) || node[0] !== '()') return null
|
|
73
|
-
const raw = node[2]
|
|
74
|
-
return raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const isSimpleArg = node => {
|
|
78
|
-
if (typeof node === 'string' || typeof node === 'number') return true
|
|
79
|
-
if (!Array.isArray(node)) return false
|
|
80
|
-
if (node[0] == null) return typeof node[1] === 'number'
|
|
81
|
-
if (node[0] === 'str') return typeof node[1] === 'string'
|
|
82
|
-
if (node[0] === 'u-' || (node[0] === '-' && node.length === 2)) return isSimpleArg(node[1])
|
|
83
|
-
if (['+', '-', '*', '%', '&', '|', '^', '<<', '>>', '>>>'].includes(node[0]))
|
|
84
|
-
return isSimpleArg(node[1]) && isSimpleArg(node[2])
|
|
85
|
-
return false
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const scanBody = (node, fn) => {
|
|
89
|
-
if (!Array.isArray(node)) return false
|
|
90
|
-
if (fn(node)) return true
|
|
91
|
-
if (node[0] === '=>') return false
|
|
92
|
-
for (let i = 1; i < node.length; i++) if (scanBody(node[i], fn)) return true
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const loopDepth = (node, depth) => {
|
|
97
|
-
if (!Array.isArray(node)) return depth
|
|
98
|
-
if (node[0] === '=>') return depth
|
|
99
|
-
const here = LOOP_OPS.has(node[0]) ? depth + 1 : depth
|
|
100
|
-
let max = here
|
|
101
|
-
for (let i = 1; i < node.length; i++) {
|
|
102
|
-
const d = loopDepth(node[i], here)
|
|
103
|
-
if (d > max) max = d
|
|
104
|
-
}
|
|
105
|
-
return max
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const nodeSize = (node) => {
|
|
109
|
-
if (!Array.isArray(node)) return 1
|
|
110
|
-
let n = 1
|
|
111
|
-
for (let i = 1; i < node.length; i++) n += nodeSize(node[i])
|
|
112
|
-
return n
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const collectBindings = (node, out) => {
|
|
116
|
-
if (!Array.isArray(node)) return
|
|
117
|
-
const op = node[0]
|
|
118
|
-
if (op === '=>') return
|
|
119
|
-
if (op === 'let' || op === 'const') {
|
|
120
|
-
for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
|
|
121
|
-
}
|
|
122
|
-
for (let i = 1; i < node.length; i++) collectBindings(node[i], out)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const collectBindingTarget = (node, out) => {
|
|
126
|
-
if (typeof node === 'string') { out.add(node); return }
|
|
127
|
-
if (!Array.isArray(node)) return
|
|
128
|
-
if (node[0] === '=') collectBindingTarget(node[1], out)
|
|
129
|
-
else if (node[0] === '...' && typeof node[1] === 'string') out.add(node[1])
|
|
130
|
-
else if (node[0] === ',' || node[0] === '[]' || node[0] === '{}')
|
|
131
|
-
for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const mutatesAny = (node, names) => scanBody(node, n => {
|
|
135
|
-
const op = n[0]
|
|
136
|
-
if ((op === '++' || op === '--') && typeof n[1] === 'string') return names.has(n[1])
|
|
137
|
-
return ASSIGN_OPS.has(op) && typeof n[1] === 'string' && names.has(n[1])
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
|
|
141
|
-
|
|
142
|
-
const intLit = node => {
|
|
143
|
-
if (typeof node === 'number' && Number.isInteger(node)) return node
|
|
144
|
-
if (Array.isArray(node) && node[0] == null && Number.isInteger(node[1])) return node[1]
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const constIntExpr = (node) => {
|
|
149
|
-
const lit = intLit(node)
|
|
150
|
-
if (lit != null) return lit
|
|
151
|
-
if (typeof node === 'string') return ctx.scope.constInts?.get(node) ?? null
|
|
152
|
-
if (!Array.isArray(node)) return null
|
|
153
|
-
const op = node[0]
|
|
154
|
-
if (op === 'u-') {
|
|
155
|
-
const v = constIntExpr(node[1])
|
|
156
|
-
return v == null ? null : -v
|
|
157
|
-
}
|
|
158
|
-
if (node.length !== 3) return null
|
|
159
|
-
const a = constIntExpr(node[1]), b = constIntExpr(node[2])
|
|
160
|
-
if (a == null || b == null) return null
|
|
161
|
-
if (op === '+') return a + b
|
|
162
|
-
if (op === '-') return a - b
|
|
163
|
-
if (op === '*') return a * b
|
|
164
|
-
if (op === '<<') return a << b
|
|
165
|
-
return null
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const setCallArgs = (node, args) => {
|
|
169
|
-
node[2] = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const scalarArrayElems = (expr) => {
|
|
173
|
-
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
174
|
-
const elems = expr.slice(1)
|
|
175
|
-
if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...') || !isSimpleArg(e))) return null
|
|
176
|
-
return elems
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const scalarObjectProps = (expr) => {
|
|
180
|
-
if (!Array.isArray(expr) || expr[0] !== '{}') return null
|
|
181
|
-
const props = staticObjectProps(expr.slice(1))
|
|
182
|
-
if (!props) return null
|
|
183
|
-
const seen = new Set()
|
|
184
|
-
for (let i = 0; i < props.names.length; i++) {
|
|
185
|
-
const name = props.names[i]
|
|
186
|
-
if (seen.has(name) || !isSimpleArg(props.values[i])) return null
|
|
187
|
-
seen.add(name)
|
|
188
|
-
}
|
|
189
|
-
return props
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const fixedScalarTypedArray = (expr) => {
|
|
193
|
-
const ctor = typedElemCtor(expr)
|
|
194
|
-
if (ctor == null || !(ctor in SCALAR_TYPED_COERCE)) return null
|
|
195
|
-
const args = callArgs(expr)
|
|
196
|
-
if (!args || args.length !== 1) return null
|
|
197
|
-
const len = constIntExpr(args[0])
|
|
198
|
-
return len != null && len >= 0 && len <= maxScalarTypedArrayLen()
|
|
199
|
-
? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
203
|
-
|
|
204
|
-
const safeScalarArrayUse = (node, name, len, parentOp = null) => {
|
|
205
|
-
if (typeof node === 'string') return node !== name
|
|
206
|
-
if (!Array.isArray(node)) return true
|
|
207
|
-
const op = node[0]
|
|
208
|
-
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
209
|
-
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
210
|
-
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
211
|
-
// Element write `name[idx] (op)= v` / `name[idx]++`: an out-of-bounds index
|
|
212
|
-
// grows the array (sparse-array semantics), which the fixed scalar slot set
|
|
213
|
-
// can't model — reject unless idx is a literal within the literal's bounds.
|
|
214
|
-
if ((ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
|
|
215
|
-
&& Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
|
|
216
|
-
const idx = intLit(node[1][2])
|
|
217
|
-
if (idx == null || idx < 0 || idx >= len) return false
|
|
218
|
-
for (let i = 2; i < node.length; i++) if (!safeScalarArrayUse(node[i], name, len, op)) return false
|
|
219
|
-
return true
|
|
220
|
-
}
|
|
221
|
-
if (op === '[]' && node[1] === name) return intLit(node[2]) != null
|
|
222
|
-
if (op === '...' && node[1] === name) return parentOp === '['
|
|
223
|
-
for (let i = 1; i < node.length; i++) {
|
|
224
|
-
if (!safeScalarArrayUse(node[i], name, len, op)) return false
|
|
225
|
-
}
|
|
226
|
-
return true
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const rewriteScalarArrayUses = (node, arrays, parentOp = null) => {
|
|
230
|
-
if (!Array.isArray(node)) return node
|
|
231
|
-
const op = node[0]
|
|
232
|
-
if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') {
|
|
233
|
-
return [, arrays.get(node[1]).length]
|
|
234
|
-
}
|
|
235
|
-
if (op === '[]' && arrays.has(node[1])) {
|
|
236
|
-
const idx = intLit(node[2])
|
|
237
|
-
const elems = arrays.get(node[1])
|
|
238
|
-
return idx != null && idx >= 0 && idx < elems.length ? elems[idx] : [, undefined]
|
|
239
|
-
}
|
|
240
|
-
if (op === '[') {
|
|
241
|
-
const out = ['[']
|
|
242
|
-
for (let i = 1; i < node.length; i++) {
|
|
243
|
-
const item = node[i]
|
|
244
|
-
if (Array.isArray(item) && item[0] === '...' && arrays.has(item[1])) {
|
|
245
|
-
out.push(...arrays.get(item[1]))
|
|
246
|
-
} else {
|
|
247
|
-
out.push(rewriteScalarArrayUses(item, arrays, op))
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return out
|
|
251
|
-
}
|
|
252
|
-
return node.map((part, i) => i === 0 ? part : rewriteScalarArrayUses(part, arrays, op))
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const safeScalarObjectUse = (node, name, keys) => {
|
|
256
|
-
if (typeof node === 'string') return node !== name
|
|
257
|
-
if (!Array.isArray(node)) return true
|
|
258
|
-
const op = node[0]
|
|
259
|
-
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
260
|
-
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
261
|
-
if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2])
|
|
262
|
-
if (op === '[]' && node[1] === name) {
|
|
263
|
-
const key = staticPropertyKey(node[2])
|
|
264
|
-
return key != null && keys.has(key)
|
|
265
|
-
}
|
|
266
|
-
if (op === '...' && node[1] === name) return false
|
|
267
|
-
for (let i = 1; i < node.length; i++) {
|
|
268
|
-
if (!safeScalarObjectUse(node[i], name, keys)) return false
|
|
269
|
-
}
|
|
270
|
-
return true
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const rewriteScalarObjectUses = (node, objects) => {
|
|
274
|
-
if (!Array.isArray(node)) return node
|
|
275
|
-
const op = node[0]
|
|
276
|
-
if ((op === '.' || op === '?.') && objects.has(node[1])) {
|
|
277
|
-
const fields = objects.get(node[1])
|
|
278
|
-
return fields.get(node[2]) ?? [, undefined]
|
|
279
|
-
}
|
|
280
|
-
if (op === '[]' && objects.has(node[1])) {
|
|
281
|
-
const key = staticPropertyKey(node[2])
|
|
282
|
-
const fields = objects.get(node[1])
|
|
283
|
-
return key != null ? (fields.get(key) ?? [, undefined]) : node
|
|
284
|
-
}
|
|
285
|
-
return node.map((part, i) => i === 0 ? part : rewriteScalarObjectUses(part, objects))
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const typedArraySlotIndex = (node, len) => {
|
|
289
|
-
const idx = constIntExpr(node)
|
|
290
|
-
return idx != null && idx >= 0 && idx < len ? idx : null
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// `coerce` truthy ⇒ the array's element type truncates on store (Int*/Uint* views),
|
|
294
|
-
// so in-place updates (`arr[i]++`, `arr[i] += x`) can't be a plain `slot`-op rewrite —
|
|
295
|
-
// reject them and only scalarize plain `arr[i] = v` writes and `arr[i]` reads.
|
|
296
|
-
const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
|
|
297
|
-
if (typeof node === 'string') return node !== name
|
|
298
|
-
if (!Array.isArray(node)) return true
|
|
299
|
-
const op = node[0]
|
|
300
|
-
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
301
|
-
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
302
|
-
if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
|
|
303
|
-
if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
|
|
304
|
-
return !coerce && typedArraySlotIndex(node[1][2], len) != null
|
|
305
|
-
if (ASSIGN_TARGET_OPS.has(op)) {
|
|
306
|
-
if (node[1] === name) return false
|
|
307
|
-
if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
|
|
308
|
-
if (coerce && op !== '=') return false
|
|
309
|
-
if (typedArraySlotIndex(node[1][2], len) == null) return false
|
|
310
|
-
for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
311
|
-
return true
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (op === '...' && node[1] === name) return false
|
|
315
|
-
for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
316
|
-
return true
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const mentionsName = (node, name) => {
|
|
320
|
-
if (typeof node === 'string') return node === name
|
|
321
|
-
if (!Array.isArray(node) || node[0] === '=>') return false
|
|
322
|
-
for (let i = 1; i < node.length; i++) if (mentionsName(node[i], name)) return true
|
|
323
|
-
return false
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const rewriteScalarTypedArrayUses = (node, arrays) => {
|
|
327
|
-
if (!Array.isArray(node)) return node
|
|
328
|
-
const op = node[0]
|
|
329
|
-
const slotFor = (idxNode, entry) => {
|
|
330
|
-
const idx = typedArraySlotIndex(idxNode, entry.len)
|
|
331
|
-
return idx == null ? null : entry.slots[idx]
|
|
332
|
-
}
|
|
333
|
-
if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') return [null, arrays.get(node[1]).len]
|
|
334
|
-
if (op === '[]' && arrays.has(node[1])) return slotFor(node[2], arrays.get(node[1])) ?? node
|
|
335
|
-
if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
|
|
336
|
-
const slot = slotFor(node[1][2], arrays.get(node[1][1]))
|
|
337
|
-
return slot ? [op, slot] : node
|
|
338
|
-
}
|
|
339
|
-
if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
|
|
340
|
-
const entry = arrays.get(node[1][1])
|
|
341
|
-
const slot = slotFor(node[1][2], entry)
|
|
342
|
-
if (!slot) return node
|
|
343
|
-
const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
|
|
344
|
-
return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
|
|
345
|
-
}
|
|
346
|
-
return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const scalarTypedArrayStores = (name, entry) =>
|
|
350
|
-
entry.slots.map((slot, i) => ['=', ['[]', name, [null, i]], slot])
|
|
351
|
-
|
|
352
|
-
const scalarTypedArrayLoads = (name, entry) =>
|
|
353
|
-
entry.slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])
|
|
354
|
-
|
|
355
|
-
const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
|
|
356
|
-
if (!Array.isArray(node)) return out
|
|
357
|
-
const op = node[0]
|
|
358
|
-
const addSlot = target => {
|
|
359
|
-
if (Array.isArray(target) && target[0] === '[]' && target[1] === name) {
|
|
360
|
-
const idx = typedArraySlotIndex(target[2], len)
|
|
361
|
-
if (idx != null) out.add(idx)
|
|
362
|
-
return true
|
|
363
|
-
}
|
|
364
|
-
return false
|
|
365
|
-
}
|
|
366
|
-
if ((op === '++' || op === '--') && addSlot(node[1])) return out
|
|
367
|
-
if (ASSIGN_TARGET_OPS.has(op) && addSlot(node[1])) {
|
|
368
|
-
for (let i = 2; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
|
|
369
|
-
return out
|
|
370
|
-
}
|
|
371
|
-
if (op !== '=>') for (let i = 1; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
|
|
372
|
-
return out
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const hasScalarTypedArrayRead = (node, name) => {
|
|
376
|
-
if (!Array.isArray(node)) return false
|
|
377
|
-
const op = node[0]
|
|
378
|
-
const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
|
|
379
|
-
if ((op === '++' || op === '--') && isTarget(node[1])) return true
|
|
380
|
-
if (ASSIGN_TARGET_OPS.has(op)) {
|
|
381
|
-
if (isTarget(node[1])) {
|
|
382
|
-
if (op !== '=') return true
|
|
383
|
-
for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
|
|
384
|
-
return false
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (op === '[]' && node[1] === name) return true
|
|
388
|
-
if (op === '=>') return false
|
|
389
|
-
for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
|
|
390
|
-
return false
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
394
|
-
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
395
|
-
let changed = false
|
|
396
|
-
const stmts = seq.slice(1).map(stmt => {
|
|
397
|
-
const r = scalarizeTypedArrayLiterals(stmt)
|
|
398
|
-
changed ||= r.changed
|
|
399
|
-
return r.node
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
const candidates = new Map()
|
|
403
|
-
const mirrored = new Map()
|
|
404
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
405
|
-
const stmt = stmts[i]
|
|
406
|
-
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
407
|
-
const decl = stmt[1]
|
|
408
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
409
|
-
const fixed = fixedScalarTypedArray(decl[2])
|
|
410
|
-
if (fixed == null) continue
|
|
411
|
-
const { len, coerce } = fixed
|
|
412
|
-
let hasSafeUse = false, hasUnsafeUse = false
|
|
413
|
-
for (let j = 0; j < stmts.length; j++) {
|
|
414
|
-
if (j === i) continue
|
|
415
|
-
if (!mentionsName(stmts[j], decl[1])) continue
|
|
416
|
-
const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
|
|
417
|
-
hasSafeUse ||= safe
|
|
418
|
-
hasUnsafeUse ||= !safe
|
|
419
|
-
}
|
|
420
|
-
if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
|
|
421
|
-
if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
|
|
422
|
-
else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
|
|
423
|
-
}
|
|
424
|
-
if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
425
|
-
|
|
426
|
-
const arrays = new Map()
|
|
427
|
-
for (const [name, c] of [...candidates, ...mirrored]) {
|
|
428
|
-
const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
|
|
429
|
-
arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const out = []
|
|
433
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
434
|
-
const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
|
|
435
|
-
[...mirrored.entries()].find(([, c]) => c.index === i)
|
|
436
|
-
if (entry) {
|
|
437
|
-
const [name] = entry
|
|
438
|
-
const arr = arrays.get(name)
|
|
439
|
-
const { slots } = arr
|
|
440
|
-
if (arr.mirrored) {
|
|
441
|
-
out.push(stmts[i])
|
|
442
|
-
if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
|
|
443
|
-
} else if (slots.length) {
|
|
444
|
-
out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
|
|
445
|
-
}
|
|
446
|
-
changed = true
|
|
447
|
-
continue
|
|
448
|
-
}
|
|
449
|
-
const unsafe = []
|
|
450
|
-
for (const [name, arr] of arrays) {
|
|
451
|
-
if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
|
|
452
|
-
}
|
|
453
|
-
if (unsafe.length) {
|
|
454
|
-
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
|
|
455
|
-
out.push(stmts[i])
|
|
456
|
-
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
|
|
457
|
-
changed = true
|
|
458
|
-
} else {
|
|
459
|
-
out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return { node: [';', ...out], changed: true }
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function scalarizeTypedArrayLiterals(node) {
|
|
466
|
-
if (!Array.isArray(node)) return { node, changed: false }
|
|
467
|
-
if (node[0] === '=>') return { node, changed: false }
|
|
468
|
-
if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
|
|
469
|
-
let changed = false
|
|
470
|
-
const out = [node[0]]
|
|
471
|
-
for (let i = 1; i < node.length; i++) {
|
|
472
|
-
const r = scalarizeTypedArrayLiterals(node[i])
|
|
473
|
-
changed ||= r.changed
|
|
474
|
-
out.push(r.node)
|
|
475
|
-
}
|
|
476
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const stmtList = (body) => {
|
|
480
|
-
if (!Array.isArray(body)) return body == null ? [] : [body]
|
|
481
|
-
if (body[0] === '{}') return stmtList(body[1])
|
|
482
|
-
if (body[0] === ';') return body.slice(1)
|
|
483
|
-
return [body]
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const hasControlTransfer = node => scanBody(node, n => CONTROL_TRANSFER.has(n[0]))
|
|
487
|
-
|
|
488
|
-
const containsDeclOf = (body, name) => scanBody(body, n => {
|
|
489
|
-
if (n[0] !== 'let' && n[0] !== 'const') return false
|
|
490
|
-
for (let i = 1; i < n.length; i++) {
|
|
491
|
-
const d = n[i]
|
|
492
|
-
if (d === name) return true
|
|
493
|
-
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
494
|
-
}
|
|
495
|
-
return false
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
const isReassigned = (body, name) => scanBody(body, n =>
|
|
499
|
-
(ASSIGN_OPS.has(n[0]) && n[1] === name) || ((n[0] === '++' || n[0] === '--') && n[1] === name))
|
|
500
|
-
|
|
501
|
-
const containsTypedArrayAccess = (body, names) => scanBody(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
|
|
502
|
-
|
|
503
|
-
function smallScalarTypedForTrip(init, cond, step) {
|
|
504
|
-
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
505
|
-
const decl = init[1]
|
|
506
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
507
|
-
const name = decl[1]
|
|
508
|
-
if (constIntExpr(decl[2]) !== 0) return null
|
|
509
|
-
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
510
|
-
const end = constIntExpr(cond[2])
|
|
511
|
-
if (end == null || end < 0 || end > maxScalarTypedLoopUnroll()) return null
|
|
512
|
-
const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
|
|
513
|
-
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
|
|
514
|
-
return stepOk ? { name, end } : null
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const scalarTypedLoopBudget = (body) => {
|
|
518
|
-
if (!Array.isArray(body) || body[0] === '=>') return 1
|
|
519
|
-
if (body[0] === 'for') {
|
|
520
|
-
const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
|
|
521
|
-
return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
|
|
522
|
-
}
|
|
523
|
-
let max = 1
|
|
524
|
-
for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
|
|
525
|
-
return max
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const unrollTypedArrayLoops = (node, names) => {
|
|
529
|
-
if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
|
|
530
|
-
if (node[0] === ';') {
|
|
531
|
-
let changed = false
|
|
532
|
-
const out = [';']
|
|
533
|
-
for (const stmt of node.slice(1)) {
|
|
534
|
-
const r = unrollTypedArrayLoops(stmt, names)
|
|
535
|
-
changed ||= r.changed
|
|
536
|
-
if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
|
|
537
|
-
else out.push(r.node)
|
|
538
|
-
}
|
|
539
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
540
|
-
}
|
|
541
|
-
if (node[0] === '{}') {
|
|
542
|
-
const r = unrollTypedArrayLoops(node[1], names)
|
|
543
|
-
return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
|
|
544
|
-
}
|
|
545
|
-
if (node[0] === 'for') {
|
|
546
|
-
const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
|
|
547
|
-
if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
|
|
548
|
-
!hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
|
|
549
|
-
const out = [';']
|
|
550
|
-
for (let i = 0; i < trip.end; i++) {
|
|
551
|
-
const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
|
|
552
|
-
const r = unrollTypedArrayLoops(cloned, names)
|
|
553
|
-
out.push(...stmtList(r.node))
|
|
554
|
-
}
|
|
555
|
-
return { node: out, changed: true }
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
let changed = false
|
|
559
|
-
const out = [node[0]]
|
|
560
|
-
for (let i = 1; i < node.length; i++) {
|
|
561
|
-
const r = unrollTypedArrayLoops(node[i], names)
|
|
562
|
-
changed ||= r.changed
|
|
563
|
-
out.push(r.node)
|
|
564
|
-
}
|
|
565
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const fixedTypedArraysInBody = (body) => {
|
|
569
|
-
const out = new Map()
|
|
570
|
-
const walk = node => {
|
|
571
|
-
if (!Array.isArray(node) || node[0] === '=>') return
|
|
572
|
-
if (node[0] === 'let' || node[0] === 'const') {
|
|
573
|
-
for (let i = 1; i < node.length; i++) {
|
|
574
|
-
const d = node[i]
|
|
575
|
-
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
576
|
-
const fixed = fixedScalarTypedArray(d[2])
|
|
577
|
-
if (fixed != null) out.set(d[1], fixed)
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
581
|
-
}
|
|
582
|
-
walk(body)
|
|
583
|
-
return out
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
|
|
587
|
-
if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
|
|
588
|
-
if (scanBody(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
|
|
589
|
-
const params = func.sig?.params || []
|
|
590
|
-
const cands = new Map()
|
|
591
|
-
for (let i = 0; i < params.length; i++) {
|
|
592
|
-
const pname = params[i].name
|
|
593
|
-
let len = null, coerce = null, ok = true
|
|
594
|
-
for (const site of sites) {
|
|
595
|
-
const arg = site.argList[i]
|
|
596
|
-
const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
|
|
597
|
-
if (!fixed) { ok = false; break }
|
|
598
|
-
if (len == null) { len = fixed.len; coerce = fixed.coerce }
|
|
599
|
-
else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
|
|
600
|
-
}
|
|
601
|
-
if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
|
|
602
|
-
}
|
|
603
|
-
if (!cands.size) return cands
|
|
604
|
-
for (const site of sites) {
|
|
605
|
-
const seen = new Set()
|
|
606
|
-
for (let i = 0; i < params.length; i++) {
|
|
607
|
-
if (!cands.has(params[i].name)) continue
|
|
608
|
-
const arg = site.argList[i]
|
|
609
|
-
if (typeof arg !== 'string' || seen.has(arg)) return new Map()
|
|
610
|
-
seen.add(arg)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return cands
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const scalarizeTypedArrayParams = (func, paramCands) => {
|
|
617
|
-
for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
|
|
618
|
-
for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
|
|
619
|
-
if (!paramCands.size) return { body: func.body, changed: false }
|
|
620
|
-
const arrays = new Map()
|
|
621
|
-
for (const [name, c] of paramCands) {
|
|
622
|
-
arrays.set(name, {
|
|
623
|
-
len: c.len,
|
|
624
|
-
coerce: c.coerce,
|
|
625
|
-
slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
|
|
626
|
-
})
|
|
627
|
-
}
|
|
628
|
-
const prologue = []
|
|
629
|
-
const writeback = []
|
|
630
|
-
for (const [name, { len, slots }] of arrays) {
|
|
631
|
-
if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
|
|
632
|
-
for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
|
|
633
|
-
}
|
|
634
|
-
const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
|
|
635
|
-
return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const scalarizeFunctionTypedArrays = (programFacts) => {
|
|
639
|
-
const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
|
|
640
|
-
const sitesByCallee = new Map()
|
|
641
|
-
for (const site of programFacts.callSites) {
|
|
642
|
-
if (!site.callerFunc) continue
|
|
643
|
-
const list = sitesByCallee.get(site.callee)
|
|
644
|
-
if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
|
|
645
|
-
}
|
|
646
|
-
let changed = false
|
|
647
|
-
for (const func of ctx.func.list) {
|
|
648
|
-
if (!func.body || func.raw) continue
|
|
649
|
-
const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
|
|
650
|
-
const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
|
|
651
|
-
if (names.size) {
|
|
652
|
-
let guard = 0
|
|
653
|
-
while (guard++ < 6) {
|
|
654
|
-
const r = unrollTypedArrayLoops(func.body, names)
|
|
655
|
-
if (!r.changed) break
|
|
656
|
-
func.body = r.node
|
|
657
|
-
changed = true
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const p = scalarizeTypedArrayParams(func, paramCands)
|
|
661
|
-
if (p.changed) { func.body = p.body; changed = true }
|
|
662
|
-
const l = scalarizeTypedArrayLiterals(func.body)
|
|
663
|
-
if (l.changed) { func.body = l.node; changed = true }
|
|
664
|
-
if (changed) invalidateLocalsCache(func.body)
|
|
665
|
-
}
|
|
666
|
-
return changed
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const scalarizeArrayLiteralSeq = (seq) => {
|
|
670
|
-
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
671
|
-
let changed = false
|
|
672
|
-
const stmts = seq.slice(1).map(stmt => {
|
|
673
|
-
const r = scalarizeArrayLiterals(stmt)
|
|
674
|
-
changed ||= r.changed
|
|
675
|
-
return r.node
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
const candidates = new Map()
|
|
679
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
680
|
-
const stmt = stmts[i]
|
|
681
|
-
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
682
|
-
const decl = stmt[1]
|
|
683
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
684
|
-
const elems = scalarArrayElems(decl[2])
|
|
685
|
-
if (!elems) continue
|
|
686
|
-
let ok = true
|
|
687
|
-
for (let j = 0; j < stmts.length && ok; j++) {
|
|
688
|
-
if (j === i) continue
|
|
689
|
-
ok = safeScalarArrayUse(stmts[j], decl[1], elems.length)
|
|
690
|
-
}
|
|
691
|
-
if (!ok) continue
|
|
692
|
-
candidates.set(decl[1], { index: i, op: stmt[0], elems })
|
|
693
|
-
}
|
|
694
|
-
if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
695
|
-
|
|
696
|
-
const arrays = new Map()
|
|
697
|
-
for (const [name, c] of candidates) {
|
|
698
|
-
const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
|
|
699
|
-
arrays.set(name, temps)
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const out = []
|
|
703
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
704
|
-
const entry = [...candidates.entries()].find(([, c]) => c.index === i)
|
|
705
|
-
if (entry) {
|
|
706
|
-
const [name, c] = entry
|
|
707
|
-
const temps = arrays.get(name)
|
|
708
|
-
if (temps.length) {
|
|
709
|
-
out.push([c.op, ...temps.map((tmp, k) =>
|
|
710
|
-
['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
|
|
711
|
-
}
|
|
712
|
-
changed = true
|
|
713
|
-
continue
|
|
714
|
-
}
|
|
715
|
-
out.push(rewriteScalarArrayUses(stmts[i], arrays))
|
|
716
|
-
}
|
|
717
|
-
return { node: [';', ...out], changed: true }
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const scalarizeObjectLiteralSeq = (seq, escapes) => {
|
|
721
|
-
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
722
|
-
let changed = false
|
|
723
|
-
const stmts = seq.slice(1).map(stmt => {
|
|
724
|
-
const r = scalarizeObjectLiterals(stmt, escapes)
|
|
725
|
-
changed ||= r.changed
|
|
726
|
-
return r.node
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
const candidates = new Map()
|
|
730
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
731
|
-
const stmt = stmts[i]
|
|
732
|
-
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
733
|
-
const decl = stmt[1]
|
|
734
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
735
|
-
if (escapes.get(decl[1]) !== false) continue
|
|
736
|
-
const props = scalarObjectProps(decl[2])
|
|
737
|
-
if (!props) continue
|
|
738
|
-
const keys = new Set(props.names)
|
|
739
|
-
let ok = true
|
|
740
|
-
for (let j = 0; j < stmts.length && ok; j++) {
|
|
741
|
-
if (j === i) continue
|
|
742
|
-
ok = safeScalarObjectUse(stmts[j], decl[1], keys)
|
|
743
|
-
}
|
|
744
|
-
if (!ok) continue
|
|
745
|
-
candidates.set(decl[1], { index: i, op: stmt[0], props })
|
|
746
|
-
}
|
|
747
|
-
if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
748
|
-
|
|
749
|
-
const objects = new Map()
|
|
750
|
-
for (const [name, c] of candidates) {
|
|
751
|
-
const fields = new Map()
|
|
752
|
-
for (let i = 0; i < c.props.names.length; i++) {
|
|
753
|
-
fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
|
|
754
|
-
}
|
|
755
|
-
objects.set(name, fields)
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const out = []
|
|
759
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
760
|
-
const entry = [...candidates.entries()].find(([, c]) => c.index === i)
|
|
761
|
-
if (entry) {
|
|
762
|
-
const [, c] = entry
|
|
763
|
-
const fields = objects.get(entry[0])
|
|
764
|
-
if (c.props.names.length) {
|
|
765
|
-
out.push([c.op, ...c.props.names.map((prop, k) =>
|
|
766
|
-
['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
|
|
767
|
-
}
|
|
768
|
-
changed = true
|
|
769
|
-
continue
|
|
770
|
-
}
|
|
771
|
-
out.push(rewriteScalarObjectUses(stmts[i], objects))
|
|
772
|
-
}
|
|
773
|
-
return { node: [';', ...out], changed: true }
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function scalarizeObjectLiterals(node, escapes) {
|
|
777
|
-
if (!Array.isArray(node)) return { node, changed: false }
|
|
778
|
-
if (node[0] === '=>') return { node, changed: false }
|
|
779
|
-
if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
|
|
780
|
-
let changed = false
|
|
781
|
-
const out = [node[0]]
|
|
782
|
-
for (let i = 1; i < node.length; i++) {
|
|
783
|
-
const r = scalarizeObjectLiterals(node[i], escapes)
|
|
784
|
-
changed ||= r.changed
|
|
785
|
-
out.push(r.node)
|
|
786
|
-
}
|
|
787
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function scalarizeArrayLiterals(node) {
|
|
791
|
-
if (!Array.isArray(node)) return { node, changed: false }
|
|
792
|
-
if (node[0] === '=>') return { node, changed: false }
|
|
793
|
-
if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
|
|
794
|
-
let changed = false
|
|
795
|
-
const out = [node[0]]
|
|
796
|
-
for (let i = 1; i < node.length; i++) {
|
|
797
|
-
const r = scalarizeArrayLiterals(node[i])
|
|
798
|
-
changed ||= r.changed
|
|
799
|
-
out.push(r.node)
|
|
800
|
-
}
|
|
801
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const scalarizeFunctionArrayLiterals = () => {
|
|
805
|
-
let changed = false
|
|
806
|
-
for (const func of ctx.func.list) {
|
|
807
|
-
if (!func.body || func.raw) continue
|
|
808
|
-
let guard = 0
|
|
809
|
-
while (guard++ < 4) {
|
|
810
|
-
const r = scalarizeArrayLiterals(func.body)
|
|
811
|
-
if (!r.changed) break
|
|
812
|
-
func.body = r.node
|
|
813
|
-
changed = true
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
return changed
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// === Int-array → Int32Array auto-promotion =================================
|
|
820
|
-
//
|
|
821
|
-
// A `let xs = [intLit, intLit, …]` binding whose every use is TYPED-compatible
|
|
822
|
-
// is rewritten to `let xs = new Int32Array([intLit, …])`. Downstream analysis
|
|
823
|
-
// then takes over: valTypeOf → VAL.TYPED, methods dispatch through `.typed:`,
|
|
824
|
-
// loops get auto-vectorized via i32x4 (SIMD pass), and the carrier shrinks
|
|
825
|
-
// from 8-byte f64 slots to packed i32. The promotion runs after literal
|
|
826
|
-
// scalarization (so arrays fully scalarized away are already gone) and before
|
|
827
|
-
// typed-array param scalarization (so a freshly-promoted array can still
|
|
828
|
-
// participate in subsequent loop unrolling).
|
|
829
|
-
//
|
|
830
|
-
// Safety: the binding must never appear in a pattern that TYPED can't honor.
|
|
831
|
-
// Disqualifiers (each fires per binding name):
|
|
832
|
-
// 1. reassignment `xs = …` / compound `xs +=` / `++xs` / `--xs` — TYPED has
|
|
833
|
-
// no value-replacement op; `xs = new TypedArray(…)` would also drop the
|
|
834
|
-
// promoted view's identity.
|
|
835
|
-
// 2. element write `xs[k] = v` — TYPED's i32-trunc store would lose
|
|
836
|
-
// fractional/NaN bits that VAL.ARRAY would have preserved.
|
|
837
|
-
// 3. method calls outside the read-safe whitelist (push, pop, shift, …) —
|
|
838
|
-
// TYPED arrays are fixed-length; mutators don't exist on the carrier.
|
|
839
|
-
// 4. `Array.isArray(xs)` — semantics flip true→false on promotion.
|
|
840
|
-
// 5. `…xs` spread / `xs` as call arg / `xs` as return / bare reference in
|
|
841
|
-
// any other position — escape; callee may rely on ARRAY layout.
|
|
842
|
-
// 6. captured by a closure / shadowed by an inner decl — same escape
|
|
843
|
-
// reasoning, plus the inner decl could rebind to a non-array.
|
|
844
|
-
//
|
|
845
|
-
// Elements at init must all be i32-range integer literals. A negative literal
|
|
846
|
-
// arrives as `[null, -n]` after prepare's constant folding; intLiteralValue
|
|
847
|
-
// recognizes that form. Float literals (`[1, 2.5]`) and out-of-range ints
|
|
848
|
-
// (`0x80000000` on the +ve side, etc.) disqualify the array as a whole.
|
|
849
|
-
|
|
850
|
-
// Methods we promote across. The bar: every entry must have a real `.typed:*`
|
|
851
|
-
// emitter in module/typedarray.js. Receiver-flow into chained methods is also
|
|
852
|
-
// typed-aware now — `.typed:map`/`.typed:filter`/`.typed:slice` all return
|
|
853
|
-
// TYPED carriers, and downstream `.filter`/`.slice`/`.map`/etc. on those
|
|
854
|
-
// re-dispatch via emit.js:2211's `.typed:<m>` lookup (VAL.TYPED ⇒ typed
|
|
855
|
-
// emitter). Methods missing here (.join, .sort, .reverse, .subarray, .fill,
|
|
856
|
-
// .toString, .copyWithin, …) lack a typed emitter — disqualify the candidate.
|
|
857
|
-
const _TYPED_SAFE_METHODS = new Set([
|
|
858
|
-
'set',
|
|
859
|
-
'map', 'filter', 'slice',
|
|
860
|
-
'forEach', 'reduce',
|
|
861
|
-
'indexOf', 'includes', 'find', 'findIndex', 'some', 'every',
|
|
862
|
-
])
|
|
863
|
-
|
|
864
|
-
// `.length` is TYPED-aware via core.js:__len (shifts the byte header by
|
|
865
|
-
// __typed_shift on TAG=3). `.byteLength`/`.byteOffset`/`.buffer` are
|
|
866
|
-
// TYPED-only — a user reading those already expects a typed array, so a
|
|
867
|
-
// promotion candidate that hits them is a coincidence we'd rather not
|
|
868
|
-
// rely on; disqualify and let them write the TypedArray construction
|
|
869
|
-
// themselves.
|
|
870
|
-
const _TYPED_SAFE_PROPS = new Set(['length'])
|
|
871
|
-
|
|
872
|
-
// Returns the i32-range integer payload of an array-literal element, or null
|
|
873
|
-
// if the element isn't a literal integer that fits in i32. Mirrors the shape
|
|
874
|
-
// check used by `intLiteralValue` but without the rep-lookup (we're pre-
|
|
875
|
-
// analysis here, and want a pure syntactic gate).
|
|
876
|
-
const _intArrayLitElems = (expr) => {
|
|
877
|
-
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
878
|
-
if (expr.length < 2) return null // empty literal — low value, skip
|
|
879
|
-
const out = []
|
|
880
|
-
for (let i = 1; i < expr.length; i++) {
|
|
881
|
-
const v = intLiteralValue(expr[i])
|
|
882
|
-
if (v == null) return null
|
|
883
|
-
out.push(v)
|
|
884
|
-
}
|
|
885
|
-
return out
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// True iff `name` appears anywhere within `node` as a bare identifier or
|
|
889
|
-
// inside any expression position. Used to detect escape across a closure
|
|
890
|
-
// boundary (where we can't trace the use sites locally).
|
|
891
|
-
const _refsName = (node, name) => {
|
|
892
|
-
if (typeof node === 'string') return node === name
|
|
893
|
-
if (!Array.isArray(node)) return false
|
|
894
|
-
for (let i = 1; i < node.length; i++) if (_refsName(node[i], name)) return true
|
|
895
|
-
return false
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// Walks `node` and disqualifies every candidate name that appears in an
|
|
899
|
-
// unsafe context. `initSet` holds the candidate's own init-decl AST nodes
|
|
900
|
-
// (their LHS reference is the binding being defined, not an escape).
|
|
901
|
-
const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
902
|
-
if (initSet.has(node)) {
|
|
903
|
-
// The init decl itself: only walk the RHS (skip the LHS `name`).
|
|
904
|
-
return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
905
|
-
}
|
|
906
|
-
if (typeof node === 'string') {
|
|
907
|
-
// Bare identifier outside any handled parent context — escape.
|
|
908
|
-
if (candidates.has(node)) disqualified.add(node)
|
|
909
|
-
return
|
|
910
|
-
}
|
|
911
|
-
if (!Array.isArray(node)) return
|
|
912
|
-
const op = node[0]
|
|
913
|
-
|
|
914
|
-
// Closure body — any candidate referenced inside is captured. Bail without
|
|
915
|
-
// recursing further (we'd otherwise hit the bare-name leaf and disqualify
|
|
916
|
-
// anyway, but this is explicit and avoids walking the inner closure).
|
|
917
|
-
if (op === '=>') {
|
|
918
|
-
for (const n of candidates.keys()) {
|
|
919
|
-
if (!disqualified.has(n) && _refsName(node, n)) disqualified.add(n)
|
|
920
|
-
}
|
|
921
|
-
return
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Property access `name.prop` / `name?.prop`. Bare property reads only —
|
|
925
|
-
// method calls reach here via the `()` handler below (which intercepts
|
|
926
|
-
// before recursing into its callee).
|
|
927
|
-
if ((op === '.' || op === '?.') && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
928
|
-
if (!_TYPED_SAFE_PROPS.has(node[2])) disqualified.add(node[1])
|
|
929
|
-
return // node[2] is the property name (string), not an expression — done
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Method or function call. Two shapes carry the candidate:
|
|
933
|
-
// `name.method(args)` — receiver at node[1][1]; method whitelist gates.
|
|
934
|
-
// `f(…, name, …)` / `Array.isArray(name)` — name appears as a plain arg.
|
|
935
|
-
if (op === '()') {
|
|
936
|
-
const callee = node[1]
|
|
937
|
-
// Method call on a candidate receiver.
|
|
938
|
-
if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
|
|
939
|
-
typeof callee[1] === 'string' && candidates.has(callee[1])) {
|
|
940
|
-
if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
|
|
941
|
-
// Walk method args (skip the receiver — already validated above).
|
|
942
|
-
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
943
|
-
return
|
|
944
|
-
}
|
|
945
|
-
// Array.isArray flips true→false under promotion.
|
|
946
|
-
if (callee === 'Array.isArray') {
|
|
947
|
-
const raw = node[2]
|
|
948
|
-
const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
|
|
949
|
-
for (const a of list) {
|
|
950
|
-
if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
|
|
951
|
-
else _disqualifyPromotion(a, candidates, disqualified, initSet)
|
|
952
|
-
}
|
|
953
|
-
return
|
|
954
|
-
}
|
|
955
|
-
// Fall through to generic recursion — `name` as a plain arg will hit the
|
|
956
|
-
// bare-name leaf above and disqualify.
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// Index read `name[k]` — read access is TYPED-safe. Walk the key in case
|
|
960
|
-
// it contains references to other candidate names.
|
|
961
|
-
if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
962
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
963
|
-
return
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Element write: `['=', ['[]', name, k], v]` (and compound forms).
|
|
967
|
-
// V1 stays read-only after init — element writes would silently truncate.
|
|
968
|
-
if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
|
|
969
|
-
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
970
|
-
disqualified.add(node[1][1])
|
|
971
|
-
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
|
|
972
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
973
|
-
return
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// Whole-binding reassign: `name = …` / `name += …` / etc.
|
|
977
|
-
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
978
|
-
disqualified.add(node[1])
|
|
979
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
980
|
-
return
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Pre/post-increment on var or element.
|
|
984
|
-
if (op === '++' || op === '--') {
|
|
985
|
-
const t = node[1]
|
|
986
|
-
if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
|
|
987
|
-
if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
|
|
988
|
-
disqualified.add(t[1])
|
|
989
|
-
_disqualifyPromotion(t[2], candidates, disqualified, initSet)
|
|
990
|
-
return
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// `let`/`const` declaration — the candidate's own init lands here via the
|
|
995
|
-
// initSet branch at the top. Any *other* decl that names a candidate is a
|
|
996
|
-
// shadow (impossible after jz hoists, but defensive) and disqualifies.
|
|
997
|
-
if (op === 'let' || op === 'const') {
|
|
998
|
-
for (let i = 1; i < node.length; i++) {
|
|
999
|
-
const d = node[i]
|
|
1000
|
-
if (typeof d === 'string' && candidates.has(d)) disqualified.add(d)
|
|
1001
|
-
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
|
|
1002
|
-
candidates.has(d[1]) && !initSet.has(d)) {
|
|
1003
|
-
disqualified.add(d[1])
|
|
1004
|
-
_disqualifyPromotion(d[2], candidates, disqualified, initSet)
|
|
1005
|
-
} else {
|
|
1006
|
-
_disqualifyPromotion(d, candidates, disqualified, initSet)
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
return
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// Spread `…name` — could be in a call, a `[`, or a destructure target.
|
|
1013
|
-
if (op === '...' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
1014
|
-
disqualified.add(node[1])
|
|
1015
|
-
return
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// for-of / for-in iteration: receiver position is `node[2]` (a bare name
|
|
1019
|
-
// there would otherwise trigger escape). TYPED supports iteration, so
|
|
1020
|
-
// allow the receiver but walk the body for other refs.
|
|
1021
|
-
if (op === 'for-of' || op === 'for-in') {
|
|
1022
|
-
// Walk decl (node[1]), iter (node[2]), body (node[3]); receiver as bare
|
|
1023
|
-
// name is fine — only the body matters for further refs to the same name
|
|
1024
|
-
// (but body refs would shadow or escape, which other rules catch).
|
|
1025
|
-
for (let i = 1; i < node.length; i++) {
|
|
1026
|
-
const child = node[i]
|
|
1027
|
-
if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
|
|
1028
|
-
_disqualifyPromotion(child, candidates, disqualified, initSet)
|
|
1029
|
-
}
|
|
1030
|
-
return
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Generic — recurse into children. Bare-name refs at unhandled positions
|
|
1034
|
-
// hit the string-leaf branch above and disqualify on contact.
|
|
1035
|
-
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
|
|
1039
|
-
// carries the exact init-decl AST node so the disqualifier can skip the
|
|
1040
|
-
// binding's own LHS reference (which would otherwise look like a reassign).
|
|
1041
|
-
const _collectIntArrayCandidates = (node, candidates) => {
|
|
1042
|
-
if (!Array.isArray(node)) return
|
|
1043
|
-
const op = node[0]
|
|
1044
|
-
if (op === '=>') return
|
|
1045
|
-
if (op === 'let' || op === 'const') {
|
|
1046
|
-
for (let i = 1; i < node.length; i++) {
|
|
1047
|
-
const d = node[i]
|
|
1048
|
-
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
1049
|
-
const elems = _intArrayLitElems(d[2])
|
|
1050
|
-
if (elems == null) continue
|
|
1051
|
-
// jz hoists `let` to function scope — duplicate candidate-name collisions
|
|
1052
|
-
// shouldn't happen, but if they do the second wins (disqualifyPromotion
|
|
1053
|
-
// will mark both via the shadow rule).
|
|
1054
|
-
candidates.set(d[1], { initDecl: d, elems })
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
for (let i = 1; i < node.length; i++) _collectIntArrayCandidates(node[i], candidates)
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Rewrite `let name = [...]` → `let name = new Int32Array([...])` for every
|
|
1061
|
-
// validated candidate. Preserves the original element AST nodes so the
|
|
1062
|
-
// downstream `Int32Array.from` lowering picks up its existing static-data
|
|
1063
|
-
// segment / per-element-store fast paths.
|
|
1064
|
-
const _rewritePromoted = (node, validated, initSet) => {
|
|
1065
|
-
if (!Array.isArray(node)) return { node, changed: false }
|
|
1066
|
-
const op = node[0]
|
|
1067
|
-
if (op === '=>') {
|
|
1068
|
-
// Closures don't reach validated bindings (we disqualified on capture).
|
|
1069
|
-
// Still recurse — a nested closure may itself hold a promotable.
|
|
1070
|
-
let changed = false
|
|
1071
|
-
const out = [op]
|
|
1072
|
-
for (let i = 1; i < node.length; i++) {
|
|
1073
|
-
const r = _rewritePromoted(node[i], validated, initSet)
|
|
1074
|
-
if (r.changed) changed = true
|
|
1075
|
-
out.push(r.node)
|
|
1076
|
-
}
|
|
1077
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
1078
|
-
}
|
|
1079
|
-
let changed = false
|
|
1080
|
-
const out = [op]
|
|
1081
|
-
for (let i = 1; i < node.length; i++) {
|
|
1082
|
-
const child = node[i]
|
|
1083
|
-
if ((op === 'let' || op === 'const') && Array.isArray(child) && child[0] === '=' &&
|
|
1084
|
-
typeof child[1] === 'string' && validated.has(child[1]) && initSet.has(child)) {
|
|
1085
|
-
const newRhs = ['()', 'new.Int32Array', child[2]]
|
|
1086
|
-
out.push(['=', child[1], newRhs])
|
|
1087
|
-
changed = true
|
|
1088
|
-
continue
|
|
1089
|
-
}
|
|
1090
|
-
const r = _rewritePromoted(child, validated, initSet)
|
|
1091
|
-
if (r.changed) changed = true
|
|
1092
|
-
out.push(r.node)
|
|
1093
|
-
}
|
|
1094
|
-
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const promoteIntArrayLiteralsInBody = (body) => {
|
|
1098
|
-
const candidates = new Map()
|
|
1099
|
-
_collectIntArrayCandidates(body, candidates)
|
|
1100
|
-
if (!candidates.size) return { node: body, changed: false }
|
|
1101
|
-
const initSet = new Set()
|
|
1102
|
-
for (const { initDecl } of candidates.values()) initSet.add(initDecl)
|
|
1103
|
-
const disqualified = new Set()
|
|
1104
|
-
_disqualifyPromotion(body, candidates, disqualified, initSet)
|
|
1105
|
-
const validated = new Set()
|
|
1106
|
-
for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
|
|
1107
|
-
if (!validated.size) return { node: body, changed: false }
|
|
1108
|
-
return _rewritePromoted(body, validated, initSet)
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// On the first successful promotion, pull in the typedarray module so the
|
|
1112
|
-
// emitted `new.Int32Array` callee has a registered emitter. Calling
|
|
1113
|
-
// `includeModule('typedarray')` on a no-op (already loaded) is cheap.
|
|
1114
|
-
const promoteIntArrayLiterals = () => {
|
|
1115
|
-
let changed = false
|
|
1116
|
-
for (const func of ctx.func.list) {
|
|
1117
|
-
if (!func.body || func.raw) continue
|
|
1118
|
-
const r = promoteIntArrayLiteralsInBody(func.body)
|
|
1119
|
-
if (r.changed) {
|
|
1120
|
-
func.body = r.node
|
|
1121
|
-
invalidateLocalsCache(func.body)
|
|
1122
|
-
if (!changed) includeModule('typedarray')
|
|
1123
|
-
changed = true
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
return changed
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
const scalarizeFunctionObjectLiterals = () => {
|
|
1130
|
-
let changed = false
|
|
1131
|
-
for (const func of ctx.func.list) {
|
|
1132
|
-
if (!func.body || func.raw) continue
|
|
1133
|
-
let guard = 0
|
|
1134
|
-
while (guard++ < 4) {
|
|
1135
|
-
const escapes = new Map(analyzeBody(func.body).escapes)
|
|
1136
|
-
invalidateLocalsCache(func.body)
|
|
1137
|
-
const r = scalarizeObjectLiterals(func.body, escapes)
|
|
1138
|
-
if (!r.changed) break
|
|
1139
|
-
func.body = r.node
|
|
1140
|
-
changed = true
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
return changed
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
const cloneWithSubst = (node, subst, rename) => {
|
|
1147
|
-
if (typeof node === 'string') {
|
|
1148
|
-
if (subst.has(node)) return clonePlain(subst.get(node))
|
|
1149
|
-
return rename.get(node) || node
|
|
1150
|
-
}
|
|
1151
|
-
if (!Array.isArray(node)) return node
|
|
1152
|
-
const op = node[0]
|
|
1153
|
-
if (op === 'str') return node.slice()
|
|
1154
|
-
if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, rename), node[2]]
|
|
1155
|
-
if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, rename)]
|
|
1156
|
-
return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, rename))
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Returns { prefix, value } where prefix is the substituted body statements
|
|
1160
|
-
// (excluding any trailing `return X`), and value is the substituted return
|
|
1161
|
-
// expression — null if void or no trailing return value.
|
|
1162
|
-
const inlinedBody = (func, args) => {
|
|
1163
|
-
const params = func.sig.params
|
|
1164
|
-
if (args.length !== params.length || !args.every(isSimpleArg)) return null
|
|
1165
|
-
const paramNames = new Set(params.map(p => p.name))
|
|
1166
|
-
if (mutatesAny(func.body, paramNames)) return null
|
|
1167
|
-
|
|
1168
|
-
const subst = new Map()
|
|
1169
|
-
for (let i = 0; i < params.length; i++) subst.set(params[i].name, args[i])
|
|
1170
|
-
|
|
1171
|
-
const locals = new Set()
|
|
1172
|
-
collectBindings(func.body, locals)
|
|
1173
|
-
for (const p of params) locals.delete(p.name)
|
|
1174
|
-
|
|
1175
|
-
const rename = new Map()
|
|
1176
|
-
for (const name of locals) rename.set(name, `${T}inl${ctx.func.uniq++}_${name}`)
|
|
1177
|
-
|
|
1178
|
-
const stmts = blockStmts(func.body)
|
|
1179
|
-
// Expression-bodied arrow `(c) => expr`: no statement block; the whole body
|
|
1180
|
-
// *is* the return value. Treat as zero-prefix + value.
|
|
1181
|
-
if (!stmts) return { prefix: [], value: cloneWithSubst(func.body, subst, rename) }
|
|
1182
|
-
const last = stmts.length ? stmts[stmts.length - 1] : null
|
|
1183
|
-
const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
|
|
1184
|
-
const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
|
|
1185
|
-
const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
|
|
1186
|
-
const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
|
|
1187
|
-
return { prefix, value }
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
const isCandidateCall = (node, candidates) =>
|
|
1191
|
-
Array.isArray(node) && node[0] === '()' && typeof node[1] === 'string' && candidates.has(node[1])
|
|
1192
|
-
|
|
1193
|
-
// Recursively substitute calls to expr-bodied candidates anywhere in `node`.
|
|
1194
|
-
// Used for tiny pure-expression helpers (`isAlpha(c) => …`) that get called
|
|
1195
|
-
// from expression contexts (if-conditions, ternary tests). For these the
|
|
1196
|
-
// inlined body is value-only (zero prefix), so a pure substitution is safe.
|
|
1197
|
-
const inlineInExpr = (node, candidates) => {
|
|
1198
|
-
if (!Array.isArray(node)) return { node, changed: false }
|
|
1199
|
-
if (node[0] === '=>') return { node, changed: false }
|
|
1200
|
-
let changed = false
|
|
1201
|
-
const next = [node[0]]
|
|
1202
|
-
for (let i = 1; i < node.length; i++) {
|
|
1203
|
-
const r = inlineInExpr(node[i], candidates)
|
|
1204
|
-
if (r.changed) changed = true
|
|
1205
|
-
next.push(r.node)
|
|
1206
|
-
}
|
|
1207
|
-
if (isCandidateCall(next, candidates)) {
|
|
1208
|
-
const args = callArgs(next)
|
|
1209
|
-
const shape = args && inlinedBody(candidates.get(next[1]), args)
|
|
1210
|
-
if (shape && shape.value !== null && shape.prefix.length === 0) {
|
|
1211
|
-
return { node: shape.value, changed: true }
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return { node: changed ? next : node, changed }
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const inlineInStmt = (stmt, candidates) => {
|
|
1218
|
-
if (!Array.isArray(stmt)) return { node: stmt, changed: false }
|
|
1219
|
-
// Statement-position call: discard return value, splice prefix in place.
|
|
1220
|
-
if (isCandidateCall(stmt, candidates)) {
|
|
1221
|
-
const args = callArgs(stmt)
|
|
1222
|
-
const shape = args && inlinedBody(candidates.get(stmt[1]), args)
|
|
1223
|
-
if (shape) return { node: ['{}', [';', ...shape.prefix]], changed: true, splice: shape.prefix }
|
|
1224
|
-
}
|
|
1225
|
-
// `let/const X = call(...)` with single decl: inline as prefix + decl(value).
|
|
1226
|
-
if ((stmt[0] === 'let' || stmt[0] === 'const') && stmt.length === 2) {
|
|
1227
|
-
const decl = stmt[1]
|
|
1228
|
-
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && isCandidateCall(decl[2], candidates)) {
|
|
1229
|
-
const args = callArgs(decl[2])
|
|
1230
|
-
const shape = args && inlinedBody(candidates.get(decl[2][1]), args)
|
|
1231
|
-
if (shape && shape.value !== null) {
|
|
1232
|
-
const splice = [...shape.prefix, [stmt[0], ['=', decl[1], shape.value]]]
|
|
1233
|
-
return { node: ['{}', [';', ...splice]], changed: true, splice }
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// `X = call(...)` at statement position: inline as prefix + assign(value).
|
|
1238
|
-
if (stmt[0] === '=' && typeof stmt[1] === 'string' && isCandidateCall(stmt[2], candidates)) {
|
|
1239
|
-
const args = callArgs(stmt[2])
|
|
1240
|
-
const shape = args && inlinedBody(candidates.get(stmt[2][1]), args)
|
|
1241
|
-
if (shape && shape.value !== null) {
|
|
1242
|
-
const splice = [...shape.prefix, ['=', stmt[1], shape.value]]
|
|
1243
|
-
return { node: ['{}', [';', ...splice]], changed: true, splice }
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
const op = stmt[0]
|
|
1247
|
-
if (op === ';') {
|
|
1248
|
-
let changed = false
|
|
1249
|
-
const next = [';']
|
|
1250
|
-
for (let i = 1; i < stmt.length; i++) {
|
|
1251
|
-
const r = inlineInStmt(stmt[i], candidates)
|
|
1252
|
-
changed ||= r.changed
|
|
1253
|
-
if (r.splice) next.push(...r.splice)
|
|
1254
|
-
else next.push(r.node)
|
|
1255
|
-
}
|
|
1256
|
-
return changed ? { node: next, changed } : { node: stmt, changed: false }
|
|
1257
|
-
}
|
|
1258
|
-
if (op === '{}') {
|
|
1259
|
-
const r = inlineInStmt(stmt[1], candidates)
|
|
1260
|
-
if (!r.changed) return { node: stmt, changed: false }
|
|
1261
|
-
// If the child was itself a candidate call (or a let/assign-of-call), it
|
|
1262
|
-
// already returned a `['{}', [';', ...prefix]]` shape. Re-wrapping here
|
|
1263
|
-
// would yield `['{}', ['{}', …]]`, which codegen rejects ("Unknown op: {}").
|
|
1264
|
-
if (Array.isArray(r.node) && r.node[0] === '{}') return { node: r.node, changed: true }
|
|
1265
|
-
return { node: ['{}', r.node], changed: true }
|
|
1266
|
-
}
|
|
1267
|
-
if (op === 'for') {
|
|
1268
|
-
const r = inlineInStmt(stmt[4], candidates)
|
|
1269
|
-
return r.changed ? { node: ['for', stmt[1], stmt[2], stmt[3], r.node], changed: true } : { node: stmt, changed: false }
|
|
1270
|
-
}
|
|
1271
|
-
if (op === 'while') {
|
|
1272
|
-
const r = inlineInStmt(stmt[2], candidates)
|
|
1273
|
-
return r.changed ? { node: ['while', stmt[1], r.node], changed: true } : { node: stmt, changed: false }
|
|
1274
|
-
}
|
|
1275
|
-
if (op === 'if') {
|
|
1276
|
-
const thenR = inlineInStmt(stmt[2], candidates)
|
|
1277
|
-
const elseR = stmt.length > 3 ? inlineInStmt(stmt[3], candidates) : null
|
|
1278
|
-
if (thenR.changed || elseR?.changed) return {
|
|
1279
|
-
node: stmt.length > 3 ? ['if', stmt[1], thenR.node, elseR.node] : ['if', stmt[1], thenR.node],
|
|
1280
|
-
changed: true,
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
if (op === 'try' || op === 'catch' || op === 'finally') {
|
|
1284
|
-
let changed = false
|
|
1285
|
-
const next = [op]
|
|
1286
|
-
for (let i = 1; i < stmt.length; i++) {
|
|
1287
|
-
const part = stmt[i]
|
|
1288
|
-
const r = Array.isArray(part) ? inlineInStmt(part, candidates) : { node: part, changed: false }
|
|
1289
|
-
changed ||= r.changed
|
|
1290
|
-
next.push(r.node)
|
|
1291
|
-
}
|
|
1292
|
-
return changed ? { node: next, changed: true } : { node: stmt, changed: false }
|
|
1293
|
-
}
|
|
1294
|
-
return { node: stmt, changed: false }
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
const inlineHotInternalCalls = (programFacts, ast) => {
|
|
1298
|
-
const cfg = ctx.transform.optimize
|
|
1299
|
-
if (cfg && cfg.sourceInline === false) return false
|
|
1300
|
-
|
|
1301
|
-
const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
|
|
1302
|
-
const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
|
|
1303
|
-
const sitesByCallee = new Map()
|
|
1304
|
-
for (const cs of programFacts.callSites) {
|
|
1305
|
-
const list = sitesByCallee.get(cs.callee)
|
|
1306
|
-
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
const containsNode = (root, needle, inLoop = false) => {
|
|
1310
|
-
if (root === needle) return inLoop
|
|
1311
|
-
if (!Array.isArray(root) || root[0] === '=>') return false
|
|
1312
|
-
const nextInLoop = inLoop || LOOP_OPS.has(root[0])
|
|
1313
|
-
for (let i = 1; i < root.length; i++) if (containsNode(root[i], needle, nextInLoop)) return true
|
|
1314
|
-
return false
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const hasFixedTypedArraySites = (func, sites) => {
|
|
1318
|
-
const params = func.sig?.params || []
|
|
1319
|
-
if (!sites?.length) return false
|
|
1320
|
-
return sites.every(site => params.some((p, i) => {
|
|
1321
|
-
const arg = site.argList[i]
|
|
1322
|
-
return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
|
|
1323
|
-
}))
|
|
1324
|
-
}
|
|
1325
|
-
const hasFullyFixedTypedArraySites = (func, sites) => {
|
|
1326
|
-
const params = func.sig?.params || []
|
|
1327
|
-
if (!sites?.length) return false
|
|
1328
|
-
let sawTypedArg = false
|
|
1329
|
-
for (const site of sites) {
|
|
1330
|
-
const typed = typedByFunc.get(site.callerFunc)
|
|
1331
|
-
const fixed = fixedByFunc.get(site.callerFunc)
|
|
1332
|
-
for (let i = 0; i < params.length; i++) {
|
|
1333
|
-
const arg = site.argList[i]
|
|
1334
|
-
if (typeof arg !== 'string' || !typed?.has(arg)) continue
|
|
1335
|
-
sawTypedArg = true
|
|
1336
|
-
if (!fixed?.has(arg)) return false
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return sawTypedArg
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const candidates = new Map()
|
|
1343
|
-
// Forwarders — a candidate whose body calls one of its own parameters.
|
|
1344
|
-
// Inlining one replaces that parameter with the call-site argument; when the
|
|
1345
|
-
// argument is a known function name the resulting indirect call collapses to
|
|
1346
|
-
// a direct `call` (devirtualization).
|
|
1347
|
-
const forwarders = new Set()
|
|
1348
|
-
for (const func of ctx.func.list) {
|
|
1349
|
-
if (func.exported || func.raw || !func.body || func.rest || programFacts.valueUsed.has(func.name)) continue
|
|
1350
|
-
if (func.defaults && Object.keys(func.defaults).length) continue
|
|
1351
|
-
const sites = sitesByCallee.get(func.name)
|
|
1352
|
-
const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
|
|
1353
|
-
const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
|
|
1354
|
-
const hasLoop = scanBody(func.body, n => LOOP_OPS.has(n[0]))
|
|
1355
|
-
const isTinyLeaf = !hasLoop && nodeSize(func.body) <= 15
|
|
1356
|
-
if (!sites || sites.length < 1 || (!isTinyLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
|
|
1357
|
-
const stmts = blockStmts(func.body)
|
|
1358
|
-
// Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
|
|
1359
|
-
// return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
|
|
1360
|
-
if (scanBody(func.body, n => n[0] === '=>')) continue
|
|
1361
|
-
// throw/break/continue are unsupported; return is OK if it's a single
|
|
1362
|
-
// trailing return (rewritten to a value at inlining time).
|
|
1363
|
-
if (scanBody(func.body, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) continue
|
|
1364
|
-
let returnCount = 0
|
|
1365
|
-
scanBody(func.body, n => { if (n[0] === 'return') returnCount++; return false })
|
|
1366
|
-
if (returnCount > 1) continue
|
|
1367
|
-
if (returnCount === 1 && stmts) {
|
|
1368
|
-
const last = stmts[stmts.length - 1]
|
|
1369
|
-
if (!Array.isArray(last) || last[0] !== 'return') continue
|
|
1370
|
-
}
|
|
1371
|
-
// Either a kernel (has a loop) or a tiny leaf (no loop, no calls, small body).
|
|
1372
|
-
// The leaf branch catches helpers like `isAlpha(c) => (c>=65 && c<=90) || …`
|
|
1373
|
-
// that get hammered from a hot caller's loop — replacing the call with its
|
|
1374
|
-
// body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
|
|
1375
|
-
if (!hasLoop) {
|
|
1376
|
-
if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]))) continue
|
|
1377
|
-
if (nodeSize(func.body) > 30) continue
|
|
1378
|
-
}
|
|
1379
|
-
if (scanBody(func.body, n => n[0] === '()' && n[1] === func.name)) continue
|
|
1380
|
-
// Kernels with nested loops (depth ≥ 2) are typically large and the inner
|
|
1381
|
-
// loop carries most of the cost. Inlining them into a host that V8 can't
|
|
1382
|
-
// tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
|
|
1383
|
-
// Keep them as standalone functions so V8 wasm tier-up can warm them.
|
|
1384
|
-
if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
|
|
1385
|
-
// Factory functions that allocate pointers (`new TypedArray`, `new Array`,
|
|
1386
|
-
// object/array literals returned) break downstream pointer-ABI specialization
|
|
1387
|
-
// when inlined: narrow.js can't trace the post-inline alias chain back to a
|
|
1388
|
-
// single ctor, so the typed-array param of a callee like processCascade(x, …)
|
|
1389
|
-
// stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
|
|
1390
|
-
// Keeping the factory as a callable function preserves the call-site type fact.
|
|
1391
|
-
if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
|
|
1392
|
-
const paramNames = new Set((func.sig?.params || []).map(p => p.name))
|
|
1393
|
-
if (paramNames.size && scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && paramNames.has(n[1])))
|
|
1394
|
-
forwarders.add(func.name)
|
|
1395
|
-
candidates.set(func.name, func)
|
|
1396
|
-
}
|
|
1397
|
-
if (!candidates.size) return false
|
|
1398
|
-
|
|
1399
|
-
// Trivial expr-bodied candidates can be substituted at any expression position
|
|
1400
|
-
// (if-condition, ternary, etc.). Stmt-bodied ones go through inlineInStmt's
|
|
1401
|
-
// statement-level path which preserves prefix ordering.
|
|
1402
|
-
const exprOnlyCandidates = new Map()
|
|
1403
|
-
for (const [name, func] of candidates) {
|
|
1404
|
-
if (!Array.isArray(func.body) || func.body[0] !== '{}') exprOnlyCandidates.set(name, func)
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
let changed = false
|
|
1408
|
-
const exportedCandidates = new Map()
|
|
1409
|
-
for (const [name, func] of candidates) {
|
|
1410
|
-
const sites = sitesByCallee.get(name)
|
|
1411
|
-
const fixedSiteExported = hasFixedTypedArraySites(func, sites) &&
|
|
1412
|
-
!sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))
|
|
1413
|
-
// Forwarders cross into an exported caller too: the tier-up rationale that
|
|
1414
|
-
// keeps candidates out of exports concerns relocated loop kernels, not
|
|
1415
|
-
// these tiny leaves — and inlining one devirtualizes a closure dispatch.
|
|
1416
|
-
if (fixedSiteExported || forwarders.has(name)) exportedCandidates.set(name, func)
|
|
1417
|
-
}
|
|
1418
|
-
for (const func of ctx.func.list) {
|
|
1419
|
-
if (!func.body || func.raw) continue
|
|
1420
|
-
// Skip exports: they're entry points usually invoked once. Inlining a
|
|
1421
|
-
// hot kernel here would put the loop into a function V8's wasm tier-up
|
|
1422
|
-
// never warms (kernel stays in baseline). Keeping the kernel as its own
|
|
1423
|
-
// callable function lets V8 promote it to TurboFan after a few calls.
|
|
1424
|
-
// Exception: fixed-size typed-array callees should inline into the exported
|
|
1425
|
-
// caller so scalar replacement can cross the call boundary and remove the
|
|
1426
|
-
// caller's heap arrays.
|
|
1427
|
-
const activeCandidates = func.exported ? exportedCandidates : candidates
|
|
1428
|
-
if (func.exported && !activeCandidates.size) continue
|
|
1429
|
-
// Expression-bodied arrows (`() => expr`) have func.body as the return
|
|
1430
|
-
// value itself — never a `{}` block. inlineInStmt treats its argument as a
|
|
1431
|
-
// statement (discards the return value of any top-level candidate call),
|
|
1432
|
-
// which would turn `() => x()` into an empty block and lose the result.
|
|
1433
|
-
// Route those through inlineInExpr so the call is replaced by the inlined
|
|
1434
|
-
// value expression instead.
|
|
1435
|
-
const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
|
|
1436
|
-
const r = isExprBody
|
|
1437
|
-
? inlineInExpr(func.body, activeCandidates)
|
|
1438
|
-
: inlineInStmt(func.body, activeCandidates)
|
|
1439
|
-
let body = r.changed ? r.node : func.body
|
|
1440
|
-
let bodyChanged = r.changed
|
|
1441
|
-
if (!func.exported && exprOnlyCandidates.size) {
|
|
1442
|
-
const e = inlineInExpr(body, exprOnlyCandidates)
|
|
1443
|
-
if (e.changed) { body = e.node; bodyChanged = true }
|
|
1444
|
-
}
|
|
1445
|
-
if (bodyChanged) { func.body = body; changed = true }
|
|
1446
|
-
}
|
|
1447
|
-
if (ast) {
|
|
1448
|
-
const r = inlineInStmt(ast, candidates)
|
|
1449
|
-
if (r.changed) changed = true
|
|
1450
|
-
}
|
|
1451
|
-
return changed
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// === Inline non-escaping local lambdas ===
|
|
1455
|
-
// `const f = (a) => …; … f(x) …` → the lambda body substituted at each call
|
|
1456
|
-
// site. A non-escaping lambda's captured free vars are still in lexical scope at
|
|
1457
|
-
// the call site, so splicing the body in place preserves capture-by-reference
|
|
1458
|
-
// semantics while eliminating the closure object (no env pointer, no NaN-box, no
|
|
1459
|
-
// call_indirect). Mirrors inlineHotInternalCalls, scoped to one function body.
|
|
1460
|
-
|
|
1461
|
-
// True iff `name` appears textually anywhere in `node` (descending into nested
|
|
1462
|
-
// arrows; `.prop` / `:key` positions are literal names, not refs — skipped to
|
|
1463
|
-
// match cloneWithSubst's structure).
|
|
1464
|
-
const referencesName = (node, name) => {
|
|
1465
|
-
if (typeof node === 'string') return node === name
|
|
1466
|
-
if (!Array.isArray(node)) return false
|
|
1467
|
-
const op = node[0]
|
|
1468
|
-
if (op === 'str') return false
|
|
1469
|
-
if (op === '.' || op === '?.') return referencesName(node[1], name)
|
|
1470
|
-
if (op === ':') return referencesName(node[2], name)
|
|
1471
|
-
for (let i = 1; i < node.length; i++) if (referencesName(node[i], name)) return true
|
|
1472
|
-
return false
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// True iff every textual reference to `name` in `node` is the callee of a
|
|
1476
|
-
// `name(...)` call (i.e. the binding never escapes — never read as a value,
|
|
1477
|
-
// reassigned, captured by a nested lambda, or shadowed).
|
|
1478
|
-
const onlyCalledNotReferenced = (node, name) => {
|
|
1479
|
-
if (typeof node === 'string') return node !== name
|
|
1480
|
-
if (!Array.isArray(node)) return true
|
|
1481
|
-
const op = node[0]
|
|
1482
|
-
if (op === 'str') return true
|
|
1483
|
-
// A nested lambda touching `name` at all (capture or shadowing param) → bail.
|
|
1484
|
-
if (op === '=>') return !referencesName(node[1], name) && !referencesName(node[2], name)
|
|
1485
|
-
if (op === '()' && node[1] === name) {
|
|
1486
|
-
for (let i = 2; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
1487
|
-
return true
|
|
1488
|
-
}
|
|
1489
|
-
if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
|
|
1490
|
-
if (op === ':') return onlyCalledNotReferenced(node[2], name)
|
|
1491
|
-
for (let i = 1; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
1492
|
-
return true
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
const bodyStmtList = body =>
|
|
1496
|
-
Array.isArray(body) && body[0] === '{}' ? blockStmts(body)
|
|
1497
|
-
: Array.isArray(body) && body[0] === ';' ? body.slice(1)
|
|
1498
|
-
: body == null ? [] : [body]
|
|
1499
|
-
|
|
1500
|
-
const removeStmts = (body, set) => {
|
|
1501
|
-
if (!Array.isArray(body)) return set.has(body) ? null : body
|
|
1502
|
-
if (body[0] === '{}') return ['{}', removeStmts(body[1], set) ?? [';']]
|
|
1503
|
-
if (body[0] === ';') {
|
|
1504
|
-
const kept = body.slice(1).filter(s => !set.has(s))
|
|
1505
|
-
return kept.length === 0 ? null : kept.length === 1 ? kept[0] : [';', ...kept]
|
|
1506
|
-
}
|
|
1507
|
-
return set.has(body) ? null : body
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// Lambda body must be a guaranteed-return shape inlinedBody can splice: ≤1
|
|
1511
|
-
// `return` (trailing, if a block), no throw/break/continue, no param mutation,
|
|
1512
|
-
// no nested lambda.
|
|
1513
|
-
const inlinableLambdaBody = (abody, params) => {
|
|
1514
|
-
if (scanBody(abody, n => n[0] === '=>')) return false
|
|
1515
|
-
if (scanBody(abody, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) return false
|
|
1516
|
-
let returns = 0
|
|
1517
|
-
scanBody(abody, n => { if (n[0] === 'return') returns++; return false })
|
|
1518
|
-
if (returns > 1) return false
|
|
1519
|
-
if (returns === 1) {
|
|
1520
|
-
const stmts = blockStmts(abody)
|
|
1521
|
-
if (!stmts || !stmts.length) return false
|
|
1522
|
-
const last = stmts[stmts.length - 1]
|
|
1523
|
-
if (!Array.isArray(last) || last[0] !== 'return') return false
|
|
1524
|
-
}
|
|
1525
|
-
return !mutatesAny(abody, new Set(params))
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
const inlineLocalLambdasInBody = (getBody, setBody) => {
|
|
1529
|
-
const body = getBody()
|
|
1530
|
-
const stmts = bodyStmtList(body)
|
|
1531
|
-
if (stmts.length < 2) return false
|
|
1532
|
-
|
|
1533
|
-
// Collect `const f = ARROW` (single-decl), all-plain params, inlinable body.
|
|
1534
|
-
const decls = new Map()
|
|
1535
|
-
for (const stmt of stmts) {
|
|
1536
|
-
if (!Array.isArray(stmt) || stmt[0] !== 'const' || stmt.length !== 2) continue
|
|
1537
|
-
const d = stmt[1]
|
|
1538
|
-
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
1539
|
-
const arrow = d[2]
|
|
1540
|
-
if (!Array.isArray(arrow) || arrow[0] !== '=>') continue
|
|
1541
|
-
const params = extractParams(arrow[1])
|
|
1542
|
-
if (!params.every(p => typeof p === 'string')) continue
|
|
1543
|
-
if (!inlinableLambdaBody(arrow[2], params)) continue
|
|
1544
|
-
decls.set(d[1], { stmt, arrow, params })
|
|
1545
|
-
}
|
|
1546
|
-
if (!decls.size) return false
|
|
1547
|
-
|
|
1548
|
-
// Drop any candidate whose body references another (or its own) candidate —
|
|
1549
|
-
// single-level inlining can't resolve such chains, and a still-referenced
|
|
1550
|
-
// candidate's decl can't be removed.
|
|
1551
|
-
for (let changed = true; changed;) {
|
|
1552
|
-
changed = false
|
|
1553
|
-
for (const [name, info] of decls) {
|
|
1554
|
-
if ([...decls.keys()].some(c => referencesName(info.arrow[2], c))) { decls.delete(name); changed = true }
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
// Every other reference to the name must be a `name(...)` call.
|
|
1558
|
-
for (const [name, info] of [...decls]) {
|
|
1559
|
-
if (!stmts.every(s => s === info.stmt || onlyCalledNotReferenced(s, name))) decls.delete(name)
|
|
1560
|
-
}
|
|
1561
|
-
if (!decls.size) return false
|
|
1562
|
-
|
|
1563
|
-
const asFunc = info => ({ sig: { params: info.params.map(name => ({ name })) }, body: info.arrow[2] })
|
|
1564
|
-
const stmtCands = new Map(), exprCands = new Map()
|
|
1565
|
-
for (const [name, info] of decls)
|
|
1566
|
-
(Array.isArray(info.arrow[2]) && info.arrow[2][0] === '{}' ? stmtCands : exprCands).set(name, asFunc(info))
|
|
1567
|
-
|
|
1568
|
-
let out = body, didChange = false
|
|
1569
|
-
if (stmtCands.size) { const r = inlineInStmt(out, stmtCands); if (r.changed) { out = r.node; didChange = true } }
|
|
1570
|
-
if (exprCands.size) { const r = inlineInExpr(out, exprCands); if (r.changed) { out = r.node; didChange = true } }
|
|
1571
|
-
if (!didChange) return false
|
|
1572
|
-
|
|
1573
|
-
// Remove decls of candidates that are now fully consumed.
|
|
1574
|
-
const newStmts = bodyStmtList(out)
|
|
1575
|
-
const dead = new Set()
|
|
1576
|
-
for (const [name, info] of decls) {
|
|
1577
|
-
if (!newStmts.some(s => s !== info.stmt && referencesName(s, name))) dead.add(info.stmt)
|
|
1578
|
-
}
|
|
1579
|
-
if (dead.size) out = removeStmts(out, dead) ?? [';']
|
|
1580
|
-
|
|
1581
|
-
setBody(out)
|
|
1582
|
-
return true
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
const inlineLocalLambdas = () => {
|
|
1586
|
-
let changed = false
|
|
1587
|
-
for (const func of ctx.func.list) {
|
|
1588
|
-
if (!func.body || func.raw) continue
|
|
1589
|
-
if (inlineLocalLambdasInBody(() => func.body, b => { func.body = b })) changed = true
|
|
1590
|
-
}
|
|
1591
|
-
return changed
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
const restIndexExpr = (idx, restParams) => {
|
|
1595
|
-
const k = intLit(idx)
|
|
1596
|
-
if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
|
|
1597
|
-
|
|
1598
|
-
let out = [, undefined]
|
|
1599
|
-
for (let i = restParams.length - 1; i >= 0; i--) {
|
|
1600
|
-
out = ['?:', ['==', clonePlain(idx), [, i]], restParams[i], out]
|
|
1601
|
-
}
|
|
1602
|
-
return out
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const rewriteRestBody = (node, restName, restParams) => {
|
|
1606
|
-
if (typeof node === 'string') return node === restName ? { ok: false } : { ok: true, node }
|
|
1607
|
-
if (!Array.isArray(node)) return { ok: true, node }
|
|
1608
|
-
if (node[0] === 'str') return { ok: true, node: node.slice() }
|
|
1609
|
-
|
|
1610
|
-
if ((node[0] === '.' || node[0] === '?.') && node[1] === restName) {
|
|
1611
|
-
return node[2] === 'length' ? { ok: true, node: [, restParams.length] } : { ok: false }
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
if (node[0] === '[]' && node[1] === restName) {
|
|
1615
|
-
if (!isSimpleArg(node[2])) return { ok: false }
|
|
1616
|
-
return { ok: true, node: restIndexExpr(node[2], restParams) }
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
const out = [node[0]]
|
|
1620
|
-
for (let i = 1; i < node.length; i++) {
|
|
1621
|
-
const r = rewriteRestBody(node[i], restName, restParams)
|
|
1622
|
-
if (!r.ok) return r
|
|
1623
|
-
out.push(r.node)
|
|
1624
|
-
}
|
|
1625
|
-
return { ok: true, node: out }
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const specializeFixedRestCalls = (programFacts) => {
|
|
1629
|
-
const sitesByKey = new Map()
|
|
1630
|
-
for (const site of programFacts.callSites) {
|
|
1631
|
-
const func = ctx.func.map.get(site.callee)
|
|
1632
|
-
if (!func?.rest || func.exported || func.raw || !func.body) continue
|
|
1633
|
-
if (programFacts.valueUsed.has(func.name)) continue
|
|
1634
|
-
if (func.defaults && Object.keys(func.defaults).length) continue
|
|
1635
|
-
if (site.argList.some(a => Array.isArray(a) && a[0] === '...')) continue
|
|
1636
|
-
|
|
1637
|
-
const fixedN = func.sig.params.length - 1
|
|
1638
|
-
const restN = Math.max(0, site.argList.length - fixedN)
|
|
1639
|
-
const key = `${func.name}/${restN}`
|
|
1640
|
-
const list = sitesByKey.get(key)
|
|
1641
|
-
if (list) list.push(site); else sitesByKey.set(key, [site])
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
let changed = false
|
|
1645
|
-
for (const [key, sites] of sitesByKey) {
|
|
1646
|
-
const [name, restNText] = key.split('/')
|
|
1647
|
-
const func = ctx.func.map.get(name)
|
|
1648
|
-
const restN = Number(restNText)
|
|
1649
|
-
const fixedParams = func.sig.params.slice(0, -1).map(p => ({ ...p }))
|
|
1650
|
-
const restName = func.rest
|
|
1651
|
-
const restParams = Array.from({ length: restN }, (_, i) => `${restName}${T}r${restN}_${i}`)
|
|
1652
|
-
const rewritten = rewriteRestBody(func.body, restName, restParams)
|
|
1653
|
-
if (!rewritten.ok) continue
|
|
1654
|
-
|
|
1655
|
-
const cloneName = `${name}${T}rest${restN}`
|
|
1656
|
-
if (!ctx.func.map.has(cloneName)) {
|
|
1657
|
-
const restSigParams = restParams.map(name => ({ name, type: 'f64' }))
|
|
1658
|
-
const clone = {
|
|
1659
|
-
...func,
|
|
1660
|
-
name: cloneName,
|
|
1661
|
-
exported: false,
|
|
1662
|
-
rest: null,
|
|
1663
|
-
sig: {
|
|
1664
|
-
...func.sig,
|
|
1665
|
-
params: [...fixedParams, ...restSigParams],
|
|
1666
|
-
results: [...func.sig.results],
|
|
1667
|
-
},
|
|
1668
|
-
body: rewritten.node,
|
|
1669
|
-
}
|
|
1670
|
-
delete clone.defaults
|
|
1671
|
-
ctx.func.list.push(clone)
|
|
1672
|
-
ctx.func.names.add(cloneName)
|
|
1673
|
-
ctx.func.map.set(cloneName, clone)
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
const fixedN = func.sig.params.length - 1
|
|
1677
|
-
for (const site of sites) {
|
|
1678
|
-
site.node[1] = cloneName
|
|
1679
|
-
setCallArgs(site.node, site.argList.slice(0, fixedN + restN))
|
|
1680
|
-
changed = true
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
return changed
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// `scanGlobalValueFacts` was deleted — prepare's depth-0 catch (calling
|
|
1687
|
-
// `recordGlobalRep` from src/infer.js) is the authoritative pass and a
|
|
1688
|
-
// strict superset of what this top-level walker observed.
|
|
1689
|
-
|
|
1690
|
-
// Flow-insensitive type inference for module-level `let` bindings whose
|
|
1691
|
-
// initial RHS doesn't pin a type (most often `let mem;` followed later by
|
|
1692
|
-
// `mem = new TypedArray(...)` inside an init function). Without this the
|
|
1693
|
-
// read site has to runtime-check the NaN-box tag on every access — game-of-life's
|
|
1694
|
-
// inner step does that 9× per cell, blowing up the hot loop. We union RHS types
|
|
1695
|
-
// across every assignment (initial decl + every `name = …` in any function);
|
|
1696
|
-
// if every observed RHS is either a typed-array ctor of the same kind, a known
|
|
1697
|
-
// VAL.TYPED binding of the same ctor, or null/undefined, the binding is
|
|
1698
|
-
// monomorphically VAL.TYPED. Anything else (literal number, non-typed call,
|
|
1699
|
-
// mixed ctors) clears the candidacy, keeping the read site polymorphic.
|
|
1700
|
-
const inferModuleLetTypes = (ast) => {
|
|
1701
|
-
if (!ctx.scope.userGlobals) return
|
|
1702
|
-
// candidates: name → { ctor: string|null, valid: true } | { valid: false }
|
|
1703
|
-
// valid=true with ctor=null means "still no positive evidence"; we promote
|
|
1704
|
-
// only when ctor is non-null at the end. Assignments to nullish (undef/null)
|
|
1705
|
-
// don't change ctor — they're consistent with any typed-array value.
|
|
1706
|
-
const seen = new Map()
|
|
1707
|
-
for (const name of ctx.scope.userGlobals) seen.set(name, { ctor: null, valid: true })
|
|
1708
|
-
|
|
1709
|
-
const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
|
|
1710
|
-
|| (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
|
|
1711
|
-
|
|
1712
|
-
const observe = (name, rhs) => {
|
|
1713
|
-
const c = seen.get(name)
|
|
1714
|
-
if (!c || !c.valid) return
|
|
1715
|
-
if (isNullishLit(rhs)) return
|
|
1716
|
-
// Resolve typed-array ctor from `new TypedArrayCtor(...)`, ternary of typed,
|
|
1717
|
-
// or a reference to a name we already know is typed.
|
|
1718
|
-
let ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
|
|
1719
|
-
if (ctor === MIXED_CTORS) { c.valid = false; return }
|
|
1720
|
-
if (!ctor && typeof rhs === 'string') {
|
|
1721
|
-
if (ctx.scope.globalValTypes?.get(rhs) === VAL.TYPED)
|
|
1722
|
-
ctor = ctx.scope.globalTypedElem?.get(rhs) ?? null
|
|
1723
|
-
}
|
|
1724
|
-
if (!ctor) { c.valid = false; return }
|
|
1725
|
-
if (c.ctor && c.ctor !== ctor) { c.valid = false; return }
|
|
1726
|
-
c.ctor = ctor
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
const walk = (node) => {
|
|
1730
|
-
if (!Array.isArray(node)) return
|
|
1731
|
-
const op = node[0]
|
|
1732
|
-
if (op === '=' && typeof node[1] === 'string' && seen.has(node[1])) observe(node[1], node[2])
|
|
1733
|
-
if ((op === 'let' || op === 'const') && node.length > 1) {
|
|
1734
|
-
for (let i = 1; i < node.length; i++) {
|
|
1735
|
-
const d = node[i]
|
|
1736
|
-
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' && seen.has(d[1]))
|
|
1737
|
-
observe(d[1], d[2])
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
// Compound-assigns (`+=`, etc.) to a typed-array binding can't preserve
|
|
1741
|
-
// the typed-array kind — invalidate.
|
|
1742
|
-
if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && seen.has(node[1])) {
|
|
1743
|
-
const c = seen.get(node[1])
|
|
1744
|
-
if (c) c.valid = false
|
|
1745
|
-
}
|
|
1746
|
-
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
1747
|
-
}
|
|
1748
|
-
walk(ast)
|
|
1749
|
-
for (const f of ctx.func.list) if (f.body && !f.raw) walk(f.body)
|
|
1750
|
-
|
|
1751
|
-
for (const [name, c] of seen) {
|
|
1752
|
-
if (!c.valid || !c.ctor) continue
|
|
1753
|
-
if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
|
|
1754
|
-
;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
|
|
1755
|
-
;(ctx.scope.globalTypedElem ||= new Map()).set(name, c.ctor)
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// `MIXED_CTORS` and `ternaryCtorOfRhs` are not exported from analyze — re-derive
|
|
1760
|
-
// the sentinel locally and look up the ctor through `typedElemCtor` alone.
|
|
1761
|
-
// `ternaryCtorOfRhs` is purely for ternary RHSs (e.g. `cond ? new Int32Array(N) : new Int32Array(M)`),
|
|
1762
|
-
// which game-of-life-style sources don't use; falling back to typedElemCtor is fine.
|
|
1763
|
-
const MIXED_CTORS = Symbol('MIXED_CTORS')
|
|
1764
|
-
const ternaryCtorOfRhs = () => null
|
|
1765
|
-
|
|
1766
|
-
const unboxConstTypedGlobals = () => {
|
|
1767
|
-
if (!ctx.scope.globalTypedElem || !ctx.scope.consts) return
|
|
1768
|
-
for (const [name, ctor] of ctx.scope.globalTypedElem) {
|
|
1769
|
-
if (!ctx.scope.consts.has(name)) continue
|
|
1770
|
-
if (ctx.scope.globalValTypes?.get(name) !== VAL.TYPED) continue
|
|
1771
|
-
const aux = typedElemAux(ctor)
|
|
1772
|
-
if (aux == null) continue
|
|
1773
|
-
const decl = ctx.scope.globals.get(name)
|
|
1774
|
-
if (typeof decl !== 'string' || !decl.includes('mut f64')) continue
|
|
1775
|
-
ctx.scope.globals.set(name, `(global $${name} (mut i32) (i32.const 0))`)
|
|
1776
|
-
ctx.scope.globalTypes.set(name, 'i32')
|
|
1777
|
-
updateGlobalRep(name, { ptrKind: VAL.TYPED, ptrAux: aux })
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
/**
|
|
1782
|
-
* Function-namespace scalar replacement + devirtualization.
|
|
1783
|
-
*
|
|
1784
|
-
* A property of a user function compiles, by default, as a dynamic object: each
|
|
1785
|
-
* `f.prop` write is a `__dyn_set` into a closure-keyed hash side-table, each
|
|
1786
|
-
* read a `__dyn_get`. But a function's property table can never be observed by
|
|
1787
|
-
* the host (the host receives only the callable; the table lives in jz linear
|
|
1788
|
-
* memory), so jz sees every `f.prop` site — the slot is a closed, fully-known
|
|
1789
|
-
* cell. Per property of a non-escaping namespace:
|
|
1790
|
-
*
|
|
1791
|
-
* - reassigned (`multiProp`) slot → dissolve into a plain f64 module global:
|
|
1792
|
-
* `__dyn_get/__dyn_set` → `global.get/global.set`. The indirect call stays
|
|
1793
|
-
* (a genuinely reassigned function pointer needs `call_indirect`). Pure
|
|
1794
|
-
* storage relocation: the global inits to `UNDEF_WAT`, exactly mirroring
|
|
1795
|
-
* "key never set → __dyn_get yields undefined".
|
|
1796
|
-
* - written once to its lifted `$f$prop` function and only ever *called*
|
|
1797
|
-
* (never read as a value) → the `__dyn_set` is dead: emit already lowers
|
|
1798
|
-
* `f.prop()` to a direct `call $f$prop`. Drop the write entirely.
|
|
1799
|
-
*
|
|
1800
|
-
* Disqualified namespaces (`f` escapes as a bare value / is computed-indexed —
|
|
1801
|
-
* an alias could reach the table) keep the dynamic path. Together these can
|
|
1802
|
-
* eliminate the `__dyn_*` machinery from a namespace-only program outright.
|
|
1803
|
-
*/
|
|
1804
|
-
const flattenFuncNamespaces = (ast) => {
|
|
1805
|
-
const names = ctx.func.names
|
|
1806
|
-
if (!names?.size) return false
|
|
1807
|
-
// Cheap structural gate: a flattenable namespace exists only if some lifted
|
|
1808
|
-
// `f$prop` name's `f` is itself a function (prepare lifts every `f.prop =
|
|
1809
|
-
// arrow` — multiProp slots included). The base `f` may itself carry a module
|
|
1810
|
-
// prefix (`mod$f`), so scan every `$` boundary, not just the first; a
|
|
1811
|
-
// populated `multiProp` registry is itself a direct namespace witness.
|
|
1812
|
-
let hasNs = ctx.func.multiProp.size > 0
|
|
1813
|
-
if (!hasNs) outer: for (const n of names) {
|
|
1814
|
-
for (let i = n.indexOf('$'); i > 0; i = n.indexOf('$', i + 1))
|
|
1815
|
-
if (names.has(n.slice(0, i))) { hasNs = true; break outer }
|
|
1816
|
-
}
|
|
1817
|
-
if (!hasNs) return false
|
|
1818
|
-
const ns = analyzeFuncNamespaces(ast)
|
|
1819
|
-
if (!ns.size) return false
|
|
1820
|
-
// f → Map<prop, decision>; decision is { global } (SROA) or { drop } (dead
|
|
1821
|
-
// write to an only-called single-write slot).
|
|
1822
|
-
const flat = new Map()
|
|
1823
|
-
for (const [f, info] of ns) {
|
|
1824
|
-
if (info.disq) continue
|
|
1825
|
-
let decide
|
|
1826
|
-
const plan = (prop, d) => { if (!decide) flat.set(f, decide = new Map()); decide.set(prop, d) }
|
|
1827
|
-
for (const prop of info.props) {
|
|
1828
|
-
if (ctx.func.multiProp.has(`${f}.${prop}`)) { plan(prop, { global: `${f}${T}${prop}` }); continue }
|
|
1829
|
-
const w = info.writes.get(prop)
|
|
1830
|
-
// Single write of the lifted `$f$prop`, never read as a value → drop it.
|
|
1831
|
-
if (w && w.length === 1 && w[0].atInit && w[0].rhs === `${f}$${prop}` && !info.valRead.has(prop))
|
|
1832
|
-
plan(prop, { drop: true })
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
if (!flat.size) return false
|
|
1836
|
-
for (const decide of flat.values())
|
|
1837
|
-
for (const d of decide.values())
|
|
1838
|
-
if (d.global && !ctx.scope.globals.has(d.global)) {
|
|
1839
|
-
ctx.scope.globals.set(d.global, `(global $${d.global} (mut f64) ${UNDEF_WAT})`)
|
|
1840
|
-
ctx.scope.globalTypes.set(d.global, 'f64')
|
|
1841
|
-
}
|
|
1842
|
-
const decisionFor = (obj, prop) =>
|
|
1843
|
-
typeof obj === 'string' && typeof prop === 'string' && flat.has(obj)
|
|
1844
|
-
? flat.get(obj).get(prop) : undefined
|
|
1845
|
-
const isEmptySeq = (n) => Array.isArray(n) && n.length === 1 && n[0] === ';'
|
|
1846
|
-
const rewrite = (node) => {
|
|
1847
|
-
if (!Array.isArray(node)) return node
|
|
1848
|
-
const op = node[0]
|
|
1849
|
-
if (op === '.' || op === '?.') {
|
|
1850
|
-
const d = decisionFor(node[1], node[2])
|
|
1851
|
-
if (d?.global) return d.global // drop-decisions leave reads/calls alone
|
|
1852
|
-
}
|
|
1853
|
-
if (op === '=' && Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.')) {
|
|
1854
|
-
const d = decisionFor(node[1][1], node[1][2])
|
|
1855
|
-
if (d?.global) return ['=', d.global, rewrite(node[2])]
|
|
1856
|
-
if (d?.drop) return [';'] // dead write — emit nothing
|
|
1857
|
-
}
|
|
1858
|
-
const out = [op]
|
|
1859
|
-
// Filter dropped writes out of statement sequences (an empty `[';']` left in
|
|
1860
|
-
// a body would lower to an unrenderable node).
|
|
1861
|
-
for (let i = 1; i < node.length; i++) {
|
|
1862
|
-
const c = rewrite(node[i])
|
|
1863
|
-
if (op === ';' && isEmptySeq(c)) continue
|
|
1864
|
-
out.push(c)
|
|
1865
|
-
}
|
|
1866
|
-
return out
|
|
1867
|
-
}
|
|
1868
|
-
const newAst = rewrite(ast)
|
|
1869
|
-
ast.length = 0
|
|
1870
|
-
for (let i = 0; i < newAst.length; i++) ast.push(newAst[i])
|
|
1871
|
-
for (const fn of ctx.func.list)
|
|
1872
|
-
if (fn.body && !fn.raw) fn.body = rewrite(fn.body)
|
|
1873
|
-
// The defining `f.prop = …` writes live in moduleInits for bundled programs —
|
|
1874
|
-
// rewrite them too, or reads would resolve to an unwritten global.
|
|
1875
|
-
if (ctx.module.moduleInits)
|
|
1876
|
-
for (let i = 0; i < ctx.module.moduleInits.length; i++)
|
|
1877
|
-
ctx.module.moduleInits[i] = rewrite(ctx.module.moduleInits[i])
|
|
1878
|
-
return true
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
/**
|
|
1882
|
-
* Closure devirtualization.
|
|
1883
|
-
*
|
|
1884
|
-
* `flattenFuncNamespaces` dissolves a reassigned `f.prop` function slot into a
|
|
1885
|
-
* module global, but the call through it stays a `call_indirect` on a
|
|
1886
|
-
* `global.get`, dispatched via an ABI-adapting trampoline. When that global is
|
|
1887
|
-
* written *only* by unconditional module-init assignments it holds, for the
|
|
1888
|
-
* entire post-init program, one statically-known function — so every call
|
|
1889
|
-
* through it collapses to a direct `call`: no table lookup, no trampoline, no
|
|
1890
|
-
* 8-wide padding ABI, no closure type guard.
|
|
1891
|
-
*
|
|
1892
|
-
* A global G qualifies iff:
|
|
1893
|
-
* 1. every assignment to G is an unconditional module-init statement — none in
|
|
1894
|
-
* a function body, none nested inside init control flow;
|
|
1895
|
-
* 2. G's final init value resolves (through global aliases) to a top-level
|
|
1896
|
-
* function F;
|
|
1897
|
-
* 3. G is never *called* by module-init code, nor by any function reachable
|
|
1898
|
-
* from it — so every call site runs strictly post-init, where G ≡ F.
|
|
1899
|
-
* Devirt then only swaps an indirect call for a direct call to the very same
|
|
1900
|
-
* callee: it cannot change behavior, only drop dispatch overhead. The result is
|
|
1901
|
-
* recorded in `ctx.func.globalDevirt` (`Map<global, fn>`) and consumed by emit.
|
|
1902
|
-
*/
|
|
1903
|
-
const devirtGlobalCalls = (ast) => {
|
|
1904
|
-
const fnNames = ctx.func.names
|
|
1905
|
-
if (!fnNames?.size || !ctx.scope.globals?.size) return
|
|
1906
|
-
|
|
1907
|
-
// Module-init statement stream, in execution order: moduleInits run first in
|
|
1908
|
-
// `$__start`, then the main module's top-level.
|
|
1909
|
-
const initStmts = []
|
|
1910
|
-
const flatten = (n) => {
|
|
1911
|
-
if (Array.isArray(n) && n[0] === ';') for (let i = 1; i < n.length; i++) flatten(n[i])
|
|
1912
|
-
else if (n != null) initStmts.push(n)
|
|
1913
|
-
}
|
|
1914
|
-
for (const mi of ctx.module.moduleInits || []) flatten(mi)
|
|
1915
|
-
flatten(ast)
|
|
1916
|
-
|
|
1917
|
-
const isGlobal = (s) => typeof s === 'string' && ctx.scope.globals.has(s)
|
|
1918
|
-
// `[target, rhs]` pairs for a `=` / `let` / `const` node assigning a global.
|
|
1919
|
-
const writesOf = (node) => {
|
|
1920
|
-
if (!Array.isArray(node)) return []
|
|
1921
|
-
if (node[0] === '=' && isGlobal(node[1])) return [[node[1], node[2]]]
|
|
1922
|
-
if (node[0] === 'let' || node[0] === 'const') {
|
|
1923
|
-
const out = []
|
|
1924
|
-
for (let i = 1; i < node.length; i++) {
|
|
1925
|
-
const d = node[i]
|
|
1926
|
-
if (Array.isArray(d) && d[0] === '=' && isGlobal(d[1])) out.push([d[1], d[2]])
|
|
1927
|
-
}
|
|
1928
|
-
return out
|
|
1929
|
-
}
|
|
1930
|
-
return []
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
// Poison a global assigned anywhere but an unconditional init statement — in a
|
|
1934
|
-
// function body, or nested in init control flow. Its value is then not a
|
|
1935
|
-
// fixed post-init constant.
|
|
1936
|
-
const poison = new Set()
|
|
1937
|
-
const scanWrites = (node, topInit) => {
|
|
1938
|
-
if (!Array.isArray(node)) return
|
|
1939
|
-
const op = node[0]
|
|
1940
|
-
if (op === 'let' || op === 'const') {
|
|
1941
|
-
// A declarator `=` is part of the declaration, not a nested assignment —
|
|
1942
|
-
// poison only when the declaration itself is non-top-level.
|
|
1943
|
-
for (let i = 1; i < node.length; i++) {
|
|
1944
|
-
const d = node[i]
|
|
1945
|
-
if (Array.isArray(d) && d[0] === '=') {
|
|
1946
|
-
if (!topInit && isGlobal(d[1])) poison.add(d[1])
|
|
1947
|
-
scanWrites(d[2], false)
|
|
1948
|
-
} else scanWrites(d, false)
|
|
1949
|
-
}
|
|
1950
|
-
return
|
|
1951
|
-
}
|
|
1952
|
-
if (op === '=') {
|
|
1953
|
-
if (!topInit && isGlobal(node[1])) poison.add(node[1])
|
|
1954
|
-
scanWrites(node[1], false)
|
|
1955
|
-
scanWrites(node[2], false)
|
|
1956
|
-
return
|
|
1957
|
-
}
|
|
1958
|
-
for (let i = 1; i < node.length; i++) scanWrites(node[i], false)
|
|
1959
|
-
}
|
|
1960
|
-
for (const stmt of initStmts) scanWrites(stmt, true)
|
|
1961
|
-
for (const fn of ctx.func.map.values())
|
|
1962
|
-
if (fn.body && !fn.raw) scanWrites(fn.body, false)
|
|
1963
|
-
|
|
1964
|
-
// Resolve each global's value by a linear pass over init in execution order.
|
|
1965
|
-
const env = new Map()
|
|
1966
|
-
const evalFn = (rhs) =>
|
|
1967
|
-
typeof rhs !== 'string' ? null
|
|
1968
|
-
: fnNames.has(rhs) ? rhs
|
|
1969
|
-
: env.has(rhs) ? env.get(rhs)
|
|
1970
|
-
: null
|
|
1971
|
-
for (const stmt of initStmts)
|
|
1972
|
-
for (const [g, rhs] of writesOf(stmt)) env.set(g, evalFn(rhs))
|
|
1973
|
-
|
|
1974
|
-
const devirt = new Map()
|
|
1975
|
-
for (const [g, fn] of env)
|
|
1976
|
-
if (fn && fnNames.has(fn) && !poison.has(g)) devirt.set(g, fn)
|
|
1977
|
-
if (!devirt.size) return
|
|
1978
|
-
|
|
1979
|
-
// Condition 3: a call through G that runs *during* init would see an
|
|
1980
|
-
// intermediate value. Drop any candidate G called by init code, or by a
|
|
1981
|
-
// function reachable from it.
|
|
1982
|
-
//
|
|
1983
|
-
// `walkStraightLine` follows only straight-line execution: a nested `=>`
|
|
1984
|
-
// literal is a closure *constructed* here, not run here, so its body is
|
|
1985
|
-
// skipped — an IIFE callee `(=> …)()` is the one exception, its body does
|
|
1986
|
-
// run. This is what keeps operator-registration init (`binary('+', 11)`
|
|
1987
|
-
// builds, but does not invoke, a parselet closure) from dragging the parser
|
|
1988
|
-
// into the init-reachable set, and keeps a wrapper body's `space()` call —
|
|
1989
|
-
// which fires at parse time — from counting as an init call. (Soundness
|
|
1990
|
-
// rests on a closure constructed during init not also being invoked during
|
|
1991
|
-
// init: true of function-slot wrappers, which are registered then called at
|
|
1992
|
-
// use time.)
|
|
1993
|
-
const walkStraightLine = (node, onCall) => {
|
|
1994
|
-
if (!Array.isArray(node)) return
|
|
1995
|
-
const op = node[0]
|
|
1996
|
-
if (op === '()') {
|
|
1997
|
-
onCall(node[1])
|
|
1998
|
-
if (Array.isArray(node[1]) && node[1][0] === '=>') walkStraightLine(node[1][2], onCall)
|
|
1999
|
-
for (let i = 2; i < node.length; i++) walkStraightLine(node[i], onCall)
|
|
2000
|
-
return
|
|
2001
|
-
}
|
|
2002
|
-
if (op === '=>' || op === 'function') return
|
|
2003
|
-
for (let i = 1; i < node.length; i++) walkStraightLine(node[i], onCall)
|
|
2004
|
-
}
|
|
2005
|
-
const reachable = new Set()
|
|
2006
|
-
const queue = []
|
|
2007
|
-
const seedCalls = (node) => walkStraightLine(node, (c) => {
|
|
2008
|
-
if (typeof c === 'string' && fnNames.has(c)) queue.push(c)
|
|
2009
|
-
})
|
|
2010
|
-
for (const s of initStmts) seedCalls(s)
|
|
2011
|
-
while (queue.length) {
|
|
2012
|
-
const f = queue.pop()
|
|
2013
|
-
if (reachable.has(f)) continue
|
|
2014
|
-
reachable.add(f)
|
|
2015
|
-
const fn = ctx.func.map.get(f)
|
|
2016
|
-
if (fn?.body && !fn.raw) seedCalls(fn.body)
|
|
2017
|
-
}
|
|
2018
|
-
const calledInInit = new Set()
|
|
2019
|
-
const collectCalled = (node) => walkStraightLine(node, (c) => {
|
|
2020
|
-
if (devirt.has(c)) calledInInit.add(c)
|
|
2021
|
-
})
|
|
2022
|
-
for (const s of initStmts) collectCalled(s)
|
|
2023
|
-
for (const f of reachable) { const fn = ctx.func.map.get(f); if (fn?.body) collectCalled(fn.body) }
|
|
2024
|
-
for (const g of calledInInit) devirt.delete(g)
|
|
2025
|
-
|
|
2026
|
-
if (devirt.size) ctx.func.globalDevirt = devirt
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
const materializeAutoBoxSchemas = (programFacts) => {
|
|
2030
|
-
if (!ctx.schema.register) return
|
|
2031
|
-
for (const [name, props] of programFacts.propMap) {
|
|
2032
|
-
if (ctx.schema.vars.has(name)) {
|
|
2033
|
-
const existing = ctx.schema.resolve(name)
|
|
2034
|
-
const newProps = [...props].filter(prop => !existing.includes(prop))
|
|
2035
|
-
if (newProps.length) {
|
|
2036
|
-
const merged = [...existing, ...newProps]
|
|
2037
|
-
const mergedId = ctx.schema.register(merged)
|
|
2038
|
-
ctx.schema.vars.set(name, mergedId)
|
|
2039
|
-
}
|
|
2040
|
-
continue
|
|
2041
|
-
}
|
|
2042
|
-
const valueProps = [...props].filter(prop => !ctx.func.names.has(`${name}$${prop}`))
|
|
2043
|
-
if (!valueProps.length) continue
|
|
2044
|
-
const allProps = [...props]
|
|
2045
|
-
const schema = ['__inner__', ...allProps]
|
|
2046
|
-
const schemaId = ctx.schema.register(schema)
|
|
2047
|
-
ctx.schema.vars.set(name, schemaId)
|
|
2048
|
-
if (ctx.func.names.has(name) && !ctx.scope.globals.has(name))
|
|
2049
|
-
ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
|
|
2050
|
-
if (!ctx.schema.autoBox) ctx.schema.autoBox = new Map()
|
|
2051
|
-
ctx.schema.autoBox.set(name, { schemaId, schema })
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
const resolveClosureWidth = (programFacts) => {
|
|
2056
|
-
if (!ctx.closure.make) return
|
|
2057
|
-
const { hasSpread, hasRest, maxCall, maxDef, valueUsed } = programFacts
|
|
2058
|
-
const floor = ctx.closure.floor ?? 0
|
|
2059
|
-
// A top-level function used as a first-class value gets a boundary trampoline
|
|
2060
|
-
// that forwards $__a0..$__a{arity-1} into it (emit.js). The uniform closure
|
|
2061
|
-
// ABI must therefore be at least as wide as any table-resident function's
|
|
2062
|
-
// fixed arity — maxDef only counts surviving `=>` literals, so lifted/hoisted
|
|
2063
|
-
// function definitions slip past it (their bodies are walked, their param
|
|
2064
|
-
// lists aren't). Without this, e.g. an arity-3 function used only via a
|
|
2065
|
-
// 1-arg indirect call emits `(local.get $__a2)` against a 2-param trampoline.
|
|
2066
|
-
let maxValueArity = 0
|
|
2067
|
-
if (valueUsed) for (const name of valueUsed) {
|
|
2068
|
-
const n = ctx.func.map.get(name)?.sig?.params?.length ?? 0
|
|
2069
|
-
if (n > maxValueArity) maxValueArity = n
|
|
2070
|
-
}
|
|
2071
|
-
ctx.closure.width = (hasSpread && hasRest)
|
|
2072
|
-
? MAX_CLOSURE_ARITY
|
|
2073
|
-
: Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), maxValueArity, floor))
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
const canSkipWholeProgramNarrowing = (programFacts) =>
|
|
2077
|
-
programFacts.callSites.length === 0 &&
|
|
2078
|
-
programFacts.valueUsed.size === 0 &&
|
|
2079
|
-
!programFacts.anyDyn &&
|
|
2080
|
-
programFacts.propMap.size === 0 &&
|
|
2081
|
-
!programFacts.hasSchemaLiterals &&
|
|
2082
|
-
!ctx.closure.make
|
|
2083
|
-
|
|
2084
|
-
export default function plan(ast) {
|
|
2085
|
-
inferModuleLetTypes(ast)
|
|
2086
|
-
unboxConstTypedGlobals()
|
|
2087
|
-
|
|
2088
|
-
let programFacts = collectProgramFacts(ast)
|
|
2089
|
-
// Function-namespace SROA — dissolve reassigned `f.prop` slots into module
|
|
2090
|
-
// globals before inlining/narrowing, so all downstream passes see plain
|
|
2091
|
-
// globals instead of the dynamic property machinery.
|
|
2092
|
-
if (flattenFuncNamespaces(ast)) programFacts = collectProgramFacts(ast)
|
|
2093
|
-
// Devirtualize calls through init-constant function globals (closure
|
|
2094
|
-
// devirtualization) — must follow the SROA above, which creates the globals.
|
|
2095
|
-
devirtGlobalCalls(ast)
|
|
2096
|
-
// The call-inlining family (`inlineHotInternalCalls` self-gates on `sourceInline`)
|
|
2097
|
-
// is a pure speed optimization — the un-inlined calls emit correctly. Scalar
|
|
2098
|
-
// replacement (`scalarize*`) is *not* gated on `sourceInline`: callers turn it on
|
|
2099
|
-
// independently via `optimize: { sourceInline: false }` to test heap elision alone.
|
|
2100
|
-
if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
|
|
2101
|
-
if (inlineLocalLambdas()) programFacts = collectProgramFacts(ast)
|
|
2102
|
-
if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
|
|
2103
|
-
if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
|
|
2104
|
-
if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
|
|
2105
|
-
// Promotion runs AFTER literal scalarization (those that fully reduce to scalars
|
|
2106
|
-
// are gone) and BEFORE typed-array scalarization (so a freshly-promoted array's
|
|
2107
|
-
// fixed-length-typed-of-known-size variant could still participate in loop
|
|
2108
|
-
// unrolling — currently it can't, since promotion produces the `[...]`-arg
|
|
2109
|
-
// form rather than `new Int32Array(N)`, but the ordering keeps the door open).
|
|
2110
|
-
if (promoteIntArrayLiterals()) programFacts = collectProgramFacts(ast)
|
|
2111
|
-
if (scalarizeFunctionTypedArrays(programFacts)) programFacts = collectProgramFacts(ast)
|
|
2112
|
-
ctx.types.dynKeyVars = programFacts.dynVars
|
|
2113
|
-
ctx.types.anyDynKey = programFacts.anyDyn
|
|
2114
|
-
|
|
2115
|
-
materializeAutoBoxSchemas(programFacts)
|
|
2116
|
-
resolveClosureWidth(programFacts)
|
|
2117
|
-
if (canSkipWholeProgramNarrowing(programFacts)) {
|
|
2118
|
-
// Phase J (jsstring boundary opt-in) is body-local and call-site-independent;
|
|
2119
|
-
// run it even when the rest of narrowing is skipped so simple `export let
|
|
2120
|
-
// f = (s) => s.length` still flips to externref. Likewise the boolean-result
|
|
2121
|
-
// fact, so `export let f = (a) => a > 2` boxes its boundary atom.
|
|
2122
|
-
applyJsstringBoundaryCarrierStandalone(programFacts)
|
|
2123
|
-
narrowBoolResults()
|
|
2124
|
-
return programFacts
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
narrowSignatures(programFacts, ast)
|
|
2128
|
-
specializeBimorphicTyped(programFacts)
|
|
2129
|
-
refineDynKeys(programFacts)
|
|
2130
|
-
|
|
2131
|
-
return programFacts
|
|
2132
|
-
}
|