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/ir.js
CHANGED
|
@@ -21,7 +21,19 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { ctx, err, inc, PTR, LAYOUT } from './ctx.js'
|
|
24
|
-
import { T, VAL, valTypeOf, lookupValType, repOf, repOfGlobal } from './analyze.js'
|
|
24
|
+
import { T, VAL, valTypeOf, lookupValType, repOf, repOfGlobal, objLiteralSchemaId } from './analyze.js'
|
|
25
|
+
|
|
26
|
+
// === Numeric range ===
|
|
27
|
+
|
|
28
|
+
/** Signed-32-bit range. Used everywhere a number value must round-trip through
|
|
29
|
+
* wasm `i32` (literal constants, default-arg folding, exprType inference). */
|
|
30
|
+
export const I32_MIN = -2147483648
|
|
31
|
+
export const I32_MAX = 2147483647
|
|
32
|
+
|
|
33
|
+
/** True when `v` is a finite integer that fits in i32 *and* isn't -0 (which i32
|
|
34
|
+
* cannot represent). Callers that don't care about -0 can compare against
|
|
35
|
+
* I32_MIN/I32_MAX directly. */
|
|
36
|
+
export const isI32 = (v) => Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX && !Object.is(v, -0)
|
|
25
37
|
|
|
26
38
|
// === Type helpers ===
|
|
27
39
|
|
|
@@ -52,8 +64,17 @@ export const asF64 = n => {
|
|
|
52
64
|
if (n == null) err(`compiler internal: expected emitted IR value in ${ctx.func.current?.name || '<module>'}, got empty value`)
|
|
53
65
|
if (n.ptrKind != null) return boxPtrIR(n, valKindToPtr(n.ptrKind), n.ptrAux || 0)
|
|
54
66
|
if (n.type === 'f64') return n
|
|
55
|
-
if (n.type === 'i64')
|
|
56
|
-
|
|
67
|
+
if (n.type === 'i64') {
|
|
68
|
+
// Cancel the reinterpret round-trip at construction: reinterpret is bit-preserving
|
|
69
|
+
// both ways, so f64.reinterpret_i64(i64.reinterpret_f64(X)) === X. Folding here keeps
|
|
70
|
+
// the pair out of the IR entirely (smaller tree for every downstream pass) instead of
|
|
71
|
+
// letting fusedRewrite untangle it post-emit.
|
|
72
|
+
if (Array.isArray(n) && n[0] === 'i64.reinterpret_f64' && Array.isArray(n[1])) return typed(n[1], 'f64')
|
|
73
|
+
return typed(['f64.reinterpret_i64', n], 'f64')
|
|
74
|
+
}
|
|
75
|
+
// A `.unsigned` const carries its uint32 value as a signed i32 bit pattern, so
|
|
76
|
+
// widen via `>>> 0` (e.g. -1 → 4294967295); a plain const copies through verbatim.
|
|
77
|
+
if (n[0] === 'i32.const' && typeof n[1] === 'number') return typed(['f64.const', n.unsigned ? n[1] >>> 0 : n[1]], 'f64')
|
|
57
78
|
return typed([n.unsigned ? 'f64.convert_i32_u' : 'f64.convert_i32_s', n], 'f64')
|
|
58
79
|
}
|
|
59
80
|
|
|
@@ -72,8 +93,13 @@ export const asI32 = n => {
|
|
|
72
93
|
/** Coerce node to i32 offset for a ptr-narrowed return / store. Same-kind unboxed
|
|
73
94
|
* ptr passes through; otherwise extract low 32 bits from the NaN-boxed f64
|
|
74
95
|
* (NOT trunc — that would convert numerically). */
|
|
75
|
-
export const asPtrOffset = (n, ptrKind) =>
|
|
76
|
-
n.ptrKind === ptrKind
|
|
96
|
+
export const asPtrOffset = (n, ptrKind) => {
|
|
97
|
+
if (n.ptrKind === ptrKind) return n
|
|
98
|
+
const f = asF64(n)
|
|
99
|
+
// Peel the inner reinterpret round-trip before wrapping: i64.reinterpret_f64(f64.reinterpret_i64(Y)) === Y.
|
|
100
|
+
const bits = Array.isArray(f) && f[0] === 'f64.reinterpret_i64' && Array.isArray(f[1]) ? f[1] : ['i64.reinterpret_f64', f]
|
|
101
|
+
return typed(['i32.wrap_i64', bits], 'i32')
|
|
102
|
+
}
|
|
77
103
|
|
|
78
104
|
/** Coerce emitted IR to a target WASM param type ('i32' | 'i64' | 'f64'). */
|
|
79
105
|
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : asF64(n)
|
|
@@ -116,30 +142,71 @@ export const toI32 = n => {
|
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
/** Extract i64 from BigInt-as-f64. */
|
|
119
|
-
export const asI64 = n =>
|
|
145
|
+
export const asI64 = n => {
|
|
146
|
+
const f = asF64(n)
|
|
147
|
+
// Cancel reinterpret round-trip: i64.reinterpret_f64(f64.reinterpret_i64(Y)) === Y.
|
|
148
|
+
if (Array.isArray(f) && f[0] === 'f64.reinterpret_i64' && Array.isArray(f[1])) return typed(f[1], 'i64')
|
|
149
|
+
return typed(['i64.reinterpret_f64', f], 'i64')
|
|
150
|
+
}
|
|
120
151
|
|
|
121
152
|
/** Wrap i64 result back to BigInt-as-f64. */
|
|
122
|
-
export const fromI64 = n =>
|
|
153
|
+
export const fromI64 = n => {
|
|
154
|
+
// Cancel reinterpret round-trip: f64.reinterpret_i64(i64.reinterpret_f64(X)) === X.
|
|
155
|
+
if (Array.isArray(n) && n[0] === 'i64.reinterpret_f64' && Array.isArray(n[1])) return typed(n[1], 'f64')
|
|
156
|
+
return typed(['f64.reinterpret_i64', n], 'f64')
|
|
157
|
+
}
|
|
123
158
|
|
|
124
159
|
// === Nullish sentinels ===
|
|
125
160
|
|
|
126
161
|
/** Reserved atoms (PTR.ATOM tag, offset=0).
|
|
127
162
|
* aux=1 → null (NULL_NAN)
|
|
128
163
|
* aux=2 → undefined (UNDEF_NAN)
|
|
164
|
+
* aux=4 → false (FALSE_NAN)
|
|
165
|
+
* aux=5 → true (TRUE_NAN)
|
|
129
166
|
* See module/symbol.js for the broader reserved-atom-id scheme.
|
|
130
167
|
* Distinct from 0, NaN, and all pointers. Triggers default params.
|
|
131
168
|
* At the JS boundary, null and undefined preserve their identity for interop. */
|
|
132
169
|
export const NULL_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (1n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
133
170
|
export const UNDEF_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (2n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
171
|
+
/** Boxed-boolean carrier. `false`/`true` are reserved atoms — materialized only
|
|
172
|
+
* where boolean identity is observed (typeof/String/JSON/host boundary); in
|
|
173
|
+
* branch/arithmetic position booleans stay raw i32/f64 0/1. The atomId encodes
|
|
174
|
+
* the truth value in its low bit (4=false, 5=true), so `aux & 1` recovers 0/1
|
|
175
|
+
* and `4 | bit` boxes it — see boolBoxIR / unboxBoolIR. */
|
|
176
|
+
export const BOOL_ATOM_BASE = 4
|
|
177
|
+
export const FALSE_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (4n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
178
|
+
export const TRUE_NAN = '0x' + (LAYOUT.NAN_PREFIX_BITS | (5n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
134
179
|
/** WAT-template-ready sentinel expressions for use in stdlib template strings.
|
|
135
180
|
* `f64.const nan:0xHEX` is 3 bytes shorter than `f64.reinterpret_i64 (i64.const ...)`. */
|
|
136
181
|
export const NULL_WAT = `(f64.const nan:${NULL_NAN})`
|
|
137
182
|
export const UNDEF_WAT = `(f64.const nan:${UNDEF_NAN})`
|
|
138
183
|
export const NULL_IR = ['f64.const', `nan:${NULL_NAN}`]
|
|
139
184
|
export const UNDEF_IR = ['f64.const', `nan:${UNDEF_NAN}`]
|
|
185
|
+
export const FALSE_IR = ['f64.const', `nan:${FALSE_NAN}`]
|
|
186
|
+
export const TRUE_IR = ['f64.const', `nan:${TRUE_NAN}`]
|
|
140
187
|
export const nullExpr = () => typed(NULL_IR, 'f64')
|
|
141
188
|
export const undefExpr = () => typed(UNDEF_IR.slice(), 'f64')
|
|
142
189
|
|
|
190
|
+
/** Materialize the boxed-boolean carrier from a 0/1-valued expression. The atom
|
|
191
|
+
* is `BOOL_ATOM_BASE | bit`, so boxing is one `i32.or` then an ATOM mkptr; when
|
|
192
|
+
* the input folds to a constant 0/1 we emit the `f64.const nan:` literal directly.
|
|
193
|
+
* Used only at observation/escape sites — never in branch or arithmetic position. */
|
|
194
|
+
export function boolBoxIR(e) {
|
|
195
|
+
const i = truthyIR(e)
|
|
196
|
+
if (Array.isArray(i) && i[0] === 'i32.const') return typed((i[1] ? TRUE_IR : FALSE_IR).slice(), 'f64')
|
|
197
|
+
return mkPtrIR(['i32.const', PTR.ATOM], ['i32.or', ['i32.const', BOOL_ATOM_BASE], i], ['i32.const', 0])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Recover the 0/1 i32 value of a known boxed-boolean f64 expression: `aux & 1`. */
|
|
201
|
+
export function unboxBoolIR(f64expr) {
|
|
202
|
+
if (Array.isArray(f64expr) && f64expr[0] === 'f64.const') {
|
|
203
|
+
const bits = typeof f64expr[1] === 'string' ? f64expr[1].replace(/^nan:/, '') : null
|
|
204
|
+
if (bits === TRUE_NAN) return typed(['i32.const', 1], 'i32')
|
|
205
|
+
if (bits === FALSE_NAN) return typed(['i32.const', 0], 'i32')
|
|
206
|
+
}
|
|
207
|
+
return typed(['i32.and', ['i32.wrap_i64', ['i64.shr_u', ['i64.reinterpret_f64', f64expr], ['i64.const', String(LAYOUT.AUX_SHIFT)]]], ['i32.const', 1]], 'i32')
|
|
208
|
+
}
|
|
209
|
+
|
|
143
210
|
// === Constants ===
|
|
144
211
|
|
|
145
212
|
/** Max arity of inline closure slots. Closures are compiled with signature
|
|
@@ -149,6 +216,7 @@ export const undefExpr = () => typed(UNDEF_IR.slice(), 'f64')
|
|
|
149
216
|
export const MAX_CLOSURE_ARITY = 8
|
|
150
217
|
|
|
151
218
|
/** Matches WASM instructions that require a memory section. */
|
|
219
|
+
// FIXME: can be simpler regex, just test second part - load vs store vs grow vs size vs memory
|
|
152
220
|
export const MEM_OPS = /\b(i32\.load|i32\.store|f64\.load|f64\.store|f32\.load|f32\.store|i64\.load|i64\.store|memory\.size|memory\.grow|i32\.load8|i32\.load16|i32\.store8|i32\.store16)\b/
|
|
153
221
|
|
|
154
222
|
export const WASM_OPS = new Set(['block','loop','if','then','else','br','br_if','call','call_indirect','return','return_call','throw','try_table','catch','nop','drop','unreachable','select','result','mut','param','func','module','memory','table','elem','data','type','import','export','local','global','ref'])
|
|
@@ -295,14 +363,14 @@ export const isPostfix = (a, op, b) => Array.isArray(a) && a[0] === op && Array.
|
|
|
295
363
|
|
|
296
364
|
/** Emit a numeric constant with correct i32/f64 typing.
|
|
297
365
|
* `-0` is f64-only (i32 has no signed zero) — preserve the sign by emitting f64. */
|
|
298
|
-
export const emitNum = v =>
|
|
366
|
+
export const emitNum = v => isI32(v)
|
|
299
367
|
? typed(['i32.const', v], 'i32') : typed(['f64.const', v], 'f64')
|
|
300
368
|
|
|
301
369
|
// === Temp locals ===
|
|
302
370
|
|
|
303
371
|
/** Allocate a temp local, returns name without $. Optional tag aids WAT readability.
|
|
304
|
-
* Skips names already registered (by
|
|
305
|
-
* to avoid collisions that would silently override the pre-analyzed type. */
|
|
372
|
+
* Skips names already registered (by analyzeBody().locals from prepare-generated
|
|
373
|
+
* names) to avoid collisions that would silently override the pre-analyzed type. */
|
|
306
374
|
export function temp(tag = '') {
|
|
307
375
|
let name
|
|
308
376
|
do { name = `${T}${tag}${ctx.func.uniq++}` } while (ctx.func.locals.has(name))
|
|
@@ -339,12 +407,58 @@ export const f64rem = (a, b) => {
|
|
|
339
407
|
['f64.sub', ga, ['f64.mul', ['f64.trunc', ['f64.div', ga, gb]], gb]]], 'f64')
|
|
340
408
|
}
|
|
341
409
|
|
|
410
|
+
/** Resolve the slot index of a ToPrimitive method (`valueOf`/`toString`) on an
|
|
411
|
+
* OBJECT operand — from a schema-bound variable or an inline object literal.
|
|
412
|
+
* Returns -1 when the method is absent. */
|
|
413
|
+
function primMethodIdx(node, name) {
|
|
414
|
+
if (typeof node === 'string') return ctx.schema.find(node, name)
|
|
415
|
+
const sid = objLiteralSchemaId(node)
|
|
416
|
+
const props = sid != null ? ctx.schema.list[sid] : null
|
|
417
|
+
return props ? props.indexOf(name) : -1
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Emit the ES `OrdinaryToPrimitive` method-fallback chain for an OBJECT operand,
|
|
421
|
+
* returning an i64 IR node holding the resulting primitive — or null when the
|
|
422
|
+
* object exposes none of the hinted methods. `order` is the method-try order
|
|
423
|
+
* (number hint → [valueOf,toString]; string hint → [toString,valueOf]). Each
|
|
424
|
+
* present method is called in turn: a primitive result short-circuits out, a
|
|
425
|
+
* non-primitive (object) result falls through to the next method, and if every
|
|
426
|
+
* method yields a non-primitive a TypeError is thrown — the spec algorithm. */
|
|
427
|
+
function toPrimitiveChain(node, v, order) {
|
|
428
|
+
const present = order.map(name => primMethodIdx(node, name)).filter(i => i >= 0)
|
|
429
|
+
if (!present.length) return null
|
|
430
|
+
ctx.runtime.throws = true
|
|
431
|
+
inc('__is_object')
|
|
432
|
+
const blk = `$tp${ctx.func.uniq++}`
|
|
433
|
+
const prim = tempI64('prim')
|
|
434
|
+
const optr = tempI32('op')
|
|
435
|
+
// Resolve the object's data pointer once — `v` may carry side effects and is
|
|
436
|
+
// referenced once per method slot below.
|
|
437
|
+
const body = [['result', 'i64'],
|
|
438
|
+
['local.set', `$${optr}`, ptrOffsetIR(v, VAL.OBJECT)]]
|
|
439
|
+
for (const idx of present) {
|
|
440
|
+
const method = typed(ctx.abi.object.ops.load(['local.get', `$${optr}`], idx), 'f64')
|
|
441
|
+
body.push(
|
|
442
|
+
['local.set', `$${prim}`, asI64(ctx.closure.call(method, []))],
|
|
443
|
+
['br_if', blk, ['local.get', `$${prim}`],
|
|
444
|
+
['i32.eqz', ['call', '$__is_object', ['local.get', `$${prim}`]]]])
|
|
445
|
+
}
|
|
446
|
+
// Every method returned a non-primitive — `Cannot convert object to primitive`.
|
|
447
|
+
body.push(['throw', '$__jz_err', ['f64.const', 0]])
|
|
448
|
+
return typed(['block', blk, ...body], 'i64')
|
|
449
|
+
}
|
|
450
|
+
|
|
342
451
|
/** Coerce an emitted IR value to a plain f64 Number per JS `ToNumber`.
|
|
343
452
|
* Skips coercion when static type proves the value is already numeric
|
|
344
|
-
* (i32 node, compile-time literal, known VAL.NUMBER/VAL.BIGINT)
|
|
345
|
-
* __to_num
|
|
453
|
+
* (i32 node, compile-time literal, known VAL.NUMBER/VAL.BIGINT). When the full
|
|
454
|
+
* string-parsing `__to_num` isn't loaded (no string module → no strings can
|
|
455
|
+
* exist) nullish *literals* still fold statically (null→+0, undefined→NaN);
|
|
456
|
+
* non-literal values pass through uncoerced. */
|
|
346
457
|
export function toNumF64(node, v) {
|
|
347
|
-
|
|
458
|
+
// An i32 node carrying `.ptrKind` is an *unboxed pointer* (object/array local),
|
|
459
|
+
// not a number — skipping coercion would reinterpret pointer bits as an f64.
|
|
460
|
+
// Only a plain i32 (loop counter, `x|0`) is genuinely already-numeric.
|
|
461
|
+
if ((v.type === 'i32' && v.ptrKind == null) || isLit(v)) return asF64(v)
|
|
348
462
|
const vt = keyValType(node)
|
|
349
463
|
if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
|
|
350
464
|
if (vt === VAL.DATE) {
|
|
@@ -353,20 +467,76 @@ export function toNumF64(node, v) {
|
|
|
353
467
|
: ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
|
|
354
468
|
return typed(['f64.load', ptr], 'f64')
|
|
355
469
|
}
|
|
470
|
+
// ToPrimitive (number hint): an OBJECT operand coerces through the
|
|
471
|
+
// `OrdinaryToPrimitive` method chain [valueOf, toString] — `valueOf` is tried
|
|
472
|
+
// first, and when it yields a non-primitive `toString` is tried; if both
|
|
473
|
+
// yield non-primitives a TypeError is thrown. The chosen primitive still
|
|
474
|
+
// flows through `__to_num` so a string return ("−7") is parsed. An abrupt
|
|
475
|
+
// completion (throwing method) propagates through the closure call.
|
|
476
|
+
if (vt === VAL.OBJECT && ctx.closure.call && ctx.schema.find) {
|
|
477
|
+
const prim = toPrimitiveChain(node, v, ['valueOf', 'toString'])
|
|
478
|
+
if (prim) {
|
|
479
|
+
// No `__to_num` helper → the program provably has no strings, so the
|
|
480
|
+
// primitive is a non-string value already usable as an f64.
|
|
481
|
+
if (!ctx.core.stdlib['__to_num']) return asF64(prim)
|
|
482
|
+
inc('__to_num')
|
|
483
|
+
return typed(['call', '$__to_num', prim], 'f64')
|
|
484
|
+
}
|
|
485
|
+
}
|
|
356
486
|
// intCertain locals: every reachable def is integer-valued, so the binding
|
|
357
487
|
// never carries a NaN-boxed pointer — skip the __to_num wrapper.
|
|
358
488
|
if (typeof node === 'string' && repOf(node)?.intCertain === true) return asF64(v)
|
|
489
|
+
// intCertain schema slot reads `o.x`: every observed write is integer-shaped,
|
|
490
|
+
// so the loaded f64 is a plain number — same justification as the local case.
|
|
491
|
+
if (Array.isArray(node) && node[0] === '.' && typeof node[1] === 'string' && typeof node[2] === 'string') {
|
|
492
|
+
if (ctx.schema.slotIntCertainAt?.(node[1], node[2]) === true) return asF64(v)
|
|
493
|
+
}
|
|
359
494
|
// IR-level shapes that produce real f64 numbers (never NaN-boxed pointers):
|
|
360
495
|
// i32→f64 conversions, stdlib clock helper. Skip the __to_num call wrapper.
|
|
361
496
|
if (Array.isArray(v)) {
|
|
362
497
|
if (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u') return v
|
|
363
498
|
if (v[0] === 'call' && v[1] === '$__time_ms') return v
|
|
364
499
|
}
|
|
365
|
-
if (!ctx.core.stdlib['__to_num'])
|
|
500
|
+
if (!ctx.core.stdlib['__to_num']) {
|
|
501
|
+
// No full ToNumber helper loaded — the program provably has no strings.
|
|
502
|
+
// A nullish *literal* still coerces (null→+0, undefined→NaN) — fold it
|
|
503
|
+
// statically so `Math.log10(null)` & friends are correct at zero cost.
|
|
504
|
+
// Non-literal values fall through to `asF64`: an untyped runtime value
|
|
505
|
+
// *could* be a nullish sentinel, but blanket per-use coercion taxes every
|
|
506
|
+
// numeric kernel (fib, math loops) — nullable-param coercion belongs once
|
|
507
|
+
// at the function boundary (null-flow inference), not at each use site.
|
|
508
|
+
const f = asF64(v)
|
|
509
|
+
if (Array.isArray(f) && f[0] === 'f64.const' && typeof f[1] === 'string') {
|
|
510
|
+
const lit = f[1]
|
|
511
|
+
if (lit.startsWith('nan:')) // NaN-boxed sentinel/pointer
|
|
512
|
+
return typed(['f64.const', lit.slice(4) === NULL_NAN ? 0 : 'nan'], 'f64')
|
|
513
|
+
}
|
|
514
|
+
return f
|
|
515
|
+
}
|
|
366
516
|
inc('__to_num')
|
|
367
517
|
return typed(['call', '$__to_num', asI64(v)], 'f64')
|
|
368
518
|
}
|
|
369
519
|
|
|
520
|
+
/** Coerce an emitted IR value to a jz string per JS `ToString`, returning an
|
|
521
|
+
* i64 string value. The mirror of `toNumF64` for the string hint: an OBJECT
|
|
522
|
+
* operand coerces through `OrdinaryToPrimitive(string)` — method chain
|
|
523
|
+
* [toString, valueOf], `toString` first with fallback to `valueOf`, TypeError
|
|
524
|
+
* if both yield non-primitives. The chosen primitive still flows through
|
|
525
|
+
* `__to_str` so a numeric return is rendered. A throwing method propagates as
|
|
526
|
+
* an abrupt completion through the closure call. */
|
|
527
|
+
export function toStrI64(node, v) {
|
|
528
|
+
const vt = keyValType(node)
|
|
529
|
+
if (vt === VAL.OBJECT && ctx.closure.call && ctx.schema.find) {
|
|
530
|
+
const prim = toPrimitiveChain(node, v, ['toString', 'valueOf'])
|
|
531
|
+
if (prim) {
|
|
532
|
+
inc('__to_str')
|
|
533
|
+
return typed(['call', '$__to_str', prim], 'i64')
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
inc('__to_str')
|
|
537
|
+
return typed(['call', '$__to_str', asI64(v)], 'i64')
|
|
538
|
+
}
|
|
539
|
+
|
|
370
540
|
/** Convert already-emitted WASM node to i32 boolean. NaN is falsy (like JS).
|
|
371
541
|
* Peepholes: i32 → as-is; `f64.convert_i32_*(x)` → x (i32 conversion never NaN);
|
|
372
542
|
* nested `__is_truthy(x)` → x (already 0/1); literal f64 const folds to 0/1. */
|
|
@@ -382,16 +552,24 @@ export function truthyIR(e) {
|
|
|
382
552
|
if (e[0] === 'f64.const' && typeof e[1] === 'number') {
|
|
383
553
|
return typed(['i32.const', (e[1] !== 0 && !Number.isNaN(e[1])) ? 1 : 0], 'i32')
|
|
384
554
|
}
|
|
555
|
+
// Fold NaN-boxed sentinel literals in `f64.const nan:0x...` form (boolean
|
|
556
|
+
// atoms, null/undefined): TRUE → 1, everything else nullish/false → 0.
|
|
557
|
+
if (e[0] === 'f64.const' && typeof e[1] === 'string' && e[1].startsWith('nan:')) {
|
|
558
|
+
const bits = e[1].slice(4)
|
|
559
|
+
if (bits === TRUE_NAN) return typed(['i32.const', 1], 'i32')
|
|
560
|
+
if (bits === FALSE_NAN || bits === UNDEF_NAN || bits === NULL_NAN) return typed(['i32.const', 0], 'i32')
|
|
561
|
+
}
|
|
385
562
|
// Fold NaN-boxed pointer literals: UNDEF/NULL/canonical-NaN sentinels are falsy;
|
|
386
563
|
// all other NaN-boxed pointers (SSO strings, heap ptrs, etc.) are truthy.
|
|
387
564
|
if (e[0] === 'f64.reinterpret_i64' && Array.isArray(e[1]) && e[1][0] === 'i64.const') {
|
|
388
565
|
const bits = String(e[1][1])
|
|
389
|
-
const FALSY = new Set([UNDEF_NAN, NULL_NAN, '0x7FF8000000000000', '0x7FFA400000000000'])
|
|
566
|
+
const FALSY = new Set([UNDEF_NAN, NULL_NAN, FALSE_NAN, '0x7FF8000000000000', '0x7FFA400000000000'])
|
|
390
567
|
return typed(['i32.const', FALSY.has(bits) ? 0 : 1], 'i32')
|
|
391
568
|
}
|
|
392
569
|
// Fresh pointer constructors never produce nullish. Treat as always truthy.
|
|
393
570
|
if (e[0] === 'call' && typeof e[1] === 'string' &&
|
|
394
|
-
(e[1].startsWith('$__mkptr') || e[1] === '$__alloc' ||
|
|
571
|
+
(e[1].startsWith('$__mkptr') || e[1] === '$__alloc' ||
|
|
572
|
+
e[1] === '$__alloc_hdr' || e[1].startsWith('$__alloc_hdr_'))) {
|
|
395
573
|
return typed(['i32.const', 1], 'i32')
|
|
396
574
|
}
|
|
397
575
|
// Pointer-typed local reads: value is never a plain number — truthy iff not nullish.
|
|
@@ -427,7 +605,7 @@ export function needsDynShadow(target) {
|
|
|
427
605
|
if (!ctx.module.modules.collection) return false
|
|
428
606
|
// Functions/CLOSURE always need dynamic props so cross-module property
|
|
429
607
|
// access (fn.parse, i32.parse aliases) sees the same value as schema slots.
|
|
430
|
-
const vt = typeof target === 'string' ? (ctx.func.
|
|
608
|
+
const vt = typeof target === 'string' ? (ctx.func.localReps?.get(target)?.val || ctx.scope.globalValTypes?.get(target)) : null
|
|
431
609
|
if (vt === 'closure' || usesDynProps(vt)) return true
|
|
432
610
|
const dyn = ctx.types?.dynKeyVars
|
|
433
611
|
if (target == null) return ctx.types?.anyDynKey ?? false
|
|
@@ -480,6 +658,11 @@ export function readVar(name) {
|
|
|
480
658
|
: typed(['f64.const', rep.intConst], 'f64')
|
|
481
659
|
}
|
|
482
660
|
const node = typed(['local.get', `$${name}`], t)
|
|
661
|
+
// Proven uint32 accumulator local (narrowUint32): a later asF64 must widen with
|
|
662
|
+
// convert_i32_u (the i32 bit pattern is an unsigned value), not _s. `.wrapSafe`
|
|
663
|
+
// marks it as the always-ToUint32-sunk kind so the arithmetic widening guards
|
|
664
|
+
// keep it on the i32 path — wrapping is its intended semantics, not a leak.
|
|
665
|
+
if (t === 'i32' && rep?.unsigned) { node.unsigned = true; node.wrapSafe = true }
|
|
483
666
|
if (rep?.ptrKind != null) {
|
|
484
667
|
node.ptrKind = rep.ptrKind
|
|
485
668
|
const aux = rep.ptrAux ?? ctx.schema.idOf?.(name)
|
|
@@ -530,7 +713,7 @@ export function writeVar(name, valIR, void_) {
|
|
|
530
713
|
|
|
531
714
|
/** Check if f64 expr is nullish (NULL_NAN or UNDEF_NAN). Returns i32.
|
|
532
715
|
* Peepholes: fold known NaN-boxed sentinel literals; elide on numeric literals;
|
|
533
|
-
* unboxed pointer locals are proven non-null by
|
|
716
|
+
* unboxed pointer locals are proven non-null by unboxablePtrs.
|
|
534
717
|
* Inlines directly: (i32.or (i64.eq bits NULL_NAN) (i64.eq bits UNDEF_NAN))
|
|
535
718
|
* rather than calling $__is_nullish — saves WASM call dispatch in V8 JIT. */
|
|
536
719
|
export const isNullish = (f64expr) => {
|
|
@@ -585,22 +768,38 @@ export const isUndef = (f64expr) => {
|
|
|
585
768
|
return typed(['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]], 'i32')
|
|
586
769
|
}
|
|
587
770
|
|
|
588
|
-
|
|
771
|
+
/** Mask that clears the boolean atom's truth bit, mapping TRUE_NAN→FALSE_NAN.
|
|
772
|
+
* `(bits & BOOL_ATOM_MASK) === FALSE_NAN` recognizes both in one i64.and+i64.eq. */
|
|
773
|
+
const BOOL_ATOM_MASK = '0x' + BigInt.asUintN(64, ~(1n << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
774
|
+
|
|
775
|
+
/** Check if f64 expr is a boxed-boolean atom (TRUE_NAN or FALSE_NAN). Returns i32.
|
|
776
|
+
* Single-eval: masks the truth bit and compares to FALSE_NAN once. */
|
|
777
|
+
export const isBoolAtom = (f64expr) => {
|
|
778
|
+
if (f64expr.ptrKind != null) return typed(['i32.const', 0], 'i32')
|
|
779
|
+
if (Array.isArray(f64expr) && f64expr[0] === 'f64.const' && String(f64expr[1]).startsWith('nan:')) {
|
|
780
|
+
const b = String(f64expr[1]).slice(4)
|
|
781
|
+
return typed(['i32.const', (b === TRUE_NAN || b === FALSE_NAN) ? 1 : 0], 'i32')
|
|
782
|
+
}
|
|
783
|
+
return typed(['i64.eq',
|
|
784
|
+
['i64.and', ['i64.reinterpret_f64', f64expr], ['i64.const', BOOL_ATOM_MASK]],
|
|
785
|
+
['i64.const', FALSE_NAN]], 'i32')
|
|
786
|
+
}
|
|
589
787
|
|
|
590
|
-
|
|
788
|
+
// === Array layout helpers — routed through the array carrier (abi/array.js) ===
|
|
789
|
+
|
|
790
|
+
/** Slot address: element `idx` off `baseLocal`. Constant idx folds the `*8`. */
|
|
591
791
|
export function slotAddr(baseLocal, idx) {
|
|
592
|
-
|
|
593
|
-
return idx === 0 ? base : ['i32.add', base, ['i32.const', idx * 8]]
|
|
792
|
+
return ctx.abi.array.ops.addr(['local.get', `$${baseLocal}`], idx)
|
|
594
793
|
}
|
|
595
794
|
|
|
596
795
|
/** Load f64 element from array data at ptr + i*8. ptr/i are local name strings. */
|
|
597
796
|
export function elemLoad(ptr, i) {
|
|
598
|
-
return
|
|
797
|
+
return ctx.abi.array.ops.load(['local.get', `$${ptr}`], ['local.get', `$${i}`])
|
|
599
798
|
}
|
|
600
799
|
|
|
601
800
|
/** Store f64 val at array data ptr + i*8. ptr/i are local name strings. */
|
|
602
801
|
export function elemStore(ptr, i, val) {
|
|
603
|
-
return
|
|
802
|
+
return ctx.abi.array.ops.store(['local.get', `$${ptr}`], ['local.get', `$${i}`], val)
|
|
604
803
|
}
|
|
605
804
|
|
|
606
805
|
/** Emit a loop iterating over array elements. Returns IR instruction list.
|
|
@@ -647,11 +846,17 @@ export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
|
|
|
647
846
|
* ptr — f64 IR expression: __mkptr(type, aux, local).
|
|
648
847
|
* Caller emits init, fills via local, then uses ptr (or local for further work). */
|
|
649
848
|
export function allocPtr({ type, aux = 0, len, cap, stride = 8, tag = 'ap' }) {
|
|
650
|
-
|
|
849
|
+
// stride=8 (f64 slots — Array/HASH/OBJECT) hits the specialized __alloc_hdr which
|
|
850
|
+
// hardcodes the multiply. Everything else (Set:16, Map probe:24, raw bytes:1) goes
|
|
851
|
+
// through the generic __alloc_hdr_n(len, cap, stride).
|
|
651
852
|
const local = tempI32(tag)
|
|
652
853
|
const irOf = v => typeof v === 'number' ? ['i32.const', v] : v
|
|
653
|
-
const
|
|
654
|
-
|
|
854
|
+
const args = [irOf(len), irOf(cap == null ? len : cap)]
|
|
855
|
+
let helper
|
|
856
|
+
if (stride === 8) helper = '__alloc_hdr'
|
|
857
|
+
else { helper = '__alloc_hdr_n'; args.push(['i32.const', stride]) }
|
|
858
|
+
inc(helper)
|
|
859
|
+
const init = ['local.set', `$${local}`, ['call', '$' + helper, ...args]]
|
|
655
860
|
const ptr = mkPtrIR(type, aux, ['local.get', `$${local}`])
|
|
656
861
|
return { local, init, ptr }
|
|
657
862
|
}
|
|
@@ -701,6 +906,76 @@ export function findBodyStart(fn) {
|
|
|
701
906
|
return fn.length
|
|
702
907
|
}
|
|
703
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Tail-call rewrite: walks tail positions of an emitted IR tree and replaces
|
|
911
|
+
* direct `(call $name args...)` ops with `(return_call $name args...)`.
|
|
912
|
+
*
|
|
913
|
+
* Tail positions, recursively from the IR root:
|
|
914
|
+
* - the root itself (function's terminal value-producing expression, or the
|
|
915
|
+
* emitted value of an explicit `return X`)
|
|
916
|
+
* - both arms of `(if (result T) cond (then ...) (else ...))`
|
|
917
|
+
* - last instruction of `(block (result T) ...)`
|
|
918
|
+
*
|
|
919
|
+
* Only fires when caller and callee result types match — if they didn't match,
|
|
920
|
+
* `asParamType`/`asPtrOffset` would have wrapped the call in a conversion op,
|
|
921
|
+
* pushing the `call` away from the tail position. We don't recurse into
|
|
922
|
+
* arithmetic / select / loop ops: their results aren't standalone-tail control
|
|
923
|
+
* transfers.
|
|
924
|
+
*
|
|
925
|
+
* Two callers:
|
|
926
|
+
* - `compile.js` runs it on the function's final value-producing IR to TCO
|
|
927
|
+
* expression-bodied arrows like `(n, acc) => n <= 0 ? acc : sum(n-1, acc+n)`
|
|
928
|
+
* where the AST has no `return` keyword.
|
|
929
|
+
* - `emit.js` `'return'` op handler runs it on the emitted return expression
|
|
930
|
+
* so explicit `return cond ? f(x) : g(x)` also gets deep tail rewriting.
|
|
931
|
+
*
|
|
932
|
+
* Returns the input unchanged when no transform applies.
|
|
933
|
+
*/
|
|
934
|
+
export const tcoTailRewrite = (ir, resultType) => {
|
|
935
|
+
if (ctx.transform.noTailCall || ctx.func.inTry) return ir
|
|
936
|
+
if (!Array.isArray(ir)) return ir
|
|
937
|
+
const op = ir[0]
|
|
938
|
+
if (op === 'call' && typeof ir[1] === 'string') {
|
|
939
|
+
// IR call name is `$name`; func.map keys are bare `name`.
|
|
940
|
+
const calleeName = ir[1].startsWith('$') ? ir[1].slice(1) : ir[1]
|
|
941
|
+
const callee = ctx.func.map.get(calleeName)
|
|
942
|
+
// If this is a known user func, verify result-type match. Otherwise
|
|
943
|
+
// (closures, imports, runtime helpers — not in `ctx.func.map`) trust the
|
|
944
|
+
// tail-position invariant: emit.js' asParamType/asPtrOffset already wrapped
|
|
945
|
+
// any mismatched call in a conversion op, so a bare `(call $X …)` at the
|
|
946
|
+
// tail of the function/if/block has by construction the same result type
|
|
947
|
+
// as the caller.
|
|
948
|
+
if (callee) {
|
|
949
|
+
if (callee.raw) return ir
|
|
950
|
+
const calleeRT = callee.sig?.results?.[0] ?? 'f64'
|
|
951
|
+
if (calleeRT !== resultType) return ir
|
|
952
|
+
}
|
|
953
|
+
return typed(['return_call', ...ir.slice(1)], resultType)
|
|
954
|
+
}
|
|
955
|
+
if (op === 'if' && Array.isArray(ir[1]) && ir[1][0] === 'result') {
|
|
956
|
+
let changed = false
|
|
957
|
+
const newIr = ir.slice()
|
|
958
|
+
for (let i = 3; i < newIr.length; i++) {
|
|
959
|
+
const arm = newIr[i]
|
|
960
|
+
if (Array.isArray(arm) && (arm[0] === 'then' || arm[0] === 'else') && arm.length > 1) {
|
|
961
|
+
const last = arm[arm.length - 1]
|
|
962
|
+
const rewritten = tcoTailRewrite(last, resultType)
|
|
963
|
+
if (rewritten !== last) {
|
|
964
|
+
newIr[i] = [...arm.slice(0, -1), rewritten]
|
|
965
|
+
changed = true
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return changed ? typed(newIr, ir.type) : ir
|
|
970
|
+
}
|
|
971
|
+
if (op === 'block' && ir.length > 1) {
|
|
972
|
+
const last = ir[ir.length - 1]
|
|
973
|
+
const rewritten = tcoTailRewrite(last, resultType)
|
|
974
|
+
if (rewritten !== last) return typed([...ir.slice(0, -1), rewritten], ir.type)
|
|
975
|
+
}
|
|
976
|
+
return ir
|
|
977
|
+
}
|
|
978
|
+
|
|
704
979
|
export function reconstructArgsWithSpreads(normal, spreads) {
|
|
705
980
|
const combined = []
|
|
706
981
|
let normalIdx = 0
|