jz 0.2.1 → 0.3.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
@@ -26,11 +26,12 @@ export default function jzify(ast) {
26
26
  swIdx = 0
27
27
  argsIdx = 0
28
28
  doIdx = 0
29
+ classIdx = 0
29
30
  // Hoist module-level vars: any `var x` inside nested blocks bubbles up.
30
31
  const names = new Set()
31
32
  ast = hoistVars(ast, names)
32
33
  if (names.size) ast = prependDecls(ast, names)
33
- return transformScope(ast)
34
+ return foldStaticExportHelpers(transformScope(ast))
34
35
  }
35
36
 
36
37
  /**
@@ -70,12 +71,20 @@ function hoistVars(node, names) {
70
71
  }
71
72
  return [op, lhs, hoistVars(node[2], names)]
72
73
  }
74
+ if (op === '=' && Array.isArray(node[1]) && node[1][0] === 'var' && typeof node[1][1] === 'string' && node[1].length === 2) {
75
+ names.add(node[1][1])
76
+ return ['=', node[1][1], hoistVars(node[2], names)]
77
+ }
73
78
  // For-head `;` is positional (init; cond; update), not a statement sequence.
74
79
  // Recurse into each slot but never filter nulls — empty slots are valid.
75
80
  if (op === 'for') {
76
81
  const head = node[1]
77
82
  let h2
78
- if (Array.isArray(head) && head[0] === ';') {
83
+ if (Array.isArray(head) && head[0] === 'var' && Array.isArray(head[1]) &&
84
+ (head[1][0] === 'in' || head[1][0] === 'of') && typeof head[1][1] === 'string') {
85
+ names.add(head[1][1])
86
+ h2 = [head[1][0], head[1][1], hoistVars(head[1][2], names)]
87
+ } else if (Array.isArray(head) && head[0] === ';') {
79
88
  h2 = [';']
80
89
  for (let i = 1; i < head.length; i++) h2.push(hoistVars(head[i], names))
81
90
  } else {
@@ -144,16 +153,43 @@ function transformScope(node) {
144
153
 
145
154
  // Single named function-statement at scope position: hoist as const arrow
146
155
  if (op === 'function' && args[0]) return hoistFnDecl(...args)
156
+ // Single statement-form class declaration: bind the factory (no hoisting — classes are TDZ)
157
+ if (op === 'class' && args[0]) return ['let', ['=', args[0], lowerClass(...args)]]
147
158
 
148
159
  // Statement sequence: collect hoisted functions
149
160
  if (op === ';') {
150
161
  const hoisted = [], rest = []
151
- for (const stmt of args) {
162
+ for (let i = 0; i < args.length; i++) {
163
+ const stmt = args[i]
164
+ // Workaround for subscript parser ASI bug: multiline named IIFE
165
+ // `(function name(){...})();` is parsed as two statements when there are
166
+ // newlines inside the function body. Reconstruct the single-statement IIFE
167
+ // so the () handler can desugar it correctly.
168
+ if (Array.isArray(stmt) && stmt[0] === '()' &&
169
+ Array.isArray(stmt[1]) && stmt[1][0] === 'function' && stmt[1][1] &&
170
+ i + 1 < args.length && Array.isArray(args[i + 1]) && args[i + 1][0] === '()') {
171
+ const merged = ['()', ['()', stmt[1]], args[i + 1][1] ?? null]
172
+ const t = transform(merged)
173
+ if (t != null) {
174
+ if (Array.isArray(t) && t[0] === ';') {
175
+ for (const s of t.slice(1)) { if (s != null) rest.push(s) }
176
+ } else {
177
+ rest.push(t)
178
+ }
179
+ }
180
+ i++
181
+ continue
182
+ }
152
183
  // Statement-form named function declaration: hoist directly (skip expression handler)
153
184
  if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
154
185
  hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
155
186
  continue
156
187
  }
188
+ // Statement-form class declaration: bind the factory in place (not hoisted — TDZ)
189
+ if (Array.isArray(stmt) && stmt[0] === 'class' && stmt[1]) {
190
+ rest.push(['let', ['=', stmt[1], lowerClass(stmt[1], stmt[2], stmt[3])]])
191
+ continue
192
+ }
157
193
  const t = transform(stmt)
158
194
  if (t == null) continue
159
195
  // Hoist function declarations to top of scope
@@ -174,13 +210,35 @@ function transformScope(node) {
174
210
  // Hoist functions AFTER imports (imports must be processed first for scope resolution)
175
211
  const imports = rest.filter(s => Array.isArray(s) && s[0] === 'import')
176
212
  const nonImports = rest.filter(s => !(Array.isArray(s) && s[0] === 'import'))
177
- const all = [...imports, ...hoisted, ...nonImports]
213
+ const all = dedupeRedecls([...imports, ...hoisted, ...nonImports])
178
214
  return all.length === 0 ? null : all.length === 1 ? all[0] : [';', ...all]
179
215
  }
180
216
 
181
217
  return transform(node)
182
218
  }
183
219
 
220
+ /**
221
+ * Drop redundant re-declarations of the same name within one scope's statement
222
+ * list. JS allows `function f(){} var f;`, `var x; var x;`, `var x = 1; var x;` —
223
+ * jzify lowers `function`→`const` and `var`→`let`, which would otherwise emit two
224
+ * bindings for one slot (and a typed-slot clash in codegen). The first declaration
225
+ * wins; a later redeclaration keeps only its initializer, as a plain assignment.
226
+ */
227
+ function dedupeRedecls(stmts) {
228
+ const nameOf = s => Array.isArray(s) && (s[0] === 'let' || s[0] === 'const' || s[0] === 'var')
229
+ ? (typeof s[1] === 'string' ? s[1]
230
+ : Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' ? s[1][1] : null)
231
+ : null
232
+ const seen = new Set(), out = []
233
+ for (const s of stmts) {
234
+ const n = nameOf(s)
235
+ if (n == null) { out.push(s); continue }
236
+ if (seen.has(n)) { if (Array.isArray(s[1]) && s[1][0] === '=') out.push(['=', s[1][1], s[1][2]]); continue }
237
+ seen.add(n); out.push(s)
238
+ }
239
+ return out
240
+ }
241
+
184
242
  /** Wrap function body for arrow conversion */
185
243
  function wrapArrowBody(body) {
186
244
  const t = transformScope(body)
@@ -211,6 +269,19 @@ function usesArguments(node) {
211
269
  return false
212
270
  }
213
271
 
272
+ // `arguments` is the implicit object only if the function body doesn't declare a
273
+ // local of that name. Scan the body's own statement list (not nested scopes) for
274
+ // `var/let/const arguments` — a regular `function` with `var arguments;` just has
275
+ // an ordinary local, no arguments object.
276
+ function bindsArguments(body) {
277
+ const isArgDecl = s => Array.isArray(s) && (s[0] === 'var' || s[0] === 'let' || s[0] === 'const') &&
278
+ s.slice(1).some(d => d === 'arguments' || (Array.isArray(d) && d[0] === '=' && d[1] === 'arguments'))
279
+ let n = body
280
+ if (Array.isArray(n) && n[0] === '{}') n = n[1]
281
+ if (Array.isArray(n) && n[0] === ';') return n.slice(1).some(isArgDecl)
282
+ return isArgDecl(n)
283
+ }
284
+
214
285
  function renameArguments(node, to) {
215
286
  if (node === 'arguments') return to
216
287
  if (!Array.isArray(node)) return node
@@ -236,8 +307,19 @@ function paramList(params) {
236
307
  return [params]
237
308
  }
238
309
 
310
+ // Destructuring pattern as a parameter — `[a,b]` / `{a,b}` (optionally with a
311
+ // default). Plain `=` defaults and `...rest` are handled natively by emit, so
312
+ // they don't by themselves force lowering.
313
+ const isDestructurePat = p => Array.isArray(p) && (p[0] === '[]' || p[0] === '{}' || (p[0] === '=' && isDestructurePat(p[1])))
314
+
239
315
  function lowerArguments(params, body) {
240
- if (!usesArguments(params) && !usesArguments(body)) return [params, body]
316
+ // A function body that declares its own `arguments` local: it's an ordinary
317
+ // variable, not the implicit object \u2014 rename it out of jz's reserved set,
318
+ // no rest param synthesized.
319
+ if (bindsArguments(body)) body = renameArguments(body, `\uE001arg${argsIdx++}`)
320
+ const paramsNeedLowering = paramList(params).some(isDestructurePat)
321
+ const usesArgsObj = usesArguments(params) || usesArguments(body)
322
+ if (!paramsNeedLowering && !usesArgsObj) return [params, body]
241
323
  const name = `\uE001arg${argsIdx++}`
242
324
  const decls = []
243
325
  for (const [idx, param] of paramList(params).entries()) {
@@ -251,12 +333,120 @@ function lowerArguments(params, body) {
251
333
  }
252
334
  decls.push(['=', param, ['[]', name, [null, idx]]])
253
335
  }
254
- const renamed = renameArguments(body, name)
255
- return [['()', ['...', name]], decls.length ? [';', ['let', ...decls], renamed] : renamed]
336
+ const renamed = usesArgsObj ? renameArguments(body, name) : body
337
+ return [['()', ['...', name]], decls.length ? prependParamDecls(['let', ...decls], renamed) : renamed]
338
+ }
339
+
340
+ function prependParamDecls(decl, body) {
341
+ if (Array.isArray(body) && body[0] === '{}') {
342
+ const inner = body[1]
343
+ if (Array.isArray(inner) && inner[0] === ';') return ['{}', [';', decl, ...inner.slice(1)]]
344
+ if (inner == null) return ['{}', decl]
345
+ return ['{}', [';', decl, inner]]
346
+ }
347
+ if (Array.isArray(body) && (body[0] === ';' || body[0] === 'return')) return [';', decl, body]
348
+ return ['{}', [';', decl, ['return', body]]]
256
349
  }
257
350
 
258
351
  const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
259
352
 
353
+ // === class lowering ===
354
+ //
355
+ // A class is lowered to a factory arrow. Instance state is a plain object;
356
+ // methods are per-instance arrows capturing it (so `obj.m()` keeps working
357
+ // without a separate `this` argument); `this` is renamed to that object;
358
+ // `new C(a)` is already turned into `C(a)` by the `new` handler.
359
+ //
360
+ // class Point { x = 0; y; constructor(a,b){ this.x = a; this.y = b }
361
+ // dist(){ return Math.hypot(this.x, this.y) } }
362
+ // →
363
+ // let Point = (a, b) => {
364
+ // let selfN = { x: undefined, y: undefined,
365
+ // dist: () => Math.hypot(selfN.x, selfN.y) }
366
+ // selfN.x = 0 // field initializers, in declaration order
367
+ // selfN.x = a // then the constructor body
368
+ // selfN.y = b
369
+ // return selfN
370
+ // }
371
+ //
372
+ // Out of scope for now (rejected with a clear message): `extends`/`super`,
373
+ // `static` members, getters/setters, computed/private-via-`#` member names are
374
+ // kept as the literal key string `#name` (jz allows it).
375
+ let classIdx = 0
376
+
377
+ const classBodyItems = (body) =>
378
+ body == null ? [] : Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body]
379
+
380
+ // Rename `this` → `to`, not crossing into a nested `function`/`class` (those
381
+ // rebind `this`); arrows inherit `this`, so they are crossed. Property *names*
382
+ // (`obj.this`, `{this: …}` value-side only) are left alone.
383
+ function renameThis(node, to) {
384
+ if (node === 'this') return to
385
+ if (!Array.isArray(node)) return node
386
+ if (node[0] === 'function' || node[0] === 'class') return node
387
+ if (node[0] === '.' || node[0] === '?.') return [node[0], renameThis(node[1], to), node[2]]
388
+ if (node[0] === ':') return [node[0], node[1], renameThis(node[2], to)]
389
+ return node.map(n => renameThis(n, to))
390
+ }
391
+
392
+ function jzifyError(msg) { throw new Error(`jzify: ${msg}`) }
393
+
394
+ function lowerClass(name, heritage, body) {
395
+ if (heritage != null) jzifyError('`class … extends …` is not supported yet — flatten the hierarchy or compose explicitly')
396
+ let ctorParams = null, ctorBody = null
397
+ const methods = [], fields = []
398
+ for (const it of classBodyItems(body)) {
399
+ if (typeof it === 'string') { fields.push([it, null]); continue } // bare `x;`
400
+ if (!Array.isArray(it)) continue
401
+ if (it[0] === ':' && Array.isArray(it[2]) && it[2][0] === '=>') {
402
+ if (typeof it[1] !== 'string') jzifyError('computed class member names are not supported')
403
+ if (it[1] === 'constructor') { ctorParams = it[2][1]; ctorBody = it[2][2] }
404
+ else methods.push([it[1], it[2][1], it[2][2]])
405
+ continue
406
+ }
407
+ if (it[0] === '=') {
408
+ const lhs = it[1]
409
+ if (Array.isArray(lhs) && lhs[0] === 'static') jzifyError('`static` class members are not supported yet')
410
+ if (typeof lhs !== 'string') jzifyError('computed/destructured class fields are not supported')
411
+ fields.push([lhs, it[2]])
412
+ continue
413
+ }
414
+ if (it[0] === 'get' || it[0] === 'set') jzifyError('class getters/setters are not supported — jz objects have no accessors')
415
+ if (it[0] === 'static') jzifyError('`static` class members are not supported yet')
416
+ jzifyError(`unsupported class member ${JSON.stringify(it).slice(0, 60)}`)
417
+ }
418
+ const self = `self${classIdx++}`
419
+ const UNDEF = [] // jessie's node for `undefined`
420
+ // A class member body from jessie is a bare statement / `;`-sequence — wrap it
421
+ // in a `{}` block so the `=>` handler treats it as a function body, not an
422
+ // expression (an unwrapped `;`-seq arrow body produces malformed IR).
423
+ const block = b => Array.isArray(b) && b[0] === '{}' ? b : ['{}', b]
424
+ const usesThis = n => n === 'this' || (Array.isArray(n) && n[0] !== 'function' && n[0] !== 'class' && n.some(usesThis))
425
+ // Object literal: every declared field (its initializer inline when it doesn't
426
+ // touch `this`, else `undefined` and assigned below), every method as its
427
+ // self-capturing arrow. Declaring all fields up front fixes the object shape.
428
+ const litProps = [], deferred = []
429
+ for (const [fname, init] of fields) {
430
+ if (init != null && !usesThis(init)) litProps.push([':', fname, transform(init)])
431
+ else { litProps.push([':', fname, UNDEF]); if (init != null) deferred.push([fname, init]) }
432
+ }
433
+ for (const [mname, mparams, mbody] of methods)
434
+ litProps.push([':', mname, transform(['=>', mparams ?? ['()', null], block(renameThis(mbody, self))])])
435
+ const lit = ['{}', litProps.length === 0 ? null : litProps.length === 1 ? litProps[0] : [',', ...litProps]]
436
+ const stmts = [['let', ['=', self, lit]]]
437
+ // `this`-dependent field initializers run, in declaration order, before the ctor.
438
+ for (const [fname, init] of deferred)
439
+ stmts.push(['=', ['.', self, fname], transform(renameThis(init, self))])
440
+ if (ctorBody != null) {
441
+ let cb = transform(renameThis(ctorBody, self))
442
+ if (Array.isArray(cb) && cb[0] === '{}') cb = cb[1]
443
+ if (Array.isArray(cb) && cb[0] === ';') stmts.push(...cb.slice(1).filter(s => s != null))
444
+ else if (cb != null) stmts.push(cb)
445
+ }
446
+ stmts.push(['return', self])
447
+ return ['=>', arrowParams(ctorParams ?? ['()', null]), ['{}', [';', ...stmts]]]
448
+ }
449
+
260
450
  const handlers = {
261
451
  // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
262
452
  '()'(callee, ...rest) {
@@ -283,6 +473,16 @@ const handlers = {
283
473
  return arrow
284
474
  },
285
475
 
476
+ '=>'(params, body) {
477
+ const [p2, b2] = lowerArguments(params, body)
478
+ return ['=>', p2, transform(b2)]
479
+ },
480
+
481
+ // Class in expression position → its factory arrow. (A named class
482
+ // expression's own inner binding is dropped — rare; statement-form
483
+ // `class C {}` is handled by transformScope, which keeps the binding.)
484
+ 'class'(name, heritage, body) { return lowerClass(name, heritage, body) },
485
+
286
486
  // `var` is hoisted away before transform reaches here. If one slips through
287
487
  // (e.g. raw subscript output without going via jzify entry/wrapArrowBody),
288
488
  // fall back to treating it as `let`.
@@ -326,10 +526,15 @@ const handlers = {
326
526
 
327
527
  // new → call (keep TypedArrays)
328
528
  'new'(ctor, ...cargs) {
529
+ if (Array.isArray(ctor) && ctor[0] === '()' && Array.isArray(ctor[1]) && ctor[1][0] === '.') {
530
+ return ['()', ['.', transform(['new', ctor[1][1]]), ctor[1][2]], ...ctor.slice(2).map(transform)]
531
+ }
329
532
  const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
330
- if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['new', transform(ctor), ...cargs.map(transform)]
533
+ if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array')) return ['new', transform(ctor), ...cargs.map(transform)]
331
534
  if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
332
- return ['()', transform(ctor), ...cargs.map(transform)]
535
+ // `new C(a)` → `C(a)`; `new C` (no parens) → `C()` — a 2-element `['()', X]`
536
+ // is grouping parens, so a no-arg call needs the explicit `null` arg slot.
537
+ return ['()', transform(ctor), ...(cargs.length ? cargs.map(transform) : [null])]
333
538
  },
334
539
 
335
540
  // instanceof → typeof / Array.isArray (jzify allows what strict mode prohibits)
@@ -363,10 +568,17 @@ const handlers = {
363
568
  if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
364
569
  return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
365
570
  }
571
+ // `export class C {}` → `export let C = factory`; named class keeps its binding.
572
+ if (Array.isArray(inner) && inner[0] === 'class' && inner[1]) {
573
+ return ['export', ['let', ['=', inner[1], lowerClass(inner[1], inner[2], inner[3])]]]
574
+ }
366
575
  if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
367
576
  const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
368
577
  return [';', decl, ['export', ['default', inner[1][1]]]]
369
578
  }
579
+ if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'class' && inner[1][1]) {
580
+ return [';', ['let', ['=', inner[1][1], lowerClass(inner[1][1], inner[1][2], inner[1][3])]], ['export', ['default', inner[1][1]]]]
581
+ }
370
582
  return ['export', transform(inner)]
371
583
  },
372
584
  }
@@ -380,6 +592,133 @@ function transform(node) {
380
592
  return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
381
593
  }
382
594
 
595
+ // Esbuild emits a small ESM helper:
596
+ //
597
+ // var __defProp = Object.defineProperty;
598
+ // var __export = (target, all) => {
599
+ // for (var name in all)
600
+ // __defProp(target, name, { get: all[name], enumerable: true });
601
+ // };
602
+ // __export(src_exports, { default: () => value });
603
+ // use(src_exports.default);
604
+ //
605
+ // Full descriptor/prototype semantics are outside JZ's fixed-shape object model.
606
+ // This pass instead recognizes the static helper pattern and rewrites reads of
607
+ // the synthetic export object to the real binding.
608
+ function foldStaticExportHelpers(ast) {
609
+ const body = astSeq(ast)
610
+ if (!body) return ast
611
+
612
+ const defPropAliases = new Set()
613
+ for (const stmt of body) {
614
+ if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' && isObjectDefineProperty(stmt[2]))
615
+ defPropAliases.add(stmt[1])
616
+ }
617
+ if (!defPropAliases.size) return ast
618
+
619
+ const helperNames = new Set()
620
+ for (const stmt of body) {
621
+ if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' &&
622
+ Array.isArray(stmt[2]) && stmt[2][0] === '=>' && containsDefinePropertyCall(stmt[2], defPropAliases))
623
+ helperNames.add(stmt[1])
624
+ }
625
+ if (!helperNames.size) return ast
626
+
627
+ const rewrites = new Map()
628
+ const removable = new Set()
629
+ for (const stmt of body) {
630
+ const ex = staticExportCall(stmt, helperNames)
631
+ if (!ex) continue
632
+ for (const [key, value] of ex.props) rewrites.set(`${ex.target}.${key}`, value)
633
+ removable.add(stmt)
634
+ }
635
+ if (!rewrites.size) return ast
636
+
637
+ const rewritten = body
638
+ .filter(stmt => !removable.has(stmt) && !isDefPropAliasAssign(stmt, defPropAliases) && !isExportHelperAssign(stmt, helperNames))
639
+ .map(stmt => replaceStaticExportReads(stmt, rewrites))
640
+ return rewritten.length === 0 ? null : rewritten.length === 1 ? rewritten[0] : [';', ...rewritten]
641
+ }
642
+
643
+ function astSeq(ast) {
644
+ if (!Array.isArray(ast)) return null
645
+ return ast[0] === ';' ? ast.slice(1).filter(Boolean) : [ast]
646
+ }
647
+
648
+ function isObjectDefineProperty(node) {
649
+ return Array.isArray(node) && node[0] === '.' && node[1] === 'Object' && node[2] === 'defineProperty'
650
+ }
651
+
652
+ function isDefPropAliasAssign(stmt, aliases) {
653
+ return Array.isArray(stmt) && stmt[0] === '=' && aliases.has(stmt[1]) && isObjectDefineProperty(stmt[2])
654
+ }
655
+
656
+ function isExportHelperAssign(stmt, helpers) {
657
+ return Array.isArray(stmt) && stmt[0] === '=' && helpers.has(stmt[1])
658
+ }
659
+
660
+ function containsDefinePropertyCall(node, aliases) {
661
+ if (!Array.isArray(node)) return false
662
+ if (node[0] === '()' && (aliases.has(node[1]) || isObjectDefineProperty(node[1]))) return true
663
+ for (let i = 1; i < node.length; i++) if (containsDefinePropertyCall(node[i], aliases)) return true
664
+ return false
665
+ }
666
+
667
+ function staticExportCall(stmt, helpers) {
668
+ if (!Array.isArray(stmt) || stmt[0] !== '()' || !helpers.has(stmt[1])) return null
669
+ const args = callArgs(stmt.slice(2))
670
+ if (args.length !== 2 || typeof args[0] !== 'string') return null
671
+ const props = objectProps(args[1])
672
+ if (!props) return null
673
+ const out = []
674
+ for (const prop of props) {
675
+ if (!Array.isArray(prop) || prop[0] !== ':' || typeof prop[1] !== 'string') return null
676
+ const value = getterReturnExpr(prop[2])
677
+ if (!value) return null
678
+ out.push([prop[1], value])
679
+ }
680
+ return { target: args[0], props: out }
681
+ }
682
+
683
+ function callArgs(args) {
684
+ if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') return args[0].slice(1)
685
+ return args.filter(a => a != null)
686
+ }
687
+
688
+ function objectProps(node) {
689
+ if (!Array.isArray(node) || node[0] !== '{}') return null
690
+ const body = node[1]
691
+ if (body == null) return []
692
+ if (Array.isArray(body) && body[0] === ',') return body.slice(1)
693
+ return [body]
694
+ }
695
+
696
+ function getterReturnExpr(node) {
697
+ if (!Array.isArray(node) || node[0] !== '=>') return null
698
+ const params = paramList(node[1])
699
+ if (params.length !== 0) return null
700
+ const body = node[2]
701
+ if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === 'return') return body[1][1]
702
+ if (Array.isArray(body) && body[0] === 'return') return body[1]
703
+ return body
704
+ }
705
+
706
+ function replaceStaticExportReads(node, rewrites) {
707
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
708
+ if ((node[0] === '.' || node[0] === '?.') && typeof node[1] === 'string' && typeof node[2] === 'string') {
709
+ const value = rewrites.get(`${node[1]}.${node[2]}`)
710
+ if (value) return cloneAst(value)
711
+ }
712
+ if (node[0] === ':') return [node[0], node[1], replaceStaticExportReads(node[2], rewrites)]
713
+ return node.map((part, i) => i === 0 ? part : replaceStaticExportReads(part, rewrites))
714
+ }
715
+
716
+ function cloneAst(node) {
717
+ if (node == null || typeof node !== 'object') return node
718
+ if (!Array.isArray(node)) return node
719
+ return node.map(cloneAst)
720
+ }
721
+
383
722
  /** Transform switch statement to if/else chain. */
384
723
  let swIdx = 0
385
724
  function transformSwitch(discriminant, cases) {
package/src/narrow.js CHANGED
@@ -389,7 +389,8 @@ export default function narrowSignatures(programFacts, ast) {
389
389
  // Safety:
390
390
  // - exclude ARRAY (forwards on realloc — f64 NaN-box is a stable identity) and
391
391
  // STRING (SSO vs heap dual encoding depends on ptr-type bits we'd drop).
392
- // - exclude CLOSURE/TYPED (aux bits carry schema/element-type, lost with offset).
392
+ // - exclude CLOSURE (aux carries funcIdx, needed for call_indirect) and TYPED
393
+ // (aux carries element-type, handled separately by applyTypedPointerParamAbi).
393
394
  // - exclude params with defaults (nullish sentinel needs the f64 NaN space).
394
395
  // - exclude rest position (array pack/unpack stays f64).
395
396
  applyPointerParamAbi(paramReps, valueUsed)