jz 0.6.0 → 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 +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
package/src/compile/emit.js
CHANGED
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
commaList, T, isBlockBody, isReassigned, mutatesArrayLength, isConstLiteral, constLiteralHoistable,
|
|
26
26
|
hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
|
|
27
27
|
} from '../ast.js'
|
|
28
|
-
import { ctx, err, inc, PTR } from '../ctx.js'
|
|
29
|
-
import { nonNegIntLiteral, staticPropertyKey } from '../static.js'
|
|
28
|
+
import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
|
|
29
|
+
import { nonNegIntLiteral, intLiteralValue, staticPropertyKey } from '../static.js'
|
|
30
30
|
import { findFreeVars } from './analyze.js'
|
|
31
31
|
import {
|
|
32
32
|
containsNestedClosure, containsNestedLoop, nestedSmallLoopBudget,
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
42
42
|
WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
43
43
|
mkPtrIR, ptrOffsetIR, ptrTypeIR, ptrTypeEq, dispatchByPtrType, sidecarOverride, valKindToPtr,
|
|
44
|
-
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64,
|
|
44
|
+
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64, maskBound,
|
|
45
45
|
truthyIR, toBoolFromEmitted, isPostfix,
|
|
46
46
|
isGlobal, isConst, usesDynProps, needsDynShadow,
|
|
47
47
|
temp, tempI32, tempI64, allocPtr,
|
|
@@ -76,7 +76,22 @@ const stringOps = (node) => {
|
|
|
76
76
|
// instead route through ToNumber (`toNumF64`), which performs ToPrimitive.
|
|
77
77
|
const isI32Num = (v) => v.type === 'i32' && v.ptrKind == null
|
|
78
78
|
|
|
79
|
+
// f64 arithmetic that can MINT a sign-nondeterministic NaN (0/0, ∞−∞, 0·∞, x%0): on x86
|
|
80
|
+
// these are 0xFFF8…, on arm 0x7FF8…. sqrt/min/max/neg are NOT here — they canon at their
|
|
81
|
+
// own emit (math.js / unary `-`), so they reach canonNum already canonical.
|
|
82
|
+
const NAN_MINTING = new Set(['f64.div', 'f64.add', 'f64.sub', 'f64.mul'])
|
|
83
|
+
|
|
79
84
|
const canonNum = (node) => {
|
|
85
|
+
// Fold a possibly-non-canonical NaN to the canonical number-NaN before it reaches a
|
|
86
|
+
// bit-comparing consumer (__is_truthy / untyped === / typeof), which match the canonical
|
|
87
|
+
// NaN by bits and so misread x86's 0xFFF8 as truthy. ONLY an un-canon'd NaN-minting
|
|
88
|
+
// arithmetic op can carry such a value — literals, i32-conversions, opaque locals/calls
|
|
89
|
+
// (canonical by the canon-at-source invariant) and already-canon'd shapes don't — so
|
|
90
|
+
// skipping everything else keeps the size win. (The broken middle ground was
|
|
91
|
+
// `02873d0`'s `isNumericIR` skip, which dropped canon for f64.div too → x86 miscompile.)
|
|
92
|
+
const arith = Array.isArray(node) &&
|
|
93
|
+
(NAN_MINTING.has(node[0]) || (node[0] === 'call' && node[1] === '$__rem'))
|
|
94
|
+
if (!arith) return node
|
|
80
95
|
const t = temp('cn')
|
|
81
96
|
return typed(['block', ['result', 'f64'],
|
|
82
97
|
['local.set', `$${t}`, node],
|
|
@@ -86,6 +101,21 @@ const canonNum = (node) => {
|
|
|
86
101
|
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
87
102
|
}
|
|
88
103
|
|
|
104
|
+
// Is an emitted arm `v` (AST `node`) a plain NUMBER? The predicate the two-arm merges
|
|
105
|
+
// (?:, ??) share to decide canon: an i32 number, NUMBER-tagged IR, or a NUMBER
|
|
106
|
+
// value-type qualifies; a pointer/opaque arm does not. `vt` is the node's resolved
|
|
107
|
+
// value-type — pass it when already computed to avoid the re-resolve.
|
|
108
|
+
const isNumArm = (v, node, vt = resolveValType(node, valTypeOf, lookupValType)) =>
|
|
109
|
+
isI32Num(v) || v.valKind === VAL.NUMBER || vt === VAL.NUMBER
|
|
110
|
+
|
|
111
|
+
// One arm of a two-arm f64 merge (?:, ??, ||, &&) whose result may be bit-tested while
|
|
112
|
+
// untyped. Canon (canonNum, a no-op unless the arm is NaN-minting arithmetic) ONLY a
|
|
113
|
+
// LONE numeric arm: when both arms are numeric the merge is value-typed NUMBER and read
|
|
114
|
+
// NaN-by-value (no canon); when the other arm is opaque the result is untyped, so a
|
|
115
|
+
// non-canonical NaN here would be misread by __is_truthy — fold it. A pointer arm
|
|
116
|
+
// (isNum=false) is never touched (canon would destroy its NaN-box).
|
|
117
|
+
const canonArm = (f, isNum, otherNum) => isNum && !otherNum ? canonNum(f) : f
|
|
118
|
+
|
|
89
119
|
// Host globals auto-imported as `(import "env" "name" (global … i64))` when
|
|
90
120
|
// referenced as a value. Drained from ctx.core.hostGlobals at assembly.
|
|
91
121
|
const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
|
|
@@ -98,6 +128,16 @@ const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'gl
|
|
|
98
128
|
// use — wrapping is exactly its intended semantics, so it stays on the i32 path.
|
|
99
129
|
const widensUnsigned = (v) => v.unsigned && !v.wrapSafe
|
|
100
130
|
|
|
131
|
+
// Strip a redundant NaN-canon wrapper (math.js `canon`) from an operand that
|
|
132
|
+
// feeds a NaN-propagating f64 op. `f64.sqrt`/`min`/`max` mint a sign-nondeterministic
|
|
133
|
+
// NaN that math.js canon-izes so it can't be bit-confused with a NaN-boxed pointer in
|
|
134
|
+
// `===`/`typeof`. But when the result flows straight into `f64.add`/`sub`/`mul`/`div`,
|
|
135
|
+
// the consumer propagates that NaN identically and is itself canon-ized if IT escapes —
|
|
136
|
+
// so the inner per-op canon (local.set + select + f64.ne, ~3 ops) is dead on the
|
|
137
|
+
// critical path. This is THE gap that put sqrt-heavy kernels ~23% behind V8
|
|
138
|
+
// (julia/raymarcher/boids); stripping it makes them match native JS.
|
|
139
|
+
const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
|
|
140
|
+
|
|
101
141
|
const FIRST_CLASS_UNARY_MATH = {
|
|
102
142
|
'math.abs': 'f64.abs',
|
|
103
143
|
'math.sqrt': 'f64.sqrt',
|
|
@@ -155,13 +195,18 @@ const foldConst = (va, vb, fn, guard) =>
|
|
|
155
195
|
// JS `*` is an f64 multiply; `i32.mul` yields only the exact product mod 2^32.
|
|
156
196
|
// Those agree under a ToInt32/ToUint32 sink (and as plain numbers) while the
|
|
157
197
|
// exact product stays f64-exact, i.e. |product| <= 2^53. Two i32 operands can
|
|
158
|
-
// reach 2^62, so `i32.mul` is sound only when one side is
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// `
|
|
198
|
+
// reach 2^62, so `i32.mul` is sound only when one side is bounded small enough
|
|
199
|
+
// that, against the full i32 range (2^31) of the other, the product holds within
|
|
200
|
+
// 2^53 — i.e. its magnitude <= 2^22. A literal qualifies directly; so does a
|
|
201
|
+
// masked operand (`x & 63`, `x >>> k`) whose value is provably bounded. Keeps
|
|
202
|
+
// index arithmetic (`i*4`) and bitwise-masked scales (bytebeat's `t*(m&63)`) on
|
|
203
|
+
// `i32.mul` while routing hash-mix-scale products to `f64.mul`.
|
|
204
|
+
const FITS_I32_MAX = 0x400000 // 2^22 — see derivation above
|
|
162
205
|
const mulFitsI32 = (va, vb) =>
|
|
163
|
-
(isLit(va) && Math.abs(litVal(va)) <=
|
|
164
|
-
(isLit(vb) && Math.abs(litVal(vb)) <=
|
|
206
|
+
(isLit(va) && Math.abs(litVal(va)) <= FITS_I32_MAX) ||
|
|
207
|
+
(isLit(vb) && Math.abs(litVal(vb)) <= FITS_I32_MAX) ||
|
|
208
|
+
(!isLit(va) && maskBound(va) <= FITS_I32_MAX) ||
|
|
209
|
+
(!isLit(vb) && maskBound(vb) <= FITS_I32_MAX)
|
|
165
210
|
|
|
166
211
|
/** Emit typeof comparison: typeof x == typeCode → type-aware check. */
|
|
167
212
|
export function emitTypeofCmp(a, b, cmpOp) {
|
|
@@ -307,6 +352,13 @@ function intIndexIR(key) {
|
|
|
307
352
|
*/
|
|
308
353
|
const I32_INDEX_OP = { '+': 'i32.add', '-': 'i32.sub', '*': 'i32.mul' }
|
|
309
354
|
function tryI32Index(e) {
|
|
355
|
+
// Integer literal first — a prepare-wrapped literal `[null, k]` (and a const-int
|
|
356
|
+
// name) is itself an Array, so the operator dispatch below would reject it and
|
|
357
|
+
// bail the WHOLE index to the f64 round-trip. The classic victim is the `+ 1` /
|
|
358
|
+
// `(j + 1)` of a bilinear/stencil gather (`a[(j+1)*W + i + 1]`): one literal leaf
|
|
359
|
+
// forced `convert_i32 … f64.mul/add … trunc_sat_f64_s` across every term.
|
|
360
|
+
const lit = nonNegIntLiteral(e)
|
|
361
|
+
if (lit != null) return typed(['i32.const', lit], 'i32')
|
|
310
362
|
if (Array.isArray(e)) {
|
|
311
363
|
const inner = I32_INDEX_OP[e[0]]
|
|
312
364
|
if (inner && e[2] != null) {
|
|
@@ -316,8 +368,6 @@ function tryI32Index(e) {
|
|
|
316
368
|
}
|
|
317
369
|
return null
|
|
318
370
|
}
|
|
319
|
-
const lit = nonNegIntLiteral(e)
|
|
320
|
-
if (lit != null) return ['i32.const', lit]
|
|
321
371
|
return exprType(e, ctx.func.locals) === 'i32' ? asI32(emit(e)) : null
|
|
322
372
|
}
|
|
323
373
|
export const emitIndex = (idx) => tryI32Index(idx) ?? asI32(emit(idx))
|
|
@@ -448,6 +498,55 @@ function emitSubstringEqCmp(a, b, negate = false) {
|
|
|
448
498
|
['i64.reinterpret_f64', ['local.get', `$${o}`]]])], 'i32')
|
|
449
499
|
}
|
|
450
500
|
|
|
501
|
+
// One half of a two-sided range test against a compile-time constant, normalized to
|
|
502
|
+
// an inclusive bound on a *local* `x`: `{ x, lo }` (x ≥ lo) or `{ x, hi }` (x ≤ hi).
|
|
503
|
+
// `>`/`<` fold to the inclusive neighbor; a const on either side is accepted. Returns
|
|
504
|
+
// null for anything else (so the caller leaves the expression untouched).
|
|
505
|
+
function rangeBound(n) {
|
|
506
|
+
if (!Array.isArray(n) || n.length !== 3) return null
|
|
507
|
+
const lc = intLiteralValue(n[1]), rc = intLiteralValue(n[2])
|
|
508
|
+
if (rc != null && typeof n[1] === 'string') { // x op CONST
|
|
509
|
+
if (n[0] === '>=') return { x: n[1], lo: rc }
|
|
510
|
+
if (n[0] === '>') return { x: n[1], lo: rc + 1 }
|
|
511
|
+
if (n[0] === '<=') return { x: n[1], hi: rc }
|
|
512
|
+
if (n[0] === '<') return { x: n[1], hi: rc - 1 }
|
|
513
|
+
}
|
|
514
|
+
if (lc != null && typeof n[2] === 'string') { // CONST op x
|
|
515
|
+
if (n[0] === '<=') return { x: n[2], lo: lc }
|
|
516
|
+
if (n[0] === '<') return { x: n[2], lo: lc + 1 }
|
|
517
|
+
if (n[0] === '>=') return { x: n[2], hi: lc }
|
|
518
|
+
if (n[0] === '>') return { x: n[2], hi: lc - 1 }
|
|
519
|
+
}
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// `x >= LO && x <= HI` (x a pure i32 local, LO ≤ HI constants) → `(x - LO) <=u (HI - LO)`.
|
|
524
|
+
// One subtract + one unsigned compare replaces two signed compares, an AND, and the
|
|
525
|
+
// short-circuit branch — the classic range-check trick (valid for any integers via
|
|
526
|
+
// wrapping subtraction). Returns the fused IR, or null to leave `&&` lowering unchanged.
|
|
527
|
+
function fuseRangeCheck(a, b) {
|
|
528
|
+
const ba = rangeBound(a), bb = rangeBound(b)
|
|
529
|
+
if (!ba || !bb || ba.x !== bb.x || (ba.lo != null) === (bb.lo != null)) return null
|
|
530
|
+
const lo = ba.lo ?? bb.lo, hi = ba.hi ?? bb.hi
|
|
531
|
+
if (lo > hi) return null
|
|
532
|
+
const xv = emit(ba.x)
|
|
533
|
+
if (xv.type !== 'i32') return null // f64 (fractional) would mis-fuse
|
|
534
|
+
return typed(['i32.le_u', ['i32.sub', xv, ['i32.const', lo]], ['i32.const', hi - lo]], 'i32')
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// The complement: `x < LO || x > HI` (the two outside half-checks — one upper-bounded,
|
|
538
|
+
// one lower-bounded, with a gap between) → `(x - LO) >u (HI - LO)`, where [LO, HI] is the
|
|
539
|
+
// inside range. Same trick, negated; returns null to leave `||` lowering unchanged.
|
|
540
|
+
function fuseRangeCheckOr(a, b) {
|
|
541
|
+
const ba = rangeBound(a), bb = rangeBound(b)
|
|
542
|
+
if (!ba || !bb || ba.x !== bb.x || (ba.lo != null) === (bb.lo != null)) return null
|
|
543
|
+
const insideLo = (ba.hi ?? bb.hi) + 1, insideHi = (ba.lo ?? bb.lo) - 1
|
|
544
|
+
if (insideLo > insideHi) return null
|
|
545
|
+
const xv = emit(ba.x)
|
|
546
|
+
if (xv.type !== 'i32') return null
|
|
547
|
+
return typed(['i32.gt_u', ['i32.sub', xv, ['i32.const', insideLo]], ['i32.const', insideHi - insideLo]], 'i32')
|
|
548
|
+
}
|
|
549
|
+
|
|
451
550
|
// Flow-sensitive type refinement moved to ./flow-types.js (extractRefinements,
|
|
452
551
|
// predicateRefinement, mergeRefinement, withRefinements). emit.js imports them
|
|
453
552
|
// from there — see the import block at the top of this file.
|
|
@@ -469,6 +568,80 @@ function unrollSmallConstFor(init, cond, step, body) {
|
|
|
469
568
|
return out
|
|
470
569
|
}
|
|
471
570
|
|
|
571
|
+
// Max distinct keys a for-in unrolls over (bounds code size; larger key sets keep
|
|
572
|
+
// the pooled-keys loop, which is already allocation-free via __keys_ro).
|
|
573
|
+
const FORIN_UNROLL_MAX = 16
|
|
574
|
+
// Total-expansion ceiling: unroll emits one body copy per key, so the size cost is
|
|
575
|
+
// keys × body, not keys alone. A large body over many keys (e.g. watr's 15-key
|
|
576
|
+
// schema loop) blows up code size for no deopt win — the pooled fallback is already
|
|
577
|
+
// allocation-free. Cap keys × nodeSize(body); past it, keep the loop. (Tuned above
|
|
578
|
+
// every unroll the corpus actually wants — the 16-key cap test lands at 80.)
|
|
579
|
+
const FORIN_UNROLL_BUDGET = 128
|
|
580
|
+
const forInBodyCost = (node) => {
|
|
581
|
+
if (!Array.isArray(node)) return 1
|
|
582
|
+
let n = 1
|
|
583
|
+
for (let i = 1; i < node.length; i++) n += forInBodyCost(node[i])
|
|
584
|
+
return n
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Pull the for-in source out of prepare's keys expression: either a bare
|
|
588
|
+
// `__keys_ro(src)` call or the nullish-guarded `cond ? [] : __keys_ro(src)`.
|
|
589
|
+
function keysRoSrc(node) {
|
|
590
|
+
if (!Array.isArray(node)) return null
|
|
591
|
+
if (node[0] === '()' && node[1] === '__keys_ro') return node[2]
|
|
592
|
+
if (node[0] === '?:' || node[0] === '?') {
|
|
593
|
+
const last = node[node.length - 1]
|
|
594
|
+
if (Array.isArray(last) && last[0] === '()' && last[1] === '__keys_ro') return last[2]
|
|
595
|
+
}
|
|
596
|
+
return null
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Unroll `for (k in o)` over a static schema. Prepare lowers for-in to a plain
|
|
600
|
+
// for-loop whose key array comes from the for-in-exclusive `__keys_ro` intrinsic,
|
|
601
|
+
// so a loop carrying it IS a for-in. When `o` is a bare OBJECT var with a complete
|
|
602
|
+
// static schema (no computed-key writes — same gate as __keys_ro pooling), replace
|
|
603
|
+
// the loop with one substituted copy of the body per key: the loop variable becomes
|
|
604
|
+
// a string literal, so `o[k]` folds to a static schema slot — no keys array, no
|
|
605
|
+
// per-element dynamic get. Falls back (returns null) to the pooled loop otherwise.
|
|
606
|
+
function unrollForIn(init, cond, step, body) {
|
|
607
|
+
if (!Array.isArray(init) || init[0] !== 'let' || !Array.isArray(init[1]) || init[1][0] !== '=') return null
|
|
608
|
+
const ksVar = init[1][1]
|
|
609
|
+
const src = keysRoSrc(init[1][2])
|
|
610
|
+
if (typeof src !== 'string') return null
|
|
611
|
+
if (!Array.isArray(cond) || cond[0] !== '<') return null
|
|
612
|
+
const ixVar = cond[1]
|
|
613
|
+
if (!Array.isArray(step) || step[0] !== '++' || step[1] !== ixVar) return null
|
|
614
|
+
// body = [';', ['let', ['=', target, ['[]', ksVar, ixVar]]], ...realBody]
|
|
615
|
+
if (!Array.isArray(body) || body[0] !== ';') return null
|
|
616
|
+
const bind = body[1]
|
|
617
|
+
if (!Array.isArray(bind) || bind[0] !== 'let' || !Array.isArray(bind[1]) || bind[1][0] !== '=') return null
|
|
618
|
+
const target = bind[1][1]
|
|
619
|
+
const acc = bind[1][2]
|
|
620
|
+
if (!Array.isArray(acc) || acc[0] !== '[]' || acc[1] !== ksVar || acc[2] !== ixVar) return null
|
|
621
|
+
|
|
622
|
+
// Unroll only with PROOF the schema is complete: a computed-key write adds
|
|
623
|
+
// enumerable keys, so bail if `src` takes one — or if the fact is unavailable
|
|
624
|
+
// (no proof ⇒ no unroll; unrolling drops the dynamic path, so erring safe matters).
|
|
625
|
+
if (!ctx.types.dynWriteVars || ctx.types.dynWriteVars.has(src)) return null
|
|
626
|
+
if (lookupValType(src) !== VAL.OBJECT) return null
|
|
627
|
+
const keys = ctx.schema.resolve(src)
|
|
628
|
+
if (!keys || !keys.length || keys.length > FORIN_UNROLL_MAX) return null
|
|
629
|
+
|
|
630
|
+
const rest = body.slice(2)
|
|
631
|
+
const realBody = rest.length === 1 ? rest[0] : [';', ...rest]
|
|
632
|
+
// Keep the pooled loop when unrolling would multiply a heavy body across many keys.
|
|
633
|
+
if (keys.length * forInBodyCost(realBody) > FORIN_UNROLL_BUDGET) return null
|
|
634
|
+
// Substitution safety, mirroring unrollSmallConstFor: no reassignment/redeclare
|
|
635
|
+
// of the loop var, no nested closure capturing it (cloneWithSubst skips `=>`),
|
|
636
|
+
// and no break/continue targeting this loop.
|
|
637
|
+
if (hasOwnBreakOrContinue(realBody) || containsNestedClosure(realBody) || containsDeclOf(realBody, target)) return null
|
|
638
|
+
if (isReassigned(realBody, target)) return null
|
|
639
|
+
|
|
640
|
+
const out = []
|
|
641
|
+
for (const key of keys) out.push(...emitVoid(cloneWithSubst(realBody, new Map([[target, ['str', key]]]))))
|
|
642
|
+
return out.length ? out : ['nop']
|
|
643
|
+
}
|
|
644
|
+
|
|
472
645
|
function canThrow(body, seen = new Set()) {
|
|
473
646
|
if (!Array.isArray(body)) return false
|
|
474
647
|
const op = body[0]
|
|
@@ -581,6 +754,40 @@ export function toBool(node) {
|
|
|
581
754
|
return toBoolFromEmitted(emit(node))
|
|
582
755
|
}
|
|
583
756
|
|
|
757
|
+
// `(a / b) | 0` (the JS integer-division idiom) → i32.div_s. jz otherwise lowers `/`
|
|
758
|
+
// to f64.div + ToInt32, paying two i32→f64 converts and the trunc; i32.div_s is
|
|
759
|
+
// direct and lets the wasm backend magic-multiply a constant divisor. Bit-exact for
|
|
760
|
+
// all i32 a,b: |a|<2³³≪2⁵³ so the f64 quotient never rounds across the truncation
|
|
761
|
+
// boundary — EXCEPT b=0 (`(a/0)|0` is ToInt32(±Inf)=0, but i32.div_s traps) and
|
|
762
|
+
// INT_MIN/-1 (ToInt32 wraps to INT_MIN, i32.div_s traps); both guarded. A constant
|
|
763
|
+
// divisor folds the guards away. `exprType==='i32'` excludes unsigned operands
|
|
764
|
+
// (those return 'f64'), where div_s would misread the sign. Returns IR or null.
|
|
765
|
+
const INT_MIN_I32 = -2147483648
|
|
766
|
+
function tryIntDivTrunc(aNode, bNode) {
|
|
767
|
+
const o = ctx.transform.optimize
|
|
768
|
+
if (!o || o.intDivLower === false) return null
|
|
769
|
+
const L = ctx.func.locals
|
|
770
|
+
if (exprType(aNode, L) !== 'i32' || exprType(bNode, L) !== 'i32') return null
|
|
771
|
+
const dv = intLiteralValue(bNode)
|
|
772
|
+
if (dv != null) { // constant divisor — no runtime guard
|
|
773
|
+
const va = asI32(emit(aNode))
|
|
774
|
+
if (dv === 0) return typed(['block', ['result', 'i32'], ['drop', va], ['i32.const', 0]], 'i32')
|
|
775
|
+
if (dv === -1) return typed(['i32.sub', ['i32.const', 0], va], 'i32') // -a, wraps at INT_MIN
|
|
776
|
+
return typed(['i32.div_s', va, ['i32.const', dv | 0]], 'i32')
|
|
777
|
+
}
|
|
778
|
+
// Runtime divisor needs a,b repeated across the guard; only intercept when both are
|
|
779
|
+
// simple re-emittable operands (var / literal) so re-emit is pure and side-effect-free.
|
|
780
|
+
const simple = (n) => typeof n === 'string' || intLiteralValue(n) != null
|
|
781
|
+
if (!simple(aNode) || !simple(bNode)) return null
|
|
782
|
+
const A = () => asI32(emit(aNode)), B = () => asI32(emit(bNode))
|
|
783
|
+
return typed(['if', ['result', 'i32'], ['i32.eqz', B()],
|
|
784
|
+
['then', ['i32.const', 0]],
|
|
785
|
+
['else', ['if', ['result', 'i32'],
|
|
786
|
+
['i32.and', ['i32.eq', A(), ['i32.const', INT_MIN_I32]], ['i32.eq', B(), ['i32.const', -1]]],
|
|
787
|
+
['then', A()],
|
|
788
|
+
['else', ['i32.div_s', A(), B()]]]]], 'i32')
|
|
789
|
+
}
|
|
790
|
+
|
|
584
791
|
/** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
|
|
585
792
|
* i32 offset), else falls back to numeric WASM type coercion. */
|
|
586
793
|
function coerceArg(ir, param) {
|
|
@@ -721,7 +928,7 @@ export function emitDecl(...inits) {
|
|
|
721
928
|
// Monotonic-extension fields (`o.newProp = …`) carry no literal value —
|
|
722
929
|
// they init to undefined so a read before the write matches JS.
|
|
723
930
|
const flatDecl = ctx.func.flatObjects?.get(name)
|
|
724
|
-
if (flatDecl && Array.isArray(init) && init[0] === '{}') {
|
|
931
|
+
if (flatDecl && Array.isArray(init) && (init[0] === '{}' || init[0] === '[' || init[0] === '[]')) {
|
|
725
932
|
for (let j = 0; j < flatDecl.names.length; j++)
|
|
726
933
|
result.push(['local.set', `$${name}#${j}`,
|
|
727
934
|
flatDecl.values[j] === undefined ? undefExpr() : asF64(emit(flatDecl.values[j]))])
|
|
@@ -790,6 +997,17 @@ export function emitDecl(...inits) {
|
|
|
790
997
|
if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
|
|
791
998
|
ctx.func.directClosures.set(name, val.closureBodyName)
|
|
792
999
|
}
|
|
1000
|
+
// Copy propagation of a direct closure: `let g = add`, where `add` is a non-escaping
|
|
1001
|
+
// directly-callable closure, makes `g` directly callable too — `g` holds the same
|
|
1002
|
+
// closure value, so `g(…)` calls add's body with g's value as env. This is what
|
|
1003
|
+
// devirtualizes `let arr = [add]; arr[0](…)`: array scalarization rewrites it to
|
|
1004
|
+
// `let g = add; g(…)` before emit (D3), and also covers the explicit `let g = arr[0]`.
|
|
1005
|
+
// Same soundness gate as the direct-closure case: stable binding (not reassigned),
|
|
1006
|
+
// not boxed, not global.
|
|
1007
|
+
if (typeof init === 'string' && ctx.func.directClosures?.has(init) && !ctx.func.boxed.has(name)
|
|
1008
|
+
&& !isGlobal(name) && ctx.func.body && !isReassigned(ctx.func.body, name)) {
|
|
1009
|
+
ctx.func.directClosures.set(name, ctx.func.directClosures.get(init))
|
|
1010
|
+
}
|
|
793
1011
|
if (ctx.func.boxed.has(name)) {
|
|
794
1012
|
const cell = ctx.func.boxed.get(name)
|
|
795
1013
|
ctx.func.locals.set(cell, 'i32')
|
|
@@ -865,8 +1083,14 @@ export function emitDecl(...inits) {
|
|
|
865
1083
|
} else {
|
|
866
1084
|
coerced = localType === 'v128' ? val : localType === 'f64' ? asF64(val) : val.type === 'i32' ? val : toI32(val)
|
|
867
1085
|
}
|
|
868
|
-
|
|
1086
|
+
// `let x = 0` at function scope is normally elided — WASM zero-inits locals. But loop
|
|
1087
|
+
// unrolling flattens iteration bodies into one scope, so the 2nd+ `let x = 0` are
|
|
1088
|
+
// genuine RE-inits between iterations (e.g. a nested reduce's accumulator). Elide only
|
|
1089
|
+
// the FIRST per name; emit the rest as resets. (Names are preserved — no renaming.)
|
|
1090
|
+
const zeroInit = isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length
|
|
1091
|
+
if (!zeroInit || ctx.func.zeroInitSeen?.has(name))
|
|
869
1092
|
result.push(['local.set', `$${name}`, coerced])
|
|
1093
|
+
else (ctx.func.zeroInitSeen ??= new Set()).add(name)
|
|
870
1094
|
|
|
871
1095
|
const schemaId = ctx.schema.idOf?.(name)
|
|
872
1096
|
if (ctx.func.localProps?.has(name) && schemaId != null) {
|
|
@@ -1705,7 +1929,9 @@ function tryStaticDispatch({ obj, method, vt, callMethod }) {
|
|
|
1705
1929
|
// emitter which already knows how to handle them.
|
|
1706
1930
|
function tryRuntimeStringFork({ obj, method, vt, callMethod }) {
|
|
1707
1931
|
const strKey = `.string:${method}`, genKey = `.${method}`
|
|
1708
|
-
|
|
1932
|
+
// VAL.ARRAY is structurally incompatible with PTR.STRING — no fork needed.
|
|
1933
|
+
// Only fork when vt is truly unknown (!vt), not for proven types.
|
|
1934
|
+
if (!vt && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
|
|
1709
1935
|
const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
|
|
1710
1936
|
ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
|
|
1711
1937
|
const strEmitter = ctx.core.emit[strKey]
|
|
@@ -1840,6 +2066,7 @@ function externalMethodFallback({ obj, method, parsed }) {
|
|
|
1840
2066
|
// can target js and wasi from one source; users who want fail-fast
|
|
1841
2067
|
// pass `strict: true` (handled above).
|
|
1842
2068
|
if (ctx.transform.host === 'wasi') return undefExpr()
|
|
2069
|
+
warnDeopt('deopt-method', `method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(…)\` on a value whose type couldn't be resolved dispatches through the JS host (\`__ext_call\`) — a wasm→JS round-trip per call, orders of magnitude slower than a direct call. Restructure so the receiver's type is provable, or keep it off the hot path.`)
|
|
1843
2070
|
inc('__ext_call')
|
|
1844
2071
|
ctx.features.external = true
|
|
1845
2072
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
@@ -2018,9 +2245,21 @@ function tryDirectClosureCall(callee, parsed) {
|
|
|
2018
2245
|
// export-param path gives. An arg we can't prove numeric poisons the slot to false.
|
|
2019
2246
|
const pt = (ctx.closure.paramTypes ||= new Map())
|
|
2020
2247
|
let row = pt.get(bodyName); if (!row) pt.set(bodyName, row = [])
|
|
2248
|
+
// Parallel typed-array ctor lattice: a param passed the SAME typed-array ctor at
|
|
2249
|
+
// every direct call site is a TYPED param, so its body reads (`buf[i]`) take the
|
|
2250
|
+
// typed fast-path instead of the dynamic `__typed_idx`/`__len` route that drags in
|
|
2251
|
+
// the string runtime. `null` (sticky) once two sites disagree or an arg isn't a
|
|
2252
|
+
// known typed array — the same monotone meet as the numeric row. Mirrors the named-fn
|
|
2253
|
+
// applyTypedPointerParamAbi, restricted to non-escaping (directly-called) closures.
|
|
2254
|
+
const tc = (ctx.closure.paramTypedCtors ||= new Map())
|
|
2255
|
+
let tcRow = tc.get(bodyName); if (!tcRow) tc.set(bodyName, tcRow = [])
|
|
2021
2256
|
for (let i = 0; i < n; i++) {
|
|
2022
2257
|
const numeric = valTypeOf(parsed.normal[i]) === VAL.NUMBER
|
|
2023
2258
|
row[i] = row[i] === undefined ? numeric : (row[i] && numeric)
|
|
2259
|
+
const arg = parsed.normal[i]
|
|
2260
|
+
const ctor = typeof arg === 'string' && valTypeOf(arg) === VAL.TYPED ? (ctx.types.typedElem?.get(arg) ?? null) : null
|
|
2261
|
+
if (tcRow[i] === undefined) tcRow[i] = ctor
|
|
2262
|
+
else if (tcRow[i] !== ctor) tcRow[i] = null
|
|
2024
2263
|
}
|
|
2025
2264
|
// Track the fewest args any call passed: a slot at index ≥ minArgc is omitted by some call
|
|
2026
2265
|
// site (padded with UNDEF_NAN), so it may be undefined — emitClosureBody flags it nullable.
|
|
@@ -2373,9 +2612,22 @@ export const emitter = {
|
|
|
2373
2612
|
// A BOOL operand renders "true"/"false" rather than its 0/1 carrier.
|
|
2374
2613
|
const strOperand = (vt, n) => vt === VAL.OBJECT ? typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2375
2614
|
: vt === VAL.BOOL ? emitBoolStr(n) : asF64(emit(n))
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2615
|
+
// Coercion-free sides are already strings: a known STRING is raw; OBJECT/BOOL
|
|
2616
|
+
// were stringified by `strOperand`. An unknown side still needs ToString, but
|
|
2617
|
+
// we can apply it *once* (explicit `__to_str` via `strI64`) and join with
|
|
2618
|
+
// concatRaw — equivalent to `__str_concat`'s internal `__to_str` on that side,
|
|
2619
|
+
// while NOT re-coercing the already-string side. This drops the redundant
|
|
2620
|
+
// per-append `__to_str` on the accumulator in `s += part` (s proven STRING):
|
|
2621
|
+
// - both coercion-free → concatRaw(ea, eb)
|
|
2622
|
+
// - one unknown → concatRaw(known, __to_str(unknown))
|
|
2623
|
+
// - both unknown → cat (unchanged; its runtime __to_str covers both)
|
|
2624
|
+
const coercionFree = (vt) => vt === VAL.STRING || vt === VAL.OBJECT || vt === VAL.BOOL
|
|
2625
|
+
const cfA = coercionFree(vtA), cfB = coercionFree(vtB)
|
|
2626
|
+
const strI64 = (n) => typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2627
|
+
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2628
|
+
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx), 'f64')
|
|
2629
|
+
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx), 'f64')
|
|
2630
|
+
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2379
2631
|
}
|
|
2380
2632
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
2381
2633
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -2409,7 +2661,7 @@ export const emitter = {
|
|
|
2409
2661
|
// op whose result can exceed i32, so `i32.add` would wrap (4294967295+1→0).
|
|
2410
2662
|
// Widen to f64 — never wrap — matching spec. Only `>>>0`/`|0`/imul wrap.
|
|
2411
2663
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.add', va, vb], 'i32')
|
|
2412
|
-
return typed(['f64.add', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
2664
|
+
return typed(['f64.add', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2413
2665
|
},
|
|
2414
2666
|
'-': (a, b) => {
|
|
2415
2667
|
if (ctx.func._expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
@@ -2424,7 +2676,7 @@ export const emitter = {
|
|
|
2424
2676
|
// Unsigned uint32 operand: JS `-` is float (can go negative / exceed i32),
|
|
2425
2677
|
// so avoid the wrapping i32.sub fast-path. See `+` above.
|
|
2426
2678
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb)) return typed(['i32.sub', va, vb], 'i32')
|
|
2427
|
-
return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
2679
|
+
return typed(['f64.sub', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2428
2680
|
},
|
|
2429
2681
|
'u+': a => {
|
|
2430
2682
|
if (valTypeOf(a) === VAL.BIGINT)
|
|
@@ -2454,7 +2706,7 @@ export const emitter = {
|
|
|
2454
2706
|
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
2455
2707
|
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
2456
2708
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && mulFitsI32(va, vb)) return typed(['i32.mul', va, vb], 'i32')
|
|
2457
|
-
return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
2709
|
+
return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2458
2710
|
},
|
|
2459
2711
|
'/': (a, b) => {
|
|
2460
2712
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
@@ -2462,7 +2714,7 @@ export const emitter = {
|
|
|
2462
2714
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
2463
2715
|
if (_f) return _f
|
|
2464
2716
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
2465
|
-
return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
2717
|
+
return typed(['f64.div', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2466
2718
|
},
|
|
2467
2719
|
'%': (a, b) => {
|
|
2468
2720
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
@@ -2539,6 +2791,15 @@ export const emitter = {
|
|
|
2539
2791
|
const elseRefs = extractRefinements(a, new Map(), false)
|
|
2540
2792
|
const vb = withRefinements(thenRefs, b, () => emit(b))
|
|
2541
2793
|
const vc = withRefinements(elseRefs, c, () => emit(c))
|
|
2794
|
+
// `cond ? 1 : 0` is the condition bit itself; `cond ? 0 : 1` its negation. `cond`
|
|
2795
|
+
// (truthyIR) is already canonical 0/1, so the select + two const arms collapse to
|
|
2796
|
+
// the bit. (Both arms are literals here, so dropping their emitted IR is side-effect
|
|
2797
|
+
// free.) Mirrors what `+(x > 0)` already produces.
|
|
2798
|
+
if (isLit(vb) && isLit(vc)) {
|
|
2799
|
+
const lb = litVal(vb), lc = litVal(vc)
|
|
2800
|
+
if (lb === 1 && lc === 0) return typed(cond, 'i32')
|
|
2801
|
+
if (lb === 0 && lc === 1) return typed(['i32.eqz', cond], 'i32')
|
|
2802
|
+
}
|
|
2542
2803
|
// L: Use WASM select for pure ternaries — branchless, smaller bytecode
|
|
2543
2804
|
if (vb.type === 'i32' && vc.type === 'i32') {
|
|
2544
2805
|
// A single i32 select is only sound when BOTH arms' i32 carriers mean the same
|
|
@@ -2577,10 +2838,24 @@ export const emitter = {
|
|
|
2577
2838
|
const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
|
|
2578
2839
|
|| vb.closureFuncIdx != null || vc.closureFuncIdx != null
|
|
2579
2840
|
|| isNaNBoxLit(fb) || isNaNBoxLit(fc)
|
|
2580
|
-
const numericB =
|
|
2581
|
-
const numericC =
|
|
2582
|
-
|
|
2583
|
-
|
|
2841
|
+
const numericB = isNumArm(vb, b, vtb)
|
|
2842
|
+
const numericC = isNumArm(vc, c, vtc)
|
|
2843
|
+
// Peephole: `cond ? 1 : 0` (or `cond ? 0 : 1`) is just `f64.convert_i32_s(cond)` —
|
|
2844
|
+
// the select collapses because cond is already 0/1. Saves 5 instructions.
|
|
2845
|
+
const isOneZero = (one, zero) => {
|
|
2846
|
+
const o = one, z = zero
|
|
2847
|
+
return o.type === 'i32' && Array.isArray(o) && o[0] === 'i32.const' && o[1] === 1 &&
|
|
2848
|
+
z.type === 'i32' && Array.isArray(z) && z[0] === 'i32.const' && z[1] === 0
|
|
2849
|
+
}
|
|
2850
|
+
if ((isOneZero(vb, vc) || isOneZero(vc, vb)) && !numericB && !numericC) {
|
|
2851
|
+
const condBool = truthyIR(emit(a))
|
|
2852
|
+
const n = isOneZero(vb, vc)
|
|
2853
|
+
? typed(['f64.convert_i32_s', condBool], 'f64')
|
|
2854
|
+
: typed(['f64.convert_i32_s', ['i32.eqz', condBool]], 'f64')
|
|
2855
|
+
n.valKind = VAL.NUMBER
|
|
2856
|
+
return n
|
|
2857
|
+
}
|
|
2858
|
+
const branchB = canonArm(fb, numericB, numericC), branchC = canonArm(fc, numericC, numericB)
|
|
2584
2859
|
const markNumeric = (n) => {
|
|
2585
2860
|
if (numericB && numericC) n.valKind = VAL.NUMBER
|
|
2586
2861
|
return n
|
|
@@ -2599,6 +2874,14 @@ export const emitter = {
|
|
|
2599
2874
|
},
|
|
2600
2875
|
|
|
2601
2876
|
'&&': (a, b) => {
|
|
2877
|
+
// Range-check fusion: `x >= LO && x <= HI` (x a pure i32 local, LO ≤ HI compile-time
|
|
2878
|
+
// constants) collapses to one unsigned compare `(x - LO) <=u (HI - LO)` — a subtract
|
|
2879
|
+
// plus a branch instead of two compares, an AND, and a short-circuit branch. This is
|
|
2880
|
+
// the per-char cost in scanners/parsers (digit/alpha classification) and in any
|
|
2881
|
+
// two-sided bounds check. Restricted to a local `x` so evaluating it once (the fused
|
|
2882
|
+
// form) matches the original's twice-read, side-effect-free semantics.
|
|
2883
|
+
const fused = fuseRangeCheck(a, b)
|
|
2884
|
+
if (fused) return fused
|
|
2602
2885
|
const va = emit(a)
|
|
2603
2886
|
// Constant-folded literal: pre-bind under truthy refinements (b runs only when a was truthy).
|
|
2604
2887
|
if (isLit(va)) {
|
|
@@ -2630,14 +2913,24 @@ export const emitter = {
|
|
|
2630
2913
|
['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
|
|
2631
2914
|
}
|
|
2632
2915
|
const t = temp()
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2916
|
+
const numA = isNumArm(va, a)
|
|
2917
|
+
const vb = emitRight(), numB = isNumArm(vb, b)
|
|
2918
|
+
// `a` is the else-arm result (returned when falsy — incl NaN), so canon a lone-numeric
|
|
2919
|
+
// `a` before the tee: `$t` then feeds both the result and the cond canonically.
|
|
2920
|
+
const teed = typed(['local.tee', `$${t}`, canonArm(asF64(va), numA, numB)], 'f64')
|
|
2921
|
+
// A numeric left arm tests truthiness NaN-by-value (not __is_truthy, which mis-reads
|
|
2922
|
+
// x86's sign-set NaN as truthy) — tag it so truthyIR takes that path.
|
|
2923
|
+
if (numA) teed.valKind = VAL.NUMBER
|
|
2924
|
+
return typed(['if', ['result', 'f64'], toBoolFromEmitted(teed),
|
|
2925
|
+
['then', canonArm(asF64(vb), numB, numA)],
|
|
2637
2926
|
['else', ['local.get', `$${t}`]]], 'f64')
|
|
2638
2927
|
},
|
|
2639
2928
|
|
|
2640
2929
|
'||': (a, b) => {
|
|
2930
|
+
// Outside-range fusion (the complement of `&&`): `x < LO || x > HI` → one unsigned
|
|
2931
|
+
// compare `(x - LO) >u (HI - LO)`. Common in validation (`if (c < 'a' || c > 'z') …`).
|
|
2932
|
+
const fusedOr = fuseRangeCheckOr(a, b)
|
|
2933
|
+
if (fusedOr) return fusedOr
|
|
2641
2934
|
const va = emit(a)
|
|
2642
2935
|
// Constant-folded literal: pre-bind under falsy refinements (b runs only when a was falsy).
|
|
2643
2936
|
if (isLit(va)) {
|
|
@@ -2665,22 +2958,30 @@ export const emitter = {
|
|
|
2665
2958
|
['else', asF64(vb)]], 'f64')
|
|
2666
2959
|
}
|
|
2667
2960
|
const t = temp()
|
|
2961
|
+
const numA = isNumArm(va, a)
|
|
2962
|
+
const vb = emitRight(), numB = isNumArm(vb, b)
|
|
2963
|
+
// `a` (then-arm) is returned only when truthy — hence never NaN — so it needs no canon;
|
|
2964
|
+
// the cond's NaN-safety comes from the valKind tag. Only the else (b) arm can surface
|
|
2965
|
+
// as a numeric NaN.
|
|
2668
2966
|
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
2669
|
-
|
|
2670
|
-
|
|
2967
|
+
if (numA) teed.valKind = VAL.NUMBER // numeric left arm: NaN-safe truthiness (see `&&`)
|
|
2968
|
+
return typed(['if', ['result', 'f64'], toBoolFromEmitted(teed),
|
|
2671
2969
|
['then', ['local.get', `$${t}`]],
|
|
2672
|
-
['else', asF64(
|
|
2970
|
+
['else', canonArm(asF64(vb), numB, numA)]], 'f64')
|
|
2673
2971
|
},
|
|
2674
2972
|
|
|
2675
2973
|
// a ?? b: returns b only if a is nullish
|
|
2676
2974
|
'??': (a, b) => {
|
|
2677
|
-
const va = emit(a)
|
|
2975
|
+
const va = emit(a), vb = emit(b)
|
|
2678
2976
|
const t = temp()
|
|
2977
|
+
const numA = isNumArm(va, a), numB = isNumArm(vb, b)
|
|
2978
|
+
// Both arms can surface as the (untyped) result — `a` when non-nullish (a NaN is not
|
|
2979
|
+
// nullish, so it IS returned), `b` otherwise. Canon a lone-numeric arm; `a` before the
|
|
2980
|
+
// tee so `local.get $t` is canonical. The cond is isNullish, robust to non-canon NaN.
|
|
2679
2981
|
return typed(['if', ['result', 'f64'],
|
|
2680
|
-
|
|
2681
|
-
['i32.eqz', isNullish(['local.tee', `$${t}`, asF64(va)])],
|
|
2982
|
+
['i32.eqz', isNullish(['local.tee', `$${t}`, canonArm(asF64(va), numA, numB)])],
|
|
2682
2983
|
['then', ['local.get', `$${t}`]],
|
|
2683
|
-
['else', asF64(
|
|
2984
|
+
['else', canonArm(asF64(vb), numB, numA)]], 'f64')
|
|
2684
2985
|
},
|
|
2685
2986
|
|
|
2686
2987
|
'void': a => {
|
|
@@ -2725,6 +3026,10 @@ export const emitter = {
|
|
|
2725
3026
|
].map(([op, fn]) => [op, (a, b) => {
|
|
2726
3027
|
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
2727
3028
|
return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
|
|
3029
|
+
if (op === '|') { // `(x / y) | 0` integer-division idiom → i32.div_s
|
|
3030
|
+
const divN = intLiteralValue(b) === 0 ? a : intLiteralValue(a) === 0 ? b : null
|
|
3031
|
+
if (Array.isArray(divN) && divN[0] === '/') { const r = tryIntDivTrunc(divN[1], divN[2]); if (r) return r }
|
|
3032
|
+
}
|
|
2728
3033
|
const va = emit(a), vb = emit(b)
|
|
2729
3034
|
if (isLit(va) && isLit(vb)) {
|
|
2730
3035
|
const la = litVal(va), lb = litVal(vb)
|
|
@@ -2794,6 +3099,12 @@ export const emitter = {
|
|
|
2794
3099
|
const unrolled = unrollSmallConstFor(init, cond, step, body)
|
|
2795
3100
|
if (unrolled) return unrolled
|
|
2796
3101
|
}
|
|
3102
|
+
// for-in over a static schema → unroll with key-literal substitution (folds
|
|
3103
|
+
// o[k] to schema slots). Recognized via the for-in-exclusive __keys_ro intrinsic.
|
|
3104
|
+
if (!labeledContinue && (!ctx.transform.optimize || ctx.transform.optimize.forInUnroll !== false)) {
|
|
3105
|
+
const fu = unrollForIn(init, cond, step, body)
|
|
3106
|
+
if (fu) return fu
|
|
3107
|
+
}
|
|
2797
3108
|
// Lift constant array/object literals out of the loop (allocate once, not per
|
|
2798
3109
|
// iteration) when they are read-only + non-escaping inside it. Strip them from the
|
|
2799
3110
|
// body up front so freshBoxed / continue analysis see the reduced body.
|