jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body scan passes — free vars, mutations, binding-use taxonomy, SRoA/slice eligibility.
|
|
3
|
+
* @module analyze-scans
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ASSIGN_OPS, collectParamNames, extractParams, REFS_IN_EXPR, refsName, T, isLiteralStr } from '../ast.js'
|
|
7
|
+
import { ctx } from '../ctx.js'
|
|
8
|
+
import { staticObjectProps, staticArrayElems, staticIndexKey, staticValue, NO_VALUE } from '../static.js'
|
|
9
|
+
import { exprType } from '../type.js'
|
|
10
|
+
|
|
11
|
+
export function findFreeVars(node, bound, free, scope) {
|
|
12
|
+
if (node == null) return
|
|
13
|
+
if (typeof node === 'string') {
|
|
14
|
+
if (bound.has(node) || free.includes(node)) return
|
|
15
|
+
const inScope = scope
|
|
16
|
+
? scope.has(node)
|
|
17
|
+
: (ctx.func.locals?.has(node) || ctx.func.current?.params.some(p => p.name === node))
|
|
18
|
+
if (inScope) free.push(node)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(node)) return
|
|
22
|
+
const [op, ...args] = node
|
|
23
|
+
if (op === '=>') {
|
|
24
|
+
const innerBound = collectParamNames(extractParams(args[0]), new Set(bound))
|
|
25
|
+
findFreeVars(args[1], innerBound, free, scope)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (op === 'catch') {
|
|
29
|
+
findFreeVars(args[0], bound, free, scope)
|
|
30
|
+
const errName = args[1]
|
|
31
|
+
const handlerBound = typeof errName === 'string' && errName
|
|
32
|
+
? new Set(bound).add(errName) : bound
|
|
33
|
+
findFreeVars(args[2], handlerBound, free, scope)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (op === 'let' || op === 'const') {
|
|
37
|
+
collectParamNames(args, bound)
|
|
38
|
+
if (scope) collectParamNames(args, scope)
|
|
39
|
+
}
|
|
40
|
+
if (op === 'for' && Array.isArray(args[0]) && (args[0][0] === 'let' || args[0][0] === 'const')) {
|
|
41
|
+
collectParamNames(args[0].slice(1), bound)
|
|
42
|
+
if (scope) collectParamNames(args[0].slice(1), scope)
|
|
43
|
+
}
|
|
44
|
+
for (const a of args) findFreeVars(a, bound, free, scope)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Check if any of the given variable names are assigned anywhere in the AST. */
|
|
48
|
+
export function findMutations(node, names, mutated) {
|
|
49
|
+
if (node == null || typeof node !== 'object' || !Array.isArray(node)) return
|
|
50
|
+
const [op, ...args] = node
|
|
51
|
+
if (op === 'let' || op === 'const') {
|
|
52
|
+
for (const decl of args)
|
|
53
|
+
if (Array.isArray(decl) && decl[0] === '=') findMutations(decl[2], names, mutated)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (ASSIGN_OPS.has(op) && typeof args[0] === 'string' && names.has(args[0]))
|
|
57
|
+
mutated.add(args[0])
|
|
58
|
+
if ((op === '++' || op === '--') && typeof args[0] === 'string' && names.has(args[0]))
|
|
59
|
+
mutated.add(args[0])
|
|
60
|
+
for (const a of args) findMutations(a, names, mutated)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pre-scan function body for captured variables that are mutated.
|
|
65
|
+
* Marks mutably-captured vars in ctx.func.boxed for cell-based capture.
|
|
66
|
+
*/
|
|
67
|
+
export function boxedCaptures(body) {
|
|
68
|
+
const outerScope = new Set()
|
|
69
|
+
;(function collectDecls(node) {
|
|
70
|
+
if (!Array.isArray(node)) return
|
|
71
|
+
const [op, ...args] = node
|
|
72
|
+
if (op === '=>') return
|
|
73
|
+
if (op === 'let' || op === 'const')
|
|
74
|
+
collectParamNames(args, outerScope)
|
|
75
|
+
for (const a of args) collectDecls(a)
|
|
76
|
+
})(body)
|
|
77
|
+
if (ctx.func.current?.params) for (const p of ctx.func.current.params) outerScope.add(p.name)
|
|
78
|
+
if (ctx.func.locals) for (const k of ctx.func.locals.keys()) outerScope.add(k)
|
|
79
|
+
|
|
80
|
+
const markArrowCaptures = (node, assignTarget, seen) => {
|
|
81
|
+
const pnode = node[1]
|
|
82
|
+
let p = pnode
|
|
83
|
+
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
84
|
+
const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
85
|
+
const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
|
|
86
|
+
const captures = []
|
|
87
|
+
findFreeVars(node[2], paramSet, captures, outerScope)
|
|
88
|
+
if (captures.length === 0) return
|
|
89
|
+
const captureSet = new Set(captures)
|
|
90
|
+
const boxed = new Set()
|
|
91
|
+
findMutations(body, captureSet, boxed)
|
|
92
|
+
for (const v of captures) if (!seen.has(v)) boxed.add(v)
|
|
93
|
+
if (assignTarget && captureSet.has(assignTarget)) boxed.add(assignTarget)
|
|
94
|
+
for (const v of boxed) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
;(function walk(node, assignTarget, seen = new Set(ctx.func.current?.params?.map(p => p.name) || [])) {
|
|
98
|
+
if (!Array.isArray(node)) return
|
|
99
|
+
const [op, ...args] = node
|
|
100
|
+
if (op === '=>') {
|
|
101
|
+
markArrowCaptures(node, assignTarget, seen)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (op === ';' || op === '{}') {
|
|
106
|
+
const blockSeen = new Set(seen)
|
|
107
|
+
for (const a of args) walk(a, null, blockSeen)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (op === 'let' || op === 'const') {
|
|
112
|
+
for (const decl of args) {
|
|
113
|
+
if (Array.isArray(decl) && decl[0] === '=') walk(decl[2], typeof decl[1] === 'string' ? decl[1] : null, seen)
|
|
114
|
+
else walk(decl, null, seen)
|
|
115
|
+
collectParamNames([decl], seen)
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (op === '=' && typeof args[0] === 'string' && Array.isArray(args[1]) && args[1][0] === '=>')
|
|
121
|
+
return walk(args[1], args[0], seen)
|
|
122
|
+
for (const a of args) walk(a, null, seen)
|
|
123
|
+
})(body)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Narrow return arr-elem-{schema|valType}: for each non-exported, non-value-used
|
|
128
|
+
* user func with `valResult === VAL.ARRAY` and `func[field] == null`, walk return
|
|
129
|
+
* exprs (and trailing-fallthrough literal), resolve each via body-local elem map
|
|
130
|
+
* + caller-param facts + transitive user-fn results, and if all agree set `func[field]`.
|
|
131
|
+
* Lets callers' `const rows = initRows()` gain the elem fact, propagating to
|
|
132
|
+
* runKernel params via paramReps. `field` selects which fact ('arrayElemSchema'
|
|
133
|
+
* | 'arrayElemValType') — slice key is derived.
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
// === body walks / program facts ===
|
|
137
|
+
|
|
138
|
+
export const USE = {
|
|
139
|
+
MEMBER_R: 1, // receiver of a `.`/`?.`/`[]` READ — {key, optional, computed}
|
|
140
|
+
MEMBER_W: 2, // base of a `.`/`[]` WRITE — {key, computed, compound}
|
|
141
|
+
REASSIGN: 3, // `=`(non-init) / `++` / `--` / compound-assign of the name
|
|
142
|
+
CALL_ARG: 4, // passed as a call argument — {callee, argIndex}
|
|
143
|
+
CALL_CALLEE: 5, // invoked: `name(...)`
|
|
144
|
+
RETURN: 6, // `return name`
|
|
145
|
+
CAPTURE: 7, // mentioned inside a nested `=>`
|
|
146
|
+
COMPARE: 8, // operand of a comparison — {nullCmp}
|
|
147
|
+
CONCAT: 9, // operand of `+`
|
|
148
|
+
BOOL_TEST: 10, // operand of `!`/`typeof`/`void`, or an `if`/`while`/`?:` test
|
|
149
|
+
DELETE_MEMBER: 11, // `delete name.member`
|
|
150
|
+
BARE: 12, // any other value position — the conservative catch-all
|
|
151
|
+
}
|
|
152
|
+
const _bindingUsesCache = new WeakMap()
|
|
153
|
+
const _CMP_OPS = new Set(['==', '!=', '===', '!==', '<', '>', '<=', '>='])
|
|
154
|
+
const _isNullishLit = (e) =>
|
|
155
|
+
e === 'null' || e === 'undefined' ||
|
|
156
|
+
(Array.isArray(e) && e[0] == null && (e[1] === null || e[1] === undefined))
|
|
157
|
+
|
|
158
|
+
export function scanBindingUses(body) {
|
|
159
|
+
const hit = _bindingUsesCache.get(body)
|
|
160
|
+
if (hit) return hit
|
|
161
|
+
|
|
162
|
+
const summary = new Map() // name → { decls, initRhs, uses }
|
|
163
|
+
const slot = (name) => {
|
|
164
|
+
let s = summary.get(name)
|
|
165
|
+
if (!s) { s = { decls: 0, initRhs: undefined, uses: [] }; summary.set(name, s) }
|
|
166
|
+
return s
|
|
167
|
+
}
|
|
168
|
+
const use = (name, kind, extra) => slot(name).uses.push(extra ? { kind, ...extra } : { kind })
|
|
169
|
+
|
|
170
|
+
// Static string key of a `[]` index node, else null (computed).
|
|
171
|
+
const litKey = (k) => (Array.isArray(k) && k[0] === 'str' && typeof k[1] === 'string') ? k[1] : staticIndexKey(k)
|
|
172
|
+
|
|
173
|
+
// A child sitting in a value position. A bare string there is a real use —
|
|
174
|
+
// `walk` alone silently drops non-array children, so every value-position
|
|
175
|
+
// child (let-rhs, assign-rhs, call/index args, closure body, …) must route
|
|
176
|
+
// through here or its use goes unrecorded (a latent miscompile: the binding
|
|
177
|
+
// looks unused and an optimization fires unsoundly).
|
|
178
|
+
const val = (child, inClosure) => {
|
|
179
|
+
if (typeof child === 'string') use(child, inClosure ? USE.CAPTURE : USE.BARE)
|
|
180
|
+
else walk(child, inClosure)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Classify the target of an assignment-like node (`=`, compound, `++`, `--`).
|
|
184
|
+
const assignTarget = (t, compound) => {
|
|
185
|
+
if (typeof t === 'string') { use(t, USE.REASSIGN); return }
|
|
186
|
+
if (!Array.isArray(t)) return
|
|
187
|
+
const o = t[0]
|
|
188
|
+
if ((o === '.' || o === '?.') && typeof t[1] === 'string') {
|
|
189
|
+
use(t[1], USE.MEMBER_W, { key: typeof t[2] === 'string' ? t[2] : null, computed: false, compound })
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
if (o === '[]' && typeof t[1] === 'string') {
|
|
193
|
+
const k = litKey(t[2])
|
|
194
|
+
use(t[1], USE.MEMBER_W, { key: k, computed: k == null, compound })
|
|
195
|
+
if (t[2] != null) val(t[2])
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
walk(t) // some other LHS shape — generic
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function walk(node, inClosure) {
|
|
202
|
+
if (!Array.isArray(node)) return
|
|
203
|
+
const op = node[0]
|
|
204
|
+
if (typeof op !== 'string') return // literal node `[null, value]`
|
|
205
|
+
if (op === 'str') return // string literal
|
|
206
|
+
if (op === '=>') { for (let i = 1; i < node.length; i++) val(node[i], true); return }
|
|
207
|
+
|
|
208
|
+
if (op === 'let' || op === 'const') {
|
|
209
|
+
for (let i = 1; i < node.length; i++) {
|
|
210
|
+
const d = node[i]
|
|
211
|
+
if (typeof d === 'string') { if (!inClosure) slot(d).decls++; continue }
|
|
212
|
+
if (Array.isArray(d) && d[0] === '=') {
|
|
213
|
+
const lhs = d[1], rhs = d[2]
|
|
214
|
+
if (typeof lhs === 'string') {
|
|
215
|
+
if (!inClosure) { const s = slot(lhs); s.decls++; if (s.initRhs === undefined) s.initRhs = rhs }
|
|
216
|
+
} else {
|
|
217
|
+
walk(lhs, inClosure) // pattern — computed keys/defaults are real uses
|
|
218
|
+
}
|
|
219
|
+
val(rhs, inClosure)
|
|
220
|
+
} else walk(d, inClosure)
|
|
221
|
+
}
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (inClosure) { // every mention here is a CAPTURE
|
|
226
|
+
for (let i = 1; i < node.length; i++) {
|
|
227
|
+
const c = node[i]
|
|
228
|
+
if (typeof c === 'string') use(c, USE.CAPTURE)
|
|
229
|
+
else walk(c, true)
|
|
230
|
+
}
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// === precise classification (outside any closure) ===
|
|
235
|
+
if (ASSIGN_OPS.has(op)) { assignTarget(node[1], op !== '='); val(node[2]); return }
|
|
236
|
+
if (op === '++' || op === '--') { assignTarget(node[1], true); return }
|
|
237
|
+
if (op === 'delete') {
|
|
238
|
+
const t = node[1]
|
|
239
|
+
if (Array.isArray(t) && (t[0] === '.' || t[0] === '?.' || t[0] === '[]') && typeof t[1] === 'string') {
|
|
240
|
+
use(t[1], USE.DELETE_MEMBER)
|
|
241
|
+
if (t[0] === '[]' && t[2] != null) val(t[2])
|
|
242
|
+
} else val(t)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (op === '.' || op === '?.') {
|
|
246
|
+
const recv = node[1]
|
|
247
|
+
if (typeof recv === 'string')
|
|
248
|
+
use(recv, USE.MEMBER_R, { key: typeof node[2] === 'string' ? node[2] : null, optional: op === '?.', computed: false })
|
|
249
|
+
else walk(recv)
|
|
250
|
+
return // node[2] is the property name
|
|
251
|
+
}
|
|
252
|
+
if (op === '[]') {
|
|
253
|
+
const recv = node[1], k = litKey(node[2])
|
|
254
|
+
if (typeof recv === 'string') use(recv, USE.MEMBER_R, { key: k, optional: false, computed: k == null })
|
|
255
|
+
else walk(recv)
|
|
256
|
+
if (node[2] != null) val(node[2])
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
if (op === ':') { // object property `{k:v}` / labeled statement
|
|
260
|
+
if (Array.isArray(node[1])) walk(node[1]) // computed key `{[expr]:v}` — a real use
|
|
261
|
+
val(node[2]) // property value (or the labeled statement)
|
|
262
|
+
return // string node[1] = plain key / label — not a use
|
|
263
|
+
}
|
|
264
|
+
if (op === 'return') {
|
|
265
|
+
const e = node[1]
|
|
266
|
+
if (typeof e === 'string') use(e, USE.RETURN)
|
|
267
|
+
else walk(e)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
if (op === '()') {
|
|
271
|
+
const callee = node[1]
|
|
272
|
+
if (typeof callee === 'string') use(callee, USE.CALL_CALLEE)
|
|
273
|
+
else walk(callee)
|
|
274
|
+
const argNode = node[2]
|
|
275
|
+
if (argNode != null) {
|
|
276
|
+
const args = (Array.isArray(argNode) && argNode[0] === ',') ? argNode.slice(1) : [argNode]
|
|
277
|
+
for (let ai = 0; ai < args.length; ai++) {
|
|
278
|
+
const a = args[ai]
|
|
279
|
+
if (Array.isArray(a) && a[0] === '...') { val(a[1]); continue }
|
|
280
|
+
if (typeof a === 'string') use(a, USE.CALL_ARG, { callee: typeof callee === 'string' ? callee : null, argIndex: ai })
|
|
281
|
+
else walk(a)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (_CMP_OPS.has(op) && node.length === 3) {
|
|
287
|
+
for (let i = 1; i <= 2; i++) {
|
|
288
|
+
const side = node[i]
|
|
289
|
+
if (typeof side === 'string') use(side, USE.COMPARE, { nullCmp: _isNullishLit(node[3 - i]) })
|
|
290
|
+
else walk(side)
|
|
291
|
+
}
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
if (op === '+') {
|
|
295
|
+
for (let i = 1; i < node.length; i++) {
|
|
296
|
+
const c = node[i]
|
|
297
|
+
if (typeof c === 'string') use(c, USE.CONCAT)
|
|
298
|
+
else walk(c)
|
|
299
|
+
}
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
if (op === '!' || op === 'typeof' || op === 'void') {
|
|
303
|
+
const c = node[1]
|
|
304
|
+
if (typeof c === 'string') use(c, USE.BOOL_TEST)
|
|
305
|
+
else walk(c)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
if (op === 'if' || op === 'while' || op === '?:') { // `prepare` normalizes `?` → `?:`
|
|
309
|
+
const c = node[1]
|
|
310
|
+
if (typeof c === 'string') use(c, USE.BOOL_TEST)
|
|
311
|
+
else walk(c)
|
|
312
|
+
for (let i = 2; i < node.length; i++) val(node[i])
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// generic — every string child is a BARE value use
|
|
317
|
+
for (let i = 1; i < node.length; i++) {
|
|
318
|
+
const c = node[i]
|
|
319
|
+
if (typeof c === 'string') use(c, USE.BARE)
|
|
320
|
+
else walk(c)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
walk(body, false)
|
|
325
|
+
|
|
326
|
+
for (const [name, s] of summary) if (s.decls === 0) summary.delete(name)
|
|
327
|
+
_bindingUsesCache.set(body, summary)
|
|
328
|
+
return summary
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* SRoA eligibility scan — which `let/const o = {staticLiteral}` bindings can
|
|
333
|
+
* have their fields dissolved into plain WASM locals (`flat` carrier): no heap
|
|
334
|
+
* alloc, no field load/store, `o.prop` becomes `local.get`.
|
|
335
|
+
*
|
|
336
|
+
* A binding is flat-eligible iff `o` appears ONLY as a literal-key `.`/`[]`
|
|
337
|
+
* READ of an in-schema prop, or the member LHS of a literal-key `.`/`[]` WRITE
|
|
338
|
+
* of an in-schema prop. Any other mention — bare ref, dynamic/numeric key,
|
|
339
|
+
* off-schema prop, `?.`, reassignment, compound assign, `++`/`--`, `delete`,
|
|
340
|
+
* closure capture, self-referential initializer, duplicate keys, or a second
|
|
341
|
+
* declaration — disqualifies it. A non-escaping object is never observed by
|
|
342
|
+
* any object walk (keys/values/entries/assign/spread/JSON/for-in/dyn), so the
|
|
343
|
+
* transform is additive and sound. Conservative: any doubt → not flat.
|
|
344
|
+
*
|
|
345
|
+
* A policy over `scanBindingUses`: the shared traversal classifies every
|
|
346
|
+
* mention; this scan keeps a binding only if its initializer is a self-
|
|
347
|
+
* contained static literal and every use is an in-schema literal-key access.
|
|
348
|
+
*
|
|
349
|
+
* Returns `Map<name, {names, values}>` — the literal's parallel prop arrays.
|
|
350
|
+
* Field `i` of binding `o` lives in WASM local `o#${i}` (`#` cannot occur in a
|
|
351
|
+
* jz identifier, so the name is collision-free).
|
|
352
|
+
*/
|
|
353
|
+
// Largest array literal that dissolves into scalar slots. Beyond this a single
|
|
354
|
+
// constant data segment is cheaper than N locals (+ the per-slot init prologue).
|
|
355
|
+
const FLAT_ARRAY_MAX = 8
|
|
356
|
+
|
|
357
|
+
export function scanFlatObjects(body) {
|
|
358
|
+
const cand = new Map() // name → {names, values}
|
|
359
|
+
|
|
360
|
+
// A binding referenced as a value inside `node` (skips `:`/`.` property-name
|
|
361
|
+
// slots). Used only to reject a self-referential initializer — a literal
|
|
362
|
+
// whose own field values mention the binding is not a self-contained object.
|
|
363
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
364
|
+
if (s.decls !== 1 || !Array.isArray(s.initRhs)) continue
|
|
365
|
+
// Candidate aggregate: an object literal `{…}` (string keys) or a small array
|
|
366
|
+
// literal `[…]` (index keys "0","1",…). An array dissolves into `name#i` scalar
|
|
367
|
+
// locals exactly like an object — same `.`/`[]` flat hooks, no heap alloc — when
|
|
368
|
+
// every use is a static-index read/write. Capped at FLAT_ARRAY_MAX: a larger
|
|
369
|
+
// literal belongs in one constant data-segment region, not N spilled locals.
|
|
370
|
+
let props
|
|
371
|
+
if (s.initRhs[0] === '{}') {
|
|
372
|
+
props = staticObjectProps(s.initRhs.slice(1))
|
|
373
|
+
} else if (s.initRhs[0] === '[' || s.initRhs[0] === '[]') {
|
|
374
|
+
const elems = staticArrayElems(s.initRhs)
|
|
375
|
+
if (!elems || !elems.length || elems.length > FLAT_ARRAY_MAX) continue
|
|
376
|
+
// Holes (`[1,,3]`) and spreads (`[...x]`) aren't a fixed positional schema.
|
|
377
|
+
if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...'))) continue
|
|
378
|
+
// Only compile-time-constant *value* elements dissolve — number/string/bool/null
|
|
379
|
+
// ("arrays hold JSON values"). A non-literal element (identifier, call, closure,
|
|
380
|
+
// arithmetic on a runtime var) can carry a function/closure whose call-indirect
|
|
381
|
+
// table index binds to the array, not a scalar local — dissolving the slot
|
|
382
|
+
// desyncs the `elem` section. Conservative: any non-constant element keeps the
|
|
383
|
+
// array heap-backed.
|
|
384
|
+
if (!elems.every(e => staticValue(e) !== NO_VALUE)) continue
|
|
385
|
+
props = { names: elems.map((_, i) => String(i)), values: elems }
|
|
386
|
+
} else continue
|
|
387
|
+
const isArr = s.initRhs[0] !== '{}'
|
|
388
|
+
if (!props || new Set(props.names).size !== props.names.length) continue
|
|
389
|
+
if (props.values.some(v => refsName(v, name, REFS_IN_EXPR))) continue
|
|
390
|
+
|
|
391
|
+
// Schema = literal keys ∪ plain literal-key member writes. For an OBJECT such a
|
|
392
|
+
// write monotonically extends the static field universe (the new field reads
|
|
393
|
+
// `undefined` until the write runs, exactly as JS does). An ARRAY has a *fixed*
|
|
394
|
+
// positional schema: `a.length = …` / `a[n] = …` (off the literal indices) resize
|
|
395
|
+
// or grow it — not a field add — so arrays never extend, and any off-schema write
|
|
396
|
+
// (including `.length`, which isn't a slot) disqualifies below.
|
|
397
|
+
// `written` = the keys a MEMBER_W reassigns — a slot is write-once (its
|
|
398
|
+
// value-type is exactly its literal initializer's) iff its key is absent here.
|
|
399
|
+
const schema = new Set(props.names)
|
|
400
|
+
const written = new Set()
|
|
401
|
+
for (const u of s.uses)
|
|
402
|
+
if (u.kind === USE.MEMBER_W && !u.compound && !u.computed && u.key != null) {
|
|
403
|
+
if (!isArr) schema.add(u.key)
|
|
404
|
+
written.add(u.key)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Flat iff every mention is an in-schema literal-key `.`/`[]` READ, or an
|
|
408
|
+
// in-schema literal-key plain `.`/`[]` WRITE. Any other use kind — `?.`,
|
|
409
|
+
// computed/off-schema key, reassignment, compound or `delete` member write,
|
|
410
|
+
// `++`/`--`, call arg, closure capture, bare ref — leaves the object live.
|
|
411
|
+
const flat = s.uses.every(u =>
|
|
412
|
+
(u.kind === USE.MEMBER_R && !u.optional && !u.computed && schema.has(u.key)) ||
|
|
413
|
+
(u.kind === USE.MEMBER_W && !u.compound && !u.computed && schema.has(u.key)))
|
|
414
|
+
if (!flat) continue
|
|
415
|
+
|
|
416
|
+
// Materialize the parallel {names, values}: literal props first, then each
|
|
417
|
+
// extension field (value `undefined`), in first-write order.
|
|
418
|
+
const names = props.names.slice(), values = props.values.slice()
|
|
419
|
+
for (const k of schema)
|
|
420
|
+
if (!names.includes(k)) { names.push(k); values.push(undefined) }
|
|
421
|
+
cand.set(name, { names, values, written })
|
|
422
|
+
}
|
|
423
|
+
return cand
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* No-copy slice scan — which `let/const t = s.slice(...)` bindings can be a
|
|
428
|
+
* VIEW (a SLICE_BIT pointer straight into `s`'s buffer) instead of a fresh
|
|
429
|
+
* byte copy.
|
|
430
|
+
*
|
|
431
|
+
* jz rewinds the bump arena only at function exit, so every string the
|
|
432
|
+
* function can observe stays alive until it returns. A view is therefore sound
|
|
433
|
+
* exactly when its binding does NOT escape the function: `t` must never be
|
|
434
|
+
* returned, passed as a call argument, stored into a heap object/array,
|
|
435
|
+
* captured by a closure, aliased to another binding, reassigned, or
|
|
436
|
+
* compound-assigned. The permitted uses — receiver of a `.`/`[]`, operand of a
|
|
437
|
+
* comparison or `+`, a boolean test — read `t` synchronously and never persist
|
|
438
|
+
* it past the function.
|
|
439
|
+
*
|
|
440
|
+
* Declared exactly once as `let/const`. The result is purely structural —
|
|
441
|
+
* whether the receiver is actually a string (so `.slice` lowers to the string
|
|
442
|
+
* view) is settled later, at emit time, when param types are known; emitDecl
|
|
443
|
+
* keeps the ordinary copying slice for any non-string receiver. Conservative:
|
|
444
|
+
* any unrecognised position disqualifies the binding.
|
|
445
|
+
*
|
|
446
|
+
* Returns `Set<name>` of view-eligible binding names.
|
|
447
|
+
*/
|
|
448
|
+
// Permitted use-kinds for a slice view — the value is read synchronously and
|
|
449
|
+
// never persisted past the function. `MEMBER_R`/`MEMBER_W` cover any `.`/`[]`
|
|
450
|
+
// receiver; `COMPARE` any comparison; `CONCAT`/`BOOL_TEST` the copy / test
|
|
451
|
+
// positions. Any other kind (reassign, call arg, return, capture, bare alias)
|
|
452
|
+
// escapes and disqualifies the binding.
|
|
453
|
+
const _SLICE_VIEW_OK = new Set([USE.MEMBER_R, USE.MEMBER_W, USE.COMPARE, USE.CONCAT, USE.BOOL_TEST])
|
|
454
|
+
|
|
455
|
+
export function scanSliceViews(body) {
|
|
456
|
+
const isSliceCall = (n) =>
|
|
457
|
+
Array.isArray(n) && n[0] === '()' && Array.isArray(n[1])
|
|
458
|
+
&& n[1][0] === '.' && n[1][2] === 'slice'
|
|
459
|
+
|
|
460
|
+
const views = new Set()
|
|
461
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
462
|
+
if (s.decls !== 1 || !isSliceCall(s.initRhs)) continue
|
|
463
|
+
if (s.uses.every(u => _SLICE_VIEW_OK.has(u.kind))) views.add(name)
|
|
464
|
+
}
|
|
465
|
+
return views
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Never-relocated array bindings — reads through them may skip the realloc-forwarding
|
|
470
|
+
* follow (`__ptr_offset`). A fresh array-literal binding is never relocated iff EVERY
|
|
471
|
+
* occurrence of it is a pure READ — `a[i]` (any index) or `a.length`. Anything else
|
|
472
|
+
* grows or escapes it: a grow method (push/unshift/shift/splice), a `.length`/element
|
|
473
|
+
* write (incl. compound `a.length += 1`), a bare value use (alias `let b=a`, store
|
|
474
|
+
* `w.x=a`, return, call argument, spread), a reassignment, or a dynamic call
|
|
475
|
+
* `a[i]()`/`a.m()`.
|
|
476
|
+
*
|
|
477
|
+
* MEMORY-SAFETY CRITICAL and so DEFAULT-DENY + self-contained: it does NOT trust the
|
|
478
|
+
* `escapes` map, which misses member-write RHS (`w.data = a`) and compound assigns. If
|
|
479
|
+
* the analysis is wrong and the array IS relocated, a read through the stale base
|
|
480
|
+
* corrupts memory — so any unrecognized use disqualifies. (Growing an INNER array,
|
|
481
|
+
* `a[0].push(x)`, never relocates `a` itself, so `a` stays eligible — see safeReads.)
|
|
482
|
+
*/
|
|
483
|
+
const grownOrEscapes = (op) => ASSIGN_OPS.has(op) || op === '++' || op === '--' || op === 'delete'
|
|
484
|
+
function safeReads(node, name) {
|
|
485
|
+
if (typeof node === 'string') return node !== name // bare value use → escape
|
|
486
|
+
if (!Array.isArray(node)) return true
|
|
487
|
+
const op = node[0]
|
|
488
|
+
// `a(…)` / `a.m(…)` / `a[i](…)` — calling `a` or a method/element of it may grow/escape it.
|
|
489
|
+
if (op === '()') {
|
|
490
|
+
const c = node[1]
|
|
491
|
+
if (c === name) return false
|
|
492
|
+
if (Array.isArray(c) && (c[0] === '.' || c[0] === '?.' || c[0] === '[]' || c[0] === '?.[]') && c[1] === name) return false
|
|
493
|
+
}
|
|
494
|
+
// write / update / delete on `a`, `a[..]`, or `a.x` (incl. `a.length = …` and compounds)
|
|
495
|
+
if (grownOrEscapes(op)) {
|
|
496
|
+
const t = node[1]
|
|
497
|
+
if (t === name) return false
|
|
498
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
499
|
+
}
|
|
500
|
+
// declaration: check each initializer RHS (so `let b = a` aliasing disqualifies);
|
|
501
|
+
// the bound names themselves are definitions, not uses (skips `a`'s own decl).
|
|
502
|
+
if (op === 'let' || op === 'const' || op === 'var') {
|
|
503
|
+
for (let i = 1; i < node.length; i++) {
|
|
504
|
+
const d = node[i]
|
|
505
|
+
if (Array.isArray(d) && d[0] === '=' && !safeReads(d[2], name)) return false
|
|
506
|
+
}
|
|
507
|
+
return true
|
|
508
|
+
}
|
|
509
|
+
// the only safe forms: `a.length` read, and `a[i]` index read (recurse the index expr).
|
|
510
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
511
|
+
if (op === '[]' && node[1] === name) return safeReads(node[2], name)
|
|
512
|
+
if (op === '...' && node[1] === name) return false // spread → escape
|
|
513
|
+
for (let i = 1; i < node.length; i++) if (!safeReads(node[i], name)) return false
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function scanNeverGrown(body) {
|
|
518
|
+
const out = new Set()
|
|
519
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
520
|
+
// Candidate: a single-declaration binding initialized from a fresh array literal.
|
|
521
|
+
if (s.decls !== 1 || !Array.isArray(s.initRhs)) continue
|
|
522
|
+
if (s.initRhs[0] !== '[' && !(s.initRhs[0] === '[]' && s.initRhs.length <= 2)) continue
|
|
523
|
+
if (safeReads(body, name)) out.add(name)
|
|
524
|
+
}
|
|
525
|
+
return out
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Numeric-fill arrays — the construct-then-fill counterpart of an all-number array
|
|
530
|
+
* literal. A fresh `Array(n)` / `new Array(n)` / `[]` binding whose EVERY element write
|
|
531
|
+
* stores a provably-NUMBER value, and which never escapes, aliases, is reassigned, grows
|
|
532
|
+
* by method, or takes a non-numeric / compound element write, holds only Numbers (unwritten
|
|
533
|
+
* holes read as 0 in jz, also a Number). So its `a[i]` reads can skip the polymorphic
|
|
534
|
+
* `__to_num` coercion — exactly the win `[1,2,3]` already gets, extended to the dominant
|
|
535
|
+
* numeric-kernel shape `let a = Array(n); for (..) a[i] = expr`.
|
|
536
|
+
*
|
|
537
|
+
* Default-deny and self-contained, like scanNeverGrown (the same memory-safety discipline):
|
|
538
|
+
* any occurrence that isn't a pure index/length READ or a NUMBER-valued `a[i] = …` write
|
|
539
|
+
* disqualifies — so `w.x = a`, `f(a)`, `let b = a`, `a.push(x)`, `a[i] += x` all bail.
|
|
540
|
+
* `isNumericRhs` injects the value-type judgement (valTypeOf === VAL.NUMBER) the syntactic
|
|
541
|
+
* scan can't make itself.
|
|
542
|
+
*/
|
|
543
|
+
// Both `Array(n)` and `new Array(n)` normalize to a `new.Array` call by prepare; an
|
|
544
|
+
// empty literal stays `['[]', null]`. (Typed ctors become `new.Float64Array` etc. — the
|
|
545
|
+
// exact-match on `new.Array` keeps them out.)
|
|
546
|
+
const isFreshArrayCtor = (rhs) =>
|
|
547
|
+
Array.isArray(rhs) && (
|
|
548
|
+
(rhs[0] === '[]' && rhs.length <= 2) || // empty `[]`
|
|
549
|
+
(rhs[0] === '()' && rhs[1] === 'new.Array') // `Array(n)` / `new Array(n)` / `Array()`
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
function numFillSafe(node, name, isNumericRhs) {
|
|
553
|
+
if (typeof node === 'string') return node !== name // bare value use → escape
|
|
554
|
+
if (!Array.isArray(node)) return true
|
|
555
|
+
const op = node[0]
|
|
556
|
+
// `a[i] = rhs` — the fill write. Allowed iff rhs is provably NUMBER; recurse the index
|
|
557
|
+
// and rhs so a stray `a` inside either still disqualifies. (Compound `a[i] += …` is NOT
|
|
558
|
+
// matched here, so it falls through to the deny below — conservative for v1.)
|
|
559
|
+
if (op === '=' && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
|
|
560
|
+
return isNumericRhs(node[2], name) &&
|
|
561
|
+
numFillSafe(node[1][2], name, isNumericRhs) && numFillSafe(node[2], name, isNumericRhs)
|
|
562
|
+
// calling `a`, `a.m(…)`, `a[i](…)` may grow/escape it
|
|
563
|
+
if (op === '()') {
|
|
564
|
+
const c = node[1]
|
|
565
|
+
if (c === name) return false
|
|
566
|
+
if (Array.isArray(c) && (c[0] === '.' || c[0] === '?.' || c[0] === '[]' || c[0] === '?.[]') && c[1] === name) return false
|
|
567
|
+
}
|
|
568
|
+
// any other write/update/delete on `a`, `a[..]`, `a.x` (incl. `a.length = …`, compounds)
|
|
569
|
+
if (grownOrEscapes(op)) {
|
|
570
|
+
const t = node[1]
|
|
571
|
+
if (t === name) return false
|
|
572
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
573
|
+
}
|
|
574
|
+
if (op === 'let' || op === 'const' || op === 'var') {
|
|
575
|
+
for (let i = 1; i < node.length; i++) {
|
|
576
|
+
const d = node[i]
|
|
577
|
+
if (Array.isArray(d) && d[0] === '=' && !numFillSafe(d[2], name, isNumericRhs)) return false
|
|
578
|
+
}
|
|
579
|
+
return true
|
|
580
|
+
}
|
|
581
|
+
// the only safe forms: `a.length` read, and `a[i]` index read (recurse the index expr).
|
|
582
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
583
|
+
if (op === '[]' && node[1] === name) return numFillSafe(node[2], name, isNumericRhs)
|
|
584
|
+
if (op === '...' && node[1] === name) return false // spread → escape
|
|
585
|
+
for (let i = 1; i < node.length; i++) if (!numFillSafe(node[i], name, isNumericRhs)) return false
|
|
586
|
+
return true
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function scanNumericFill(body, isNumericRhs) {
|
|
590
|
+
const out = new Set()
|
|
591
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
592
|
+
if (s.decls !== 1 || !isFreshArrayCtor(s.initRhs)) continue
|
|
593
|
+
if (numFillSafe(body, name, isNumericRhs)) out.add(name)
|
|
594
|
+
}
|
|
595
|
+
return out
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Narrow uint32 accumulator locals to unsigned i32. A local qualifies when its
|
|
600
|
+
* initializer is a non-negative integer literal in [0, 2^32), every
|
|
601
|
+
* reassignment is `name = (…) >>> k` (so it always holds a canonical uint32),
|
|
602
|
+
* and every read sits inside a `>>>` (ToUint32) sink reached only through
|
|
603
|
+
* bit-faithful operators (`^ & | ~ << >> + - *`). Under those constraints the
|
|
604
|
+
* raw i32 bit pattern reproduces JS semantics exactly — every observable use is
|
|
605
|
+
* funnelled through ToUint32 — so the f64 round-trip on the hot path is pure
|
|
606
|
+
* overhead. Names that escape (closures, bare `return`, signed-sensitive
|
|
607
|
+
* operands) keep their wider type. Returns the qualifying set; callers retype
|
|
608
|
+
* `locals` to 'i32' and tag `readVar` reads `.unsigned` for convert_i32_u.
|
|
609
|
+
*/
|
|
610
|
+
export function narrowUint32(body, locals) {
|
|
611
|
+
const TRANSPARENT = new Set(['^', '&', '|', '~', '<<', '>>', '+', '-', '*'])
|
|
612
|
+
const initLit = new Set() // names with a valid u32-literal initializer
|
|
613
|
+
const disq = new Set() // names disqualified by an unsafe occurrence
|
|
614
|
+
const seen = new Set()
|
|
615
|
+
const isU32Lit = e => {
|
|
616
|
+
const v = typeof e === 'number' ? e
|
|
617
|
+
: Array.isArray(e) && e[0] == null && typeof e[1] === 'number' ? e[1] : NaN
|
|
618
|
+
return Number.isInteger(v) && v >= 0 && v < 4294967296
|
|
619
|
+
}
|
|
620
|
+
const banNames = n => {
|
|
621
|
+
if (typeof n === 'string') disq.add(n)
|
|
622
|
+
else if (Array.isArray(n)) for (let i = 1; i < n.length; i++) banNames(n[i])
|
|
623
|
+
}
|
|
624
|
+
const walk = (node, underShr, inClosure) => {
|
|
625
|
+
if (typeof node === 'string') { if (inClosure) disq.add(node); return }
|
|
626
|
+
if (!Array.isArray(node)) return
|
|
627
|
+
const op = node[0]
|
|
628
|
+
if (typeof op !== 'string') {
|
|
629
|
+
for (let i = 1; i < node.length; i++) walk(node[i], false, inClosure)
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
if (op === '=>') { for (let i = 1; i < node.length; i++) walk(node[i], false, true); return }
|
|
633
|
+
if (op === 'let' || op === 'const') {
|
|
634
|
+
for (let i = 1; i < node.length; i++) {
|
|
635
|
+
const d = node[i]
|
|
636
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
|
|
637
|
+
const nm = d[1]
|
|
638
|
+
if (seen.has(nm) || inClosure || !isU32Lit(d[2])) disq.add(nm)
|
|
639
|
+
else initLit.add(nm)
|
|
640
|
+
seen.add(nm)
|
|
641
|
+
walk(d[2], false, inClosure)
|
|
642
|
+
} else if (typeof d === 'string') { disq.add(d); seen.add(d) }
|
|
643
|
+
else if (Array.isArray(d) && d[0] === '=') { banNames(d[1]); walk(d[2], false, inClosure) }
|
|
644
|
+
}
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
if ((op === '++' || op === '--') && typeof node[1] === 'string') { disq.add(node[1]); return }
|
|
648
|
+
if (ASSIGN_OPS.has(op)) {
|
|
649
|
+
const lhs = node[1]
|
|
650
|
+
if (typeof lhs === 'string') {
|
|
651
|
+
if (op !== '=' || inClosure || !(Array.isArray(node[2]) && node[2][0] === '>>>')) disq.add(lhs)
|
|
652
|
+
} else banNames(lhs)
|
|
653
|
+
walk(node[2], false, inClosure)
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
const childShr = op === '>>>' ? true : TRANSPARENT.has(op) ? underShr : false
|
|
657
|
+
for (let i = 1; i < node.length; i++) {
|
|
658
|
+
const c = node[i]
|
|
659
|
+
if (typeof c === 'string') { if (inClosure || !childShr) disq.add(c) }
|
|
660
|
+
else walk(c, childShr, inClosure)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
walk(body, false, false)
|
|
664
|
+
const result = new Set()
|
|
665
|
+
for (const nm of initLit) {
|
|
666
|
+
if (disq.has(nm)) continue
|
|
667
|
+
const t = locals.get(nm)
|
|
668
|
+
if (t !== 'i32' && t !== 'f64') continue
|
|
669
|
+
locals.set(nm, 'i32')
|
|
670
|
+
result.add(nm)
|
|
671
|
+
}
|
|
672
|
+
return result
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Operators under which a counter remains a *monotone, bounded* function of the
|
|
676
|
+
// index root: an affine index `base + i*stride` (and `i << k`) whose computed
|
|
677
|
+
// offset must fit i32-addressable wasm32 memory therefore bounds the counter to
|
|
678
|
+
// i32 range. `/ % & | ^ >> >>>` are excluded — they decouple the index magnitude
|
|
679
|
+
// from the counter (`arr[i & 7]` stays small however large `i` grows), so they
|
|
680
|
+
// prove nothing about the counter's range.
|
|
681
|
+
const AFFINE_INDEX_OPS = new Set(['+', '-', '*', '<<', 'u-'])
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Locals proven to stay within i32 range, so they need not widen to f64 when
|
|
685
|
+
* compared against an f64 loop bound. Keeping them i32 yields direct i32 indexing
|
|
686
|
+
* (no per-access `trunc_sat_f64_s`) and lets the relational compare coerce the
|
|
687
|
+
* counter instead — the compiler-inferred form of the manual `let n = N | 0` hoist.
|
|
688
|
+
*
|
|
689
|
+
* Two sound sources of an i32-range proof:
|
|
690
|
+
* 1. Direct: a local appears as an *affine* component of an array index. A valid
|
|
691
|
+
* wasm32 access requires the byte offset to fit i32, and an affine index is
|
|
692
|
+
* monotone in the local, so the local is i32-bounded for every non-trapping run.
|
|
693
|
+
* 2. Transitive (back-propagation): a local that flows — via affine
|
|
694
|
+
* assignment/step (`let i0 = ix`, `i0 += id`) — into an already-bounded index
|
|
695
|
+
* var is itself bounded by that var's range. This captures the common
|
|
696
|
+
* nested-loop shape where the outer bound seeds an inner index (FFT butterflies:
|
|
697
|
+
* `while (ix < N) { let i0 = ix; while (i0 < N) … x[i0] … i0 += id }`).
|
|
698
|
+
*
|
|
699
|
+
* Fractional locals are unaffected: this set only suppresses *comparison*-driven
|
|
700
|
+
* widening; the assignment fixpoint that follows still widens any local with an
|
|
701
|
+
* f64-typed RHS (`i = i / 3`), overriding membership here.
|
|
702
|
+
*/
|
|
703
|
+
// An integer literal that fits signed i32 — the only constant a promoted i32
|
|
704
|
+
// local may hold. A larger integer (`0xFFFFFFFF`, a NaN-box mask) is emitted as
|
|
705
|
+
// an f64.const, so treating it as an i32 leaf would store f64 into an i32 local.
|
|
706
|
+
const isI32Lit = (v) => typeof v === 'number' && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647
|
|
707
|
+
|
|
708
|
+
export function collectI32SafeIndexVars(body, locals) {
|
|
709
|
+
const safe = new Set()
|
|
710
|
+
// Collect names reachable from `node` through affine ops only, into `sink`.
|
|
711
|
+
const addAffine = (node, sink) => {
|
|
712
|
+
if (typeof node === 'string') { sink.add(node); return }
|
|
713
|
+
if (!Array.isArray(node)) return
|
|
714
|
+
if (AFFINE_INDEX_OPS.has(node[0])) for (let i = 1; i < node.length; i++) addAffine(node[i], sink)
|
|
715
|
+
}
|
|
716
|
+
// Pass 1: record assignment edges (back-prop) + a name→definitions map (for the
|
|
717
|
+
// integer-shape test). `+= …` reconstructs to `name + …` so its shape includes
|
|
718
|
+
// the prior value.
|
|
719
|
+
const edges = []
|
|
720
|
+
const defs = new Map()
|
|
721
|
+
const addDef = (name, rhs) => { (defs.get(name) ?? defs.set(name, []).get(name)).push(rhs) }
|
|
722
|
+
const collect = (node) => {
|
|
723
|
+
if (!Array.isArray(node)) return
|
|
724
|
+
const op = node[0]
|
|
725
|
+
if (op === 'let' || op === 'const') {
|
|
726
|
+
for (let i = 1; i < node.length; i++) {
|
|
727
|
+
const d = node[i]
|
|
728
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') { edges.push({ target: d[1], rhs: d[2] }); addDef(d[1], d[2]) }
|
|
729
|
+
}
|
|
730
|
+
} else if (op === '=' && typeof node[1] === 'string') { edges.push({ target: node[1], rhs: node[2] }); addDef(node[1], node[2]) }
|
|
731
|
+
else if ((op === '+=' || op === '-=' || op === '*=') && typeof node[1] === 'string') { edges.push({ target: node[1], rhs: node[2] }); addDef(node[1], [op[0], node[1], node[2]]) }
|
|
732
|
+
if (op === '=>') return
|
|
733
|
+
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
734
|
+
}
|
|
735
|
+
collect(body)
|
|
736
|
+
|
|
737
|
+
// Integer-shaped AND i32-representable: provably an integer through `+ - * << u-`
|
|
738
|
+
// (AFFINE_INDEX_OPS — excludes `/`/`**`/fractional ops) over leaves that are
|
|
739
|
+
// i32-typed, i32-range integer literals, or other integer-shaped locals. Lets a
|
|
740
|
+
// hoisted offset `let o = y*w` (f64-typed product, integer-valued) qualify as an
|
|
741
|
+
// index leaf before narrowing. A fractional leaf, an out-of-i32-range literal, or
|
|
742
|
+
// a param of unknown type disqualifies — so no truncation and no f64.const→i32.
|
|
743
|
+
const isIntShaped = (node, seen) => {
|
|
744
|
+
if (typeof node === 'number') return isI32Lit(node)
|
|
745
|
+
if (typeof node === 'string') {
|
|
746
|
+
if (exprType(node, locals) === 'i32') return true
|
|
747
|
+
if (seen.has(node)) return true // recursion through a self-step — other defs still gate
|
|
748
|
+
const ds = defs.get(node)
|
|
749
|
+
if (!ds || !ds.length) return false // param / unknown source — not provably integer
|
|
750
|
+
seen.add(node)
|
|
751
|
+
const r = ds.every(d => isIntShaped(d, seen))
|
|
752
|
+
seen.delete(node)
|
|
753
|
+
return r
|
|
754
|
+
}
|
|
755
|
+
if (!Array.isArray(node)) return false
|
|
756
|
+
const op = node[0]
|
|
757
|
+
if (op == null) return isI32Lit(node[1]) // [null, value] literal
|
|
758
|
+
if (!AFFINE_INDEX_OPS.has(op)) return false
|
|
759
|
+
for (let i = 1; i < node.length; i++) if (node[i] != null && !isIntShaped(node[i], new Set(seen))) return false
|
|
760
|
+
return true
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Pass 2: seed from array indices already i32 OR integer-shaped (the latter
|
|
764
|
+
// rescues hoisted integer offsets the type pass left at f64). A fractional index
|
|
765
|
+
// (`mem[y*w+x]` with fractional `w`) is not integer-shaped → still truncs per
|
|
766
|
+
// access and is left to widen, preserving the prior guard.
|
|
767
|
+
const seed = (node) => {
|
|
768
|
+
if (!Array.isArray(node)) return
|
|
769
|
+
const op = node[0]
|
|
770
|
+
if (op === '[]' && !isLiteralStr(node[2]) && (exprType(node[2], locals) === 'i32' || isIntShaped(node[2], new Set()))) addAffine(node[2], safe)
|
|
771
|
+
if (op === '=>') return
|
|
772
|
+
for (let i = 1; i < node.length; i++) seed(node[i])
|
|
773
|
+
}
|
|
774
|
+
seed(body)
|
|
775
|
+
|
|
776
|
+
// Back-propagate to a fixpoint: feeders of a bounded index var are bounded.
|
|
777
|
+
let changed = true
|
|
778
|
+
while (changed) {
|
|
779
|
+
changed = false
|
|
780
|
+
for (const { target, rhs } of edges) {
|
|
781
|
+
if (!safe.has(target)) continue
|
|
782
|
+
const src = new Set()
|
|
783
|
+
addAffine(rhs, src)
|
|
784
|
+
for (const s of src) if (!safe.has(s)) { safe.add(s); changed = true }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// Promote integer-shaped index feeders the type pass left at f64 (a hoisted
|
|
788
|
+
// `o = y*w`). The byte offset must fit i32-addressable memory, so the i32-wrap
|
|
789
|
+
// residue reproduces the true in-bounds value — same contract as inline `a[y*w+x]`.
|
|
790
|
+
// Skip boxed (closure-captured) cells — those live as f64 in memory.
|
|
791
|
+
for (const n of safe) if (locals.get(n) === 'f64' && !ctx.func.boxed?.has(n) && isIntShaped(n, new Set())) locals.set(n, 'i32')
|
|
792
|
+
return safe
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Locals that affinely feed an *f64-typed* array index (e.g. `mem[i*w + x]` with
|
|
797
|
+
* an f64 stride/global `w`). The access truncs the byte offset regardless, so
|
|
798
|
+
* keeping such a counter i32 buys no trunc savings and ADDS a per-iteration
|
|
799
|
+
* compare-convert — a net loss (the game-of-life regression). These are excluded
|
|
800
|
+
* from the integer-counter i32-keep in analyzeBody's widenPass, so they widen to
|
|
801
|
+
* f64 as before. (A counter used only in arithmetic — no f64 index — is NOT here,
|
|
802
|
+
* so it stays i32, where the i32 body + increment is the real win.)
|
|
803
|
+
*/
|
|
804
|
+
export function collectF64StridedIndexVars(body, locals) {
|
|
805
|
+
const set = new Set()
|
|
806
|
+
const addAffine = (node) => {
|
|
807
|
+
if (typeof node === 'string') { set.add(node); return }
|
|
808
|
+
if (Array.isArray(node) && AFFINE_INDEX_OPS.has(node[0])) for (let i = 1; i < node.length; i++) addAffine(node[i])
|
|
809
|
+
}
|
|
810
|
+
const walk = (node) => {
|
|
811
|
+
if (!Array.isArray(node)) return
|
|
812
|
+
if (node[0] === '[]' && !isLiteralStr(node[2]) && exprType(node[2], locals) === 'f64') addAffine(node[2])
|
|
813
|
+
if (node[0] === '=>') return
|
|
814
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
815
|
+
}
|
|
816
|
+
walk(body)
|
|
817
|
+
return set
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Returns the cached facts object directly — DO NOT MUTATE the returned maps.
|
|
822
|
+
* Callers that need to extend (e.g. add params to locals) must clone explicitly
|
|
823
|
+
* before mutating. Slice reads via `analyzeBody(body).<slice>`.
|
|
824
|
+
*/
|