jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/src/ir.js
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { ctx, err, inc, PTR, LAYOUT } from './ctx.js'
|
|
24
|
-
import { ptrBoxPrefixBigInt, atomNanHex, nanPrefixHex } from '../layout.js'
|
|
25
|
-
import { I32_MIN, I32_MAX, isI32, isLiteralStr, isFuncRef } from './ast.js'
|
|
24
|
+
import { ptrBoxPrefixBigInt, ptrBits, i64Hex, atomNanHex, nanPrefixHex } from '../layout.js'
|
|
25
|
+
import { I32_MIN, I32_MAX, isI32, isLiteralStr, isFuncRef, isLeaf } from './ast.js'
|
|
26
26
|
import { VAL, lookupValType, repOf, repOfGlobal } from './reps.js'
|
|
27
27
|
import { valTypeOf } from './kind.js'
|
|
28
28
|
import { T } from './ast.js'
|
|
@@ -55,6 +55,15 @@ function boxPtrIR(i32node, ptrType, aux = 0) {
|
|
|
55
55
|
* `(x >>> 0)` uint32 idiom converts to a positive f64 in [0, 2^32) instead of sign-flipping. */
|
|
56
56
|
export const asF64 = n => {
|
|
57
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.`)
|
|
58
67
|
if (n.ptrKind != null) return boxPtrIR(n, valKindToPtr(n.ptrKind), n.ptrAux || 0)
|
|
59
68
|
if (n.type === 'f64') return n
|
|
60
69
|
if (n.type === 'i64') {
|
|
@@ -97,6 +106,22 @@ export const asPtrOffset = (n, ptrKind) => {
|
|
|
97
106
|
/** Coerce emitted IR to a target WASM param type ('i32' | 'i64' | 'f64'). */
|
|
98
107
|
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : t === 'v128' ? n : asF64(n)
|
|
99
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
|
+
|
|
100
125
|
/**
|
|
101
126
|
* Narrow an f64 arithmetic tree under ToInt32 — the general int-accumulator path.
|
|
102
127
|
*
|
|
@@ -157,6 +182,82 @@ const narrowI32 = (x, isRoot) => {
|
|
|
157
182
|
return null
|
|
158
183
|
}
|
|
159
184
|
|
|
185
|
+
// Conservative VALUE-RANGE for a pure f64 expression tree: returns { lo, hi } bounding
|
|
186
|
+
// every value the node can take, or null when unknown. SOUND by construction — each rule
|
|
187
|
+
// over-approximates using the SAME f64 ops the runtime uses (f64 +/−/× are monotone in
|
|
188
|
+
// each argument, so combining endpoint-bounds with plain JS doubles yields bounds that
|
|
189
|
+
// contain the true value). A null/non-finite endpoint anywhere collapses to null, so a
|
|
190
|
+
// non-null result also PROVES the value is finite (never NaN, never ±∞). Used by toI32 to
|
|
191
|
+
// drop the +∞-guard `select` (and the i64 round-trip when the value fits i32) — the guard
|
|
192
|
+
// exists only to remap +∞→0, so a proof of finiteness retires it.
|
|
193
|
+
const fin = (lo, hi) => (Number.isFinite(lo) && Number.isFinite(hi) && lo <= hi) ? { lo, hi } : null
|
|
194
|
+
// Range of the i32 that feeds an `f64.convert_i32_*`, refined by a narrowing load width.
|
|
195
|
+
const convRange = (child, signed) => {
|
|
196
|
+
if (Array.isArray(child)) {
|
|
197
|
+
const o = child[0]
|
|
198
|
+
if (o === 'i32.load8_u') return { lo: 0, hi: 255 }
|
|
199
|
+
if (o === 'i32.load8_s') return { lo: -128, hi: 127 }
|
|
200
|
+
if (o === 'i32.load16_u') return { lo: 0, hi: 65535 }
|
|
201
|
+
if (o === 'i32.load16_s') return { lo: -32768, hi: 32767 }
|
|
202
|
+
if (o === 'i32.const' && typeof child[1] === 'number') return signed ? { lo: child[1] | 0, hi: child[1] | 0 } : { lo: child[1] >>> 0, hi: child[1] >>> 0 }
|
|
203
|
+
}
|
|
204
|
+
return signed ? { lo: I32_MIN, hi: I32_MAX } : { lo: 0, hi: 4294967295 }
|
|
205
|
+
}
|
|
206
|
+
// `get`, when supplied, resolves a `(local.get $V)` to $V's single defining value node
|
|
207
|
+
// (a `name → defExpr | null` map/function built from a one-def-per-local scan). This lets the
|
|
208
|
+
// range see through the temps that inlining introduces — e.g. `floor(mul(convert($px),0.03125))`
|
|
209
|
+
// stashed in `$xi` before truncation — so the i32-fit proof survives the indirection. SOUND
|
|
210
|
+
// without code motion: a single-textual-def local holds exactly the value its def computes, so
|
|
211
|
+
// the def's range bounds every value the local can take, even if the def's inputs vary across
|
|
212
|
+
// iterations. A self-referential (loop-carried) single def is caught by the `seen` cycle guard
|
|
213
|
+
// and yields null (unknown), which is conservative.
|
|
214
|
+
export const f64Range = (n, get) => {
|
|
215
|
+
const seen = get ? new Set() : null
|
|
216
|
+
const r = (n) => {
|
|
217
|
+
if (!Array.isArray(n)) return null
|
|
218
|
+
const op = n[0]
|
|
219
|
+
if (op === 'local.get' && get && typeof n[1] === 'string') {
|
|
220
|
+
if (seen.has(n[1])) return null // loop-carried / cyclic def → unknown
|
|
221
|
+
const def = typeof get === 'function' ? get(n[1]) : get.get(n[1])
|
|
222
|
+
if (!def) return null
|
|
223
|
+
seen.add(n[1]); const rng = r(def); seen.delete(n[1])
|
|
224
|
+
return rng
|
|
225
|
+
}
|
|
226
|
+
if (op === 'f64.const') return typeof n[1] === 'number' ? fin(n[1], n[1]) : null // `nan:…`/Inf literal strings → null
|
|
227
|
+
if (op === 'f64.convert_i32_s') return convRange(n[1], true)
|
|
228
|
+
if (op === 'f64.convert_i32_u') return convRange(n[1], false)
|
|
229
|
+
if (op === 'f64.neg') { const a = r(n[1]); return a && fin(-a.hi, -a.lo) }
|
|
230
|
+
if (op === 'f64.abs') { const a = r(n[1]); return a && fin(a.lo > 0 ? a.lo : a.hi < 0 ? -a.hi : 0, Math.max(-a.lo, a.hi)) }
|
|
231
|
+
if (op === 'f64.sqrt') { const a = r(n[1]); return a && a.lo >= 0 && fin(Math.sqrt(a.lo), Math.sqrt(a.hi)) }
|
|
232
|
+
// Rounding ops preserve finiteness and are monotonic, so the range maps elementwise. This lets
|
|
233
|
+
// `Math.floor(x)|0` over a bounded x (every grid/image/audio index: `px*scale`, perm[] lookups)
|
|
234
|
+
// drop the +∞-guard + i64 round-trip in toI32 down to a single i32.trunc_sat_f64_s. `nearest`
|
|
235
|
+
// (round-half-to-even) lands in {floor,ceil} so its bounds are floor(lo)..ceil(hi).
|
|
236
|
+
if (op === 'f64.floor') { const a = r(n[1]); return a && fin(Math.floor(a.lo), Math.floor(a.hi)) }
|
|
237
|
+
if (op === 'f64.ceil') { const a = r(n[1]); return a && fin(Math.ceil(a.lo), Math.ceil(a.hi)) }
|
|
238
|
+
if (op === 'f64.trunc') { const a = r(n[1]); return a && fin(Math.trunc(a.lo), Math.trunc(a.hi)) }
|
|
239
|
+
if (op === 'f64.nearest') { const a = r(n[1]); return a && fin(Math.floor(a.lo), Math.ceil(a.hi)) }
|
|
240
|
+
if (op === 'f64.add') { const a = r(n[1]), b = r(n[2]); return a && b && fin(a.lo + b.lo, a.hi + b.hi) }
|
|
241
|
+
if (op === 'f64.sub') { const a = r(n[1]), b = r(n[2]); return a && b && fin(a.lo - b.hi, a.hi - b.lo) }
|
|
242
|
+
if (op === 'f64.mul') {
|
|
243
|
+
const a = r(n[1]), b = r(n[2]); if (!a || !b) return null
|
|
244
|
+
const p = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi]
|
|
245
|
+
return fin(Math.min(...p), Math.max(...p))
|
|
246
|
+
}
|
|
247
|
+
if (op === 'f64.div') {
|
|
248
|
+
const c = Array.isArray(n[2]) && n[2][0] === 'f64.const' && typeof n[2][1] === 'number' ? n[2][1] : null
|
|
249
|
+
if (c == null || c === 0) return null // variable / zero divisor → may be ±∞
|
|
250
|
+
const a = r(n[1]); if (!a) return null
|
|
251
|
+
const p = [a.lo / c, a.hi / c]
|
|
252
|
+
return fin(Math.min(...p), Math.max(...p))
|
|
253
|
+
}
|
|
254
|
+
if (op === 'f64.min') { const a = r(n[1]), b = r(n[2]); return a && b && fin(Math.min(a.lo, b.lo), Math.min(a.hi, b.hi)) }
|
|
255
|
+
if (op === 'f64.max') { const a = r(n[1]), b = r(n[2]); return a && b && fin(Math.max(a.lo, b.lo), Math.max(a.hi, b.hi)) }
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
return r(n)
|
|
259
|
+
}
|
|
260
|
+
|
|
160
261
|
/** Coerce node to i32 with wrapping (JS `|0` semantics: values > 2^31 wrap to negative).
|
|
161
262
|
* Per ECMAScript ToInt32, NaN and ±∞ map to 0. `i64.trunc_sat_f64_s` handles NaN
|
|
162
263
|
* and -∞ correctly, but +∞ saturates to i64_max which wraps to -1 — guard +∞ via
|
|
@@ -179,15 +280,27 @@ export const toI32 = n => {
|
|
|
179
280
|
// computes in i32 (mod-2^32 ring) — no trunc/guard at all.
|
|
180
281
|
const nw = narrowI32(n, true)
|
|
181
282
|
if (nw) return nw.node
|
|
283
|
+
// Value-range narrowing: a NON-integer f64 tree (e.g. `10 + 200·(u8[i]/255)`) the ring
|
|
284
|
+
// path rejects, but whose value is PROVABLY FINITE — so the +∞-guard `select` is dead.
|
|
285
|
+
// When the value also provably fits i32, a single `i32.trunc_sat_f64_s` IS exact ToInt32
|
|
286
|
+
// (no saturation can fire in-range, no NaN, no ±∞) — dropping the i64 round-trip AND the
|
|
287
|
+
// guard. Pervasive in pixel/colour packing: `(base + scale·v)|0`.
|
|
288
|
+
const rng = f64Range(n)
|
|
289
|
+
if (rng) {
|
|
290
|
+
if (rng.lo >= I32_MIN && rng.hi <= I32_MAX) return typed(['i32.trunc_sat_f64_s', n], 'i32')
|
|
291
|
+
// Finite and within (−2^63, 2^63): keep the mod-2^32 wrap, drop the (now-dead) +∞ guard.
|
|
292
|
+
// i64.trunc_sat does not saturate in this window, so wrap_i64 == ToInt32. Beyond ±2^63 we
|
|
293
|
+
// fall through to the guarded path (which already saturates there — the documented boundary).
|
|
294
|
+
if (rng.lo >= -9223372036854775808 && rng.hi < 9223372036854775808)
|
|
295
|
+
return typed(['i32.wrap_i64', ['i64.trunc_sat_f64_s', n]], 'i32')
|
|
296
|
+
}
|
|
182
297
|
// Leaf nodes are cheap to duplicate; for everything else, evaluate once via local.tee.
|
|
183
|
-
const isLeaf = Array.isArray(n) && n.length <= 2 &&
|
|
184
|
-
(n[0] === 'f64.const' || n[0] === 'local.get' || n[0] === 'global.get')
|
|
185
298
|
// `i32.wrap_i64(i64.trunc_sat_f64_s x)` is exact ToInt32 for |x| < 2^63 (the
|
|
186
299
|
// overwhelming common range), maps NaN/−∞→0, and +∞ is guarded to 0 by the
|
|
187
300
|
// select. For |x| ≥ 2^63 it saturates rather than wrapping mod 2^32 — a
|
|
188
301
|
// deliberately-allowed asm.js-style boundary (no per-`|0` helper/guard cost).
|
|
189
302
|
const wrap = x => typed(['i32.wrap_i64', ['i64.trunc_sat_f64_s', x]], 'i32')
|
|
190
|
-
if (isLeaf) {
|
|
303
|
+
if (isLeaf(n)) {
|
|
191
304
|
return typed(['select', wrap(n), ['i32.const', 0], ['f64.ne', n, ['f64.const', Infinity]]], 'i32')
|
|
192
305
|
}
|
|
193
306
|
const t = temp('inf')
|
|
@@ -292,13 +405,7 @@ export const BOXED_MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splic
|
|
|
292
405
|
const litI32 = n => Array.isArray(n) && n[0] === 'i32.const' && typeof n[1] === 'number' ? n[1] : null
|
|
293
406
|
|
|
294
407
|
/** Pack (type, aux, offset) into the f64 NaN-box bit pattern as a hex string. */
|
|
295
|
-
|
|
296
|
-
const bits = LAYOUT.NAN_PREFIX_BITS
|
|
297
|
-
| ((BigInt(type) & BigInt(LAYOUT.TAG_MASK)) << BigInt(LAYOUT.TAG_SHIFT))
|
|
298
|
-
| ((BigInt(aux) & BigInt(LAYOUT.AUX_MASK)) << BigInt(LAYOUT.AUX_SHIFT))
|
|
299
|
-
| (BigInt(offset >>> 0) & BigInt(LAYOUT.OFFSET_MASK))
|
|
300
|
-
return '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
|
|
301
|
-
}
|
|
408
|
+
const packPtrBits = (type, aux, offset) => i64Hex(ptrBits(type, aux, offset))
|
|
302
409
|
|
|
303
410
|
/** Build `__mkptr(type, aux, offset)` IR. Folds to `(f64.const nan:0x...)` — 9 bytes
|
|
304
411
|
* vs 12 for `f64.reinterpret_i64 (i64.const ...)` — when all args are i32 literals.
|
|
@@ -359,7 +466,7 @@ export function ptrTypeIR(valIR, valType) {
|
|
|
359
466
|
// op on it misdispatches; BigInt64Array/BigUint64Array views and
|
|
360
467
|
// DataView.{get,set}BigUint64 are a legacy f64-value shim there. Strings are
|
|
361
468
|
// tagged and survive every boundary; BigInt math happens only inside single
|
|
362
|
-
// expressions. (Same contract as
|
|
469
|
+
// expressions. (Same contract as watr/optimize's i64 VALUE CONTRACT.)
|
|
363
470
|
const _F64_BITS_BUF = new ArrayBuffer(8)
|
|
364
471
|
const _F64_BITS_F = new Float64Array(_F64_BITS_BUF)
|
|
365
472
|
const _F64_BITS_U32 = new Uint32Array(_F64_BITS_BUF) // LE halves: [0]=lo, [1]=hi
|
|
@@ -459,7 +566,7 @@ const PURE_F64_OPS = new Set([
|
|
|
459
566
|
* is correctly NOT numeric, while `cond ? n*2 : n*3` is. Conservative: any shape
|
|
460
567
|
* not provably numeric (property gets, user calls, local.get, f64.const nan:…)
|
|
461
568
|
* returns false, so the caller keeps the __to_num coercion. */
|
|
462
|
-
const isNumericIR = (r) => {
|
|
569
|
+
export const isNumericIR = (r) => {
|
|
463
570
|
if (!Array.isArray(r)) return false
|
|
464
571
|
const op = r[0]
|
|
465
572
|
if (PURE_F64_OPS.has(op)) return true
|
|
@@ -550,19 +657,21 @@ export function buildRefcount(fn) {
|
|
|
550
657
|
* existing ids in a single walk. Replaces the per-pass
|
|
551
658
|
* `while (fn.some(... === $__prefixK)) k++` (O(K·N)) with one O(N) scan. */
|
|
552
659
|
export function nextLocalId(fn, prefix) {
|
|
553
|
-
|
|
660
|
+
// HIGH-WATER mark (max existing + 1), NOT the first free id. Callers allocate sequentially
|
|
661
|
+
// (id++), so a first-gap start would walk straight into an existing higher local once watr's
|
|
662
|
+
// coalesce has left non-contiguous numbering (e.g. $__pe0,$__pe1,$__pe5 → start at 2, then
|
|
663
|
+
// mint 3,4,5 and collide on $__pe5 = "Duplicate local"). High-water is always collision-free.
|
|
554
664
|
const needle = `$__${prefix}`
|
|
665
|
+
let id = 0
|
|
555
666
|
const walk = (n) => {
|
|
556
667
|
if (!Array.isArray(n)) return
|
|
557
668
|
if (n[0] === 'local' && typeof n[1] === 'string' && n[1].startsWith(needle)) {
|
|
558
669
|
const tail = n[1].slice(needle.length)
|
|
559
|
-
if (/^\d+$/.test(tail))
|
|
670
|
+
if (/^\d+$/.test(tail)) { const k = +tail; if (k >= id) id = k + 1 }
|
|
560
671
|
}
|
|
561
672
|
for (let i = 0; i < n.length; i++) walk(n[i])
|
|
562
673
|
}
|
|
563
674
|
walk(fn)
|
|
564
|
-
let id = 0
|
|
565
|
-
while (seen.has(id)) id++
|
|
566
675
|
return id
|
|
567
676
|
}
|
|
568
677
|
|
|
@@ -731,10 +840,15 @@ export function toNumF64(node, v) {
|
|
|
731
840
|
if (ctx.schema.slotIntCertainAt?.(node[1], node[2]) === true) return asF64(v)
|
|
732
841
|
}
|
|
733
842
|
// IR-level shapes that produce real f64 numbers (never NaN-boxed pointers):
|
|
734
|
-
// i32→f64 conversions, stdlib clock helper
|
|
843
|
+
// i32→f64 conversions, stdlib clock helper, length/ptr helpers.
|
|
844
|
+
// Skip the __to_num call wrapper for these — they always return plain f64.
|
|
735
845
|
if (Array.isArray(v)) {
|
|
736
846
|
if (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u') return v
|
|
737
847
|
if (v[0] === 'call' && v[1] === '$__time_ms') return v
|
|
848
|
+
// __length, __str_len return f64.convert_i32_s of an i32 — never a boxed pointer.
|
|
849
|
+
if (v[0] === 'call' && (v[1] === '$__length' || v[1] === '$__len' || v[1] === '$__str_len')) return v
|
|
850
|
+
// __ptr_type returns i32 tag, __ptr_offset returns i32 offset — both numeric.
|
|
851
|
+
if (v[0] === 'call' && (v[1] === '$__ptr_type' || v[1] === '$__ptr_offset')) return v
|
|
738
852
|
}
|
|
739
853
|
// f64 arithmetic ops and math intrinsics never produce NaN-boxed pointers — the
|
|
740
854
|
// result is always a plain f64 number. Skip __to_num for these, eliminating the
|
|
@@ -785,6 +899,14 @@ export function toStrI64(node, v) {
|
|
|
785
899
|
return typed(['call', '$__to_str', prim], 'i64')
|
|
786
900
|
}
|
|
787
901
|
}
|
|
902
|
+
// Provably-integer operand → render with the i32-only formatter, bypassing __to_str's
|
|
903
|
+
// float machinery (__ftoa/__toExp/__pow10, ~2 KB). A raw i32 value (`n|0`, a bitwise
|
|
904
|
+
// result, a loop counter) carries no NaN-box, so its ToString is just digits + sign.
|
|
905
|
+
// ptrKind != null means it's an unboxed pointer (i32 offset), NOT a number — exclude.
|
|
906
|
+
if (v.type === 'i32' && v.ptrKind == null) {
|
|
907
|
+
inc('__i32_to_str')
|
|
908
|
+
return typed(['i64.reinterpret_f64', ['call', '$__i32_to_str', v]], 'i64')
|
|
909
|
+
}
|
|
788
910
|
inc('__to_str')
|
|
789
911
|
return typed(['call', '$__to_str', asI64(v)], 'i64')
|
|
790
912
|
}
|
|
@@ -809,8 +931,16 @@ const numericTruthy = e => {
|
|
|
809
931
|
|
|
810
932
|
// i32 ops whose result is already a 0/1 boolean (comparisons + eqz) — safe to use
|
|
811
933
|
// directly as a truthiness without a redundant `!= 0`.
|
|
934
|
+
// Ops whose result is already a canonical i32 boolean (0 or 1) — a condition built
|
|
935
|
+
// from one needs no `i32.ne(_, 0)` normalization. Every wasm comparison returns 0/1,
|
|
936
|
+
// so the f64/f32/i64 relations belong here too (they were missing — a `a > b ? …`
|
|
937
|
+
// f64 compare was wrapped in a dead `i32.ne(f64.gt …, 0)` in every branch/select).
|
|
812
938
|
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'
|
|
939
|
+
'i32.le_s', 'i32.le_u', 'i32.ge_s', 'i32.ge_u', 'i32.eqz',
|
|
940
|
+
'f64.eq', 'f64.ne', 'f64.lt', 'f64.gt', 'f64.le', 'f64.ge',
|
|
941
|
+
'f32.eq', 'f32.ne', 'f32.lt', 'f32.gt', 'f32.le', 'f32.ge',
|
|
942
|
+
'i64.eq', 'i64.ne', 'i64.lt_s', 'i64.lt_u', 'i64.gt_s', 'i64.gt_u',
|
|
943
|
+
'i64.le_s', 'i64.le_u', 'i64.ge_s', 'i64.ge_u', 'i64.eqz'])
|
|
814
944
|
|
|
815
945
|
export function truthyIR(e) {
|
|
816
946
|
// An i32 *constant* is a concrete number, not a known 0/1 boolean — fold it to its
|
|
@@ -961,6 +1091,13 @@ export function readVar(name) {
|
|
|
961
1091
|
return typed(['f64.load', boxedAddr(name)], 'f64')
|
|
962
1092
|
}
|
|
963
1093
|
if (isGlobal(name)) {
|
|
1094
|
+
// A module-level integer const (`const N = 16384`) is an immutable compile-time
|
|
1095
|
+
// value: emit i32.const directly (when it fits i32) so `x % N` / `x & N` / `x / N`
|
|
1096
|
+
// and counters bounded by N take the native integer path, instead of the global
|
|
1097
|
+
// folding to an f64 constant and routing through the f64 round-trip. Value-preserving
|
|
1098
|
+
// — an f64 consumer widens the i32.const via convert, which folds back to f64.const.
|
|
1099
|
+
const ci = ctx.scope.constInts?.get?.(name)
|
|
1100
|
+
if (ci != null && isI32(ci)) return typed(['i32.const', ci], 'i32')
|
|
964
1101
|
const gt = ctx.scope.globalTypes.get(name) || 'f64'
|
|
965
1102
|
const node = typed(['global.get', dollar(name)], gt)
|
|
966
1103
|
const grep = repOfGlobal(name)
|
package/src/kind-traits.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { VAL } from './reps.js'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Comparison / logical-not ops — result is a 0|1 boolean carried as i32. The one
|
|
9
|
+
// source of truth for "this operator yields a boolean": valTypeOf reads it as
|
|
10
|
+
// VAL.BOOL, exprType/isIntExpr read it as integer-certain. `in`/`instanceof` also
|
|
11
|
+
// yield a boolean but are membership ops, kept out of the integer-certainty set
|
|
12
|
+
// (they throw on BigInt operands and never reach numeric analysis).
|
|
13
|
+
export const CMP_OPS = new Set(['!', '<', '<=', '>', '>=', '==', '!=', '===', '!=='])
|
|
14
|
+
export const BOOL_OPS = new Set([...CMP_OPS, 'in', 'instanceof'])
|
|
11
15
|
|
|
12
16
|
export const NUMERIC_BINARY_OPS = ['-', 'u-', '*', '/', '%', '&', '|', '^', '<<', '>>']
|
|
13
17
|
export const NUMERIC_UNARY_OPS = new Set(['**', '++', '--', '~', '>>>', 'u+'])
|
|
@@ -94,9 +98,36 @@ export function methodValType(method, obj, objType, ctx) {
|
|
|
94
98
|
if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
|
|
95
99
|
return null
|
|
96
100
|
}
|
|
101
|
+
// .subarray / .toReversed / .toSorted / .with all return a typed array of the same kind.
|
|
102
|
+
if (method === 'subarray' || method === 'toReversed' || method === 'toSorted' || method === 'with')
|
|
103
|
+
return objType === VAL.TYPED ? VAL.TYPED : null
|
|
97
104
|
return null
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
// Built-in PROPERTY val-types — the property-read mirror of methodValType.
|
|
108
|
+
// These are language invariants: `.length` is always a number on the sized
|
|
109
|
+
// value kinds, `.size` on Set/Map, `.byteLength`/`.byteOffset` on typed arrays.
|
|
110
|
+
// Without this, `arr.length + x` sees `.length` as untyped and routes `+`
|
|
111
|
+
// through the __is_str_key string-concat dispatch — even though `.length` can
|
|
112
|
+
// never be a string on a known sized kind. (Object schema slots override this
|
|
113
|
+
// earlier in VT['.'], so `{length:'x'}.length` keeps its true slot type.)
|
|
114
|
+
//
|
|
115
|
+
// Gate on a known objType: an untyped receiver could be an object with a
|
|
116
|
+
// string-valued shadow of the same name, so leave it null there (conservative).
|
|
117
|
+
// null-proto: user code reads `.valueOf`, `.toString` etc. — a plain `{}` would
|
|
118
|
+
// expose inherited Object.prototype members as bogus "numeric props".
|
|
119
|
+
const NUMERIC_PROPS = Object.assign(Object.create(null), {
|
|
120
|
+
length: new Set([VAL.STRING, VAL.ARRAY, VAL.TYPED]),
|
|
121
|
+
byteLength: new Set([VAL.TYPED, VAL.BUFFER]),
|
|
122
|
+
byteOffset: new Set([VAL.TYPED]),
|
|
123
|
+
size: new Set([VAL.SET, VAL.MAP]),
|
|
124
|
+
})
|
|
125
|
+
export function propValType(prop, objType) {
|
|
126
|
+
if (objType == null) return null
|
|
127
|
+
const kinds = NUMERIC_PROPS[prop]
|
|
128
|
+
return kinds && kinds.has(objType) ? VAL.NUMBER : null
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
export function typedCtorElemValType(ctor) {
|
|
101
132
|
if (!ctor) return null
|
|
102
133
|
const isView = ctor.endsWith('.view')
|
package/src/kind.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import { ctx } from './ctx.js'
|
|
10
10
|
import { VAL, lookupValType } from './reps.js'
|
|
11
|
-
import { intLiteralValue } from './static.js'
|
|
11
|
+
import { intLiteralValue, staticIndexKey } from './static.js'
|
|
12
12
|
import {
|
|
13
13
|
BOOL_OPS, NUMERIC_BINARY_OPS, NUMERIC_UNARY_OPS, COMPOUND_NUMERIC_OPS,
|
|
14
|
-
calleeValType, methodValType, typedCtorElemValType,
|
|
14
|
+
calleeValType, methodValType, propValType, typedCtorElemValType,
|
|
15
15
|
} from './kind-traits.js'
|
|
16
16
|
|
|
17
17
|
export { typedCtorElemValType } from './kind-traits.js'
|
|
@@ -137,7 +137,16 @@ VT['?:'] = (args) => {
|
|
|
137
137
|
const truthy = literalTruthiness(args[0])
|
|
138
138
|
if (truthy != null) return valTypeOf(truthy ? args[1] : args[2])
|
|
139
139
|
const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
|
|
140
|
-
|
|
140
|
+
if (ta && ta === tb) return ta
|
|
141
|
+
// A boolean branch coerces to 0/1 in numeric context (same rule as &&/||/?? below):
|
|
142
|
+
// when the other branch has a known non-boolean type, the conditional carries it.
|
|
143
|
+
// Without this, `num + (cond ? num : num>k)` sees a null-typed operand and emits the
|
|
144
|
+
// polymorphic string-concat dispatch on two pure-numeric subexprs — which pins the
|
|
145
|
+
// whole number→string formatter (__str_concat → __to_str → __static_str), a pure-int
|
|
146
|
+
// program ballooning 1 → ~19 funcs (see test/wat-invariants.js, .work/todo.md).
|
|
147
|
+
if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
|
|
148
|
+
if (tb === VAL.BOOL && ta && ta !== VAL.BOOL) return ta
|
|
149
|
+
return null
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
// Value-preserving logical: `&&`/`||` return one of their operands.
|
|
@@ -145,7 +154,11 @@ VT['?:'] = (args) => {
|
|
|
145
154
|
// (a condition/guard) and the other has a known non-boolean type,
|
|
146
155
|
// return the non-boolean type — common in `condition && numericValue`
|
|
147
156
|
// guard patterns where the falsey boolean is coerced to 0 in numeric context.
|
|
148
|
-
|
|
157
|
+
// `a && b` / `a || b` / `a ?? b` all yield one of the two operands, so the result
|
|
158
|
+
// type is their common type (else unknown). Giving `??` a type — not just ||/&& —
|
|
159
|
+
// lets `numA ?? numB` read NaN-safe (value-typed NUMBER → f64.eq) instead of routing
|
|
160
|
+
// through the bit-comparing __is_truthy, which mis-reads a non-canonical NaN.
|
|
161
|
+
VT['&&'] = VT['||'] = VT['??'] = (args) => {
|
|
149
162
|
const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
|
|
150
163
|
if (ta && ta === tb) return ta
|
|
151
164
|
if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
|
|
@@ -158,6 +171,24 @@ VT['&&'] = VT['||'] = (args) => {
|
|
|
158
171
|
// Index access: `arr[i]` → ['[]', arr, i].
|
|
159
172
|
VT['[]'] = (args) => {
|
|
160
173
|
if (args.length < 2) return VAL.ARRAY
|
|
174
|
+
// A literal NEGATIVE index is always out of range → reads undefined, not the
|
|
175
|
+
// element type. Returning a numeric elem type here would let `a[-1] === undefined`
|
|
176
|
+
// fold to false (a NUMBER can't be undefined), silently dropping the guard.
|
|
177
|
+
{ const li = intLiteralValue(args[1]); if (li != null && li < 0) return null }
|
|
178
|
+
// SRoA flat-array slot read: `a[k]` (static index) where `a` dissolved into
|
|
179
|
+
// scalar `a#i` locals (scanFlatObjects). A write-once slot's value-type is its
|
|
180
|
+
// element literal's — same numeric-binding as the `VT['.']` object case, so
|
|
181
|
+
// `a[0] * 2` stays a plain f64 op instead of the polymorphic ToNumber battery.
|
|
182
|
+
if (typeof args[0] === 'string') {
|
|
183
|
+
const flat = ctx.func.flatObjects?.get(args[0])
|
|
184
|
+
if (flat) {
|
|
185
|
+
const k = staticIndexKey(args[1])
|
|
186
|
+
if (k != null && !flat.written?.has(k)) {
|
|
187
|
+
const i = flat.names.indexOf(k)
|
|
188
|
+
if (i >= 0 && flat.values[i] !== undefined) return valTypeOf(flat.values[i])
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
161
192
|
// Indexed read on a known typed-array receiver yields Number except for
|
|
162
193
|
// BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
|
|
163
194
|
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
|
|
@@ -170,6 +201,25 @@ VT['[]'] = (args) => {
|
|
|
170
201
|
if (typeof args[0] === 'string') {
|
|
171
202
|
const elemVt = ctx.func.localReps?.get(args[0])?.arrayElemValType
|
|
172
203
|
if (elemVt) return elemVt
|
|
204
|
+
// Module-level const array (a numeric/uniform table): its element val-type was
|
|
205
|
+
// recorded on the global rep at decl time. Trust it only when no function element-
|
|
206
|
+
// writes the array — dynWriteVars holds every var written via a non-named-property
|
|
207
|
+
// index, so a `X[i]=str` anywhere disables this and falls back to the untyped read.
|
|
208
|
+
if (!ctx.func.localReps?.has(args[0])) {
|
|
209
|
+
const gElem = ctx.scope.globalReps?.get(args[0])?.arrayElemValType
|
|
210
|
+
if (gElem && !ctx.types?.dynWriteVars?.has(args[0])) return gElem
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Direct double-index on a module-level nested numeric table — `C[i][j]` where
|
|
214
|
+
// `C = [[…number…], …]`. The receiver is itself a single-index read of a global
|
|
215
|
+
// array whose nested element kind was recorded at decl time. Same dynWriteVars
|
|
216
|
+
// guard (now root-aware, so a `C[i][j]=…` write anywhere disables it).
|
|
217
|
+
if (Array.isArray(args[0]) && args[0][0] === '[]' && args[0].length === 3 && typeof args[0][1] === 'string') {
|
|
218
|
+
const base = args[0][1]
|
|
219
|
+
if (!ctx.func.localReps?.has(base)) {
|
|
220
|
+
const gNested = ctx.scope.globalReps?.get(base)?.arrayElemElemValType
|
|
221
|
+
if (gNested && !ctx.types?.dynWriteVars?.has(base)) return gNested
|
|
222
|
+
}
|
|
173
223
|
}
|
|
174
224
|
// Indexed read on an inline all-numeric array literal — `[2,4,2,9][i]` (floatbeat
|
|
175
225
|
// chord/pattern tables; literal op is `[`, elements inline). Every element is a
|
|
@@ -183,6 +233,21 @@ VT['[]'] = (args) => {
|
|
|
183
233
|
|
|
184
234
|
VT['.'] = (args) => {
|
|
185
235
|
if (typeof args[1] !== 'string') return null
|
|
236
|
+
// SRoA flat-object slot read: `p.x` where `p` dissolved into scalar `p#i`
|
|
237
|
+
// locals (scanFlatObjects). A write-once slot's value-type IS its literal
|
|
238
|
+
// initializer's, so bind by it — exactly as a plain `let slot = value` local
|
|
239
|
+
// would. Without this `p.x * 2` looks like "could be anything" and pulls the
|
|
240
|
+
// ToNumber + string-format battery, though it can only be numeric. Computed
|
|
241
|
+
// on-demand (not cached at analyze time) because param val-types — `{x:n}`'s
|
|
242
|
+
// `n` is numeric-by-divergence — are only seeded at emit. A reassigned slot
|
|
243
|
+
// (`p.x = …`) stays untyped: its runtime value may differ from the literal.
|
|
244
|
+
if (typeof args[0] === 'string') {
|
|
245
|
+
const flat = ctx.func.flatObjects?.get(args[0])
|
|
246
|
+
if (flat && !flat.written?.has(args[1])) {
|
|
247
|
+
const i = flat.names.indexOf(args[1])
|
|
248
|
+
if (i >= 0 && flat.values[i] !== undefined) return valTypeOf(flat.values[i])
|
|
249
|
+
}
|
|
250
|
+
}
|
|
186
251
|
// Schema slot read: when `varName` has a bound schemaId and `.prop` resolves
|
|
187
252
|
// to a slot whose VAL kind is monomorphic across program-wide observations,
|
|
188
253
|
// return that kind. Lets `+`, `===`, method dispatch skip runtime str-key
|
|
@@ -200,6 +265,14 @@ VT['.'] = (args) => {
|
|
|
200
265
|
const child = sh.props[args[1]]
|
|
201
266
|
if (child) return child.val
|
|
202
267
|
}
|
|
268
|
+
// Built-in property on a known sized kind — `.length` on STRING/ARRAY/TYPED,
|
|
269
|
+
// `.size` on SET/MAP, `.byteLength`/`.byteOffset` on TYPED/BUFFER. These are
|
|
270
|
+
// language invariants (the property is always a number on that kind), so typing
|
|
271
|
+
// them NUMBER lets `+` skip the string-concat dispatch. Object schema slots
|
|
272
|
+
// resolved above override this, keeping user-defined same-name slots sound.
|
|
273
|
+
const objType = typeof args[0] === 'string' ? lookupValType(args[0]) : valTypeOf(args[0])
|
|
274
|
+
const pvt = propValType(args[1], objType)
|
|
275
|
+
if (pvt) return pvt
|
|
203
276
|
return null
|
|
204
277
|
}
|
|
205
278
|
|
|
@@ -248,6 +321,8 @@ VT['()'] = (args) => {
|
|
|
248
321
|
const t = valTypeOf(args[1])
|
|
249
322
|
return t === VAL.SET || t === VAL.MAP ? VAL.ARRAY : t
|
|
250
323
|
}
|
|
324
|
+
// for-in's read-only key list (src/prepare) — always an Array of key strings.
|
|
325
|
+
if (callee === '__keys_ro') return VAL.ARRAY
|
|
251
326
|
// Ternary is parsed as call to '?' operator: ['()', ['?', cond, a, b]]
|
|
252
327
|
if (Array.isArray(callee) && callee[0] === '?') {
|
|
253
328
|
const truthy = literalTruthiness(callee[1])
|
package/src/op-policy.js
CHANGED
|
@@ -22,7 +22,10 @@ export const REJECT_OPS = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Bare identifiers prepare rejects (no jzify lowering). */
|
|
25
|
-
|
|
25
|
+
// Prototype-less (Object.create(null)): a plain `{}` inherits Object.prototype in V8, so
|
|
26
|
+
// `REJECT_IDENTS['valueOf']` would return the inherited method (truthy) and wrongly reject
|
|
27
|
+
// a user identifier named like an Object method. Kernel objects are already prototype-less.
|
|
28
|
+
export const REJECT_IDENTS = Object.assign(Object.create(null), {
|
|
26
29
|
with: '`with` not supported',
|
|
27
30
|
class: '`class` not supported',
|
|
28
31
|
yield: '`yield` not supported',
|
|
@@ -36,7 +39,7 @@ export const REJECT_IDENTS = {
|
|
|
36
39
|
// identifier in sloppy JS (`var let = 5`), so rejecting it would refuse valid JS
|
|
37
40
|
// (test262 language/expressions/object/let-non-strict-*).
|
|
38
41
|
const: '`const` is a reserved word, not a valid name',
|
|
39
|
-
}
|
|
42
|
+
})
|
|
40
43
|
|
|
41
44
|
/** jzify-only errors for class lowering (no prepare counterpart). */
|
|
42
45
|
export const JZIFY_CLASS_ERRORS = {
|
package/src/ops.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// === AST op tags — integer-tagged-union representation ===
|
|
2
|
+
// internOps (prepare->compile boundary) converts array node[0] from op STRING to its
|
|
3
|
+
// integer tag; the compile half then dispatches via integer-keyed (eventually array)
|
|
4
|
+
// tables, removing the per-node string-hash lookup in the self-host kernel.
|
|
5
|
+
// OP: string -> int (1-based; 0 reserved => a missing tag is falsy). OPS: int -> string.
|
|
6
|
+
export const OP = {
|
|
7
|
+
"!": 1,
|
|
8
|
+
"!=": 2,
|
|
9
|
+
"!==": 3,
|
|
10
|
+
"%": 4,
|
|
11
|
+
"%=": 5,
|
|
12
|
+
"&": 6,
|
|
13
|
+
"&&": 7,
|
|
14
|
+
"&&=": 8,
|
|
15
|
+
"&=": 9,
|
|
16
|
+
"(": 10,
|
|
17
|
+
"()": 11,
|
|
18
|
+
"*": 12,
|
|
19
|
+
"**": 13,
|
|
20
|
+
"*=": 14,
|
|
21
|
+
"+": 15,
|
|
22
|
+
"++": 16,
|
|
23
|
+
"+=": 17,
|
|
24
|
+
",": 18,
|
|
25
|
+
"-": 19,
|
|
26
|
+
"--": 20,
|
|
27
|
+
"-=": 21,
|
|
28
|
+
".": 22,
|
|
29
|
+
"...": 23,
|
|
30
|
+
"/": 24,
|
|
31
|
+
"//": 25,
|
|
32
|
+
"/=": 26,
|
|
33
|
+
";": 27,
|
|
34
|
+
"<": 28,
|
|
35
|
+
"<<": 29,
|
|
36
|
+
"<<=": 30,
|
|
37
|
+
"<=": 31,
|
|
38
|
+
"=": 32,
|
|
39
|
+
"==": 33,
|
|
40
|
+
"===": 34,
|
|
41
|
+
"=>": 35,
|
|
42
|
+
">": 36,
|
|
43
|
+
">=": 37,
|
|
44
|
+
">>": 38,
|
|
45
|
+
">>=": 39,
|
|
46
|
+
">>>": 40,
|
|
47
|
+
">>>=": 41,
|
|
48
|
+
"?": 42,
|
|
49
|
+
"?:": 43,
|
|
50
|
+
"??": 44,
|
|
51
|
+
"??=": 45,
|
|
52
|
+
"[": 46,
|
|
53
|
+
"[]": 47,
|
|
54
|
+
"^": 48,
|
|
55
|
+
"^=": 49,
|
|
56
|
+
"async": 50,
|
|
57
|
+
"await": 51,
|
|
58
|
+
"bigint": 52,
|
|
59
|
+
"block": 53,
|
|
60
|
+
"bool": 54,
|
|
61
|
+
"break": 55,
|
|
62
|
+
"call": 56,
|
|
63
|
+
"catch": 57,
|
|
64
|
+
"const": 58,
|
|
65
|
+
"continue": 59,
|
|
66
|
+
"default": 60,
|
|
67
|
+
"delete": 61,
|
|
68
|
+
"export": 62,
|
|
69
|
+
"finally": 63,
|
|
70
|
+
"for": 64,
|
|
71
|
+
"if": 65,
|
|
72
|
+
"import": 66,
|
|
73
|
+
"in": 67,
|
|
74
|
+
"instanceof": 68,
|
|
75
|
+
"label": 69,
|
|
76
|
+
"let": 70,
|
|
77
|
+
"nan": 71,
|
|
78
|
+
"new": 72,
|
|
79
|
+
"return": 73,
|
|
80
|
+
"spread": 74,
|
|
81
|
+
"str": 75,
|
|
82
|
+
"strcat": 76,
|
|
83
|
+
"switch": 77,
|
|
84
|
+
"throw": 78,
|
|
85
|
+
"typeof": 79,
|
|
86
|
+
"u+": 80,
|
|
87
|
+
"u-": 81,
|
|
88
|
+
"void": 82,
|
|
89
|
+
"while": 83,
|
|
90
|
+
"yield": 84,
|
|
91
|
+
"{": 85,
|
|
92
|
+
"{}": 86,
|
|
93
|
+
"|": 87,
|
|
94
|
+
"|=": 88,
|
|
95
|
+
"||": 89,
|
|
96
|
+
"||=": 90,
|
|
97
|
+
"~": 91,
|
|
98
|
+
}
|
|
99
|
+
export const OPS = [null, "!", "!=", "!==", "%", "%=", "&", "&&", "&&=", "&=", "(", "()", "*", "**", "*=", "+", "++", "+=", ",", "-", "--", "-=", ".", "...", "/", "//", "/=", ";", "<", "<<", "<<=", "<=", "=", "==", "===", "=>", ">", ">=", ">>", ">>=", ">>>", ">>>=", "?", "?:", "??", "??=", "[", "[]", "^", "^=", "async", "await", "bigint", "block", "bool", "break", "call", "catch", "const", "continue", "default", "delete", "export", "finally", "for", "if", "import", "in", "instanceof", "label", "let", "nan", "new", "return", "spread", "str", "strcat", "switch", "throw", "typeof", "u+", "u-", "void", "while", "yield", "{", "{}", "|", "|=", "||", "||=", "~"]
|
|
100
|
+
export const OP_COUNT = 92
|
|
101
|
+
|
|
102
|
+
// Normalize an op tag to its string form for op-Set membership / switch checks,
|
|
103
|
+
// so `SET.has(opStr(node[0]))` and `switch (opStr(op))` work whether node[0] is
|
|
104
|
+
// still a string (intern off / non-interned op like ':') or an integer (intern
|
|
105
|
+
// on). Self-host-safe: the typeof guard avoids indexing the OPS array by a string
|
|
106
|
+
// (jz arrays trap on non-integer indices), and it never grows a Set.
|
|
107
|
+
export const opStr = (op) => typeof op === "number" ? OPS[op] : op
|
|
108
|
+
|
|
109
|
+
// Convert array node[0] from op-STRING to integer tag, recursively. Once per node at
|
|
110
|
+
// the prepare boundary. Unknown op-strings stay strings (dual-keyed tables handle them
|
|
111
|
+
// until the int-only phase). node[0]===null (numeric literal) stays null; identifiers
|
|
112
|
+
// (bare strings at n[1+]) untouched.
|
|
113
|
+
export const internOps = (n) => {
|
|
114
|
+
if (!Array.isArray(n)) return n
|
|
115
|
+
const t = n[0]
|
|
116
|
+
if (typeof t === "string") { const id = OP[t]; if (id !== undefined) n[0] = id }
|
|
117
|
+
for (let i = 1; i < n.length; i++) if (Array.isArray(n[i])) internOps(n[i])
|
|
118
|
+
return n
|
|
119
|
+
}
|