jz 0.3.1 → 0.5.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/assemble.js CHANGED
@@ -13,9 +13,28 @@
13
13
  */
14
14
 
15
15
  import { parse as parseWat } from 'watr'
16
- import { ctx, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
16
+ import { ctx, inc, resolveIncludes, err, PTR, LAYOUT } from './ctx.js'
17
+
18
+ // Stdlib WAT templates are fixed text (or feature-keyed text from a factory) —
19
+ // `parseWat` of the same string always yields the same tree. Parsing is the
20
+ // dominant cost when a program pulls heavy stdlib (Math pow/sqrt, JSON, regex):
21
+ // it re-tokenizes ~KB of text every compile. Parse once per distinct resolved
22
+ // string, then hand out a deep clone (downstream passes mutate nodes in place).
23
+ // Module-level on purpose: the cache persists across compile() calls.
24
+ const stdlibParseCache = new Map() // resolved WAT string → pristine parsed tree
25
+ const cloneTemplate = (node) => {
26
+ if (!Array.isArray(node)) return node
27
+ const copy = node.map(cloneTemplate)
28
+ if (node.loc != null) copy.loc = node.loc
29
+ return copy
30
+ }
31
+ const parseTemplate = (str) => {
32
+ let tmpl = stdlibParseCache.get(str)
33
+ if (tmpl === undefined) stdlibParseCache.set(str, tmpl = parseWat(str))
34
+ return cloneTemplate(tmpl)
35
+ }
17
36
  import { T, VAL, analyzeValTypes } from './analyze.js'
18
- import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
37
+ import { optimizeFunc, collectVolatileGlobals, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
19
38
  import { emit } from './emit.js'
20
39
  import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from './ir.js'
21
40
 
@@ -27,16 +46,20 @@ const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
27
46
  const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
28
47
  const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
29
48
 
30
- const heapGetIR = () => ctx.memory.shared
49
+ // memory[1020] holds the heap pointer only for shared memory (wasm globals are
50
+ // per-instance — see module/core.js comment). Non-shared memory uses $__heap.
51
+ const heapUsesMem = () => ctx.memory.shared
52
+
53
+ const heapGetIR = () => heapUsesMem()
31
54
  ? ['i32.load', ['i32.const', 1020]]
32
55
  : ['global.get', '$__heap']
33
56
 
34
- const heapSetIR = value => ctx.memory.shared
57
+ const heapSetIR = value => heapUsesMem()
35
58
  ? ['i32.store', ['i32.const', 1020], value]
36
59
  : ['global.set', '$__heap', value]
37
60
 
38
61
  const ARENA_SAFE_CALLS = new Set([
39
- '$__alloc', '$__alloc_hdr', '$__mkptr',
62
+ '$__alloc', '$__alloc_hdr', '$__alloc_hdr_n', '$__mkptr',
40
63
  '$__ptr_offset', '$__ptr_type', '$__ptr_aux',
41
64
  '$__len', '$__cap', '$__typed_shift', '$__typed_data',
42
65
  ])
@@ -60,7 +83,7 @@ function applyArenaRewind(func, fn, safeCallees) {
60
83
  }
61
84
  if (op === 'call') {
62
85
  const name = node[1]
63
- if (name === '$__alloc' || name === '$__alloc_hdr') hasAlloc = true
86
+ if (name === '$__alloc' || name === '$__alloc_hdr' || name === '$__alloc_hdr_n') hasAlloc = true
64
87
  if (!(safeCallees ?? ARENA_SAFE_CALLS).has(name)) {
65
88
  unsafe = true
66
89
  return
@@ -109,7 +132,7 @@ function applyArenaRewind(func, fn, safeCallees) {
109
132
 
110
133
  export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
111
134
  ctx.func.locals = new Map()
112
- ctx.func.repByLocal = null
135
+ ctx.func.localReps = null
113
136
  ctx.func.boxed = new Map()
114
137
  ctx.func.stack = []
115
138
  ctx.func.current = { params: [], results: [] }
@@ -133,7 +156,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
133
156
  const beforeLateClosures = closureFuncs.length
134
157
  const startCtx = {
135
158
  locals: ctx.func.locals,
136
- repByLocal: ctx.func.repByLocal,
159
+ localReps: ctx.func.localReps,
137
160
  boxed: ctx.func.boxed,
138
161
  stack: ctx.func.stack,
139
162
  current: ctx.func.current,
@@ -154,7 +177,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
154
177
  for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
155
178
  inc('__alloc_hdr', '__mkptr')
156
179
  boxInit.push(
157
- ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
180
+ ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
158
181
  ['f64.store', ['local.get', `$${bt}`],
159
182
  ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
160
183
  ...schema.slice(1).map((_, i) =>
@@ -165,17 +188,24 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
165
188
 
166
189
  const schemaInit = []
167
190
  const hasJpObj = ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
168
- const needsSchemaTbl = (ctx.schema.list.length && (
169
- ctx.core.includes.has('__stringify') ||
191
+ const hasStringify = ctx.core.includes.has('__stringify')
192
+ // Empty object literals register a `[]` schema so their schemaId indexes a
193
+ // valid list entry. But __dyn_get already guards `$__schema_tbl == 0`, so a
194
+ // table holding only empty schemas is pure dead weight there. __json_obj has
195
+ // no such guard — it must read the table whenever stringify is in play.
196
+ const tblConsumed = hasStringify ||
170
197
  ctx.core.includes.has('__dyn_get') ||
171
198
  ctx.core.includes.has('__dyn_get_t') ||
172
199
  ctx.core.includes.has('__dyn_get_t_h') ||
173
200
  ctx.core.includes.has('__dyn_get_expr_t_h') ||
174
201
  ctx.core.includes.has('__dyn_get_any') ||
175
202
  ctx.core.includes.has('__dyn_get_any_t') ||
203
+ ctx.core.includes.has('__dyn_get_any_t_h') ||
176
204
  ctx.core.includes.has('__dyn_get_expr') ||
177
205
  ctx.core.includes.has('__dyn_get_expr_t') ||
178
- ctx.core.includes.has('__dyn_get_or'))) ||
206
+ ctx.core.includes.has('__dyn_get_or')
207
+ const needsSchemaTbl = (ctx.schema.list.length && tblConsumed &&
208
+ (hasStringify || ctx.schema.list.some(s => s.length > 0))) ||
179
209
  hasJpObj
180
210
  if (needsSchemaTbl) {
181
211
  const nSchemas = ctx.schema.list.length
@@ -195,7 +225,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
195
225
  const keys = ctx.schema.list[s]
196
226
  const n = keys.length
197
227
  schemaInit.push(
198
- ['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n], ['i32.const', 8]]])
228
+ ['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n]]])
199
229
  for (let k = 0; k < n; k++)
200
230
  schemaInit.push(
201
231
  ['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
@@ -384,7 +414,7 @@ export function pullStdlib(sec) {
384
414
  if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
385
415
  else sec.memory.push(['memory', ['export', '"memory"'], pages])
386
416
  if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
387
- sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
417
+ sec.funcs.push(...ctx.core._allocRawFuncs.map(parseTemplate))
388
418
  }
389
419
 
390
420
  const stdlibStr = (name) => {
@@ -394,14 +424,14 @@ export function pullStdlib(sec) {
394
424
  ctx.core.extImports ??= new Set()
395
425
  for (const name of Object.keys(ctx.core.stdlib)) {
396
426
  if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
397
- const parsed = parseWat(stdlibStr(name))
427
+ const parsed = parseTemplate(stdlibStr(name))
398
428
  sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
399
429
  ctx.core.extImports.add(name)
400
430
  ctx.core.includes.delete(name)
401
431
  }
402
432
  }
403
- for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) console.error("MISSING stdlib:", n)
404
- sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
433
+ for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) err(`internal: stdlib '${n}' was requested but never registered (this is a jz bug — feature pulled in something it can't deliver)`)
434
+ sec.stdlib.push(...[...ctx.core.includes].map(n => parseTemplate(stdlibStr(n))))
405
435
  }
406
436
 
407
437
  export function syncImports(sec) {
@@ -435,7 +465,9 @@ export function optimizeModule(sec) {
435
465
  }
436
466
  // Build global name→type map from ctx.scope.globalTypes (keys without $) for promoteGlobals
437
467
  const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
438
- for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) optimizeFunc(s, cfg, globalTypesMap)
468
+ const allFuncs = [...sec.funcs, ...sec.stdlib, ...sec.start]
469
+ const volatileGlobals = collectVolatileGlobals(allFuncs)
470
+ for (const s of allFuncs) optimizeFunc(s, cfg, globalTypesMap, volatileGlobals)
439
471
  if (!cfg || cfg.arenaRewind !== false) {
440
472
  const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
441
473
  const fnByName = new Map()
@@ -449,7 +481,11 @@ export function optimizeModule(sec) {
449
481
  }
450
482
  }
451
483
  if (!cfg || cfg.hoistConstantPool !== false)
452
- hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
484
+ hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => {
485
+ ctx.scope.globals.set(name, wat)
486
+ const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
487
+ if (m) ctx.scope.globalTypes.set(name, m[1])
488
+ })
453
489
 
454
490
  // Second promoteGlobals pass disabled: promoting hoistConstantPool's __fc*
455
491
  // globals regressed the watr perf micro-pin (WASM compile time increased).
@@ -464,7 +500,9 @@ export function optimizeModule(sec) {
464
500
  const dataLen = ctx.runtime.data?.length || 0
465
501
  if (dataLen > 1024 && !ctx.memory.shared) {
466
502
  const heapBase = (dataLen + 7) & ~7
467
- ctx.scope.globals.set('__heap', `(global $__heap (mut i32) (i32.const ${heapBase}))`)
503
+ // Non-shared memory always carries a $__heap global start it past the
504
+ // static data so the bump allocator never overwrites a literal.
505
+ ctx.scope.globals.set('__heap', `(global $__heap (export "__heap") (mut i32) (i32.const ${heapBase}))`)
468
506
  ctx.scope.globalTypes.set('__heap', 'i32')
469
507
  if (ctx.scope.globals.has('__heap_start')) {
470
508
  ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
package/src/autoload.js CHANGED
@@ -36,9 +36,12 @@ export const OP_MODULES = {
36
36
  'in': ['core', 'collection', 'string'],
37
37
  '==': ['core', 'string'],
38
38
  '!=': ['core', 'string'],
39
+ '===': ['core', 'string'],
40
+ '!==': ['core', 'string'],
39
41
  'typeof': ['core', 'string'],
40
42
  '[': ['core', 'array'],
41
43
  '{': ['core', 'object', 'string', 'collection'],
44
+ 'delete': ['core', 'collection', 'string'],
42
45
  '//': ['core', 'string', 'regex'],
43
46
  '**': ['math'],
44
47
  }
@@ -50,6 +53,8 @@ export const CALL_MODULES = dict({
50
53
  BigUint64Array: ['core', 'typedarray'],
51
54
  parseFloat: ['number', 'string'],
52
55
  parseInt: ['number', 'string'],
56
+ encodeURIComponent: ['core', 'string', 'number'],
57
+ decodeURIComponent: ['core', 'string', 'number'],
53
58
  String: ['core', 'string', 'number'],
54
59
  Number: ['number', 'string'],
55
60
  Boolean: ['number'],
@@ -60,10 +65,13 @@ export const CALL_MODULES = dict({
60
65
  'console.log': ['core', 'string', 'number', 'console'],
61
66
  'console.warn': ['core', 'string', 'number', 'console'],
62
67
  'console.error': ['core', 'string', 'number', 'console'],
63
- 'Object.fromEntries': ['collection', 'string'],
68
+ 'Object.fromEntries': ['core', 'object', 'collection', 'string'],
64
69
  'Object.keys': ['core', 'object', 'string'],
70
+ 'Object.getOwnPropertyNames': ['core', 'object', 'string'],
65
71
  'Object.values': ['core', 'object', 'string'],
66
72
  'Object.entries': ['core', 'object', 'string'],
73
+ 'Object.hasOwn': ['core', 'object', 'string', 'collection'],
74
+ 'Object.freeze': ['core', 'object'],
67
75
  'Object.assign': ['core', 'object'],
68
76
  'Object.create': ['core', 'object'],
69
77
  'Object.defineProperty': ['core', 'object'],
@@ -85,6 +93,10 @@ export const CALL_MODULES = dict({
85
93
  'Int8Array.from': ['core', 'typedarray', 'array'],
86
94
  'Uint8Array.from': ['core', 'typedarray', 'array'],
87
95
  'ArrayBuffer.isView': ['core', 'typedarray'],
96
+ // instanceof Map / Set / TypedArray predicates (synthesized by jzify).
97
+ '__is_map': ['core', 'collection'],
98
+ '__is_set': ['core', 'collection'],
99
+ '__is_typed': ['core', 'typedarray'],
88
100
  })
89
101
 
90
102
  export const GENERIC_METHOD_MODULES = dict({
package/src/codegen.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * AST → jz source codegen.
3
+ *
4
+ * Pretty-prints a jzify-transformed AST back to jz source text. CLI-only
5
+ * (`jz jzify file.js` → `file.jz`); the compile path consumes the AST directly
6
+ * and never round-trips through source.
7
+ *
8
+ * @module codegen
9
+ */
10
+
11
+ const INDENT = ' '
12
+ const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
13
+ '??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
14
+ '<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
15
+ '+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
16
+
17
+ /** Wrap statement in { } if not already a block */
18
+ function wrapBlock(node, depth) {
19
+ if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
20
+ return '{ ' + codegen(node, depth) + '; }'
21
+ }
22
+
23
+ /** Generate jz source from AST. Enforces semicolons. */
24
+ export function codegen(node, depth = 0) {
25
+ if (node == null) return ''
26
+ if (typeof node === 'number') return String(node)
27
+ if (typeof node === 'bigint') return node + 'n'
28
+ if (typeof node === 'string') return node
29
+ if (!Array.isArray(node)) return String(node)
30
+
31
+ const [op, ...a] = node
32
+ const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
33
+
34
+ // Literal: [, value]
35
+ if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
36
+
37
+ // Statements
38
+ if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
39
+ if (op === '{}') {
40
+ // Discriminate object literal / destructuring pattern from block.
41
+ // Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
42
+ // lone string shorthand. Empty `{}` outputs the same string either way.
43
+ const body = a[0]
44
+ const isObjItem = (n) => typeof n === 'string' ||
45
+ (Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
46
+ (n[0] === '=' && typeof n[1] === 'string')))
47
+ const isObj = body == null ? false
48
+ : typeof body === 'string' ? true
49
+ : Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
50
+ (body[0] === ',' && body.slice(1).every(isObjItem)))
51
+ if (isObj) {
52
+ if (typeof body === 'string') return '{ ' + body + ' }'
53
+ if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
54
+ return '{ ' + codegen(body) + ' }'
55
+ }
56
+ // Block: body is null, a single statement, or [';', ...stmts]
57
+ const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
58
+ const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
59
+ return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
60
+ }
61
+
62
+ // Declarations
63
+ if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
64
+ if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
65
+ if (op === 'default') return 'default ' + codegen(a[0], depth)
66
+
67
+ // Control flow
68
+ if (op === 'if') {
69
+ const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
70
+ return a[2] != null
71
+ ? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
72
+ : 'if (' + cond + ') ' + then
73
+ }
74
+ if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
75
+ if (op === 'for') {
76
+ if (a.length === 2) { // ['for', head, body] — subscript shape
77
+ const [head, body] = a
78
+ if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
79
+ return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
80
+ // ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
81
+ if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
82
+ return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
83
+ // C-style head [';', init, cond, update] is positional — empty slots are valid,
84
+ // must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
85
+ if (Array.isArray(head) && head[0] === ';')
86
+ return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
87
+ return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
88
+ }
89
+ return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
90
+ }
91
+ if (op === 'return') return 'return ' + codegen(a[0])
92
+ if (op === 'throw') return 'throw ' + codegen(a[0])
93
+ if (op === 'break') return 'break'
94
+ if (op === 'continue') return 'continue'
95
+ // catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
96
+ if (op === 'catch') {
97
+ if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
98
+ return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
99
+ }
100
+
101
+ // Arrow
102
+ if (op === '=>') {
103
+ // Params: already wrapped in () by parser, or bare name
104
+ const p = a[0]
105
+ const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
106
+ const body = a[1]
107
+ const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
108
+ const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
109
+ ? '{ ' + codegen(body, depth) + '; }'
110
+ : codegen(body, depth)
111
+ return params + ' => ' + bodyStr
112
+ }
113
+
114
+ // Grouping parens / function call
115
+ if (op === '()') {
116
+ if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
117
+ return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
118
+ }
119
+
120
+ // Property access
121
+ if (op === '.') return codegen(a[0]) + '.' + a[1]
122
+ if (op === '?.') return codegen(a[0]) + '?.' + a[1]
123
+ if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
124
+ if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
125
+ if (op === '[]') {
126
+ // Array literal: ['[]', body] (length 2 → a.length 1). body may be null (empty),
127
+ // a single element, or a [',', ...items] sequence.
128
+ if (a.length === 1) {
129
+ if (a[0] == null) return '[]'
130
+ const body = a[0]
131
+ if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
132
+ return '[' + codegen(body) + ']'
133
+ }
134
+ // Subscript: ['[]', obj, idx]
135
+ return codegen(a[0]) + '[' + codegen(a[1]) + ']'
136
+ }
137
+ if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
138
+ if (op === 'str') return JSON.stringify(a[0])
139
+ if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
140
+
141
+ // Comma
142
+ if (op === ',') return a.map(x => codegen(x)).join(', ')
143
+ // Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
144
+ if (op === '`') return '`' + a.map(p => {
145
+ if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
146
+ return '${' + codegen(p) + '}'
147
+ }).join('') + '`'
148
+
149
+ // Spread
150
+ if (op === '...') return '...' + codegen(a[0])
151
+
152
+ // Import / export rename
153
+ if (op === 'import') return 'import ' + codegen(a[0])
154
+ if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
155
+ if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
156
+
157
+ // Unary prefix
158
+ if (a.length === 1) {
159
+ if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
160
+ if (op === 'typeof') return 'typeof ' + codegen(a[0])
161
+ if (op === 'u-') return '-' + codegen(a[0])
162
+ if (op === 'u+') return '+' + codegen(a[0])
163
+ return op + codegen(a[0])
164
+ }
165
+
166
+ // Postfix
167
+ if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
168
+
169
+ // Binary
170
+ if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
171
+
172
+ // Ternary
173
+ if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
174
+
175
+ // Fallback
176
+ return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
177
+ }