jz 0.5.0 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +281 -142
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +461 -185
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +591 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +600 -205
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/ir.js
CHANGED
|
@@ -21,19 +21,14 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { ctx, err, inc, PTR, LAYOUT } from './ctx.js'
|
|
24
|
-
import {
|
|
24
|
+
import { ptrBoxPrefixBigInt, atomNanHex, nanPrefixHex } from '../layout.js'
|
|
25
|
+
import { I32_MIN, I32_MAX, isI32, isLiteralStr, isFuncRef } from './ast.js'
|
|
26
|
+
import { VAL, lookupValType, repOf, repOfGlobal } from './reps.js'
|
|
27
|
+
import { valTypeOf } from './kind.js'
|
|
28
|
+
import { T } from './ast.js'
|
|
29
|
+
import { objLiteralSchemaId } from './static.js'
|
|
25
30
|
|
|
26
|
-
|
|
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)
|
|
31
|
+
export { I32_MIN, I32_MAX, isI32, isLiteralStr, isFuncRef }
|
|
37
32
|
|
|
38
33
|
// === Type helpers ===
|
|
39
34
|
|
|
@@ -42,9 +37,7 @@ export const typed = (node, type) => (node.type = type, node)
|
|
|
42
37
|
|
|
43
38
|
/** NaN-box prefix for a pointer of VAL kind K with aux bits: `0x7FF8 | type<<47 | aux<<32`. */
|
|
44
39
|
function ptrBoxPrefix(ptrType, aux = 0) {
|
|
45
|
-
return (
|
|
46
|
-
| ((BigInt(ptrType) & 0xFn) << 47n)
|
|
47
|
-
| ((BigInt(aux) & 0x7FFFn) << 32n)
|
|
40
|
+
return ptrBoxPrefixBigInt(ptrType, aux)
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
/** Build f64 NaN-boxed pointer IR from an i32 offset node of known kind.
|
|
@@ -102,7 +95,67 @@ export const asPtrOffset = (n, ptrKind) => {
|
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
/** Coerce emitted IR to a target WASM param type ('i32' | 'i64' | 'f64'). */
|
|
105
|
-
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : asF64(n)
|
|
98
|
+
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : t === 'v128' ? n : asF64(n)
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Narrow an f64 arithmetic tree under ToInt32 — the general int-accumulator path.
|
|
102
|
+
*
|
|
103
|
+
* ToInt32 is reduction mod 2^32, and {+, −, ×} form a RING under that modulus:
|
|
104
|
+
* operands may wrap to i32 eagerly and the final result still equals ToInt32 of
|
|
105
|
+
* the JS value — PROVIDED the original f64 computation was exact (no rounding).
|
|
106
|
+
* Exactness is tracked structurally: every interior node's worst-case magnitude
|
|
107
|
+
* (`maxAbs`, real un-wrapped value) must stay below 2^53. Leaves are peeled
|
|
108
|
+
* `f64.convert_i32_*` wrappers (≤2^31/2^32) and integer constants.
|
|
109
|
+
*
|
|
110
|
+
* `/` is NOT a ring op (fractions): it narrows only at the ToInt32 ROOT, with a
|
|
111
|
+
* FAITHFUL numerator (i32 value == JS value — wrapped sums excluded) and a
|
|
112
|
+
* constant integer divisor. i32.div_s truncates toward zero exactly like
|
|
113
|
+
* ToInt32 of the f64 quotient (error < ulp/2 < distance-to-integer for any i32
|
|
114
|
+
* numerator); c ∈ {0,−1,1} are diverted (trap / INT_MIN trap / identity).
|
|
115
|
+
*
|
|
116
|
+
* Returns {node (i32-typed), maxAbs, faithful} or null — callers use `.node`.
|
|
117
|
+
*/
|
|
118
|
+
const narrowI32 = (x, isRoot) => {
|
|
119
|
+
if (!Array.isArray(x)) return null
|
|
120
|
+
if (x.type === 'i32') return { node: x, maxAbs: 2 ** 31, faithful: true }
|
|
121
|
+
const op = x[0]
|
|
122
|
+
if (op === 'f64.convert_i32_s' || op === 'f64.convert_i32_u')
|
|
123
|
+
// Peel — same as toI32's peephole. _u values ∈ [0, 2^32): the re-tag IS the
|
|
124
|
+
// wrap (ring-compatible), but the i32 view differs from the JS value above
|
|
125
|
+
// 2^31, so _u is not faithful.
|
|
126
|
+
return {
|
|
127
|
+
node: Array.isArray(x[1]) ? typed(x[1], 'i32') : x[1],
|
|
128
|
+
maxAbs: op === 'f64.convert_i32_s' ? 2 ** 31 : 2 ** 32,
|
|
129
|
+
faithful: op === 'f64.convert_i32_s',
|
|
130
|
+
}
|
|
131
|
+
if (op === 'f64.const' && typeof x[1] === 'number' && Number.isInteger(x[1]) && Math.abs(x[1]) < 2 ** 52)
|
|
132
|
+
return { node: typed(['i32.const', x[1] | 0], 'i32'), maxAbs: Math.abs(x[1]), faithful: Math.abs(x[1]) < 2 ** 31 }
|
|
133
|
+
if (op === 'f64.add' || op === 'f64.sub' || op === 'f64.mul') {
|
|
134
|
+
const a = narrowI32(x[1]), b = narrowI32(x[2])
|
|
135
|
+
if (!a || !b) return null
|
|
136
|
+
const maxAbs = op === 'f64.mul' ? a.maxAbs * b.maxAbs : a.maxAbs + b.maxAbs
|
|
137
|
+
if (maxAbs >= 2 ** 53) return null
|
|
138
|
+
const iop = op === 'f64.add' ? 'i32.add' : op === 'f64.sub' ? 'i32.sub' : 'i32.mul'
|
|
139
|
+
return { node: typed([iop, a.node, b.node], 'i32'), maxAbs, faithful: false }
|
|
140
|
+
}
|
|
141
|
+
if (op === 'f64.neg') {
|
|
142
|
+
const a = narrowI32(x[1])
|
|
143
|
+
if (!a) return null
|
|
144
|
+
return { node: typed(['i32.sub', ['i32.const', 0], a.node], 'i32'), maxAbs: a.maxAbs, faithful: false }
|
|
145
|
+
}
|
|
146
|
+
if (op === 'f64.div' && isRoot) {
|
|
147
|
+
const a = narrowI32(x[1])
|
|
148
|
+
if (!a || !a.faithful) return null
|
|
149
|
+
const c = Array.isArray(x[2]) && x[2][0] === 'f64.const' && typeof x[2][1] === 'number' ? x[2][1] : null
|
|
150
|
+
if (c == null || !Number.isInteger(c) || c === 0 || c === 1 || Math.abs(c) >= 2 ** 31) return null
|
|
151
|
+
// c = −1 would trap on INT_MIN; 0 − x wraps INT_MIN → INT_MIN, matching ToInt32(2^31).
|
|
152
|
+
const node = c === -1
|
|
153
|
+
? typed(['i32.sub', ['i32.const', 0], a.node], 'i32')
|
|
154
|
+
: typed(['i32.div_s', a.node, ['i32.const', c]], 'i32')
|
|
155
|
+
return { node, maxAbs: 2 ** 31, faithful: c !== -1 }
|
|
156
|
+
}
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
106
159
|
|
|
107
160
|
/** Coerce node to i32 with wrapping (JS `|0` semantics: values > 2^31 wrap to negative).
|
|
108
161
|
* Per ECMAScript ToInt32, NaN and ±∞ map to 0. `i64.trunc_sat_f64_s` handles NaN
|
|
@@ -120,18 +173,22 @@ export const toI32 = n => {
|
|
|
120
173
|
}
|
|
121
174
|
if (Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'number') {
|
|
122
175
|
const v = n[1]
|
|
123
|
-
return typed(['i32.const', Number.isFinite(v) ? v | 0 : 0], 'i32')
|
|
176
|
+
return typed(['i32.const', Number.isFinite(v) ? v | 0 : 0], 'i32') // JS `|0` is ToInt32
|
|
124
177
|
}
|
|
178
|
+
// General int-arithmetic narrowing: an exact-int f64 tree of {+,−,×,neg,/C}
|
|
179
|
+
// computes in i32 (mod-2^32 ring) — no trunc/guard at all.
|
|
180
|
+
const nw = narrowI32(n, true)
|
|
181
|
+
if (nw) return nw.node
|
|
125
182
|
// Leaf nodes are cheap to duplicate; for everything else, evaluate once via local.tee.
|
|
126
183
|
const isLeaf = Array.isArray(n) && n.length <= 2 &&
|
|
127
184
|
(n[0] === 'f64.const' || n[0] === 'local.get' || n[0] === 'global.get')
|
|
185
|
+
// `i32.wrap_i64(i64.trunc_sat_f64_s x)` is exact ToInt32 for |x| < 2^63 (the
|
|
186
|
+
// overwhelming common range), maps NaN/−∞→0, and +∞ is guarded to 0 by the
|
|
187
|
+
// select. For |x| ≥ 2^63 it saturates rather than wrapping mod 2^32 — a
|
|
188
|
+
// deliberately-allowed asm.js-style boundary (no per-`|0` helper/guard cost).
|
|
128
189
|
const wrap = x => typed(['i32.wrap_i64', ['i64.trunc_sat_f64_s', x]], 'i32')
|
|
129
190
|
if (isLeaf) {
|
|
130
|
-
return typed(['select',
|
|
131
|
-
wrap(n),
|
|
132
|
-
['i32.const', 0],
|
|
133
|
-
['f64.ne', n, ['f64.const', Infinity]]
|
|
134
|
-
], 'i32')
|
|
191
|
+
return typed(['select', wrap(n), ['i32.const', 0], ['f64.ne', n, ['f64.const', Infinity]]], 'i32')
|
|
135
192
|
}
|
|
136
193
|
const t = temp('inf')
|
|
137
194
|
return typed(['select',
|
|
@@ -166,16 +223,16 @@ export const fromI64 = n => {
|
|
|
166
223
|
* See module/symbol.js for the broader reserved-atom-id scheme.
|
|
167
224
|
* Distinct from 0, NaN, and all pointers. Triggers default params.
|
|
168
225
|
* At the JS boundary, null and undefined preserve their identity for interop. */
|
|
169
|
-
export const NULL_NAN =
|
|
170
|
-
export const UNDEF_NAN =
|
|
226
|
+
export const NULL_NAN = atomNanHex(1)
|
|
227
|
+
export const UNDEF_NAN = atomNanHex(2)
|
|
171
228
|
/** Boxed-boolean carrier. `false`/`true` are reserved atoms — materialized only
|
|
172
229
|
* where boolean identity is observed (typeof/String/JSON/host boundary); in
|
|
173
230
|
* branch/arithmetic position booleans stay raw i32/f64 0/1. The atomId encodes
|
|
174
231
|
* the truth value in its low bit (4=false, 5=true), so `aux & 1` recovers 0/1
|
|
175
232
|
* and `4 | bit` boxes it — see boolBoxIR / unboxBoolIR. */
|
|
176
233
|
export const BOOL_ATOM_BASE = 4
|
|
177
|
-
export const FALSE_NAN =
|
|
178
|
-
export const TRUE_NAN =
|
|
234
|
+
export const FALSE_NAN = atomNanHex(4)
|
|
235
|
+
export const TRUE_NAN = atomNanHex(5)
|
|
179
236
|
/** WAT-template-ready sentinel expressions for use in stdlib template strings.
|
|
180
237
|
* `f64.const nan:0xHEX` is 3 bytes shorter than `f64.reinterpret_i64 (i64.const ...)`. */
|
|
181
238
|
export const NULL_WAT = `(f64.const nan:${NULL_NAN})`
|
|
@@ -211,13 +268,20 @@ export function unboxBoolIR(f64expr) {
|
|
|
211
268
|
|
|
212
269
|
/** Max arity of inline closure slots. Closures are compiled with signature
|
|
213
270
|
* (env f64, argc i32, a0..a{MAX-1} f64) → f64 — no per-call heap alloc.
|
|
214
|
-
*
|
|
215
|
-
*
|
|
271
|
+
* Direct (non-spread) calls with more args than MAX error. Spread calls are
|
|
272
|
+
* unbounded: the spread site publishes the full args-array offset in
|
|
273
|
+
* $__closure_spill, and a rest-param callee reads args[MAX..argc-1] from it
|
|
274
|
+
* (see module/function.js spread path + compile/index.js rest collection). */
|
|
216
275
|
export const MAX_CLOSURE_ARITY = 8
|
|
217
276
|
|
|
218
277
|
/** Matches WASM instructions that require a memory section. */
|
|
219
|
-
//
|
|
220
|
-
|
|
278
|
+
// Any instruction that touches linear memory ⇒ the module must declare memory.
|
|
279
|
+
// Matches every `memory.*` op (size/grow/copy/fill/init) and every typed load/store
|
|
280
|
+
// incl. width suffixes (load8_u, store16, i64.load32_s, v128.load, …). The old
|
|
281
|
+
// hand-enumerated list silently missed memory.copy/fill, v128.load/store and
|
|
282
|
+
// i64.store8/16/32 (all used in stdlib) — a body using only those would wrongly
|
|
283
|
+
// report no-memory. Broad-but-precise: only `memory.` and `<type>.load|store` match.
|
|
284
|
+
export const MEM_OPS = /\b(memory\.\w+|(?:i32|i64|f32|f64|v128)\.(?:load|store)\w*)\b/
|
|
221
285
|
|
|
222
286
|
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'])
|
|
223
287
|
export const SPREAD_MUTATORS = new Set(['push', 'add', 'set', 'unshift'])
|
|
@@ -250,15 +314,22 @@ export function mkPtrIR(type, aux, offset) {
|
|
|
250
314
|
return typed(['call', '$__mkptr', tIR, aIR, oIR], 'f64')
|
|
251
315
|
}
|
|
252
316
|
|
|
253
|
-
/** Offset extraction for a NaN-boxed pointer
|
|
254
|
-
*
|
|
255
|
-
*
|
|
317
|
+
/** Offset extraction for a NaN-boxed pointer.
|
|
318
|
+
* Goes through `__ptr_offset`, which chases the relocation-forwarding chain
|
|
319
|
+
* (cap == -1 sentinel at off-4 → relocated offset at off-8). The chase is a
|
|
320
|
+
* single load+compare for any live (non-forwarded) header, so it is a no-op for
|
|
321
|
+
* fixed-shape receivers (OBJECT/TYPED/…) whose cap word is never -1.
|
|
322
|
+
*
|
|
323
|
+
* We do NOT skip it for "non-ARRAY" static types: that shortcut was unsound on
|
|
324
|
+
* two counts. (1) ARRAY is not the only growable container — HASH/SET/MAP relocate
|
|
325
|
+
* too. (2) jz value types are not always precise: a binding inferred OBJECT (a
|
|
326
|
+
* polymorphic parameter, a widened union) can hold a relocated ARRAY at runtime.
|
|
327
|
+
* Writing through its stale pre-relocation base then clobbers whatever now occupies
|
|
328
|
+
* that freed region — a memory-safety hazard that must not depend on inference
|
|
329
|
+
* precision. Memory safety is unconditional; the forwarding follow stays.
|
|
256
330
|
* If the node is already an unboxed pointer (ptrKind), return it directly. */
|
|
257
331
|
export function ptrOffsetIR(valIR, valType) {
|
|
258
332
|
if (valIR.ptrKind != null && valIR.ptrKind !== VAL.ARRAY) return valIR
|
|
259
|
-
if (valType != null && valType !== VAL.ARRAY) {
|
|
260
|
-
return ['i32.wrap_i64', ['i64.reinterpret_f64', valIR]]
|
|
261
|
-
}
|
|
262
333
|
inc('__ptr_offset')
|
|
263
334
|
return ['call', '$__ptr_offset', ['i64.reinterpret_f64', valIR]]
|
|
264
335
|
}
|
|
@@ -282,48 +353,73 @@ export function ptrTypeIR(valIR, valType) {
|
|
|
282
353
|
['i64.const', 0xF]]]
|
|
283
354
|
}
|
|
284
355
|
|
|
356
|
+
// SELF-HOST CONTRACT: f64 slot BITS travel as canonical '0x'+16-hex STRINGS.
|
|
357
|
+
// A BigInt crossing a function return / array element / object slot is
|
|
358
|
+
// kind-erased in the kernel (raw i64 bits are untagged) and every subsequent
|
|
359
|
+
// op on it misdispatches; BigInt64Array/BigUint64Array views and
|
|
360
|
+
// DataView.{get,set}BigUint64 are a legacy f64-value shim there. Strings are
|
|
361
|
+
// tagged and survive every boundary; BigInt math happens only inside single
|
|
362
|
+
// expressions. (Same contract as wat/optimize.js's i64 VALUE CONTRACT.)
|
|
285
363
|
const _F64_BITS_BUF = new ArrayBuffer(8)
|
|
286
364
|
const _F64_BITS_F = new Float64Array(_F64_BITS_BUF)
|
|
287
|
-
const
|
|
365
|
+
const _F64_BITS_U32 = new Uint32Array(_F64_BITS_BUF) // LE halves: [0]=lo, [1]=hi
|
|
366
|
+
const _hx8 = (u) => (u >>> 0).toString(16).padStart(8, '0')
|
|
288
367
|
|
|
289
368
|
/** Return i64 bit pattern (BigInt) of a pure-literal IR node, or null if non-literal. */
|
|
290
369
|
export function extractF64Bits(node) {
|
|
291
370
|
if (!Array.isArray(node)) return null
|
|
292
371
|
if (node[0] === 'f64.const') {
|
|
293
|
-
if (typeof node[1] === 'number') { _F64_BITS_F[0] = node[1]; return
|
|
372
|
+
if (typeof node[1] === 'number') { _F64_BITS_F[0] = node[1]; return '0x' + _hx8(_F64_BITS_U32[1]) + _hx8(_F64_BITS_U32[0]) }
|
|
294
373
|
if (typeof node[1] === 'string' && node[1].startsWith('nan:')) {
|
|
295
|
-
try {
|
|
374
|
+
try {
|
|
375
|
+
const v = BigInt(node[1].slice(4)) | 0x7ff0000000000000n
|
|
376
|
+
return '0x' + v.toString(16).padStart(16, '0')
|
|
377
|
+
} catch { return null }
|
|
296
378
|
}
|
|
297
379
|
return null
|
|
298
380
|
}
|
|
299
381
|
if (node[0] === 'f64.reinterpret_i64' && Array.isArray(node[1]) && node[1][0] === 'i64.const' && typeof node[1][1] === 'string') {
|
|
300
382
|
const s = node[1][1]
|
|
301
383
|
if (s.startsWith('-')) {
|
|
302
|
-
|
|
303
|
-
|
|
384
|
+
// Two's complement WITHOUT a 2^64 term: (-1 − |v|) + 1 ≡ 2^64 − |v| both
|
|
385
|
+
// natively and on the kernel's mod-2^64 carrier (1n<<64n is unrepresentable
|
|
386
|
+
// there and would silently corrupt).
|
|
387
|
+
try {
|
|
388
|
+
const v = (0xffffffffffffffffn - BigInt(s.slice(1)) + 1n) & 0xffffffffffffffffn
|
|
389
|
+
return '0x' + v.toString(16).padStart(16, '0')
|
|
390
|
+
} catch { return null }
|
|
304
391
|
}
|
|
305
|
-
try {
|
|
392
|
+
try {
|
|
393
|
+
const v = BigInt(s)
|
|
394
|
+
return '0x' + v.toString(16).padStart(16, '0')
|
|
395
|
+
} catch { return null }
|
|
306
396
|
}
|
|
307
397
|
return null
|
|
308
398
|
}
|
|
309
399
|
|
|
310
|
-
/** Append `slots` (
|
|
311
|
-
*
|
|
312
|
-
*
|
|
400
|
+
/** Append `slots` ('0x'+16-hex bit strings, see contract above) to
|
|
401
|
+
* ctx.runtime.data 8-byte aligned, return raw byte offset of first slot.
|
|
402
|
+
* Slots that look like NaN-boxed pointers are recorded in
|
|
403
|
+
* `ctx.runtime.staticPtrSlots` so the prefix-strip pass can patch their
|
|
404
|
+
* embedded offsets. Writes go through u32 halves — DataView's BigInt
|
|
405
|
+
* accessors are unfaithful in the self-host kernel. */
|
|
313
406
|
export function appendStaticSlots(slots, headerBytes = 0) {
|
|
314
407
|
if (!ctx.runtime.data) ctx.runtime.data = ''
|
|
315
408
|
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
316
409
|
const off = ctx.runtime.data.length
|
|
317
410
|
const u8 = new Uint8Array(headerBytes + slots.length * 8)
|
|
318
411
|
const dv = new DataView(u8.buffer)
|
|
319
|
-
for (let i = 0; i < slots.length; i++)
|
|
412
|
+
for (let i = 0; i < slots.length; i++) {
|
|
413
|
+
const h = slots[i]
|
|
414
|
+
dv.setUint32(headerBytes + i * 8, parseInt(h.slice(10), 16) >>> 0, true)
|
|
415
|
+
dv.setUint32(headerBytes + i * 8 + 4, parseInt(h.slice(2, 10), 16) >>> 0, true)
|
|
416
|
+
}
|
|
320
417
|
let chunk = ''
|
|
321
418
|
for (let i = 0; i < u8.length; i++) chunk += String.fromCharCode(u8[i])
|
|
322
419
|
ctx.runtime.data += chunk
|
|
323
420
|
if (!ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = []
|
|
324
421
|
for (let i = 0; i < slots.length; i++) {
|
|
325
|
-
|
|
326
|
-
if (((bits >> 48n) & 0xFFF8n) === BigInt(LAYOUT.NAN_PREFIX)) {
|
|
422
|
+
if ((parseInt(slots[i].slice(2, 6), 16) & 0xFFF8) === LAYOUT.NAN_PREFIX) {
|
|
327
423
|
ctx.runtime.staticPtrSlots.push(off + i * 8)
|
|
328
424
|
}
|
|
329
425
|
}
|
|
@@ -335,8 +431,8 @@ export function appendStaticSlots(slots, headerBytes = 0) {
|
|
|
335
431
|
/** Check if emitted node is a compile-time constant. */
|
|
336
432
|
export const isLit = n => (n[0] === 'i32.const' || n[0] === 'f64.const') && typeof n[1] === 'number'
|
|
337
433
|
export const litVal = n => n[1]
|
|
338
|
-
const isNullLit = n => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] == null
|
|
339
|
-
const isUndefLit = n => Array.isArray(n) && n.length === 0
|
|
434
|
+
export const isNullLit = n => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] == null
|
|
435
|
+
export const isUndefLit = n => Array.isArray(n) && n.length === 0
|
|
340
436
|
export const isNullishLit = n => isNullLit(n) || isUndefLit(n)
|
|
341
437
|
|
|
342
438
|
/** Side-effect-free (safe for WASM select). */
|
|
@@ -348,46 +444,173 @@ const PURE_OPS = new Set(['i32.const', 'f64.const', 'local.get', 'global.get',
|
|
|
348
444
|
'i32.eq', 'i32.ne', 'i32.lt_s', 'i32.gt_s', 'i32.le_s', 'i32.ge_s', 'i32.eqz'])
|
|
349
445
|
export const isPureIR = n => Array.isArray(n) && PURE_OPS.has(n[0]) && n.slice(1).every(c => !Array.isArray(c) || isPureIR(c))
|
|
350
446
|
|
|
351
|
-
/**
|
|
352
|
-
|
|
447
|
+
/** Ops whose f64 result is always a plain number (never a NaN-boxed pointer).
|
|
448
|
+
* Used by toNumF64 to skip the __to_num wrapper when the value is provably numeric.
|
|
449
|
+
* NOTE: f64.const is NOT included — it may encode a NaN-boxed pointer. */
|
|
450
|
+
const PURE_F64_OPS = new Set([
|
|
451
|
+
'f64.add', 'f64.sub', 'f64.mul', 'f64.div', 'f64.neg', 'f64.abs', 'f64.sqrt',
|
|
452
|
+
'f64.min', 'f64.max', 'f64.ceil', 'f64.floor', 'f64.trunc', 'f64.nearest', 'f64.copysign',
|
|
453
|
+
'f64.convert_i32_s', 'f64.convert_i32_u', 'f64.promote_f32',
|
|
454
|
+
])
|
|
455
|
+
|
|
456
|
+
/** True iff `r` provably yields a plain f64 NUMBER (never a NaN-boxed pointer or
|
|
457
|
+
* nullish sentinel). A `block`/`if` is numeric only when its value-producing tail
|
|
458
|
+
* is — so `o.a?.b` (a block whose result is a property value or undef sentinel)
|
|
459
|
+
* is correctly NOT numeric, while `cond ? n*2 : n*3` is. Conservative: any shape
|
|
460
|
+
* not provably numeric (property gets, user calls, local.get, f64.const nan:…)
|
|
461
|
+
* returns false, so the caller keeps the __to_num coercion. */
|
|
462
|
+
const isNumericIR = (r) => {
|
|
463
|
+
if (!Array.isArray(r)) return false
|
|
464
|
+
const op = r[0]
|
|
465
|
+
if (PURE_F64_OPS.has(op)) return true
|
|
466
|
+
if (op === 'call' && typeof r[1] === 'string' && (r[1].startsWith('$math.') || r[1] === '$__time_ms')) return true
|
|
467
|
+
if (op === 'f64.const') return typeof r[1] === 'number' // 'nan:…' carrier ⇒ pointer/sentinel
|
|
468
|
+
if (op === 'block') return isNumericIR(r[r.length - 1]) // block value = its tail expr
|
|
469
|
+
if (op === 'if') { // both arms must be numeric
|
|
470
|
+
const thenArm = r.find(x => Array.isArray(x) && x[0] === 'then')
|
|
471
|
+
const elseArm = r.find(x => Array.isArray(x) && x[0] === 'else')
|
|
472
|
+
return !!thenArm && !!elseArm &&
|
|
473
|
+
isNumericIR(thenArm[thenArm.length - 1]) && isNumericIR(elseArm[elseArm.length - 1])
|
|
474
|
+
}
|
|
475
|
+
return false
|
|
476
|
+
}
|
|
353
477
|
|
|
354
|
-
/** Resolve compile-time value type from AST node (literal →
|
|
478
|
+
/** Resolve compile-time value type from AST node (literal → name → lookup). */
|
|
355
479
|
export const resolveValType = (node, valTypeOf, lookupValType) =>
|
|
356
480
|
valTypeOf(node) ?? (typeof node === 'string' ? lookupValType(node) : null)
|
|
357
481
|
|
|
358
|
-
/** Check if AST node is a string reference to a known function name. */
|
|
359
|
-
export const isFuncRef = (node, funcNames) => typeof node === 'string' && funcNames.has(node)
|
|
360
|
-
|
|
361
482
|
/** Check if (a, op, b) is a postfix pattern: [op, name] and [, 1] literal. */
|
|
362
483
|
export const isPostfix = (a, op, b) => Array.isArray(a) && a[0] === op && Array.isArray(b) && b[0] == null && b[1] === 1
|
|
363
484
|
|
|
364
485
|
/** Emit a numeric constant with correct i32/f64 typing.
|
|
365
486
|
* `-0` is f64-only (i32 has no signed zero) — preserve the sign by emitting f64. */
|
|
366
487
|
export const emitNum = v => isI32(v)
|
|
367
|
-
? typed(['i32.const', v], 'i32')
|
|
488
|
+
? typed(['i32.const', v], 'i32')
|
|
489
|
+
// Emit NaN via the `nan` token, not the raw JS number: a numeric NaN literal in
|
|
490
|
+
// the IR loses its quiet-mantissa bit (0x7FF8→0x7FF0, i.e. becomes Infinity) when
|
|
491
|
+
// the self-host kernel marshals the IR back across the wasm→host boundary. The
|
|
492
|
+
// `nan` token assembles to the canonical 0x7FF8 number-NaN unambiguously.
|
|
493
|
+
: typed(['f64.const', v !== v ? 'nan' : v], 'f64')
|
|
368
494
|
|
|
369
495
|
// === Temp locals ===
|
|
370
496
|
|
|
371
|
-
/** Allocate a
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
|
|
497
|
+
/** Allocate a fresh local name with the given tag, registered as `type`. The
|
|
498
|
+
* selfhost compiler doesn't yet handle exported-const arrow factories returning
|
|
499
|
+
* closures, so the three temp() helpers stay as `function` declarations and
|
|
500
|
+
* delegate to this shared core. */
|
|
501
|
+
function freshLocal(type, tag) {
|
|
375
502
|
let name
|
|
376
503
|
do { name = `${T}${tag}${ctx.func.uniq++}` } while (ctx.func.locals.has(name))
|
|
377
|
-
ctx.func.locals.set(name,
|
|
504
|
+
ctx.func.locals.set(name, type)
|
|
378
505
|
return name
|
|
379
506
|
}
|
|
380
|
-
export function
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
507
|
+
export function temp (tag = '') { return freshLocal('f64', tag) }
|
|
508
|
+
export function tempI32 (tag = '') { return freshLocal('i32', tag) }
|
|
509
|
+
export function tempI64 (tag = '') { return freshLocal('i64', tag) }
|
|
510
|
+
|
|
511
|
+
// === IR scaffolds ===
|
|
512
|
+
|
|
513
|
+
/** Wrap a sequence of statements as a typed `(block (result <type>) …)`.
|
|
514
|
+
* Default result is `f64` (the value-type for most jz emissions).
|
|
515
|
+
* Shorthand for the `typed(['block', ['result', T], …stmts], T)` pattern that
|
|
516
|
+
* appears in nearly every emitter — keeps call sites focused on the body. */
|
|
517
|
+
export const block64 = (...stmts) => typed(['block', ['result', 'f64'], ...stmts], 'f64')
|
|
518
|
+
export const blockTyped = (type, ...stmts) => typed(['block', ['result', type], ...stmts], type)
|
|
519
|
+
|
|
520
|
+
/** Allocate an f64 temp, set it to `val`, run `body(name)` and yield its result.
|
|
521
|
+
* `body` may return either a single IR node (used as the block result) or an
|
|
522
|
+
* array of nodes whose last expression becomes the result. Eliminates the
|
|
523
|
+
* repetitive `const t = temp(); …['local.set', $t, val]; …['local.get', $t]`
|
|
524
|
+
* scaffold around tee-and-use patterns. */
|
|
525
|
+
export function withTemp(val, body, tag = '') {
|
|
526
|
+
const t = temp(tag)
|
|
527
|
+
const out = body(t)
|
|
528
|
+
const tail = Array.isArray(out) && out.every(n => Array.isArray(n)) ? out : [out]
|
|
529
|
+
return block64(['local.set', `$${t}`, val], ...tail)
|
|
385
530
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
531
|
+
|
|
532
|
+
/** Whole-fn structural refcount: walks `fn`, counting how many times each
|
|
533
|
+
* array node is referenced. Used by optimizer passes to skip shared subtrees
|
|
534
|
+
* (watr CSE may leave them) — mutating a node with refcount > 1 would also
|
|
535
|
+
* affect references outside the current region. Single-pass O(N). */
|
|
536
|
+
export function buildRefcount(fn) {
|
|
537
|
+
const refcount = new Map()
|
|
538
|
+
const walk = (node) => {
|
|
539
|
+
if (!Array.isArray(node)) return
|
|
540
|
+
const n = (refcount.get(node) || 0) + 1
|
|
541
|
+
refcount.set(node, n)
|
|
542
|
+
if (n > 1) return // already counted children below
|
|
543
|
+
for (let i = 0; i < node.length; i++) walk(node[i])
|
|
544
|
+
}
|
|
545
|
+
walk(fn)
|
|
546
|
+
return refcount
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Pick the next free `$__<prefix><id>` local-name id by collecting all
|
|
550
|
+
* existing ids in a single walk. Replaces the per-pass
|
|
551
|
+
* `while (fn.some(... === $__prefixK)) k++` (O(K·N)) with one O(N) scan. */
|
|
552
|
+
export function nextLocalId(fn, prefix) {
|
|
553
|
+
const seen = new Set()
|
|
554
|
+
const needle = `$__${prefix}`
|
|
555
|
+
const walk = (n) => {
|
|
556
|
+
if (!Array.isArray(n)) return
|
|
557
|
+
if (n[0] === 'local' && typeof n[1] === 'string' && n[1].startsWith(needle)) {
|
|
558
|
+
const tail = n[1].slice(needle.length)
|
|
559
|
+
if (/^\d+$/.test(tail)) seen.add(+tail)
|
|
560
|
+
}
|
|
561
|
+
for (let i = 0; i < n.length; i++) walk(n[i])
|
|
562
|
+
}
|
|
563
|
+
walk(fn)
|
|
564
|
+
let id = 0
|
|
565
|
+
while (seen.has(id)) id++
|
|
566
|
+
return id
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Single-kind ptr-tag predicate: `__ptr_type(bits) == ptr`. Takes the f64
|
|
570
|
+
* carrier expression and the PTR constant. Use this when guarding one branch;
|
|
571
|
+
* use `dispatchByPtrType` for multi-case forks. Stamps `inc('__ptr_type')`. */
|
|
572
|
+
export function ptrTypeEq(f64Expr, ptr) {
|
|
573
|
+
inc('__ptr_type')
|
|
574
|
+
return typed(['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', f64Expr]], ['i32.const', ptr]], 'i32')
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** ToPrimitive sidecar probe (ES2024 7.1.1): an own `valueOf`/`toString` data
|
|
578
|
+
* property shadows the builtin. Reads the dynamic-prop sidecar slot keyed by
|
|
579
|
+
* `nameIR` (an emitted i64 string key) off receiver `objIR`; if it holds a
|
|
580
|
+
* closure, yields `onOverride($p)`, else `onFallback($o)` (both f64). Shared by
|
|
581
|
+
* the member-READ path (module/core.js — onOverride returns the closure value,
|
|
582
|
+
* onFallback calls the arity-≤1 builtin) and the method-CALL path (emit.js —
|
|
583
|
+
* onOverride invokes the closure, onFallback calls the builtin method). */
|
|
584
|
+
export function sidecarOverride(objIR, nameIR, onOverride, onFallback) {
|
|
585
|
+
const o = temp('vo'), p = temp('vp')
|
|
586
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
587
|
+
return block64(
|
|
588
|
+
['local.set', `$${o}`, asF64(objIR)],
|
|
589
|
+
['local.set', `$${p}`, ['f64.reinterpret_i64',
|
|
590
|
+
['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${o}`]], nameIR]]],
|
|
591
|
+
['if', ['result', 'f64'],
|
|
592
|
+
ptrTypeEq(['local.get', `$${p}`], PTR.CLOSURE),
|
|
593
|
+
['then', onOverride(p, o)],
|
|
594
|
+
['else', onFallback(o)]])
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Dispatch on `__ptr_type(bits)` — emits a right-leaning if/else chain over
|
|
598
|
+
* PTR constants. `cases` is `[[PTR.X, ir], …]`; `fallback` is the else IR.
|
|
599
|
+
* `resultType` defaults to `'f64'`; pass `null` for a void dispatch (e.g.
|
|
600
|
+
* pure memory-writing branches). Centralizes the
|
|
601
|
+
* `i32.eq (call $__ptr_type bits) (i32.const PTR.X)` pattern so emitters
|
|
602
|
+
* dispatching by pointer kind stay declarative. */
|
|
603
|
+
export function dispatchByPtrType(typeLocal, cases, fallback, resultType = 'f64') {
|
|
604
|
+
let out = fallback
|
|
605
|
+
const head = resultType ? ['if', ['result', resultType]] : ['if']
|
|
606
|
+
for (let i = cases.length - 1; i >= 0; i--) {
|
|
607
|
+
const [ptr, ir] = cases[i]
|
|
608
|
+
out = [...head,
|
|
609
|
+
['i32.eq', ['local.get', `$${typeLocal}`], ['i32.const', ptr]],
|
|
610
|
+
['then', ir],
|
|
611
|
+
['else', out]]
|
|
612
|
+
}
|
|
613
|
+
return out
|
|
391
614
|
}
|
|
392
615
|
|
|
393
616
|
// === Numeric helpers ===
|
|
@@ -395,23 +618,17 @@ export function tempI64(tag = '') {
|
|
|
395
618
|
/** WASM has no f64.rem — implement as a - trunc(a/b) * b.
|
|
396
619
|
* Both `a` and `b` appear twice in the expansion; cache non-pure operands
|
|
397
620
|
* in locals so side effects (e.g. assignments) only execute once. */
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const pre = []
|
|
404
|
-
if (!pa) pre.push(['local.set', `$${ta}`, a])
|
|
405
|
-
if (!pb) pre.push(['local.set', `$${tb}`, b])
|
|
406
|
-
return typed(['block', ['result', 'f64'], ...pre,
|
|
407
|
-
['f64.sub', ga, ['f64.mul', ['f64.trunc', ['f64.div', ga, gb]], gb]]], 'f64')
|
|
408
|
-
}
|
|
621
|
+
// JS `%` on the f64 path. Delegates to the exact `__rem` (binary fmod) stdlib —
|
|
622
|
+
// the textbook `a - b*trunc(a/b)` is inexact for large a/b and wrong on the
|
|
623
|
+
// ±Inf / 0 / NaN edges. The i32.rem_s fast path in emit.js handles the common
|
|
624
|
+
// integer-with-nonzero-literal-divisor case; everything else lands here.
|
|
625
|
+
export const f64rem = (a, b) => (inc('__rem'), typed(['call', '$__rem', a, b], 'f64'))
|
|
409
626
|
|
|
410
627
|
/** Resolve the slot index of a ToPrimitive method (`valueOf`/`toString`) on an
|
|
411
628
|
* OBJECT operand — from a schema-bound variable or an inline object literal.
|
|
412
629
|
* Returns -1 when the method is absent. */
|
|
413
630
|
function primMethodIdx(node, name) {
|
|
414
|
-
if (typeof node === 'string') return ctx.schema.
|
|
631
|
+
if (typeof node === 'string') return ctx.schema.slotOf(node, name)
|
|
415
632
|
const sid = objLiteralSchemaId(node)
|
|
416
633
|
const props = sid != null ? ctx.schema.list[sid] : null
|
|
417
634
|
return props ? props.indexOf(name) : -1
|
|
@@ -448,18 +665,40 @@ function toPrimitiveChain(node, v, order) {
|
|
|
448
665
|
return typed(['block', blk, ...body], 'i64')
|
|
449
666
|
}
|
|
450
667
|
|
|
668
|
+
const cloneIR = (n) => Array.isArray(n) ? n.map(cloneIR) : n
|
|
669
|
+
|
|
670
|
+
/** ToNumber for a runtime value that may carry a nullish sentinel: null→+0, undefined→NaN,
|
|
671
|
+
* anything else → itself. `valIR` must be side-effect-free (a local read) — it is duplicated,
|
|
672
|
+
* so each occurrence gets a fresh clone. Used for bindings flagged in ctx.func.maybeNullish;
|
|
673
|
+
* a real number isn't either sentinel, so it falls through the `else` unchanged. */
|
|
674
|
+
const coerceNullishToNum = (valIR) => typed(
|
|
675
|
+
['if', ['result', 'f64'],
|
|
676
|
+
['i64.eq', ['i64.reinterpret_f64', cloneIR(valIR)], ['i64.const', NULL_NAN]],
|
|
677
|
+
['then', ['f64.const', 0]],
|
|
678
|
+
['else', ['if', ['result', 'f64'],
|
|
679
|
+
['i64.eq', ['i64.reinterpret_f64', cloneIR(valIR)], ['i64.const', UNDEF_NAN]],
|
|
680
|
+
['then', ['f64.const', 'nan']],
|
|
681
|
+
['else', cloneIR(valIR)]]]],
|
|
682
|
+
'f64')
|
|
683
|
+
|
|
451
684
|
/** Coerce an emitted IR value to a plain f64 Number per JS `ToNumber`.
|
|
452
685
|
* Skips coercion when static type proves the value is already numeric
|
|
453
686
|
* (i32 node, compile-time literal, known VAL.NUMBER/VAL.BIGINT). When the full
|
|
454
687
|
* string-parsing `__to_num` isn't loaded (no string module → no strings can
|
|
455
688
|
* exist) nullish *literals* still fold statically (null→+0, undefined→NaN);
|
|
456
|
-
* non-literal values pass through uncoerced
|
|
689
|
+
* non-literal values pass through uncoerced — except bindings flagged
|
|
690
|
+
* maybeNullish, which get a runtime nullish coerce (null-flow correctness). */
|
|
457
691
|
export function toNumF64(node, v) {
|
|
458
692
|
// An i32 node carrying `.ptrKind` is an *unboxed pointer* (object/array local),
|
|
459
693
|
// not a number — skipping coercion would reinterpret pointer bits as an f64.
|
|
460
694
|
// Only a plain i32 (loop counter, `x|0`) is genuinely already-numeric.
|
|
461
695
|
if ((v.type === 'i32' && v.ptrKind == null) || isLit(v)) return asF64(v)
|
|
462
|
-
|
|
696
|
+
// A binding assigned a nullish literal may hold null/undefined here — coerce per ToNumber
|
|
697
|
+
// (null→+0, undefined→NaN); a real number falls through unchanged. Only flagged bindings pay
|
|
698
|
+
// this, so the numeric kernels jz optimizes for (which never assign null) stay untouched.
|
|
699
|
+
if (typeof node === 'string' && ctx.func.maybeNullish?.has(node)) return coerceNullishToNum(asF64(v))
|
|
700
|
+
const vt = valTypeOf(node)
|
|
701
|
+
if (vt === VAL.BOOL) return typed(['f64.convert_i32_s', truthyIR(v)], 'f64')
|
|
463
702
|
if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
|
|
464
703
|
if (vt === VAL.DATE) {
|
|
465
704
|
const ptr = v.ptrKind === VAL.DATE
|
|
@@ -473,7 +712,7 @@ export function toNumF64(node, v) {
|
|
|
473
712
|
// yield non-primitives a TypeError is thrown. The chosen primitive still
|
|
474
713
|
// flows through `__to_num` so a string return ("−7") is parsed. An abrupt
|
|
475
714
|
// completion (throwing method) propagates through the closure call.
|
|
476
|
-
if (vt === VAL.OBJECT && ctx.closure.call && ctx.schema.
|
|
715
|
+
if (vt === VAL.OBJECT && ctx.closure.call && ctx.schema.slotOf) {
|
|
477
716
|
const prim = toPrimitiveChain(node, v, ['valueOf', 'toString'])
|
|
478
717
|
if (prim) {
|
|
479
718
|
// No `__to_num` helper → the program provably has no strings, so the
|
|
@@ -497,6 +736,19 @@ export function toNumF64(node, v) {
|
|
|
497
736
|
if (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u') return v
|
|
498
737
|
if (v[0] === 'call' && v[1] === '$__time_ms') return v
|
|
499
738
|
}
|
|
739
|
+
// f64 arithmetic ops and math intrinsics never produce NaN-boxed pointers — the
|
|
740
|
+
// result is always a plain f64 number. Skip __to_num for these, eliminating the
|
|
741
|
+
// call overhead that dominates tight numeric kernels (floatbeats, matrix loops).
|
|
742
|
+
// A `block`/`if` qualifies only when its value-producing tail is provably numeric
|
|
743
|
+
// (`isNumericIR`): `cond ? n*2 : n*3` skips, but `o.a?.b` (block yielding a
|
|
744
|
+
// property value / undef sentinel) does NOT — else `o.a?.b > 6` would compare the
|
|
745
|
+
// boxed string's NaN bits (NaN > 6 → false). User function calls are excluded too
|
|
746
|
+
// (may return dynamic-property strings); only $math.* is provably numeric.
|
|
747
|
+
if (v.type === 'f64' && Array.isArray(v) && (
|
|
748
|
+
PURE_F64_OPS.has(v[0]) ||
|
|
749
|
+
(v[0] === 'call' && typeof v[1] === 'string' && v[1].startsWith('$math.')) ||
|
|
750
|
+
((v[0] === 'block' || v[0] === 'if') && isNumericIR(v))
|
|
751
|
+
)) return v
|
|
500
752
|
if (!ctx.core.stdlib['__to_num']) {
|
|
501
753
|
// No full ToNumber helper loaded — the program provably has no strings.
|
|
502
754
|
// A nullish *literal* still coerces (null→+0, undefined→NaN) — fold it
|
|
@@ -525,8 +777,8 @@ export function toNumF64(node, v) {
|
|
|
525
777
|
* `__to_str` so a numeric return is rendered. A throwing method propagates as
|
|
526
778
|
* an abrupt completion through the closure call. */
|
|
527
779
|
export function toStrI64(node, v) {
|
|
528
|
-
const vt =
|
|
529
|
-
if (vt === VAL.OBJECT && ctx.closure.call && ctx.schema.
|
|
780
|
+
const vt = valTypeOf(node)
|
|
781
|
+
if (vt === VAL.OBJECT && ctx.closure.call && ctx.schema.slotOf) {
|
|
530
782
|
const prim = toPrimitiveChain(node, v, ['toString', 'valueOf'])
|
|
531
783
|
if (prim) {
|
|
532
784
|
inc('__to_str')
|
|
@@ -540,8 +792,37 @@ export function toStrI64(node, v) {
|
|
|
540
792
|
/** Convert already-emitted WASM node to i32 boolean. NaN is falsy (like JS).
|
|
541
793
|
* Peepholes: i32 → as-is; `f64.convert_i32_*(x)` → x (i32 conversion never NaN);
|
|
542
794
|
* nested `__is_truthy(x)` → x (already 0/1); literal f64 const folds to 0/1. */
|
|
795
|
+
// f64 ops whose result is always a plain NUMBER (never a NaN-boxed carrier) and can
|
|
796
|
+
// be NaN — their truthiness must test NaN by value, not by bit pattern (see truthyIR).
|
|
797
|
+
const NUM_F64_TRUTHY_OPS = new Set([
|
|
798
|
+
'f64.add', 'f64.sub', 'f64.mul', 'f64.div', 'f64.neg', 'f64.abs', 'f64.sqrt',
|
|
799
|
+
'f64.min', 'f64.max', 'f64.ceil', 'f64.floor', 'f64.trunc', 'f64.nearest', 'f64.copysign',
|
|
800
|
+
])
|
|
801
|
+
|
|
802
|
+
const numericTruthy = e => {
|
|
803
|
+
const t = temp('tb')
|
|
804
|
+
const g = () => typed(['local.get', `$${t}`], 'f64')
|
|
805
|
+
return typed(['block', ['result', 'i32'],
|
|
806
|
+
['local.set', `$${t}`, e],
|
|
807
|
+
['i32.and', ['f64.ne', g(), ['f64.const', 0]], ['f64.eq', g(), g()]]], 'i32')
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// i32 ops whose result is already a 0/1 boolean (comparisons + eqz) — safe to use
|
|
811
|
+
// directly as a truthiness without a redundant `!= 0`.
|
|
812
|
+
const I32_BOOL_OPS = new Set(['i32.eq', 'i32.ne', 'i32.lt_s', 'i32.lt_u', 'i32.gt_s', 'i32.gt_u',
|
|
813
|
+
'i32.le_s', 'i32.le_u', 'i32.ge_s', 'i32.ge_u', 'i32.eqz'])
|
|
814
|
+
|
|
543
815
|
export function truthyIR(e) {
|
|
544
|
-
|
|
816
|
+
// An i32 *constant* is a concrete number, not a known 0/1 boolean — fold it to its
|
|
817
|
+
// truthiness (nonzero → 1).
|
|
818
|
+
if (Array.isArray(e) && e[0] === 'i32.const') return typed(['i32.const', e[1] ? 1 : 0], 'i32')
|
|
819
|
+
if (e.type === 'i32') {
|
|
820
|
+
// A comparison/eqz result is already 0/1 → use directly. Any *other* i32 may be a
|
|
821
|
+
// concrete narrowed integer (e.g. `Boolean(n)` where n is an i32 number), which is
|
|
822
|
+
// NOT a 0/1 boolean — normalize via `!= 0` so its truthiness is correct.
|
|
823
|
+
if (Array.isArray(e) && I32_BOOL_OPS.has(e[0])) return e
|
|
824
|
+
return typed(['i32.ne', e, ['i32.const', 0]], 'i32')
|
|
825
|
+
}
|
|
545
826
|
// Unboxed pointer offsets: truthy iff non-zero offset.
|
|
546
827
|
if (e.ptrKind != null) return typed(['i32.ne', e, ['i32.const', 0]], 'i32')
|
|
547
828
|
if (Array.isArray(e)) {
|
|
@@ -563,7 +844,7 @@ export function truthyIR(e) {
|
|
|
563
844
|
// all other NaN-boxed pointers (SSO strings, heap ptrs, etc.) are truthy.
|
|
564
845
|
if (e[0] === 'f64.reinterpret_i64' && Array.isArray(e[1]) && e[1][0] === 'i64.const') {
|
|
565
846
|
const bits = String(e[1][1])
|
|
566
|
-
const FALSY = new Set([UNDEF_NAN, NULL_NAN, FALSE_NAN,
|
|
847
|
+
const FALSY = new Set([UNDEF_NAN, NULL_NAN, FALSE_NAN, nanPrefixHex(), '0x7FFA400000000000'])
|
|
567
848
|
return typed(['i32.const', FALSY.has(bits) ? 0 : 1], 'i32')
|
|
568
849
|
}
|
|
569
850
|
// Fresh pointer constructors never produce nullish. Treat as always truthy.
|
|
@@ -581,8 +862,25 @@ export function truthyIR(e) {
|
|
|
581
862
|
vt === VAL.CLOSURE || vt === VAL.TYPED || vt === VAL.BUFFER || vt === VAL.REGEX || vt === VAL.DATE) {
|
|
582
863
|
return typed(['i32.eqz', isNullish(e)], 'i32')
|
|
583
864
|
}
|
|
865
|
+
// A plain NUMBER is truthy iff non-zero AND not NaN. `f64.eq x x` tests NaN by
|
|
866
|
+
// VALUE (false for ANY NaN bits), so this is correct on every platform — unlike
|
|
867
|
+
// __is_truthy, which bit-compares the canonical number-NaN and so mis-reads
|
|
868
|
+
// x86's sign-set 0xFFF8.. NaN (from f64.div(0,0) / %) as a truthy box. (local.get
|
|
869
|
+
// is pure → duplicated, not teed.) Bigint carriers are reinterpret/i64 shapes
|
|
870
|
+
// and never reach here as VAL.NUMBER.
|
|
871
|
+
if (vt === VAL.NUMBER) {
|
|
872
|
+
const g = () => typed(['local.get', e[1]], 'f64')
|
|
873
|
+
return typed(['i32.and', ['f64.ne', g(), ['f64.const', 0]], ['f64.eq', g(), g()]], 'i32')
|
|
874
|
+
}
|
|
584
875
|
}
|
|
876
|
+
// Direct number-producing f64 expression (arithmetic, or the `%` / __rem helper):
|
|
877
|
+
// same NaN-safe test, single-evaluated through a temp (the value may be a call).
|
|
878
|
+
if (NUM_F64_TRUTHY_OPS.has(e[0]) || (e[0] === 'call' && e[1] === '$__rem')) return numericTruthy(e)
|
|
585
879
|
}
|
|
880
|
+
// Composite IR tagged by emit as a definite NUMBER. Use value-based NaN
|
|
881
|
+
// truthiness; opaque f64 carriers (strings/objects/bigints/nullish/booleans)
|
|
882
|
+
// remain on __is_truthy so NaN-boxed payloads stay truthy/falsy by tag.
|
|
883
|
+
if (e.valKind === VAL.NUMBER) return numericTruthy(e)
|
|
586
884
|
inc('__is_truthy')
|
|
587
885
|
return typed(['call', '$__is_truthy', asI64(e)], 'i32')
|
|
588
886
|
}
|
|
@@ -590,10 +888,6 @@ export const toBoolFromEmitted = truthyIR
|
|
|
590
888
|
|
|
591
889
|
// === Value-type classification ===
|
|
592
890
|
|
|
593
|
-
export function keyValType(node) {
|
|
594
|
-
return typeof node === 'string' ? lookupValType(node) : valTypeOf(node)
|
|
595
|
-
}
|
|
596
|
-
|
|
597
891
|
export function usesDynProps(vt) {
|
|
598
892
|
return vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.CLOSURE
|
|
599
893
|
|| vt === VAL.TYPED || vt === VAL.SET || vt === VAL.MAP || vt === VAL.REGEX
|
|
@@ -607,15 +901,27 @@ export function needsDynShadow(target) {
|
|
|
607
901
|
// access (fn.parse, i32.parse aliases) sees the same value as schema slots.
|
|
608
902
|
const vt = typeof target === 'string' ? (ctx.func.localReps?.get(target)?.val || ctx.scope.globalValTypes?.get(target)) : null
|
|
609
903
|
if (vt === 'closure' || usesDynProps(vt)) return true
|
|
904
|
+
// A module-wide dynamic-key access (`obj[expr]`) means ANY object may later be
|
|
905
|
+
// read through the dyn-props hash (__dyn_get_any), so every object literal is
|
|
906
|
+
// built with a shadow. Mutation sites (Object.assign, `o.k = v`) must mirror
|
|
907
|
+
// into that same shadow or a subsequent hash read returns a stale slot value.
|
|
908
|
+
// Honor anyDynKey for NAMED targets too — not just anonymous (target == null)
|
|
909
|
+
// literals — so construct-time shadowing and mutate-time mirroring agree. They
|
|
910
|
+
// desynced before: a named literal shadowed via anyDynKey, but its assign saw
|
|
911
|
+
// only dynKeyVars (which holds the *dynamically-keyed* vars, not this binding).
|
|
912
|
+
if (ctx.types?.anyDynKey) return true
|
|
610
913
|
const dyn = ctx.types?.dynKeyVars
|
|
611
|
-
|
|
612
|
-
return dyn ? dyn.has(target) : false
|
|
914
|
+
return target != null && dyn ? dyn.has(target) : false
|
|
613
915
|
}
|
|
614
916
|
|
|
615
917
|
// === Variable storage abstraction ===
|
|
616
918
|
// Centralizes the boxed/global/local 3-way dispatch (used by =, ++/--, +=, etc.)
|
|
617
919
|
|
|
618
920
|
/** Check if name is a module-scope global (not shadowed by local/param). */
|
|
921
|
+
/** Bound in the current function frame — a declared local or a parameter. */
|
|
922
|
+
export const isBoundName = name =>
|
|
923
|
+
ctx.func.locals?.has(name) || ctx.func.current?.params?.some(p => p.name === name)
|
|
924
|
+
|
|
619
925
|
export function isGlobal(name) {
|
|
620
926
|
return ctx.scope.globals.has(name) && !ctx.func.locals?.has(name) && !ctx.func.current?.params?.some(p => p.name === name)
|
|
621
927
|
}
|
|
@@ -630,16 +936,43 @@ export function boxedAddr(name) {
|
|
|
630
936
|
return ['local.get', `$${ctx.func.boxed.get(name)}`]
|
|
631
937
|
}
|
|
632
938
|
|
|
939
|
+
// '$'-prefixed name memo. readVar/writeVar run per IR node; rebuilding the
|
|
940
|
+
// `$name` string each time costs an alloc+copy in the self-host kernel AND
|
|
941
|
+
// produces a fresh instance per use — making watr's name-keyed lookups
|
|
942
|
+
// content-compare. The memo returns ONE canonical instance per name, so
|
|
943
|
+
// construction is a map hit and every downstream comparison is bit-eq.
|
|
944
|
+
// Module-level: in-kernel it lives per instance (arena strings are immortal),
|
|
945
|
+
// natively it is a plain cross-compile cache; the name vocabulary is bounded.
|
|
946
|
+
const DOLLAR = new Map()
|
|
947
|
+
export const dollar = (name) => {
|
|
948
|
+
let v = DOLLAR.get(name)
|
|
949
|
+
if (v === undefined) { v = '$' + name; DOLLAR.set(name, v) }
|
|
950
|
+
return v
|
|
951
|
+
}
|
|
952
|
+
|
|
633
953
|
/** Read variable value: boxed → f64.load, global → global.get, local → local.get.
|
|
634
954
|
* Unboxed pointer locals (repOf(name).ptrKind) tag the returned node with `.ptrKind`
|
|
635
955
|
* so downstream coercions know it's an i32 offset, not a numeric. */
|
|
636
956
|
export function readVar(name) {
|
|
637
|
-
if (ctx.func.boxed?.has(name))
|
|
957
|
+
if (ctx.func.boxed?.has(name)) {
|
|
958
|
+
// i32-narrowed cell (closure-capture narrowing — see analyzeFuncForEmit's
|
|
959
|
+
// cellTypes): the cell stores a raw i32, load it directly.
|
|
960
|
+
if (ctx.func.cellTypes?.has(name)) return typed(['i32.load', boxedAddr(name)], 'i32')
|
|
638
961
|
return typed(['f64.load', boxedAddr(name)], 'f64')
|
|
962
|
+
}
|
|
639
963
|
if (isGlobal(name)) {
|
|
640
|
-
const
|
|
964
|
+
const gt = ctx.scope.globalTypes.get(name) || 'f64'
|
|
965
|
+
const node = typed(['global.get', dollar(name)], gt)
|
|
641
966
|
const grep = repOfGlobal(name)
|
|
642
|
-
if (grep?.
|
|
967
|
+
if (gt === 'f64' && (lookupValType(name) === VAL.NUMBER || grep?.val === VAL.NUMBER)) node.valKind = VAL.NUMBER
|
|
968
|
+
// ptrKind tags a raw i32 pointer offset — meaningful only for an i32-STORED
|
|
969
|
+
// global (a typed-array/buffer carrier unboxed by unboxConstTypedGlobals). An
|
|
970
|
+
// f64 global holds a NaN-boxed value: object/array reads unbox at the access
|
|
971
|
+
// site via the schema/reinterpret path, never an i32 reinterpret of the storage.
|
|
972
|
+
// Attaching ptrKind to an f64 global makes `asF64` box the f64 *as if it were an
|
|
973
|
+
// i32* (i64.extend_i32_u on a global.get of type f64 → invalid wasm). Gate on the
|
|
974
|
+
// storage type so the tag follows the declared ABI.
|
|
975
|
+
if (gt === 'i32' && grep?.ptrKind != null) {
|
|
643
976
|
node.ptrKind = grep.ptrKind
|
|
644
977
|
if (grep.ptrAux != null) node.ptrAux = grep.ptrAux
|
|
645
978
|
}
|
|
@@ -657,7 +990,8 @@ export function readVar(name) {
|
|
|
657
990
|
return t === 'i32' ? typed(['i32.const', rep.intConst], 'i32')
|
|
658
991
|
: typed(['f64.const', rep.intConst], 'f64')
|
|
659
992
|
}
|
|
660
|
-
const node = typed(['local.get',
|
|
993
|
+
const node = typed(['local.get', dollar(name)], t)
|
|
994
|
+
if (t === 'f64' && (lookupValType(name) === VAL.NUMBER || rep?.val === VAL.NUMBER)) node.valKind = VAL.NUMBER
|
|
661
995
|
// Proven uint32 accumulator local (narrowUint32): a later asF64 must widen with
|
|
662
996
|
// convert_i32_u (the i32 bit pattern is an unsigned value), not _s. `.wrapSafe`
|
|
663
997
|
// marks it as the always-ToUint32-sunk kind so the arithmetic widening guards
|
|
@@ -676,22 +1010,29 @@ export function readVar(name) {
|
|
|
676
1010
|
export function writeVar(name, valIR, void_) {
|
|
677
1011
|
if (ctx.func.boxed?.has(name)) {
|
|
678
1012
|
const addr = boxedAddr(name)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
1013
|
+
// i32-narrowed cell: store the raw i32 (mirrors the integer-global write
|
|
1014
|
+
// gate below — the storage type decides the coercion).
|
|
1015
|
+
const i32Cell = ctx.func.cellTypes?.has(name)
|
|
1016
|
+
const st = i32Cell ? 'i32.store' : 'f64.store'
|
|
1017
|
+
const v = i32Cell ? asI32(valIR) : asF64(valIR)
|
|
1018
|
+
if (void_) return typed(['block', [st, addr, v]], 'void')
|
|
1019
|
+
const t = i32Cell ? tempI32() : temp()
|
|
1020
|
+
return typed(['block', ['result', i32Cell ? 'i32' : 'f64'],
|
|
683
1021
|
['local.set', `$${t}`, v],
|
|
684
|
-
[
|
|
685
|
-
['local.get', `$${t}`]], 'f64')
|
|
1022
|
+
[st, addr, ['local.get', `$${t}`]],
|
|
1023
|
+
['local.get', `$${t}`]], i32Cell ? 'i32' : 'f64')
|
|
686
1024
|
}
|
|
687
1025
|
if (isGlobal(name)) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
1026
|
+
// Scalar globals are f64 by default, but integer-global inference (plan.js)
|
|
1027
|
+
// narrows purpose-focused counters/sizes to i32 — coerce the write to match.
|
|
1028
|
+
const gt = ctx.scope.globalTypes.get(name) || 'f64'
|
|
1029
|
+
const v = gt === 'i32' ? asI32(valIR) : asF64(valIR)
|
|
1030
|
+
if (void_) return typed(['block', ['global.set', dollar(name), v]], 'void')
|
|
1031
|
+
const t = gt === 'i32' ? tempI32() : temp()
|
|
1032
|
+
return typed(['block', ['result', gt],
|
|
692
1033
|
['local.set', `$${t}`, v],
|
|
693
|
-
['global.set',
|
|
694
|
-
['local.get', `$${t}`]],
|
|
1034
|
+
['global.set', dollar(name), ['local.get', `$${t}`]],
|
|
1035
|
+
['local.get', `$${t}`]], gt)
|
|
695
1036
|
}
|
|
696
1037
|
const t = ctx.func.locals.get(name) || 'f64'
|
|
697
1038
|
const ptrKind = repOf(name)?.ptrKind
|
|
@@ -703,10 +1044,10 @@ export function writeVar(name, valIR, void_) {
|
|
|
703
1044
|
? valIR
|
|
704
1045
|
: typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(valIR)]], 'i32')
|
|
705
1046
|
} else {
|
|
706
|
-
coerced = t === 'f64' ? asF64(valIR) : asI32(valIR)
|
|
1047
|
+
coerced = t === 'v128' ? valIR : t === 'f64' ? asF64(valIR) : asI32(valIR)
|
|
707
1048
|
}
|
|
708
|
-
if (void_) return typed(['local.set',
|
|
709
|
-
const teeNode = typed(['local.tee',
|
|
1049
|
+
if (void_) return typed(['local.set', dollar(name), coerced], 'void')
|
|
1050
|
+
const teeNode = typed(['local.tee', dollar(name), coerced], t)
|
|
710
1051
|
if (ptrKind != null) teeNode.ptrKind = ptrKind
|
|
711
1052
|
return teeNode
|
|
712
1053
|
}
|
|
@@ -716,57 +1057,52 @@ export function writeVar(name, valIR, void_) {
|
|
|
716
1057
|
* unboxed pointer locals are proven non-null by unboxablePtrs.
|
|
717
1058
|
* Inlines directly: (i32.or (i64.eq bits NULL_NAN) (i64.eq bits UNDEF_NAN))
|
|
718
1059
|
* rather than calling $__is_nullish — saves WASM call dispatch in V8 JIT. */
|
|
719
|
-
|
|
720
|
-
|
|
1060
|
+
// Shared peephole for the NaN-box sentinel checks. When the operand's bits are
|
|
1061
|
+
// statically known — an unboxed pointer (never an atom → 0), a numeric `f64.const`
|
|
1062
|
+
// (never an atom → 0), or a boxed `(f64.const nan:…)` / `(f64.reinterpret_i64
|
|
1063
|
+
// (i64.const …))` literal — resolve `onBits(bitsHex)` / 0 at compile time; else
|
|
1064
|
+
// hand the expr to `fallback` for the runtime test. One place owns the literal set.
|
|
1065
|
+
const constI32 = (b) => typed(['i32.const', b ? 1 : 0], 'i32')
|
|
1066
|
+
const matchF64Bits = (f64expr, onBits, fallback) => {
|
|
1067
|
+
if (f64expr.ptrKind != null) return constI32(0)
|
|
721
1068
|
if (Array.isArray(f64expr)) {
|
|
722
1069
|
if (f64expr[0] === 'f64.const') {
|
|
723
|
-
// Check for NaN-boxed sentinel: (f64.const nan:0x...) — matches NULL_IR/UNDEF_IR form.
|
|
724
1070
|
const lit = String(f64expr[1])
|
|
725
|
-
|
|
726
|
-
const bits = lit.slice(4)
|
|
727
|
-
return typed(['i32.const', (bits === NULL_NAN || bits === UNDEF_NAN) ? 1 : 0], 'i32')
|
|
728
|
-
}
|
|
729
|
-
return typed(['i32.const', 0], 'i32') // numeric literal — never nullish
|
|
1071
|
+
return lit.startsWith('nan:') ? onBits(lit.slice(4)) : constI32(0)
|
|
730
1072
|
}
|
|
731
|
-
if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const')
|
|
732
|
-
|
|
733
|
-
return typed(['i32.const', (bits === NULL_NAN || bits === UNDEF_NAN) ? 1 : 0], 'i32')
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// Inline the 3-op nullish test. Tee into a temp i64 so the value is computed once.
|
|
737
|
-
// For simple (local.get $x) we can just reinterpret twice — V8 CSEs it.
|
|
738
|
-
if (Array.isArray(f64expr) && f64expr[0] === 'local.get') {
|
|
739
|
-
const bits = ['i64.reinterpret_f64', f64expr]
|
|
740
|
-
return typed(['i32.or',
|
|
741
|
-
['i64.eq', bits, ['i64.const', NULL_NAN]],
|
|
742
|
-
['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]]], 'i32')
|
|
1073
|
+
if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const')
|
|
1074
|
+
return onBits(String(f64expr[1][1]))
|
|
743
1075
|
}
|
|
744
|
-
|
|
745
|
-
inc('__is_nullish')
|
|
746
|
-
return typed(['call', '$__is_nullish', ['i64.reinterpret_f64', f64expr]], 'i32')
|
|
1076
|
+
return fallback(f64expr)
|
|
747
1077
|
}
|
|
748
1078
|
|
|
1079
|
+
export const isNullish = (f64expr) => matchF64Bits(f64expr,
|
|
1080
|
+
bits => constI32(bits === NULL_NAN || bits === UNDEF_NAN),
|
|
1081
|
+
(e) => {
|
|
1082
|
+
// (local.get $x): inline the test, reinterpreting twice (V8 CSEs it). Other
|
|
1083
|
+
// exprs call $__is_nullish — keeps binary size stable and evaluates once.
|
|
1084
|
+
if (Array.isArray(e) && e[0] === 'local.get') {
|
|
1085
|
+
const bits = ['i64.reinterpret_f64', e]
|
|
1086
|
+
return typed(['i32.or',
|
|
1087
|
+
['i64.eq', bits, ['i64.const', NULL_NAN]],
|
|
1088
|
+
['i64.eq', ['i64.reinterpret_f64', e], ['i64.const', UNDEF_NAN]]], 'i32')
|
|
1089
|
+
}
|
|
1090
|
+
inc('__is_nullish')
|
|
1091
|
+
return typed(['call', '$__is_nullish', ['i64.reinterpret_f64', e]], 'i32')
|
|
1092
|
+
})
|
|
1093
|
+
|
|
749
1094
|
/** Check if f64 expr is exactly `undefined` (UNDEF_NAN). Returns i32.
|
|
750
1095
|
* Used by default-param semantics — only `undefined` (or missing arg) triggers
|
|
751
1096
|
* the default; `null` should pass through. */
|
|
752
|
-
export const isUndef = (f64expr) =>
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
return typed(['i32.const', 0], 'i32')
|
|
762
|
-
}
|
|
763
|
-
if (f64expr[0] === 'f64.reinterpret_i64' && Array.isArray(f64expr[1]) && f64expr[1][0] === 'i64.const') {
|
|
764
|
-
const bits = String(f64expr[1][1])
|
|
765
|
-
return typed(['i32.const', bits === UNDEF_NAN ? 1 : 0], 'i32')
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
return typed(['i64.eq', ['i64.reinterpret_f64', f64expr], ['i64.const', UNDEF_NAN]], 'i32')
|
|
769
|
-
}
|
|
1097
|
+
export const isUndef = (f64expr) => matchF64Bits(f64expr,
|
|
1098
|
+
bits => constI32(bits === UNDEF_NAN),
|
|
1099
|
+
(e) => typed(['i64.eq', ['i64.reinterpret_f64', e], ['i64.const', UNDEF_NAN]], 'i32'))
|
|
1100
|
+
|
|
1101
|
+
/** Check if f64 expr is exactly `null` (NULL_NAN). Returns i32.
|
|
1102
|
+
* Strict `=== null` must match only null — not undefined (use isUndef for that). */
|
|
1103
|
+
export const isNull = (f64expr) => matchF64Bits(f64expr,
|
|
1104
|
+
bits => constI32(bits === NULL_NAN),
|
|
1105
|
+
(e) => typed(['i64.eq', ['i64.reinterpret_f64', e], ['i64.const', NULL_NAN]], 'i32'))
|
|
770
1106
|
|
|
771
1107
|
/** Mask that clears the boolean atom's truth bit, mapping TRUE_NAN→FALSE_NAN.
|
|
772
1108
|
* `(bits & BOOL_ATOM_MASK) === FALSE_NAN` recognizes both in one i64.and+i64.eq. */
|
|
@@ -774,16 +1110,11 @@ const BOOL_ATOM_MASK = '0x' + BigInt.asUintN(64, ~(1n << BigInt(LAYOUT.AUX_SHIFT
|
|
|
774
1110
|
|
|
775
1111
|
/** Check if f64 expr is a boxed-boolean atom (TRUE_NAN or FALSE_NAN). Returns i32.
|
|
776
1112
|
* Single-eval: masks the truth bit and compares to FALSE_NAN once. */
|
|
777
|
-
export const isBoolAtom = (f64expr) =>
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
}
|
|
1113
|
+
export const isBoolAtom = (f64expr) => matchF64Bits(f64expr,
|
|
1114
|
+
bits => constI32(bits === TRUE_NAN || bits === FALSE_NAN),
|
|
1115
|
+
(e) => typed(['i64.eq',
|
|
1116
|
+
['i64.and', ['i64.reinterpret_f64', e], ['i64.const', BOOL_ATOM_MASK]],
|
|
1117
|
+
['i64.const', FALSE_NAN]], 'i32'))
|
|
787
1118
|
|
|
788
1119
|
// === Array layout helpers — routed through the array carrier (abi/array.js) ===
|
|
789
1120
|
|
|
@@ -813,7 +1144,7 @@ export function elemStore(ptr, i, val) {
|
|
|
813
1144
|
* re-loading from ptr-8.
|
|
814
1145
|
* Optional `ptrLocal`: caller already has the resolved ARRAY data pointer in
|
|
815
1146
|
* an i32 local. Reuses it instead of calling __ptr_offset again. */
|
|
816
|
-
export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
|
|
1147
|
+
export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal, reverse) {
|
|
817
1148
|
const arr = ptrLocal ? null : temp('aa'), ptr = ptrLocal ?? tempI32('ap'), i = tempI32('ai'), item = temp('av')
|
|
818
1149
|
const len = lenLocal ?? tempI32('al')
|
|
819
1150
|
const id = ctx.func.uniq++
|
|
@@ -827,13 +1158,18 @@ export function arrayLoop(arrExpr, bodyFn, lenLocal, ptrLocal) {
|
|
|
827
1158
|
}
|
|
828
1159
|
if (!lenLocal) setup.push(
|
|
829
1160
|
['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]])
|
|
1161
|
+
// Forward: i 0→len-1. Reverse (findLast*): i len-1→0, same elem indices.
|
|
1162
|
+
const start = reverse ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]
|
|
1163
|
+
const done = reverse ? ['i32.lt_s', ['local.get', `$${i}`], ['i32.const', 0]]
|
|
1164
|
+
: ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]
|
|
1165
|
+
const step = ['i32.const', reverse ? -1 : 1]
|
|
830
1166
|
setup.push(
|
|
831
|
-
['local.set', `$${i}`,
|
|
1167
|
+
['local.set', `$${i}`, start],
|
|
832
1168
|
['block', `$brk${id}`, ['loop', `$loop${id}`,
|
|
833
|
-
['br_if', `$brk${id}`,
|
|
1169
|
+
['br_if', `$brk${id}`, done],
|
|
834
1170
|
['local.set', `$${item}`, elemLoad(ptr, i)],
|
|
835
1171
|
...bodyFn(ptr, len, i, typed(['local.get', `$${item}`], 'f64')),
|
|
836
|
-
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`],
|
|
1172
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], step]],
|
|
837
1173
|
['br', `$loop${id}`]]])
|
|
838
1174
|
return setup
|
|
839
1175
|
}
|
|
@@ -885,6 +1221,7 @@ export function loopTop() {
|
|
|
885
1221
|
export const flat = ir => {
|
|
886
1222
|
if (ir == null) return []
|
|
887
1223
|
if (!Array.isArray(ir)) return [ir] // bare 'drop', 'nop', etc.
|
|
1224
|
+
if (ir.length === 0) return []
|
|
888
1225
|
if (typeof ir[0] === 'string' || ir[0] == null) return [ir] // single instruction: ['op', ...args] or [null, val]
|
|
889
1226
|
return ir // multi-instruction: [instr1, instr2, ...]
|
|
890
1227
|
}
|
|
@@ -906,6 +1243,38 @@ export function findBodyStart(fn) {
|
|
|
906
1243
|
return fn.length
|
|
907
1244
|
}
|
|
908
1245
|
|
|
1246
|
+
/** Debug-mode structural check of a `(func …)` IR node. Catches the bug classes
|
|
1247
|
+
* that otherwise surface as OPAQUE watr errors several phases later — `Duplicate
|
|
1248
|
+
* local $x`, `Unknown local $x` — but here pinned to the exact name (and, via the
|
|
1249
|
+
* caller, the phase + function) that produced them, so a codegen/optimizer bug is
|
|
1250
|
+
* localized at its source instead of at watr. Self-contained: validates every
|
|
1251
|
+
* `local.{get,set,tee}` against the function header's param/local declarations,
|
|
1252
|
+
* and rejects a duplicate declaration. Returns an error string, or null if clean.
|
|
1253
|
+
* (Call-target and type-tag validation need the module symbol table + a type pass;
|
|
1254
|
+
* deferred — locals are the common codegen-bug class and need nothing external.) */
|
|
1255
|
+
export function verifyFn(fn) {
|
|
1256
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return null
|
|
1257
|
+
const bodyStart = findBodyStart(fn)
|
|
1258
|
+
const declared = new Set()
|
|
1259
|
+
for (let i = 2; i < bodyStart; i++) {
|
|
1260
|
+
const c = fn[i]
|
|
1261
|
+
if (!Array.isArray(c) || (c[0] !== 'param' && c[0] !== 'local') || typeof c[1] !== 'string') continue
|
|
1262
|
+
if (declared.has(c[1])) return `duplicate local/param ${c[1]}`
|
|
1263
|
+
declared.add(c[1])
|
|
1264
|
+
}
|
|
1265
|
+
let bad = null
|
|
1266
|
+
const walk = (n) => {
|
|
1267
|
+
if (bad || !Array.isArray(n)) return
|
|
1268
|
+
const op = n[0]
|
|
1269
|
+
if ((op === 'local.get' || op === 'local.set' || op === 'local.tee') && typeof n[1] === 'string' && !declared.has(n[1])) {
|
|
1270
|
+
bad = `${op} of undeclared local ${n[1]}`; return
|
|
1271
|
+
}
|
|
1272
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
1273
|
+
}
|
|
1274
|
+
for (let i = bodyStart; i < fn.length; i++) walk(fn[i])
|
|
1275
|
+
return bad
|
|
1276
|
+
}
|
|
1277
|
+
|
|
909
1278
|
/**
|
|
910
1279
|
* Tail-call rewrite: walks tail positions of an emitted IR tree and replaces
|
|
911
1280
|
* direct `(call $name args...)` ops with `(return_call $name args...)`.
|