jz 0.3.1 → 0.4.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 +15 -16
- package/cli.js +2 -1
- package/index.js +32 -15
- package/module/array.js +16 -7
- package/module/collection.js +28 -13
- package/module/core.js +16 -1
- package/module/json.js +8 -8
- package/module/math.js +21 -4
- package/module/object.js +104 -9
- package/module/string.js +15 -0
- package/package.json +7 -4
- package/src/analyze.js +2 -2
- package/src/assemble.js +31 -8
- package/src/codegen.js +177 -0
- package/src/compile.js +21 -12
- package/src/emit.js +6 -1
- package/src/fuse.js +159 -0
- package/src/ir.js +11 -4
- package/src/jzify.js +122 -175
- package/src/narrow.js +36 -9
- package/src/optimize.js +182 -22
- package/src/plan.js +10 -1
- package/src/prepare.js +2 -149
package/src/emit.js
CHANGED
|
@@ -536,7 +536,7 @@ export function emitDecl(...inits) {
|
|
|
536
536
|
ctx.func.locals.set(innerName, 'f64')
|
|
537
537
|
result.push(
|
|
538
538
|
['local.set', `$${innerName}`, ['local.get', `$${name}`]],
|
|
539
|
-
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]
|
|
539
|
+
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
|
|
540
540
|
['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
|
|
541
541
|
...schema.slice(1).map((_, j) =>
|
|
542
542
|
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
|
|
@@ -1879,6 +1879,7 @@ export const emitter = {
|
|
|
1879
1879
|
const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
|
|
1880
1880
|
if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
1881
1881
|
if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
1882
|
+
if (func.sig.unsignedResult) callIR.unsigned = true
|
|
1882
1883
|
return callIR
|
|
1883
1884
|
}
|
|
1884
1885
|
}
|
|
@@ -2282,6 +2283,7 @@ export const emitter = {
|
|
|
2282
2283
|
arrayIR], func.sig.results[0])
|
|
2283
2284
|
if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
2284
2285
|
if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
2286
|
+
if (func.sig.unsignedResult) callIR.unsigned = true
|
|
2285
2287
|
return callIR
|
|
2286
2288
|
}
|
|
2287
2289
|
|
|
@@ -2302,6 +2304,9 @@ export const emitter = {
|
|
|
2302
2304
|
const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
|
|
2303
2305
|
if (func?.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
2304
2306
|
if (func?.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
2307
|
+
// Unsigned-uint32 result (every tail was `>>>`): consumer's asF64 must use
|
|
2308
|
+
// `f64.convert_i32_u` instead of `_s`, preserving the [0, 2^32) range.
|
|
2309
|
+
if (func?.sig.unsignedResult) callIR.unsigned = true
|
|
2305
2310
|
return callIR
|
|
2306
2311
|
}
|
|
2307
2312
|
|
package/src/fuse.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-level fusion passes.
|
|
3
|
+
*
|
|
4
|
+
* Unlike src/optimize.js (which is a pure WAT IR→IR rewrite, post-emission),
|
|
5
|
+
* these rewrites need the *raw, pre-resolution* AST shape — bindings still named,
|
|
6
|
+
* arrow bodies still inline — so they run inside prepare(), before scope
|
|
7
|
+
* resolution and emit. They mutate the AST in place and always fire (cheap; the
|
|
8
|
+
* shape guards are strict enough that misfires are impossible).
|
|
9
|
+
*
|
|
10
|
+
* @module fuse
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
|
|
14
|
+
* into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
|
|
15
|
+
* intermediate array. Only fires on shapes where every use of `b` is a numeric `b[idx]` read or a
|
|
16
|
+
* `b.length` read, the arrow is pure with a single named param, and `b` is not referenced after the
|
|
17
|
+
* consumer for-loop. Preserves observable behavior because the arrow's pure-expression body has no
|
|
18
|
+
* order-dependent effects. */
|
|
19
|
+
export function fuseSparseMapReads(root) {
|
|
20
|
+
walkSparse(root)
|
|
21
|
+
}
|
|
22
|
+
function walkSparse(node) {
|
|
23
|
+
if (!Array.isArray(node)) return
|
|
24
|
+
for (let i = 1; i < node.length; i++) walkSparse(node[i])
|
|
25
|
+
if (node[0] === ';') tryFuseInBlock(node)
|
|
26
|
+
}
|
|
27
|
+
function tryFuseInBlock(seq) {
|
|
28
|
+
for (let i = 1; i < seq.length - 1; i++) {
|
|
29
|
+
const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
|
|
30
|
+
if (fused) {
|
|
31
|
+
seq.splice(i, 2, ...fused)
|
|
32
|
+
i-- // re-examine same position (chained fusions)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function tryFusePair(decl, forNode, seq, declIdx) {
|
|
37
|
+
if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
|
|
38
|
+
if (decl.length !== 2) return null // single binding only
|
|
39
|
+
const bind = decl[1]
|
|
40
|
+
if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
|
|
41
|
+
const NAME = bind[1], rhs = bind[2]
|
|
42
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()') return null
|
|
43
|
+
const callee = rhs[1]
|
|
44
|
+
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
|
|
45
|
+
const RECV = callee[1]
|
|
46
|
+
if (typeof RECV !== 'string' || RECV === NAME) return null
|
|
47
|
+
const arrow = rhs[2]
|
|
48
|
+
if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
|
|
49
|
+
// Single-name param only: `x => …` or `(x) => …`
|
|
50
|
+
const ap = arrow[1]
|
|
51
|
+
const PARAM = typeof ap === 'string' ? ap :
|
|
52
|
+
(Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
|
|
53
|
+
if (!PARAM || PARAM === NAME || PARAM === RECV) return null
|
|
54
|
+
// Body: single-expression arrow only (block bodies skipped — could extend later).
|
|
55
|
+
const aBody = arrow[2]
|
|
56
|
+
if (Array.isArray(aBody) && aBody[0] === '{}') return null
|
|
57
|
+
if (!isPureSparseArrowBody(aBody, PARAM)) return null
|
|
58
|
+
// For-loop: ['for', [';', initStmt, cond, inc], body]
|
|
59
|
+
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
|
|
60
|
+
const head = forNode[1]
|
|
61
|
+
if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
|
|
62
|
+
const cond = head[2], forBody = forNode[2]
|
|
63
|
+
// Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
|
|
64
|
+
if (!hasOnlySparseUses(cond, NAME)) return null
|
|
65
|
+
if (!hasOnlySparseUses(forBody, NAME)) return null
|
|
66
|
+
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
67
|
+
// `NAME` must not be read after the for-loop in the same block.
|
|
68
|
+
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
69
|
+
if (refsName(seq[k], NAME)) return null
|
|
70
|
+
}
|
|
71
|
+
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
72
|
+
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
73
|
+
// PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
|
|
74
|
+
if (bindsName(forNode, PARAM)) return null
|
|
75
|
+
// Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
|
|
76
|
+
const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
|
|
77
|
+
const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
|
|
78
|
+
const newHead = [';', head[1], newCond, head[3]]
|
|
79
|
+
return [['for', newHead, newBody]]
|
|
80
|
+
}
|
|
81
|
+
function isPureSparseArrowBody(n, PARAM) {
|
|
82
|
+
if (typeof n === 'string') return true
|
|
83
|
+
if (!Array.isArray(n)) return true
|
|
84
|
+
const op = n[0]
|
|
85
|
+
// Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
|
|
86
|
+
if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
|
|
87
|
+
if (op === '=>') return false // nested closure is opaque
|
|
88
|
+
if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
|
|
89
|
+
if (op === '=') return false
|
|
90
|
+
for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
function hasOnlySparseUses(n, NAME) {
|
|
94
|
+
if (typeof n === 'string') return n !== NAME
|
|
95
|
+
if (!Array.isArray(n)) return true
|
|
96
|
+
const op = n[0]
|
|
97
|
+
if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
|
|
98
|
+
if (op === '.' && n[1] === NAME) {
|
|
99
|
+
if (n[2] === 'length') return true
|
|
100
|
+
return false // any other property access on NAME is opaque
|
|
101
|
+
}
|
|
102
|
+
for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
function hasAnyIndexedRead(n, NAME) {
|
|
106
|
+
if (!Array.isArray(n)) return false
|
|
107
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
|
|
108
|
+
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
function refsName(n, NAME) {
|
|
112
|
+
if (typeof n === 'string') return n === NAME
|
|
113
|
+
if (!Array.isArray(n)) return false
|
|
114
|
+
for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
function assignsName(n, NAME) {
|
|
118
|
+
if (!Array.isArray(n)) return false
|
|
119
|
+
const op = n[0]
|
|
120
|
+
if ((op === '=' || op === '++' || op === '--' ||
|
|
121
|
+
(typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
122
|
+
&& n[1] === NAME) return true
|
|
123
|
+
for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
function bindsName(n, NAME) {
|
|
127
|
+
if (!Array.isArray(n)) return false
|
|
128
|
+
const op = n[0]
|
|
129
|
+
if ((op === 'let' || op === 'const')) {
|
|
130
|
+
for (let i = 1; i < n.length; i++) {
|
|
131
|
+
const bind = n[i]
|
|
132
|
+
if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (op === '=>') {
|
|
136
|
+
const p = n[1]
|
|
137
|
+
if (p === NAME) return true
|
|
138
|
+
if (Array.isArray(p)) {
|
|
139
|
+
if (p[0] === '()' && p[1] === NAME) return true
|
|
140
|
+
// skip deeper destructuring forms — conservative
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
function substSparse(n, NAME, RECV, PARAM, arrowBody) {
|
|
147
|
+
if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
|
|
148
|
+
if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
|
|
149
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
|
|
150
|
+
const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
|
|
151
|
+
return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
|
|
152
|
+
}
|
|
153
|
+
return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
|
|
154
|
+
}
|
|
155
|
+
function cloneAndBind(node, PARAM, replacement) {
|
|
156
|
+
if (node === PARAM) return replacement
|
|
157
|
+
if (!Array.isArray(node)) return node
|
|
158
|
+
return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
|
|
159
|
+
}
|
package/src/ir.js
CHANGED
|
@@ -391,7 +391,8 @@ export function truthyIR(e) {
|
|
|
391
391
|
}
|
|
392
392
|
// Fresh pointer constructors never produce nullish. Treat as always truthy.
|
|
393
393
|
if (e[0] === 'call' && typeof e[1] === 'string' &&
|
|
394
|
-
(e[1].startsWith('$__mkptr') || e[1] === '$__alloc' ||
|
|
394
|
+
(e[1].startsWith('$__mkptr') || e[1] === '$__alloc' ||
|
|
395
|
+
e[1] === '$__alloc_hdr' || e[1].startsWith('$__alloc_hdr_'))) {
|
|
395
396
|
return typed(['i32.const', 1], 'i32')
|
|
396
397
|
}
|
|
397
398
|
// Pointer-typed local reads: value is never a plain number — truthy iff not nullish.
|
|
@@ -647,11 +648,17 @@ export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
|
|
|
647
648
|
* ptr — f64 IR expression: __mkptr(type, aux, local).
|
|
648
649
|
* Caller emits init, fills via local, then uses ptr (or local for further work). */
|
|
649
650
|
export function allocPtr({ type, aux = 0, len, cap, stride = 8, tag = 'ap' }) {
|
|
650
|
-
|
|
651
|
+
// stride=8 (f64 slots — Array/HASH/OBJECT) hits the specialized __alloc_hdr which
|
|
652
|
+
// hardcodes the multiply. Everything else (Set:16, Map probe:24, raw bytes:1) goes
|
|
653
|
+
// through the generic __alloc_hdr_n(len, cap, stride).
|
|
651
654
|
const local = tempI32(tag)
|
|
652
655
|
const irOf = v => typeof v === 'number' ? ['i32.const', v] : v
|
|
653
|
-
const
|
|
654
|
-
|
|
656
|
+
const args = [irOf(len), irOf(cap == null ? len : cap)]
|
|
657
|
+
let helper
|
|
658
|
+
if (stride === 8) helper = '__alloc_hdr'
|
|
659
|
+
else { helper = '__alloc_hdr_n'; args.push(['i32.const', stride]) }
|
|
660
|
+
inc(helper)
|
|
661
|
+
const init = ['local.set', `$${local}`, ['call', '$' + helper, ...args]]
|
|
655
662
|
const ptr = mkPtrIR(type, aux, ['local.get', `$${local}`])
|
|
656
663
|
return { local, init, ptr }
|
|
657
664
|
}
|
package/src/jzify.js
CHANGED
|
@@ -31,7 +31,7 @@ export default function jzify(ast) {
|
|
|
31
31
|
const names = new Set()
|
|
32
32
|
ast = hoistVars(ast, names)
|
|
33
33
|
if (names.size) ast = prependDecls(ast, names)
|
|
34
|
-
return foldStaticExportHelpers(transformScope(ast))
|
|
34
|
+
return foldStaticExportHelpers(canonicalizeObjectIdioms(transformScope(ast)))
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -80,7 +80,10 @@ function hoistVars(node, names) {
|
|
|
80
80
|
if (op === 'for') {
|
|
81
81
|
const head = node[1]
|
|
82
82
|
let h2
|
|
83
|
-
|
|
83
|
+
const normalizedHead = normalizeForDeclHead(head, names)
|
|
84
|
+
if (normalizedHead) {
|
|
85
|
+
h2 = normalizedHead
|
|
86
|
+
} else if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
|
|
84
87
|
(head[1][0] === 'in' || head[1][0] === 'of') && typeof head[1][1] === 'string') {
|
|
85
88
|
names.add(head[1][1])
|
|
86
89
|
h2 = [head[1][0], head[1][1], hoistVars(head[1][2], names)]
|
|
@@ -137,6 +140,27 @@ function prependDecls(body, names) {
|
|
|
137
140
|
return body == null ? decl : [';', decl, body]
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
function normalizeForDeclHead(head, names) {
|
|
144
|
+
if (!Array.isArray(head) || (head[0] !== 'var' && head[0] !== 'let' && head[0] !== 'const') || head.length !== 2) return null
|
|
145
|
+
const kind = head[0]
|
|
146
|
+
const expr = head[1]
|
|
147
|
+
if (!Array.isArray(expr)) return null
|
|
148
|
+
if (expr.length >= 3 && Array.isArray(expr[1]) &&
|
|
149
|
+
(expr[1][0] === 'in' || expr[1][0] === 'of') && typeof expr[1][1] === 'string') {
|
|
150
|
+
const iter = expr[1]
|
|
151
|
+
return [iter[0], normalizeForDecl(kind, iter[1], names), hoistVars([expr[0], iter[2], ...expr.slice(2)], names)]
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeForDecl(kind, name, names) {
|
|
157
|
+
if (kind === 'var') {
|
|
158
|
+
names.add(name)
|
|
159
|
+
return name
|
|
160
|
+
}
|
|
161
|
+
return [kind, name]
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
/** Convert a named function declaration to a hoisted const arrow */
|
|
141
165
|
function hoistFnDecl(name, params, body) {
|
|
142
166
|
const [p2, b2] = lowerArguments(params, body)
|
|
@@ -507,12 +531,16 @@ const handlers = {
|
|
|
507
531
|
const clean = cases.map(c => {
|
|
508
532
|
if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
|
|
509
533
|
const body = c[2].slice(1).filter(s => typeof s !== 'number')
|
|
510
|
-
|
|
534
|
+
const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
|
|
535
|
+
return ['case', c[1], stripped]
|
|
511
536
|
}
|
|
512
537
|
if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
|
|
513
538
|
const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
|
|
514
|
-
|
|
539
|
+
const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
|
|
540
|
+
return ['default', stripped]
|
|
515
541
|
}
|
|
542
|
+
if (c[0] === 'case') return ['case', c[1], stripTerminalSwitchBreak(c[2])]
|
|
543
|
+
if (c[0] === 'default') return ['default', stripTerminalSwitchBreak(c[1])]
|
|
516
544
|
return c
|
|
517
545
|
})
|
|
518
546
|
return transformSwitch(disc, clean)
|
|
@@ -589,7 +617,10 @@ function transform(node) {
|
|
|
589
617
|
const [op, ...args] = node
|
|
590
618
|
if (op == null) return node
|
|
591
619
|
const h = handlers[op]
|
|
592
|
-
|
|
620
|
+
// A handler that returns nullish (including no `return`) means "no rewrite at
|
|
621
|
+
// this node" — fall through to a generic recurse. `??` (not `||`) so handlers
|
|
622
|
+
// like `'==='` can legitimately return `0`.
|
|
623
|
+
return (h && h(...args)) ?? [op, ...args.map(transform)]
|
|
593
624
|
}
|
|
594
625
|
|
|
595
626
|
// Esbuild emits a small ESM helper:
|
|
@@ -713,12 +744,98 @@ function replaceStaticExportReads(node, rewrites) {
|
|
|
713
744
|
return node.map((part, i) => i === 0 ? part : replaceStaticExportReads(part, rewrites))
|
|
714
745
|
}
|
|
715
746
|
|
|
747
|
+
function canonicalizeObjectIdioms(node) {
|
|
748
|
+
if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
|
|
749
|
+
|
|
750
|
+
const out = node.map((part, i) => i === 0 ? part : canonicalizeObjectIdioms(part))
|
|
751
|
+
|
|
752
|
+
const hasOwnCall = objectHasOwnPropertyCall(out)
|
|
753
|
+
if (hasOwnCall) return ['()', ['.', hasOwnCall.obj, 'hasOwnProperty'], hasOwnCall.key]
|
|
754
|
+
|
|
755
|
+
if (out[0] === '&&') {
|
|
756
|
+
const leftCtor = constructorIsObject(out[1])
|
|
757
|
+
const rightKeys = objectKeysLengthZero(out[2])
|
|
758
|
+
if (leftCtor && rightKeys && astEqual(leftCtor.obj, rightKeys.obj)) return out[2]
|
|
759
|
+
|
|
760
|
+
const leftKeys = objectKeysLengthZero(out[1])
|
|
761
|
+
const rightCtor = constructorIsObject(out[2])
|
|
762
|
+
if (leftKeys && rightCtor && astEqual(leftKeys.obj, rightCtor.obj)) return out[1]
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return out
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function objectHasOwnPropertyCall(node) {
|
|
769
|
+
if (!Array.isArray(node) || node[0] !== '()') return null
|
|
770
|
+
const callee = node[1]
|
|
771
|
+
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'call') return null
|
|
772
|
+
if (!Array.isArray(callee[1]) || callee[1][0] !== '.' || callee[1][1] !== 'Object' || callee[1][2] !== 'hasOwnProperty') return null
|
|
773
|
+
const args = callArgs(node.slice(2))
|
|
774
|
+
if (args.length < 2) return null
|
|
775
|
+
return { obj: args[0], key: args[1] }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function constructorIsObject(node) {
|
|
779
|
+
if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
|
|
780
|
+
const left = constructorReceiver(node[1])
|
|
781
|
+
if (left && node[2] === 'Object') return { obj: left }
|
|
782
|
+
const right = constructorReceiver(node[2])
|
|
783
|
+
if (right && node[1] === 'Object') return { obj: right }
|
|
784
|
+
return null
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function constructorReceiver(node) {
|
|
788
|
+
return Array.isArray(node) && node[0] === '.' && node[2] === 'constructor' ? node[1] : null
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function objectKeysLengthZero(node) {
|
|
792
|
+
if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
|
|
793
|
+
const left = objectKeysLengthReceiver(node[1])
|
|
794
|
+
if (left && isZeroLiteral(node[2])) return { obj: left }
|
|
795
|
+
const right = objectKeysLengthReceiver(node[2])
|
|
796
|
+
if (right && isZeroLiteral(node[1])) return { obj: right }
|
|
797
|
+
return null
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function objectKeysLengthReceiver(node) {
|
|
801
|
+
if (!Array.isArray(node) || node[0] !== '.' || node[2] !== 'length') return null
|
|
802
|
+
const call = node[1]
|
|
803
|
+
if (!Array.isArray(call) || call[0] !== '()') return null
|
|
804
|
+
const callee = call[1]
|
|
805
|
+
if (!Array.isArray(callee) || callee[0] !== '.' || callee[1] !== 'Object' || callee[2] !== 'keys') return null
|
|
806
|
+
const args = callArgs(call.slice(2))
|
|
807
|
+
return args.length === 1 ? args[0] : null
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function isZeroLiteral(node) {
|
|
811
|
+
return Array.isArray(node) && node[0] == null && node[1] === 0
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function astEqual(a, b) {
|
|
815
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
816
|
+
}
|
|
817
|
+
|
|
716
818
|
function cloneAst(node) {
|
|
717
819
|
if (node == null || typeof node !== 'object') return node
|
|
718
820
|
if (!Array.isArray(node)) return node
|
|
719
821
|
return node.map(cloneAst)
|
|
720
822
|
}
|
|
721
823
|
|
|
824
|
+
function stripTerminalSwitchBreak(body) {
|
|
825
|
+
if (!Array.isArray(body)) return body
|
|
826
|
+
if (body[0] === 'break') return null
|
|
827
|
+
if (body[0] === '{}') {
|
|
828
|
+
const inner = stripTerminalSwitchBreak(body[1])
|
|
829
|
+
if (inner == null) return ['{}', [';']]
|
|
830
|
+
return ['{}', Array.isArray(inner) && inner[0] === ';' ? inner : [';', inner]]
|
|
831
|
+
}
|
|
832
|
+
if (body[0] !== ';') return body
|
|
833
|
+
|
|
834
|
+
const stmts = body.slice(1)
|
|
835
|
+
if (Array.isArray(stmts.at(-1)) && stmts.at(-1)[0] === 'break') stmts.pop()
|
|
836
|
+
return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
|
|
837
|
+
}
|
|
838
|
+
|
|
722
839
|
/** Transform switch statement to if/else chain. */
|
|
723
840
|
let swIdx = 0
|
|
724
841
|
function transformSwitch(discriminant, cases) {
|
|
@@ -742,173 +859,3 @@ function transformSwitch(discriminant, cases) {
|
|
|
742
859
|
if (chain) stmts.push(chain)
|
|
743
860
|
return [';', ...stmts]
|
|
744
861
|
}
|
|
745
|
-
|
|
746
|
-
// === AST → jz source codegen ===
|
|
747
|
-
|
|
748
|
-
const INDENT = ' '
|
|
749
|
-
const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
|
|
750
|
-
'??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
|
|
751
|
-
'<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
|
|
752
|
-
'+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
|
|
753
|
-
|
|
754
|
-
/** Wrap statement in { } if not already a block */
|
|
755
|
-
function wrapBlock(node, depth) {
|
|
756
|
-
if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
|
|
757
|
-
return '{ ' + codegen(node, depth) + '; }'
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/** Generate jz source from AST. Enforces semicolons. */
|
|
761
|
-
export function codegen(node, depth = 0) {
|
|
762
|
-
if (node == null) return ''
|
|
763
|
-
if (typeof node === 'number') return String(node)
|
|
764
|
-
if (typeof node === 'bigint') return node + 'n'
|
|
765
|
-
if (typeof node === 'string') return node
|
|
766
|
-
if (!Array.isArray(node)) return String(node)
|
|
767
|
-
|
|
768
|
-
const [op, ...a] = node
|
|
769
|
-
const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
|
|
770
|
-
|
|
771
|
-
// Literal: [, value]
|
|
772
|
-
if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
|
|
773
|
-
|
|
774
|
-
// Statements
|
|
775
|
-
if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
|
|
776
|
-
if (op === '{}') {
|
|
777
|
-
// Discriminate object literal / destructuring pattern from block.
|
|
778
|
-
// Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
|
|
779
|
-
// lone string shorthand. Empty `{}` outputs the same string either way.
|
|
780
|
-
const body = a[0]
|
|
781
|
-
const isObjItem = (n) => typeof n === 'string' ||
|
|
782
|
-
(Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
|
|
783
|
-
(n[0] === '=' && typeof n[1] === 'string')))
|
|
784
|
-
const isObj = body == null ? false
|
|
785
|
-
: typeof body === 'string' ? true
|
|
786
|
-
: Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
|
|
787
|
-
(body[0] === ',' && body.slice(1).every(isObjItem)))
|
|
788
|
-
if (isObj) {
|
|
789
|
-
if (typeof body === 'string') return '{ ' + body + ' }'
|
|
790
|
-
if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
|
|
791
|
-
return '{ ' + codegen(body) + ' }'
|
|
792
|
-
}
|
|
793
|
-
// Block: body is null, a single statement, or [';', ...stmts]
|
|
794
|
-
const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
|
|
795
|
-
const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
|
|
796
|
-
return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Declarations
|
|
800
|
-
if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
|
|
801
|
-
if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
|
|
802
|
-
if (op === 'default') return 'default ' + codegen(a[0], depth)
|
|
803
|
-
|
|
804
|
-
// Control flow
|
|
805
|
-
if (op === 'if') {
|
|
806
|
-
const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
|
|
807
|
-
return a[2] != null
|
|
808
|
-
? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
|
|
809
|
-
: 'if (' + cond + ') ' + then
|
|
810
|
-
}
|
|
811
|
-
if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
|
|
812
|
-
if (op === 'for') {
|
|
813
|
-
if (a.length === 2) { // ['for', head, body] — subscript shape
|
|
814
|
-
const [head, body] = a
|
|
815
|
-
if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
|
|
816
|
-
return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
|
|
817
|
-
// ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
|
|
818
|
-
if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
|
|
819
|
-
return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
|
|
820
|
-
// C-style head [';', init, cond, update] is positional — empty slots are valid,
|
|
821
|
-
// must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
|
|
822
|
-
if (Array.isArray(head) && head[0] === ';')
|
|
823
|
-
return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
|
|
824
|
-
return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
|
|
825
|
-
}
|
|
826
|
-
return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
|
|
827
|
-
}
|
|
828
|
-
if (op === 'return') return 'return ' + codegen(a[0])
|
|
829
|
-
if (op === 'throw') return 'throw ' + codegen(a[0])
|
|
830
|
-
if (op === 'break') return 'break'
|
|
831
|
-
if (op === 'continue') return 'continue'
|
|
832
|
-
// catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
|
|
833
|
-
if (op === 'catch') {
|
|
834
|
-
if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
|
|
835
|
-
return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Arrow
|
|
839
|
-
if (op === '=>') {
|
|
840
|
-
// Params: already wrapped in () by parser, or bare name
|
|
841
|
-
const p = a[0]
|
|
842
|
-
const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
|
|
843
|
-
const body = a[1]
|
|
844
|
-
const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
|
|
845
|
-
const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
|
|
846
|
-
? '{ ' + codegen(body, depth) + '; }'
|
|
847
|
-
: codegen(body, depth)
|
|
848
|
-
return params + ' => ' + bodyStr
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Grouping parens / function call
|
|
852
|
-
if (op === '()') {
|
|
853
|
-
if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
|
|
854
|
-
return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// Property access
|
|
858
|
-
if (op === '.') return codegen(a[0]) + '.' + a[1]
|
|
859
|
-
if (op === '?.') return codegen(a[0]) + '?.' + a[1]
|
|
860
|
-
if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
|
|
861
|
-
if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
862
|
-
if (op === '[]') {
|
|
863
|
-
// Array literal: ['[]', body] (length 2 → a.length 1). body may be null (empty),
|
|
864
|
-
// a single element, or a [',', ...items] sequence.
|
|
865
|
-
if (a.length === 1) {
|
|
866
|
-
if (a[0] == null) return '[]'
|
|
867
|
-
const body = a[0]
|
|
868
|
-
if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
|
|
869
|
-
return '[' + codegen(body) + ']'
|
|
870
|
-
}
|
|
871
|
-
// Subscript: ['[]', obj, idx]
|
|
872
|
-
return codegen(a[0]) + '[' + codegen(a[1]) + ']'
|
|
873
|
-
}
|
|
874
|
-
if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
|
|
875
|
-
if (op === 'str') return JSON.stringify(a[0])
|
|
876
|
-
if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
|
|
877
|
-
|
|
878
|
-
// Comma
|
|
879
|
-
if (op === ',') return a.map(x => codegen(x)).join(', ')
|
|
880
|
-
// Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
|
|
881
|
-
if (op === '`') return '`' + a.map(p => {
|
|
882
|
-
if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
|
|
883
|
-
return '${' + codegen(p) + '}'
|
|
884
|
-
}).join('') + '`'
|
|
885
|
-
|
|
886
|
-
// Spread
|
|
887
|
-
if (op === '...') return '...' + codegen(a[0])
|
|
888
|
-
|
|
889
|
-
// Import / export rename
|
|
890
|
-
if (op === 'import') return 'import ' + codegen(a[0])
|
|
891
|
-
if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
|
|
892
|
-
if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
|
|
893
|
-
|
|
894
|
-
// Unary prefix
|
|
895
|
-
if (a.length === 1) {
|
|
896
|
-
if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
|
|
897
|
-
if (op === 'typeof') return 'typeof ' + codegen(a[0])
|
|
898
|
-
if (op === 'u-') return '-' + codegen(a[0])
|
|
899
|
-
if (op === 'u+') return '+' + codegen(a[0])
|
|
900
|
-
return op + codegen(a[0])
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// Postfix
|
|
904
|
-
if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
|
|
905
|
-
|
|
906
|
-
// Binary
|
|
907
|
-
if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
|
|
908
|
-
|
|
909
|
-
// Ternary
|
|
910
|
-
if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
|
|
911
|
-
|
|
912
|
-
// Fallback
|
|
913
|
-
return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
|
|
914
|
-
}
|
package/src/narrow.js
CHANGED
|
@@ -192,11 +192,20 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
|
192
192
|
function refreshCallerLocals(callerCtx) {
|
|
193
193
|
for (const func of ctx.func.list) {
|
|
194
194
|
if (!func.body || func.raw) continue
|
|
195
|
+
// Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
|
|
196
|
+
// `n = arr.length` (arr a TYPED/BUFFER pointer param) as an i32 local — without
|
|
197
|
+
// this, post-G `refreshCallerLocals` still walks bodies with arr untyped, the
|
|
198
|
+
// length stays f64, and any callee taking that length never gets an i32 param
|
|
199
|
+
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
200
|
+
// emit time, so this transient repByLocal doesn't leak past narrowing.
|
|
201
|
+
ctx.func.repByLocal = new Map()
|
|
202
|
+
for (const p of func.sig.params) if (p.ptrKind != null) ctx.func.repByLocal.set(p.name, { val: p.ptrKind })
|
|
195
203
|
invalidateLocalsCache(func.body)
|
|
196
204
|
const fresh = analyzeLocals(func.body)
|
|
197
205
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
198
206
|
callerCtx.get(func).callerLocals = fresh
|
|
199
207
|
}
|
|
208
|
+
ctx.func.repByLocal = null
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
function resetParamWasmFacts(paramReps) {
|
|
@@ -361,10 +370,24 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
361
370
|
},
|
|
362
371
|
},
|
|
363
372
|
])
|
|
373
|
+
// Transitive ctor/schema propagation down call chains. A naive single-pass
|
|
374
|
+
// mergeRule poisons a callee's param on the *first* sweep if the caller's own
|
|
375
|
+
// param (the very thing that supplies the ctor) hasn't been typed yet — and the
|
|
376
|
+
// poison is sticky, so later sweeps can't recover. Two-pass was the old patch;
|
|
377
|
+
// it still loses any chain deeper than `main→f→g→h` (e.g. heapsort's siftDown).
|
|
378
|
+
// Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
|
|
379
|
+
// as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
|
|
380
|
+
// poisons params whose call sites still can't be proven (genuinely-untyped args).
|
|
364
381
|
const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)])
|
|
382
|
+
const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
|
|
383
|
+
let changed
|
|
384
|
+
const bump = (r, v) => { if (v == null || r[field] === null) return; const b = r[field]; mergeParamFact(r, field, v); if (r[field] !== b) changed = true }
|
|
385
|
+
const soft = {
|
|
386
|
+
missing(r, k, state) { const def = defaultArg(state, k); if (def != null) bump(r, infer(def, k, state)) },
|
|
387
|
+
apply(r, arg, k, state) { bump(r, infer(arg, k, state)) },
|
|
388
|
+
}
|
|
389
|
+
do { changed = false; runCallsiteLattice([soft]) } while (changed)
|
|
390
|
+
runCallsiteLattice([mergeRule(field, infer)])
|
|
368
391
|
}
|
|
369
392
|
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArgArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
370
393
|
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArgArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
@@ -423,18 +446,22 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
423
446
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
424
447
|
const exprs = returnExprs(body)
|
|
425
448
|
if (!exprs.length) continue
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
449
|
+
// Narrow result to i32 when every return-tail types as i32 (incl. bitwise ops, `|0`,
|
|
450
|
+
// and `>>>`). When ANY tail is `>>>` (unsigned uint32 idiom) we still narrow, but tag
|
|
451
|
+
// `sig.unsignedResult` so the call-site rebox uses `f64.convert_i32_u` — preserving
|
|
452
|
+
// the [0, 2^32) range that `(x >>> 0)` carries through hash/CRC finalizers.
|
|
453
|
+
const anyUnsigned = exprs.some(e => Array.isArray(e) && e[0] === '>>>')
|
|
431
454
|
const savedCurrent = ctx.func.current
|
|
432
455
|
ctx.func.current = func.sig
|
|
433
456
|
const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
|
|
434
457
|
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
435
458
|
const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
|
|
436
459
|
ctx.func.current = savedCurrent
|
|
437
|
-
if (allI32) {
|
|
460
|
+
if (allI32) {
|
|
461
|
+
func.sig.results = ['i32']
|
|
462
|
+
if (anyUnsigned) func.sig.unsignedResult = true
|
|
463
|
+
changed = true
|
|
464
|
+
}
|
|
438
465
|
}
|
|
439
466
|
}
|
|
440
467
|
|