jz 0.1.1 → 0.2.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/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 ===
@@ -328,6 +347,12 @@ export function toNumF64(node, v) {
328
347
  if (v.type === 'i32' || isLit(v)) return asF64(v)
329
348
  const vt = keyValType(node)
330
349
  if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
350
+ if (vt === VAL.DATE) {
351
+ const ptr = v.ptrKind === VAL.DATE
352
+ ? v
353
+ : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
354
+ return typed(['f64.load', ptr], 'f64')
355
+ }
331
356
  // intCertain locals: every reachable def is integer-valued, so the binding
332
357
  // never carries a NaN-boxed pointer — skip the __to_num wrapper.
333
358
  if (typeof node === 'string' && repOf(node)?.intCertain === true) return asF64(v)
@@ -339,7 +364,7 @@ export function toNumF64(node, v) {
339
364
  }
340
365
  if (!ctx.core.stdlib['__to_num']) return asF64(v)
341
366
  inc('__to_num')
342
- return typed(['call', '$__to_num', asF64(v)], 'f64')
367
+ return typed(['call', '$__to_num', asI64(v)], 'f64')
343
368
  }
344
369
 
345
370
  /** Convert already-emitted WASM node to i32 boolean. NaN is falsy (like JS).
@@ -361,7 +386,7 @@ export function truthyIR(e) {
361
386
  // all other NaN-boxed pointers (SSO strings, heap ptrs, etc.) are truthy.
362
387
  if (e[0] === 'f64.reinterpret_i64' && Array.isArray(e[1]) && e[1][0] === 'i64.const') {
363
388
  const bits = String(e[1][1])
364
- const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA800000000000'])
389
+ const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA400000000000'])
365
390
  return typed(['i32.const', FALSY.has(bits) ? 0 : 1], 'i32')
366
391
  }
367
392
  // Fresh pointer constructors never produce nullish. Treat as always truthy.
@@ -375,13 +400,13 @@ export function truthyIR(e) {
375
400
  const name = e[1][0] === '$' ? e[1].slice(1) : e[1]
376
401
  const vt = lookupValType(name)
377
402
  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) {
403
+ vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX || vt === VAL.DATE) {
379
404
  return typed(['i32.eqz', isNullish(e)], 'i32')
380
405
  }
381
406
  }
382
407
  }
383
408
  inc('__is_truthy')
384
- return typed(['call', '$__is_truthy', asF64(e)], 'i32')
409
+ return typed(['call', '$__is_truthy', asI64(e)], 'i32')
385
410
  }
386
411
  export const toBoolFromEmitted = truthyIR
387
412
 
@@ -400,9 +425,13 @@ export function usesDynProps(vt) {
400
425
  * `target` is the var name receiving the literal (or null when escaping). */
401
426
  export function needsDynShadow(target) {
402
427
  if (!ctx.module.modules.collection) return false
428
+ // Functions/CLOSURE always need dynamic props so cross-module property
429
+ // access (fn.parse, i32.parse aliases) sees the same value as schema slots.
430
+ const vt = typeof target === 'string' ? (ctx.func.repByLocal?.get(target)?.val || ctx.scope.globalValTypes?.get(target)) : null
431
+ if (vt === 'closure' || usesDynProps(vt)) return true
403
432
  const dyn = ctx.types?.dynKeyVars
404
- if (target == null) return ctx.types?.anyDynKey ?? true
405
- return dyn ? dyn.has(target) : true
433
+ if (target == null) return ctx.types?.anyDynKey ?? false
434
+ return dyn ? dyn.has(target) : false
406
435
  }
407
436
 
408
437
  // === Variable storage abstraction ===
@@ -531,7 +560,29 @@ export const isNullish = (f64expr) => {
531
560
  }
532
561
  // Non-trivial expr: fall back to the helper — keeps binary size stable & preserves eval once.
533
562
  inc('__is_nullish')
534
- return typed(['call', '$__is_nullish', f64expr], 'i32')
563
+ return typed(['call', '$__is_nullish', ['i64.reinterpret_f64', f64expr]], 'i32')
564
+ }
565
+
566
+ /** Check if f64 expr is exactly `undefined` (UNDEF_NAN). Returns i32.
567
+ * Used by default-param semantics — only `undefined` (or missing arg) triggers
568
+ * the default; `null` should pass through. */
569
+ export const isUndef = (f64expr) => {
570
+ if (f64expr.ptrKind != null) return typed(['i32.const', 0], 'i32')
571
+ if (Array.isArray(f64expr)) {
572
+ if (f64expr[0] === 'f64.const') {
573
+ const lit = String(f64expr[1])
574
+ if (lit.startsWith('nan:')) {
575
+ const bits = lit.slice(4)
576
+ return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
577
+ }
578
+ return typed(['i32.const', 0], 'i32')
579
+ }
580
+ if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const') {
581
+ const bits = String(f64expr[1][1])
582
+ return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
583
+ }
584
+ }
585
+ return typed(['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]], 'i32')
535
586
  }
536
587
 
537
588
  // === Array layout helpers ===
@@ -560,16 +611,21 @@ export function elemStore(ptr, i, val) {
560
611
  *
561
612
  * Optional `lenLocal`: caller already has the array length in an i32 local
562
613
  * (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')
614
+ * re-loading from ptr-8.
615
+ * Optional `ptrLocal`: caller already has the resolved ARRAY data pointer in
616
+ * an i32 local. Reuses it instead of calling __ptr_offset again. */
617
+ export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
618
+ const arr = ptrLocal ? null : temp('aa'), ptr = ptrLocal ?? tempI32('ap'), i = tempI32('ai'), item = temp('av')
567
619
  const len = lenLocal ?? tempI32('al')
568
620
  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
- ]
621
+ const setup = []
622
+ if (!ptrLocal) {
623
+ inc('__ptr_offset')
624
+ setup.push(
625
+ ['local.set', `$${arr}`, asF64(arrExpr)],
626
+ ['local.set', `$${ptr}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]],
627
+ )
628
+ }
573
629
  if (!lenLocal) setup.push(
574
630
  ['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]])
575
631
  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
@@ -152,27 +267,30 @@ const handlers = {
152
267
  }
153
268
  },
154
269
 
155
- // 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.
156
273
  'function'(name, params, body) {
157
274
  const [p2, b2] = lowerArguments(params, body)
158
275
  const arrow = ['=>', p2, wrapArrowBody(b2)]
159
- 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
+ }
160
283
  return arrow
161
284
  },
162
285
 
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`.
163
289
  'var'(...args) {
164
- // for-in/for-of: ['var', ['in', 'k', obj]] → ['in', ['let', 'k'], obj]
165
- if (args.length === 1 && Array.isArray(args[0]) && (args[0][0] === 'in' || args[0][0] === 'of')) {
166
- const [, name, src] = args[0]
167
- return [args[0][0], ['let', typeof name === 'string' ? name : transform(name)], transform(src)]
168
- }
169
290
  return ['let', ...args.map(transform)]
170
291
  },
171
292
 
172
293
  '='(lhs, rhs) {
173
- // var assignment: ['=', ['var', name], init] → let
174
- if (Array.isArray(lhs) && lhs[0] === 'var')
175
- return ['let', ['=', lhs[1], transform(rhs)]]
176
294
  // Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
177
295
  if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
178
296
  const targets = []
@@ -237,10 +355,16 @@ const handlers = {
237
355
  // Block body: recurse as scope for hoisting
238
356
  '{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
239
357
 
240
- // 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.
241
362
  'export'(inner) {
363
+ if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
364
+ return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
365
+ }
242
366
  if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
243
- const decl = transform(inner[1])
367
+ const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
244
368
  return [';', decl, ['export', ['default', inner[1][1]]]]
245
369
  }
246
370
  return ['export', transform(inner)]
@@ -311,8 +435,26 @@ export function codegen(node, depth = 0) {
311
435
  // Statements
312
436
  if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
313
437
  if (op === '{}') {
314
- const body = a.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
315
- 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 + '}'
316
458
  }
317
459
 
318
460
  // Declarations
@@ -329,16 +471,20 @@ export function codegen(node, depth = 0) {
329
471
  }
330
472
  if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
331
473
  if (op === 'for') {
332
- if (a.length === 2) { // for...of / for...in
474
+ if (a.length === 2) { // ['for', head, body] — subscript shape
333
475
  const [head, body] = a
334
476
  if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
335
- return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + codegen(body, depth)
477
+ return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
336
478
  // ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
337
479
  if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
338
- return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + codegen(body, depth)
339
- return 'for (' + codegen(head) + ') ' + codegen(body, depth)
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)
340
486
  }
341
- 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)
342
488
  }
343
489
  if (op === 'return') return 'return ' + codegen(a[0])
344
490
  if (op === 'throw') return 'throw ' + codegen(a[0])
@@ -372,10 +518,20 @@ export function codegen(node, depth = 0) {
372
518
  // Property access
373
519
  if (op === '.') return codegen(a[0]) + '.' + a[1]
374
520
  if (op === '?.') return codegen(a[0]) + '?.' + a[1]
375
- if (op === '[]') return codegen(a[0]) + '[' + codegen(a[1]) + ']'
376
-
377
- // Array/object literals
378
- 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
+ }
379
535
  if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
380
536
  if (op === 'str') return JSON.stringify(a[0])
381
537
  if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
@@ -391,9 +547,10 @@ export function codegen(node, depth = 0) {
391
547
  // Spread
392
548
  if (op === '...') return '...' + codegen(a[0])
393
549
 
394
- // Import
550
+ // Import / export rename
395
551
  if (op === 'import') return 'import ' + codegen(a[0])
396
552
  if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
553
+ if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
397
554
 
398
555
  // Unary prefix
399
556
  if (a.length === 1) {
package/src/narrow.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { ctx } from './ctx.js'
11
+ import { isLiteralStr } from './ir.js'
11
12
  import {
12
13
  VAL,
13
14
  analyzeBody, analyzeLocals,
@@ -307,14 +308,41 @@ export default function narrowSignatures(programFacts, ast) {
307
308
  }
308
309
 
309
310
  const poison = field => r => { r[field] = null }
311
+ // Default-aware val inference. Adds two fallbacks beyond inferArgType's
312
+ // body-local `callerValTypes` lookup so a hot recursive helper like
313
+ // `uleb(n, buffer = []) { ... return uleb(n, buffer) }` resolves the
314
+ // recursive `buffer` arg to VAL.ARRAY (via callerParamFacts on iter 2,
315
+ // or via the caller's own default expression on iter 1).
316
+ const inferValAtSite = (arg, state) => {
317
+ const v = inferArgType(arg, state.callerValTypes)
318
+ if (v != null) return v
319
+ if (typeof arg !== 'string') return null
320
+ const fromParam = state.callerParamFacts('val')?.get(arg)
321
+ if (fromParam != null) return fromParam
322
+ const def = state.callerFunc?.defaults?.[arg]
323
+ return def != null ? valTypeOf(def) || null : null
324
+ }
325
+ // Substitute the default expression for a missing positional arg, so
326
+ // `uleb(n)` doesn't poison buffer.val despite `buffer = []` provably
327
+ // yielding VAL.ARRAY at runtime — unblocks inline ARRAY len/push fast
328
+ // paths in encode.js's hot uleb/i32/i64 helpers.
329
+ const defaultArg = (state, k) => {
330
+ const pname = state.func.sig.params[k]?.name
331
+ return pname != null ? state.func.defaults?.[pname] : null
332
+ }
310
333
  const mergeRule = (field, infer) => ({
311
- missing: poison(field),
334
+ missing(r, k, state) {
335
+ if (r[field] === null) return
336
+ const def = defaultArg(state, k)
337
+ if (def != null) mergeParamFact(r, field, infer(def, k, state))
338
+ else r[field] = null
339
+ },
312
340
  apply(r, arg, k, state) {
313
341
  if (r[field] !== null) mergeParamFact(r, field, infer(arg, k, state))
314
342
  },
315
343
  })
316
344
  const runFixpoint = () => runCallsiteLattice([
317
- mergeRule('val', (arg, _k, state) => inferArgType(arg, state.callerValTypes)),
345
+ mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state)),
318
346
  {
319
347
  missing: poison('wasm'),
320
348
  apply(r, arg, _k, state) {
@@ -856,7 +884,6 @@ const TYPED_ARRAY_CTOR = /^(Float|Int|Uint|BigInt|BigUint)(8|16|32|64)(Clamped)?
856
884
  export function refineDynKeys(programFacts) {
857
885
  if (!ctx.types.anyDynKey) return
858
886
  const { paramReps, valueUsed } = programFacts
859
- const isLitStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
860
887
 
861
888
  // Per-function type map: param vtypes from paramReps, plus locals
862
889
  // we can prove are typed arrays from `let v = new TypedArray(...)`. After
@@ -882,6 +909,7 @@ export function refineDynKeys(programFacts) {
882
909
  if (Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string' && init[1].startsWith('new.'))
883
910
  ctor = init[1].slice(4)
884
911
  if (ctor && TYPED_ARRAY_CTOR.test(ctor)) map.set(d[1], VAL.TYPED)
912
+ else if (Array.isArray(init) && init[0] === '[') map.set(d[1], VAL.ARRAY)
885
913
  else if (typeof init === 'string' && map.has(init)) map.set(d[1], map.get(init))
886
914
  }
887
915
  }
@@ -898,7 +926,7 @@ export function refineDynKeys(programFacts) {
898
926
  const op = node[0]
899
927
  if (op === '[]') {
900
928
  const idx = node[2]
901
- if (!isLitStr(idx)) {
929
+ if (!isLiteralStr(idx)) {
902
930
  const obj = node[1]
903
931
  const vt = typeof obj === 'string' ? typeMap.get(obj) : null
904
932
  if (!NON_DYN_VTS.has(vt)) real = true