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/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)], ['i32.const', 8]]],
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' || e[1].startsWith('$__alloc_hdr'))) {
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
- inc('__alloc_hdr')
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 init = ['local.set', `$${local}`,
654
- ['call', '$__alloc_hdr', irOf(len), irOf(cap == null ? len : cap), ['i32.const', stride]]]
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
- if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
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
- if (!paramsNeedLowering && !usesArguments(params) && !usesArguments(body)) return [params, body]
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
- return ['case', c[1], body.length === 1 ? body[0] : [';', ...body]]
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
- return ['default', body.length === 1 ? body[0] : [';', ...body]]
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
- return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
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
- }