jz 0.5.0 → 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 +281 -142
- 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 +461 -185
- 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 +591 -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} +600 -205
- 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 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Literal-form heap elision & narrowing.
|
|
3
|
+
*
|
|
4
|
+
* Three related transforms over `let`/`const` literal bindings that the
|
|
5
|
+
* back-end would otherwise allocate on the heap:
|
|
6
|
+
*
|
|
7
|
+
* - `scalarizeFunctionTypedArrays` — fixed-size `new Int32Array(N)` etc.
|
|
8
|
+
* whose every use is statically indexed
|
|
9
|
+
* collapse to N WASM locals; element
|
|
10
|
+
* coercion stays inline.
|
|
11
|
+
* - `scalarizeFunctionArrayLiterals`/`scalarizeFunctionObjectLiterals` —
|
|
12
|
+
* fixed-size `[…]` and `{…}` literals
|
|
13
|
+
* with static accesses collapse to
|
|
14
|
+
* per-slot locals (heap → registers).
|
|
15
|
+
* - `promoteIntArrayLiterals` — `let xs = [1,2,3]` with every use
|
|
16
|
+
* typed-array-compatible promotes to
|
|
17
|
+
* `new Int32Array([1,2,3])`. Downstream
|
|
18
|
+
* narrows the carrier to a tight i32 vec.
|
|
19
|
+
*
|
|
20
|
+
* Each wrapper iterates to fixpoint and returns `boolean changed` so the
|
|
21
|
+
* orchestrator knows when to invalidate the program-facts cache.
|
|
22
|
+
*
|
|
23
|
+
* @module compile/plan/literals
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { ctx } from '../../ctx.js'
|
|
27
|
+
import {
|
|
28
|
+
some, T, stmtList, refsName, ASSIGN_OPS, isReassigned, hasControlTransfer,
|
|
29
|
+
} from '../../ast.js'
|
|
30
|
+
import {
|
|
31
|
+
intLiteralValue, constIntExpr, staticObjectProps, staticPropertyKey,
|
|
32
|
+
} from '../../static.js'
|
|
33
|
+
import {
|
|
34
|
+
smallConstForTripCount, containsDeclOf, cloneWithSubst,
|
|
35
|
+
} from '../../type.js'
|
|
36
|
+
import { VAL } from '../../reps.js'
|
|
37
|
+
import { includeModule } from '../../autoload.js'
|
|
38
|
+
import { analyzeBody, invalidateLocalsCache } from '../analyze.js'
|
|
39
|
+
import {
|
|
40
|
+
isSimpleArg, fixedScalarTypedArray, fixedTypedArraysInBody, maxScalarTypedArrayLen,
|
|
41
|
+
} from './common.js'
|
|
42
|
+
|
|
43
|
+
// === Loop unrolling & scalarization ===
|
|
44
|
+
|
|
45
|
+
// AST for the store coercion a typed-array element does on write (`arr[i] = v`).
|
|
46
|
+
// All expressible with operators jz already lowers post-plan (no module deps).
|
|
47
|
+
const coerceAST = (kind, expr) => {
|
|
48
|
+
if (kind === 'i32') return ['|', expr, [null, 0]]
|
|
49
|
+
if (kind === 'i16') return ['>>', ['<<', expr, [null, 16]], [null, 16]]
|
|
50
|
+
if (kind === 'u16') return ['&', expr, [null, 0xffff]]
|
|
51
|
+
if (kind === 'i8') return ['>>', ['<<', expr, [null, 24]], [null, 24]]
|
|
52
|
+
if (kind === 'u8') return ['&', expr, [null, 0xff]]
|
|
53
|
+
return expr
|
|
54
|
+
}
|
|
55
|
+
const maxScalarTypedLoopUnroll = () => ctx.transform.optimize?.scalarTypedLoopUnroll ?? 16
|
|
56
|
+
const maxScalarTypedNestedUnroll = () => ctx.transform.optimize?.scalarTypedNestedUnroll ?? 128
|
|
57
|
+
|
|
58
|
+
const scalarArrayElems = (expr) => {
|
|
59
|
+
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
60
|
+
const elems = expr.slice(1)
|
|
61
|
+
if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...') || !isSimpleArg(e))) return null
|
|
62
|
+
return elems
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const scalarObjectProps = (expr) => {
|
|
66
|
+
if (!Array.isArray(expr) || expr[0] !== '{}') return null
|
|
67
|
+
const props = staticObjectProps(expr.slice(1))
|
|
68
|
+
if (!props) return null
|
|
69
|
+
const seen = new Set()
|
|
70
|
+
for (let i = 0; i < props.names.length; i++) {
|
|
71
|
+
const name = props.names[i]
|
|
72
|
+
if (seen.has(name) || !isSimpleArg(props.values[i])) return null
|
|
73
|
+
seen.add(name)
|
|
74
|
+
}
|
|
75
|
+
return props
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
79
|
+
|
|
80
|
+
// `name.length = n` (resize) / `name.prop op= v` / `++name.length`: a member
|
|
81
|
+
// write on the binding can't be modeled by fixed scalar slots — the fold would
|
|
82
|
+
// turn the assignment TARGET into a literal (`[null, len] = v`).
|
|
83
|
+
const isMemberWriteTarget = (op, node, name) =>
|
|
84
|
+
(ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
|
|
85
|
+
&& Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') && node[1][1] === name
|
|
86
|
+
|
|
87
|
+
const safeScalarArrayUse = (node, name, len, parentOp = null) => {
|
|
88
|
+
if (typeof node === 'string') return node !== name
|
|
89
|
+
if (!Array.isArray(node)) return true
|
|
90
|
+
const op = node[0]
|
|
91
|
+
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
92
|
+
if (isMemberWriteTarget(op, node, name)) return false
|
|
93
|
+
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
94
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
95
|
+
// Element write `name[idx] (op)= v` / `name[idx]++`: an out-of-bounds index
|
|
96
|
+
// grows the array (sparse-array semantics), which the fixed scalar slot set
|
|
97
|
+
// can't model — reject unless idx is a literal within the literal's bounds.
|
|
98
|
+
if ((ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
|
|
99
|
+
&& Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
|
|
100
|
+
const idx = constIntExpr(node[1][2])
|
|
101
|
+
if (idx == null || idx < 0 || idx >= len) return false
|
|
102
|
+
for (let i = 2; i < node.length; i++) if (!safeScalarArrayUse(node[i], name, len, op)) return false
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
if (op === '[]' && node[1] === name) return constIntExpr(node[2]) != null
|
|
106
|
+
if (op === '...' && node[1] === name) return parentOp === '['
|
|
107
|
+
for (let i = 1; i < node.length; i++) {
|
|
108
|
+
if (!safeScalarArrayUse(node[i], name, len, op)) return false
|
|
109
|
+
}
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const rewriteScalarArrayUses = (node, arrays, parentOp = null) => {
|
|
114
|
+
if (!Array.isArray(node)) return node
|
|
115
|
+
const op = node[0]
|
|
116
|
+
if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') {
|
|
117
|
+
return [, arrays.get(node[1]).length]
|
|
118
|
+
}
|
|
119
|
+
if (op === '[]' && arrays.has(node[1])) {
|
|
120
|
+
const idx = constIntExpr(node[2])
|
|
121
|
+
const elems = arrays.get(node[1])
|
|
122
|
+
return idx != null && idx >= 0 && idx < elems.length ? elems[idx] : [, undefined]
|
|
123
|
+
}
|
|
124
|
+
if (op === '[') {
|
|
125
|
+
const out = ['[']
|
|
126
|
+
for (let i = 1; i < node.length; i++) {
|
|
127
|
+
const item = node[i]
|
|
128
|
+
if (Array.isArray(item) && item[0] === '...' && arrays.has(item[1])) {
|
|
129
|
+
out.push(...arrays.get(item[1]))
|
|
130
|
+
} else {
|
|
131
|
+
out.push(rewriteScalarArrayUses(item, arrays, op))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
return node.map((part, i) => i === 0 ? part : rewriteScalarArrayUses(part, arrays, op))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const safeScalarObjectUse = (node, name, keys) => {
|
|
140
|
+
if (typeof node === 'string') return node !== name
|
|
141
|
+
if (!Array.isArray(node)) return true
|
|
142
|
+
const op = node[0]
|
|
143
|
+
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
144
|
+
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
145
|
+
if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2])
|
|
146
|
+
if (op === '[]' && node[1] === name) {
|
|
147
|
+
const key = staticPropertyKey(node[2])
|
|
148
|
+
return key != null && keys.has(key)
|
|
149
|
+
}
|
|
150
|
+
if (op === '...' && node[1] === name) return false
|
|
151
|
+
for (let i = 1; i < node.length; i++) {
|
|
152
|
+
if (!safeScalarObjectUse(node[i], name, keys)) return false
|
|
153
|
+
}
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const rewriteScalarObjectUses = (node, objects) => {
|
|
158
|
+
if (!Array.isArray(node)) return node
|
|
159
|
+
const op = node[0]
|
|
160
|
+
if ((op === '.' || op === '?.') && objects.has(node[1])) {
|
|
161
|
+
const fields = objects.get(node[1])
|
|
162
|
+
return fields.get(node[2]) ?? [, undefined]
|
|
163
|
+
}
|
|
164
|
+
if (op === '[]' && objects.has(node[1])) {
|
|
165
|
+
const key = staticPropertyKey(node[2])
|
|
166
|
+
const fields = objects.get(node[1])
|
|
167
|
+
return key != null ? (fields.get(key) ?? [, undefined]) : node
|
|
168
|
+
}
|
|
169
|
+
return node.map((part, i) => i === 0 ? part : rewriteScalarObjectUses(part, objects))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const typedArraySlotIndex = (node, len) => {
|
|
173
|
+
const idx = constIntExpr(node)
|
|
174
|
+
return idx != null && idx >= 0 && idx < len ? idx : null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// `coerce` truthy ⇒ the array's element type truncates on store (Int*/Uint* views),
|
|
178
|
+
// so in-place updates (`arr[i]++`, `arr[i] += x`) can't be a plain `slot`-op rewrite —
|
|
179
|
+
// reject them and only scalarize plain `arr[i] = v` writes and `arr[i]` reads.
|
|
180
|
+
const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
|
|
181
|
+
if (typeof node === 'string') return node !== name
|
|
182
|
+
if (!Array.isArray(node)) return true
|
|
183
|
+
const op = node[0]
|
|
184
|
+
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
185
|
+
if (isMemberWriteTarget(op, node, name)) return false
|
|
186
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
187
|
+
if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
|
|
188
|
+
if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
|
|
189
|
+
return !coerce && typedArraySlotIndex(node[1][2], len) != null
|
|
190
|
+
if (ASSIGN_TARGET_OPS.has(op)) {
|
|
191
|
+
if (node[1] === name) return false
|
|
192
|
+
if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
|
|
193
|
+
if (coerce && op !== '=') return false
|
|
194
|
+
if (typedArraySlotIndex(node[1][2], len) == null) return false
|
|
195
|
+
for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (op === '...' && node[1] === name) return false
|
|
200
|
+
for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
const rewriteScalarTypedArrayUses = (node, arrays) => {
|
|
204
|
+
if (!Array.isArray(node)) return node
|
|
205
|
+
const op = node[0]
|
|
206
|
+
const slotFor = (idxNode, entry) => {
|
|
207
|
+
const idx = typedArraySlotIndex(idxNode, entry.len)
|
|
208
|
+
return idx == null ? null : entry.slots[idx]
|
|
209
|
+
}
|
|
210
|
+
if ((op === '.' || op === '?.') && arrays.has(node[1]) && node[2] === 'length') return [null, arrays.get(node[1]).len]
|
|
211
|
+
if (op === '[]' && arrays.has(node[1])) return slotFor(node[2], arrays.get(node[1])) ?? node
|
|
212
|
+
if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
|
|
213
|
+
const slot = slotFor(node[1][2], arrays.get(node[1][1]))
|
|
214
|
+
return slot ? [op, slot] : node
|
|
215
|
+
}
|
|
216
|
+
if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
|
|
217
|
+
const entry = arrays.get(node[1][1])
|
|
218
|
+
const slot = slotFor(node[1][2], entry)
|
|
219
|
+
if (!slot) return node
|
|
220
|
+
const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
|
|
221
|
+
return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
|
|
222
|
+
}
|
|
223
|
+
return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const scalarTypedArrayStores = (name, entry) =>
|
|
227
|
+
entry.slots.map((slot, i) => ['=', ['[]', name, [null, i]], slot])
|
|
228
|
+
|
|
229
|
+
const scalarTypedArrayLoads = (name, entry) =>
|
|
230
|
+
entry.slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])
|
|
231
|
+
|
|
232
|
+
const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
|
|
233
|
+
if (!Array.isArray(node)) return out
|
|
234
|
+
const op = node[0]
|
|
235
|
+
const addSlot = target => {
|
|
236
|
+
if (Array.isArray(target) && target[0] === '[]' && target[1] === name) {
|
|
237
|
+
const idx = typedArraySlotIndex(target[2], len)
|
|
238
|
+
if (idx != null) out.add(idx)
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
if ((op === '++' || op === '--') && addSlot(node[1])) return out
|
|
244
|
+
if (ASSIGN_TARGET_OPS.has(op) && addSlot(node[1])) {
|
|
245
|
+
for (let i = 2; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
if (op !== '=>') for (let i = 1; i < node.length; i++) collectScalarTypedArrayWrites(node[i], name, len, out)
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const hasScalarTypedArrayRead = (node, name) => {
|
|
253
|
+
if (!Array.isArray(node)) return false
|
|
254
|
+
const op = node[0]
|
|
255
|
+
const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
|
|
256
|
+
if ((op === '++' || op === '--') && isTarget(node[1])) return true
|
|
257
|
+
if (ASSIGN_TARGET_OPS.has(op)) {
|
|
258
|
+
if (isTarget(node[1])) {
|
|
259
|
+
if (op !== '=') return true
|
|
260
|
+
for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (op === '[]' && node[1] === name) return true
|
|
265
|
+
if (op === '=>') return false
|
|
266
|
+
for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
271
|
+
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
272
|
+
let changed = false
|
|
273
|
+
const stmts = seq.slice(1).map(stmt => {
|
|
274
|
+
const r = scalarizeTypedArrayLiterals(stmt)
|
|
275
|
+
changed ||= r.changed
|
|
276
|
+
return r.node
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const candidates = new Map()
|
|
280
|
+
const mirrored = new Map()
|
|
281
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
282
|
+
const stmt = stmts[i]
|
|
283
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
284
|
+
const decl = stmt[1]
|
|
285
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
286
|
+
const fixed = fixedScalarTypedArray(decl[2])
|
|
287
|
+
if (fixed == null) continue
|
|
288
|
+
const { len, coerce } = fixed
|
|
289
|
+
let hasSafeUse = false, hasUnsafeUse = false
|
|
290
|
+
for (let j = 0; j < stmts.length; j++) {
|
|
291
|
+
if (j === i) continue
|
|
292
|
+
if (!refsName(stmts[j], decl[1])) continue
|
|
293
|
+
const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
|
|
294
|
+
hasSafeUse ||= safe
|
|
295
|
+
hasUnsafeUse ||= !safe
|
|
296
|
+
}
|
|
297
|
+
if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
|
|
298
|
+
if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
|
|
299
|
+
else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
|
|
300
|
+
}
|
|
301
|
+
if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
302
|
+
|
|
303
|
+
const arrays = new Map()
|
|
304
|
+
for (const [name, c] of [...candidates, ...mirrored]) {
|
|
305
|
+
const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
|
|
306
|
+
arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const out = []
|
|
310
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
311
|
+
const entry = [...candidates.entries()].find(([, c]) => c.index === i) ||
|
|
312
|
+
[...mirrored.entries()].find(([, c]) => c.index === i)
|
|
313
|
+
if (entry) {
|
|
314
|
+
const [name] = entry
|
|
315
|
+
const arr = arrays.get(name)
|
|
316
|
+
const { slots } = arr
|
|
317
|
+
if (arr.mirrored) {
|
|
318
|
+
out.push(stmts[i])
|
|
319
|
+
if (slots.length) out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
|
|
320
|
+
} else if (slots.length) {
|
|
321
|
+
out.push(['let', ...slots.map(slot => ['=', slot, [null, 0]])])
|
|
322
|
+
}
|
|
323
|
+
changed = true
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
const unsafe = []
|
|
327
|
+
for (const [name, arr] of arrays) {
|
|
328
|
+
if (arr.mirrored && refsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
|
|
329
|
+
}
|
|
330
|
+
if (unsafe.length) {
|
|
331
|
+
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
|
|
332
|
+
out.push(stmts[i])
|
|
333
|
+
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
|
|
334
|
+
changed = true
|
|
335
|
+
} else {
|
|
336
|
+
out.push(rewriteScalarTypedArrayUses(stmts[i], arrays))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { node: [';', ...out], changed: true }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function scalarizeTypedArrayLiterals(node) {
|
|
343
|
+
if (!Array.isArray(node)) return { node, changed: false }
|
|
344
|
+
if (node[0] === '=>') return { node, changed: false }
|
|
345
|
+
if (node[0] === ';') return scalarizeTypedArrayLiteralSeq(node)
|
|
346
|
+
let changed = false
|
|
347
|
+
const out = [node[0]]
|
|
348
|
+
for (let i = 1; i < node.length; i++) {
|
|
349
|
+
const r = scalarizeTypedArrayLiterals(node[i])
|
|
350
|
+
changed ||= r.changed
|
|
351
|
+
out.push(r.node)
|
|
352
|
+
}
|
|
353
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const containsTypedArrayAccess = (body, names) => some(body, n => n[0] === '[]' && typeof n[1] === 'string' && names.has(n[1]))
|
|
357
|
+
|
|
358
|
+
function smallScalarTypedForTrip(init, cond, step) {
|
|
359
|
+
const end = smallConstForTripCount(init, cond, step, maxScalarTypedLoopUnroll())
|
|
360
|
+
if (end == null) return null
|
|
361
|
+
const decl = init[1]
|
|
362
|
+
return { name: decl[1], end }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const scalarTypedLoopBudget = (body) => {
|
|
366
|
+
if (!Array.isArray(body) || body[0] === '=>') return 1
|
|
367
|
+
if (body[0] === 'for') {
|
|
368
|
+
const trip = smallScalarTypedForTrip(body[1], body[2], body[3])
|
|
369
|
+
return trip ? trip.end * scalarTypedLoopBudget(body[4]) : 1
|
|
370
|
+
}
|
|
371
|
+
let max = 1
|
|
372
|
+
for (let i = 1; i < body.length; i++) max = Math.max(max, scalarTypedLoopBudget(body[i]))
|
|
373
|
+
return max
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const unrollTypedArrayLoops = (node, names) => {
|
|
377
|
+
if (!Array.isArray(node) || node[0] === '=>') return { node, changed: false }
|
|
378
|
+
if (node[0] === ';') {
|
|
379
|
+
let changed = false
|
|
380
|
+
const out = [';']
|
|
381
|
+
for (const stmt of node.slice(1)) {
|
|
382
|
+
const r = unrollTypedArrayLoops(stmt, names)
|
|
383
|
+
changed ||= r.changed
|
|
384
|
+
if (Array.isArray(r.node) && r.node[0] === ';') out.push(...r.node.slice(1))
|
|
385
|
+
else out.push(r.node)
|
|
386
|
+
}
|
|
387
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
388
|
+
}
|
|
389
|
+
if (node[0] === '{}') {
|
|
390
|
+
const r = unrollTypedArrayLoops(node[1], names)
|
|
391
|
+
return r.changed ? { node: ['{}', r.node], changed: true } : { node, changed: false }
|
|
392
|
+
}
|
|
393
|
+
if (node[0] === 'for') {
|
|
394
|
+
const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
|
|
395
|
+
if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
|
|
396
|
+
!hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
|
|
397
|
+
const out = [';']
|
|
398
|
+
for (let i = 0; i < trip.end; i++) {
|
|
399
|
+
const cloned = cloneWithSubst(node[4], new Map([[trip.name, [null, i]]]), new Map())
|
|
400
|
+
const r = unrollTypedArrayLoops(cloned, names)
|
|
401
|
+
out.push(...stmtList(r.node))
|
|
402
|
+
}
|
|
403
|
+
return { node: out, changed: true }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
let changed = false
|
|
407
|
+
const out = [node[0]]
|
|
408
|
+
for (let i = 1; i < node.length; i++) {
|
|
409
|
+
const r = unrollTypedArrayLoops(node[i], names)
|
|
410
|
+
changed ||= r.changed
|
|
411
|
+
out.push(r.node)
|
|
412
|
+
}
|
|
413
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
|
|
417
|
+
if (!sites?.length || func.exported || func.raw || !func.body || !Array.isArray(func.body) || func.body[0] !== '{}') return new Map()
|
|
418
|
+
if (some(func.body, n => n[0] === 'return' || n[0] === 'throw')) return new Map()
|
|
419
|
+
const params = func.sig?.params || []
|
|
420
|
+
const cands = new Map()
|
|
421
|
+
for (let i = 0; i < params.length; i++) {
|
|
422
|
+
const pname = params[i].name
|
|
423
|
+
let len = null, coerce = null, ok = true
|
|
424
|
+
for (const site of sites) {
|
|
425
|
+
const arg = site.argList[i]
|
|
426
|
+
const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
|
|
427
|
+
if (!fixed) { ok = false; break }
|
|
428
|
+
if (len == null) { len = fixed.len; coerce = fixed.coerce }
|
|
429
|
+
else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
|
|
430
|
+
}
|
|
431
|
+
if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
|
|
432
|
+
}
|
|
433
|
+
if (!cands.size) return cands
|
|
434
|
+
for (const site of sites) {
|
|
435
|
+
const seen = new Set()
|
|
436
|
+
for (let i = 0; i < params.length; i++) {
|
|
437
|
+
if (!cands.has(params[i].name)) continue
|
|
438
|
+
const arg = site.argList[i]
|
|
439
|
+
if (typeof arg !== 'string' || seen.has(arg)) return new Map()
|
|
440
|
+
seen.add(arg)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return cands
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const scalarizeTypedArrayParams = (func, paramCands) => {
|
|
447
|
+
for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
|
|
448
|
+
for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
|
|
449
|
+
if (!paramCands.size) return { body: func.body, changed: false }
|
|
450
|
+
const arrays = new Map()
|
|
451
|
+
for (const [name, c] of paramCands) {
|
|
452
|
+
arrays.set(name, {
|
|
453
|
+
len: c.len,
|
|
454
|
+
coerce: c.coerce,
|
|
455
|
+
slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
const prologue = []
|
|
459
|
+
const writeback = []
|
|
460
|
+
for (const [name, { len, slots }] of arrays) {
|
|
461
|
+
if (slots.length) prologue.push(['let', ...slots.map((slot, i) => ['=', slot, ['[]', name, [null, i]]])])
|
|
462
|
+
for (const i of collectScalarTypedArrayWrites(func.body, name, len)) writeback.push(['=', ['[]', name, [null, i]], slots[i]])
|
|
463
|
+
}
|
|
464
|
+
const rewritten = stmtList(func.body).map(stmt => rewriteScalarTypedArrayUses(stmt, arrays))
|
|
465
|
+
return { body: ['{}', [';', ...prologue, ...rewritten, ...writeback]], changed: true }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export const scalarizeFunctionTypedArrays = (programFacts) => {
|
|
469
|
+
const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
|
|
470
|
+
const sitesByCallee = new Map()
|
|
471
|
+
for (const site of programFacts.callSites) {
|
|
472
|
+
if (!site.callerFunc) continue
|
|
473
|
+
const list = sitesByCallee.get(site.callee)
|
|
474
|
+
if (list) list.push(site); else sitesByCallee.set(site.callee, [site])
|
|
475
|
+
}
|
|
476
|
+
let changed = false
|
|
477
|
+
for (const func of ctx.func.list) {
|
|
478
|
+
if (!func.body || func.raw) continue
|
|
479
|
+
const paramCands = scalarTypedParamCandidates(func, sitesByCallee.get(func.name), fixedByFunc)
|
|
480
|
+
const names = new Set([...paramCands.keys(), ...fixedByFunc.get(func).keys()])
|
|
481
|
+
if (names.size) {
|
|
482
|
+
let guard = 0
|
|
483
|
+
while (guard++ < 6) {
|
|
484
|
+
const r = unrollTypedArrayLoops(func.body, names)
|
|
485
|
+
if (!r.changed) break
|
|
486
|
+
func.body = r.node
|
|
487
|
+
changed = true
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const p = scalarizeTypedArrayParams(func, paramCands)
|
|
491
|
+
if (p.changed) { func.body = p.body; changed = true }
|
|
492
|
+
const l = scalarizeTypedArrayLiterals(func.body)
|
|
493
|
+
if (l.changed) { func.body = l.node; changed = true }
|
|
494
|
+
if (changed) invalidateLocalsCache(func.body)
|
|
495
|
+
}
|
|
496
|
+
return changed
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const scalarizeArrayLiteralSeq = (seq) => {
|
|
500
|
+
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
501
|
+
let changed = false
|
|
502
|
+
const stmts = seq.slice(1).map(stmt => {
|
|
503
|
+
const r = scalarizeArrayLiterals(stmt)
|
|
504
|
+
changed ||= r.changed
|
|
505
|
+
return r.node
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const candidates = new Map()
|
|
509
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
510
|
+
const stmt = stmts[i]
|
|
511
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
512
|
+
const decl = stmt[1]
|
|
513
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
514
|
+
const elems = scalarArrayElems(decl[2])
|
|
515
|
+
if (!elems) continue
|
|
516
|
+
let ok = true
|
|
517
|
+
for (let j = 0; j < stmts.length && ok; j++) {
|
|
518
|
+
if (j === i) continue
|
|
519
|
+
ok = safeScalarArrayUse(stmts[j], decl[1], elems.length)
|
|
520
|
+
}
|
|
521
|
+
if (!ok) continue
|
|
522
|
+
candidates.set(decl[1], { index: i, op: stmt[0], elems })
|
|
523
|
+
}
|
|
524
|
+
if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
525
|
+
|
|
526
|
+
const arrays = new Map()
|
|
527
|
+
for (const [name, c] of candidates) {
|
|
528
|
+
const temps = c.elems.map((_, k) => `${name}${T}arr${ctx.func.uniq++}_${k}`)
|
|
529
|
+
arrays.set(name, temps)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const out = []
|
|
533
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
534
|
+
const entry = [...candidates.entries()].find(([, c]) => c.index === i)
|
|
535
|
+
if (entry) {
|
|
536
|
+
const [name, c] = entry
|
|
537
|
+
const temps = arrays.get(name)
|
|
538
|
+
if (temps.length) {
|
|
539
|
+
out.push([c.op, ...temps.map((tmp, k) =>
|
|
540
|
+
['=', tmp, rewriteScalarArrayUses(c.elems[k], arrays)])])
|
|
541
|
+
}
|
|
542
|
+
changed = true
|
|
543
|
+
continue
|
|
544
|
+
}
|
|
545
|
+
out.push(rewriteScalarArrayUses(stmts[i], arrays))
|
|
546
|
+
}
|
|
547
|
+
return { node: [';', ...out], changed: true }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const scalarizeObjectLiteralSeq = (seq, escapes) => {
|
|
551
|
+
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
552
|
+
let changed = false
|
|
553
|
+
const stmts = seq.slice(1).map(stmt => {
|
|
554
|
+
const r = scalarizeObjectLiterals(stmt, escapes)
|
|
555
|
+
changed ||= r.changed
|
|
556
|
+
return r.node
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const candidates = new Map()
|
|
560
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
561
|
+
const stmt = stmts[i]
|
|
562
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
|
|
563
|
+
const decl = stmt[1]
|
|
564
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
565
|
+
if (escapes.get(decl[1]) !== false) continue
|
|
566
|
+
const props = scalarObjectProps(decl[2])
|
|
567
|
+
if (!props) continue
|
|
568
|
+
const keys = new Set(props.names)
|
|
569
|
+
let ok = true
|
|
570
|
+
for (let j = 0; j < stmts.length && ok; j++) {
|
|
571
|
+
if (j === i) continue
|
|
572
|
+
ok = safeScalarObjectUse(stmts[j], decl[1], keys)
|
|
573
|
+
}
|
|
574
|
+
if (!ok) continue
|
|
575
|
+
candidates.set(decl[1], { index: i, op: stmt[0], props })
|
|
576
|
+
}
|
|
577
|
+
if (!candidates.size) return { node: changed ? [';', ...stmts] : seq, changed }
|
|
578
|
+
|
|
579
|
+
const objects = new Map()
|
|
580
|
+
for (const [name, c] of candidates) {
|
|
581
|
+
const fields = new Map()
|
|
582
|
+
for (let i = 0; i < c.props.names.length; i++) {
|
|
583
|
+
fields.set(c.props.names[i], `${name}${T}obj${ctx.func.uniq++}_${i}`)
|
|
584
|
+
}
|
|
585
|
+
objects.set(name, fields)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const out = []
|
|
589
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
590
|
+
const entry = [...candidates.entries()].find(([, c]) => c.index === i)
|
|
591
|
+
if (entry) {
|
|
592
|
+
const [, c] = entry
|
|
593
|
+
const fields = objects.get(entry[0])
|
|
594
|
+
if (c.props.names.length) {
|
|
595
|
+
out.push([c.op, ...c.props.names.map((prop, k) =>
|
|
596
|
+
['=', fields.get(prop), rewriteScalarObjectUses(c.props.values[k], objects)])])
|
|
597
|
+
}
|
|
598
|
+
changed = true
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
out.push(rewriteScalarObjectUses(stmts[i], objects))
|
|
602
|
+
}
|
|
603
|
+
return { node: [';', ...out], changed: true }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function scalarizeObjectLiterals(node, escapes) {
|
|
607
|
+
if (!Array.isArray(node)) return { node, changed: false }
|
|
608
|
+
if (node[0] === '=>') {
|
|
609
|
+
const r = scalarizeObjectLiterals(node[2], escapes)
|
|
610
|
+
if (!r.changed) return { node, changed: false }
|
|
611
|
+
return { node: [node[0], node[1], r.node], changed: true }
|
|
612
|
+
}
|
|
613
|
+
if (node[0] === ';') return scalarizeObjectLiteralSeq(node, escapes)
|
|
614
|
+
let changed = false
|
|
615
|
+
const out = [node[0]]
|
|
616
|
+
for (let i = 1; i < node.length; i++) {
|
|
617
|
+
const r = scalarizeObjectLiterals(node[i], escapes)
|
|
618
|
+
changed ||= r.changed
|
|
619
|
+
out.push(r.node)
|
|
620
|
+
}
|
|
621
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function scalarizeArrayLiterals(node) {
|
|
625
|
+
if (!Array.isArray(node)) return { node, changed: false }
|
|
626
|
+
if (node[0] === '=>') {
|
|
627
|
+
const r = scalarizeArrayLiterals(node[2])
|
|
628
|
+
if (!r.changed) return { node, changed: false }
|
|
629
|
+
return { node: [node[0], node[1], r.node], changed: true }
|
|
630
|
+
}
|
|
631
|
+
if (node[0] === ';') return scalarizeArrayLiteralSeq(node)
|
|
632
|
+
let changed = false
|
|
633
|
+
const out = [node[0]]
|
|
634
|
+
for (let i = 1; i < node.length; i++) {
|
|
635
|
+
const r = scalarizeArrayLiterals(node[i])
|
|
636
|
+
changed ||= r.changed
|
|
637
|
+
out.push(r.node)
|
|
638
|
+
}
|
|
639
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export const scalarizeFunctionArrayLiterals = () => {
|
|
643
|
+
let changed = false
|
|
644
|
+
for (const func of ctx.func.list) {
|
|
645
|
+
if (!func.body || func.raw) continue
|
|
646
|
+
let guard = 0
|
|
647
|
+
while (guard++ < 4) {
|
|
648
|
+
const r = scalarizeArrayLiterals(func.body)
|
|
649
|
+
if (!r.changed) break
|
|
650
|
+
func.body = r.node
|
|
651
|
+
changed = true
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return changed
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
// === Int-array → Int32Array auto-promotion =================================
|
|
660
|
+
//
|
|
661
|
+
// A `let xs = [intLit, intLit, …]` binding whose every use is TYPED-compatible
|
|
662
|
+
// is rewritten to `let xs = new Int32Array([intLit, …])`. Downstream analysis
|
|
663
|
+
// then takes over: valTypeOf → VAL.TYPED, methods dispatch through `.typed:`,
|
|
664
|
+
// loops get auto-vectorized via i32x4 (SIMD pass), and the carrier shrinks
|
|
665
|
+
// from 8-byte f64 slots to packed i32. The promotion runs after literal
|
|
666
|
+
// scalarization (so arrays fully scalarized away are already gone) and before
|
|
667
|
+
// typed-array param scalarization (so a freshly-promoted array can still
|
|
668
|
+
// participate in subsequent loop unrolling).
|
|
669
|
+
//
|
|
670
|
+
// Safety: the binding must never appear in a pattern that TYPED can't honor.
|
|
671
|
+
// Disqualifiers (each fires per binding name):
|
|
672
|
+
// 1. reassignment `xs = …` / compound `xs +=` / `++xs` / `--xs` — TYPED has
|
|
673
|
+
// no value-replacement op; `xs = new TypedArray(…)` would also drop the
|
|
674
|
+
// promoted view's identity.
|
|
675
|
+
// 2. element write `xs[k] = v` — TYPED's i32-trunc store would lose
|
|
676
|
+
// fractional/NaN bits that VAL.ARRAY would have preserved.
|
|
677
|
+
// 3. method calls outside the read-safe whitelist (push, pop, shift, …) —
|
|
678
|
+
// TYPED arrays are fixed-length; mutators don't exist on the carrier.
|
|
679
|
+
// 4. `Array.isArray(xs)` — semantics flip true→false on promotion.
|
|
680
|
+
// 5. `…xs` spread / `xs` as call arg / `xs` as return / bare reference in
|
|
681
|
+
// any other position — escape; callee may rely on ARRAY layout.
|
|
682
|
+
// 6. captured by a closure / shadowed by an inner decl — same escape
|
|
683
|
+
// reasoning, plus the inner decl could rebind to a non-array.
|
|
684
|
+
//
|
|
685
|
+
// Elements at init must all be i32-range integer literals. A negative literal
|
|
686
|
+
// arrives as `[null, -n]` after prepare's constant folding; intLiteralValue
|
|
687
|
+
// recognizes that form. Float literals (`[1, 2.5]`) and out-of-range ints
|
|
688
|
+
// (`0x80000000` on the +ve side, etc.) disqualify the array as a whole.
|
|
689
|
+
|
|
690
|
+
// Methods we promote across. The bar: every entry must have a real `.typed:*`
|
|
691
|
+
// emitter in module/typedarray.js. Receiver-flow into chained methods is also
|
|
692
|
+
// typed-aware now — `.typed:map`/`.typed:filter`/`.typed:slice` all return
|
|
693
|
+
// TYPED carriers, and downstream `.filter`/`.slice`/`.map`/etc. on those
|
|
694
|
+
// re-dispatch via emit.js:2211's `.typed:<m>` lookup (VAL.TYPED ⇒ typed
|
|
695
|
+
// emitter). Methods missing here (.join, .sort, .reverse, .subarray, .fill,
|
|
696
|
+
// .toString, .copyWithin, …) lack a typed emitter — disqualify the candidate.
|
|
697
|
+
const _TYPED_SAFE_METHODS = new Set([
|
|
698
|
+
'set',
|
|
699
|
+
'map', 'filter', 'slice',
|
|
700
|
+
'forEach', 'reduce',
|
|
701
|
+
'indexOf', 'includes', 'find', 'findIndex', 'some', 'every',
|
|
702
|
+
])
|
|
703
|
+
|
|
704
|
+
// `.length` is TYPED-aware via core.js:__len (shifts the byte header by
|
|
705
|
+
// __typed_shift on TAG=3). `.byteLength`/`.byteOffset`/`.buffer` are
|
|
706
|
+
// TYPED-only — a user reading those already expects a typed array, so a
|
|
707
|
+
// promotion candidate that hits them is a coincidence we'd rather not
|
|
708
|
+
// rely on; disqualify and let them write the TypedArray construction
|
|
709
|
+
// themselves.
|
|
710
|
+
const _TYPED_SAFE_PROPS = new Set(['length'])
|
|
711
|
+
|
|
712
|
+
// Returns the i32-range integer payload of an array-literal element, or null
|
|
713
|
+
// if the element isn't a literal integer that fits in i32. Mirrors the shape
|
|
714
|
+
// check used by `intLiteralValue` but without the rep-lookup (we're pre-
|
|
715
|
+
// analysis here, and want a pure syntactic gate).
|
|
716
|
+
const _intArrayLitElems = (expr) => {
|
|
717
|
+
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
718
|
+
if (expr.length < 2) return null // empty literal — low value, skip
|
|
719
|
+
const out = []
|
|
720
|
+
for (let i = 1; i < expr.length; i++) {
|
|
721
|
+
const v = intLiteralValue(expr[i])
|
|
722
|
+
if (v == null) return null
|
|
723
|
+
out.push(v)
|
|
724
|
+
}
|
|
725
|
+
return out
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Walks `node` and disqualifies every candidate name that appears in an
|
|
729
|
+
// unsafe context. `initSet` holds the candidate's own init-decl AST nodes
|
|
730
|
+
// (their LHS reference is the binding being defined, not an escape).
|
|
731
|
+
const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
732
|
+
if (initSet.has(node)) {
|
|
733
|
+
// The init decl itself: only walk the RHS (skip the LHS `name`).
|
|
734
|
+
return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
735
|
+
}
|
|
736
|
+
if (typeof node === 'string') {
|
|
737
|
+
// Bare identifier outside any handled parent context — escape.
|
|
738
|
+
if (candidates.has(node)) disqualified.add(node)
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
if (!Array.isArray(node)) return
|
|
742
|
+
const op = node[0]
|
|
743
|
+
|
|
744
|
+
// Closure body — any candidate referenced inside is captured. Bail without
|
|
745
|
+
// recursing further (we'd otherwise hit the bare-name leaf and disqualify
|
|
746
|
+
// anyway, but this is explicit and avoids walking the inner closure).
|
|
747
|
+
if (op === '=>') {
|
|
748
|
+
for (const n of candidates.keys()) {
|
|
749
|
+
if (!disqualified.has(n) && refsName(node, n, { skipArrow: false })) disqualified.add(n)
|
|
750
|
+
}
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Member write target `name.length = n` / `++name.length` — resize is an
|
|
755
|
+
// ARRAY-only op (TYPED is fixed-size); disqualify.
|
|
756
|
+
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') &&
|
|
757
|
+
Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') &&
|
|
758
|
+
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
759
|
+
disqualified.add(node[1][1])
|
|
760
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Property access `name.prop` / `name?.prop`. Bare property reads only —
|
|
765
|
+
// method calls reach here via the `()` handler below (which intercepts
|
|
766
|
+
// before recursing into its callee).
|
|
767
|
+
if ((op === '.' || op === '?.') && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
768
|
+
if (!_TYPED_SAFE_PROPS.has(node[2])) disqualified.add(node[1])
|
|
769
|
+
return // node[2] is the property name (string), not an expression — done
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Method or function call. Two shapes carry the candidate:
|
|
773
|
+
// `name.method(args)` — receiver at node[1][1]; method whitelist gates.
|
|
774
|
+
// `f(…, name, …)` / `Array.isArray(name)` — name appears as a plain arg.
|
|
775
|
+
if (op === '()') {
|
|
776
|
+
const callee = node[1]
|
|
777
|
+
// Method call on a candidate receiver.
|
|
778
|
+
if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
|
|
779
|
+
typeof callee[1] === 'string' && candidates.has(callee[1])) {
|
|
780
|
+
if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
|
|
781
|
+
// Walk method args (skip the receiver — already validated above).
|
|
782
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
// Array.isArray flips true→false under promotion.
|
|
786
|
+
if (callee === 'Array.isArray') {
|
|
787
|
+
const raw = node[2]
|
|
788
|
+
const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
|
|
789
|
+
for (const a of list) {
|
|
790
|
+
if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
|
|
791
|
+
else _disqualifyPromotion(a, candidates, disqualified, initSet)
|
|
792
|
+
}
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
// Fall through to generic recursion — `name` as a plain arg will hit the
|
|
796
|
+
// bare-name leaf above and disqualify.
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Index read `name[k]` — read access is TYPED-safe. Walk the key in case
|
|
800
|
+
// it contains references to other candidate names.
|
|
801
|
+
if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
802
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Element write: `['=', ['[]', name, k], v]` (and compound forms).
|
|
807
|
+
// V1 stays read-only after init — element writes would silently truncate.
|
|
808
|
+
if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
|
|
809
|
+
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
810
|
+
disqualified.add(node[1][1])
|
|
811
|
+
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
|
|
812
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Whole-binding reassign: `name = …` / `name += …` / etc.
|
|
817
|
+
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
818
|
+
disqualified.add(node[1])
|
|
819
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Pre/post-increment on var or element.
|
|
824
|
+
if (op === '++' || op === '--') {
|
|
825
|
+
const t = node[1]
|
|
826
|
+
if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
|
|
827
|
+
if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
|
|
828
|
+
disqualified.add(t[1])
|
|
829
|
+
_disqualifyPromotion(t[2], candidates, disqualified, initSet)
|
|
830
|
+
return
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// `let`/`const` declaration — the candidate's own init lands here via the
|
|
835
|
+
// initSet branch at the top. Any *other* decl that names a candidate is a
|
|
836
|
+
// shadow (impossible after jz hoists, but defensive) and disqualifies.
|
|
837
|
+
if (op === 'let' || op === 'const') {
|
|
838
|
+
for (let i = 1; i < node.length; i++) {
|
|
839
|
+
const d = node[i]
|
|
840
|
+
if (typeof d === 'string' && candidates.has(d)) disqualified.add(d)
|
|
841
|
+
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
|
|
842
|
+
candidates.has(d[1]) && !initSet.has(d)) {
|
|
843
|
+
disqualified.add(d[1])
|
|
844
|
+
_disqualifyPromotion(d[2], candidates, disqualified, initSet)
|
|
845
|
+
} else {
|
|
846
|
+
_disqualifyPromotion(d, candidates, disqualified, initSet)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Spread `…name` — could be in a call, a `[`, or a destructure target.
|
|
853
|
+
if (op === '...' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
854
|
+
disqualified.add(node[1])
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// for-of / for-in iteration: receiver position is `node[2]` (a bare name
|
|
859
|
+
// there would otherwise trigger escape). TYPED supports iteration, so
|
|
860
|
+
// allow the receiver but walk the body for other refs.
|
|
861
|
+
if (op === 'for-of' || op === 'for-in') {
|
|
862
|
+
// Walk decl (node[1]), iter (node[2]), body (node[3]); receiver as bare
|
|
863
|
+
// name is fine — only the body matters for further refs to the same name
|
|
864
|
+
// (but body refs would shadow or escape, which other rules catch).
|
|
865
|
+
for (let i = 1; i < node.length; i++) {
|
|
866
|
+
const child = node[i]
|
|
867
|
+
if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
|
|
868
|
+
_disqualifyPromotion(child, candidates, disqualified, initSet)
|
|
869
|
+
}
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Generic — recurse into children. Bare-name refs at unhandled positions
|
|
874
|
+
// hit the string-leaf branch above and disqualify on contact.
|
|
875
|
+
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
|
|
879
|
+
// carries the exact init-decl AST node so the disqualifier can skip the
|
|
880
|
+
// binding's own LHS reference (which would otherwise look like a reassign).
|
|
881
|
+
const _collectIntArrayCandidates = (node, candidates) => {
|
|
882
|
+
if (!Array.isArray(node)) return
|
|
883
|
+
const op = node[0]
|
|
884
|
+
if (op === '=>') return
|
|
885
|
+
if (op === 'let' || op === 'const') {
|
|
886
|
+
for (let i = 1; i < node.length; i++) {
|
|
887
|
+
const d = node[i]
|
|
888
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
889
|
+
const elems = _intArrayLitElems(d[2])
|
|
890
|
+
if (elems == null) continue
|
|
891
|
+
// jz hoists `let` to function scope — duplicate candidate-name collisions
|
|
892
|
+
// shouldn't happen, but if they do the second wins (disqualifyPromotion
|
|
893
|
+
// will mark both via the shadow rule).
|
|
894
|
+
candidates.set(d[1], { initDecl: d, elems })
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
for (let i = 1; i < node.length; i++) _collectIntArrayCandidates(node[i], candidates)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Rewrite `let name = [...]` → `let name = new Int32Array([...])` for every
|
|
901
|
+
// validated candidate. Preserves the original element AST nodes so the
|
|
902
|
+
// downstream `Int32Array.from` lowering picks up its existing static-data
|
|
903
|
+
// segment / per-element-store fast paths.
|
|
904
|
+
const _rewritePromoted = (node, validated, initSet) => {
|
|
905
|
+
if (!Array.isArray(node)) return { node, changed: false }
|
|
906
|
+
const op = node[0]
|
|
907
|
+
if (op === '=>') {
|
|
908
|
+
// Closures don't reach validated bindings (we disqualified on capture).
|
|
909
|
+
// Still recurse — a nested closure may itself hold a promotable.
|
|
910
|
+
let changed = false
|
|
911
|
+
const out = [op]
|
|
912
|
+
for (let i = 1; i < node.length; i++) {
|
|
913
|
+
const r = _rewritePromoted(node[i], validated, initSet)
|
|
914
|
+
if (r.changed) changed = true
|
|
915
|
+
out.push(r.node)
|
|
916
|
+
}
|
|
917
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
918
|
+
}
|
|
919
|
+
let changed = false
|
|
920
|
+
const out = [op]
|
|
921
|
+
for (let i = 1; i < node.length; i++) {
|
|
922
|
+
const child = node[i]
|
|
923
|
+
if ((op === 'let' || op === 'const') && Array.isArray(child) && child[0] === '=' &&
|
|
924
|
+
typeof child[1] === 'string' && validated.has(child[1]) && initSet.has(child)) {
|
|
925
|
+
const newRhs = ['()', 'new.Int32Array', child[2]]
|
|
926
|
+
out.push(['=', child[1], newRhs])
|
|
927
|
+
changed = true
|
|
928
|
+
continue
|
|
929
|
+
}
|
|
930
|
+
const r = _rewritePromoted(child, validated, initSet)
|
|
931
|
+
if (r.changed) changed = true
|
|
932
|
+
out.push(r.node)
|
|
933
|
+
}
|
|
934
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const promoteIntArrayLiteralsInBody = (body) => {
|
|
938
|
+
const candidates = new Map()
|
|
939
|
+
_collectIntArrayCandidates(body, candidates)
|
|
940
|
+
if (!candidates.size) return { node: body, changed: false }
|
|
941
|
+
const initSet = new Set()
|
|
942
|
+
for (const { initDecl } of candidates.values()) initSet.add(initDecl)
|
|
943
|
+
const disqualified = new Set()
|
|
944
|
+
_disqualifyPromotion(body, candidates, disqualified, initSet)
|
|
945
|
+
const validated = new Set()
|
|
946
|
+
for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
|
|
947
|
+
if (!validated.size) return { node: body, changed: false }
|
|
948
|
+
return _rewritePromoted(body, validated, initSet)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// On the first successful promotion, pull in the typedarray module so the
|
|
952
|
+
// emitted `new.Int32Array` callee has a registered emitter. Calling
|
|
953
|
+
// `includeModule('typedarray')` on a no-op (already loaded) is cheap.
|
|
954
|
+
export const promoteIntArrayLiterals = () => {
|
|
955
|
+
let changed = false
|
|
956
|
+
for (const func of ctx.func.list) {
|
|
957
|
+
if (!func.body || func.raw) continue
|
|
958
|
+
const r = promoteIntArrayLiteralsInBody(func.body)
|
|
959
|
+
if (r.changed) {
|
|
960
|
+
func.body = r.node
|
|
961
|
+
invalidateLocalsCache(func.body)
|
|
962
|
+
if (!changed) includeModule('typedarray')
|
|
963
|
+
changed = true
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return changed
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
export const scalarizeFunctionObjectLiterals = () => {
|
|
970
|
+
let changed = false
|
|
971
|
+
for (const func of ctx.func.list) {
|
|
972
|
+
if (!func.body || func.raw) continue
|
|
973
|
+
let guard = 0
|
|
974
|
+
while (guard++ < 4) {
|
|
975
|
+
const escapes = new Map(analyzeBody(func.body).escapes)
|
|
976
|
+
invalidateLocalsCache(func.body)
|
|
977
|
+
const r = scalarizeObjectLiterals(func.body, escapes)
|
|
978
|
+
if (!r.changed) break
|
|
979
|
+
func.body = r.node
|
|
980
|
+
changed = true
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return changed
|
|
984
|
+
}
|