jz 0.1.0 → 0.2.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 +221 -62
- package/cli.js +34 -8
- package/index.js +133 -14
- package/module/array.js +399 -131
- package/module/collection.js +457 -212
- package/module/console.js +170 -87
- package/module/core.js +247 -143
- package/module/date.js +691 -0
- package/module/function.js +84 -23
- package/module/index.js +2 -1
- package/module/json.js +485 -138
- package/module/math.js +81 -40
- package/module/number.js +189 -83
- package/module/object.js +228 -46
- package/module/regex.js +34 -30
- package/module/schema.js +17 -18
- package/module/string.js +483 -227
- package/module/symbol.js +6 -4
- package/module/timer.js +90 -32
- package/module/typedarray.js +176 -44
- package/package.json +5 -10
- package/src/analyze.js +639 -124
- package/src/autoload.js +183 -0
- package/src/compile.js +448 -1083
- package/src/ctx.js +42 -12
- package/src/emit.js +646 -147
- package/src/host.js +273 -68
- package/src/ir.js +81 -31
- package/src/jzify.js +220 -36
- package/src/narrow.js +956 -0
- package/src/optimize.js +628 -42
- package/src/plan.js +1233 -0
- package/src/prepare.js +438 -256
- package/src/vectorize.js +1016 -0
- package/wasi.js +23 -4
package/src/ir.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* @module ir
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { ctx, err, inc, PTR } from './ctx.js'
|
|
23
|
+
import { ctx, err, inc, PTR, LAYOUT } from './ctx.js'
|
|
24
24
|
import { T, VAL, valTypeOf, lookupValType, repOf, repOfGlobal } from './analyze.js'
|
|
25
25
|
|
|
26
26
|
// === Type helpers ===
|
|
@@ -49,8 +49,10 @@ function boxPtrIR(i32node, ptrType, aux = 0) {
|
|
|
49
49
|
* The `unsigned` flag (set by `>>>` codegen) opts into `convert_i32_u` so the canonical
|
|
50
50
|
* `(x >>> 0)` uint32 idiom converts to a positive f64 in [0, 2^32) instead of sign-flipping. */
|
|
51
51
|
export const asF64 = n => {
|
|
52
|
+
if (n == null) err(`compiler internal: expected emitted IR value in ${ctx.func.current?.name || '<module>'}, got empty value`)
|
|
52
53
|
if (n.ptrKind != null) return boxPtrIR(n, valKindToPtr(n.ptrKind), n.ptrAux || 0)
|
|
53
54
|
if (n.type === 'f64') return n
|
|
55
|
+
if (n.type === 'i64') return typed(['f64.reinterpret_i64', n], 'f64')
|
|
54
56
|
if (n[0] === 'i32.const' && typeof n[1] === 'number') return typed(['f64.const', n[1]], 'f64')
|
|
55
57
|
return typed([n.unsigned ? 'f64.convert_i32_u' : 'f64.convert_i32_s', n], 'f64')
|
|
56
58
|
}
|
|
@@ -73,8 +75,8 @@ export const asI32 = n => {
|
|
|
73
75
|
export const asPtrOffset = (n, ptrKind) =>
|
|
74
76
|
n.ptrKind === ptrKind ? n : typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(n)]], 'i32')
|
|
75
77
|
|
|
76
|
-
/** Coerce emitted IR to a target WASM param type ('i32' | 'f64'). */
|
|
77
|
-
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : asF64(n)
|
|
78
|
+
/** Coerce emitted IR to a target WASM param type ('i32' | 'i64' | 'f64'). */
|
|
79
|
+
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : asF64(n)
|
|
78
80
|
|
|
79
81
|
/** Coerce node to i32 with wrapping (JS `|0` semantics: values > 2^31 wrap to negative).
|
|
80
82
|
* Per ECMAScript ToInt32, NaN and ±∞ map to 0. `i64.trunc_sat_f64_s` handles NaN
|
|
@@ -90,6 +92,10 @@ export const toI32 = n => {
|
|
|
90
92
|
const inner = n[1]
|
|
91
93
|
return Array.isArray(inner) ? typed(inner, 'i32') : inner
|
|
92
94
|
}
|
|
95
|
+
if (Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'number') {
|
|
96
|
+
const v = n[1]
|
|
97
|
+
return typed(['i32.const', Number.isFinite(v) ? v | 0 : 0], 'i32')
|
|
98
|
+
}
|
|
93
99
|
// Leaf nodes are cheap to duplicate; for everything else, evaluate once via local.tee.
|
|
94
100
|
const isLeaf = Array.isArray(n) && n.length <= 2 &&
|
|
95
101
|
(n[0] === 'f64.const' || n[0] === 'local.get' || n[0] === 'global.get')
|
|
@@ -117,11 +123,14 @@ export const fromI64 = n => typed(['f64.reinterpret_i64', n], 'f64')
|
|
|
117
123
|
|
|
118
124
|
// === Nullish sentinels ===
|
|
119
125
|
|
|
120
|
-
/**
|
|
126
|
+
/** Reserved atoms (PTR.ATOM tag, offset=0).
|
|
127
|
+
* aux=1 → null (NULL_NAN)
|
|
128
|
+
* aux=2 → undefined (UNDEF_NAN)
|
|
129
|
+
* See module/symbol.js for the broader reserved-atom-id scheme.
|
|
121
130
|
* Distinct from 0, NaN, and all pointers. Triggers default params.
|
|
122
131
|
* At the JS boundary, null and undefined preserve their identity for interop. */
|
|
123
|
-
export const NULL_NAN = '
|
|
124
|
-
export const UNDEF_NAN = '
|
|
132
|
+
export const NULL_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (1n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
133
|
+
export const UNDEF_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
125
134
|
/** WAT-template-ready sentinel expressions for use in stdlib template strings.
|
|
126
135
|
* `f64.const nan:0xHEX` is 3 bytes shorter than `f64.reinterpret_i64 (i64.const ...)`. */
|
|
127
136
|
export const NULL_WAT = `(f64.const nan:${NULL_NAN})`
|
|
@@ -148,15 +157,14 @@ export const BOXED_MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splic
|
|
|
148
157
|
|
|
149
158
|
// === Pointer construction ===
|
|
150
159
|
|
|
151
|
-
const NAN_PREFIX_BITS = 0x7FF8n
|
|
152
160
|
const litI32 = n => Array.isArray(n) && n[0] === 'i32.const' && typeof n[1] === 'number' ? n[1] : null
|
|
153
161
|
|
|
154
162
|
/** Pack (type, aux, offset) into the f64 NaN-box bit pattern as a hex string. */
|
|
155
163
|
function packPtrBits(type, aux, offset) {
|
|
156
|
-
const bits =
|
|
157
|
-
| ((BigInt(type) &
|
|
158
|
-
| ((BigInt(aux) &
|
|
159
|
-
| (BigInt(offset >>> 0) &
|
|
164
|
+
const bits = LAYOUT.NAN_PREFIX_BITS
|
|
165
|
+
| ((BigInt(type) & BigInt(LAYOUT.TAG_MASK)) << BigInt(LAYOUT.TAG_SHIFT))
|
|
166
|
+
| ((BigInt(aux) & BigInt(LAYOUT.AUX_MASK)) << BigInt(LAYOUT.AUX_SHIFT))
|
|
167
|
+
| (BigInt(offset >>> 0) & BigInt(LAYOUT.OFFSET_MASK))
|
|
160
168
|
return '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
|
|
161
169
|
}
|
|
162
170
|
|
|
@@ -184,14 +192,14 @@ export function ptrOffsetIR(valIR, valType) {
|
|
|
184
192
|
return ['i32.wrap_i64', ['i64.reinterpret_f64', valIR]]
|
|
185
193
|
}
|
|
186
194
|
inc('__ptr_offset')
|
|
187
|
-
return ['call', '$__ptr_offset', valIR]
|
|
195
|
+
return ['call', '$__ptr_offset', ['i64.reinterpret_f64', valIR]]
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
/** Map VAL.* → PTR.* when unambiguous. STRING is ambiguous (heap vs SSO). ARRAY maps
|
|
191
199
|
* to PTR.ARRAY but callers that want to skip forwarding must check separately. */
|
|
192
200
|
const VAL_TO_PTR = {
|
|
193
201
|
array: PTR.ARRAY, object: PTR.OBJECT, set: PTR.SET, map: PTR.MAP,
|
|
194
|
-
closure: PTR.CLOSURE, typed: PTR.TYPED, buffer: PTR.BUFFER,
|
|
202
|
+
closure: PTR.CLOSURE, typed: PTR.TYPED, buffer: PTR.BUFFER, date: PTR.OBJECT,
|
|
195
203
|
}
|
|
196
204
|
export const valKindToPtr = (vt) => VAL_TO_PTR[vt]
|
|
197
205
|
|
|
@@ -247,7 +255,7 @@ export function appendStaticSlots(slots, headerBytes = 0) {
|
|
|
247
255
|
if (!ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = []
|
|
248
256
|
for (let i = 0; i < slots.length; i++) {
|
|
249
257
|
const bits = slots[i]
|
|
250
|
-
if (((bits >> 48n) & 0xFFF8n) ===
|
|
258
|
+
if (((bits >> 48n) & 0xFFF8n) === BigInt(LAYOUT.NAN_PREFIX)) {
|
|
251
259
|
ctx.runtime.staticPtrSlots.push(off + i * 8)
|
|
252
260
|
}
|
|
253
261
|
}
|
|
@@ -272,11 +280,22 @@ const PURE_OPS = new Set(['i32.const', 'f64.const', 'local.get', 'global.get',
|
|
|
272
280
|
'i32.eq', 'i32.ne', 'i32.lt_s', 'i32.gt_s', 'i32.le_s', 'i32.ge_s', 'i32.eqz'])
|
|
273
281
|
export const isPureIR = n => Array.isArray(n) && PURE_OPS.has(n[0]) && n.slice(1).every(c => !Array.isArray(c) || isPureIR(c))
|
|
274
282
|
|
|
283
|
+
/** Check if AST node is a literal string: ['str', 'value']. */
|
|
284
|
+
export const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
285
|
+
|
|
286
|
+
/** Resolve compile-time value type from AST node (literal → type, name → lookup). */
|
|
287
|
+
export const resolveValType = (node, valTypeOf, lookupValType) =>
|
|
288
|
+
valTypeOf(node) ?? (typeof node === 'string' ? lookupValType(node) : null)
|
|
289
|
+
|
|
290
|
+
/** Check if AST node is a string reference to a known function name. */
|
|
291
|
+
export const isFuncRef = (node, funcNames) => typeof node === 'string' && funcNames.has(node)
|
|
292
|
+
|
|
275
293
|
/** Check if (a, op, b) is a postfix pattern: [op, name] and [, 1] literal. */
|
|
276
294
|
export const isPostfix = (a, op, b) => Array.isArray(a) && a[0] === op && Array.isArray(b) && b[0] == null && b[1] === 1
|
|
277
295
|
|
|
278
|
-
/** Emit a numeric constant with correct i32/f64 typing.
|
|
279
|
-
|
|
296
|
+
/** Emit a numeric constant with correct i32/f64 typing.
|
|
297
|
+
* `-0` is f64-only (i32 has no signed zero) — preserve the sign by emitting f64. */
|
|
298
|
+
export const emitNum = v => Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 && !Object.is(v, -0)
|
|
280
299
|
? typed(['i32.const', v], 'i32') : typed(['f64.const', v], 'f64')
|
|
281
300
|
|
|
282
301
|
// === Temp locals ===
|
|
@@ -339,7 +358,7 @@ export function toNumF64(node, v) {
|
|
|
339
358
|
}
|
|
340
359
|
if (!ctx.core.stdlib['__to_num']) return asF64(v)
|
|
341
360
|
inc('__to_num')
|
|
342
|
-
return typed(['call', '$__to_num',
|
|
361
|
+
return typed(['call', '$__to_num', asI64(v)], 'f64')
|
|
343
362
|
}
|
|
344
363
|
|
|
345
364
|
/** Convert already-emitted WASM node to i32 boolean. NaN is falsy (like JS).
|
|
@@ -361,7 +380,7 @@ export function truthyIR(e) {
|
|
|
361
380
|
// all other NaN-boxed pointers (SSO strings, heap ptrs, etc.) are truthy.
|
|
362
381
|
if (e[0] === 'f64.reinterpret_i64' && Array.isArray(e[1]) && e[1][0] === 'i64.const') {
|
|
363
382
|
const bits = String(e[1][1])
|
|
364
|
-
const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '
|
|
383
|
+
const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA400000000000'])
|
|
365
384
|
return typed(['i32.const', FALSY.has(bits) ? 0 : 1], 'i32')
|
|
366
385
|
}
|
|
367
386
|
// Fresh pointer constructors never produce nullish. Treat as always truthy.
|
|
@@ -375,13 +394,13 @@ export function truthyIR(e) {
|
|
|
375
394
|
const name = e[1][0] === '$' ? e[1].slice(1) : e[1]
|
|
376
395
|
const vt = lookupValType(name)
|
|
377
396
|
if (vt === VAL.ARRAY || vt === VAL.OBJECT || vt === VAL.SET || vt === VAL.MAP ||
|
|
378
|
-
vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX) {
|
|
397
|
+
vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX || vt === VAL.DATE) {
|
|
379
398
|
return typed(['i32.eqz', isNullish(e)], 'i32')
|
|
380
399
|
}
|
|
381
400
|
}
|
|
382
401
|
}
|
|
383
402
|
inc('__is_truthy')
|
|
384
|
-
return typed(['call', '$__is_truthy',
|
|
403
|
+
return typed(['call', '$__is_truthy', asI64(e)], 'i32')
|
|
385
404
|
}
|
|
386
405
|
export const toBoolFromEmitted = truthyIR
|
|
387
406
|
|
|
@@ -400,9 +419,13 @@ export function usesDynProps(vt) {
|
|
|
400
419
|
* `target` is the var name receiving the literal (or null when escaping). */
|
|
401
420
|
export function needsDynShadow(target) {
|
|
402
421
|
if (!ctx.module.modules.collection) return false
|
|
422
|
+
// Functions/CLOSURE always need dynamic props so cross-module property
|
|
423
|
+
// access (fn.parse, i32.parse aliases) sees the same value as schema slots.
|
|
424
|
+
const vt = typeof target === 'string' ? (ctx.func.repByLocal?.get(target)?.val || ctx.scope.globalValTypes?.get(target)) : null
|
|
425
|
+
if (vt === 'closure' || usesDynProps(vt)) return true
|
|
403
426
|
const dyn = ctx.types?.dynKeyVars
|
|
404
|
-
if (target == null) return ctx.types?.anyDynKey ??
|
|
405
|
-
return dyn ? dyn.has(target) :
|
|
427
|
+
if (target == null) return ctx.types?.anyDynKey ?? false
|
|
428
|
+
return dyn ? dyn.has(target) : false
|
|
406
429
|
}
|
|
407
430
|
|
|
408
431
|
// === Variable storage abstraction ===
|
|
@@ -531,7 +554,29 @@ export const isNullish = (f64expr) => {
|
|
|
531
554
|
}
|
|
532
555
|
// Non-trivial expr: fall back to the helper — keeps binary size stable & preserves eval once.
|
|
533
556
|
inc('__is_nullish')
|
|
534
|
-
return typed(['call', '$__is_nullish', f64expr], 'i32')
|
|
557
|
+
return typed(['call', '$__is_nullish', ['i64.reinterpret_f64', f64expr]], 'i32')
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Check if f64 expr is exactly `undefined` (UNDEF_NAN). Returns i32.
|
|
561
|
+
* Used by default-param semantics — only `undefined` (or missing arg) triggers
|
|
562
|
+
* the default; `null` should pass through. */
|
|
563
|
+
export const isUndef = (f64expr) => {
|
|
564
|
+
if (f64expr.ptrKind != null) return typed(['i32.const', 0], 'i32')
|
|
565
|
+
if (Array.isArray(f64expr)) {
|
|
566
|
+
if (f64expr[0] === 'f64.const') {
|
|
567
|
+
const lit = String(f64expr[1])
|
|
568
|
+
if (lit.startsWith('nan:')) {
|
|
569
|
+
const bits = lit.slice(4)
|
|
570
|
+
return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
|
|
571
|
+
}
|
|
572
|
+
return typed(['i32.const', 0], 'i32')
|
|
573
|
+
}
|
|
574
|
+
if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const') {
|
|
575
|
+
const bits = String(f64expr[1][1])
|
|
576
|
+
return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return typed(['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]], 'i32')
|
|
535
580
|
}
|
|
536
581
|
|
|
537
582
|
// === Array layout helpers ===
|
|
@@ -560,16 +605,21 @@ export function elemStore(ptr, i, val) {
|
|
|
560
605
|
*
|
|
561
606
|
* Optional `lenLocal`: caller already has the array length in an i32 local
|
|
562
607
|
* (e.g. from sizing the output before the loop). Reuses it instead of
|
|
563
|
-
* re-loading from ptr-8.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
608
|
+
* re-loading from ptr-8.
|
|
609
|
+
* Optional `ptrLocal`: caller already has the resolved ARRAY data pointer in
|
|
610
|
+
* an i32 local. Reuses it instead of calling __ptr_offset again. */
|
|
611
|
+
export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
|
|
612
|
+
const arr = ptrLocal ? null : temp('aa'), ptr = ptrLocal ?? tempI32('ap'), i = tempI32('ai'), item = temp('av')
|
|
567
613
|
const len = lenLocal ?? tempI32('al')
|
|
568
614
|
const id = ctx.func.uniq++
|
|
569
|
-
const setup = [
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
615
|
+
const setup = []
|
|
616
|
+
if (!ptrLocal) {
|
|
617
|
+
inc('__ptr_offset')
|
|
618
|
+
setup.push(
|
|
619
|
+
['local.set', `$${arr}`, asF64(arrExpr)],
|
|
620
|
+
['local.set', `$${ptr}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]],
|
|
621
|
+
)
|
|
622
|
+
}
|
|
573
623
|
if (!lenLocal) setup.push(
|
|
574
624
|
['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]])
|
|
575
625
|
setup.push(
|
package/src/jzify.js
CHANGED
|
@@ -26,19 +26,134 @@ export default function jzify(ast) {
|
|
|
26
26
|
swIdx = 0
|
|
27
27
|
argsIdx = 0
|
|
28
28
|
doIdx = 0
|
|
29
|
+
// Hoist module-level vars: any `var x` inside nested blocks bubbles up.
|
|
30
|
+
const names = new Set()
|
|
31
|
+
ast = hoistVars(ast, names)
|
|
32
|
+
if (names.size) ast = prependDecls(ast, names)
|
|
29
33
|
return transformScope(ast)
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Walk function/script body, replacing `var` declarations with assignments and
|
|
38
|
+
* collecting names. Does not cross function/arrow boundaries — nested functions
|
|
39
|
+
* get their own hoist pass when wrapArrowBody processes them.
|
|
40
|
+
*
|
|
41
|
+
* ['var', 'x'] → null (bare decl, no-op)
|
|
42
|
+
* ['var', ['=', x, init]] → ['=', x, init]
|
|
43
|
+
* ['var', ['=', x, 1], ['=', y, 2]] → [',', ['=', x, 1], ['=', y, 2]]
|
|
44
|
+
* ['var', 'x', 'y'] → null
|
|
45
|
+
* ['in', ['var', x], obj] → ['in', x, obj] (for-in head)
|
|
46
|
+
*/
|
|
47
|
+
function hoistVars(node, names) {
|
|
48
|
+
if (node == null || !Array.isArray(node)) return node
|
|
49
|
+
const op = node[0]
|
|
50
|
+
// Nested function/arrow: hoist within its own scope, prepend let-decl, return new node.
|
|
51
|
+
if (op === 'function') {
|
|
52
|
+
const inner = new Set()
|
|
53
|
+
let body = hoistVars(node[3], inner)
|
|
54
|
+
if (inner.size) body = prependDecls(body, inner)
|
|
55
|
+
return ['function', node[1], node[2], body]
|
|
56
|
+
}
|
|
57
|
+
if (op === '=>') {
|
|
58
|
+
const inner = new Set()
|
|
59
|
+
let body = hoistVars(node[2], inner)
|
|
60
|
+
if (inner.size) body = prependDecls(body, inner)
|
|
61
|
+
return ['=>', node[1], body]
|
|
62
|
+
}
|
|
63
|
+
if (op === 'in' || op === 'of') {
|
|
64
|
+
let lhs = node[1]
|
|
65
|
+
if (Array.isArray(lhs) && lhs[0] === 'var' && typeof lhs[1] === 'string' && lhs.length === 2) {
|
|
66
|
+
names.add(lhs[1])
|
|
67
|
+
lhs = lhs[1]
|
|
68
|
+
} else {
|
|
69
|
+
lhs = hoistVars(lhs, names)
|
|
70
|
+
}
|
|
71
|
+
return [op, lhs, hoistVars(node[2], names)]
|
|
72
|
+
}
|
|
73
|
+
// For-head `;` is positional (init; cond; update), not a statement sequence.
|
|
74
|
+
// Recurse into each slot but never filter nulls — empty slots are valid.
|
|
75
|
+
if (op === 'for') {
|
|
76
|
+
const head = node[1]
|
|
77
|
+
let h2
|
|
78
|
+
if (Array.isArray(head) && head[0] === ';') {
|
|
79
|
+
h2 = [';']
|
|
80
|
+
for (let i = 1; i < head.length; i++) h2.push(hoistVars(head[i], names))
|
|
81
|
+
} else {
|
|
82
|
+
h2 = hoistVars(head, names)
|
|
83
|
+
}
|
|
84
|
+
return ['for', h2, hoistVars(node[2], names)]
|
|
85
|
+
}
|
|
86
|
+
if (op === 'var') {
|
|
87
|
+
const decls = []
|
|
88
|
+
for (let i = 1; i < node.length; i++) {
|
|
89
|
+
const d = node[i]
|
|
90
|
+
if (typeof d === 'string') { names.add(d); continue }
|
|
91
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
|
|
92
|
+
names.add(d[1])
|
|
93
|
+
decls.push(['=', d[1], hoistVars(d[2], names)])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (decls.length === 0) return null
|
|
97
|
+
if (decls.length === 1) return decls[0]
|
|
98
|
+
return [',', ...decls]
|
|
99
|
+
}
|
|
100
|
+
// Filter null returns from `;` sequences (bare-var no-ops). `{}` is left
|
|
101
|
+
// to recurse normally — it may be either a block or an object literal,
|
|
102
|
+
// and we don't want to clobber `['{}', null]` (empty object literal).
|
|
103
|
+
if (op === ';') {
|
|
104
|
+
const out = [op]
|
|
105
|
+
for (let i = 1; i < node.length; i++) {
|
|
106
|
+
const c = hoistVars(node[i], names)
|
|
107
|
+
if (c != null) out.push(c)
|
|
108
|
+
}
|
|
109
|
+
if (out.length === 1) return null
|
|
110
|
+
if (out.length === 2) return out[1]
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
const out = new Array(node.length)
|
|
114
|
+
out[0] = op
|
|
115
|
+
for (let i = 1; i < node.length; i++) out[i] = hoistVars(node[i], names)
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function prependDecls(body, names) {
|
|
120
|
+
const decl = ['let', ...names]
|
|
121
|
+
if (Array.isArray(body) && body[0] === ';') return [';', decl, ...body.slice(1)]
|
|
122
|
+
if (Array.isArray(body) && body[0] === '{}') {
|
|
123
|
+
const inner = body[1]
|
|
124
|
+
if (Array.isArray(inner) && inner[0] === ';') return ['{}', [';', decl, ...inner.slice(1)]]
|
|
125
|
+
if (inner == null) return ['{}', decl]
|
|
126
|
+
return ['{}', [';', decl, inner]]
|
|
127
|
+
}
|
|
128
|
+
return body == null ? decl : [';', decl, body]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Convert a named function declaration to a hoisted const arrow */
|
|
132
|
+
function hoistFnDecl(name, params, body) {
|
|
133
|
+
const [p2, b2] = lowerArguments(params, body)
|
|
134
|
+
const decl = ['const', ['=', name, ['=>', p2, wrapArrowBody(b2)]]]
|
|
135
|
+
decl._hoisted = true
|
|
136
|
+
return decl
|
|
137
|
+
}
|
|
138
|
+
|
|
32
139
|
/** Transform a scope (module top-level or block body). Collects hoisted functions. */
|
|
33
140
|
function transformScope(node) {
|
|
34
141
|
if (!Array.isArray(node)) return transform(node)
|
|
35
142
|
|
|
36
143
|
const [op, ...args] = node
|
|
37
144
|
|
|
145
|
+
// Single named function-statement at scope position: hoist as const arrow
|
|
146
|
+
if (op === 'function' && args[0]) return hoistFnDecl(...args)
|
|
147
|
+
|
|
38
148
|
// Statement sequence: collect hoisted functions
|
|
39
149
|
if (op === ';') {
|
|
40
150
|
const hoisted = [], rest = []
|
|
41
151
|
for (const stmt of args) {
|
|
152
|
+
// Statement-form named function declaration: hoist directly (skip expression handler)
|
|
153
|
+
if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
|
|
154
|
+
hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
42
157
|
const t = transform(stmt)
|
|
43
158
|
if (t == null) continue
|
|
44
159
|
// Hoist function declarations to top of scope
|
|
@@ -122,38 +237,60 @@ function paramList(params) {
|
|
|
122
237
|
}
|
|
123
238
|
|
|
124
239
|
function lowerArguments(params, body) {
|
|
125
|
-
if (!usesArguments(body)) return [params, body]
|
|
240
|
+
if (!usesArguments(params) && !usesArguments(body)) return [params, body]
|
|
126
241
|
const name = `\uE001arg${argsIdx++}`
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
242
|
+
const decls = []
|
|
243
|
+
for (const [idx, param] of paramList(params).entries()) {
|
|
244
|
+
if (Array.isArray(param) && param[0] === '...') {
|
|
245
|
+
decls.push(['=', param[1], ['()', ['.', name, 'slice'], [null, idx]]])
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
if (Array.isArray(param) && param[0] === '=') {
|
|
249
|
+
decls.push(['=', param[1], ['??', ['[]', name, [null, idx]], renameArguments(param[2], name)]])
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
decls.push(['=', param, ['[]', name, [null, idx]]])
|
|
253
|
+
}
|
|
254
|
+
const renamed = renameArguments(body, name)
|
|
255
|
+
return [['()', ['...', name]], decls.length ? [';', ['let', ...decls], renamed] : renamed]
|
|
131
256
|
}
|
|
132
257
|
|
|
258
|
+
const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
|
|
259
|
+
|
|
133
260
|
const handlers = {
|
|
134
261
|
// Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
|
|
135
262
|
'()'(callee, ...rest) {
|
|
136
263
|
if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
|
|
137
264
|
const [, name, params, body] = callee[1]
|
|
138
265
|
const [p2, b2] = lowerArguments(params, body)
|
|
139
|
-
return [';', ['let', ['=', name, ['=>', p2, wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
|
|
266
|
+
return [';', ['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
|
|
140
267
|
}
|
|
141
268
|
},
|
|
142
269
|
|
|
143
|
-
// function → arrow
|
|
270
|
+
// function → arrow. Named function expression desugars to IIFE so the name is
|
|
271
|
+
// bound inside body per ES spec: `function f(){...f...}` → `(()=>{let f;f=arrow;return f})()`.
|
|
272
|
+
// Statement-form named functions are hoisted by transformScope before reaching here.
|
|
144
273
|
'function'(name, params, body) {
|
|
145
274
|
const [p2, b2] = lowerArguments(params, body)
|
|
146
275
|
const arrow = ['=>', p2, wrapArrowBody(b2)]
|
|
147
|
-
if (name) {
|
|
276
|
+
if (name) {
|
|
277
|
+
return ['()', ['()', ['=>', null, ['{}', [';',
|
|
278
|
+
['let', name],
|
|
279
|
+
['=', name, arrow],
|
|
280
|
+
['return', name]
|
|
281
|
+
]]]], null]
|
|
282
|
+
}
|
|
148
283
|
return arrow
|
|
149
284
|
},
|
|
150
285
|
|
|
151
|
-
|
|
286
|
+
// `var` is hoisted away before transform reaches here. If one slips through
|
|
287
|
+
// (e.g. raw subscript output without going via jzify entry/wrapArrowBody),
|
|
288
|
+
// fall back to treating it as `let`.
|
|
289
|
+
'var'(...args) {
|
|
290
|
+
return ['let', ...args.map(transform)]
|
|
291
|
+
},
|
|
152
292
|
|
|
153
293
|
'='(lhs, rhs) {
|
|
154
|
-
// var assignment: ['=', ['var', name], init] → let
|
|
155
|
-
if (Array.isArray(lhs) && lhs[0] === 'var')
|
|
156
|
-
return ['let', ['=', lhs[1], transform(rhs)]]
|
|
157
294
|
// Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
|
|
158
295
|
if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
|
|
159
296
|
const targets = []
|
|
@@ -205,25 +342,29 @@ const handlers = {
|
|
|
205
342
|
return ['===', ['typeof', t], [null, 'object']]
|
|
206
343
|
},
|
|
207
344
|
|
|
208
|
-
// do { body } while (cond) →
|
|
209
|
-
// Avoids body duplication
|
|
345
|
+
// do { body } while (cond) → let _once = true; while (_once || cond) { _once = false; body }
|
|
346
|
+
// Avoids body duplication and preserves continue: `continue` jumps back to the
|
|
347
|
+
// while condition after the one-shot flag has been cleared.
|
|
210
348
|
'do'(body, cond) {
|
|
211
349
|
const flag = `do${doIdx++}`
|
|
212
|
-
return ['
|
|
213
|
-
['
|
|
214
|
-
|
|
215
|
-
['||', flag, transform(cond)],
|
|
216
|
-
['=', flag, [null, false]]],
|
|
217
|
-
transform(body)]
|
|
350
|
+
return [';',
|
|
351
|
+
['let', ['=', flag, [null, true]]],
|
|
352
|
+
['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
|
|
218
353
|
},
|
|
219
354
|
|
|
220
355
|
// Block body: recurse as scope for hoisting
|
|
221
356
|
'{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
|
|
222
357
|
|
|
223
|
-
// Export: recurse into exported declaration
|
|
358
|
+
// Export: recurse into exported declaration. Statement-form `export function name`
|
|
359
|
+
// and `export default function name` must be hoisted as const-arrows — otherwise
|
|
360
|
+
// the generic `function` handler wraps them in a named-IIFE (correct for *expressions*,
|
|
361
|
+
// wrong for declarations), producing `export ['()', IIFE]` which has no exportable binding.
|
|
224
362
|
'export'(inner) {
|
|
363
|
+
if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
|
|
364
|
+
return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
|
|
365
|
+
}
|
|
225
366
|
if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
|
|
226
|
-
const decl =
|
|
367
|
+
const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
|
|
227
368
|
return [';', decl, ['export', ['default', inner[1][1]]]]
|
|
228
369
|
}
|
|
229
370
|
return ['export', transform(inner)]
|
|
@@ -294,8 +435,26 @@ export function codegen(node, depth = 0) {
|
|
|
294
435
|
// Statements
|
|
295
436
|
if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
|
|
296
437
|
if (op === '{}') {
|
|
297
|
-
|
|
298
|
-
|
|
438
|
+
// Discriminate object literal / destructuring pattern from block.
|
|
439
|
+
// Object: `:` key-value, `,` of object-pattern items (id / `:` / `...` / `= default`),
|
|
440
|
+
// lone string shorthand. Empty `{}` outputs the same string either way.
|
|
441
|
+
const body = a[0]
|
|
442
|
+
const isObjItem = (n) => typeof n === 'string' ||
|
|
443
|
+
(Array.isArray(n) && (n[0] === ':' || n[0] === '...' || n[0] === 'as' ||
|
|
444
|
+
(n[0] === '=' && typeof n[1] === 'string')))
|
|
445
|
+
const isObj = body == null ? false
|
|
446
|
+
: typeof body === 'string' ? true
|
|
447
|
+
: Array.isArray(body) && (body[0] === ':' || body[0] === '...' || body[0] === 'as' ||
|
|
448
|
+
(body[0] === ',' && body.slice(1).every(isObjItem)))
|
|
449
|
+
if (isObj) {
|
|
450
|
+
if (typeof body === 'string') return '{ ' + body + ' }'
|
|
451
|
+
if (body[0] === ',') return '{ ' + body.slice(1).map(x => codegen(x)).join(', ') + ' }'
|
|
452
|
+
return '{ ' + codegen(body) + ' }'
|
|
453
|
+
}
|
|
454
|
+
// Block: body is null, a single statement, or [';', ...stmts]
|
|
455
|
+
const stmts = body == null ? [] : (Array.isArray(body) && body[0] === ';' ? body.slice(1) : [body])
|
|
456
|
+
const rendered = stmts.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
|
|
457
|
+
return '{\n' + ind1 + rendered + (rendered ? ';' : '') + '\n' + ind + '}'
|
|
299
458
|
}
|
|
300
459
|
|
|
301
460
|
// Declarations
|
|
@@ -312,19 +471,30 @@ export function codegen(node, depth = 0) {
|
|
|
312
471
|
}
|
|
313
472
|
if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
|
|
314
473
|
if (op === 'for') {
|
|
315
|
-
if (a.length === 2) { // for
|
|
474
|
+
if (a.length === 2) { // ['for', head, body] — subscript shape
|
|
316
475
|
const [head, body] = a
|
|
317
476
|
if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
|
|
318
|
-
return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' +
|
|
319
|
-
|
|
477
|
+
return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
|
|
478
|
+
// ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
|
|
479
|
+
if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
|
|
480
|
+
return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + wrapBlock(body, depth)
|
|
481
|
+
// C-style head [';', init, cond, update] is positional — empty slots are valid,
|
|
482
|
+
// must not flow through the generic `;` joiner (which adds newlines + a trailing `;`).
|
|
483
|
+
if (Array.isArray(head) && head[0] === ';')
|
|
484
|
+
return 'for (' + (head[1] == null ? '' : codegen(head[1])) + '; ' + (head[2] == null ? '' : codegen(head[2])) + '; ' + (head[3] == null ? '' : codegen(head[3])) + ') ' + wrapBlock(body, depth)
|
|
485
|
+
return 'for (' + codegen(head) + ') ' + wrapBlock(body, depth)
|
|
320
486
|
}
|
|
321
|
-
return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' +
|
|
487
|
+
return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + wrapBlock(a[3], depth)
|
|
322
488
|
}
|
|
323
489
|
if (op === 'return') return 'return ' + codegen(a[0])
|
|
324
490
|
if (op === 'throw') return 'throw ' + codegen(a[0])
|
|
325
491
|
if (op === 'break') return 'break'
|
|
326
492
|
if (op === 'continue') return 'continue'
|
|
327
|
-
|
|
493
|
+
// catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
|
|
494
|
+
if (op === 'catch') {
|
|
495
|
+
if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
|
|
496
|
+
return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
|
|
497
|
+
}
|
|
328
498
|
|
|
329
499
|
// Arrow
|
|
330
500
|
if (op === '=>') {
|
|
@@ -348,25 +518,39 @@ export function codegen(node, depth = 0) {
|
|
|
348
518
|
// Property access
|
|
349
519
|
if (op === '.') return codegen(a[0]) + '.' + a[1]
|
|
350
520
|
if (op === '?.') return codegen(a[0]) + '?.' + a[1]
|
|
351
|
-
if (op === '[]') return codegen(a[0]) + '[' + codegen(a[1]) + ']'
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
521
|
+
if (op === '?.[]') return codegen(a[0]) + '?.[' + codegen(a[1]) + ']'
|
|
522
|
+
if (op === '?.()') return codegen(a[0]) + '?.(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
|
|
523
|
+
if (op === '[]') {
|
|
524
|
+
// Array literal: ['[]', body] (length 2 → a.length 1). body may be null (empty),
|
|
525
|
+
// a single element, or a [',', ...items] sequence.
|
|
526
|
+
if (a.length === 1) {
|
|
527
|
+
if (a[0] == null) return '[]'
|
|
528
|
+
const body = a[0]
|
|
529
|
+
if (Array.isArray(body) && body[0] === ',') return '[' + body.slice(1).map(x => codegen(x)).join(', ') + ']'
|
|
530
|
+
return '[' + codegen(body) + ']'
|
|
531
|
+
}
|
|
532
|
+
// Subscript: ['[]', obj, idx]
|
|
533
|
+
return codegen(a[0]) + '[' + codegen(a[1]) + ']'
|
|
534
|
+
}
|
|
355
535
|
if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
|
|
356
536
|
if (op === 'str') return JSON.stringify(a[0])
|
|
357
537
|
if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
|
|
358
538
|
|
|
359
539
|
// Comma
|
|
360
540
|
if (op === ',') return a.map(x => codegen(x)).join(', ')
|
|
361
|
-
// Template literal
|
|
362
|
-
if (op === '`') return '`' + a.map(
|
|
541
|
+
// Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
|
|
542
|
+
if (op === '`') return '`' + a.map(p => {
|
|
543
|
+
if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
|
|
544
|
+
return '${' + codegen(p) + '}'
|
|
545
|
+
}).join('') + '`'
|
|
363
546
|
|
|
364
547
|
// Spread
|
|
365
548
|
if (op === '...') return '...' + codegen(a[0])
|
|
366
549
|
|
|
367
|
-
// Import
|
|
550
|
+
// Import / export rename
|
|
368
551
|
if (op === 'import') return 'import ' + codegen(a[0])
|
|
369
552
|
if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
|
|
553
|
+
if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
|
|
370
554
|
|
|
371
555
|
// Unary prefix
|
|
372
556
|
if (a.length === 1) {
|