jz 0.4.0 → 0.5.1

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(canonicalizeObjectIdioms(transformScope(ast)))
37
+ return foldStaticBundlerHelpers(foldStaticExportHelpers(canonicalizeObjectIdioms(transformScope(ast))))
35
38
  }
36
39
 
37
40
  /**
@@ -71,16 +74,24 @@ 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
- const normalizedHead = normalizeForDeclHead(head, names)
94
+ const normalizedHead = normalizeForDeclHead(head, names) || normalizeForCommaHead(head, names)
84
95
  if (normalizedHead) {
85
96
  h2 = normalizedHead
86
97
  } else if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
@@ -109,25 +120,84 @@ function hoistVars(node, names) {
109
120
  if (decls.length === 1) return decls[0]
110
121
  return [',', ...decls]
111
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
+ }
112
135
  // Filter null returns from `;` sequences (bare-var no-ops). `{}` is left
113
136
  // to recurse normally — it may be either a block or an object literal,
114
137
  // and we don't want to clobber `['{}', null]` (empty object literal).
115
138
  if (op === ';') {
116
139
  const out = [op]
117
140
  for (let i = 1; i < node.length; i++) {
118
- 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)
119
157
  if (c != null) out.push(c)
120
158
  }
121
159
  if (out.length === 1) return null
122
160
  if (out.length === 2) return out[1]
123
161
  return out
124
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
+ }
125
175
  const out = new Array(node.length)
126
176
  out[0] = op
127
177
  for (let i = 1; i < node.length; i++) out[i] = hoistVars(node[i], names)
128
178
  return out
129
179
  }
130
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
+
131
201
  function prependDecls(body, names) {
132
202
  const decl = ['let', ...names]
133
203
  if (Array.isArray(body) && body[0] === ';') return [';', decl, ...body.slice(1)]
@@ -141,18 +211,39 @@ function prependDecls(body, names) {
141
211
  }
142
212
 
143
213
  function normalizeForDeclHead(head, names) {
144
- if (!Array.isArray(head) || (head[0] !== 'var' && head[0] !== 'let' && head[0] !== 'const') || head.length !== 2) return null
214
+ if (!Array.isArray(head) || (head[0] !== 'var' && head[0] !== 'let' && head[0] !== 'const')) return null
145
215
  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)]
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)]
152
234
  }
153
235
  return null
154
236
  }
155
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
+
156
247
  function normalizeForDecl(kind, name, names) {
157
248
  if (kind === 'var') {
158
249
  names.add(name)
@@ -163,7 +254,7 @@ function normalizeForDecl(kind, name, names) {
163
254
 
164
255
  /** Convert a named function declaration to a hoisted const arrow */
165
256
  function hoistFnDecl(name, params, body) {
166
- const [p2, b2] = lowerArguments(params, body)
257
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
167
258
  const decl = ['const', ['=', name, ['=>', p2, wrapArrowBody(b2)]]]
168
259
  decl._hoisted = true
169
260
  return decl
@@ -185,25 +276,6 @@ function transformScope(node) {
185
276
  const hoisted = [], rest = []
186
277
  for (let i = 0; i < args.length; i++) {
187
278
  const stmt = args[i]
188
- // Workaround for subscript parser ASI bug: multiline named IIFE
189
- // `(function name(){...})();` is parsed as two statements when there are
190
- // newlines inside the function body. Reconstruct the single-statement IIFE
191
- // so the () handler can desugar it correctly.
192
- if (Array.isArray(stmt) && stmt[0] === '()' &&
193
- Array.isArray(stmt[1]) && stmt[1][0] === 'function' && stmt[1][1] &&
194
- i + 1 < args.length && Array.isArray(args[i + 1]) && args[i + 1][0] === '()') {
195
- const merged = ['()', ['()', stmt[1]], args[i + 1][1] ?? null]
196
- const t = transform(merged)
197
- if (t != null) {
198
- if (Array.isArray(t) && t[0] === ';') {
199
- for (const s of t.slice(1)) { if (s != null) rest.push(s) }
200
- } else {
201
- rest.push(t)
202
- }
203
- }
204
- i++
205
- continue
206
- }
207
279
  // Statement-form named function declaration: hoist directly (skip expression handler)
208
280
  if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
209
281
  hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
@@ -249,33 +321,109 @@ function transformScope(node) {
249
321
  * wins; a later redeclaration keeps only its initializer, as a plain assignment.
250
322
  */
251
323
  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
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
256
327
  const seen = new Set(), out = []
257
328
  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)
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)
262
343
  }
263
344
  return out
264
345
  }
265
346
 
266
- /** 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. */
267
353
  function wrapArrowBody(body) {
268
354
  const t = transformScope(body)
269
- 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]]
270
366
  }
271
367
 
272
368
  /** Prototype identity check: X.prototype.Y */
273
369
  const isProto = n => Array.isArray(n) && n[0] === '.' && Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'prototype'
274
370
 
371
+ /** `obj.M <eq> Ctor.prototype.M` is not a real reference comparison — jz has no
372
+ * prototype objects — it asks whether `obj` overrides `M`, which is exactly
373
+ * `obj.hasOwnProperty('M')`. Returns that call node when the shape matches
374
+ * (a member access `obj.M` against a same-named prototype method), else null. */
375
+ const methodOverrideHasOwn = (a, b) => {
376
+ const proto = isProto(a) ? a : isProto(b) ? b : null
377
+ if (!proto) return null
378
+ const other = proto === a ? b : a
379
+ if (!Array.isArray(other) || other[0] !== '.' || other[2] !== proto[2]) return null
380
+ return ['()', ['.', transform(other[1]), 'hasOwnProperty'], [null, proto[2]]]
381
+ }
382
+
275
383
  const TYPED_ARRAYS = new Set(['Float64Array','Float32Array','Int32Array','Uint32Array',
276
384
  'Int16Array','Uint16Array','Int8Array','Uint8Array',
277
385
  'ArrayBuffer','BigInt64Array','BigUint64Array','DataView'])
278
386
 
387
+ // Block-shape ops used to detect "this `{}` is a block body, not an object literal".
388
+ // Mirrors analyze.STMT_OPS — kept inline so jzify stays self-contained.
389
+ const JZ_BLOCK_OPS = new Set([';', 'let', 'const', 'var', 'return', 'if', 'for', 'for-in', 'for-of',
390
+ 'while', 'do', 'break', 'continue', 'switch', 'throw', 'try', 'catch', 'finally',
391
+ '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
392
+ '++', '--', '()', 'function', 'class', 'import', 'export', 'label'])
393
+ const LABEL_BODY_OPS = new Set([';', 'if', 'for', 'for-in', 'for-of', 'while', 'do', 'switch', 'try', 'throw'])
394
+
395
+ /** Statically discriminate `x instanceof Ctor` when the LHS's syntactic shape
396
+ * already pins down its runtime type. Returns true/false or null (unknown).
397
+ * Matches the lhs forms a user might plausibly write: literal arrays, object
398
+ * literals, string/number literals, and `new C(...)` whose ctor name is known.
399
+ * Stays conservative — anything else returns null and falls back to the runtime
400
+ * predicate so behavior is never silently changed. */
401
+ function staticInstanceofFold(val, ctor) {
402
+ if (typeof ctor !== 'string' || !Array.isArray(val)) return null
403
+ // Unwrap grouping parens: `(expr)` parses as `['()', expr]`
404
+ if (val[0] === '()' && val.length === 2) return staticInstanceofFold(val[1], ctor)
405
+ // Array literal: `[1,2,3]` → `['[]', [',', …]]` (or `['[]']` for empty array)
406
+ if (val[0] === '[]' && val.length <= 2) return ctor === 'Array' || ctor === 'Object'
407
+ // Object literal: `{a:1}` → `['{}', [':', …], …]`
408
+ if (val[0] === '{}') return ctor === 'Object'
409
+ // Regex literal: `/x/` → `['//', …]`
410
+ if (val[0] === '//') return ctor === 'RegExp' || ctor === 'Object'
411
+ // `new C(...)` — `['new', ['()', 'C', args]]` or `['new', 'C']`
412
+ if (val[0] === 'new') {
413
+ const inner = val[1]
414
+ const cname = typeof inner === 'string' ? inner
415
+ : (Array.isArray(inner) && inner[0] === '()' && typeof inner[1] === 'string') ? inner[1]
416
+ : null
417
+ if (cname) return cname === ctor || (cname !== 'Object' && ctor === 'Object')
418
+ }
419
+ // Bare primitive: `[null, v]` where v is a primitive — never an instance per JS spec.
420
+ if (val[0] == null && val.length === 2) {
421
+ const v = val[1]
422
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v == null) return false
423
+ }
424
+ return null
425
+ }
426
+
279
427
  // `arguments` lowering: regular `function` has implicit `arguments`; arrow doesn't.
280
428
  // jzify converts function → arrow, so any `arguments` use must be rewritten to a rest param.
281
429
  // Arrow functions inherit `arguments` from enclosing function — don't stop at '=>'.
@@ -373,6 +521,10 @@ function prependParamDecls(decl, body) {
373
521
  }
374
522
 
375
523
  const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
524
+ // A method body from jessie is a bare statement / `;`-sequence — wrap it in a
525
+ // `{}` block so the `=>` handler treats it as a function body, not an expression
526
+ // (an unwrapped `;`-seq arrow body produces malformed IR).
527
+ const block = b => Array.isArray(b) && b[0] === '{}' ? b : ['{}', b]
376
528
 
377
529
  // === class lowering ===
378
530
  //
@@ -393,10 +545,19 @@ const arrowParams = params => Array.isArray(params) && params[0] === '()' ? para
393
545
  // return selfN
394
546
  // }
395
547
  //
396
- // Out of scope for now (rejected with a clear message): `extends`/`super`,
397
- // `static` members, getters/setters, computed/private-via-`#` member names are
398
- // kept as the literal key string `#name` (jz allows it).
548
+ // Simple inheritance is lowered too: `class D extends B` builds the instance
549
+ // from `B`'s factory — forwarding `super(...)` args, or the derived ctor params
550
+ // when the derived constructor is implicit then applies D's own fields and
551
+ // methods over it.
552
+ //
553
+ // Out of scope (rejected with a clear message): full `super.foo` property
554
+ // semantics, getters/setters, non-constant computed member names. Private
555
+ // `#name` members are kept as the literal key string `#name` (jz allows it).
399
556
  let classIdx = 0
557
+ let objThisIdx = 0
558
+ let staticClassIdx = 0
559
+ let classBaseIdx = 0
560
+ const DEFAULT_DERIVED_CTOR_ARITY = 8
400
561
 
401
562
  const classBodyItems = (body) =>
402
563
  body == null ? [] : Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body]
@@ -413,39 +574,200 @@ function renameThis(node, to) {
413
574
  return node.map(n => renameThis(n, to))
414
575
  }
415
576
 
577
+ function usesThis(node) {
578
+ if (node === 'this') return true
579
+ if (!Array.isArray(node)) return false
580
+ if (node[0] === 'function' || node[0] === 'class') return false
581
+ if (node[0] === '.' || node[0] === '?.') return usesThis(node[1])
582
+ if (node[0] === ':') return usesThis(node[2])
583
+ return node.some(usesThis)
584
+ }
585
+
586
+ function hasSuperProp(node) {
587
+ if (!Array.isArray(node)) return false
588
+ if ((node[0] === '.' || node[0] === '?.') && node[1] === 'super') return true
589
+ if (node[0] === '[]' && node[1] === 'super') return true
590
+ return node.some(hasSuperProp)
591
+ }
592
+
593
+ function isSuperCall(node) {
594
+ return Array.isArray(node) && node[0] === '()' && node[1] === 'super'
595
+ }
596
+
597
+ function literalStringKey(node) {
598
+ return Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
599
+ }
600
+
601
+ function constStringKey(node) {
602
+ if (typeof node === 'string') return node
603
+ const lit = literalStringKey(node)
604
+ if (lit != null) return lit
605
+ if (Array.isArray(node) && node[0] === '[]') return literalStringKey(node[1])
606
+ return null
607
+ }
608
+
609
+ function superMethodName(callee) {
610
+ if (!Array.isArray(callee)) return null
611
+ if ((callee[0] === '.' || callee[0] === '?.') && callee[1] === 'super') return callee[2]
612
+ if (callee[0] === '[]' && callee[1] === 'super') return literalStringKey(callee[2])
613
+ return null
614
+ }
615
+
616
+ function collectSuperMethodCalls(node, out = new Set()) {
617
+ if (!Array.isArray(node)) return out
618
+ if (node[0] === 'function' || node[0] === 'class') return out
619
+ if (node[0] === '()') {
620
+ const name = superMethodName(node[1])
621
+ if (name) out.add(name)
622
+ }
623
+ for (const n of node) collectSuperMethodCalls(n, out)
624
+ return out
625
+ }
626
+
627
+ function rewriteSuperMethodCalls(node, baseMethodVars) {
628
+ if (!Array.isArray(node)) return node
629
+ if (node[0] === 'function' || node[0] === 'class') return node
630
+ if (node[0] === '()') {
631
+ const name = superMethodName(node[1])
632
+ if (name) {
633
+ const fn = baseMethodVars.get(name)
634
+ if (!fn) jzifyError(`super.${name} is not available on the base class`)
635
+ return ['()', fn, ...node.slice(2).map(n => rewriteSuperMethodCalls(n, baseMethodVars))]
636
+ }
637
+ }
638
+ return node.map(n => rewriteSuperMethodCalls(n, baseMethodVars))
639
+ }
640
+
641
+ function splitCtorSuper(body) {
642
+ if (body == null) return { args: null, body }
643
+ if (isSuperCall(body)) return { args: body.slice(2), body: null }
644
+ if (Array.isArray(body) && body[0] === '{}') {
645
+ const inner = splitCtorSuper(body[1])
646
+ return { args: inner.args, body: ['{}', inner.body] }
647
+ }
648
+ if (Array.isArray(body) && body[0] === ';') {
649
+ const out = [';']
650
+ let args = null
651
+ for (const stmt of body.slice(1)) {
652
+ if (args == null && isSuperCall(stmt)) { args = stmt.slice(2); continue }
653
+ out.push(stmt)
654
+ }
655
+ return { args, body: out.length === 1 ? null : out.length === 2 ? out[1] : out }
656
+ }
657
+ return { args: null, body }
658
+ }
659
+
660
+ // Object shorthand methods and arrow-valued properties both parse as `=>`.
661
+ // Stay conservative: only statement-shaped bodies are receiver methods here;
662
+ // expression-bodied arrows keep their lexical `this` and remain unsupported.
663
+ const OBJ_METHOD_BODY_OPS = new Set([';', 'return', 'if', 'for', 'for-in', 'for-of',
664
+ 'while', 'do', 'switch', 'throw', 'try', 'break', 'continue'])
665
+
666
+ function objectLiteralProps(args) {
667
+ const raw = args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
668
+ return raw.filter(p => p != null)
669
+ }
670
+
671
+ function isStatementBody(body) {
672
+ return Array.isArray(body) && OBJ_METHOD_BODY_OPS.has(body[0])
673
+ }
674
+
675
+ function objectMethodUsesThis(prop) {
676
+ if (!Array.isArray(prop) || prop[0] !== ':' || typeof prop[1] !== 'string') return false
677
+ const value = prop[2]
678
+ if (!Array.isArray(value)) return false
679
+ if (value[0] === '=>' && isStatementBody(value[2])) return usesThis(value[2])
680
+ return false
681
+ }
682
+
683
+ function lowerObjectLiteralThis(args) {
684
+ const props = objectLiteralProps(args)
685
+ if (props.length === 0 || !props.some(objectMethodUsesThis)) return null
686
+ if (!props.every(p => Array.isArray(p) && p[0] === ':' && typeof p[1] === 'string')) return null
687
+
688
+ const self = `obj${objThisIdx++}`
689
+ const litProps = props.map(p => {
690
+ const value = p[2]
691
+ if (objectMethodUsesThis(p)) {
692
+ return [':', p[1], transform(['=>', value[1], block(renameThis(value[2], self))])]
693
+ }
694
+ return [':', p[1], transform(value)]
695
+ })
696
+ const lit = ['{}', litProps.length === 1 ? litProps[0] : [',', ...litProps]]
697
+ return ['()', ['()', ['=>', null, ['{}', [';',
698
+ ['let', ['=', self, lit]],
699
+ ['return', self]
700
+ ]]]], null]
701
+ }
702
+
416
703
  function jzifyError(msg) { throw new Error(`jzify: ${msg}`) }
417
704
 
418
705
  function lowerClass(name, heritage, body) {
419
- if (heritage != null) jzifyError('`class … extends …` is not supported yet — flatten the hierarchy or compose explicitly')
420
706
  let ctorParams = null, ctorBody = null
421
- const methods = [], fields = []
707
+ const methods = [], fields = [], statics = []
422
708
  for (const it of classBodyItems(body)) {
423
709
  if (typeof it === 'string') { fields.push([it, null]); continue } // bare `x;`
424
710
  if (!Array.isArray(it)) continue
711
+ const bareFieldName = constStringKey(it)
712
+ if (bareFieldName != null) { fields.push([bareFieldName, null]); continue }
425
713
  if (it[0] === ':' && Array.isArray(it[2]) && it[2][0] === '=>') {
426
- if (typeof it[1] !== 'string') jzifyError('computed class member names are not supported')
427
- if (it[1] === 'constructor') { ctorParams = it[2][1]; ctorBody = it[2][2] }
428
- else methods.push([it[1], it[2][1], it[2][2]])
714
+ const key = constStringKey(it[1])
715
+ if (key == null) jzifyError('non-constant computed class member names are not supported')
716
+ if (key === 'constructor') { ctorParams = it[2][1]; ctorBody = it[2][2] }
717
+ else methods.push([key, it[2][1], it[2][2]])
429
718
  continue
430
719
  }
431
720
  if (it[0] === '=') {
432
721
  const lhs = it[1]
433
- if (Array.isArray(lhs) && lhs[0] === 'static') jzifyError('`static` class members are not supported yet')
434
- if (typeof lhs !== 'string') jzifyError('computed/destructured class fields are not supported')
435
- fields.push([lhs, it[2]])
722
+ if (Array.isArray(lhs) && lhs[0] === 'static') {
723
+ const key = constStringKey(lhs[1])
724
+ if (key == null) jzifyError('non-constant computed static class fields are not supported')
725
+ statics.push([key, it[2]])
726
+ continue
727
+ }
728
+ const key = constStringKey(lhs)
729
+ if (key == null) jzifyError('non-constant computed/destructured class fields are not supported')
730
+ fields.push([key, it[2]])
731
+ continue
732
+ }
733
+ if (it[0] === 'static') {
734
+ const key = constStringKey(it[1])
735
+ if (key != null) {
736
+ statics.push([key, null])
737
+ continue
738
+ }
739
+ }
740
+ if (it[0] === 'static' && typeof it[1] === 'string') {
741
+ statics.push([it[1], null])
742
+ continue
743
+ }
744
+ if (it[0] === 'static' && Array.isArray(it[1]) && it[1][0] === ':' && Array.isArray(it[1][2]) && it[1][2][0] === '=>') {
745
+ const key = constStringKey(it[1][1])
746
+ if (key == null) jzifyError('non-constant computed static class member names are not supported')
747
+ statics.push([key, it[1][2], true])
436
748
  continue
437
749
  }
438
750
  if (it[0] === 'get' || it[0] === 'set') jzifyError('class getters/setters are not supported — jz objects have no accessors')
439
751
  if (it[0] === 'static') jzifyError('`static` class members are not supported yet')
440
752
  jzifyError(`unsupported class member ${JSON.stringify(it).slice(0, 60)}`)
441
753
  }
754
+ const superMethods = heritage == null ? new Set() : new Set([
755
+ ...collectSuperMethodCalls(ctorBody),
756
+ ...fields.flatMap(([, init]) => init == null ? [] : [...collectSuperMethodCalls(init)]),
757
+ ...methods.flatMap(([, , mbody]) => [...collectSuperMethodCalls(mbody)])
758
+ ])
759
+ if (heritage != null) {
760
+ const dummySuperVars = new Map([...superMethods].map((k, i) => [k, `super_${i}`]))
761
+ const unsupportedSuperProp = node => node != null && hasSuperProp(rewriteSuperMethodCalls(node, dummySuperVars))
762
+ if (
763
+ unsupportedSuperProp(ctorBody) ||
764
+ fields.some(([, init]) => unsupportedSuperProp(init)) ||
765
+ methods.some(([, , mbody]) => unsupportedSuperProp(mbody))
766
+ )
767
+ jzifyError('`super` property access is not supported yet')
768
+ }
442
769
  const self = `self${classIdx++}`
443
770
  const UNDEF = [] // jessie's node for `undefined`
444
- // A class member body from jessie is a bare statement / `;`-sequence — wrap it
445
- // in a `{}` block so the `=>` handler treats it as a function body, not an
446
- // expression (an unwrapped `;`-seq arrow body produces malformed IR).
447
- const block = b => Array.isArray(b) && b[0] === '{}' ? b : ['{}', b]
448
- const usesThis = n => n === 'this' || (Array.isArray(n) && n[0] !== 'function' && n[0] !== 'class' && n.some(usesThis))
449
771
  // Object literal: every declared field (its initializer inline when it doesn't
450
772
  // touch `this`, else `undefined` and assigned below), every method as its
451
773
  // self-capturing arrow. Declaring all fields up front fixes the object shape.
@@ -457,10 +779,39 @@ function lowerClass(name, heritage, body) {
457
779
  for (const [mname, mparams, mbody] of methods)
458
780
  litProps.push([':', mname, transform(['=>', mparams ?? ['()', null], block(renameThis(mbody, self))])])
459
781
  const lit = ['{}', litProps.length === 0 ? null : litProps.length === 1 ? litProps[0] : [',', ...litProps]]
460
- const stmts = [['let', ['=', self, lit]]]
782
+ let params = ctorParams ?? ['()', null]
783
+ const dynamicBase = heritage != null && typeof heritage !== 'string'
784
+ const baseRef = heritage == null ? null : dynamicBase ? `base${classBaseIdx++}` : heritage
785
+ const stmts = []
786
+ if (heritage != null) {
787
+ const split = splitCtorSuper(ctorBody)
788
+ ctorBody = split.body
789
+ const defaultArgs = ctorParams == null
790
+ ? Array.from({ length: DEFAULT_DERIVED_CTOR_ARITY }, (_, i) => `superArg${classIdx}_${i}`)
791
+ : null
792
+ const baseArgs = split.args ?? (defaultArgs ? [defaultArgs.length === 1 ? defaultArgs[0] : [',', ...defaultArgs]] : paramList(ctorParams))
793
+ stmts.push(['let', ['=', self, ['()', baseRef, ...baseArgs.map(transform)]]])
794
+ const superMethodVars = new Map()
795
+ let superIdx = 0
796
+ for (const mname of superMethods) {
797
+ const v = `super${classIdx}_${superIdx++}`
798
+ superMethodVars.set(mname, v)
799
+ stmts.push(['let', ['=', v, ['.', self, mname]]])
800
+ }
801
+ for (const [fname, init] of fields)
802
+ stmts.push(['=', ['.', self, fname], init != null ? transform(renameThis(rewriteSuperMethodCalls(init, superMethodVars), self)) : UNDEF])
803
+ for (const [mname, mparams, mbody] of methods)
804
+ stmts.push(['=', ['.', self, mname], transform(['=>', mparams ?? ['()', null], block(renameThis(rewriteSuperMethodCalls(mbody, superMethodVars), self))])])
805
+ ctorBody = rewriteSuperMethodCalls(ctorBody, superMethodVars)
806
+ if (defaultArgs) params = ['()', defaultArgs.length === 1 ? defaultArgs[0] : [',', ...defaultArgs]]
807
+ } else {
808
+ stmts.push(['let', ['=', self, lit]])
809
+ }
461
810
  // `this`-dependent field initializers run, in declaration order, before the ctor.
462
- for (const [fname, init] of deferred)
463
- stmts.push(['=', ['.', self, fname], transform(renameThis(init, self))])
811
+ if (heritage == null) {
812
+ for (const [fname, init] of deferred)
813
+ stmts.push(['=', ['.', self, fname], transform(renameThis(init, self))])
814
+ }
464
815
  if (ctorBody != null) {
465
816
  let cb = transform(renameThis(ctorBody, self))
466
817
  if (Array.isArray(cb) && cb[0] === '{}') cb = cb[1]
@@ -468,15 +819,43 @@ function lowerClass(name, heritage, body) {
468
819
  else if (cb != null) stmts.push(cb)
469
820
  }
470
821
  stmts.push(['return', self])
471
- return ['=>', arrowParams(ctorParams ?? ['()', null]), ['{}', [';', ...stmts]]]
822
+ const factory = ['=>', arrowParams(params), ['{}', [';', ...stmts]]]
823
+ if (!dynamicBase && statics.length === 0) return factory
824
+
825
+ const cls = name || `class${staticClassIdx++}`
826
+ const staticStmts = []
827
+ if (dynamicBase) staticStmts.push(['let', ['=', baseRef, transform(heritage)]])
828
+ staticStmts.push(['let', ['=', cls, factory]])
829
+ for (const [sname, value, isMethod] of statics) {
830
+ const rhs = isMethod
831
+ ? transform(['=>', value[1], block(renameThis(value[2], cls))])
832
+ : value == null ? UNDEF : transform(renameThis(value, cls))
833
+ staticStmts.push(['=', ['.', cls, sname], rhs])
834
+ }
835
+ staticStmts.push(['return', cls])
836
+ return ['()', ['()', ['=>', null, ['{}', [';', ...staticStmts]]]], null]
837
+ }
838
+
839
+ // Array(a, b, …) / new Array(a, b, …) → array literal [a, b, …]; Array() → [].
840
+ // The single-argument Array(n) is a length constructor (n holes), not a
841
+ // literal — return null there so the caller keeps it as a constructor call.
842
+ function lowerArrayConstructor(arg) {
843
+ if (arg == null) return ['[]', null]
844
+ if (Array.isArray(arg) && arg[0] === ',' && arg.length > 2)
845
+ return ['[]', [',', ...arg.slice(1).map(transform)]]
846
+ return null
472
847
  }
473
848
 
474
849
  const handlers = {
475
850
  // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
476
851
  '()'(callee, ...rest) {
852
+ if (callee === 'Array') {
853
+ const lit = lowerArrayConstructor(rest[0])
854
+ if (lit) return lit
855
+ }
477
856
  if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
478
857
  const [, name, params, body] = callee[1]
479
- const [p2, b2] = lowerArguments(params, body)
858
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
480
859
  return [';', ['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
481
860
  }
482
861
  },
@@ -485,7 +864,7 @@ const handlers = {
485
864
  // bound inside body per ES spec: `function f(){...f...}` → `(()=>{let f;f=arrow;return f})()`.
486
865
  // Statement-form named functions are hoisted by transformScope before reaching here.
487
866
  'function'(name, params, body) {
488
- const [p2, b2] = lowerArguments(params, body)
867
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
489
868
  const arrow = ['=>', p2, wrapArrowBody(b2)]
490
869
  if (name) {
491
870
  return ['()', ['()', ['=>', null, ['{}', [';',
@@ -498,7 +877,21 @@ const handlers = {
498
877
  },
499
878
 
500
879
  '=>'(params, body) {
501
- const [p2, b2] = lowerArguments(params, body)
880
+ // The subscript parser elides the `[';', stmt, null]` wrapper inside an
881
+ // arrow's `{` block when the block holds a single statement — the result
882
+ // `['{}', stmt]` is syntactically identical to an object-literal shape, and
883
+ // downstream `{}` handlers can no longer tell them apart. JS grammar makes
884
+ // it unambiguous: `=>` followed by `{` is always a block (use `=> ({...})`
885
+ // for an object return), so coerce every single-statement body to the
886
+ // canonical `['{}', [';', stmt]]` block form — `;`-wrapped, never bare.
887
+ let b = body
888
+ if (Array.isArray(b) && b[0] === '{}' && b.length === 2) {
889
+ const inner = b[1]
890
+ if (inner != null && !(Array.isArray(inner) && inner[0] === ';')) {
891
+ b = ['{}', [';', inner]]
892
+ }
893
+ }
894
+ const [p2, b2] = lowerArguments(params, b)
502
895
  return ['=>', p2, transform(b2)]
503
896
  },
504
897
 
@@ -514,64 +907,69 @@ const handlers = {
514
907
  return ['let', ...args.map(transform)]
515
908
  },
516
909
 
910
+ ':'(label, body) {
911
+ if (typeof label === 'string' && Array.isArray(body) && LABEL_BODY_OPS.has(body[0]))
912
+ return ['label', label, transform(body)]
913
+ },
914
+
517
915
  '='(lhs, rhs) {
518
- // Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
519
- if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
520
- const targets = []
521
- let cur = ['=', lhs, rhs]
522
- while (Array.isArray(cur) && cur[0] === '=') { targets.push(cur[1]); cur = cur[2] }
523
- const val = transform(cur)
524
- const stmts = []
525
- for (let i = targets.length - 1; i >= 0; i--) stmts.push(['=', transform(targets[i]), val])
526
- return stmts.length === 1 ? stmts[0] : [';', ...stmts]
527
- }
916
+ if (isDestructurePat(lhs)) return ['=', transformPattern(lhs), transform(rhs)]
528
917
  },
529
918
 
530
919
  'switch'(disc, ...cases) {
531
920
  const clean = cases.map(c => {
532
- if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
533
- const body = c[2].slice(1).filter(s => typeof s !== 'number')
534
- const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
535
- return ['case', c[1], stripped]
536
- }
537
- if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
538
- const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
539
- const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
540
- return ['default', stripped]
541
- }
542
- if (c[0] === 'case') return ['case', c[1], stripTerminalSwitchBreak(c[2])]
543
- if (c[0] === 'default') return ['default', stripTerminalSwitchBreak(c[1])]
921
+ if (c[0] === 'case') return ['case', c[1], normalizeCaseBody(c[2])]
922
+ if (c[0] === 'default') return ['default', normalizeCaseBody(c[1])]
544
923
  return c
545
924
  })
546
925
  return transformSwitch(disc, clean)
547
926
  },
548
927
 
549
- // == ===, != !== (with prototype identity folding)
550
- '=='(a, b) { return isProto(a) || isProto(b) ? 1 : ['===', transform(a), transform(b)] },
551
- '!='(a, b) { return isProto(a) || isProto(b) ? 0 : ['!==', transform(a), transform(b)] },
552
- '==='(a, b) { if (isProto(a) || isProto(b)) return 1 },
553
- '!=='(a, b) { if (isProto(a) || isProto(b)) return 0 },
928
+ // Equality keeps the JS loose/strict distinction (jz core now lowers both):
929
+ // `==`/`!=` stay loose, `===`/`!==` stay strict. A comparison against a prototype
930
+ // object folds to a boolean jz has no prototype objects, so identity against one
931
+ // is decided statically. The one exception is `obj.M <eq> Ctor.prototype.M`: that
932
+ // probes whether `obj` overrides the builtin `M`, so it lowers to a runtime
933
+ // `obj.hasOwnProperty('M')` (equal ⇒ not overridden, unequal ⇒ overridden).
934
+ '=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; return isProto(a) || isProto(b) ? 1 : ['==', transform(a), transform(b)] },
935
+ '!='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; return isProto(a) || isProto(b) ? 0 : ['!=', transform(a), transform(b)] },
936
+ '==='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; if (isProto(a) || isProto(b)) return 1 },
937
+ '!=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; if (isProto(a) || isProto(b)) return 0 },
554
938
 
555
939
  // new → call (keep TypedArrays)
556
940
  'new'(ctor, ...cargs) {
557
- if (Array.isArray(ctor) && ctor[0] === '()' && Array.isArray(ctor[1]) && ctor[1][0] === '.') {
558
- return ['()', ['.', transform(['new', ctor[1][1]]), ctor[1][2]], ...ctor.slice(2).map(transform)]
941
+ if (Array.isArray(ctor) && ctor[0] === '()' && ctor[1] === 'Array') {
942
+ const lit = lowerArrayConstructor(ctor[2])
943
+ if (lit) return lit
559
944
  }
560
945
  const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
561
- if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array')) return ['new', transform(ctor), ...cargs.map(transform)]
946
+ if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array' || name === 'RegExp')) return ['new', transform(ctor), ...cargs.map(transform)]
562
947
  if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
563
948
  // `new C(a)` → `C(a)`; `new C` (no parens) → `C()` — a 2-element `['()', X]`
564
949
  // is grouping parens, so a no-arg call needs the explicit `null` arg slot.
565
950
  return ['()', transform(ctor), ...(cargs.length ? cargs.map(transform) : [null])]
566
951
  },
567
952
 
568
- // instanceof → typeof / Array.isArray (jzify allows what strict mode prohibits)
953
+ // instanceof → typeof / Array.isArray / __is_* helpers. jzify lets us preserve
954
+ // constructor identity that strict-mode `instanceof` discards — Map, Set, and
955
+ // TypedArrays get dedicated typed predicates (__is_map / __is_set / __is_typed)
956
+ // that both compile to a runtime __ptr_type check and feed extractRefinements
957
+ // for downstream method-dispatch elision (e.g. `if (x instanceof Map) x.has(k)`
958
+ // resolves to __map_has, not the default __set_has fallback). Date / RegExp /
959
+ // Object stay on the weak typeof-object lowering — they share PTR.OBJECT and
960
+ // the JS runtime offers no cheaper discrimination.
569
961
  'instanceof'(val, ctor) {
570
962
  const t = transform(val)
571
963
  const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
964
+ // Static fold: literal shape of LHS already discriminates against the constructor.
965
+ const fold = staticInstanceofFold(val, name)
966
+ if (fold != null) return [null, fold]
572
967
  if (name === 'Array') return ['()', ['.', 'Array', 'isArray'], t]
573
- if (name === 'Object') return ['===', ['typeof', t], [null, 'object']]
574
- if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['===', ['typeof', t], [null, 'object']]
968
+ if (name === 'Map') return ['()', '__is_map', t]
969
+ if (name === 'Set') return ['()', '__is_set', t]
970
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name) && name !== 'ArrayBuffer' && name !== 'DataView')
971
+ return ['()', '__is_typed', t]
972
+ // Object / ArrayBuffer / DataView / RegExp / Date / unknown: weak typeof-object check.
575
973
  return ['===', ['typeof', t], [null, 'object']]
576
974
  },
577
975
 
@@ -585,8 +983,27 @@ const handlers = {
585
983
  ['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
586
984
  },
587
985
 
588
- // Block body: recurse as scope for hoisting
589
- '{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
986
+ // Block body: recurse as scope for hoisting. transformScope reduces a single-statement
987
+ // sequence to its bare element; if the input WAS block-shaped (`;` list or a single
988
+ // statement op), we re-wrap the collapsed result in `[';', ...]` so prepare.js keeps
989
+ // routing the `{}` through the block branch instead of mistaking it for `{ expr }`.
990
+ '{}'(...args) {
991
+ const loweredObject = lowerObjectLiteralThis(args)
992
+ if (loweredObject) return loweredObject
993
+
994
+ return ['{}', ...args.map((a, i) => {
995
+ const t = transformScope(a) ?? a
996
+ if (i !== 0 || a == null) return t
997
+ const blockIn = Array.isArray(a) && JZ_BLOCK_OPS.has(a[0])
998
+ if (!blockIn || t == null) return t
999
+ // transformScope collapses a single-statement `;`-list to its bare element;
1000
+ // re-wrap so the block always stays `['{}', [';', ...]]`. The `;` is the
1001
+ // only reliable block marker — a bare statement op can desugar to an
1002
+ // expression op downstream (postfix `c++` → `['-', ...]`) and lose its
1003
+ // block identity, leaving `['{}', expr]` mistakable for an object literal.
1004
+ return Array.isArray(t) && t[0] === ';' ? t : [';', t]
1005
+ })]
1006
+ },
590
1007
 
591
1008
  // Export: recurse into exported declaration. Statement-form `export function name`
592
1009
  // and `export default function name` must be hoisted as const-arrows — otherwise
@@ -642,16 +1059,16 @@ function foldStaticExportHelpers(ast) {
642
1059
 
643
1060
  const defPropAliases = new Set()
644
1061
  for (const stmt of body) {
645
- if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' && isObjectDefineProperty(stmt[2]))
646
- defPropAliases.add(stmt[1])
1062
+ const b = bindingOf(stmt)
1063
+ if (b && isObjectDefineProperty(b[1])) defPropAliases.add(b[0])
647
1064
  }
648
1065
  if (!defPropAliases.size) return ast
649
1066
 
650
1067
  const helperNames = new Set()
651
1068
  for (const stmt of body) {
652
- if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' &&
653
- Array.isArray(stmt[2]) && stmt[2][0] === '=>' && containsDefinePropertyCall(stmt[2], defPropAliases))
654
- helperNames.add(stmt[1])
1069
+ const b = bindingOf(stmt)
1070
+ if (b && Array.isArray(b[1]) && b[1][0] === '=>' && containsDefinePropertyCall(b[1], defPropAliases))
1071
+ helperNames.add(b[0])
655
1072
  }
656
1073
  if (!helperNames.size) return ast
657
1074
 
@@ -671,6 +1088,210 @@ function foldStaticExportHelpers(ast) {
671
1088
  return rewritten.length === 0 ? null : rewritten.length === 1 ? rewritten[0] : [';', ...rewritten]
672
1089
  }
673
1090
 
1091
+ // Esbuild's CommonJS/ESM interop helpers alias Object reflection built-ins into
1092
+ // locals (`var __create = Object.create`, `var __getOwnPropNames =
1093
+ // Object.getOwnPropertyNames`, ...). jz deliberately does not expose those
1094
+ // built-ins as first-class function values, but the helpers are static enough to
1095
+ // lower back to the supported direct calls and module reads.
1096
+ function foldStaticBundlerHelpers(ast) {
1097
+ const body = astSeq(ast)
1098
+ if (!body) return ast
1099
+ const binds = body.map(bindingOf) // [name, init] | null, index-aligned with body
1100
+
1101
+ // Local aliases of Object reflection built-ins: name -> canonical built-in.
1102
+ // esbuild's interop preamble always emits these (`var __defProp =
1103
+ // Object.defineProperty`, ...); their absence proves the input is not a
1104
+ // bundle, so the fold stays a strict no-op rather than guessing.
1105
+ const aliases = new Map()
1106
+ for (const b of binds) {
1107
+ const key = b && objectBuiltinKey(b[1])
1108
+ if (key) aliases.set(b[0], key)
1109
+ }
1110
+ if (!aliases.size) return ast
1111
+
1112
+ // __copyProps: an arrow driving both aliased getOwnPropertyNames + defineProperty.
1113
+ const copyHelpers = new Set()
1114
+ for (const b of binds)
1115
+ if (b && isArrow(b[1]) &&
1116
+ containsCall(b[1], c => aliases.get(c) === 'Object.getOwnPropertyNames') &&
1117
+ containsCall(b[1], c => aliases.get(c) === 'Object.defineProperty'))
1118
+ copyHelpers.add(b[0])
1119
+
1120
+ // __toESM: an arrow cloning a module behind a prototype, tagging default/__esModule.
1121
+ const interopHelpers = new Set()
1122
+ for (const b of binds)
1123
+ if (b && isArrow(b[1]) &&
1124
+ containsCall(b[1], c => aliases.get(c) === 'Object.create') &&
1125
+ containsCall(b[1], c => aliases.get(c) === 'Object.getPrototypeOf') &&
1126
+ containsCall(b[1], c => copyHelpers.has(c)) &&
1127
+ astSome(b[1], n => n === 'default') && astSome(b[1], n => n === '__esModule'))
1128
+ interopHelpers.add(b[0])
1129
+
1130
+ // Bindings produced by an interop-helper call: name -> wrapped module expression.
1131
+ const interopBindings = new Map()
1132
+ for (const b of binds)
1133
+ if (b && Array.isArray(b[1]) && b[1][0] === '()' && interopHelpers.has(b[1][1])) {
1134
+ const args = callArgs(b[1].slice(2))
1135
+ if (args.length) interopBindings.set(b[0], args[0])
1136
+ }
1137
+
1138
+ let out = body.map(stmt => rewriteBundlerAliases(stmt, aliases, interopBindings))
1139
+ if (interopBindings.size) out = out.map(stmt => replaceInteropReads(stmt, interopBindings))
1140
+ out = out.map(stmt => rewriteBundlerAliases(stmt, aliases, interopBindings)).filter(s => s != null)
1141
+
1142
+ // Drop synthetic alias/helper bindings nothing references after rewriting.
1143
+ const synthetic = n => aliases.has(n) || copyHelpers.has(n) || interopHelpers.has(n) || interopBindings.has(n)
1144
+ const live = new Set()
1145
+ for (const stmt of out) {
1146
+ const b = bindingOf(stmt)
1147
+ if (!(b && synthetic(b[0]))) collectRefs(stmt, live)
1148
+ }
1149
+ out = out.filter(stmt => {
1150
+ const b = bindingOf(stmt)
1151
+ return !(b && synthetic(b[0]) && !live.has(b[0]))
1152
+ })
1153
+
1154
+ return out.length === 0 ? null : out.length === 1 ? out[0] : [';', ...out]
1155
+ }
1156
+
1157
+ const isArrow = node => Array.isArray(node) && node[0] === '=>'
1158
+
1159
+ const OBJECT_BUILTINS = new Set(['create', 'getPrototypeOf', 'getOwnPropertyNames', 'getOwnPropertyDescriptor', 'defineProperty'])
1160
+
1161
+ // Canonical name of the Object reflection built-in `node` references, or null.
1162
+ function objectBuiltinKey(node) {
1163
+ if (!Array.isArray(node) || node[0] !== '.') return null
1164
+ if (node[1] === 'Object' && OBJECT_BUILTINS.has(node[2])) return 'Object.' + node[2]
1165
+ return isObjectHasOwnPropertyRef(node) ? 'Object.prototype.hasOwnProperty' : null
1166
+ }
1167
+
1168
+ // Deep `some` over AST children (skips the op slot, so op names never match).
1169
+ function astSome(node, pred) {
1170
+ if (pred(node)) return true
1171
+ if (!Array.isArray(node)) return false
1172
+ for (let i = 1; i < node.length; i++) if (astSome(node[i], pred)) return true
1173
+ return false
1174
+ }
1175
+
1176
+ // Does `node` contain a `()` call whose string callee satisfies `ok`?
1177
+ const containsCall = (node, ok) =>
1178
+ astSome(node, n => Array.isArray(n) && n[0] === '()' && typeof n[1] === 'string' && ok(n[1]))
1179
+
1180
+ function rewriteBundlerAliases(node, aliases, interopBindings) {
1181
+ if (!Array.isArray(node)) return node
1182
+ const rec = n => rewriteBundlerAliases(n, aliases, interopBindings)
1183
+
1184
+ if (node[0] === ';') {
1185
+ const out = [';']
1186
+ for (let i = 1; i < node.length; i++) {
1187
+ const child = rec(node[i])
1188
+ if (child != null) out.push(child)
1189
+ }
1190
+ return out.length === 1 ? null : out.length === 2 ? out[1] : out
1191
+ }
1192
+ if (node[0] === '{}' && node.length === 2) {
1193
+ const wasBlock = Array.isArray(node[1]) && JZ_BLOCK_OPS.has(node[1][0])
1194
+ const inner = rec(node[1])
1195
+ if (!wasBlock || inner == null) return ['{}', inner]
1196
+ const stayed = Array.isArray(inner) && JZ_BLOCK_OPS.has(inner[0])
1197
+ return ['{}', stayed ? inner : [';', inner]]
1198
+ }
1199
+
1200
+ if (node[0] === '()') {
1201
+ const callee = node[1]
1202
+ const args = callArgs(node.slice(2))
1203
+
1204
+ if (typeof callee === 'string') {
1205
+ const key = aliases.get(callee)
1206
+ if (key === 'Object.defineProperty') {
1207
+ const define = staticDefineProperty(args)
1208
+ if (define !== undefined) return define
1209
+ }
1210
+ if (key === 'Object.getOwnPropertyNames' || key === 'Object.create') {
1211
+ if (key === 'Object.create' && isGetPrototypeOfCall(args[0], aliases)) return ['{}', null]
1212
+ return ['()', key, ...args.map(rec)]
1213
+ }
1214
+ }
1215
+ // `__hasOwnProp.call(o, k)` -> `o.hasOwnProperty(k)`.
1216
+ if (Array.isArray(callee) && callee[0] === '.' && callee[2] === 'call' && args.length >= 2 &&
1217
+ typeof callee[1] === 'string' && aliases.get(callee[1]) === 'Object.prototype.hasOwnProperty')
1218
+ return ['()', ['.', rec(args[0]), 'hasOwnProperty'], rec(args[1])]
1219
+ // `(0, fn)(...)` comma-call resolving to an interop module read.
1220
+ const seqCall = commaZeroCall(callee, interopBindings)
1221
+ if (seqCall) return ['()', seqCall, ...args.map(rec)]
1222
+ }
1223
+
1224
+ if (node[0] === '.' || node[0] === '?.') return [node[0], rec(node[1]), node[2]]
1225
+ if (node[0] === ':') return [node[0], node[1], rec(node[2])]
1226
+ return node.map((part, i) => i === 0 ? part : rec(part))
1227
+ }
1228
+
1229
+ function replaceInteropReads(node, bindings) {
1230
+ if (typeof node === 'string' && bindings.has(node)) return cloneAst(bindings.get(node))
1231
+ if (!Array.isArray(node)) return node
1232
+ if (node[0] === '=' && typeof node[1] === 'string') return ['=', node[1], replaceInteropReads(node[2], bindings)]
1233
+ if (node[0] === 'let' || node[0] === 'const' || node[0] === 'var')
1234
+ return [node[0], ...node.slice(1).map(decl =>
1235
+ Array.isArray(decl) && decl[0] === '=' ? ['=', decl[1], replaceInteropReads(decl[2], bindings)] : decl)]
1236
+ if ((node[0] === '.' || node[0] === '?.') && typeof node[1] === 'string' && typeof node[2] === 'string' && bindings.has(node[1])) {
1237
+ const mod = cloneAst(bindings.get(node[1]))
1238
+ return node[2] === 'default' ? mod : [node[0], mod, node[2]]
1239
+ }
1240
+ if (node[0] === ':') return [node[0], node[1], replaceInteropReads(node[2], bindings)]
1241
+ return node.map((part, i) => i === 0 ? part : replaceInteropReads(part, bindings))
1242
+ }
1243
+
1244
+ function isGetPrototypeOfCall(node, aliases) {
1245
+ if (!Array.isArray(node) || node[0] !== '()') return false
1246
+ const callee = node[1]
1247
+ return (typeof callee === 'string' && aliases.get(callee) === 'Object.getPrototypeOf') ||
1248
+ objectBuiltinKey(callee) === 'Object.getPrototypeOf'
1249
+ }
1250
+
1251
+ function commaZeroCall(callee, bindings) {
1252
+ if (!Array.isArray(callee) || callee[0] !== '()' || !Array.isArray(callee[1]) || callee[1][0] !== ',') return null
1253
+ const parts = callee[1].slice(1)
1254
+ if (parts.length !== 2 || !isZeroLiteral(parts[0])) return null
1255
+ const fn = replaceInteropReads(parts[1], bindings)
1256
+ return fn === parts[1] ? null : fn
1257
+ }
1258
+
1259
+ // `defProp(obj, "key", descriptor)` -> `obj.key = value`; null drops `__esModule`.
1260
+ function staticDefineProperty(args) {
1261
+ if (args.length < 3) return undefined
1262
+ const [obj, keyExpr, desc] = args
1263
+ const key = stringLiteral(keyExpr)
1264
+ const props = objectProps(desc)
1265
+ if (typeof key !== 'string' || !props) return undefined
1266
+ if (key === '__esModule') return null
1267
+ const prop = name => props.find(p => Array.isArray(p) && p[0] === ':' && p[1] === name)?.[2]
1268
+ const value = prop('value')
1269
+ if (value !== undefined) return ['=', ['.', obj, key], value]
1270
+ const got = getterReturnExpr(prop('get'))
1271
+ return got !== null ? ['=', ['.', obj, key], got] : undefined
1272
+ }
1273
+
1274
+ function stringLiteral(node) {
1275
+ return Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
1276
+ }
1277
+
1278
+ // Identifier references in `node`, excluding declaration names, member property
1279
+ // names and object keys — enough to decide whether a synthetic binding is live.
1280
+ function collectRefs(node, out) {
1281
+ if (typeof node === 'string') return void out.add(node)
1282
+ if (!Array.isArray(node)) return
1283
+ if (node[0] === 'let' || node[0] === 'const' || node[0] === 'var') {
1284
+ for (let i = 1; i < node.length; i++)
1285
+ if (Array.isArray(node[i]) && node[i][0] === '=') collectRefs(node[i][2], out)
1286
+ } else if ((node[0] === '.' || node[0] === '?.') && typeof node[2] === 'string') {
1287
+ collectRefs(node[1], out)
1288
+ } else if (node[0] === ':') {
1289
+ collectRefs(node[2], out)
1290
+ } else {
1291
+ for (let i = 1; i < node.length; i++) collectRefs(node[i], out)
1292
+ }
1293
+ }
1294
+
674
1295
  function astSeq(ast) {
675
1296
  if (!Array.isArray(ast)) return null
676
1297
  return ast[0] === ';' ? ast.slice(1).filter(Boolean) : [ast]
@@ -680,12 +1301,28 @@ function isObjectDefineProperty(node) {
680
1301
  return Array.isArray(node) && node[0] === '.' && node[1] === 'Object' && node[2] === 'defineProperty'
681
1302
  }
682
1303
 
1304
+ /** Unwrap an esbuild module binding to `[name, init]`. After hoistVars, a binding
1305
+ * reaches this pass either split into a bare `['=', name, init]` (RHS hoisted out
1306
+ * as a separate `let name;`) or kept as a single `['let', ['=', name, init]]` decl
1307
+ * (arrow RHS — see the `;` handler in hoistVars). The fold keys on name/init,
1308
+ * so it must see through both shapes. */
1309
+ function bindingOf(stmt) {
1310
+ if (!Array.isArray(stmt)) return null
1311
+ if (stmt[0] === '=' && typeof stmt[1] === 'string') return [stmt[1], stmt[2]]
1312
+ if ((stmt[0] === 'let' || stmt[0] === 'const' || stmt[0] === 'var') && stmt.length === 2 &&
1313
+ Array.isArray(stmt[1]) && stmt[1][0] === '=' && typeof stmt[1][1] === 'string')
1314
+ return [stmt[1][1], stmt[1][2]]
1315
+ return null
1316
+ }
1317
+
683
1318
  function isDefPropAliasAssign(stmt, aliases) {
684
- return Array.isArray(stmt) && stmt[0] === '=' && aliases.has(stmt[1]) && isObjectDefineProperty(stmt[2])
1319
+ const b = bindingOf(stmt)
1320
+ return b != null && aliases.has(b[0]) && isObjectDefineProperty(b[1])
685
1321
  }
686
1322
 
687
1323
  function isExportHelperAssign(stmt, helpers) {
688
- return Array.isArray(stmt) && stmt[0] === '=' && helpers.has(stmt[1])
1324
+ const b = bindingOf(stmt)
1325
+ return b != null && helpers.has(b[0])
689
1326
  }
690
1327
 
691
1328
  function containsDefinePropertyCall(node, aliases) {
@@ -730,6 +1367,8 @@ function getterReturnExpr(node) {
730
1367
  if (params.length !== 0) return null
731
1368
  const body = node[2]
732
1369
  if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === 'return') return body[1][1]
1370
+ if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';' &&
1371
+ Array.isArray(body[1][1]) && body[1][1][0] === 'return') return body[1][1][1]
733
1372
  if (Array.isArray(body) && body[0] === 'return') return body[1]
734
1373
  return body
735
1374
  }
@@ -752,6 +1391,9 @@ function canonicalizeObjectIdioms(node) {
752
1391
  const hasOwnCall = objectHasOwnPropertyCall(out)
753
1392
  if (hasOwnCall) return ['()', ['.', hasOwnCall.obj, 'hasOwnProperty'], hasOwnCall.key]
754
1393
 
1394
+ const mapString = arrayMapStringCallback(out)
1395
+ if (mapString) return mapString
1396
+
755
1397
  if (out[0] === '&&') {
756
1398
  const leftCtor = constructorIsObject(out[1])
757
1399
  const rightKeys = objectKeysLengthZero(out[2])
@@ -765,16 +1407,31 @@ function canonicalizeObjectIdioms(node) {
765
1407
  return out
766
1408
  }
767
1409
 
1410
+ function arrayMapStringCallback(node) {
1411
+ if (!Array.isArray(node) || node[0] !== '()') return null
1412
+ const callee = node[1]
1413
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
1414
+ const args = callArgs(node.slice(2))
1415
+ if (args.length !== 1 || args[0] !== 'String') return null
1416
+ return ['()', callee, ['=>', 'value', ['()', 'String', 'value']]]
1417
+ }
1418
+
768
1419
  function objectHasOwnPropertyCall(node) {
769
1420
  if (!Array.isArray(node) || node[0] !== '()') return null
770
1421
  const callee = node[1]
771
1422
  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
1423
+ if (!isObjectHasOwnPropertyRef(callee[1])) return null
773
1424
  const args = callArgs(node.slice(2))
774
1425
  if (args.length < 2) return null
775
1426
  return { obj: args[0], key: args[1] }
776
1427
  }
777
1428
 
1429
+ function isObjectHasOwnPropertyRef(node) {
1430
+ if (!Array.isArray(node) || node[0] !== '.' || node[2] !== 'hasOwnProperty') return false
1431
+ if (node[1] === 'Object') return true
1432
+ return Array.isArray(node[1]) && node[1][0] === '.' && node[1][1] === 'Object' && node[1][2] === 'prototype'
1433
+ }
1434
+
778
1435
  function constructorIsObject(node) {
779
1436
  if (!Array.isArray(node) || (node[0] !== '===' && node[0] !== '==')) return null
780
1437
  const left = constructorReceiver(node[1])
@@ -821,41 +1478,103 @@ function cloneAst(node) {
821
1478
  return node.map(cloneAst)
822
1479
  }
823
1480
 
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]]
1481
+ /** Flatten a switch clause body to a single node, dropping ASI position markers.
1482
+ * Unlike a plain statement list this keeps any `break` intact — transformSwitch
1483
+ * needs the breaks to gate fall-through; it rewrites them to a sticky flag. */
1484
+ function normalizeCaseBody(body) {
1485
+ if (!Array.isArray(body) || body[0] !== ';') return body
1486
+ const stmts = body.slice(1).filter(s => s != null && typeof s !== 'number')
1487
+ return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
1488
+ }
1489
+
1490
+ const SWITCH_BREAK_BOUNDARIES = new Set(['for', 'for-in', 'for-of', 'while', 'do', 'switch', '=>', 'function', 'class'])
1491
+
1492
+ function hasOwnSwitchBreak(node) {
1493
+ if (!Array.isArray(node)) return false
1494
+ if (node[0] === 'break') return true
1495
+ if (SWITCH_BREAK_BOUNDARIES.has(node[0])) return false
1496
+ for (let i = 1; i < node.length; i++) if (hasOwnSwitchBreak(node[i])) return true
1497
+ return false
1498
+ }
1499
+
1500
+ function rewriteSwitchBreaks(node, flag) {
1501
+ if (!Array.isArray(node)) return node
1502
+ const op = node[0]
1503
+ if (op === 'break') return ['=', flag, [null, true]]
1504
+ if (SWITCH_BREAK_BOUNDARIES.has(op)) return node
1505
+
1506
+ if (op === ';') {
1507
+ const out = []
1508
+ const stmts = node.slice(1)
1509
+ for (let i = 0; i < stmts.length; i++) {
1510
+ const stmt = stmts[i]
1511
+ out.push(rewriteSwitchBreaks(stmt, flag))
1512
+ if (hasOwnSwitchBreak(stmt) && i < stmts.length - 1) {
1513
+ const tail = rewriteSwitchBreaks([';', ...stmts.slice(i + 1)], flag)
1514
+ out.push(['if', ['!', flag], tail])
1515
+ break
1516
+ }
1517
+ }
1518
+ return out.length === 0 ? null : out.length === 1 ? out[0] : [';', ...out]
831
1519
  }
832
- if (body[0] !== ';') return body
833
1520
 
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]
1521
+ return node.map((part, i) => i === 0 ? part : rewriteSwitchBreaks(part, flag))
837
1522
  }
838
1523
 
839
- /** Transform switch statement to if/else chain. */
1524
+ /** Transform a switch into structured control flow with faithful fall-through.
1525
+ *
1526
+ * A pure if/else-if chain (the former lowering) can't express fall-through,
1527
+ * stacked labels, or a `default` clause that isn't last \u2014 it ran only the first
1528
+ * matching body. The correct model is two-phase, evaluated once with no goto:
1529
+ *
1530
+ * 1. ENTRY \u2014 compare the discriminant against each `case` label in source
1531
+ * order; the first `===` match fixes the entry index. No case matches \u2192
1532
+ * entry = the `default` clause's source index (or past-end if none).
1533
+ * 2. RUN \u2014 walk clauses in source order; `entry <= i` runs clause i, so every
1534
+ * clause from the entry onward executes (fall-through). A `break` flips the
1535
+ * sticky `brk` flag (via rewriteSwitchBreaks) and gates the rest.
1536
+ *
1537
+ * The discriminant is bound to a temp only when re-reading it isn't free/safe; a
1538
+ * bare identifier is compared directly \u2014 a synthetic temp would shed its STRING
1539
+ * val-type and mis-fold string `case`s to `false` under strict-=== folding. */
840
1540
  let swIdx = 0
841
1541
  function transformSwitch(discriminant, cases) {
842
1542
  const disc = transform(discriminant)
843
- const tmp = `\uE000sw${swIdx++}`
844
-
845
- // Collect case/default
846
- const stmts = [['let', ['=', tmp, disc]]]
1543
+ const simple = typeof disc === 'string' || (Array.isArray(disc) && disc[0] == null)
1544
+ const tmp = simple ? disc : `\uE000sw${swIdx++}`
1545
+ const start = `\uE000swst${swIdx++}`
1546
+ const needsBreakFlag = cases.some(c => hasOwnSwitchBreak(c[0] === 'case' ? c[2] : c[1]))
1547
+ const brk = needsBreakFlag ? `\uE000swbrk${swIdx++}` : null
1548
+
1549
+ const n = cases.length
1550
+ let defaultIdx = -1
1551
+ const bodies = cases.map((c, i) => {
1552
+ if (c[0] === 'default') { defaultIdx = i; return transform(c[1]) }
1553
+ return transform(c[2])
1554
+ })
1555
+
1556
+ const stmts = []
1557
+ if (!simple) stmts.push(['let', ['=', tmp, disc]])
1558
+
1559
+ // Phase 1 \u2014 entry index. Init to default's position (or n = "no clause runs"),
1560
+ // then let the first matching label override it via an if/else-if chain.
1561
+ stmts.push(['let', ['=', start, [null, defaultIdx >= 0 ? defaultIdx : n]]])
847
1562
  let chain = null
848
-
849
- for (let i = cases.length - 1; i >= 0; i--) {
850
- const c = cases[i]
851
- if (c[0] === 'default') {
852
- chain = transform(c[1])
853
- } else if (c[0] === 'case') {
854
- const cond = ['===', tmp, transform(c[1])]
855
- const body = transform(c[2])
856
- chain = chain != null ? ['if', cond, body, chain] : ['if', cond, body]
857
- }
1563
+ for (let i = n - 1; i >= 0; i--) {
1564
+ if (cases[i][0] !== 'case') continue
1565
+ const hit = ['=', start, [null, i]]
1566
+ const cond = ['===', tmp, transform(cases[i][1])]
1567
+ chain = chain != null ? ['if', cond, hit, chain] : ['if', cond, hit]
858
1568
  }
859
1569
  if (chain) stmts.push(chain)
1570
+ if (brk) stmts.push(['let', ['=', brk, [null, false]]])
1571
+
1572
+ // Phase 2 \u2014 run clauses from the entry index, falling through until a break.
1573
+ for (let i = 0; i < n; i++) {
1574
+ if (bodies[i] == null) continue
1575
+ const body = brk ? rewriteSwitchBreaks(bodies[i], brk) : bodies[i]
1576
+ const reached = ['<=', start, [null, i]]
1577
+ stmts.push(['if', brk ? ['&&', ['!', brk], reached] : reached, body])
1578
+ }
860
1579
  return [';', ...stmts]
861
1580
  }