jz 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/jzify.js CHANGED
@@ -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
@@ -236,8 +272,14 @@ function paramList(params) {
236
272
  return [params]
237
273
  }
238
274
 
275
+ // Destructuring pattern as a parameter — `[a,b]` / `{a,b}` (optionally with a
276
+ // default). Plain `=` defaults and `...rest` are handled natively by emit, so
277
+ // they don't by themselves force lowering.
278
+ const isDestructurePat = p => Array.isArray(p) && (p[0] === '[]' || p[0] === '{}' || (p[0] === '=' && isDestructurePat(p[1])))
279
+
239
280
  function lowerArguments(params, body) {
240
- if (!usesArguments(params) && !usesArguments(body)) return [params, body]
281
+ const paramsNeedLowering = paramList(params).some(isDestructurePat)
282
+ if (!paramsNeedLowering && !usesArguments(params) && !usesArguments(body)) return [params, body]
241
283
  const name = `\uE001arg${argsIdx++}`
242
284
  const decls = []
243
285
  for (const [idx, param] of paramList(params).entries()) {
@@ -252,11 +294,119 @@ function lowerArguments(params, body) {
252
294
  decls.push(['=', param, ['[]', name, [null, idx]]])
253
295
  }
254
296
  const renamed = renameArguments(body, name)
255
- return [['()', ['...', name]], decls.length ? [';', ['let', ...decls], renamed] : renamed]
297
+ return [['()', ['...', name]], decls.length ? prependParamDecls(['let', ...decls], renamed) : renamed]
298
+ }
299
+
300
+ function prependParamDecls(decl, body) {
301
+ if (Array.isArray(body) && body[0] === '{}') {
302
+ const inner = body[1]
303
+ if (Array.isArray(inner) && inner[0] === ';') return ['{}', [';', decl, ...inner.slice(1)]]
304
+ if (inner == null) return ['{}', decl]
305
+ return ['{}', [';', decl, inner]]
306
+ }
307
+ if (Array.isArray(body) && (body[0] === ';' || body[0] === 'return')) return [';', decl, body]
308
+ return ['{}', [';', decl, ['return', body]]]
256
309
  }
257
310
 
258
311
  const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
259
312
 
313
+ // === class lowering ===
314
+ //
315
+ // A class is lowered to a factory arrow. Instance state is a plain object;
316
+ // methods are per-instance arrows capturing it (so `obj.m()` keeps working
317
+ // without a separate `this` argument); `this` is renamed to that object;
318
+ // `new C(a)` is already turned into `C(a)` by the `new` handler.
319
+ //
320
+ // class Point { x = 0; y; constructor(a,b){ this.x = a; this.y = b }
321
+ // dist(){ return Math.hypot(this.x, this.y) } }
322
+ // →
323
+ // let Point = (a, b) => {
324
+ // let selfN = { x: undefined, y: undefined,
325
+ // dist: () => Math.hypot(selfN.x, selfN.y) }
326
+ // selfN.x = 0 // field initializers, in declaration order
327
+ // selfN.x = a // then the constructor body
328
+ // selfN.y = b
329
+ // return selfN
330
+ // }
331
+ //
332
+ // Out of scope for now (rejected with a clear message): `extends`/`super`,
333
+ // `static` members, getters/setters, computed/private-via-`#` member names are
334
+ // kept as the literal key string `#name` (jz allows it).
335
+ let classIdx = 0
336
+
337
+ const classBodyItems = (body) =>
338
+ body == null ? [] : Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body]
339
+
340
+ // Rename `this` → `to`, not crossing into a nested `function`/`class` (those
341
+ // rebind `this`); arrows inherit `this`, so they are crossed. Property *names*
342
+ // (`obj.this`, `{this: …}` value-side only) are left alone.
343
+ function renameThis(node, to) {
344
+ if (node === 'this') return to
345
+ if (!Array.isArray(node)) return node
346
+ if (node[0] === 'function' || node[0] === 'class') return node
347
+ if (node[0] === '.' || node[0] === '?.') return [node[0], renameThis(node[1], to), node[2]]
348
+ if (node[0] === ':') return [node[0], node[1], renameThis(node[2], to)]
349
+ return node.map(n => renameThis(n, to))
350
+ }
351
+
352
+ function jzifyError(msg) { throw new Error(`jzify: ${msg}`) }
353
+
354
+ function lowerClass(name, heritage, body) {
355
+ if (heritage != null) jzifyError('`class … extends …` is not supported yet — flatten the hierarchy or compose explicitly')
356
+ let ctorParams = null, ctorBody = null
357
+ const methods = [], fields = []
358
+ for (const it of classBodyItems(body)) {
359
+ if (typeof it === 'string') { fields.push([it, null]); continue } // bare `x;`
360
+ if (!Array.isArray(it)) continue
361
+ if (it[0] === ':' && Array.isArray(it[2]) && it[2][0] === '=>') {
362
+ if (typeof it[1] !== 'string') jzifyError('computed class member names are not supported')
363
+ if (it[1] === 'constructor') { ctorParams = it[2][1]; ctorBody = it[2][2] }
364
+ else methods.push([it[1], it[2][1], it[2][2]])
365
+ continue
366
+ }
367
+ if (it[0] === '=') {
368
+ const lhs = it[1]
369
+ if (Array.isArray(lhs) && lhs[0] === 'static') jzifyError('`static` class members are not supported yet')
370
+ if (typeof lhs !== 'string') jzifyError('computed/destructured class fields are not supported')
371
+ fields.push([lhs, it[2]])
372
+ continue
373
+ }
374
+ if (it[0] === 'get' || it[0] === 'set') jzifyError('class getters/setters are not supported — jz objects have no accessors')
375
+ if (it[0] === 'static') jzifyError('`static` class members are not supported yet')
376
+ jzifyError(`unsupported class member ${JSON.stringify(it).slice(0, 60)}`)
377
+ }
378
+ const self = `self${classIdx++}`
379
+ const UNDEF = [] // jessie's node for `undefined`
380
+ // A class member body from jessie is a bare statement / `;`-sequence — wrap it
381
+ // in a `{}` block so the `=>` handler treats it as a function body, not an
382
+ // expression (an unwrapped `;`-seq arrow body produces malformed IR).
383
+ const block = b => Array.isArray(b) && b[0] === '{}' ? b : ['{}', b]
384
+ const usesThis = n => n === 'this' || (Array.isArray(n) && n[0] !== 'function' && n[0] !== 'class' && n.some(usesThis))
385
+ // Object literal: every declared field (its initializer inline when it doesn't
386
+ // touch `this`, else `undefined` and assigned below), every method as its
387
+ // self-capturing arrow. Declaring all fields up front fixes the object shape.
388
+ const litProps = [], deferred = []
389
+ for (const [fname, init] of fields) {
390
+ if (init != null && !usesThis(init)) litProps.push([':', fname, transform(init)])
391
+ else { litProps.push([':', fname, UNDEF]); if (init != null) deferred.push([fname, init]) }
392
+ }
393
+ for (const [mname, mparams, mbody] of methods)
394
+ litProps.push([':', mname, transform(['=>', mparams ?? ['()', null], block(renameThis(mbody, self))])])
395
+ const lit = ['{}', litProps.length === 0 ? null : litProps.length === 1 ? litProps[0] : [',', ...litProps]]
396
+ const stmts = [['let', ['=', self, lit]]]
397
+ // `this`-dependent field initializers run, in declaration order, before the ctor.
398
+ for (const [fname, init] of deferred)
399
+ stmts.push(['=', ['.', self, fname], transform(renameThis(init, self))])
400
+ if (ctorBody != null) {
401
+ let cb = transform(renameThis(ctorBody, self))
402
+ if (Array.isArray(cb) && cb[0] === '{}') cb = cb[1]
403
+ if (Array.isArray(cb) && cb[0] === ';') stmts.push(...cb.slice(1).filter(s => s != null))
404
+ else if (cb != null) stmts.push(cb)
405
+ }
406
+ stmts.push(['return', self])
407
+ return ['=>', arrowParams(ctorParams ?? ['()', null]), ['{}', [';', ...stmts]]]
408
+ }
409
+
260
410
  const handlers = {
261
411
  // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
262
412
  '()'(callee, ...rest) {
@@ -283,6 +433,16 @@ const handlers = {
283
433
  return arrow
284
434
  },
285
435
 
436
+ '=>'(params, body) {
437
+ const [p2, b2] = lowerArguments(params, body)
438
+ return ['=>', p2, transform(b2)]
439
+ },
440
+
441
+ // Class in expression position → its factory arrow. (A named class
442
+ // expression's own inner binding is dropped — rare; statement-form
443
+ // `class C {}` is handled by transformScope, which keeps the binding.)
444
+ 'class'(name, heritage, body) { return lowerClass(name, heritage, body) },
445
+
286
446
  // `var` is hoisted away before transform reaches here. If one slips through
287
447
  // (e.g. raw subscript output without going via jzify entry/wrapArrowBody),
288
448
  // fall back to treating it as `let`.
@@ -326,10 +486,15 @@ const handlers = {
326
486
 
327
487
  // new → call (keep TypedArrays)
328
488
  'new'(ctor, ...cargs) {
489
+ if (Array.isArray(ctor) && ctor[0] === '()' && Array.isArray(ctor[1]) && ctor[1][0] === '.') {
490
+ return ['()', ['.', transform(['new', ctor[1][1]]), ctor[1][2]], ...ctor.slice(2).map(transform)]
491
+ }
329
492
  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)]
493
+ if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array')) return ['new', transform(ctor), ...cargs.map(transform)]
331
494
  if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
332
- return ['()', transform(ctor), ...cargs.map(transform)]
495
+ // `new C(a)` → `C(a)`; `new C` (no parens) → `C()` — a 2-element `['()', X]`
496
+ // is grouping parens, so a no-arg call needs the explicit `null` arg slot.
497
+ return ['()', transform(ctor), ...(cargs.length ? cargs.map(transform) : [null])]
333
498
  },
334
499
 
335
500
  // instanceof → typeof / Array.isArray (jzify allows what strict mode prohibits)
@@ -363,10 +528,17 @@ const handlers = {
363
528
  if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
364
529
  return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
365
530
  }
531
+ // `export class C {}` → `export let C = factory`; named class keeps its binding.
532
+ if (Array.isArray(inner) && inner[0] === 'class' && inner[1]) {
533
+ return ['export', ['let', ['=', inner[1], lowerClass(inner[1], inner[2], inner[3])]]]
534
+ }
366
535
  if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
367
536
  const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
368
537
  return [';', decl, ['export', ['default', inner[1][1]]]]
369
538
  }
539
+ if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'class' && inner[1][1]) {
540
+ return [';', ['let', ['=', inner[1][1], lowerClass(inner[1][1], inner[1][2], inner[1][3])]], ['export', ['default', inner[1][1]]]]
541
+ }
370
542
  return ['export', transform(inner)]
371
543
  },
372
544
  }
@@ -380,6 +552,133 @@ function transform(node) {
380
552
  return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
381
553
  }
382
554
 
555
+ // Esbuild emits a small ESM helper:
556
+ //
557
+ // var __defProp = Object.defineProperty;
558
+ // var __export = (target, all) => {
559
+ // for (var name in all)
560
+ // __defProp(target, name, { get: all[name], enumerable: true });
561
+ // };
562
+ // __export(src_exports, { default: () => value });
563
+ // use(src_exports.default);
564
+ //
565
+ // Full descriptor/prototype semantics are outside JZ's fixed-shape object model.
566
+ // This pass instead recognizes the static helper pattern and rewrites reads of
567
+ // the synthetic export object to the real binding.
568
+ function foldStaticExportHelpers(ast) {
569
+ const body = astSeq(ast)
570
+ if (!body) return ast
571
+
572
+ const defPropAliases = new Set()
573
+ for (const stmt of body) {
574
+ if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' && isObjectDefineProperty(stmt[2]))
575
+ defPropAliases.add(stmt[1])
576
+ }
577
+ if (!defPropAliases.size) return ast
578
+
579
+ const helperNames = new Set()
580
+ for (const stmt of body) {
581
+ if (Array.isArray(stmt) && stmt[0] === '=' && typeof stmt[1] === 'string' &&
582
+ Array.isArray(stmt[2]) && stmt[2][0] === '=>' && containsDefinePropertyCall(stmt[2], defPropAliases))
583
+ helperNames.add(stmt[1])
584
+ }
585
+ if (!helperNames.size) return ast
586
+
587
+ const rewrites = new Map()
588
+ const removable = new Set()
589
+ for (const stmt of body) {
590
+ const ex = staticExportCall(stmt, helperNames)
591
+ if (!ex) continue
592
+ for (const [key, value] of ex.props) rewrites.set(`${ex.target}.${key}`, value)
593
+ removable.add(stmt)
594
+ }
595
+ if (!rewrites.size) return ast
596
+
597
+ const rewritten = body
598
+ .filter(stmt => !removable.has(stmt) && !isDefPropAliasAssign(stmt, defPropAliases) && !isExportHelperAssign(stmt, helperNames))
599
+ .map(stmt => replaceStaticExportReads(stmt, rewrites))
600
+ return rewritten.length === 0 ? null : rewritten.length === 1 ? rewritten[0] : [';', ...rewritten]
601
+ }
602
+
603
+ function astSeq(ast) {
604
+ if (!Array.isArray(ast)) return null
605
+ return ast[0] === ';' ? ast.slice(1).filter(Boolean) : [ast]
606
+ }
607
+
608
+ function isObjectDefineProperty(node) {
609
+ return Array.isArray(node) && node[0] === '.' && node[1] === 'Object' && node[2] === 'defineProperty'
610
+ }
611
+
612
+ function isDefPropAliasAssign(stmt, aliases) {
613
+ return Array.isArray(stmt) && stmt[0] === '=' && aliases.has(stmt[1]) && isObjectDefineProperty(stmt[2])
614
+ }
615
+
616
+ function isExportHelperAssign(stmt, helpers) {
617
+ return Array.isArray(stmt) && stmt[0] === '=' && helpers.has(stmt[1])
618
+ }
619
+
620
+ function containsDefinePropertyCall(node, aliases) {
621
+ if (!Array.isArray(node)) return false
622
+ if (node[0] === '()' && (aliases.has(node[1]) || isObjectDefineProperty(node[1]))) return true
623
+ for (let i = 1; i < node.length; i++) if (containsDefinePropertyCall(node[i], aliases)) return true
624
+ return false
625
+ }
626
+
627
+ function staticExportCall(stmt, helpers) {
628
+ if (!Array.isArray(stmt) || stmt[0] !== '()' || !helpers.has(stmt[1])) return null
629
+ const args = callArgs(stmt.slice(2))
630
+ if (args.length !== 2 || typeof args[0] !== 'string') return null
631
+ const props = objectProps(args[1])
632
+ if (!props) return null
633
+ const out = []
634
+ for (const prop of props) {
635
+ if (!Array.isArray(prop) || prop[0] !== ':' || typeof prop[1] !== 'string') return null
636
+ const value = getterReturnExpr(prop[2])
637
+ if (!value) return null
638
+ out.push([prop[1], value])
639
+ }
640
+ return { target: args[0], props: out }
641
+ }
642
+
643
+ function callArgs(args) {
644
+ if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') return args[0].slice(1)
645
+ return args.filter(a => a != null)
646
+ }
647
+
648
+ function objectProps(node) {
649
+ if (!Array.isArray(node) || node[0] !== '{}') return null
650
+ const body = node[1]
651
+ if (body == null) return []
652
+ if (Array.isArray(body) && body[0] === ',') return body.slice(1)
653
+ return [body]
654
+ }
655
+
656
+ function getterReturnExpr(node) {
657
+ if (!Array.isArray(node) || node[0] !== '=>') return null
658
+ const params = paramList(node[1])
659
+ if (params.length !== 0) return null
660
+ const body = node[2]
661
+ if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === 'return') return body[1][1]
662
+ if (Array.isArray(body) && body[0] === 'return') return body[1]
663
+ return body
664
+ }
665
+
666
+ function replaceStaticExportReads(node, rewrites) {
667
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
668
+ if ((node[0] === '.' || node[0] === '?.') && typeof node[1] === 'string' && typeof node[2] === 'string') {
669
+ const value = rewrites.get(`${node[1]}.${node[2]}`)
670
+ if (value) return cloneAst(value)
671
+ }
672
+ if (node[0] === ':') return [node[0], node[1], replaceStaticExportReads(node[2], rewrites)]
673
+ return node.map((part, i) => i === 0 ? part : replaceStaticExportReads(part, rewrites))
674
+ }
675
+
676
+ function cloneAst(node) {
677
+ if (node == null || typeof node !== 'object') return node
678
+ if (!Array.isArray(node)) return node
679
+ return node.map(cloneAst)
680
+ }
681
+
383
682
  /** Transform switch statement to if/else chain. */
384
683
  let swIdx = 0
385
684
  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)