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/README.md +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
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
|
-
|
|
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 =>
|
|
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.
|
|
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
|
-
|
|
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)]
|
|
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
|
|
169
|
-
|
|
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]
|
|
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(
|
|
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 =
|
|
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])
|
|
404
|
-
sec.stdlib.push(...[...ctx.core.includes].map(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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
+
}
|