jz 0.3.0 → 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 +24 -14
- package/cli.js +37 -8
- 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 +165 -178
- 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)
|
|
@@ -210,13 +234,35 @@ function transformScope(node) {
|
|
|
210
234
|
// Hoist functions AFTER imports (imports must be processed first for scope resolution)
|
|
211
235
|
const imports = rest.filter(s => Array.isArray(s) && s[0] === 'import')
|
|
212
236
|
const nonImports = rest.filter(s => !(Array.isArray(s) && s[0] === 'import'))
|
|
213
|
-
const all = [...imports, ...hoisted, ...nonImports]
|
|
237
|
+
const all = dedupeRedecls([...imports, ...hoisted, ...nonImports])
|
|
214
238
|
return all.length === 0 ? null : all.length === 1 ? all[0] : [';', ...all]
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
return transform(node)
|
|
218
242
|
}
|
|
219
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Drop redundant re-declarations of the same name within one scope's statement
|
|
246
|
+
* list. JS allows `function f(){} var f;`, `var x; var x;`, `var x = 1; var x;` —
|
|
247
|
+
* jzify lowers `function`→`const` and `var`→`let`, which would otherwise emit two
|
|
248
|
+
* bindings for one slot (and a typed-slot clash in codegen). The first declaration
|
|
249
|
+
* wins; a later redeclaration keeps only its initializer, as a plain assignment.
|
|
250
|
+
*/
|
|
251
|
+
function dedupeRedecls(stmts) {
|
|
252
|
+
const nameOf = s => Array.isArray(s) && (s[0] === 'let' || s[0] === 'const' || s[0] === 'var')
|
|
253
|
+
? (typeof s[1] === 'string' ? s[1]
|
|
254
|
+
: Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' ? s[1][1] : null)
|
|
255
|
+
: null
|
|
256
|
+
const seen = new Set(), out = []
|
|
257
|
+
for (const s of stmts) {
|
|
258
|
+
const n = nameOf(s)
|
|
259
|
+
if (n == null) { out.push(s); continue }
|
|
260
|
+
if (seen.has(n)) { if (Array.isArray(s[1]) && s[1][0] === '=') out.push(['=', s[1][1], s[1][2]]); continue }
|
|
261
|
+
seen.add(n); out.push(s)
|
|
262
|
+
}
|
|
263
|
+
return out
|
|
264
|
+
}
|
|
265
|
+
|
|
220
266
|
/** Wrap function body for arrow conversion */
|
|
221
267
|
function wrapArrowBody(body) {
|
|
222
268
|
const t = transformScope(body)
|
|
@@ -247,6 +293,19 @@ function usesArguments(node) {
|
|
|
247
293
|
return false
|
|
248
294
|
}
|
|
249
295
|
|
|
296
|
+
// `arguments` is the implicit object only if the function body doesn't declare a
|
|
297
|
+
// local of that name. Scan the body's own statement list (not nested scopes) for
|
|
298
|
+
// `var/let/const arguments` — a regular `function` with `var arguments;` just has
|
|
299
|
+
// an ordinary local, no arguments object.
|
|
300
|
+
function bindsArguments(body) {
|
|
301
|
+
const isArgDecl = s => Array.isArray(s) && (s[0] === 'var' || s[0] === 'let' || s[0] === 'const') &&
|
|
302
|
+
s.slice(1).some(d => d === 'arguments' || (Array.isArray(d) && d[0] === '=' && d[1] === 'arguments'))
|
|
303
|
+
let n = body
|
|
304
|
+
if (Array.isArray(n) && n[0] === '{}') n = n[1]
|
|
305
|
+
if (Array.isArray(n) && n[0] === ';') return n.slice(1).some(isArgDecl)
|
|
306
|
+
return isArgDecl(n)
|
|
307
|
+
}
|
|
308
|
+
|
|
250
309
|
function renameArguments(node, to) {
|
|
251
310
|
if (node === 'arguments') return to
|
|
252
311
|
if (!Array.isArray(node)) return node
|
|
@@ -278,8 +337,13 @@ function paramList(params) {
|
|
|
278
337
|
const isDestructurePat = p => Array.isArray(p) && (p[0] === '[]' || p[0] === '{}' || (p[0] === '=' && isDestructurePat(p[1])))
|
|
279
338
|
|
|
280
339
|
function lowerArguments(params, body) {
|
|
340
|
+
// A function body that declares its own `arguments` local: it's an ordinary
|
|
341
|
+
// variable, not the implicit object \u2014 rename it out of jz's reserved set,
|
|
342
|
+
// no rest param synthesized.
|
|
343
|
+
if (bindsArguments(body)) body = renameArguments(body, `\uE001arg${argsIdx++}`)
|
|
281
344
|
const paramsNeedLowering = paramList(params).some(isDestructurePat)
|
|
282
|
-
|
|
345
|
+
const usesArgsObj = usesArguments(params) || usesArguments(body)
|
|
346
|
+
if (!paramsNeedLowering && !usesArgsObj) return [params, body]
|
|
283
347
|
const name = `\uE001arg${argsIdx++}`
|
|
284
348
|
const decls = []
|
|
285
349
|
for (const [idx, param] of paramList(params).entries()) {
|
|
@@ -293,7 +357,7 @@ function lowerArguments(params, body) {
|
|
|
293
357
|
}
|
|
294
358
|
decls.push(['=', param, ['[]', name, [null, idx]]])
|
|
295
359
|
}
|
|
296
|
-
const renamed = renameArguments(body, name)
|
|
360
|
+
const renamed = usesArgsObj ? renameArguments(body, name) : body
|
|
297
361
|
return [['()', ['...', name]], decls.length ? prependParamDecls(['let', ...decls], renamed) : renamed]
|
|
298
362
|
}
|
|
299
363
|
|
|
@@ -467,12 +531,16 @@ const handlers = {
|
|
|
467
531
|
const clean = cases.map(c => {
|
|
468
532
|
if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
|
|
469
533
|
const body = c[2].slice(1).filter(s => typeof s !== 'number')
|
|
470
|
-
|
|
534
|
+
const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
|
|
535
|
+
return ['case', c[1], stripped]
|
|
471
536
|
}
|
|
472
537
|
if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
|
|
473
538
|
const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
|
|
474
|
-
|
|
539
|
+
const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
|
|
540
|
+
return ['default', stripped]
|
|
475
541
|
}
|
|
542
|
+
if (c[0] === 'case') return ['case', c[1], stripTerminalSwitchBreak(c[2])]
|
|
543
|
+
if (c[0] === 'default') return ['default', stripTerminalSwitchBreak(c[1])]
|
|
476
544
|
return c
|
|
477
545
|
})
|
|
478
546
|
return transformSwitch(disc, clean)
|
|
@@ -549,7 +617,10 @@ function transform(node) {
|
|
|
549
617
|
const [op, ...args] = node
|
|
550
618
|
if (op == null) return node
|
|
551
619
|
const h = handlers[op]
|
|
552
|
-
|
|
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)]
|
|
553
624
|
}
|
|
554
625
|
|
|
555
626
|
// Esbuild emits a small ESM helper:
|
|
@@ -673,12 +744,98 @@ function replaceStaticExportReads(node, rewrites) {
|
|
|
673
744
|
return node.map((part, i) => i === 0 ? part : replaceStaticExportReads(part, rewrites))
|
|
674
745
|
}
|
|
675
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
|
+
|
|
676
818
|
function cloneAst(node) {
|
|
677
819
|
if (node == null || typeof node !== 'object') return node
|
|
678
820
|
if (!Array.isArray(node)) return node
|
|
679
821
|
return node.map(cloneAst)
|
|
680
822
|
}
|
|
681
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
|
+
|
|
682
839
|
/** Transform switch statement to if/else chain. */
|
|
683
840
|
let swIdx = 0
|
|
684
841
|
function transformSwitch(discriminant, cases) {
|
|
@@ -702,173 +859,3 @@ function transformSwitch(discriminant, cases) {
|
|
|
702
859
|
if (chain) stmts.push(chain)
|
|
703
860
|
return [';', ...stmts]
|
|
704
861
|
}
|
|
705
|
-
|
|
706
|
-
// === AST → jz source codegen ===
|
|
707
|
-
|
|
708
|
-
const INDENT = ' '
|
|
709
|
-
const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
|
|
710
|
-
'??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
|
|
711
|
-
'<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
|
|
712
|
-
'+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
|
|
713
|
-
|
|
714
|
-
/** Wrap statement in { } if not already a block */
|
|
715
|
-
function wrapBlock(node, depth) {
|
|
716
|
-
if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
|
|
717
|
-
return '{ ' + codegen(node, depth) + '; }'
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/** Generate jz source from AST. Enforces semicolons. */
|
|
721
|
-
export function codegen(node, depth = 0) {
|
|
722
|
-
if (node == null) return ''
|
|
723
|
-
if (typeof node === 'number') return String(node)
|
|
724
|
-
if (typeof node === 'bigint') return node + 'n'
|
|
725
|
-
if (typeof node === 'string') return node
|
|
726
|
-
if (!Array.isArray(node)) return String(node)
|
|
727
|
-
|
|
728
|
-
const [op, ...a] = node
|
|
729
|
-
const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
|
|
730
|
-
|
|
731
|
-
// Literal: [, value]
|
|
732
|
-
if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
|
|
733
|
-
|
|
734
|
-
// Statements
|
|
735
|
-
if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
|
|
736
|
-
if (op === '{}') {
|
|
737
|
-
// Discriminate object literal / destructuring pattern from block.
|
|
738
|
-
// Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
|
|
739
|
-
// lone string shorthand. Empty `{}` outputs the same string either way.
|
|
740
|
-
const body = a[0]
|
|
741
|
-
const isObjItem = (n) => typeof n === 'string' ||
|
|
742
|
-
(Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
|
|
743
|
-
(n[0] === '=' && typeof n[1] === 'string')))
|
|
744
|
-
const isObj = body == null ? false
|
|
745
|
-
: typeof body === 'string' ? true
|
|
746
|
-
: Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
|
|
747
|
-
(body[0] === ',' && body.slice(1).every(isObjItem)))
|
|
748
|
-
if (isObj) {
|
|
749
|
-
if (typeof body === 'string') return '{ ' + body + ' }'
|
|
750
|
-
if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
|
|
751
|
-
return '{ ' + codegen(body) + ' }'
|
|
752
|
-
}
|
|
753
|
-
// Block: body is null, a single statement, or [';', ...stmts]
|
|
754
|
-
const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
|
|
755
|
-
const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
|
|
756
|
-
return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Declarations
|
|
760
|
-
if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
|
|
761
|
-
if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
|
|
762
|
-
if (op === 'default') return 'default ' + codegen(a[0], depth)
|
|
763
|
-
|
|
764
|
-
// Control flow
|
|
765
|
-
if (op === 'if') {
|
|
766
|
-
const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
|
|
767
|
-
return a[2] != null
|
|
768
|
-
? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
|
|
769
|
-
: 'if (' + cond + ') ' + then
|
|
770
|
-
}
|
|
771
|
-
if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
|
|
772
|
-
if (op === 'for') {
|
|
773
|
-
if (a.length === 2) { // ['for', head, body] — subscript shape
|
|
774
|
-
const [head, body] = a
|
|
775
|
-
if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
|
|
776
|
-
return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
|
|
777
|
-
// ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
|
|
778
|
-
if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
|
|
779
|
-
return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
|
|
780
|
-
// C-style head [';', init, cond, update] is positional — empty slots are valid,
|
|
781
|
-
// must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
|
|
782
|
-
if (Array.isArray(head) && head[0] === ';')
|
|
783
|
-
return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
|
|
784
|
-
return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
|
|
785
|
-
}
|
|
786
|
-
return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
|
|
787
|
-
}
|
|
788
|
-
if (op === 'return') return 'return ' + codegen(a[0])
|
|
789
|
-
if (op === 'throw') return 'throw ' + codegen(a[0])
|
|
790
|
-
if (op === 'break') return 'break'
|
|
791
|
-
if (op === 'continue') return 'continue'
|
|
792
|
-
// catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
|
|
793
|
-
if (op === 'catch') {
|
|
794
|
-
if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
|
|
795
|
-
return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Arrow
|
|
799
|
-
if (op === '=>') {
|
|
800
|
-
// Params: already wrapped in () by parser, or bare name
|
|
801
|
-
const p = a[0]
|
|
802
|
-
const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
|
|
803
|
-
const body = a[1]
|
|
804
|
-
const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
|
|
805
|
-
const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
|
|
806
|
-
? '{ ' + codegen(body, depth) + '; }'
|
|
807
|
-
: codegen(body, depth)
|
|
808
|
-
return params + ' => ' + bodyStr
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Grouping parens / function call
|
|
812
|
-
if (op === '()') {
|
|
813
|
-
if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
|
|
814
|
-
return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// Property access
|
|
818
|
-
if (op === '.') return codegen(a[0]) + '.' + a[1]
|
|
819
|
-
if (op === '?.') return codegen(a[0]) + '?.' + a[1]
|
|
820
|
-
if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
|
|
821
|
-
if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
822
|
-
if (op === '[]') {
|
|
823
|
-
// Array literal: ['[]', body] (length 2 → a.length 1). body may be null (empty),
|
|
824
|
-
// a single element, or a [',', ...items] sequence.
|
|
825
|
-
if (a.length === 1) {
|
|
826
|
-
if (a[0] == null) return '[]'
|
|
827
|
-
const body = a[0]
|
|
828
|
-
if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
|
|
829
|
-
return '[' + codegen(body) + ']'
|
|
830
|
-
}
|
|
831
|
-
// Subscript: ['[]', obj, idx]
|
|
832
|
-
return codegen(a[0]) + '[' + codegen(a[1]) + ']'
|
|
833
|
-
}
|
|
834
|
-
if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
|
|
835
|
-
if (op === 'str') return JSON.stringify(a[0])
|
|
836
|
-
if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
|
|
837
|
-
|
|
838
|
-
// Comma
|
|
839
|
-
if (op === ',') return a.map(x => codegen(x)).join(', ')
|
|
840
|
-
// Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
|
|
841
|
-
if (op === '`') return '`' + a.map(p => {
|
|
842
|
-
if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
|
|
843
|
-
return '${' + codegen(p) + '}'
|
|
844
|
-
}).join('') + '`'
|
|
845
|
-
|
|
846
|
-
// Spread
|
|
847
|
-
if (op === '...') return '...' + codegen(a[0])
|
|
848
|
-
|
|
849
|
-
// Import / export rename
|
|
850
|
-
if (op === 'import') return 'import ' + codegen(a[0])
|
|
851
|
-
if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
|
|
852
|
-
if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
|
|
853
|
-
|
|
854
|
-
// Unary prefix
|
|
855
|
-
if (a.length === 1) {
|
|
856
|
-
if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
|
|
857
|
-
if (op === 'typeof') return 'typeof ' + codegen(a[0])
|
|
858
|
-
if (op === 'u-') return '-' + codegen(a[0])
|
|
859
|
-
if (op === 'u+') return '+' + codegen(a[0])
|
|
860
|
-
return op + codegen(a[0])
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// Postfix
|
|
864
|
-
if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
|
|
865
|
-
|
|
866
|
-
// Binary
|
|
867
|
-
if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
|
|
868
|
-
|
|
869
|
-
// Ternary
|
|
870
|
-
if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
|
|
871
|
-
|
|
872
|
-
// Fallback
|
|
873
|
-
return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
|
|
874
|
-
}
|