jz 0.1.0 → 0.2.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/ir.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * @module ir
21
21
  */
22
22
 
23
- import { ctx, err, inc, PTR } from './ctx.js'
23
+ import { ctx, err, inc, PTR, LAYOUT } from './ctx.js'
24
24
  import { T, VAL, valTypeOf, lookupValType, repOf, repOfGlobal } from './analyze.js'
25
25
 
26
26
  // === Type helpers ===
@@ -49,8 +49,10 @@ function boxPtrIR(i32node, ptrType, aux = 0) {
49
49
  * The `unsigned` flag (set by `>>>` codegen) opts into `convert_i32_u` so the canonical
50
50
  * `(x >>> 0)` uint32 idiom converts to a positive f64 in [0, 2^32) instead of sign-flipping. */
51
51
  export const asF64 = n => {
52
+ if (n == null) err(`compiler internal: expected emitted IR value in ${ctx.func.current?.name || '<module>'}, got empty value`)
52
53
  if (n.ptrKind != null) return boxPtrIR(n, valKindToPtr(n.ptrKind), n.ptrAux || 0)
53
54
  if (n.type === 'f64') return n
55
+ if (n.type === 'i64') return typed(['f64.reinterpret_i64', n], 'f64')
54
56
  if (n[0] === 'i32.const' && typeof n[1] === 'number') return typed(['f64.const', n[1]], 'f64')
55
57
  return typed([n.unsigned ? 'f64.convert_i32_u' : 'f64.convert_i32_s', n], 'f64')
56
58
  }
@@ -73,8 +75,8 @@ export const asI32 = n => {
73
75
  export const asPtrOffset = (n, ptrKind) =>
74
76
  n.ptrKind === ptrKind ? n : typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(n)]], 'i32')
75
77
 
76
- /** Coerce emitted IR to a target WASM param type ('i32' | 'f64'). */
77
- export const asParamType = (n, t) => t === 'i32' ? asI32(n) : asF64(n)
78
+ /** Coerce emitted IR to a target WASM param type ('i32' | 'i64' | 'f64'). */
79
+ export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : asF64(n)
78
80
 
79
81
  /** Coerce node to i32 with wrapping (JS `|0` semantics: values > 2^31 wrap to negative).
80
82
  * Per ECMAScript ToInt32, NaN and ±∞ map to 0. `i64.trunc_sat_f64_s` handles NaN
@@ -90,6 +92,10 @@ export const toI32 = n => {
90
92
  const inner = n[1]
91
93
  return Array.isArray(inner) ? typed(inner, 'i32') : inner
92
94
  }
95
+ if (Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'number') {
96
+ const v = n[1]
97
+ return typed(['i32.const', Number.isFinite(v) ? v | 0 : 0], 'i32')
98
+ }
93
99
  // Leaf nodes are cheap to duplicate; for everything else, evaluate once via local.tee.
94
100
  const isLeaf = Array.isArray(n) && n.length <= 2 &&
95
101
  (n[0] === 'f64.const' || n[0] === 'local.get' || n[0] === 'global.get')
@@ -117,11 +123,14 @@ export const fromI64 = n => typed(['f64.reinterpret_i64', n], 'f64')
117
123
 
118
124
  // === Nullish sentinels ===
119
125
 
120
- /** Null/undefined: one nullish value inside jz. NaN-boxed ATOM (type=0, aux=1, offset=0).
126
+ /** Reserved atoms (PTR.ATOM tag, offset=0).
127
+ * aux=1 → null (NULL_NAN)
128
+ * aux=2 → undefined (UNDEF_NAN)
129
+ * See module/symbol.js for the broader reserved-atom-id scheme.
121
130
  * Distinct from 0, NaN, and all pointers. Triggers default params.
122
131
  * At the JS boundary, null and undefined preserve their identity for interop. */
123
- export const NULL_NAN = '0x7FF8000100000000'
124
- export const UNDEF_NAN = '0x7FF8000000000001'
132
+ export const NULL_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (1n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
133
+ export const UNDEF_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
125
134
  /** WAT-template-ready sentinel expressions for use in stdlib template strings.
126
135
  * `f64.const nan:0xHEX` is 3 bytes shorter than `f64.reinterpret_i64 (i64.const ...)`. */
127
136
  export const NULL_WAT = `(f64.const nan:${NULL_NAN})`
@@ -148,15 +157,14 @@ export const BOXED_MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splic
148
157
 
149
158
  // === Pointer construction ===
150
159
 
151
- const NAN_PREFIX_BITS = 0x7FF8n
152
160
  const litI32 = n => Array.isArray(n) && n[0] === 'i32.const' && typeof n[1] === 'number' ? n[1] : null
153
161
 
154
162
  /** Pack (type, aux, offset) into the f64 NaN-box bit pattern as a hex string. */
155
163
  function packPtrBits(type, aux, offset) {
156
- const bits = (NAN_PREFIX_BITS << 48n)
157
- | ((BigInt(type) & 0xFn) << 47n)
158
- | ((BigInt(aux) & 0x7FFFn) << 32n)
159
- | (BigInt(offset >>> 0) & 0xFFFFFFFFn)
164
+ const bits = LAYOUT.NAN_PREFIX_BITS
165
+ | ((BigInt(type) & BigInt(LAYOUT.TAG_MASK)) << BigInt(LAYOUT.TAG_SHIFT))
166
+ | ((BigInt(aux) & BigInt(LAYOUT.AUX_MASK)) << BigInt(LAYOUT.AUX_SHIFT))
167
+ | (BigInt(offset >>> 0) & BigInt(LAYOUT.OFFSET_MASK))
160
168
  return '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
161
169
  }
162
170
 
@@ -184,14 +192,14 @@ export function ptrOffsetIR(valIR, valType) {
184
192
  return ['i32.wrap_i64', ['i64.reinterpret_f64', valIR]]
185
193
  }
186
194
  inc('__ptr_offset')
187
- return ['call', '$__ptr_offset', valIR]
195
+ return ['call', '$__ptr_offset', ['i64.reinterpret_f64', valIR]]
188
196
  }
189
197
 
190
198
  /** Map VAL.* → PTR.* when unambiguous. STRING is ambiguous (heap vs SSO). ARRAY maps
191
199
  * to PTR.ARRAY but callers that want to skip forwarding must check separately. */
192
200
  const VAL_TO_PTR = {
193
201
  array: PTR.ARRAY, object: PTR.OBJECT, set: PTR.SET, map: PTR.MAP,
194
- closure: PTR.CLOSURE, typed: PTR.TYPED, buffer: PTR.BUFFER,
202
+ closure: PTR.CLOSURE, typed: PTR.TYPED, buffer: PTR.BUFFER, date: PTR.OBJECT,
195
203
  }
196
204
  export const valKindToPtr = (vt) => VAL_TO_PTR[vt]
197
205
 
@@ -247,7 +255,7 @@ export function appendStaticSlots(slots, headerBytes = 0) {
247
255
  if (!ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = []
248
256
  for (let i = 0; i < slots.length; i++) {
249
257
  const bits = slots[i]
250
- if (((bits >> 48n) & 0xFFF8n) === NAN_PREFIX_BITS) {
258
+ if (((bits >> 48n) & 0xFFF8n) === BigInt(LAYOUT.NAN_PREFIX)) {
251
259
  ctx.runtime.staticPtrSlots.push(off + i * 8)
252
260
  }
253
261
  }
@@ -272,11 +280,22 @@ const PURE_OPS = new Set(['i32.const', 'f64.const', 'local.get', 'global.get',
272
280
  'i32.eq', 'i32.ne', 'i32.lt_s', 'i32.gt_s', 'i32.le_s', 'i32.ge_s', 'i32.eqz'])
273
281
  export const isPureIR = n => Array.isArray(n) && PURE_OPS.has(n[0]) && n.slice(1).every(c => !Array.isArray(c) || isPureIR(c))
274
282
 
283
+ /** Check if AST node is a literal string: ['str', 'value']. */
284
+ export const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
285
+
286
+ /** Resolve compile-time value type from AST node (literal → type, name → lookup). */
287
+ export const resolveValType = (node, valTypeOf, lookupValType) =>
288
+ valTypeOf(node) ?? (typeof node === 'string' ? lookupValType(node) : null)
289
+
290
+ /** Check if AST node is a string reference to a known function name. */
291
+ export const isFuncRef = (node, funcNames) => typeof node === 'string' && funcNames.has(node)
292
+
275
293
  /** Check if (a, op, b) is a postfix pattern: [op, name] and [, 1] literal. */
276
294
  export const isPostfix = (a, op, b) => Array.isArray(a) && a[0] === op && Array.isArray(b) && b[0] == null && b[1] === 1
277
295
 
278
- /** Emit a numeric constant with correct i32/f64 typing. */
279
- export const emitNum = v => Number.isInteger(v) && v >= -2147483648 && v <= 2147483647
296
+ /** Emit a numeric constant with correct i32/f64 typing.
297
+ * `-0` is f64-only (i32 has no signed zero) preserve the sign by emitting f64. */
298
+ export const emitNum = v => Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 && !Object.is(v, -0)
280
299
  ? typed(['i32.const', v], 'i32') : typed(['f64.const', v], 'f64')
281
300
 
282
301
  // === Temp locals ===
@@ -339,7 +358,7 @@ export function toNumF64(node, v) {
339
358
  }
340
359
  if (!ctx.core.stdlib['__to_num']) return asF64(v)
341
360
  inc('__to_num')
342
- return typed(['call', '$__to_num', asF64(v)], 'f64')
361
+ return typed(['call', '$__to_num', asI64(v)], 'f64')
343
362
  }
344
363
 
345
364
  /** Convert already-emitted WASM node to i32 boolean. NaN is falsy (like JS).
@@ -361,7 +380,7 @@ export function truthyIR(e) {
361
380
  // all other NaN-boxed pointers (SSO strings, heap ptrs, etc.) are truthy.
362
381
  if (e[0] === 'f64.reinterpret_i64' && Array.isArray(e[1]) && e[1][0] === 'i64.const') {
363
382
  const bits = String(e[1][1])
364
- const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA800000000000'])
383
+ const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA400000000000'])
365
384
  return typed(['i32.const', FALSY.has(bits) ? 0 : 1], 'i32')
366
385
  }
367
386
  // Fresh pointer constructors never produce nullish. Treat as always truthy.
@@ -375,13 +394,13 @@ export function truthyIR(e) {
375
394
  const name = e[1][0] === '$' ? e[1].slice(1) : e[1]
376
395
  const vt = lookupValType(name)
377
396
  if (vt === VAL.ARRAY || vt === VAL.OBJECT || vt === VAL.SET || vt === VAL.MAP ||
378
- vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX) {
397
+ vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX || vt === VAL.DATE) {
379
398
  return typed(['i32.eqz', isNullish(e)], 'i32')
380
399
  }
381
400
  }
382
401
  }
383
402
  inc('__is_truthy')
384
- return typed(['call', '$__is_truthy', asF64(e)], 'i32')
403
+ return typed(['call', '$__is_truthy', asI64(e)], 'i32')
385
404
  }
386
405
  export const toBoolFromEmitted = truthyIR
387
406
 
@@ -400,9 +419,13 @@ export function usesDynProps(vt) {
400
419
  * `target` is the var name receiving the literal (or null when escaping). */
401
420
  export function needsDynShadow(target) {
402
421
  if (!ctx.module.modules.collection) return false
422
+ // Functions/CLOSURE always need dynamic props so cross-module property
423
+ // access (fn.parse, i32.parse aliases) sees the same value as schema slots.
424
+ const vt = typeof target === 'string' ? (ctx.func.repByLocal?.get(target)?.val || ctx.scope.globalValTypes?.get(target)) : null
425
+ if (vt === 'closure' || usesDynProps(vt)) return true
403
426
  const dyn = ctx.types?.dynKeyVars
404
- if (target == null) return ctx.types?.anyDynKey ?? true
405
- return dyn ? dyn.has(target) : true
427
+ if (target == null) return ctx.types?.anyDynKey ?? false
428
+ return dyn ? dyn.has(target) : false
406
429
  }
407
430
 
408
431
  // === Variable storage abstraction ===
@@ -531,7 +554,29 @@ export const isNullish = (f64expr) => {
531
554
  }
532
555
  // Non-trivial expr: fall back to the helper — keeps binary size stable & preserves eval once.
533
556
  inc('__is_nullish')
534
- return typed(['call', '$__is_nullish', f64expr], 'i32')
557
+ return typed(['call', '$__is_nullish', ['i64.reinterpret_f64', f64expr]], 'i32')
558
+ }
559
+
560
+ /** Check if f64 expr is exactly `undefined` (UNDEF_NAN). Returns i32.
561
+ * Used by default-param semantics — only `undefined` (or missing arg) triggers
562
+ * the default; `null` should pass through. */
563
+ export const isUndef = (f64expr) => {
564
+ if (f64expr.ptrKind != null) return typed(['i32.const', 0], 'i32')
565
+ if (Array.isArray(f64expr)) {
566
+ if (f64expr[0] === 'f64.const') {
567
+ const lit = String(f64expr[1])
568
+ if (lit.startsWith('nan:')) {
569
+ const bits = lit.slice(4)
570
+ return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
571
+ }
572
+ return typed(['i32.const', 0], 'i32')
573
+ }
574
+ if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const') {
575
+ const bits = String(f64expr[1][1])
576
+ return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
577
+ }
578
+ }
579
+ return typed(['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]], 'i32')
535
580
  }
536
581
 
537
582
  // === Array layout helpers ===
@@ -560,16 +605,21 @@ export function elemStore(ptr, i, val) {
560
605
  *
561
606
  * Optional `lenLocal`: caller already has the array length in an i32 local
562
607
  * (e.g. from sizing the output before the loop). Reuses it instead of
563
- * re-loading from ptr-8. */
564
- export function arrayLoop(arrExpr, bodyFn, lenLocal) {
565
- inc('__ptr_offset')
566
- const arr = temp('aa'), ptr = tempI32('ap'), i = tempI32('ai'), item = temp('av')
608
+ * re-loading from ptr-8.
609
+ * Optional `ptrLocal`: caller already has the resolved ARRAY data pointer in
610
+ * an i32 local. Reuses it instead of calling __ptr_offset again. */
611
+ export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
612
+ const arr = ptrLocal ? null : temp('aa'), ptr = ptrLocal ?? tempI32('ap'), i = tempI32('ai'), item = temp('av')
567
613
  const len = lenLocal ?? tempI32('al')
568
614
  const id = ctx.func.uniq++
569
- const setup = [
570
- ['local.set', `$${arr}`, asF64(arrExpr)],
571
- ['local.set', `$${ptr}`, ['call', '$__ptr_offset', ['local.get', `$${arr}`]]],
572
- ]
615
+ const setup = []
616
+ if (!ptrLocal) {
617
+ inc('__ptr_offset')
618
+ setup.push(
619
+ ['local.set', `$${arr}`, asF64(arrExpr)],
620
+ ['local.set', `$${ptr}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]],
621
+ )
622
+ }
573
623
  if (!lenLocal) setup.push(
574
624
  ['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]])
575
625
  setup.push(
package/src/jzify.js CHANGED
@@ -26,19 +26,134 @@ export default function jzify(ast) {
26
26
  swIdx = 0
27
27
  argsIdx = 0
28
28
  doIdx = 0
29
+ // Hoist module-level vars: any `var x` inside nested blocks bubbles up.
30
+ const names = new Set()
31
+ ast = hoistVars(ast, names)
32
+ if (names.size) ast = prependDecls(ast, names)
29
33
  return transformScope(ast)
30
34
  }
31
35
 
36
+ /**
37
+ * Walk function/script body, replacing `var` declarations with assignments and
38
+ * collecting names. Does not cross function/arrow boundaries — nested functions
39
+ * get their own hoist pass when wrapArrowBody processes them.
40
+ *
41
+ * ['var', 'x'] → null (bare decl, no-op)
42
+ * ['var', ['=', x, init]] → ['=', x, init]
43
+ * ['var', ['=', x, 1], ['=', y, 2]] → [',', ['=', x, 1], ['=', y, 2]]
44
+ * ['var', 'x', 'y'] → null
45
+ * ['in', ['var', x], obj] → ['in', x, obj] (for-in head)
46
+ */
47
+ function hoistVars(node, names) {
48
+ if (node == null || !Array.isArray(node)) return node
49
+ const op = node[0]
50
+ // Nested function/arrow: hoist within its own scope, prepend let-decl, return new node.
51
+ if (op === 'function') {
52
+ const inner = new Set()
53
+ let body = hoistVars(node[3], inner)
54
+ if (inner.size) body = prependDecls(body, inner)
55
+ return ['function', node[1], node[2], body]
56
+ }
57
+ if (op === '=>') {
58
+ const inner = new Set()
59
+ let body = hoistVars(node[2], inner)
60
+ if (inner.size) body = prependDecls(body, inner)
61
+ return ['=>', node[1], body]
62
+ }
63
+ if (op === 'in' || op === 'of') {
64
+ let lhs = node[1]
65
+ if (Array.isArray(lhs) && lhs[0] === 'var' && typeof lhs[1] === 'string' && lhs.length === 2) {
66
+ names.add(lhs[1])
67
+ lhs = lhs[1]
68
+ } else {
69
+ lhs = hoistVars(lhs, names)
70
+ }
71
+ return [op, lhs, hoistVars(node[2], names)]
72
+ }
73
+ // For-head `;` is positional (init; cond; update), not a statement sequence.
74
+ // Recurse into each slot but never filter nulls — empty slots are valid.
75
+ if (op === 'for') {
76
+ const head = node[1]
77
+ let h2
78
+ if (Array.isArray(head) && head[0] === ';') {
79
+ h2 = [';']
80
+ for (let i = 1; i < head.length; i++) h2.push(hoistVars(head[i], names))
81
+ } else {
82
+ h2 = hoistVars(head, names)
83
+ }
84
+ return ['for', h2, hoistVars(node[2], names)]
85
+ }
86
+ if (op === 'var') {
87
+ const decls = []
88
+ for (let i = 1; i < node.length; i++) {
89
+ const d = node[i]
90
+ if (typeof d === 'string') { names.add(d); continue }
91
+ if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
92
+ names.add(d[1])
93
+ decls.push(['=', d[1], hoistVars(d[2], names)])
94
+ }
95
+ }
96
+ if (decls.length === 0) return null
97
+ if (decls.length === 1) return decls[0]
98
+ return [',', ...decls]
99
+ }
100
+ // Filter null returns from `;` sequences (bare-var no-ops). `{}` is left
101
+ // to recurse normally — it may be either a block or an object literal,
102
+ // and we don't want to clobber `['{}', null]` (empty object literal).
103
+ if (op === ';') {
104
+ const out = [op]
105
+ for (let i = 1; i < node.length; i++) {
106
+ const c = hoistVars(node[i], names)
107
+ if (c != null) out.push(c)
108
+ }
109
+ if (out.length === 1) return null
110
+ if (out.length === 2) return out[1]
111
+ return out
112
+ }
113
+ const out = new Array(node.length)
114
+ out[0] = op
115
+ for (let i = 1; i < node.length; i++) out[i] = hoistVars(node[i], names)
116
+ return out
117
+ }
118
+
119
+ function prependDecls(body, names) {
120
+ const decl = ['let', ...names]
121
+ if (Array.isArray(body) && body[0] === ';') return [';', decl, ...body.slice(1)]
122
+ if (Array.isArray(body) && body[0] === '{}') {
123
+ const inner = body[1]
124
+ if (Array.isArray(inner) && inner[0] === ';') return ['{}', [';', decl, ...inner.slice(1)]]
125
+ if (inner == null) return ['{}', decl]
126
+ return ['{}', [';', decl, inner]]
127
+ }
128
+ return body == null ? decl : [';', decl, body]
129
+ }
130
+
131
+ /** Convert a named function declaration to a hoisted const arrow */
132
+ function hoistFnDecl(name, params, body) {
133
+ const [p2, b2] = lowerArguments(params, body)
134
+ const decl = ['const', ['=', name, ['=>', p2, wrapArrowBody(b2)]]]
135
+ decl._hoisted = true
136
+ return decl
137
+ }
138
+
32
139
  /** Transform a scope (module top-level or block body). Collects hoisted functions. */
33
140
  function transformScope(node) {
34
141
  if (!Array.isArray(node)) return transform(node)
35
142
 
36
143
  const [op, ...args] = node
37
144
 
145
+ // Single named function-statement at scope position: hoist as const arrow
146
+ if (op === 'function' && args[0]) return hoistFnDecl(...args)
147
+
38
148
  // Statement sequence: collect hoisted functions
39
149
  if (op === ';') {
40
150
  const hoisted = [], rest = []
41
151
  for (const stmt of args) {
152
+ // Statement-form named function declaration: hoist directly (skip expression handler)
153
+ if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
154
+ hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
155
+ continue
156
+ }
42
157
  const t = transform(stmt)
43
158
  if (t == null) continue
44
159
  // Hoist function declarations to top of scope
@@ -122,38 +237,60 @@ function paramList(params) {
122
237
  }
123
238
 
124
239
  function lowerArguments(params, body) {
125
- if (!usesArguments(body)) return [params, body]
240
+ if (!usesArguments(params) && !usesArguments(body)) return [params, body]
126
241
  const name = `\uE001arg${argsIdx++}`
127
- const items = paramList(params)
128
- items.push(['...', name])
129
- const inner = items.length === 1 ? items[0] : [',', ...items]
130
- return [['()', inner], renameArguments(body, name)]
242
+ const decls = []
243
+ for (const [idx, param] of paramList(params).entries()) {
244
+ if (Array.isArray(param) && param[0] === '...') {
245
+ decls.push(['=', param[1], ['()', ['.', name, 'slice'], [null, idx]]])
246
+ continue
247
+ }
248
+ if (Array.isArray(param) && param[0] === '=') {
249
+ decls.push(['=', param[1], ['??', ['[]', name, [null, idx]], renameArguments(param[2], name)]])
250
+ continue
251
+ }
252
+ decls.push(['=', param, ['[]', name, [null, idx]]])
253
+ }
254
+ const renamed = renameArguments(body, name)
255
+ return [['()', ['...', name]], decls.length ? [';', ['let', ...decls], renamed] : renamed]
131
256
  }
132
257
 
258
+ const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
259
+
133
260
  const handlers = {
134
261
  // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
135
262
  '()'(callee, ...rest) {
136
263
  if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
137
264
  const [, name, params, body] = callee[1]
138
265
  const [p2, b2] = lowerArguments(params, body)
139
- return [';', ['let', ['=', name, ['=>', p2, wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
266
+ return [';', ['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
140
267
  }
141
268
  },
142
269
 
143
- // function → arrow (named hoisted const)
270
+ // function → arrow. Named function expression desugars to IIFE so the name is
271
+ // bound inside body per ES spec: `function f(){...f...}` → `(()=>{let f;f=arrow;return f})()`.
272
+ // Statement-form named functions are hoisted by transformScope before reaching here.
144
273
  'function'(name, params, body) {
145
274
  const [p2, b2] = lowerArguments(params, body)
146
275
  const arrow = ['=>', p2, wrapArrowBody(b2)]
147
- if (name) { const decl = ['const', ['=', name, arrow]]; decl._hoisted = true; return decl }
276
+ if (name) {
277
+ return ['()', ['()', ['=>', null, ['{}', [';',
278
+ ['let', name],
279
+ ['=', name, arrow],
280
+ ['return', name]
281
+ ]]]], null]
282
+ }
148
283
  return arrow
149
284
  },
150
285
 
151
- 'var'(...args) { return ['let', ...args.map(transform)] },
286
+ // `var` is hoisted away before transform reaches here. If one slips through
287
+ // (e.g. raw subscript output without going via jzify entry/wrapArrowBody),
288
+ // fall back to treating it as `let`.
289
+ 'var'(...args) {
290
+ return ['let', ...args.map(transform)]
291
+ },
152
292
 
153
293
  '='(lhs, rhs) {
154
- // var assignment: ['=', ['var', name], init] → let
155
- if (Array.isArray(lhs) && lhs[0] === 'var')
156
- return ['let', ['=', lhs[1], transform(rhs)]]
157
294
  // Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
158
295
  if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
159
296
  const targets = []
@@ -205,25 +342,29 @@ const handlers = {
205
342
  return ['===', ['typeof', t], [null, 'object']]
206
343
  },
207
344
 
208
- // do { body } while (cond) → for (let _once = true; _once || cond; _once = false) body
209
- // Avoids body duplication; matches JS continue semantics (continue runs cond, not body).
345
+ // do { body } while (cond) → let _once = true; while (_once || cond) { _once = false; body }
346
+ // Avoids body duplication and preserves continue: `continue` jumps back to the
347
+ // while condition after the one-shot flag has been cleared.
210
348
  'do'(body, cond) {
211
349
  const flag = `do${doIdx++}`
212
- return ['for',
213
- [';',
214
- ['let', ['=', flag, [null, true]]],
215
- ['||', flag, transform(cond)],
216
- ['=', flag, [null, false]]],
217
- transform(body)]
350
+ return [';',
351
+ ['let', ['=', flag, [null, true]]],
352
+ ['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
218
353
  },
219
354
 
220
355
  // Block body: recurse as scope for hoisting
221
356
  '{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
222
357
 
223
- // Export: recurse into exported declaration
358
+ // Export: recurse into exported declaration. Statement-form `export function name`
359
+ // and `export default function name` must be hoisted as const-arrows — otherwise
360
+ // the generic `function` handler wraps them in a named-IIFE (correct for *expressions*,
361
+ // wrong for declarations), producing `export ['()', IIFE]` which has no exportable binding.
224
362
  'export'(inner) {
363
+ if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
364
+ return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
365
+ }
225
366
  if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
226
- const decl = transform(inner[1])
367
+ const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
227
368
  return [';', decl, ['export', ['default', inner[1][1]]]]
228
369
  }
229
370
  return ['export', transform(inner)]
@@ -294,8 +435,26 @@ export function codegen(node, depth = 0) {
294
435
  // Statements
295
436
  if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
296
437
  if (op === '{}') {
297
- const body = a.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
298
- return '{\n' + ind1 + body + (body ? ';' : '') + '\n' + ind + '}'
438
+ // Discriminate object literal / destructuring pattern from block.
439
+ // Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
440
+ // lone string shorthand. Empty `{}` outputs the same string either way.
441
+ const body = a[0]
442
+ const isObjItem = (n) => typeof n === 'string' ||
443
+ (Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
444
+ (n[0] === '=' && typeof n[1] === 'string')))
445
+ const isObj = body == null ? false
446
+ : typeof body === 'string' ? true
447
+ : Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
448
+ (body[0] === ',' && body.slice(1).every(isObjItem)))
449
+ if (isObj) {
450
+ if (typeof body === 'string') return '{ ' + body + ' }'
451
+ if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
452
+ return '{ ' + codegen(body) + ' }'
453
+ }
454
+ // Block: body is null, a single statement, or [';', ...stmts]
455
+ const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
456
+ const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
457
+ return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
299
458
  }
300
459
 
301
460
  // Declarations
@@ -312,19 +471,30 @@ export function codegen(node, depth = 0) {
312
471
  }
313
472
  if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
314
473
  if (op === 'for') {
315
- if (a.length === 2) { // for...of / for...in
474
+ if (a.length === 2) { // ['for', head, body] — subscript shape
316
475
  const [head, body] = a
317
476
  if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
318
- return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + codegen(body, depth)
319
- return 'for (' + codegen(head) + ') ' + codegen(body, depth)
477
+ return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
478
+ // ['let'/'const', ['in'/'of', name, obj]] subscript wraps var→let around in/of
479
+ if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
480
+ return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
481
+ // C-style head [';', init, cond, update] is positional — empty slots are valid,
482
+ // must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
483
+ if (Array.isArray(head) && head[0] === ';')
484
+ return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
485
+ return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
320
486
  }
321
- return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + codegen(a[3], depth)
487
+ return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
322
488
  }
323
489
  if (op === 'return') return 'return ' + codegen(a[0])
324
490
  if (op === 'throw') return 'throw ' + codegen(a[0])
325
491
  if (op === 'break') return 'break'
326
492
  if (op === 'continue') return 'continue'
327
- if (op === 'catch') return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
493
+ // catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
494
+ if (op === 'catch') {
495
+ if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
496
+ return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
497
+ }
328
498
 
329
499
  // Arrow
330
500
  if (op === '=>') {
@@ -348,25 +518,39 @@ export function codegen(node, depth = 0) {
348
518
  // Property access
349
519
  if (op === '.') return codegen(a[0]) + '.' + a[1]
350
520
  if (op === '?.') return codegen(a[0]) + '?.' + a[1]
351
- if (op === '[]') return codegen(a[0]) + '[' + codegen(a[1]) + ']'
352
-
353
- // Array/object literals
354
- if (op === '[') return '[' + a.map(x => codegen(x)).join(', ') + ']'
521
+ if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
522
+ if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
523
+ if (op === '[]') {
524
+ // Array literal: ['[]', body] (length 2 a.length 1). body may be null (empty),
525
+ // a single element, or a [',', ...items] sequence.
526
+ if (a.length === 1) {
527
+ if (a[0] == null) return '[]'
528
+ const body = a[0]
529
+ if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
530
+ return '[' + codegen(body) + ']'
531
+ }
532
+ // Subscript: ['[]', obj, idx]
533
+ return codegen(a[0]) + '[' + codegen(a[1]) + ']'
534
+ }
355
535
  if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
356
536
  if (op === 'str') return JSON.stringify(a[0])
357
537
  if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
358
538
 
359
539
  // Comma
360
540
  if (op === ',') return a.map(x => codegen(x)).join(', ')
361
- // Template literal
362
- if (op === '`') return '`' + a.map((p, i) => i % 2 ? '${' + codegen(p) + '}' : (p?.[1] ?? '')).join('') + '`'
541
+ // Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
542
+ if (op === '`') return '`' + a.map(p => {
543
+ if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
544
+ return '${' + codegen(p) + '}'
545
+ }).join('') + '`'
363
546
 
364
547
  // Spread
365
548
  if (op === '...') return '...' + codegen(a[0])
366
549
 
367
- // Import
550
+ // Import / export rename
368
551
  if (op === 'import') return 'import ' + codegen(a[0])
369
552
  if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
553
+ if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
370
554
 
371
555
  // Unary prefix
372
556
  if (a.length === 1) {