jz 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -90
- package/cli.js +34 -8
- package/index.js +100 -14
- package/module/array.js +396 -133
- package/module/collection.js +455 -212
- package/module/console.js +169 -88
- package/module/core.js +246 -144
- package/module/date.js +783 -0
- package/module/function.js +81 -21
- package/module/index.js +2 -1
- package/module/json.js +483 -138
- package/module/math.js +79 -39
- package/module/number.js +188 -78
- package/module/object.js +227 -47
- package/module/regex.js +33 -30
- package/module/schema.js +14 -17
- package/module/string.js +439 -235
- package/module/symbol.js +4 -3
- package/module/timer.js +89 -31
- package/module/typedarray.js +174 -44
- package/package.json +3 -10
- package/src/analyze.js +627 -133
- package/src/autoload.js +15 -7
- package/src/compile.js +399 -70
- package/src/ctx.js +41 -12
- package/src/emit.js +658 -145
- package/src/host.js +244 -51
- package/src/ir.js +87 -31
- package/src/jzify.js +181 -24
- package/src/narrow.js +32 -4
- package/src/optimize.js +628 -42
- package/src/plan.js +1167 -4
- package/src/prepare.js +322 -75
- package/src/vectorize.js +1016 -0
- package/wasi.js +13 -0
- package/src/key.js +0 -73
- package/src/source.js +0 -76
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 ===
|
|
@@ -328,6 +347,12 @@ export function toNumF64(node, v) {
|
|
|
328
347
|
if (v.type === 'i32' || isLit(v)) return asF64(v)
|
|
329
348
|
const vt = keyValType(node)
|
|
330
349
|
if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
|
|
350
|
+
if (vt === VAL.DATE) {
|
|
351
|
+
const ptr = v.ptrKind === VAL.DATE
|
|
352
|
+
? v
|
|
353
|
+
: ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
|
|
354
|
+
return typed(['f64.load', ptr], 'f64')
|
|
355
|
+
}
|
|
331
356
|
// intCertain locals: every reachable def is integer-valued, so the binding
|
|
332
357
|
// never carries a NaN-boxed pointer — skip the __to_num wrapper.
|
|
333
358
|
if (typeof node === 'string' && repOf(node)?.intCertain === true) return asF64(v)
|
|
@@ -339,7 +364,7 @@ export function toNumF64(node, v) {
|
|
|
339
364
|
}
|
|
340
365
|
if (!ctx.core.stdlib['__to_num']) return asF64(v)
|
|
341
366
|
inc('__to_num')
|
|
342
|
-
return typed(['call', '$__to_num',
|
|
367
|
+
return typed(['call', '$__to_num', asI64(v)], 'f64')
|
|
343
368
|
}
|
|
344
369
|
|
|
345
370
|
/** Convert already-emitted WASM node to i32 boolean. NaN is falsy (like JS).
|
|
@@ -361,7 +386,7 @@ export function truthyIR(e) {
|
|
|
361
386
|
// all other NaN-boxed pointers (SSO strings, heap ptrs, etc.) are truthy.
|
|
362
387
|
if (e[0] === 'f64.reinterpret_i64' && Array.isArray(e[1]) && e[1][0] === 'i64.const') {
|
|
363
388
|
const bits = String(e[1][1])
|
|
364
|
-
const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '
|
|
389
|
+
const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA400000000000'])
|
|
365
390
|
return typed(['i32.const', FALSY.has(bits) ? 0 : 1], 'i32')
|
|
366
391
|
}
|
|
367
392
|
// Fresh pointer constructors never produce nullish. Treat as always truthy.
|
|
@@ -375,13 +400,13 @@ export function truthyIR(e) {
|
|
|
375
400
|
const name = e[1][0] === '$' ? e[1].slice(1) : e[1]
|
|
376
401
|
const vt = lookupValType(name)
|
|
377
402
|
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) {
|
|
403
|
+
vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX || vt === VAL.DATE) {
|
|
379
404
|
return typed(['i32.eqz', isNullish(e)], 'i32')
|
|
380
405
|
}
|
|
381
406
|
}
|
|
382
407
|
}
|
|
383
408
|
inc('__is_truthy')
|
|
384
|
-
return typed(['call', '$__is_truthy',
|
|
409
|
+
return typed(['call', '$__is_truthy', asI64(e)], 'i32')
|
|
385
410
|
}
|
|
386
411
|
export const toBoolFromEmitted = truthyIR
|
|
387
412
|
|
|
@@ -400,9 +425,13 @@ export function usesDynProps(vt) {
|
|
|
400
425
|
* `target` is the var name receiving the literal (or null when escaping). */
|
|
401
426
|
export function needsDynShadow(target) {
|
|
402
427
|
if (!ctx.module.modules.collection) return false
|
|
428
|
+
// Functions/CLOSURE always need dynamic props so cross-module property
|
|
429
|
+
// access (fn.parse, i32.parse aliases) sees the same value as schema slots.
|
|
430
|
+
const vt = typeof target === 'string' ? (ctx.func.repByLocal?.get(target)?.val || ctx.scope.globalValTypes?.get(target)) : null
|
|
431
|
+
if (vt === 'closure' || usesDynProps(vt)) return true
|
|
403
432
|
const dyn = ctx.types?.dynKeyVars
|
|
404
|
-
if (target == null) return ctx.types?.anyDynKey ??
|
|
405
|
-
return dyn ? dyn.has(target) :
|
|
433
|
+
if (target == null) return ctx.types?.anyDynKey ?? false
|
|
434
|
+
return dyn ? dyn.has(target) : false
|
|
406
435
|
}
|
|
407
436
|
|
|
408
437
|
// === Variable storage abstraction ===
|
|
@@ -531,7 +560,29 @@ export const isNullish = (f64expr) => {
|
|
|
531
560
|
}
|
|
532
561
|
// Non-trivial expr: fall back to the helper — keeps binary size stable & preserves eval once.
|
|
533
562
|
inc('__is_nullish')
|
|
534
|
-
return typed(['call', '$__is_nullish', f64expr], 'i32')
|
|
563
|
+
return typed(['call', '$__is_nullish', ['i64.reinterpret_f64', f64expr]], 'i32')
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Check if f64 expr is exactly `undefined` (UNDEF_NAN). Returns i32.
|
|
567
|
+
* Used by default-param semantics — only `undefined` (or missing arg) triggers
|
|
568
|
+
* the default; `null` should pass through. */
|
|
569
|
+
export const isUndef = (f64expr) => {
|
|
570
|
+
if (f64expr.ptrKind != null) return typed(['i32.const', 0], 'i32')
|
|
571
|
+
if (Array.isArray(f64expr)) {
|
|
572
|
+
if (f64expr[0] === 'f64.const') {
|
|
573
|
+
const lit = String(f64expr[1])
|
|
574
|
+
if (lit.startsWith('nan:')) {
|
|
575
|
+
const bits = lit.slice(4)
|
|
576
|
+
return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
|
|
577
|
+
}
|
|
578
|
+
return typed(['i32.const', 0], 'i32')
|
|
579
|
+
}
|
|
580
|
+
if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const') {
|
|
581
|
+
const bits = String(f64expr[1][1])
|
|
582
|
+
return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return typed(['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]], 'i32')
|
|
535
586
|
}
|
|
536
587
|
|
|
537
588
|
// === Array layout helpers ===
|
|
@@ -560,16 +611,21 @@ export function elemStore(ptr, i, val) {
|
|
|
560
611
|
*
|
|
561
612
|
* Optional `lenLocal`: caller already has the array length in an i32 local
|
|
562
613
|
* (e.g. from sizing the output before the loop). Reuses it instead of
|
|
563
|
-
* re-loading from ptr-8.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
614
|
+
* re-loading from ptr-8.
|
|
615
|
+
* Optional `ptrLocal`: caller already has the resolved ARRAY data pointer in
|
|
616
|
+
* an i32 local. Reuses it instead of calling __ptr_offset again. */
|
|
617
|
+
export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
|
|
618
|
+
const arr = ptrLocal ? null : temp('aa'), ptr = ptrLocal ?? tempI32('ap'), i = tempI32('ai'), item = temp('av')
|
|
567
619
|
const len = lenLocal ?? tempI32('al')
|
|
568
620
|
const id = ctx.func.uniq++
|
|
569
|
-
const setup = [
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
621
|
+
const setup = []
|
|
622
|
+
if (!ptrLocal) {
|
|
623
|
+
inc('__ptr_offset')
|
|
624
|
+
setup.push(
|
|
625
|
+
['local.set', `$${arr}`, asF64(arrExpr)],
|
|
626
|
+
['local.set', `$${ptr}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]],
|
|
627
|
+
)
|
|
628
|
+
}
|
|
573
629
|
if (!lenLocal) setup.push(
|
|
574
630
|
['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]])
|
|
575
631
|
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
|
|
@@ -152,27 +267,30 @@ const handlers = {
|
|
|
152
267
|
}
|
|
153
268
|
},
|
|
154
269
|
|
|
155
|
-
// 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.
|
|
156
273
|
'function'(name, params, body) {
|
|
157
274
|
const [p2, b2] = lowerArguments(params, body)
|
|
158
275
|
const arrow = ['=>', p2, wrapArrowBody(b2)]
|
|
159
|
-
if (name) {
|
|
276
|
+
if (name) {
|
|
277
|
+
return ['()', ['()', ['=>', null, ['{}', [';',
|
|
278
|
+
['let', name],
|
|
279
|
+
['=', name, arrow],
|
|
280
|
+
['return', name]
|
|
281
|
+
]]]], null]
|
|
282
|
+
}
|
|
160
283
|
return arrow
|
|
161
284
|
},
|
|
162
285
|
|
|
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`.
|
|
163
289
|
'var'(...args) {
|
|
164
|
-
// for-in/for-of: ['var', ['in', 'k', obj]] → ['in', ['let', 'k'], obj]
|
|
165
|
-
if (args.length === 1 && Array.isArray(args[0]) && (args[0][0] === 'in' || args[0][0] === 'of')) {
|
|
166
|
-
const [, name, src] = args[0]
|
|
167
|
-
return [args[0][0], ['let', typeof name === 'string' ? name : transform(name)], transform(src)]
|
|
168
|
-
}
|
|
169
290
|
return ['let', ...args.map(transform)]
|
|
170
291
|
},
|
|
171
292
|
|
|
172
293
|
'='(lhs, rhs) {
|
|
173
|
-
// var assignment: ['=', ['var', name], init] → let
|
|
174
|
-
if (Array.isArray(lhs) && lhs[0] === 'var')
|
|
175
|
-
return ['let', ['=', lhs[1], transform(rhs)]]
|
|
176
294
|
// Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
|
|
177
295
|
if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
|
|
178
296
|
const targets = []
|
|
@@ -237,10 +355,16 @@ const handlers = {
|
|
|
237
355
|
// Block body: recurse as scope for hoisting
|
|
238
356
|
'{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
|
|
239
357
|
|
|
240
|
-
// 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.
|
|
241
362
|
'export'(inner) {
|
|
363
|
+
if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
|
|
364
|
+
return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
|
|
365
|
+
}
|
|
242
366
|
if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
|
|
243
|
-
const decl =
|
|
367
|
+
const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
|
|
244
368
|
return [';', decl, ['export', ['default', inner[1][1]]]]
|
|
245
369
|
}
|
|
246
370
|
return ['export', transform(inner)]
|
|
@@ -311,8 +435,26 @@ export function codegen(node, depth = 0) {
|
|
|
311
435
|
// Statements
|
|
312
436
|
if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
|
|
313
437
|
if (op === '{}') {
|
|
314
|
-
|
|
315
|
-
|
|
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 + '}'
|
|
316
458
|
}
|
|
317
459
|
|
|
318
460
|
// Declarations
|
|
@@ -329,16 +471,20 @@ export function codegen(node, depth = 0) {
|
|
|
329
471
|
}
|
|
330
472
|
if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
|
|
331
473
|
if (op === 'for') {
|
|
332
|
-
if (a.length === 2) { // for
|
|
474
|
+
if (a.length === 2) { // ['for', head, body] — subscript shape
|
|
333
475
|
const [head, body] = a
|
|
334
476
|
if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
|
|
335
|
-
return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' +
|
|
477
|
+
return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + wrapBlock(body, depth)
|
|
336
478
|
// ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
|
|
337
479
|
if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
|
|
338
|
-
return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' +
|
|
339
|
-
|
|
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)
|
|
340
486
|
}
|
|
341
|
-
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)
|
|
342
488
|
}
|
|
343
489
|
if (op === 'return') return 'return ' + codegen(a[0])
|
|
344
490
|
if (op === 'throw') return 'throw ' + codegen(a[0])
|
|
@@ -372,10 +518,20 @@ export function codegen(node, depth = 0) {
|
|
|
372
518
|
// Property access
|
|
373
519
|
if (op === '.') return codegen(a[0]) + '.' + a[1]
|
|
374
520
|
if (op === '?.') return codegen(a[0]) + '?.' + a[1]
|
|
375
|
-
if (op === '[]') return codegen(a[0]) + '[' + codegen(a[1]) + ']'
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
535
|
if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
|
|
380
536
|
if (op === 'str') return JSON.stringify(a[0])
|
|
381
537
|
if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
|
|
@@ -391,9 +547,10 @@ export function codegen(node, depth = 0) {
|
|
|
391
547
|
// Spread
|
|
392
548
|
if (op === '...') return '...' + codegen(a[0])
|
|
393
549
|
|
|
394
|
-
// Import
|
|
550
|
+
// Import / export rename
|
|
395
551
|
if (op === 'import') return 'import ' + codegen(a[0])
|
|
396
552
|
if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
|
|
553
|
+
if (op === 'as') return codegen(a[0]) + ' as ' + codegen(a[1])
|
|
397
554
|
|
|
398
555
|
// Unary prefix
|
|
399
556
|
if (a.length === 1) {
|
package/src/narrow.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ctx } from './ctx.js'
|
|
11
|
+
import { isLiteralStr } from './ir.js'
|
|
11
12
|
import {
|
|
12
13
|
VAL,
|
|
13
14
|
analyzeBody, analyzeLocals,
|
|
@@ -307,14 +308,41 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
const poison = field => r => { r[field] = null }
|
|
311
|
+
// Default-aware val inference. Adds two fallbacks beyond inferArgType's
|
|
312
|
+
// body-local `callerValTypes` lookup so a hot recursive helper like
|
|
313
|
+
// `uleb(n, buffer = []) { ... return uleb(n, buffer) }` resolves the
|
|
314
|
+
// recursive `buffer` arg to VAL.ARRAY (via callerParamFacts on iter 2,
|
|
315
|
+
// or via the caller's own default expression on iter 1).
|
|
316
|
+
const inferValAtSite = (arg, state) => {
|
|
317
|
+
const v = inferArgType(arg, state.callerValTypes)
|
|
318
|
+
if (v != null) return v
|
|
319
|
+
if (typeof arg !== 'string') return null
|
|
320
|
+
const fromParam = state.callerParamFacts('val')?.get(arg)
|
|
321
|
+
if (fromParam != null) return fromParam
|
|
322
|
+
const def = state.callerFunc?.defaults?.[arg]
|
|
323
|
+
return def != null ? valTypeOf(def) || null : null
|
|
324
|
+
}
|
|
325
|
+
// Substitute the default expression for a missing positional arg, so
|
|
326
|
+
// `uleb(n)` doesn't poison buffer.val despite `buffer = []` provably
|
|
327
|
+
// yielding VAL.ARRAY at runtime — unblocks inline ARRAY len/push fast
|
|
328
|
+
// paths in encode.js's hot uleb/i32/i64 helpers.
|
|
329
|
+
const defaultArg = (state, k) => {
|
|
330
|
+
const pname = state.func.sig.params[k]?.name
|
|
331
|
+
return pname != null ? state.func.defaults?.[pname] : null
|
|
332
|
+
}
|
|
310
333
|
const mergeRule = (field, infer) => ({
|
|
311
|
-
missing
|
|
334
|
+
missing(r, k, state) {
|
|
335
|
+
if (r[field] === null) return
|
|
336
|
+
const def = defaultArg(state, k)
|
|
337
|
+
if (def != null) mergeParamFact(r, field, infer(def, k, state))
|
|
338
|
+
else r[field] = null
|
|
339
|
+
},
|
|
312
340
|
apply(r, arg, k, state) {
|
|
313
341
|
if (r[field] !== null) mergeParamFact(r, field, infer(arg, k, state))
|
|
314
342
|
},
|
|
315
343
|
})
|
|
316
344
|
const runFixpoint = () => runCallsiteLattice([
|
|
317
|
-
mergeRule('val', (arg, _k, state) =>
|
|
345
|
+
mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state)),
|
|
318
346
|
{
|
|
319
347
|
missing: poison('wasm'),
|
|
320
348
|
apply(r, arg, _k, state) {
|
|
@@ -856,7 +884,6 @@ const TYPED_ARRAY_CTOR = /^(Float|Int|Uint|BigInt|BigUint)(8|16|32|64)(Clamped)?
|
|
|
856
884
|
export function refineDynKeys(programFacts) {
|
|
857
885
|
if (!ctx.types.anyDynKey) return
|
|
858
886
|
const { paramReps, valueUsed } = programFacts
|
|
859
|
-
const isLitStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
860
887
|
|
|
861
888
|
// Per-function type map: param vtypes from paramReps, plus locals
|
|
862
889
|
// we can prove are typed arrays from `let v = new TypedArray(...)`. After
|
|
@@ -882,6 +909,7 @@ export function refineDynKeys(programFacts) {
|
|
|
882
909
|
if (Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string' && init[1].startsWith('new.'))
|
|
883
910
|
ctor = init[1].slice(4)
|
|
884
911
|
if (ctor && TYPED_ARRAY_CTOR.test(ctor)) map.set(d[1], VAL.TYPED)
|
|
912
|
+
else if (Array.isArray(init) && init[0] === '[') map.set(d[1], VAL.ARRAY)
|
|
885
913
|
else if (typeof init === 'string' && map.has(init)) map.set(d[1], map.get(init))
|
|
886
914
|
}
|
|
887
915
|
}
|
|
@@ -898,7 +926,7 @@ export function refineDynKeys(programFacts) {
|
|
|
898
926
|
const op = node[0]
|
|
899
927
|
if (op === '[]') {
|
|
900
928
|
const idx = node[2]
|
|
901
|
-
if (!
|
|
929
|
+
if (!isLiteralStr(idx)) {
|
|
902
930
|
const obj = node[1]
|
|
903
931
|
const vt = typeof obj === 'string' ? typeMap.get(obj) : null
|
|
904
932
|
if (!NON_DYN_VTS.has(vt)) real = true
|