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,661 @@
|
|
|
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 } 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] : null
|
|
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
|
+
export function scanFlatObjects(body) {
|
|
354
|
+
const cand = new Map() // name → {names, values}
|
|
355
|
+
|
|
356
|
+
// A binding referenced as a value inside `node` (skips `:`/`.` property-name
|
|
357
|
+
// slots). Used only to reject a self-referential initializer — a literal
|
|
358
|
+
// whose own field values mention the binding is not a self-contained object.
|
|
359
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
360
|
+
if (s.decls !== 1 || !Array.isArray(s.initRhs) || s.initRhs[0] !== '{}') continue
|
|
361
|
+
const props = staticObjectProps(s.initRhs.slice(1))
|
|
362
|
+
if (!props || new Set(props.names).size !== props.names.length) continue
|
|
363
|
+
if (props.values.some(v => refsName(v, name, REFS_IN_EXPR))) continue
|
|
364
|
+
|
|
365
|
+
// Schema = literal keys ∪ plain literal-key member writes. Such a write
|
|
366
|
+
// monotonically extends the static field universe (the new field reads
|
|
367
|
+
// `undefined` until the write runs, exactly as JS does); the schema stays
|
|
368
|
+
// closed because any computed/off-schema access disqualifies below.
|
|
369
|
+
const schema = new Set(props.names)
|
|
370
|
+
for (const u of s.uses)
|
|
371
|
+
if (u.kind === USE.MEMBER_W && !u.compound && !u.computed && u.key != null)
|
|
372
|
+
schema.add(u.key)
|
|
373
|
+
|
|
374
|
+
// Flat iff every mention is an in-schema literal-key `.`/`[]` READ, or an
|
|
375
|
+
// in-schema literal-key plain `.`/`[]` WRITE. Any other use kind — `?.`,
|
|
376
|
+
// computed/off-schema key, reassignment, compound or `delete` member write,
|
|
377
|
+
// `++`/`--`, call arg, closure capture, bare ref — leaves the object live.
|
|
378
|
+
const flat = s.uses.every(u =>
|
|
379
|
+
(u.kind === USE.MEMBER_R && !u.optional && !u.computed && schema.has(u.key)) ||
|
|
380
|
+
(u.kind === USE.MEMBER_W && !u.compound && !u.computed && schema.has(u.key)))
|
|
381
|
+
if (!flat) continue
|
|
382
|
+
|
|
383
|
+
// Materialize the parallel {names, values}: literal props first, then each
|
|
384
|
+
// extension field (value `undefined`), in first-write order.
|
|
385
|
+
const names = props.names.slice(), values = props.values.slice()
|
|
386
|
+
for (const k of schema)
|
|
387
|
+
if (!names.includes(k)) { names.push(k); values.push(undefined) }
|
|
388
|
+
cand.set(name, { names, values })
|
|
389
|
+
}
|
|
390
|
+
return cand
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* No-copy slice scan — which `let/const t = s.slice(...)` bindings can be a
|
|
395
|
+
* VIEW (a SLICE_BIT pointer straight into `s`'s buffer) instead of a fresh
|
|
396
|
+
* byte copy.
|
|
397
|
+
*
|
|
398
|
+
* jz rewinds the bump arena only at function exit, so every string the
|
|
399
|
+
* function can observe stays alive until it returns. A view is therefore sound
|
|
400
|
+
* exactly when its binding does NOT escape the function: `t` must never be
|
|
401
|
+
* returned, passed as a call argument, stored into a heap object/array,
|
|
402
|
+
* captured by a closure, aliased to another binding, reassigned, or
|
|
403
|
+
* compound-assigned. The permitted uses — receiver of a `.`/`[]`, operand of a
|
|
404
|
+
* comparison or `+`, a boolean test — read `t` synchronously and never persist
|
|
405
|
+
* it past the function.
|
|
406
|
+
*
|
|
407
|
+
* Declared exactly once as `let/const`. The result is purely structural —
|
|
408
|
+
* whether the receiver is actually a string (so `.slice` lowers to the string
|
|
409
|
+
* view) is settled later, at emit time, when param types are known; emitDecl
|
|
410
|
+
* keeps the ordinary copying slice for any non-string receiver. Conservative:
|
|
411
|
+
* any unrecognised position disqualifies the binding.
|
|
412
|
+
*
|
|
413
|
+
* Returns `Set<name>` of view-eligible binding names.
|
|
414
|
+
*/
|
|
415
|
+
// Permitted use-kinds for a slice view — the value is read synchronously and
|
|
416
|
+
// never persisted past the function. `MEMBER_R`/`MEMBER_W` cover any `.`/`[]`
|
|
417
|
+
// receiver; `COMPARE` any comparison; `CONCAT`/`BOOL_TEST` the copy / test
|
|
418
|
+
// positions. Any other kind (reassign, call arg, return, capture, bare alias)
|
|
419
|
+
// escapes and disqualifies the binding.
|
|
420
|
+
const _SLICE_VIEW_OK = new Set([USE.MEMBER_R, USE.MEMBER_W, USE.COMPARE, USE.CONCAT, USE.BOOL_TEST])
|
|
421
|
+
|
|
422
|
+
export function scanSliceViews(body) {
|
|
423
|
+
const isSliceCall = (n) =>
|
|
424
|
+
Array.isArray(n) && n[0] === '()' && Array.isArray(n[1])
|
|
425
|
+
&& n[1][0] === '.' && n[1][2] === 'slice'
|
|
426
|
+
|
|
427
|
+
const views = new Set()
|
|
428
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
429
|
+
if (s.decls !== 1 || !isSliceCall(s.initRhs)) continue
|
|
430
|
+
if (s.uses.every(u => _SLICE_VIEW_OK.has(u.kind))) views.add(name)
|
|
431
|
+
}
|
|
432
|
+
return views
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Narrow uint32 accumulator locals to unsigned i32. A local qualifies when its
|
|
437
|
+
* initializer is a non-negative integer literal in [0, 2^32), every
|
|
438
|
+
* reassignment is `name = (…) >>> k` (so it always holds a canonical uint32),
|
|
439
|
+
* and every read sits inside a `>>>` (ToUint32) sink reached only through
|
|
440
|
+
* bit-faithful operators (`^ & | ~ << >> + - *`). Under those constraints the
|
|
441
|
+
* raw i32 bit pattern reproduces JS semantics exactly — every observable use is
|
|
442
|
+
* funnelled through ToUint32 — so the f64 round-trip on the hot path is pure
|
|
443
|
+
* overhead. Names that escape (closures, bare `return`, signed-sensitive
|
|
444
|
+
* operands) keep their wider type. Returns the qualifying set; callers retype
|
|
445
|
+
* `locals` to 'i32' and tag `readVar` reads `.unsigned` for convert_i32_u.
|
|
446
|
+
*/
|
|
447
|
+
export function narrowUint32(body, locals) {
|
|
448
|
+
const TRANSPARENT = new Set(['^', '&', '|', '~', '<<', '>>', '+', '-', '*'])
|
|
449
|
+
const initLit = new Set() // names with a valid u32-literal initializer
|
|
450
|
+
const disq = new Set() // names disqualified by an unsafe occurrence
|
|
451
|
+
const seen = new Set()
|
|
452
|
+
const isU32Lit = e => {
|
|
453
|
+
const v = typeof e === 'number' ? e
|
|
454
|
+
: Array.isArray(e) && e[0] == null && typeof e[1] === 'number' ? e[1] : NaN
|
|
455
|
+
return Number.isInteger(v) && v >= 0 && v < 4294967296
|
|
456
|
+
}
|
|
457
|
+
const banNames = n => {
|
|
458
|
+
if (typeof n === 'string') disq.add(n)
|
|
459
|
+
else if (Array.isArray(n)) for (let i = 1; i < n.length; i++) banNames(n[i])
|
|
460
|
+
}
|
|
461
|
+
const walk = (node, underShr, inClosure) => {
|
|
462
|
+
if (typeof node === 'string') { if (inClosure) disq.add(node); return }
|
|
463
|
+
if (!Array.isArray(node)) return
|
|
464
|
+
const op = node[0]
|
|
465
|
+
if (typeof op !== 'string') {
|
|
466
|
+
for (let i = 1; i < node.length; i++) walk(node[i], false, inClosure)
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
if (op === '=>') { for (let i = 1; i < node.length; i++) walk(node[i], false, true); return }
|
|
470
|
+
if (op === 'let' || op === 'const') {
|
|
471
|
+
for (let i = 1; i < node.length; i++) {
|
|
472
|
+
const d = node[i]
|
|
473
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
|
|
474
|
+
const nm = d[1]
|
|
475
|
+
if (seen.has(nm) || inClosure || !isU32Lit(d[2])) disq.add(nm)
|
|
476
|
+
else initLit.add(nm)
|
|
477
|
+
seen.add(nm)
|
|
478
|
+
walk(d[2], false, inClosure)
|
|
479
|
+
} else if (typeof d === 'string') { disq.add(d); seen.add(d) }
|
|
480
|
+
else if (Array.isArray(d) && d[0] === '=') { banNames(d[1]); walk(d[2], false, inClosure) }
|
|
481
|
+
}
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
if ((op === '++' || op === '--') && typeof node[1] === 'string') { disq.add(node[1]); return }
|
|
485
|
+
if (ASSIGN_OPS.has(op)) {
|
|
486
|
+
const lhs = node[1]
|
|
487
|
+
if (typeof lhs === 'string') {
|
|
488
|
+
if (op !== '=' || inClosure || !(Array.isArray(node[2]) && node[2][0] === '>>>')) disq.add(lhs)
|
|
489
|
+
} else banNames(lhs)
|
|
490
|
+
walk(node[2], false, inClosure)
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
const childShr = op === '>>>' ? true : TRANSPARENT.has(op) ? underShr : false
|
|
494
|
+
for (let i = 1; i < node.length; i++) {
|
|
495
|
+
const c = node[i]
|
|
496
|
+
if (typeof c === 'string') { if (inClosure || !childShr) disq.add(c) }
|
|
497
|
+
else walk(c, childShr, inClosure)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
walk(body, false, false)
|
|
501
|
+
const result = new Set()
|
|
502
|
+
for (const nm of initLit) {
|
|
503
|
+
if (disq.has(nm)) continue
|
|
504
|
+
const t = locals.get(nm)
|
|
505
|
+
if (t !== 'i32' && t !== 'f64') continue
|
|
506
|
+
locals.set(nm, 'i32')
|
|
507
|
+
result.add(nm)
|
|
508
|
+
}
|
|
509
|
+
return result
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Operators under which a counter remains a *monotone, bounded* function of the
|
|
513
|
+
// index root: an affine index `base + i*stride` (and `i << k`) whose computed
|
|
514
|
+
// offset must fit i32-addressable wasm32 memory therefore bounds the counter to
|
|
515
|
+
// i32 range. `/ % & | ^ >> >>>` are excluded — they decouple the index magnitude
|
|
516
|
+
// from the counter (`arr[i & 7]` stays small however large `i` grows), so they
|
|
517
|
+
// prove nothing about the counter's range.
|
|
518
|
+
const AFFINE_INDEX_OPS = new Set(['+', '-', '*', '<<', 'u-'])
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Locals proven to stay within i32 range, so they need not widen to f64 when
|
|
522
|
+
* compared against an f64 loop bound. Keeping them i32 yields direct i32 indexing
|
|
523
|
+
* (no per-access `trunc_sat_f64_s`) and lets the relational compare coerce the
|
|
524
|
+
* counter instead — the compiler-inferred form of the manual `let n = N | 0` hoist.
|
|
525
|
+
*
|
|
526
|
+
* Two sound sources of an i32-range proof:
|
|
527
|
+
* 1. Direct: a local appears as an *affine* component of an array index. A valid
|
|
528
|
+
* wasm32 access requires the byte offset to fit i32, and an affine index is
|
|
529
|
+
* monotone in the local, so the local is i32-bounded for every non-trapping run.
|
|
530
|
+
* 2. Transitive (back-propagation): a local that flows — via affine
|
|
531
|
+
* assignment/step (`let i0 = ix`, `i0 += id`) — into an already-bounded index
|
|
532
|
+
* var is itself bounded by that var's range. This captures the common
|
|
533
|
+
* nested-loop shape where the outer bound seeds an inner index (FFT butterflies:
|
|
534
|
+
* `while (ix < N) { let i0 = ix; while (i0 < N) … x[i0] … i0 += id }`).
|
|
535
|
+
*
|
|
536
|
+
* Fractional locals are unaffected: this set only suppresses *comparison*-driven
|
|
537
|
+
* widening; the assignment fixpoint that follows still widens any local with an
|
|
538
|
+
* f64-typed RHS (`i = i / 3`), overriding membership here.
|
|
539
|
+
*/
|
|
540
|
+
// An integer literal that fits signed i32 — the only constant a promoted i32
|
|
541
|
+
// local may hold. A larger integer (`0xFFFFFFFF`, a NaN-box mask) is emitted as
|
|
542
|
+
// an f64.const, so treating it as an i32 leaf would store f64 into an i32 local.
|
|
543
|
+
const isI32Lit = (v) => typeof v === 'number' && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647
|
|
544
|
+
|
|
545
|
+
export function collectI32SafeIndexVars(body, locals) {
|
|
546
|
+
const safe = new Set()
|
|
547
|
+
// Collect names reachable from `node` through affine ops only, into `sink`.
|
|
548
|
+
const addAffine = (node, sink) => {
|
|
549
|
+
if (typeof node === 'string') { sink.add(node); return }
|
|
550
|
+
if (!Array.isArray(node)) return
|
|
551
|
+
if (AFFINE_INDEX_OPS.has(node[0])) for (let i = 1; i < node.length; i++) addAffine(node[i], sink)
|
|
552
|
+
}
|
|
553
|
+
// Pass 1: record assignment edges (back-prop) + a name→definitions map (for the
|
|
554
|
+
// integer-shape test). `+= …` reconstructs to `name + …` so its shape includes
|
|
555
|
+
// the prior value.
|
|
556
|
+
const edges = []
|
|
557
|
+
const defs = new Map()
|
|
558
|
+
const addDef = (name, rhs) => { (defs.get(name) ?? defs.set(name, []).get(name)).push(rhs) }
|
|
559
|
+
const collect = (node) => {
|
|
560
|
+
if (!Array.isArray(node)) return
|
|
561
|
+
const op = node[0]
|
|
562
|
+
if (op === 'let' || op === 'const') {
|
|
563
|
+
for (let i = 1; i < node.length; i++) {
|
|
564
|
+
const d = node[i]
|
|
565
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') { edges.push({ target: d[1], rhs: d[2] }); addDef(d[1], d[2]) }
|
|
566
|
+
}
|
|
567
|
+
} else if (op === '=' && typeof node[1] === 'string') { edges.push({ target: node[1], rhs: node[2] }); addDef(node[1], node[2]) }
|
|
568
|
+
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]]) }
|
|
569
|
+
if (op === '=>') return
|
|
570
|
+
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
571
|
+
}
|
|
572
|
+
collect(body)
|
|
573
|
+
|
|
574
|
+
// Integer-shaped AND i32-representable: provably an integer through `+ - * << u-`
|
|
575
|
+
// (AFFINE_INDEX_OPS — excludes `/`/`**`/fractional ops) over leaves that are
|
|
576
|
+
// i32-typed, i32-range integer literals, or other integer-shaped locals. Lets a
|
|
577
|
+
// hoisted offset `let o = y*w` (f64-typed product, integer-valued) qualify as an
|
|
578
|
+
// index leaf before narrowing. A fractional leaf, an out-of-i32-range literal, or
|
|
579
|
+
// a param of unknown type disqualifies — so no truncation and no f64.const→i32.
|
|
580
|
+
const isIntShaped = (node, seen) => {
|
|
581
|
+
if (typeof node === 'number') return isI32Lit(node)
|
|
582
|
+
if (typeof node === 'string') {
|
|
583
|
+
if (exprType(node, locals) === 'i32') return true
|
|
584
|
+
if (seen.has(node)) return true // recursion through a self-step — other defs still gate
|
|
585
|
+
const ds = defs.get(node)
|
|
586
|
+
if (!ds || !ds.length) return false // param / unknown source — not provably integer
|
|
587
|
+
seen.add(node)
|
|
588
|
+
const r = ds.every(d => isIntShaped(d, seen))
|
|
589
|
+
seen.delete(node)
|
|
590
|
+
return r
|
|
591
|
+
}
|
|
592
|
+
if (!Array.isArray(node)) return false
|
|
593
|
+
const op = node[0]
|
|
594
|
+
if (op == null) return isI32Lit(node[1]) // [null, value] literal
|
|
595
|
+
if (!AFFINE_INDEX_OPS.has(op)) return false
|
|
596
|
+
for (let i = 1; i < node.length; i++) if (node[i] != null && !isIntShaped(node[i], new Set(seen))) return false
|
|
597
|
+
return true
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Pass 2: seed from array indices already i32 OR integer-shaped (the latter
|
|
601
|
+
// rescues hoisted integer offsets the type pass left at f64). A fractional index
|
|
602
|
+
// (`mem[y*w+x]` with fractional `w`) is not integer-shaped → still truncs per
|
|
603
|
+
// access and is left to widen, preserving the prior guard.
|
|
604
|
+
const seed = (node) => {
|
|
605
|
+
if (!Array.isArray(node)) return
|
|
606
|
+
const op = node[0]
|
|
607
|
+
if (op === '[]' && !isLiteralStr(node[2]) && (exprType(node[2], locals) === 'i32' || isIntShaped(node[2], new Set()))) addAffine(node[2], safe)
|
|
608
|
+
if (op === '=>') return
|
|
609
|
+
for (let i = 1; i < node.length; i++) seed(node[i])
|
|
610
|
+
}
|
|
611
|
+
seed(body)
|
|
612
|
+
|
|
613
|
+
// Back-propagate to a fixpoint: feeders of a bounded index var are bounded.
|
|
614
|
+
let changed = true
|
|
615
|
+
while (changed) {
|
|
616
|
+
changed = false
|
|
617
|
+
for (const { target, rhs } of edges) {
|
|
618
|
+
if (!safe.has(target)) continue
|
|
619
|
+
const src = new Set()
|
|
620
|
+
addAffine(rhs, src)
|
|
621
|
+
for (const s of src) if (!safe.has(s)) { safe.add(s); changed = true }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Promote integer-shaped index feeders the type pass left at f64 (a hoisted
|
|
625
|
+
// `o = y*w`). The byte offset must fit i32-addressable memory, so the i32-wrap
|
|
626
|
+
// residue reproduces the true in-bounds value — same contract as inline `a[y*w+x]`.
|
|
627
|
+
// Skip boxed (closure-captured) cells — those live as f64 in memory.
|
|
628
|
+
for (const n of safe) if (locals.get(n) === 'f64' && !ctx.func.boxed?.has(n) && isIntShaped(n, new Set())) locals.set(n, 'i32')
|
|
629
|
+
return safe
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Locals that affinely feed an *f64-typed* array index (e.g. `mem[i*w + x]` with
|
|
634
|
+
* an f64 stride/global `w`). The access truncs the byte offset regardless, so
|
|
635
|
+
* keeping such a counter i32 buys no trunc savings and ADDS a per-iteration
|
|
636
|
+
* compare-convert — a net loss (the game-of-life regression). These are excluded
|
|
637
|
+
* from the integer-counter i32-keep in analyzeBody's widenPass, so they widen to
|
|
638
|
+
* f64 as before. (A counter used only in arithmetic — no f64 index — is NOT here,
|
|
639
|
+
* so it stays i32, where the i32 body + increment is the real win.)
|
|
640
|
+
*/
|
|
641
|
+
export function collectF64StridedIndexVars(body, locals) {
|
|
642
|
+
const set = new Set()
|
|
643
|
+
const addAffine = (node) => {
|
|
644
|
+
if (typeof node === 'string') { set.add(node); return }
|
|
645
|
+
if (Array.isArray(node) && AFFINE_INDEX_OPS.has(node[0])) for (let i = 1; i < node.length; i++) addAffine(node[i])
|
|
646
|
+
}
|
|
647
|
+
const walk = (node) => {
|
|
648
|
+
if (!Array.isArray(node)) return
|
|
649
|
+
if (node[0] === '[]' && !isLiteralStr(node[2]) && exprType(node[2], locals) === 'f64') addAffine(node[2])
|
|
650
|
+
if (node[0] === '=>') return
|
|
651
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
652
|
+
}
|
|
653
|
+
walk(body)
|
|
654
|
+
return set
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Returns the cached facts object directly — DO NOT MUTATE the returned maps.
|
|
659
|
+
* Callers that need to extend (e.g. add params to locals) must clone explicitly
|
|
660
|
+
* before mutating. Slice reads via `analyzeBody(body).<slice>`.
|
|
661
|
+
*/
|