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