jz 0.3.1 → 0.5.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/jzify.js CHANGED
@@ -27,11 +27,14 @@ export default function jzify(ast) {
27
27
  argsIdx = 0
28
28
  doIdx = 0
29
29
  classIdx = 0
30
+ objThisIdx = 0
31
+ staticClassIdx = 0
32
+ classBaseIdx = 0
30
33
  // Hoist module-level vars: any `var x` inside nested blocks bubbles up.
31
34
  const names = new Set()
32
35
  ast = hoistVars(ast, names)
33
36
  if (names.size) ast = prependDecls(ast, names)
34
- return foldStaticExportHelpers(transformScope(ast))
37
+ return foldStaticBundlerHelpers(foldStaticExportHelpers(canonicalizeObjectIdioms(transformScope(ast))))
35
38
  }
36
39
 
37
40
  /**
@@ -71,16 +74,27 @@ function hoistVars(node, names) {
71
74
  }
72
75
  return [op, lhs, hoistVars(node[2], names)]
73
76
  }
77
+ // Labeled statement: recurse into the body so its `var`s hoist to the
78
+ // enclosing function, not stop at the label.
79
+ if (op === ':' && typeof node[1] === 'string') {
80
+ return [':', node[1], hoistVars(node[2], names)]
81
+ }
74
82
  if (op === '=' && Array.isArray(node[1]) && node[1][0] === 'var' && typeof node[1][1] === 'string' && node[1].length === 2) {
75
83
  names.add(node[1][1])
76
84
  return ['=', node[1][1], hoistVars(node[2], names)]
77
85
  }
86
+ if (op === '=' && isDestructurePat(node[1])) {
87
+ return ['=', hoistPattern(node[1], names), hoistVars(node[2], names)]
88
+ }
78
89
  // For-head `;` is positional (init; cond; update), not a statement sequence.
79
90
  // Recurse into each slot but never filter nulls — empty slots are valid.
80
91
  if (op === 'for') {
81
92
  const head = node[1]
82
93
  let h2
83
- if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
94
+ const normalizedHead = normalizeForDeclHead(head, names) || normalizeForCommaHead(head, names)
95
+ if (normalizedHead) {
96
+ h2 = normalizedHead
97
+ } else if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
84
98
  (head[1][0] === 'in' || head[1][0] === 'of') && typeof head[1][1] === 'string') {
85
99
  names.add(head[1][1])
86
100
  h2 = [head[1][0], head[1][1], hoistVars(head[1][2], names)]
@@ -106,25 +120,84 @@ function hoistVars(node, names) {
106
120
  if (decls.length === 1) return decls[0]
107
121
  return [',', ...decls]
108
122
  }
123
+ if (op === 'let' || op === 'const') {
124
+ const decls = [op]
125
+ for (let i = 1; i < node.length; i++) {
126
+ const d = node[i]
127
+ if (Array.isArray(d) && d[0] === '=' && isDestructurePat(d[1])) {
128
+ decls.push(['=', hoistPattern(d[1], names), hoistVars(d[2], names)])
129
+ } else {
130
+ decls.push(hoistVars(d, names))
131
+ }
132
+ }
133
+ return decls
134
+ }
109
135
  // Filter null returns from `;` sequences (bare-var no-ops). `{}` is left
110
136
  // to recurse normally — it may be either a block or an object literal,
111
137
  // and we don't want to clobber `['{}', null]` (empty object literal).
112
138
  if (op === ';') {
113
139
  const out = [op]
114
140
  for (let i = 1; i < node.length; i++) {
115
- const c = hoistVars(node[i], names)
141
+ const child = node[i]
142
+ // A direct scope-child `var f = <arrow>` keeps its decl shape (→ a single
143
+ // `let f = arrow`) instead of splitting into a hoisted `let f` + later
144
+ // assignment. `var` and `let` are equivalent at a direct statement
145
+ // position, and the single-decl form is what top-level-function
146
+ // recognition keys on — bundlers (esbuild &c.) emit `var` for every
147
+ // module binding, so without this every bundled function would degrade
148
+ // to a closure value (and hit MAX_CLOSURE_ARITY). Redeclarations are
149
+ // collapsed downstream by the binding-dedup pass.
150
+ if (Array.isArray(child) && child[0] === 'var' && child.length === 2 &&
151
+ Array.isArray(child[1]) && child[1][0] === '=' && typeof child[1][1] === 'string' &&
152
+ Array.isArray(child[1][2]) && child[1][2][0] === '=>') {
153
+ out.push(['let', ['=', child[1][1], hoistVars(child[1][2], names)]])
154
+ continue
155
+ }
156
+ const c = hoistVars(child, names)
116
157
  if (c != null) out.push(c)
117
158
  }
118
159
  if (out.length === 1) return null
119
160
  if (out.length === 2) return out[1]
120
161
  return out
121
162
  }
163
+ // Block body: recursing the inner `;` may collapse it to a bare expression
164
+ // (e.g. `{ v + 1; }` → `['{}', ['+','v',1]]`), which prepare.js would then
165
+ // mistake for an object literal. Re-wrap as a 1-stmt `;` if the inner was
166
+ // block-shaped going in but collapsed to a non-statement-op expression.
167
+ if (op === '{}' && node.length === 2) {
168
+ const inner = node[1]
169
+ const wasBlock = inner != null && Array.isArray(inner) && JZ_BLOCK_OPS.has(inner[0])
170
+ const t = hoistVars(inner, names)
171
+ if (!wasBlock || t == null) return ['{}', t]
172
+ const stayed = Array.isArray(t) && JZ_BLOCK_OPS.has(t[0])
173
+ return ['{}', stayed ? t : [';', t]]
174
+ }
122
175
  const out = new Array(node.length)
123
176
  out[0] = op
124
177
  for (let i = 1; i < node.length; i++) out[i] = hoistVars(node[i], names)
125
178
  return out
126
179
  }
127
180
 
181
+ function hoistPattern(node, names) {
182
+ if (node == null || !Array.isArray(node)) return node
183
+ const op = node[0]
184
+ if (op === '=') return ['=', hoistPattern(node[1], names), hoistVars(node[2], names)]
185
+ if (op === ':') return [':', hoistVars(node[1], names), hoistPattern(node[2], names)]
186
+ if (op === '...') return ['...', hoistPattern(node[1], names)]
187
+ if (op === '[]' || op === '{}' || op === ',') return [op, ...node.slice(1).map(n => hoistPattern(n, names))]
188
+ return hoistVars(node, names)
189
+ }
190
+
191
+ function transformPattern(node) {
192
+ if (node == null || !Array.isArray(node)) return node
193
+ const op = node[0]
194
+ if (op === '=') return ['=', transformPattern(node[1]), transform(node[2])]
195
+ if (op === ':') return [':', transform(node[1]), transformPattern(node[2])]
196
+ if (op === '...') return ['...', transformPattern(node[1])]
197
+ if (op === '[]' || op === '{}' || op === ',') return [op, ...node.slice(1).map(transformPattern)]
198
+ return transform(node)
199
+ }
200
+
128
201
  function prependDecls(body, names) {
129
202
  const decl = ['let', ...names]
130
203
  if (Array.isArray(body) && body[0] === ';') return [';', decl, ...body.slice(1)]
@@ -137,9 +210,51 @@ function prependDecls(body, names) {
137
210
  return body == null ? decl : [';', decl, body]
138
211
  }
139
212
 
213
+ function normalizeForDeclHead(head, names) {
214
+ if (!Array.isArray(head) || (head[0] !== 'var' && head[0] !== 'let' && head[0] !== 'const')) return null
215
+ const kind = head[0]
216
+ if (head.length === 2) {
217
+ const expr = head[1]
218
+ if (!Array.isArray(expr)) return null
219
+ if (expr.length >= 3 && Array.isArray(expr[1]) &&
220
+ (expr[1][0] === 'in' || expr[1][0] === 'of') && typeof expr[1][1] === 'string') {
221
+ const iter = expr[1]
222
+ return [iter[0], normalizeForDecl(kind, iter[1], names), hoistVars([expr[0], iter[2], ...expr.slice(2)], names)]
223
+ }
224
+ return null
225
+ }
226
+ // Comma Expression in a for-in/of head: subscript parses with no for-head
227
+ // context, so `for (let x in A, B)` becomes a multi-declarator `let` whose
228
+ // tail declarators are really the rest of a comma Expression. Fold them back
229
+ // into the iterated source so the comma operator evaluates them in order.
230
+ if (head.length > 2 && Array.isArray(head[1]) &&
231
+ (head[1][0] === 'in' || head[1][0] === 'of') && typeof head[1][1] === 'string') {
232
+ const iter = head[1]
233
+ return [iter[0], normalizeForDecl(kind, iter[1], names), hoistVars([',', iter[2], ...head.slice(2)], names)]
234
+ }
235
+ return null
236
+ }
237
+
238
+ // Bare-LHS counterpart of the above: `for (x in A, B)` (no declaration) parses
239
+ // as a comma expression whose first operand is the for-in/of head.
240
+ function normalizeForCommaHead(head, names) {
241
+ if (!Array.isArray(head) || head[0] !== ',' || head.length < 3) return null
242
+ const iter = head[1]
243
+ if (!Array.isArray(iter) || (iter[0] !== 'in' && iter[0] !== 'of') || typeof iter[1] !== 'string') return null
244
+ return [iter[0], iter[1], hoistVars([',', iter[2], ...head.slice(2)], names)]
245
+ }
246
+
247
+ function normalizeForDecl(kind, name, names) {
248
+ if (kind === 'var') {
249
+ names.add(name)
250
+ return name
251
+ }
252
+ return [kind, name]
253
+ }
254
+
140
255
  /** Convert a named function declaration to a hoisted const arrow */
141
256
  function hoistFnDecl(name, params, body) {
142
- const [p2, b2] = lowerArguments(params, body)
257
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
143
258
  const decl = ['const', ['=', name, ['=>', p2, wrapArrowBody(b2)]]]
144
259
  decl._hoisted = true
145
260
  return decl
@@ -161,25 +276,6 @@ function transformScope(node) {
161
276
  const hoisted = [], rest = []
162
277
  for (let i = 0; i < args.length; i++) {
163
278
  const stmt = args[i]
164
- // Workaround for subscript parser ASI bug: multiline named IIFE
165
- // `(function name(){...})();` is parsed as two statements when there are
166
- // newlines inside the function body. Reconstruct the single-statement IIFE
167
- // so the () handler can desugar it correctly.
168
- if (Array.isArray(stmt) && stmt[0] === '()' &&
169
- Array.isArray(stmt[1]) && stmt[1][0] === 'function' && stmt[1][1] &&
170
- i + 1 < args.length && Array.isArray(args[i + 1]) && args[i + 1][0] === '()') {
171
- const merged = ['()', ['()', stmt[1]], args[i + 1][1] ?? null]
172
- const t = transform(merged)
173
- if (t != null) {
174
- if (Array.isArray(t) && t[0] === ';') {
175
- for (const s of t.slice(1)) { if (s != null) rest.push(s) }
176
- } else {
177
- rest.push(t)
178
- }
179
- }
180
- i++
181
- continue
182
- }
183
279
  // Statement-form named function declaration: hoist directly (skip expression handler)
184
280
  if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
185
281
  hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
@@ -225,24 +321,48 @@ function transformScope(node) {
225
321
  * wins; a later redeclaration keeps only its initializer, as a plain assignment.
226
322
  */
227
323
  function dedupeRedecls(stmts) {
228
- const nameOf = s => Array.isArray(s) && (s[0] === 'let' || s[0] === 'const' || s[0] === 'var')
229
- ? (typeof s[1] === 'string' ? s[1]
230
- : Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' ? s[1][1] : null)
231
- : null
324
+ // Per-declarator name bare `x` or `['=', x, init]`; null for patterns.
325
+ const declName = d => typeof d === 'string' ? d
326
+ : Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' ? d[1] : null
232
327
  const seen = new Set(), out = []
233
328
  for (const s of stmts) {
234
- const n = nameOf(s)
235
- if (n == null) { out.push(s); continue }
236
- if (seen.has(n)) { if (Array.isArray(s[1]) && s[1][0] === '=') out.push(['=', s[1][1], s[1][2]]); continue }
237
- seen.add(n); out.push(s)
329
+ if (!Array.isArray(s) || (s[0] !== 'let' && s[0] !== 'const' && s[0] !== 'var')) { out.push(s); continue }
330
+ // Walk every declarator: a multi-name bare `let` (the var-hoist
331
+ // `prependDecls` output) can carry a name already bound by a hoisted
332
+ // function `const`. Keep only fresh declarators; a redeclaration with an
333
+ // initializer survives as a plain assignment.
334
+ const keep = [s[0]], reassign = []
335
+ for (let i = 1; i < s.length; i++) {
336
+ const d = s[i], n = declName(d)
337
+ if (n == null) { keep.push(d); continue }
338
+ if (seen.has(n)) { if (Array.isArray(d) && d[0] === '=') reassign.push(['=', d[1], d[2]]) }
339
+ else { seen.add(n); keep.push(d) }
340
+ }
341
+ if (keep.length > 1) out.push(keep)
342
+ for (const r of reassign) out.push(r)
238
343
  }
239
344
  return out
240
345
  }
241
346
 
242
- /** Wrap function body for arrow conversion */
347
+ /** Wrap function body for arrow conversion.
348
+ * Produces the canonical block form `['{}', [';', ...stmts]]`: a `{}` whose
349
+ * sole child is a `;`-list. A bare single statement (`['{}', stmt]`, from the
350
+ * parser eliding the `;` wrapper) would otherwise be mistaken for an object
351
+ * literal, so it is `;`-wrapped here too — `function`→arrow conversions bypass
352
+ * the `=>` transform handler and must normalize their own bodies. */
243
353
  function wrapArrowBody(body) {
244
354
  const t = transformScope(body)
245
- return Array.isArray(t) && (t[0] === '{}' || t[0] === ';') ? (t[0] === '{}' ? t : ['{}', t]) : ['{}', t]
355
+ if (!Array.isArray(t)) return ['{}', [';', t]]
356
+ if (t[0] === ';') return ['{}', t]
357
+ if (t[0] !== '{}') return ['{}', [';', t]]
358
+ if (t.length === 2 && !(Array.isArray(t[1]) && t[1][0] === ';')) return ['{}', [';', t[1]]]
359
+ return t
360
+ }
361
+
362
+ function functionBodyBlock(body) {
363
+ if (Array.isArray(body) && body[0] === '{}') return body
364
+ if (Array.isArray(body) && body[0] === ';') return ['{}', body]
365
+ return ['{}', [';', body]]
246
366
  }
247
367
 
248
368
  /** Prototype identity check: X.prototype.Y */
@@ -252,6 +372,46 @@ const TYPED_ARRAYS = new Set(['Float64Array','Float32Array','Int32Array','Uint32
252
372
  'Int16Array','Uint16Array','Int8Array','Uint8Array',
253
373
  'ArrayBuffer','BigInt64Array','BigUint64Array','DataView'])
254
374
 
375
+ // Block-shape ops used to detect "this `{}` is a block body, not an object literal".
376
+ // Mirrors analyze.STMT_OPS — kept inline so jzify stays self-contained.
377
+ const JZ_BLOCK_OPS = new Set([';', 'let', 'const', 'var', 'return', 'if', 'for', 'for-in', 'for-of',
378
+ 'while', 'do', 'break', 'continue', 'switch', 'throw', 'try', 'catch', 'finally',
379
+ '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
380
+ '++', '--', '()', 'function', 'class', 'import', 'export', 'label'])
381
+ const LABEL_BODY_OPS = new Set([';', 'if', 'for', 'for-in', 'for-of', 'while', 'do', 'switch', 'try', 'throw'])
382
+
383
+ /** Statically discriminate `x instanceof Ctor` when the LHS's syntactic shape
384
+ * already pins down its runtime type. Returns true/false or null (unknown).
385
+ * Matches the lhs forms a user might plausibly write: literal arrays, object
386
+ * literals, string/number literals, and `new C(...)` whose ctor name is known.
387
+ * Stays conservative — anything else returns null and falls back to the runtime
388
+ * predicate so behavior is never silently changed. */
389
+ function staticInstanceofFold(val, ctor) {
390
+ if (typeof ctor !== 'string' || !Array.isArray(val)) return null
391
+ // Unwrap grouping parens: `(expr)` parses as `['()', expr]`
392
+ if (val[0] === '()' && val.length === 2) return staticInstanceofFold(val[1], ctor)
393
+ // Array literal: `[1,2,3]` → `['[]', [',', …]]` (or `['[]']` for empty array)
394
+ if (val[0] === '[]' && val.length <= 2) return ctor === 'Array' || ctor === 'Object'
395
+ // Object literal: `{a:1}` → `['{}', [':', …], …]`
396
+ if (val[0] === '{}') return ctor === 'Object'
397
+ // Regex literal: `/x/` → `['//', …]`
398
+ if (val[0] === '//') return ctor === 'RegExp' || ctor === 'Object'
399
+ // `new C(...)` — `['new', ['()', 'C', args]]` or `['new', 'C']`
400
+ if (val[0] === 'new') {
401
+ const inner = val[1]
402
+ const cname = typeof inner === 'string' ? inner
403
+ : (Array.isArray(inner) && inner[0] === '()' && typeof inner[1] === 'string') ? inner[1]
404
+ : null
405
+ if (cname) return cname === ctor || (cname !== 'Object' && ctor === 'Object')
406
+ }
407
+ // Bare primitive: `[null, v]` where v is a primitive — never an instance per JS spec.
408
+ if (val[0] == null && val.length === 2) {
409
+ const v = val[1]
410
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v == null) return false
411
+ }
412
+ return null
413
+ }
414
+
255
415
  // `arguments` lowering: regular `function` has implicit `arguments`; arrow doesn't.
256
416
  // jzify converts function → arrow, so any `arguments` use must be rewritten to a rest param.
257
417
  // Arrow functions inherit `arguments` from enclosing function — don't stop at '=>'.
@@ -349,6 +509,10 @@ function prependParamDecls(decl, body) {
349
509
  }
350
510
 
351
511
  const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
512
+ // A method body from jessie is a bare statement / `;`-sequence — wrap it in a
513
+ // `{}` block so the `=>` handler treats it as a function body, not an expression
514
+ // (an unwrapped `;`-seq arrow body produces malformed IR).
515
+ const block = b => Array.isArray(b) && b[0] === '{}' ? b : ['{}', b]
352
516
 
353
517
  // === class lowering ===
354
518
  //
@@ -369,10 +533,19 @@ const arrowParams = params => Array.isArray(params) && params[0] === '()' ? para
369
533
  // return selfN
370
534
  // }
371
535
  //
372
- // Out of scope for now (rejected with a clear message): `extends`/`super`,
373
- // `static` members, getters/setters, computed/private-via-`#` member names are
374
- // kept as the literal key string `#name` (jz allows it).
536
+ // Simple inheritance is lowered too: `class D extends B` builds the instance
537
+ // from `B`'s factory — forwarding `super(...)` args, or the derived ctor params
538
+ // when the derived constructor is implicit then applies D's own fields and
539
+ // methods over it.
540
+ //
541
+ // Out of scope (rejected with a clear message): full `super.foo` property
542
+ // semantics, getters/setters, non-constant computed member names. Private
543
+ // `#name` members are kept as the literal key string `#name` (jz allows it).
375
544
  let classIdx = 0
545
+ let objThisIdx = 0
546
+ let staticClassIdx = 0
547
+ let classBaseIdx = 0
548
+ const DEFAULT_DERIVED_CTOR_ARITY = 8
376
549
 
377
550
  const classBodyItems = (body) =>
378
551
  body == null ? [] : Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body]
@@ -389,39 +562,200 @@ function renameThis(node, to) {
389
562
  return node.map(n => renameThis(n, to))
390
563
  }
391
564
 
565
+ function usesThis(node) {
566
+ if (node === 'this') return true
567
+ if (!Array.isArray(node)) return false
568
+ if (node[0] === 'function' || node[0] === 'class') return false
569
+ if (node[0] === '.' || node[0] === '?.') return usesThis(node[1])
570
+ if (node[0] === ':') return usesThis(node[2])
571
+ return node.some(usesThis)
572
+ }
573
+
574
+ function hasSuperProp(node) {
575
+ if (!Array.isArray(node)) return false
576
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === 'super') return true
577
+ if (node[0] === '[]' && node[1] === 'super') return true
578
+ return node.some(hasSuperProp)
579
+ }
580
+
581
+ function isSuperCall(node) {
582
+ return Array.isArray(node) && node[0] === '()' && node[1] === 'super'
583
+ }
584
+
585
+ function literalStringKey(node) {
586
+ return Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
587
+ }
588
+
589
+ function constStringKey(node) {
590
+ if (typeof node === 'string') return node
591
+ const lit = literalStringKey(node)
592
+ if (lit != null) return lit
593
+ if (Array.isArray(node) && node[0] === '[]') return literalStringKey(node[1])
594
+ return null
595
+ }
596
+
597
+ function superMethodName(callee) {
598
+ if (!Array.isArray(callee)) return null
599
+ if ((callee[0] === '.' || callee[0] === '?.') && callee[1] === 'super') return callee[2]
600
+ if (callee[0] === '[]' && callee[1] === 'super') return literalStringKey(callee[2])
601
+ return null
602
+ }
603
+
604
+ function collectSuperMethodCalls(node, out = new Set()) {
605
+ if (!Array.isArray(node)) return out
606
+ if (node[0] === 'function' || node[0] === 'class') return out
607
+ if (node[0] === '()') {
608
+ const name = superMethodName(node[1])
609
+ if (name) out.add(name)
610
+ }
611
+ for (const n of node) collectSuperMethodCalls(n, out)
612
+ return out
613
+ }
614
+
615
+ function rewriteSuperMethodCalls(node, baseMethodVars) {
616
+ if (!Array.isArray(node)) return node
617
+ if (node[0] === 'function' || node[0] === 'class') return node
618
+ if (node[0] === '()') {
619
+ const name = superMethodName(node[1])
620
+ if (name) {
621
+ const fn = baseMethodVars.get(name)
622
+ if (!fn) jzifyError(`super.${name} is not available on the base class`)
623
+ return ['()', fn, ...node.slice(2).map(n => rewriteSuperMethodCalls(n, baseMethodVars))]
624
+ }
625
+ }
626
+ return node.map(n => rewriteSuperMethodCalls(n, baseMethodVars))
627
+ }
628
+
629
+ function splitCtorSuper(body) {
630
+ if (body == null) return { args: null, body }
631
+ if (isSuperCall(body)) return { args: body.slice(2), body: null }
632
+ if (Array.isArray(body) && body[0] === '{}') {
633
+ const inner = splitCtorSuper(body[1])
634
+ return { args: inner.args, body: ['{}', inner.body] }
635
+ }
636
+ if (Array.isArray(body) && body[0] === ';') {
637
+ const out = [';']
638
+ let args = null
639
+ for (const stmt of body.slice(1)) {
640
+ if (args == null && isSuperCall(stmt)) { args = stmt.slice(2); continue }
641
+ out.push(stmt)
642
+ }
643
+ return { args, body: out.length === 1 ? null : out.length === 2 ? out[1] : out }
644
+ }
645
+ return { args: null, body }
646
+ }
647
+
648
+ // Object shorthand methods and arrow-valued properties both parse as `=>`.
649
+ // Stay conservative: only statement-shaped bodies are receiver methods here;
650
+ // expression-bodied arrows keep their lexical `this` and remain unsupported.
651
+ const OBJ_METHOD_BODY_OPS = new Set([';', 'return', 'if', 'for', 'for-in', 'for-of',
652
+ 'while', 'do', 'switch', 'throw', 'try', 'break', 'continue'])
653
+
654
+ function objectLiteralProps(args) {
655
+ const raw = args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
656
+ return raw.filter(p => p != null)
657
+ }
658
+
659
+ function isStatementBody(body) {
660
+ return Array.isArray(body) && OBJ_METHOD_BODY_OPS.has(body[0])
661
+ }
662
+
663
+ function objectMethodUsesThis(prop) {
664
+ if (!Array.isArray(prop) || prop[0] !== ':' || typeof prop[1] !== 'string') return false
665
+ const value = prop[2]
666
+ if (!Array.isArray(value)) return false
667
+ if (value[0] === '=>' && isStatementBody(value[2])) return usesThis(value[2])
668
+ return false
669
+ }
670
+
671
+ function lowerObjectLiteralThis(args) {
672
+ const props = objectLiteralProps(args)
673
+ if (props.length === 0 || !props.some(objectMethodUsesThis)) return null
674
+ if (!props.every(p => Array.isArray(p) && p[0] === ':' && typeof p[1] === 'string')) return null
675
+
676
+ const self = `obj${objThisIdx++}`
677
+ const litProps = props.map(p => {
678
+ const value = p[2]
679
+ if (objectMethodUsesThis(p)) {
680
+ return [':', p[1], transform(['=>', value[1], block(renameThis(value[2], self))])]
681
+ }
682
+ return [':', p[1], transform(value)]
683
+ })
684
+ const lit = ['{}', litProps.length === 1 ? litProps[0] : [',', ...litProps]]
685
+ return ['()', ['()', ['=>', null, ['{}', [';',
686
+ ['let', ['=', self, lit]],
687
+ ['return', self]
688
+ ]]]], null]
689
+ }
690
+
392
691
  function jzifyError(msg) { throw new Error(`jzify: ${msg}`) }
393
692
 
394
693
  function lowerClass(name, heritage, body) {
395
- if (heritage != null) jzifyError('`class … extends …` is not supported yet — flatten the hierarchy or compose explicitly')
396
694
  let ctorParams = null, ctorBody = null
397
- const methods = [], fields = []
695
+ const methods = [], fields = [], statics = []
398
696
  for (const it of classBodyItems(body)) {
399
697
  if (typeof it === 'string') { fields.push([it, null]); continue } // bare `x;`
400
698
  if (!Array.isArray(it)) continue
699
+ const bareFieldName = constStringKey(it)
700
+ if (bareFieldName != null) { fields.push([bareFieldName, null]); continue }
401
701
  if (it[0] === ':' && Array.isArray(it[2]) && it[2][0] === '=>') {
402
- if (typeof it[1] !== 'string') jzifyError('computed class member names are not supported')
403
- if (it[1] === 'constructor') { ctorParams = it[2][1]; ctorBody = it[2][2] }
404
- else methods.push([it[1], it[2][1], it[2][2]])
702
+ const key = constStringKey(it[1])
703
+ if (key == null) jzifyError('non-constant computed class member names are not supported')
704
+ if (key === 'constructor') { ctorParams = it[2][1]; ctorBody = it[2][2] }
705
+ else methods.push([key, it[2][1], it[2][2]])
405
706
  continue
406
707
  }
407
708
  if (it[0] === '=') {
408
709
  const lhs = it[1]
409
- if (Array.isArray(lhs) && lhs[0] === 'static') jzifyError('`static` class members are not supported yet')
410
- if (typeof lhs !== 'string') jzifyError('computed/destructured class fields are not supported')
411
- fields.push([lhs, it[2]])
710
+ if (Array.isArray(lhs) && lhs[0] === 'static') {
711
+ const key = constStringKey(lhs[1])
712
+ if (key == null) jzifyError('non-constant computed static class fields are not supported')
713
+ statics.push([key, it[2]])
714
+ continue
715
+ }
716
+ const key = constStringKey(lhs)
717
+ if (key == null) jzifyError('non-constant computed/destructured class fields are not supported')
718
+ fields.push([key, it[2]])
719
+ continue
720
+ }
721
+ if (it[0] === 'static') {
722
+ const key = constStringKey(it[1])
723
+ if (key != null) {
724
+ statics.push([key, null])
725
+ continue
726
+ }
727
+ }
728
+ if (it[0] === 'static' && typeof it[1] === 'string') {
729
+ statics.push([it[1], null])
730
+ continue
731
+ }
732
+ if (it[0] === 'static' && Array.isArray(it[1]) && it[1][0] === ':' && Array.isArray(it[1][2]) && it[1][2][0] === '=>') {
733
+ const key = constStringKey(it[1][1])
734
+ if (key == null) jzifyError('non-constant computed static class member names are not supported')
735
+ statics.push([key, it[1][2], true])
412
736
  continue
413
737
  }
414
738
  if (it[0] === 'get' || it[0] === 'set') jzifyError('class getters/setters are not supported — jz objects have no accessors')
415
739
  if (it[0] === 'static') jzifyError('`static` class members are not supported yet')
416
740
  jzifyError(`unsupported class member ${JSON.stringify(it).slice(0, 60)}`)
417
741
  }
742
+ const superMethods = heritage == null ? new Set() : new Set([
743
+ ...collectSuperMethodCalls(ctorBody),
744
+ ...fields.flatMap(([, init]) => init == null ? [] : [...collectSuperMethodCalls(init)]),
745
+ ...methods.flatMap(([, , mbody]) => [...collectSuperMethodCalls(mbody)])
746
+ ])
747
+ if (heritage != null) {
748
+ const dummySuperVars = new Map([...superMethods].map((k, i) => [k, `super_${i}`]))
749
+ const unsupportedSuperProp = node => node != null && hasSuperProp(rewriteSuperMethodCalls(node, dummySuperVars))
750
+ if (
751
+ unsupportedSuperProp(ctorBody) ||
752
+ fields.some(([, init]) => unsupportedSuperProp(init)) ||
753
+ methods.some(([, , mbody]) => unsupportedSuperProp(mbody))
754
+ )
755
+ jzifyError('`super` property access is not supported yet')
756
+ }
418
757
  const self = `self${classIdx++}`
419
758
  const UNDEF = [] // jessie's node for `undefined`
420
- // A class member body from jessie is a bare statement / `;`-sequence — wrap it
421
- // in a `{}` block so the `=>` handler treats it as a function body, not an
422
- // expression (an unwrapped `;`-seq arrow body produces malformed IR).
423
- const block = b => Array.isArray(b) && b[0] === '{}' ? b : ['{}', b]
424
- const usesThis = n => n === 'this' || (Array.isArray(n) && n[0] !== 'function' && n[0] !== 'class' && n.some(usesThis))
425
759
  // Object literal: every declared field (its initializer inline when it doesn't
426
760
  // touch `this`, else `undefined` and assigned below), every method as its
427
761
  // self-capturing arrow. Declaring all fields up front fixes the object shape.
@@ -433,10 +767,39 @@ function lowerClass(name, heritage, body) {
433
767
  for (const [mname, mparams, mbody] of methods)
434
768
  litProps.push([':', mname, transform(['=>', mparams ?? ['()', null], block(renameThis(mbody, self))])])
435
769
  const lit = ['{}', litProps.length === 0 ? null : litProps.length === 1 ? litProps[0] : [',', ...litProps]]
436
- const stmts = [['let', ['=', self, lit]]]
770
+ let params = ctorParams ?? ['()', null]
771
+ const dynamicBase = heritage != null && typeof heritage !== 'string'
772
+ const baseRef = heritage == null ? null : dynamicBase ? `base${classBaseIdx++}` : heritage
773
+ const stmts = []
774
+ if (heritage != null) {
775
+ const split = splitCtorSuper(ctorBody)
776
+ ctorBody = split.body
777
+ const defaultArgs = ctorParams == null
778
+ ? Array.from({ length: DEFAULT_DERIVED_CTOR_ARITY }, (_, i) => `superArg${classIdx}_${i}`)
779
+ : null
780
+ const baseArgs = split.args ?? (defaultArgs ? [defaultArgs.length === 1 ? defaultArgs[0] : [',', ...defaultArgs]] : paramList(ctorParams))
781
+ stmts.push(['let', ['=', self, ['()', baseRef, ...baseArgs.map(transform)]]])
782
+ const superMethodVars = new Map()
783
+ let superIdx = 0
784
+ for (const mname of superMethods) {
785
+ const v = `super${classIdx}_${superIdx++}`
786
+ superMethodVars.set(mname, v)
787
+ stmts.push(['let', ['=', v, ['.', self, mname]]])
788
+ }
789
+ for (const [fname, init] of fields)
790
+ stmts.push(['=', ['.', self, fname], init != null ? transform(renameThis(rewriteSuperMethodCalls(init, superMethodVars), self)) : UNDEF])
791
+ for (const [mname, mparams, mbody] of methods)
792
+ stmts.push(['=', ['.', self, mname], transform(['=>', mparams ?? ['()', null], block(renameThis(rewriteSuperMethodCalls(mbody, superMethodVars), self))])])
793
+ ctorBody = rewriteSuperMethodCalls(ctorBody, superMethodVars)
794
+ if (defaultArgs) params = ['()', defaultArgs.length === 1 ? defaultArgs[0] : [',', ...defaultArgs]]
795
+ } else {
796
+ stmts.push(['let', ['=', self, lit]])
797
+ }
437
798
  // `this`-dependent field initializers run, in declaration order, before the ctor.
438
- for (const [fname, init] of deferred)
439
- stmts.push(['=', ['.', self, fname], transform(renameThis(init, self))])
799
+ if (heritage == null) {
800
+ for (const [fname, init] of deferred)
801
+ stmts.push(['=', ['.', self, fname], transform(renameThis(init, self))])
802
+ }
440
803
  if (ctorBody != null) {
441
804
  let cb = transform(renameThis(ctorBody, self))
442
805
  if (Array.isArray(cb) && cb[0] === '{}') cb = cb[1]
@@ -444,15 +807,43 @@ function lowerClass(name, heritage, body) {
444
807
  else if (cb != null) stmts.push(cb)
445
808
  }
446
809
  stmts.push(['return', self])
447
- return ['=>', arrowParams(ctorParams ?? ['()', null]), ['{}', [';', ...stmts]]]
810
+ const factory = ['=>', arrowParams(params), ['{}', [';', ...stmts]]]
811
+ if (!dynamicBase && statics.length === 0) return factory
812
+
813
+ const cls = name || `class${staticClassIdx++}`
814
+ const staticStmts = []
815
+ if (dynamicBase) staticStmts.push(['let', ['=', baseRef, transform(heritage)]])
816
+ staticStmts.push(['let', ['=', cls, factory]])
817
+ for (const [sname, value, isMethod] of statics) {
818
+ const rhs = isMethod
819
+ ? transform(['=>', value[1], block(renameThis(value[2], cls))])
820
+ : value == null ? UNDEF : transform(renameThis(value, cls))
821
+ staticStmts.push(['=', ['.', cls, sname], rhs])
822
+ }
823
+ staticStmts.push(['return', cls])
824
+ return ['()', ['()', ['=>', null, ['{}', [';', ...staticStmts]]]], null]
825
+ }
826
+
827
+ // Array(a, b, …) / new Array(a, b, …) → array literal [a, b, …]; Array() → [].
828
+ // The single-argument Array(n) is a length constructor (n holes), not a
829
+ // literal — return null there so the caller keeps it as a constructor call.
830
+ function lowerArrayConstructor(arg) {
831
+ if (arg == null) return ['[]', null]
832
+ if (Array.isArray(arg) && arg[0] === ',' && arg.length > 2)
833
+ return ['[]', [',', ...arg.slice(1).map(transform)]]
834
+ return null
448
835
  }
449
836
 
450
837
  const handlers = {
451
838
  // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
452
839
  '()'(callee, ...rest) {
840
+ if (callee === 'Array') {
841
+ const lit = lowerArrayConstructor(rest[0])
842
+ if (lit) return lit
843
+ }
453
844
  if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
454
845
  const [, name, params, body] = callee[1]
455
- const [p2, b2] = lowerArguments(params, body)
846
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
456
847
  return [';', ['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
457
848
  }
458
849
  },
@@ -461,7 +852,7 @@ const handlers = {
461
852
  // bound inside body per ES spec: `function f(){...f...}` → `(()=>{let f;f=arrow;return f})()`.
462
853
  // Statement-form named functions are hoisted by transformScope before reaching here.
463
854
  'function'(name, params, body) {
464
- const [p2, b2] = lowerArguments(params, body)
855
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
465
856
  const arrow = ['=>', p2, wrapArrowBody(b2)]
466
857
  if (name) {
467
858
  return ['()', ['()', ['=>', null, ['{}', [';',
@@ -474,7 +865,21 @@ const handlers = {
474
865
  },
475
866
 
476
867
  '=>'(params, body) {
477
- const [p2, b2] = lowerArguments(params, body)
868
+ // The subscript parser elides the `[';', stmt, null]` wrapper inside an
869
+ // arrow's `{` block when the block holds a single statement — the result
870
+ // `['{}', stmt]` is syntactically identical to an object-literal shape, and
871
+ // downstream `{}` handlers can no longer tell them apart. JS grammar makes
872
+ // it unambiguous: `=>` followed by `{` is always a block (use `=> ({...})`
873
+ // for an object return), so coerce every single-statement body to the
874
+ // canonical `['{}', [';', stmt]]` block form — `;`-wrapped, never bare.
875
+ let b = body
876
+ if (Array.isArray(b) && b[0] === '{}' && b.length === 2) {
877
+ const inner = b[1]
878
+ if (inner != null && !(Array.isArray(inner) && inner[0] === ';')) {
879
+ b = ['{}', [';', inner]]
880
+ }
881
+ }
882
+ const [p2, b2] = lowerArguments(params, b)
478
883
  return ['=>', p2, transform(b2)]
479
884
  },
480
885
 
@@ -490,60 +895,77 @@ const handlers = {
490
895
  return ['let', ...args.map(transform)]
491
896
  },
492
897
 
898
+ ':'(label, body) {
899
+ if (typeof label === 'string' && Array.isArray(body) && LABEL_BODY_OPS.has(body[0]))
900
+ return ['label', label, transform(body)]
901
+ },
902
+
493
903
  '='(lhs, rhs) {
494
- // Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
495
- if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
496
- const targets = []
497
- let cur = ['=', lhs, rhs]
498
- while (Array.isArray(cur) && cur[0] === '=') { targets.push(cur[1]); cur = cur[2] }
499
- const val = transform(cur)
500
- const stmts = []
501
- for (let i = targets.length - 1; i >= 0; i--) stmts.push(['=', transform(targets[i]), val])
502
- return stmts.length === 1 ? stmts[0] : [';', ...stmts]
503
- }
904
+ if (isDestructurePat(lhs)) return ['=', transformPattern(lhs), transform(rhs)]
504
905
  },
505
906
 
506
907
  'switch'(disc, ...cases) {
507
908
  const clean = cases.map(c => {
508
909
  if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
509
910
  const body = c[2].slice(1).filter(s => typeof s !== 'number')
510
- return ['case', c[1], body.length === 1 ? body[0] : [';', ...body]]
911
+ const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
912
+ return ['case', c[1], stripped]
511
913
  }
512
914
  if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
513
915
  const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
514
- return ['default', body.length === 1 ? body[0] : [';', ...body]]
916
+ const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
917
+ return ['default', stripped]
515
918
  }
919
+ if (c[0] === 'case') return ['case', c[1], stripTerminalSwitchBreak(c[2])]
920
+ if (c[0] === 'default') return ['default', stripTerminalSwitchBreak(c[1])]
516
921
  return c
517
922
  })
518
923
  return transformSwitch(disc, clean)
519
924
  },
520
925
 
521
- // == ===, != !== (with prototype identity folding)
522
- '=='(a, b) { return isProto(a) || isProto(b) ? 1 : ['===', transform(a), transform(b)] },
523
- '!='(a, b) { return isProto(a) || isProto(b) ? 0 : ['!==', transform(a), transform(b)] },
926
+ // Equality keeps the JS loose/strict distinction (jz core now lowers both):
927
+ // `==`/`!=` stay loose, `===`/`!==` stay strict. A comparison against a prototype
928
+ // object (e.g. `x.constructor === Object`) folds to a boolean jz has no
929
+ // prototype objects, so identity against one is decided statically.
930
+ '=='(a, b) { return isProto(a) || isProto(b) ? 1 : ['==', transform(a), transform(b)] },
931
+ '!='(a, b) { return isProto(a) || isProto(b) ? 0 : ['!=', transform(a), transform(b)] },
524
932
  '==='(a, b) { if (isProto(a) || isProto(b)) return 1 },
525
933
  '!=='(a, b) { if (isProto(a) || isProto(b)) return 0 },
526
934
 
527
935
  // new → call (keep TypedArrays)
528
936
  'new'(ctor, ...cargs) {
529
- if (Array.isArray(ctor) && ctor[0] === '()' && Array.isArray(ctor[1]) && ctor[1][0] === '.') {
530
- return ['()', ['.', transform(['new', ctor[1][1]]), ctor[1][2]], ...ctor.slice(2).map(transform)]
937
+ if (Array.isArray(ctor) && ctor[0] === '()' && ctor[1] === 'Array') {
938
+ const lit = lowerArrayConstructor(ctor[2])
939
+ if (lit) return lit
531
940
  }
532
941
  const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
533
- if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array')) return ['new', transform(ctor), ...cargs.map(transform)]
942
+ if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array' || name === 'RegExp')) return ['new', transform(ctor), ...cargs.map(transform)]
534
943
  if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
535
944
  // `new C(a)` → `C(a)`; `new C` (no parens) → `C()` — a 2-element `['()', X]`
536
945
  // is grouping parens, so a no-arg call needs the explicit `null` arg slot.
537
946
  return ['()', transform(ctor), ...(cargs.length ? cargs.map(transform) : [null])]
538
947
  },
539
948
 
540
- // instanceof → typeof / Array.isArray (jzify allows what strict mode prohibits)
949
+ // instanceof → typeof / Array.isArray / __is_* helpers. jzify lets us preserve
950
+ // constructor identity that strict-mode `instanceof` discards — Map, Set, and
951
+ // TypedArrays get dedicated typed predicates (__is_map / __is_set / __is_typed)
952
+ // that both compile to a runtime __ptr_type check and feed extractRefinements
953
+ // for downstream method-dispatch elision (e.g. `if (x instanceof Map) x.has(k)`
954
+ // resolves to __map_has, not the default __set_has fallback). Date / RegExp /
955
+ // Object stay on the weak typeof-object lowering — they share PTR.OBJECT and
956
+ // the JS runtime offers no cheaper discrimination.
541
957
  'instanceof'(val, ctor) {
542
958
  const t = transform(val)
543
959
  const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
960
+ // Static fold: literal shape of LHS already discriminates against the constructor.
961
+ const fold = staticInstanceofFold(val, name)
962
+ if (fold != null) return [null, fold]
544
963
  if (name === 'Array') return ['()', ['.', 'Array', 'isArray'], t]
545
- if (name === 'Object') return ['===', ['typeof', t], [null, 'object']]
546
- if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['===', ['typeof', t], [null, 'object']]
964
+ if (name === 'Map') return ['()', '__is_map', t]
965
+ if (name === 'Set') return ['()', '__is_set', t]
966
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name) && name !== 'ArrayBuffer' && name !== 'DataView')
967
+ return ['()', '__is_typed', t]
968
+ // Object / ArrayBuffer / DataView / RegExp / Date / unknown: weak typeof-object check.
547
969
  return ['===', ['typeof', t], [null, 'object']]
548
970
  },
549
971
 
@@ -557,8 +979,27 @@ const handlers = {
557
979
  ['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
558
980
  },
559
981
 
560
- // Block body: recurse as scope for hoisting
561
- '{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
982
+ // Block body: recurse as scope for hoisting. transformScope reduces a single-statement
983
+ // sequence to its bare element; if the input WAS block-shaped (`;` list or a single
984
+ // statement op), we re-wrap the collapsed result in `[';', ...]` so prepare.js keeps
985
+ // routing the `{}` through the block branch instead of mistaking it for `{ expr }`.
986
+ '{}'(...args) {
987
+ const loweredObject = lowerObjectLiteralThis(args)
988
+ if (loweredObject) return loweredObject
989
+
990
+ return ['{}', ...args.map((a, i) => {
991
+ const t = transformScope(a) ?? a
992
+ if (i !== 0 || a == null) return t
993
+ const blockIn = Array.isArray(a) && JZ_BLOCK_OPS.has(a[0])
994
+ if (!blockIn || t == null) return t
995
+ // transformScope collapses a single-statement `;`-list to its bare element;
996
+ // re-wrap so the block always stays `['{}', [';', ...]]`. The `;` is the
997
+ // only reliable block marker — a bare statement op can desugar to an
998
+ // expression op downstream (postfix `c++` → `['-', ...]`) and lose its
999
+ // block identity, leaving `['{}', expr]` mistakable for an object literal.
1000
+ return Array.isArray(t) && t[0] === ';' ? t : [';', t]
1001
+ })]
1002
+ },
562
1003
 
563
1004
  // Export: recurse into exported declaration. Statement-form `export function name`
564
1005
  // and `export default function name` must be hoisted as const-arrows — otherwise
@@ -589,7 +1030,10 @@ function transform(node) {
589
1030
  const [op, ...args] = node
590
1031
  if (op == null) return node
591
1032
  const h = handlers[op]
592
- return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
1033
+ // A handler that returns nullish (including no `return`) means "no rewrite at
1034
+ // this node" — fall through to a generic recurse. `??` (not `||`) so handlers
1035
+ // like `'==='` can legitimately return `0`.
1036
+ return (h && h(...args)) ?? [op, ...args.map(transform)]
593
1037
  }
594
1038
 
595
1039
  // Esbuild emits a small ESM helper:
@@ -611,16 +1055,16 @@ function foldStaticExportHelpers(ast) {
611
1055
 
612
1056
  const defPropAliases = new Set()
613
1057
  for (const stmt of body) {
614
- if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' && isObjectDefineProperty(stmt[2]))
615
- defPropAliases.add(stmt[1])
1058
+ const b = bindingOf(stmt)
1059
+ if (b && isObjectDefineProperty(b[1])) defPropAliases.add(b[0])
616
1060
  }
617
1061
  if (!defPropAliases.size) return ast
618
1062
 
619
1063
  const helperNames = new Set()
620
1064
  for (const stmt of body) {
621
- if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' &&
622
- Array.isArray(stmt[2]) && stmt[2][0] === '=>' && containsDefinePropertyCall(stmt[2], defPropAliases))
623
- helperNames.add(stmt[1])
1065
+ const b = bindingOf(stmt)
1066
+ if (b && Array.isArray(b[1]) && b[1][0] === '=>' && containsDefinePropertyCall(b[1], defPropAliases))
1067
+ helperNames.add(b[0])
624
1068
  }
625
1069
  if (!helperNames.size) return ast
626
1070
 
@@ -640,6 +1084,210 @@ function foldStaticExportHelpers(ast) {
640
1084
  return rewritten.length === 0 ? null : rewritten.length === 1 ? rewritten[0] : [';', ...rewritten]
641
1085
  }
642
1086
 
1087
+ // Esbuild's CommonJS/ESM interop helpers alias Object reflection built-ins into
1088
+ // locals (`var __create = Object.create`, `var __getOwnPropNames =
1089
+ // Object.getOwnPropertyNames`, ...). jz deliberately does not expose those
1090
+ // built-ins as first-class function values, but the helpers are static enough to
1091
+ // lower back to the supported direct calls and module reads.
1092
+ function foldStaticBundlerHelpers(ast) {
1093
+ const body = astSeq(ast)
1094
+ if (!body) return ast
1095
+ const binds = body.map(bindingOf) // [name, init] | null, index-aligned with body
1096
+
1097
+ // Local aliases of Object reflection built-ins: name -> canonical built-in.
1098
+ // esbuild's interop preamble always emits these (`var __defProp =
1099
+ // Object.defineProperty`, ...); their absence proves the input is not a
1100
+ // bundle, so the fold stays a strict no-op rather than guessing.
1101
+ const aliases = new Map()
1102
+ for (const b of binds) {
1103
+ const key = b && objectBuiltinKey(b[1])
1104
+ if (key) aliases.set(b[0], key)
1105
+ }
1106
+ if (!aliases.size) return ast
1107
+
1108
+ // __copyProps: an arrow driving both aliased getOwnPropertyNames + defineProperty.
1109
+ const copyHelpers = new Set()
1110
+ for (const b of binds)
1111
+ if (b && isArrow(b[1]) &&
1112
+ containsCall(b[1], c => aliases.get(c) === 'Object.getOwnPropertyNames') &&
1113
+ containsCall(b[1], c => aliases.get(c) === 'Object.defineProperty'))
1114
+ copyHelpers.add(b[0])
1115
+
1116
+ // __toESM: an arrow cloning a module behind a prototype, tagging default/__esModule.
1117
+ const interopHelpers = new Set()
1118
+ for (const b of binds)
1119
+ if (b && isArrow(b[1]) &&
1120
+ containsCall(b[1], c => aliases.get(c) === 'Object.create') &&
1121
+ containsCall(b[1], c => aliases.get(c) === 'Object.getPrototypeOf') &&
1122
+ containsCall(b[1], c => copyHelpers.has(c)) &&
1123
+ astSome(b[1], n => n === 'default') && astSome(b[1], n => n === '__esModule'))
1124
+ interopHelpers.add(b[0])
1125
+
1126
+ // Bindings produced by an interop-helper call: name -> wrapped module expression.
1127
+ const interopBindings = new Map()
1128
+ for (const b of binds)
1129
+ if (b && Array.isArray(b[1]) && b[1][0] === '()' && interopHelpers.has(b[1][1])) {
1130
+ const args = callArgs(b[1].slice(2))
1131
+ if (args.length) interopBindings.set(b[0], args[0])
1132
+ }
1133
+
1134
+ let out = body.map(stmt => rewriteBundlerAliases(stmt, aliases, interopBindings))
1135
+ if (interopBindings.size) out = out.map(stmt => replaceInteropReads(stmt, interopBindings))
1136
+ out = out.map(stmt => rewriteBundlerAliases(stmt, aliases, interopBindings)).filter(s => s != null)
1137
+
1138
+ // Drop synthetic alias/helper bindings nothing references after rewriting.
1139
+ const synthetic = n => aliases.has(n) || copyHelpers.has(n) || interopHelpers.has(n) || interopBindings.has(n)
1140
+ const live = new Set()
1141
+ for (const stmt of out) {
1142
+ const b = bindingOf(stmt)
1143
+ if (!(b && synthetic(b[0]))) collectRefs(stmt, live)
1144
+ }
1145
+ out = out.filter(stmt => {
1146
+ const b = bindingOf(stmt)
1147
+ return !(b && synthetic(b[0]) && !live.has(b[0]))
1148
+ })
1149
+
1150
+ return out.length === 0 ? null : out.length === 1 ? out[0] : [';', ...out]
1151
+ }
1152
+
1153
+ const isArrow = node => Array.isArray(node) && node[0] === '=>'
1154
+
1155
+ const OBJECT_BUILTINS = new Set(['create', 'getPrototypeOf', 'getOwnPropertyNames', 'getOwnPropertyDescriptor', 'defineProperty'])
1156
+
1157
+ // Canonical name of the Object reflection built-in `node` references, or null.
1158
+ function objectBuiltinKey(node) {
1159
+ if (!Array.isArray(node) || node[0] !== '.') return null
1160
+ if (node[1] === 'Object' && OBJECT_BUILTINS.has(node[2])) return 'Object.' + node[2]
1161
+ return isObjectHasOwnPropertyRef(node) ? 'Object.prototype.hasOwnProperty' : null
1162
+ }
1163
+
1164
+ // Deep `some` over AST children (skips the op slot, so op names never match).
1165
+ function astSome(node, pred) {
1166
+ if (pred(node)) return true
1167
+ if (!Array.isArray(node)) return false
1168
+ for (let i = 1; i < node.length; i++) if (astSome(node[i], pred)) return true
1169
+ return false
1170
+ }
1171
+
1172
+ // Does `node` contain a `()` call whose string callee satisfies `ok`?
1173
+ const containsCall = (node, ok) =>
1174
+ astSome(node, n => Array.isArray(n) && n[0] === '()' && typeof n[1] === 'string' && ok(n[1]))
1175
+
1176
+ function rewriteBundlerAliases(node, aliases, interopBindings) {
1177
+ if (!Array.isArray(node)) return node
1178
+ const rec = n => rewriteBundlerAliases(n, aliases, interopBindings)
1179
+
1180
+ if (node[0] === ';') {
1181
+ const out = [';']
1182
+ for (let i = 1; i < node.length; i++) {
1183
+ const child = rec(node[i])
1184
+ if (child != null) out.push(child)
1185
+ }
1186
+ return out.length === 1 ? null : out.length === 2 ? out[1] : out
1187
+ }
1188
+ if (node[0] === '{}' && node.length === 2) {
1189
+ const wasBlock = Array.isArray(node[1]) && JZ_BLOCK_OPS.has(node[1][0])
1190
+ const inner = rec(node[1])
1191
+ if (!wasBlock || inner == null) return ['{}', inner]
1192
+ const stayed = Array.isArray(inner) && JZ_BLOCK_OPS.has(inner[0])
1193
+ return ['{}', stayed ? inner : [';', inner]]
1194
+ }
1195
+
1196
+ if (node[0] === '()') {
1197
+ const callee = node[1]
1198
+ const args = callArgs(node.slice(2))
1199
+
1200
+ if (typeof callee === 'string') {
1201
+ const key = aliases.get(callee)
1202
+ if (key === 'Object.defineProperty') {
1203
+ const define = staticDefineProperty(args)
1204
+ if (define !== undefined) return define
1205
+ }
1206
+ if (key === 'Object.getOwnPropertyNames' || key === 'Object.create') {
1207
+ if (key === 'Object.create' && isGetPrototypeOfCall(args[0], aliases)) return ['{}', null]
1208
+ return ['()', key, ...args.map(rec)]
1209
+ }
1210
+ }
1211
+ // `__hasOwnProp.call(o, k)` -> `o.hasOwnProperty(k)`.
1212
+ if (Array.isArray(callee) && callee[0] === '.' && callee[2] === 'call' && args.length >= 2 &&
1213
+ typeof callee[1] === 'string' && aliases.get(callee[1]) === 'Object.prototype.hasOwnProperty')
1214
+ return ['()', ['.', rec(args[0]), 'hasOwnProperty'], rec(args[1])]
1215
+ // `(0, fn)(...)` comma-call resolving to an interop module read.
1216
+ const seqCall = commaZeroCall(callee, interopBindings)
1217
+ if (seqCall) return ['()', seqCall, ...args.map(rec)]
1218
+ }
1219
+
1220
+ if (node[0] === '.' || node[0] === '?.') return [node[0], rec(node[1]), node[2]]
1221
+ if (node[0] === ':') return [node[0], node[1], rec(node[2])]
1222
+ return node.map((part, i) => i === 0 ? part : rec(part))
1223
+ }
1224
+
1225
+ function replaceInteropReads(node, bindings) {
1226
+ if (typeof node === 'string' && bindings.has(node)) return cloneAst(bindings.get(node))
1227
+ if (!Array.isArray(node)) return node
1228
+ if (node[0] === '=' && typeof node[1] === 'string') return ['=', node[1], replaceInteropReads(node[2], bindings)]
1229
+ if (node[0] === 'let' || node[0] === 'const' || node[0] === 'var')
1230
+ return [node[0], ...node.slice(1).map(decl =>
1231
+ Array.isArray(decl) && decl[0] === '=' ? ['=', decl[1], replaceInteropReads(decl[2], bindings)] : decl)]
1232
+ if ((node[0] === '.' || node[0] === '?.') && typeof node[1] === 'string' && typeof node[2] === 'string' && bindings.has(node[1])) {
1233
+ const mod = cloneAst(bindings.get(node[1]))
1234
+ return node[2] === 'default' ? mod : [node[0], mod, node[2]]
1235
+ }
1236
+ if (node[0] === ':') return [node[0], node[1], replaceInteropReads(node[2], bindings)]
1237
+ return node.map((part, i) => i === 0 ? part : replaceInteropReads(part, bindings))
1238
+ }
1239
+
1240
+ function isGetPrototypeOfCall(node, aliases) {
1241
+ if (!Array.isArray(node) || node[0] !== '()') return false
1242
+ const callee = node[1]
1243
+ return (typeof callee === 'string' && aliases.get(callee) === 'Object.getPrototypeOf') ||
1244
+ objectBuiltinKey(callee) === 'Object.getPrototypeOf'
1245
+ }
1246
+
1247
+ function commaZeroCall(callee, bindings) {
1248
+ if (!Array.isArray(callee) || callee[0] !== '()' || !Array.isArray(callee[1]) || callee[1][0] !== ',') return null
1249
+ const parts = callee[1].slice(1)
1250
+ if (parts.length !== 2 || !isZeroLiteral(parts[0])) return null
1251
+ const fn = replaceInteropReads(parts[1], bindings)
1252
+ return fn === parts[1] ? null : fn
1253
+ }
1254
+
1255
+ // `defProp(obj, "key", descriptor)` -> `obj.key = value`; null drops `__esModule`.
1256
+ function staticDefineProperty(args) {
1257
+ if (args.length < 3) return undefined
1258
+ const [obj, keyExpr, desc] = args
1259
+ const key = stringLiteral(keyExpr)
1260
+ const props = objectProps(desc)
1261
+ if (typeof key !== 'string' || !props) return undefined
1262
+ if (key === '__esModule') return null
1263
+ const prop = name => props.find(p => Array.isArray(p) && p[0] === ':' && p[1] === name)?.[2]
1264
+ const value = prop('value')
1265
+ if (value !== undefined) return ['=', ['.', obj, key], value]
1266
+ const got = getterReturnExpr(prop('get'))
1267
+ return got !== null ? ['=', ['.', obj, key], got] : undefined
1268
+ }
1269
+
1270
+ function stringLiteral(node) {
1271
+ return Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
1272
+ }
1273
+
1274
+ // Identifier references in `node`, excluding declaration names, member property
1275
+ // names and object keys — enough to decide whether a synthetic binding is live.
1276
+ function collectRefs(node, out) {
1277
+ if (typeof node === 'string') return void out.add(node)
1278
+ if (!Array.isArray(node)) return
1279
+ if (node[0] === 'let' || node[0] === 'const' || node[0] === 'var') {
1280
+ for (let i = 1; i < node.length; i++)
1281
+ if (Array.isArray(node[i]) && node[i][0] === '=') collectRefs(node[i][2], out)
1282
+ } else if ((node[0] === '.' || node[0] === '?.') && typeof node[2] === 'string') {
1283
+ collectRefs(node[1], out)
1284
+ } else if (node[0] === ':') {
1285
+ collectRefs(node[2], out)
1286
+ } else {
1287
+ for (let i = 1; i < node.length; i++) collectRefs(node[i], out)
1288
+ }
1289
+ }
1290
+
643
1291
  function astSeq(ast) {
644
1292
  if (!Array.isArray(ast)) return null
645
1293
  return ast[0] === ';' ? ast.slice(1).filter(Boolean) : [ast]
@@ -649,12 +1297,28 @@ function isObjectDefineProperty(node) {
649
1297
  return Array.isArray(node) && node[0] === '.' && node[1] === 'Object' && node[2] === 'defineProperty'
650
1298
  }
651
1299
 
1300
+ /** Unwrap an esbuild module binding to `[name, init]`. After hoistVars, a binding
1301
+ * reaches this pass either split into a bare `['=', name, init]` (RHS hoisted out
1302
+ * as a separate `let name;`) or kept as a single `['let', ['=', name, init]]` decl
1303
+ * (arrow RHS — see the `;` handler in hoistVars). The fold keys on name/init,
1304
+ * so it must see through both shapes. */
1305
+ function bindingOf(stmt) {
1306
+ if (!Array.isArray(stmt)) return null
1307
+ if (stmt[0] === '=' && typeof stmt[1] === 'string') return [stmt[1], stmt[2]]
1308
+ if ((stmt[0] === 'let' || stmt[0] === 'const' || stmt[0] === 'var') && stmt.length === 2 &&
1309
+ Array.isArray(stmt[1]) && stmt[1][0] === '=' && typeof stmt[1][1] === 'string')
1310
+ return [stmt[1][1], stmt[1][2]]
1311
+ return null
1312
+ }
1313
+
652
1314
  function isDefPropAliasAssign(stmt, aliases) {
653
- return Array.isArray(stmt) && stmt[0] === '=' && aliases.has(stmt[1]) && isObjectDefineProperty(stmt[2])
1315
+ const b = bindingOf(stmt)
1316
+ return b != null && aliases.has(b[0]) && isObjectDefineProperty(b[1])
654
1317
  }
655
1318
 
656
1319
  function isExportHelperAssign(stmt, helpers) {
657
- return Array.isArray(stmt) && stmt[0] === '=' && helpers.has(stmt[1])
1320
+ const b = bindingOf(stmt)
1321
+ return b != null && helpers.has(b[0])
658
1322
  }
659
1323
 
660
1324
  function containsDefinePropertyCall(node, aliases) {
@@ -699,6 +1363,8 @@ function getterReturnExpr(node) {
699
1363
  if (params.length !== 0) return null
700
1364
  const body = node[2]
701
1365
  if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === 'return') return body[1][1]
1366
+ if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';' &&
1367
+ Array.isArray(body[1][1]) && body[1][1][0] === 'return') return body[1][1][1]
702
1368
  if (Array.isArray(body) && body[0] === 'return') return body[1]
703
1369
  return body
704
1370
  }
@@ -713,202 +1379,175 @@ function replaceStaticExportReads(node, rewrites) {
713
1379
  return node.map((part, i) => i === 0 ? part : replaceStaticExportReads(part, rewrites))
714
1380
  }
715
1381
 
1382
+ function canonicalizeObjectIdioms(node) {
1383
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
1384
+
1385
+ const out = node.map((part, i) => i === 0 ? part : canonicalizeObjectIdioms(part))
1386
+
1387
+ const hasOwnCall = objectHasOwnPropertyCall(out)
1388
+ if (hasOwnCall) return ['()', ['.', hasOwnCall.obj, 'hasOwnProperty'], hasOwnCall.key]
1389
+
1390
+ const mapString = arrayMapStringCallback(out)
1391
+ if (mapString) return mapString
1392
+
1393
+ if (out[0] === '&&') {
1394
+ const leftCtor = constructorIsObject(out[1])
1395
+ const rightKeys = objectKeysLengthZero(out[2])
1396
+ if (leftCtor && rightKeys && astEqual(leftCtor.obj, rightKeys.obj)) return out[2]
1397
+
1398
+ const leftKeys = objectKeysLengthZero(out[1])
1399
+ const rightCtor = constructorIsObject(out[2])
1400
+ if (leftKeys && rightCtor && astEqual(leftKeys.obj, rightCtor.obj)) return out[1]
1401
+ }
1402
+
1403
+ return out
1404
+ }
1405
+
1406
+ function arrayMapStringCallback(node) {
1407
+ if (!Array.isArray(node) || node[0] !== '()') return null
1408
+ const callee = node[1]
1409
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
1410
+ const args = callArgs(node.slice(2))
1411
+ if (args.length !== 1 || args[0] !== 'String') return null
1412
+ return ['()', callee, ['=>', 'value', ['()', 'String', 'value']]]
1413
+ }
1414
+
1415
+ function objectHasOwnPropertyCall(node) {
1416
+ if (!Array.isArray(node) || node[0] !== '()') return null
1417
+ const callee = node[1]
1418
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'call') return null
1419
+ if (!isObjectHasOwnPropertyRef(callee[1])) return null
1420
+ const args = callArgs(node.slice(2))
1421
+ if (args.length < 2) return null
1422
+ return { obj: args[0], key: args[1] }
1423
+ }
1424
+
1425
+ function isObjectHasOwnPropertyRef(node) {
1426
+ if (!Array.isArray(node) || node[0] !== '.' || node[2] !== 'hasOwnProperty') return false
1427
+ if (node[1] === 'Object') return true
1428
+ return Array.isArray(node[1]) && node[1][0] === '.' && node[1][1] === 'Object' && node[1][2] === 'prototype'
1429
+ }
1430
+
1431
+ function constructorIsObject(node) {
1432
+ if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
1433
+ const left = constructorReceiver(node[1])
1434
+ if (left && node[2] === 'Object') return { obj: left }
1435
+ const right = constructorReceiver(node[2])
1436
+ if (right && node[1] === 'Object') return { obj: right }
1437
+ return null
1438
+ }
1439
+
1440
+ function constructorReceiver(node) {
1441
+ return Array.isArray(node) && node[0] === '.' && node[2] === 'constructor' ? node[1] : null
1442
+ }
1443
+
1444
+ function objectKeysLengthZero(node) {
1445
+ if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
1446
+ const left = objectKeysLengthReceiver(node[1])
1447
+ if (left && isZeroLiteral(node[2])) return { obj: left }
1448
+ const right = objectKeysLengthReceiver(node[2])
1449
+ if (right && isZeroLiteral(node[1])) return { obj: right }
1450
+ return null
1451
+ }
1452
+
1453
+ function objectKeysLengthReceiver(node) {
1454
+ if (!Array.isArray(node) || node[0] !== '.' || node[2] !== 'length') return null
1455
+ const call = node[1]
1456
+ if (!Array.isArray(call) || call[0] !== '()') return null
1457
+ const callee = call[1]
1458
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[1] !== 'Object' || callee[2] !== 'keys') return null
1459
+ const args = callArgs(call.slice(2))
1460
+ return args.length === 1 ? args[0] : null
1461
+ }
1462
+
1463
+ function isZeroLiteral(node) {
1464
+ return Array.isArray(node) && node[0] == null && node[1] === 0
1465
+ }
1466
+
1467
+ function astEqual(a, b) {
1468
+ return JSON.stringify(a) === JSON.stringify(b)
1469
+ }
1470
+
716
1471
  function cloneAst(node) {
717
1472
  if (node == null || typeof node !== 'object') return node
718
1473
  if (!Array.isArray(node)) return node
719
1474
  return node.map(cloneAst)
720
1475
  }
721
1476
 
1477
+ function stripTerminalSwitchBreak(body) {
1478
+ if (!Array.isArray(body)) return body
1479
+ if (body[0] === 'break') return null
1480
+ if (body[0] === '{}') {
1481
+ const inner = stripTerminalSwitchBreak(body[1])
1482
+ if (inner == null) return ['{}', [';']]
1483
+ return ['{}', Array.isArray(inner) && inner[0] === ';' ? inner : [';', inner]]
1484
+ }
1485
+ if (body[0] !== ';') return body
1486
+
1487
+ const stmts = body.slice(1)
1488
+ if (Array.isArray(stmts.at(-1)) && stmts.at(-1)[0] === 'break') stmts.pop()
1489
+ return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
1490
+ }
1491
+
1492
+ const SWITCH_BREAK_BOUNDARIES = new Set(['for', 'for-in', 'for-of', 'while', 'do', 'switch', '=>', 'function', 'class'])
1493
+
1494
+ function hasOwnSwitchBreak(node) {
1495
+ if (!Array.isArray(node)) return false
1496
+ if (node[0] === 'break') return true
1497
+ if (SWITCH_BREAK_BOUNDARIES.has(node[0])) return false
1498
+ for (let i = 1; i < node.length; i++) if (hasOwnSwitchBreak(node[i])) return true
1499
+ return false
1500
+ }
1501
+
1502
+ function rewriteSwitchBreaks(node, flag) {
1503
+ if (!Array.isArray(node)) return node
1504
+ const op = node[0]
1505
+ if (op === 'break') return ['=', flag, [null, true]]
1506
+ if (SWITCH_BREAK_BOUNDARIES.has(op)) return node
1507
+
1508
+ if (op === ';') {
1509
+ const out = []
1510
+ const stmts = node.slice(1)
1511
+ for (let i = 0; i < stmts.length; i++) {
1512
+ const stmt = stmts[i]
1513
+ out.push(rewriteSwitchBreaks(stmt, flag))
1514
+ if (hasOwnSwitchBreak(stmt) && i < stmts.length - 1) {
1515
+ const tail = rewriteSwitchBreaks([';', ...stmts.slice(i + 1)], flag)
1516
+ out.push(['if', ['!', flag], tail])
1517
+ break
1518
+ }
1519
+ }
1520
+ return out.length === 0 ? null : out.length === 1 ? out[0] : [';', ...out]
1521
+ }
1522
+
1523
+ return node.map((part, i) => i === 0 ? part : rewriteSwitchBreaks(part, flag))
1524
+ }
1525
+
722
1526
  /** Transform switch statement to if/else chain. */
723
1527
  let swIdx = 0
724
1528
  function transformSwitch(discriminant, cases) {
725
1529
  const disc = transform(discriminant)
726
1530
  const tmp = `\uE000sw${swIdx++}`
1531
+ const needsBreakFlag = cases.some(c => hasOwnSwitchBreak(c[0] === 'case' ? c[2] : c[1]))
1532
+ const brk = needsBreakFlag ? `\uE000swbrk${swIdx++}` : null
727
1533
 
728
1534
  // Collect case/default
729
1535
  const stmts = [['let', ['=', tmp, disc]]]
1536
+ if (brk) stmts.push(['let', ['=', brk, [null, false]]])
730
1537
  let chain = null
731
1538
 
732
1539
  for (let i = cases.length - 1; i >= 0; i--) {
733
1540
  const c = cases[i]
734
1541
  if (c[0] === 'default') {
735
- chain = transform(c[1])
1542
+ const body = transform(c[1])
1543
+ chain = brk ? rewriteSwitchBreaks(body, brk) : body
736
1544
  } else if (c[0] === 'case') {
737
1545
  const cond = ['===', tmp, transform(c[1])]
738
1546
  const body = transform(c[2])
739
- chain = chain != null ? ['if', cond, body, chain] : ['if', cond, body]
1547
+ const lowered = brk ? rewriteSwitchBreaks(body, brk) : body
1548
+ chain = chain != null ? ['if', cond, lowered, chain] : ['if', cond, lowered]
740
1549
  }
741
1550
  }
742
1551
  if (chain) stmts.push(chain)
743
1552
  return [';', ...stmts]
744
1553
  }
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
- }