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/module/math.js
CHANGED
|
@@ -129,7 +129,11 @@ export default (ctx) => {
|
|
|
129
129
|
// undefined→NaN, and strings get parsed. Without this, raw NaN-boxed pointers
|
|
130
130
|
// (null/undefined/strings) would propagate through math.log etc. and surface
|
|
131
131
|
// as the original null/undefined sentinel after decode.
|
|
132
|
-
|
|
132
|
+
// A canon'd operand feeding a math call is redundant: the callee (log/sin/exp/…)
|
|
133
|
+
// propagates a non-canonical NaN identically and re-canon-izes its own result.
|
|
134
|
+
// `Math.log(Math.log(x))` thus sheds the inner per-call select + f64.ne.
|
|
135
|
+
const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
|
|
136
|
+
const fn = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => stripCanon(toNumF64(a, emit(a))))], 'f64')
|
|
133
137
|
|
|
134
138
|
// Canonicalize a possibly-NaN f64 result. A wasm arithmetic op that mints a
|
|
135
139
|
// fresh NaN (f64.sqrt of a negative, f64.min/max with a NaN operand) leaves
|
|
@@ -139,12 +143,18 @@ export default (ctx) => {
|
|
|
139
143
|
// untyped === / typeof. So fold any NaN back to canonical where one is born.
|
|
140
144
|
const canon = (node) => {
|
|
141
145
|
const t = temp('cn')
|
|
142
|
-
|
|
146
|
+
const ir = typed(['block', ['result', 'f64'],
|
|
143
147
|
['local.set', `$${t}`, node],
|
|
144
148
|
['select',
|
|
145
149
|
['f64.const', 'nan'],
|
|
146
150
|
['local.get', `$${t}`],
|
|
147
151
|
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
152
|
+
// Tag the wrapper so a NaN-propagating f64 consumer (`f64.add`/`mul`/… that
|
|
153
|
+
// itself canon-izes on escape) can strip the redundant inner canon: the raw
|
|
154
|
+
// op result `node` propagates a freshly-minted NaN identically through the
|
|
155
|
+
// consumer, and only the OUTERMOST escaping value needs the canonical form.
|
|
156
|
+
ir.canonOf = node
|
|
157
|
+
return ir
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
// sqrt(x) needs no NaN-canon when its argument is provably ≥ 0 with no spurious NaN:
|
|
@@ -166,7 +176,9 @@ export default (ctx) => {
|
|
|
166
176
|
n[0] === 'f64.const' ? (typeof n[1] === 'number' && n[1] >= 0) : false
|
|
167
177
|
const sqrtIR = (a) => { const ir = f('f64.sqrt', a); return nonNegF64(ir[1]) ? ir : canon(ir) }
|
|
168
178
|
|
|
169
|
-
// Constants
|
|
179
|
+
// Constants — each folds to its `(f64.const …)` inline (no stdlib dep, hence
|
|
180
|
+
// direct emit rather than reg). Written out (not a `Math[name]` loop) because the
|
|
181
|
+
// self-host subset can't resolve dynamic access on the `Math` compile-time namespace.
|
|
170
182
|
ctx.core.emit['math.PI'] = () => typed(['f64.const', Math.PI], 'f64')
|
|
171
183
|
ctx.core.emit['math.E'] = () => typed(['f64.const', Math.E], 'f64')
|
|
172
184
|
ctx.core.emit['math.LN2'] = () => typed(['f64.const', Math.LN2], 'f64')
|
|
@@ -348,6 +360,22 @@ export default (ctx) => {
|
|
|
348
360
|
// We emit, check, and if not foldable, the emitted IR is used by the fallthrough paths.
|
|
349
361
|
const irA = toNumF64(a, emit(a)), irB = toNumF64(b, emit(b))
|
|
350
362
|
if (isLit(irA) && isLit(irB)) return typed(['f64.const', Math.pow(litVal(irA), litVal(irB))], 'f64')
|
|
363
|
+
// Constant non-integer exponent c: inline Math.pow(x,c) as exp(c·log(x)) — that IS $math.pow's
|
|
364
|
+
// own non-integer tail (the final line of $math.pow below), so it is BIT-IDENTICAL to the call
|
|
365
|
+
// for every finite x and for x ∈ {±0, +∞, NaN}: log+exp carry the edges (log(NaN)=NaN,
|
|
366
|
+
// log(0)=−∞, log(<0)=NaN, log(+∞)=+∞; exp(±∞)=∞/0). Skipping the ~15-branch pow special-case
|
|
367
|
+
// ladder + the call frame is a per-pixel win on the gamma curves (v**0.45, a**(1/2.4)) that
|
|
368
|
+
// dominate tone-mapping, and a program whose only pow is folded this way never pulls $math.pow.
|
|
369
|
+
// The ONE divergence is x=−∞: this yields NaN where Math.pow gives ±∞ — the same deliberate
|
|
370
|
+
// boundary trade jz already makes for `(−∞)**0.5` (see the sqrt fold above); −∞ is never a real
|
|
371
|
+
// tone-map/gamma base. Integers stay on $math.pow (its square-and-multiply path is bit-different
|
|
372
|
+
// from exp/log); ±0.5 stays sqrt / the exact pow path (handled above).
|
|
373
|
+
if (isLit(irB)) {
|
|
374
|
+
const c = litVal(irB)
|
|
375
|
+
if (Number.isFinite(c) && !Number.isInteger(c) && c !== 0.5 && c !== -0.5)
|
|
376
|
+
return (inc('math.exp'), inc('math.log'),
|
|
377
|
+
typed(['call', '$math.exp', ['f64.mul', irB, ['call', '$math.log', irA]]], 'f64'))
|
|
378
|
+
}
|
|
351
379
|
// base 2 → dedicated 2^y (exp2 is exact for integer y, and skips exp's ×ln2/÷ln2).
|
|
352
380
|
// Every other literal base keeps $math.pow: `exp(y·ln base)` would lose ulps and,
|
|
353
381
|
// worse, miss Math.pow's integer-exponent semantics — e.g. `16 ** flen` with a
|
|
@@ -499,6 +527,137 @@ export default (ctx) => {
|
|
|
499
527
|
(if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
|
|
500
528
|
(f64.div (call $math.sin (local.get $x)) (call $math.cos (local.get $x))))`)
|
|
501
529
|
|
|
530
|
+
// ── f64x2 SIMD sin/cos — both lanes through one polynomial ───────────────────
|
|
531
|
+
// The scalar sin_core/cos_core algorithm lifted to two f64 lanes: same
|
|
532
|
+
// round-to-nearest π reduction, same minimax poly (SIN_C/COS_C), same quadrant
|
|
533
|
+
// parity — but every branch becomes branchless so two independent angles cost one
|
|
534
|
+
// evaluation. A kernel computing sin and cos of distinct args (rotations, de Jong /
|
|
535
|
+
// Clifford maps, oscillator banks) packs them two-per-vector and ≈halves trig cost.
|
|
536
|
+
// • Both reduction passes run unconditionally: for an in-range r the second pass'
|
|
537
|
+
// q2 = nearest(r/π) = 0, so it's an exact no-op — no per-lane branch needed, and
|
|
538
|
+
// it still rescues |x| up to ~1e15 just like the scalar's gated pass.
|
|
539
|
+
// • NaN and ±∞ fall out as NaN through the arithmetic (∞ − ∞·π = NaN); a v128 lane
|
|
540
|
+
// is raw f64, not a NaN-box, so the canonical-NaN guard the scalar needs is moot.
|
|
541
|
+
// • Sign flip for odd quadrants is `r XOR (mask & −0.0)` (mask = |q|>0.5); final
|
|
542
|
+
// min/max clamps the ~1e-8 poly overshoot to [−1,1], same as scalar.
|
|
543
|
+
const splat = (c) => `(f64x2.splat (f64.const ${c}))`
|
|
544
|
+
const horner2 = (cs, v = '$r2') => cs.reduceRight((acc, c, i) =>
|
|
545
|
+
i === cs.length - 1 ? splat(c)
|
|
546
|
+
: `(f64x2.add ${splat(c)} (f64x2.mul (local.get ${v}) ${acc}))`, '')
|
|
547
|
+
// fdlibm log's even/odd split, as f64x2 coefficient arrays (the scalar $math.log inlines them):
|
|
548
|
+
// t1 = w·(L3 + w·(L5 + w·L7)), t2 = z·(L2 + w·(L4 + w·(L6 + w·L8))). Vectorized log_v reuses these.
|
|
549
|
+
// STRING coefficients (not numbers): horner2 only ever interpolates them into the WAT
|
|
550
|
+
// (`(f64.const ${c})`), and under self-host that `${number}` goes through jz's shortest-repr
|
|
551
|
+
// __ftoa, which truncates to ~9 sig figs — so the kernel's log_v poly diverged from scalar
|
|
552
|
+
// $math.log (which inlines the exact literals). As strings, the full-precision decimal is
|
|
553
|
+
// embedded verbatim and watr parses it exactly, in both the host and the self-host kernel.
|
|
554
|
+
const LOG_T1 = ['0.3999999999940941908', '0.2222219843214978396', '0.1531383769920937332']
|
|
555
|
+
const LOG_T2 = ['0.6666666666666735130', '0.2857142874366239149', '0.1818357216161805012', '0.1479819860511658591']
|
|
556
|
+
// Shared reduce → r ∈ [−π/2,π/2] in $r, quadrant parity in $q (branchless, 2 passes).
|
|
557
|
+
const reduce2 = `
|
|
558
|
+
(local.set $q (f64x2.nearest (f64x2.mul (local.get $x) ${splat(INV_PI)})))
|
|
559
|
+
(local.set $r (f64x2.sub (local.get $x) (f64x2.mul (local.get $q) ${splat(PI)})))
|
|
560
|
+
(local.set $q2 (f64x2.nearest (f64x2.mul (local.get $r) ${splat(INV_PI)})))
|
|
561
|
+
(local.set $r (f64x2.sub (local.get $r) (f64x2.mul (local.get $q2) ${splat(PI)})))
|
|
562
|
+
(local.set $q (f64x2.add (local.get $q) (local.get $q2)))
|
|
563
|
+
(local.set $q (f64x2.sub (local.get $q) (f64x2.mul ${splat(2)} (f64x2.nearest (f64x2.mul (local.get $q) ${splat(0.5)})))))
|
|
564
|
+
(local.set $r2 (f64x2.mul (local.get $r) (local.get $r)))`
|
|
565
|
+
// r XOR (|q|>0.5 ? −0.0 : 0), then clamp to [−1,1].
|
|
566
|
+
const signClamp = `
|
|
567
|
+
(local.set $r (v128.xor (local.get $r)
|
|
568
|
+
(v128.and (f64x2.gt (f64x2.abs (local.get $q)) ${splat(0.5)}) ${splat('-0.0')})))
|
|
569
|
+
(f64x2.min (f64x2.max (local.get $r) ${splat(-1)}) ${splat(1)})`
|
|
570
|
+
wat('math.sin2', `(func $math.sin2 (param $x v128) (result v128)
|
|
571
|
+
(local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
|
|
572
|
+
(local.set $r (f64x2.mul (local.get $r) ${horner2(SIN_C)}))${signClamp})`)
|
|
573
|
+
wat('math.cos2', `(func $math.cos2 (param $x v128) (result v128)
|
|
574
|
+
(local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
|
|
575
|
+
(local.set $r ${horner2(COS_C)})${signClamp})`)
|
|
576
|
+
// pow has no cheap 2-lane polynomial (it is exp(y·ln x) with cancellation-sensitive reductions),
|
|
577
|
+
// so the f64x2 mirror computes each lane with the scalar $math.pow and repacks — BIT-EXACT by
|
|
578
|
+
// construction. No transcendental speedup, but it keeps a pow-bearing pixel kernel's surrounding
|
|
579
|
+
// f64x2 arithmetic vectorized (the per-pixel-color pass only emits this when a truly-2-wide op —
|
|
580
|
+
// sin2/cos2/sqrt — already justifies the pair, so the extract/repack never makes a kernel slower).
|
|
581
|
+
wat('math.pow2', `(func $math.pow2 (param $x v128) (param $y v128) (result v128)
|
|
582
|
+
(f64x2.replace_lane 1
|
|
583
|
+
(f64x2.splat (call $math.pow (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
|
|
584
|
+
(call $math.pow (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.pow'])
|
|
585
|
+
|
|
586
|
+
// atan2/hypot/log have no cheap 2-lane polynomial (multi-`return` fdlibm bodies), so — like pow2 —
|
|
587
|
+
// each f64x2 mirror computes both lanes with the SCALAR helper and repacks: BIT-EXACT by
|
|
588
|
+
// construction. The per-pixel-color pass only emits these when a truly-2-wide op (sin2/cos2/sqrt)
|
|
589
|
+
// already justifies the f64x2 pair, so the extract/repack never makes a kernel slower.
|
|
590
|
+
// NOTE: names avoid the $math.log2/$math.exp2 collision (those are log-/exp-BASE-2).
|
|
591
|
+
wat('math.atan2_2', `(func $math.atan2_2 (param $y v128) (param $x v128) (result v128)
|
|
592
|
+
(f64x2.replace_lane 1
|
|
593
|
+
(f64x2.splat (call $math.atan2 (f64x2.extract_lane 0 (local.get $y)) (f64x2.extract_lane 0 (local.get $x))))
|
|
594
|
+
(call $math.atan2 (f64x2.extract_lane 1 (local.get $y)) (f64x2.extract_lane 1 (local.get $x)))))`, ['math.atan2'])
|
|
595
|
+
wat('math.hypot_2', `(func $math.hypot_2 (param $x v128) (param $y v128) (result v128)
|
|
596
|
+
(f64x2.replace_lane 1
|
|
597
|
+
(f64x2.splat (call $math.hypot (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
|
|
598
|
+
(call $math.hypot (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.hypot'])
|
|
599
|
+
// True f64x2 log — both lanes through one fdlibm poly (≈2× over two scalar calls). The HOT path
|
|
600
|
+
// (both lanes a normal finite x>0) mirrors $math.log's normal branch op-for-op: bit-exact (the
|
|
601
|
+
// sqrt2-center conditional becomes a per-lane bitselect; the i32 exponent k becomes an f64 via the
|
|
602
|
+
// 2^52 magic-add, identical to convert_i32_s for |k|≤1075). Any other lane (≤0/∞/NaN/denormal)
|
|
603
|
+
// routes BOTH lanes to the scalar fallback → bit-exact by construction, edges never lose precision.
|
|
604
|
+
wat('math.log_v', `(func $math.log_v (param $x v128) (result v128)
|
|
605
|
+
(local $k v128) (local $m v128) (local $mask v128) (local $f v128) (local $s v128) (local $z v128) (local $w v128) (local $hfsq v128)
|
|
606
|
+
(if (result v128)
|
|
607
|
+
(i64x2.all_true (v128.and
|
|
608
|
+
(f64x2.ge (local.get $x) (f64x2.splat (f64.const 0x1p-1022)))
|
|
609
|
+
(f64x2.lt (local.get $x) (f64x2.splat (f64.const inf)))))
|
|
610
|
+
(then
|
|
611
|
+
(local.set $k (f64x2.sub
|
|
612
|
+
(v128.or (v128.and (i64x2.shr_u (local.get $x) (i32.const 52)) (i64x2.splat (i64.const 0x7ff)))
|
|
613
|
+
(i64x2.splat (i64.const 0x4330000000000000)))
|
|
614
|
+
(f64x2.splat (f64.const 4503599627371519))))
|
|
615
|
+
(local.set $m (v128.or (v128.and (local.get $x) (i64x2.splat (i64.const 0x000fffffffffffff))) (i64x2.splat (i64.const 0x3ff0000000000000))))
|
|
616
|
+
(local.set $mask (f64x2.ge (local.get $m) (f64x2.splat (f64.const 1.4142135623730951))))
|
|
617
|
+
(local.set $m (v128.bitselect (f64x2.mul (local.get $m) (f64x2.splat (f64.const 0.5))) (local.get $m) (local.get $mask)))
|
|
618
|
+
(local.set $k (f64x2.add (local.get $k) (v128.and (local.get $mask) (f64x2.splat (f64.const 1.0)))))
|
|
619
|
+
(local.set $f (f64x2.sub (local.get $m) (f64x2.splat (f64.const 1.0))))
|
|
620
|
+
(local.set $s (f64x2.div (local.get $f) (f64x2.add (local.get $f) (f64x2.splat (f64.const 2.0)))))
|
|
621
|
+
(local.set $z (f64x2.mul (local.get $s) (local.get $s)))
|
|
622
|
+
(local.set $w (f64x2.mul (local.get $z) (local.get $z)))
|
|
623
|
+
(local.set $hfsq (f64x2.mul (f64x2.splat (f64.const 0.5)) (f64x2.mul (local.get $f) (local.get $f))))
|
|
624
|
+
(f64x2.add
|
|
625
|
+
(f64x2.mul (local.get $k) (f64x2.splat (f64.const ${Math.LN2})))
|
|
626
|
+
(f64x2.add (f64x2.sub (local.get $f) (local.get $hfsq))
|
|
627
|
+
(f64x2.mul (local.get $s) (f64x2.add (local.get $hfsq)
|
|
628
|
+
(f64x2.add
|
|
629
|
+
(f64x2.mul (local.get $w) ${horner2(LOG_T1, '$w')})
|
|
630
|
+
(f64x2.mul (local.get $z) ${horner2(LOG_T2, '$w')})))))))
|
|
631
|
+
(else
|
|
632
|
+
(f64x2.replace_lane 1
|
|
633
|
+
(f64x2.splat (call $math.log (f64x2.extract_lane 0 (local.get $x))))
|
|
634
|
+
(call $math.log (f64x2.extract_lane 1 (local.get $x)))))))`, ['math.log'])
|
|
635
|
+
|
|
636
|
+
// True f64x2 exp2 — hot path (round(y) ∈ (−1023,1024), the normal-result range) mirrors $math.exp2's
|
|
637
|
+
// single-IEEE-build branch op-for-op (Horner over f=y−round(y), 2^k via (k+1023)<<52); edges
|
|
638
|
+
// (overflow/underflow/denormal/NaN) route both lanes to the scalar fallback → bit-exact.
|
|
639
|
+
wat('math.exp2_v', `(func $math.exp2_v (param $y v128) (result v128)
|
|
640
|
+
(local $k v128) (local $f v128)
|
|
641
|
+
(local.set $k (f64x2.nearest (local.get $y)))
|
|
642
|
+
(if (result v128)
|
|
643
|
+
(i64x2.all_true (v128.and
|
|
644
|
+
(f64x2.gt (local.get $k) (f64x2.splat (f64.const -1023)))
|
|
645
|
+
(f64x2.lt (local.get $k) (f64x2.splat (f64.const 1024)))))
|
|
646
|
+
(then
|
|
647
|
+
(local.set $f (f64x2.sub (local.get $y) (local.get $k)))
|
|
648
|
+
(f64x2.mul ${horner2(EXP2_C, '$f')}
|
|
649
|
+
(i64x2.shl (i64x2.add
|
|
650
|
+
(i64x2.extend_low_i32x4_s (i32x4.trunc_sat_f64x2_s_zero (local.get $k)))
|
|
651
|
+
(i64x2.splat (i64.const 1023))) (i32.const 52))))
|
|
652
|
+
(else
|
|
653
|
+
(f64x2.replace_lane 1
|
|
654
|
+
(f64x2.splat (call $math.exp2 (f64x2.extract_lane 0 (local.get $y))))
|
|
655
|
+
(call $math.exp2 (f64x2.extract_lane 1 (local.get $y)))))))`, ['math.exp2'])
|
|
656
|
+
|
|
657
|
+
// e^x = 2^(x·log2e) — defers to exp2_v exactly as scalar $math.exp defers to $math.exp2. Bit-exact.
|
|
658
|
+
wat('math.exp_v', `(func $math.exp_v (param $x v128) (result v128)
|
|
659
|
+
(call $math.exp2_v (f64x2.mul (local.get $x) (f64x2.splat (f64.const ${Math.LOG2E})))))`, ['math.exp2_v'])
|
|
660
|
+
|
|
502
661
|
// e^x = 2^(x·log2 e) — defer to the faster $math.exp2 (one multiply, no division, and
|
|
503
662
|
// exp2's NaN/overflow/underflow guards cover exp's). Accurate to exp2's ~6e-9, better
|
|
504
663
|
// than the old 7-term Taylor, and it shares one code path with `2**`.
|
package/module/number.js
CHANGED
|
@@ -145,24 +145,237 @@ const sciExponent = (tail) => `
|
|
|
145
145
|
${tail}))`
|
|
146
146
|
|
|
147
147
|
// Apply the accumulated base-10 exponent to $result via __pow10.
|
|
148
|
+
// Used as fallback when EL cannot determine a unique rounding.
|
|
148
149
|
const POW10_SCALE = `
|
|
149
150
|
(if (i32.gt_s (local.get $decExp) (i32.const 0))
|
|
150
151
|
(then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
|
|
151
152
|
(if (i32.lt_s (local.get $decExp) (i32.const 0))
|
|
152
153
|
(then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))`
|
|
153
154
|
|
|
155
|
+
// Eisel-Lemire correctly-rounded decimal-to-f64.
|
|
156
|
+
// Used in place of FINISH_SIGNIFICAND + POW10_SCALE. Keeps $mant as i64 until after
|
|
157
|
+
// sciExponent finalizes $decExp, then calls $__dec_to_f64 with both.
|
|
158
|
+
// Falls back to f64.convert_i64_u + POW10_SCALE when EL returns NaN (ambiguous).
|
|
159
|
+
// The caller handles sign INSIDE this fragment (so the final return is the signed result).
|
|
160
|
+
const EL_SCALE = `
|
|
161
|
+
(if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
|
|
162
|
+
(if (local.get $round) (then (local.set $mant (i64.add (local.get $mant) (i64.const 1)))))
|
|
163
|
+
(local.set $result (call $__dec_to_f64 (local.get $mant) (local.get $decExp)))
|
|
164
|
+
(if (f64.ne (local.get $result) (local.get $result))
|
|
165
|
+
(then
|
|
166
|
+
(local.set $result (f64.convert_i64_u (local.get $mant)))
|
|
167
|
+
${POW10_SCALE}))
|
|
168
|
+
(if (local.get $neg) (then (local.set $result (f64.neg (local.get $result)))))`
|
|
169
|
+
|
|
170
|
+
// $__dec_to_f64: Eisel-Lemire correctly-rounded f64 from (mant × 10^exp10).
|
|
171
|
+
// Returns NaN (as sentinel) for ambiguous cases — caller falls back to POW10_SCALE.
|
|
172
|
+
// Reads 651-entry 128-bit power-of-ten table from global $__el_tbl (injected at compile time).
|
|
173
|
+
// Table layout: entry[i] = lo_i64_LE || hi_i64_LE, i = exp10 + 342.
|
|
174
|
+
//
|
|
175
|
+
// Algorithm: normalize mant to 64 bits, multiply by 128-bit table entry to get 128-bit
|
|
176
|
+
// product (prodHi:prodLo), extract 52-bit IEEE mantissa with correct rounding.
|
|
177
|
+
// Handles subnormals, overflow to Infinity, and early-exit for 0/trivial ranges.
|
|
178
|
+
const DEC_TO_F64_WAT = `(func $__dec_to_f64
|
|
179
|
+
(param $mant i64) (param $exp10 i32)
|
|
180
|
+
(result f64)
|
|
181
|
+
(local $mBits i32)
|
|
182
|
+
(local $w i64)
|
|
183
|
+
(local $tbl i32)
|
|
184
|
+
(local $tblHi i64) (local $tblLo i64)
|
|
185
|
+
;; 128-bit product: prodHi × 2^64 + prodLo
|
|
186
|
+
(local $p1hi i64) (local $p1lo i64)
|
|
187
|
+
(local $p2hi i64) (local $p2lo i64)
|
|
188
|
+
(local $prodHi i64) (local $prodLo i64)
|
|
189
|
+
(local $carry i64)
|
|
190
|
+
(local $lz i32)
|
|
191
|
+
(local $log2floor i32)
|
|
192
|
+
(local $exp2 i32) (local $biased i32)
|
|
193
|
+
(local $roundShift i32)
|
|
194
|
+
(local $roundBit i64)
|
|
195
|
+
(local $sticky i64)
|
|
196
|
+
(local $mant52 i64)
|
|
197
|
+
(local $subnShift i32) (local $totalShift i32)
|
|
198
|
+
(local $snRound i64) (local $snSticky i64) (local $snMant i64)
|
|
199
|
+
;; 32-bit limb temps for mul64
|
|
200
|
+
(local $a0 i32) (local $a1 i32) (local $b0 i32) (local $b1 i32)
|
|
201
|
+
(local $t00 i64) (local $t01 i64) (local $t10 i64) (local $t11 i64)
|
|
202
|
+
(local $mid i64) (local $mid_carry i64)
|
|
203
|
+
;; Zero check
|
|
204
|
+
(if (i64.eqz (local.get $mant)) (then (return (f64.const 0))))
|
|
205
|
+
;; Compute bit length of mant (1..64) by normalizing to 64 bits via clz
|
|
206
|
+
;; mBits = 64 - clz64(mant)
|
|
207
|
+
(local.set $mBits (i32.sub (i32.const 64) (i32.wrap_i64 (i64.clz (local.get $mant)))))
|
|
208
|
+
;; Normalize mant to 64-bit: w = mant << (64 - mBits)
|
|
209
|
+
(local.set $w (i64.shl (local.get $mant) (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $mBits)))))
|
|
210
|
+
;; Range checks: the table is TRIMMED to exp10 in [-65..65] (covers every
|
|
211
|
+
;; real-world / source / JSON constant; fft/synth's coefficients are ~10^-23).
|
|
212
|
+
;; Outside that, return NaN → caller falls back to POW10_SCALE (the pre-EL path,
|
|
213
|
+
;; ~1 ULP for moderate exponents, exact only near the f64 limits no real literal
|
|
214
|
+
;; reaches). This keeps the data segment ~2KB instead of ~10KB on every program
|
|
215
|
+
;; that does an untyped numeric coercion (which pulls __to_num).
|
|
216
|
+
(if (i32.or (i32.lt_s (local.get $exp10) (i32.const -65))
|
|
217
|
+
(i32.gt_s (local.get $exp10) (i32.const 65)))
|
|
218
|
+
(then (return (f64.const nan))))
|
|
219
|
+
;; Load 128-bit table entry for exp10: tbl = $__el_tbl + (exp10 + 65) * 16
|
|
220
|
+
(local.set $tbl (i32.add (global.get $__el_tbl) (i32.shl (i32.add (local.get $exp10) (i32.const 65)) (i32.const 4))))
|
|
221
|
+
(local.set $tblLo (i64.load (local.get $tbl)))
|
|
222
|
+
(local.set $tblHi (i64.load (i32.add (local.get $tbl) (i32.const 8))))
|
|
223
|
+
;; ─── Two-product: (prodHi:prodLo) = w × tblHi + (w × tblLo >> 64) ──────────
|
|
224
|
+
;; mul64(w, tblHi) → p1hi:p1lo
|
|
225
|
+
;; Split w into 32-bit halves
|
|
226
|
+
(local.set $a0 (i32.wrap_i64 (i64.and (local.get $w) (i64.const 0xFFFFFFFF))))
|
|
227
|
+
(local.set $a1 (i32.wrap_i64 (i64.shr_u (local.get $w) (i64.const 32))))
|
|
228
|
+
(local.set $b0 (i32.wrap_i64 (i64.and (local.get $tblHi) (i64.const 0xFFFFFFFF))))
|
|
229
|
+
(local.set $b1 (i32.wrap_i64 (i64.shr_u (local.get $tblHi) (i64.const 32))))
|
|
230
|
+
(local.set $t00 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b0))))
|
|
231
|
+
(local.set $t01 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b1))))
|
|
232
|
+
(local.set $t10 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b0))))
|
|
233
|
+
(local.set $t11 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b1))))
|
|
234
|
+
;; mid = t01 + (t00>>32), track carry; then mid += t10, track carry
|
|
235
|
+
;; Each addition can carry at most 1 bit; total carry ≤ 2 → hi += carry<<32
|
|
236
|
+
(local.set $mid (i64.add (local.get $t01) (i64.shr_u (local.get $t00) (i64.const 32))))
|
|
237
|
+
(local.set $mid_carry (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t01))))
|
|
238
|
+
(local.set $mid (i64.add (local.get $mid) (local.get $t10)))
|
|
239
|
+
(local.set $mid_carry (i64.add (local.get $mid_carry)
|
|
240
|
+
(i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t10)))))
|
|
241
|
+
(local.set $p1hi (i64.add
|
|
242
|
+
(i64.add (local.get $t11) (i64.shr_u (local.get $mid) (i64.const 32)))
|
|
243
|
+
(i64.shl (local.get $mid_carry) (i64.const 32))))
|
|
244
|
+
(local.set $p1lo (i64.or
|
|
245
|
+
(i64.and (local.get $t00) (i64.const 0xFFFFFFFF))
|
|
246
|
+
(i64.shl (i64.and (local.get $mid) (i64.const 0xFFFFFFFF)) (i64.const 32))))
|
|
247
|
+
;; mul64(w, tblLo) → p2hi:p2lo
|
|
248
|
+
(local.set $b0 (i32.wrap_i64 (i64.and (local.get $tblLo) (i64.const 0xFFFFFFFF))))
|
|
249
|
+
(local.set $b1 (i32.wrap_i64 (i64.shr_u (local.get $tblLo) (i64.const 32))))
|
|
250
|
+
(local.set $t00 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b0))))
|
|
251
|
+
(local.set $t01 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b1))))
|
|
252
|
+
(local.set $t10 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b0))))
|
|
253
|
+
(local.set $t11 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b1))))
|
|
254
|
+
(local.set $mid (i64.add (local.get $t01) (i64.shr_u (local.get $t00) (i64.const 32))))
|
|
255
|
+
(local.set $mid_carry (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t01))))
|
|
256
|
+
(local.set $mid (i64.add (local.get $mid) (local.get $t10)))
|
|
257
|
+
(local.set $mid_carry (i64.add (local.get $mid_carry)
|
|
258
|
+
(i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t10)))))
|
|
259
|
+
(local.set $p2hi (i64.add
|
|
260
|
+
(i64.add (local.get $t11) (i64.shr_u (local.get $mid) (i64.const 32)))
|
|
261
|
+
(i64.shl (local.get $mid_carry) (i64.const 32))))
|
|
262
|
+
(local.set $p2lo (i64.or
|
|
263
|
+
(i64.and (local.get $t00) (i64.const 0xFFFFFFFF))
|
|
264
|
+
(i64.shl (i64.and (local.get $mid) (i64.const 0xFFFFFFFF)) (i64.const 32))))
|
|
265
|
+
;; Combine: prodHi:prodLo = p1hi:(p1lo + p2hi) with carry propagation
|
|
266
|
+
(local.set $carry (i64.add (local.get $p1lo) (local.get $p2hi)))
|
|
267
|
+
;; Check if carry propagated (carry < p1lo → overflow into prodHi)
|
|
268
|
+
(local.set $prodHi (i64.add (local.get $p1hi)
|
|
269
|
+
(i64.extend_i32_u (i64.lt_u (local.get $carry) (local.get $p1lo)))))
|
|
270
|
+
(local.set $prodLo (local.get $carry))
|
|
271
|
+
;; ─── Determine lz (leading-zero flag): 1 if MSB of prodHi is 0 ──────────────
|
|
272
|
+
(local.set $lz (i32.wrap_i64 (i64.xor (i64.shr_u (local.get $prodHi) (i64.const 63)) (i64.const 1))))
|
|
273
|
+
;; ─── Compute biased exponent ─────────────────────────────────────────────────
|
|
274
|
+
;; floor(exp10 * log2(10)) via fixed-point: (exp10 * 14267572527) >> 32
|
|
275
|
+
;; 14267572527 = floor(log2(10) * 2^32) — correct for all exp10 in -342..308.
|
|
276
|
+
(local.set $log2floor (i32.wrap_i64 (i64.shr_s
|
|
277
|
+
(i64.mul (i64.extend_i32_s (local.get $exp10)) (i64.const 14267572527))
|
|
278
|
+
(i64.const 32))))
|
|
279
|
+
;; exp2 = mBits + floor(exp10 * log2(10)) - lz
|
|
280
|
+
(local.set $exp2 (i32.sub (i32.add (local.get $mBits) (local.get $log2floor)) (local.get $lz)))
|
|
281
|
+
(local.set $biased (i32.add (local.get $exp2) (i32.const 1023)))
|
|
282
|
+
;; Overflow → Infinity
|
|
283
|
+
(if (i32.ge_s (local.get $biased) (i32.const 2047)) (then (return (f64.const inf))))
|
|
284
|
+
;; ─── Subnormal path (biased ≤ 0) ────────────────────────────────────────────
|
|
285
|
+
(if (i32.le_s (local.get $biased) (i32.const 0))
|
|
286
|
+
(then
|
|
287
|
+
;; total_shift = 11 - lz + (1 - biased) = 12 - lz - biased
|
|
288
|
+
(local.set $totalShift (i32.sub (i32.sub (i32.const 12) (local.get $lz)) (local.get $biased)))
|
|
289
|
+
;; If totalShift >= 64: prodHi >> totalShift == 0 in BigInt, but here:
|
|
290
|
+
;; For totalShift in [64..127]: snMant = (prodHi >> (totalShift-64)) >> 64... = 0
|
|
291
|
+
;; We just use BigInt-style: clamp to 0 if >=64 (prodHi is 64-bit)
|
|
292
|
+
(if (i32.ge_u (local.get $totalShift) (i32.const 64))
|
|
293
|
+
(then
|
|
294
|
+
;; All mantissa bits are 0; only rounding could give min subnormal.
|
|
295
|
+
;; round bit: at position (totalShift-1) of prodHi → always 0 for totalShift>=65
|
|
296
|
+
;; For totalShift==64: round bit = bit 63 of prodHi = MSB
|
|
297
|
+
(if (i32.eq (local.get $totalShift) (i32.const 64))
|
|
298
|
+
(then
|
|
299
|
+
(local.set $snRound (i64.shr_u (local.get $prodHi) (i64.const 63)))
|
|
300
|
+
(local.set $snSticky (i64.or (local.get $prodLo) (local.get $p2lo)))
|
|
301
|
+
;; Return min-subnormal if snRound=1 AND snSticky!=0 (boolean AND, not bitwise)
|
|
302
|
+
(if (i32.and
|
|
303
|
+
(i32.wrap_i64 (local.get $snRound))
|
|
304
|
+
(i64.ne (local.get $snSticky) (i64.const 0)))
|
|
305
|
+
(then (return (f64.reinterpret_i64 (i64.const 1)))))
|
|
306
|
+
))
|
|
307
|
+
(return (f64.const 0))))
|
|
308
|
+
;; totalShift in [1..63]: extract directly from prodHi
|
|
309
|
+
(local.set $snMant (i64.and
|
|
310
|
+
(i64.shr_u (local.get $prodHi) (i64.extend_i32_u (local.get $totalShift)))
|
|
311
|
+
(i64.const 0x000FFFFFFFFFFFFF)))
|
|
312
|
+
(local.set $snRound
|
|
313
|
+
(i64.and (i64.shr_u (local.get $prodHi)
|
|
314
|
+
(i64.extend_i32_u (i32.sub (local.get $totalShift) (i32.const 1))))
|
|
315
|
+
(i64.const 1)))
|
|
316
|
+
(local.set $snSticky (i64.or
|
|
317
|
+
(i64.and (local.get $prodHi)
|
|
318
|
+
(i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (i32.sub (local.get $totalShift) (i32.const 1))))
|
|
319
|
+
(i64.const 1)))
|
|
320
|
+
(i64.or (local.get $prodLo) (local.get $p2lo))))
|
|
321
|
+
;; Round: snMant++ if roundBit && (sticky > 0 || snMant is odd)
|
|
322
|
+
(if (i64.ne (i64.and (local.get $snRound)
|
|
323
|
+
(i64.or (i64.extend_i32_u (i64.ne (local.get $snSticky) (i64.const 0)))
|
|
324
|
+
(i64.and (local.get $snMant) (i64.const 1))))
|
|
325
|
+
(i64.const 0))
|
|
326
|
+
(then (local.set $snMant (i64.add (local.get $snMant) (i64.const 1)))))
|
|
327
|
+
;; Overflow of subnormal mantissa → minimum normal (biased=1, mant=0)
|
|
328
|
+
(if (i64.ge_u (local.get $snMant) (i64.const 0x0010000000000000))
|
|
329
|
+
(then (return (f64.reinterpret_i64 (i64.const 0x0010000000000000)))))
|
|
330
|
+
(return (f64.reinterpret_i64 (local.get $snMant)))))
|
|
331
|
+
;; ─── Normal path ─────────────────────────────────────────────────────────────
|
|
332
|
+
;; roundShift = 10 - lz (bit position of round bit in prodHi)
|
|
333
|
+
(local.set $roundShift (i32.sub (i32.const 10) (local.get $lz)))
|
|
334
|
+
(local.set $roundBit (i64.and
|
|
335
|
+
(i64.shr_u (local.get $prodHi) (i64.extend_i32_u (local.get $roundShift)))
|
|
336
|
+
(i64.const 1)))
|
|
337
|
+
;; sticky = bits below roundBit in prodHi, plus all of prodLo and p2lo
|
|
338
|
+
(local.set $sticky (i64.or
|
|
339
|
+
(i64.and (local.get $prodHi)
|
|
340
|
+
(i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (local.get $roundShift)))
|
|
341
|
+
(i64.const 1)))
|
|
342
|
+
(i64.or (local.get $prodLo) (local.get $p2lo))))
|
|
343
|
+
;; mant52 = (prodHi >> (11 - lz)) & MASK52
|
|
344
|
+
(local.set $mant52 (i64.and
|
|
345
|
+
(i64.shr_u (local.get $prodHi) (i64.extend_i32_u (i32.sub (i32.const 11) (local.get $lz))))
|
|
346
|
+
(i64.const 0x000FFFFFFFFFFFFF)))
|
|
347
|
+
;; Round: mant52++ if roundBit && (sticky > 0 || mant52 is odd)
|
|
348
|
+
(if (i64.ne (i64.and (local.get $roundBit)
|
|
349
|
+
(i64.or (i64.extend_i32_u (i64.ne (local.get $sticky) (i64.const 0)))
|
|
350
|
+
(i64.and (local.get $mant52) (i64.const 1))))
|
|
351
|
+
(i64.const 0))
|
|
352
|
+
(then (local.set $mant52 (i64.add (local.get $mant52) (i64.const 1)))))
|
|
353
|
+
;; Mantissa overflow: carry into exponent
|
|
354
|
+
(if (i64.ge_u (local.get $mant52) (i64.const 0x0010000000000000))
|
|
355
|
+
(then
|
|
356
|
+
(local.set $mant52 (i64.const 0))
|
|
357
|
+
(local.set $biased (i32.add (local.get $biased) (i32.const 1)))))
|
|
358
|
+
;; Final overflow check after rounding
|
|
359
|
+
(if (i32.ge_s (local.get $biased) (i32.const 2047)) (then (return (f64.const inf))))
|
|
360
|
+
;; Assemble IEEE 754 bits: biased_exp << 52 | mant52
|
|
361
|
+
(f64.reinterpret_i64
|
|
362
|
+
(i64.or
|
|
363
|
+
(i64.shl (i64.extend_i32_u (local.get $biased)) (i64.const 52))
|
|
364
|
+
(local.get $mant52))))`
|
|
365
|
+
|
|
154
366
|
export default (ctx) => {
|
|
155
367
|
deps({
|
|
156
368
|
__mkstr: ['__alloc'],
|
|
157
369
|
__ftoa: ['__itoa', '__pow10', '__mkstr', '__static_str', '__toExp'],
|
|
370
|
+
__i32_to_str: ['__itoa', '__mkstr'],
|
|
158
371
|
__toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
|
|
159
372
|
__radix_str: ['__mkstr'],
|
|
160
373
|
__num_radix: ['__ftoa', '__mkstr'],
|
|
161
|
-
__to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str', '__skipws', '__ptr_aux'],
|
|
374
|
+
__to_num: ['__char_at', '__str_byteLen', '__pow10', '__dec_to_f64', '__to_str', '__skipws', '__ptr_aux'],
|
|
162
375
|
__skipws: ['__char_at', '__strws'],
|
|
163
376
|
__to_bigint: ['__char_at', '__str_byteLen', '__num_to_bigint'],
|
|
164
377
|
__parseInt: ['__char_at', '__str_byteLen'],
|
|
165
|
-
__parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
|
|
378
|
+
__parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__dec_to_f64', '__to_str'],
|
|
166
379
|
})
|
|
167
380
|
|
|
168
381
|
|
|
@@ -215,6 +428,26 @@ export default (ctx) => {
|
|
|
215
428
|
${reverseBytesWat()}
|
|
216
429
|
(local.get $len))`
|
|
217
430
|
|
|
431
|
+
// __i32_to_str(val: i32) → f64 (NaN-boxed string) — ToString for a value the
|
|
432
|
+
// compiler proved is a signed i32. The whole point is to bypass __ftoa's float
|
|
433
|
+
// machinery (shortest-repr search, __toExp, __pow10): a known integer renders with
|
|
434
|
+
// just __itoa over its magnitude plus a sign byte. Lets `"id " + (n|0)` and integer
|
|
435
|
+
// templates skip the ~2 KB float formatter the generic ToString hard-pulls.
|
|
436
|
+
ctx.core.stdlib['__i32_to_str'] = `(func $__i32_to_str (param $val i32) (result f64)
|
|
437
|
+
(local $buf i32) (local $len i32) (local $u i32)
|
|
438
|
+
(local.set $buf (call $__alloc (i32.const 12)))
|
|
439
|
+
(if (i32.lt_s (local.get $val) (i32.const 0))
|
|
440
|
+
(then
|
|
441
|
+
(i32.store8 (local.get $buf) (i32.const 45)) ;; '-'
|
|
442
|
+
;; magnitude as unsigned: negate via 0 - val (INT_MIN maps to itself, read u below)
|
|
443
|
+
(local.set $u (i32.sub (i32.const 0) (local.get $val)))
|
|
444
|
+
(local.set $len (call $__itoa (local.get $u) (i32.add (local.get $buf) (i32.const 1))))
|
|
445
|
+
(return (call $__mkstr (local.get $buf) (i32.add (local.get $len) (i32.const 1)))))
|
|
446
|
+
(else
|
|
447
|
+
(local.set $len (call $__itoa (local.get $val) (local.get $buf)))
|
|
448
|
+
(return (call $__mkstr (local.get $buf) (local.get $len)))))
|
|
449
|
+
(f64.const 0))`
|
|
450
|
+
|
|
218
451
|
// __radix_str(val: i64, radix: i32) → f64 (NaN-boxed string)
|
|
219
452
|
// Signed integer → radix string for BigInt.prototype.toString(radix). Digits go
|
|
220
453
|
// 0-9 then a-z (lowercase, per spec); magnitude is taken unsigned so i64.MIN
|
|
@@ -333,10 +566,11 @@ export default (ctx) => {
|
|
|
333
566
|
(then
|
|
334
567
|
(local.set $b (i32.load8_u (i32.add (local.get $buf) (local.get $i))))
|
|
335
568
|
(br_if $heap (i32.ge_u (local.get $b) (i32.const 0x80)))
|
|
336
|
-
|
|
569
|
+
;; 7-bit ASCII SSO: char i at bit i*7 (chars fit the offset for len ≤4); len at aux bits 10-12.
|
|
570
|
+
(local.set $packed (i32.or (local.get $packed) (i32.shl (local.get $b) (i32.mul (local.get $i) (i32.const 7)))))
|
|
337
571
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
338
572
|
(br $pk))))
|
|
339
|
-
(return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $packed))))))
|
|
573
|
+
(return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $len) (i32.const 10))) (local.get $packed))))))
|
|
340
574
|
(local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
|
|
341
575
|
(i32.store (local.get $off) (local.get $len))
|
|
342
576
|
(local.set $off (i32.add (local.get $off) (i32.const 4)))
|
|
@@ -623,8 +857,31 @@ export default (ctx) => {
|
|
|
623
857
|
ctx.runtime.staticDataLen = staticStr.length
|
|
624
858
|
ctx.runtime.data = (ctx.runtime.data || '') + staticStr
|
|
625
859
|
|
|
860
|
+
// Eisel-Lemire power-of-10 table: 131 entries × 16 bytes = 2096 bytes,
|
|
861
|
+
// staticStr.length = 57, pad 7 → table starts at byte 64 (always constant).
|
|
862
|
+
// The table bytes are stashed in ctx.runtime.elTable. src/compile/index.js appends
|
|
863
|
+
// them to ctx.runtime.data (padded to 8-byte boundary) only when $__dec_to_f64 is
|
|
864
|
+
// actually pulled in, then declares global $__el_tbl = that offset. This keeps the
|
|
865
|
+
// 2096-byte data segment out of modules that don't do decimal→f64 conversion.
|
|
866
|
+
// TRIMMED to exp10 in [-65..65] (see __dec_to_f64 range check). Each entry: 8
|
|
867
|
+
// bytes tblLo (LE) + 8 bytes tblHi (LE), for exp10 = -65 + i.
|
|
868
|
+
const EL_TABLE_HEX = 'e9b4c29f1247e998eaba94ea52bbcc862462b347d798233fa5e939a527ea7fa8ad3aa0190d7fec8e0e64888eb1e49fd2ac24043068cf5319893e15f9eeeea383d72d053c42c3a85f2b8e5ab7aaea8ca44d7906cb12f49237b63131655525b0cdd00be4be8bd8bbe211bf3e5f55178e80c40e9daeaece6a5bd66e0eb72a9db1a07552445a5a8245f28b0ad2647504dec81267d5f0f0e2d6ee2e8d06be928515fb6b608596d64d46553d18c4b67b73ed9c86b8263c4ce197aa4c1e75a45ad028c4a866304b9fd93dd5df65924d710433f52940fe8e03a846e5ab7f7bd0c6e23f9933d0bd72045298de965f9a8478db8fbf40446d8f85663e967cf7c0a556d273efa84aa4791300e7ddad9a98277663a895525d0d5818c0605559c17eb1537c12bba6b4106e1ef0b8aaaf71de9d681bd7e9e870ca041396b3ca0d07ab6221712692220dfdc5977b603dd1c855bb690db0b66a507cb77d9ab88c053b2b2ac4105ce442b2ad928e60f377e3045b9a7a8ab98ed31e5937b238f0551cc6f14019ed67b288662fc5de466c6ba3372e915fe801df15a03d3b4bac2323c6e2bcba3b31618b1a080d0a5e97ecab771b6ca98a7d39ae214a908c35bde7965522c753eddcc7d9542eda7741d6507e75755c5414ea1c88e9b9d0d5d10be5ddd2927369992424aa64e8444bc64e5e958777d0c3bf2dadd43e110bef3bf15abdb44a62da973cec848ed5cdea8aadb1ec61ddfad0bd4b27a6f24a81a5ed18de67ba943945ad1eb1cfd7ce708794cfea80f4fc434b2cb3ce818d024da9798325a131fc145ef75f42a23043a01358e46e093e3b9a35f5f7d2cafc5388186e9dca8b0dca0083f2b587fd7d3455cf64a25e77487ee091b7d1749e9d812a03fe4a3695da9d5876250612c60422f583bddd833a51c5eed3ae8796f742357972966a92c4523b7544cd14be9a9382170f3c05b775278a9295009a6dc13863dd128bc62453b12cf7ba8000c9f1035ecaeb16fcf6d3ee7bda7450a01d9784f5bca61cbbf488ea1a11926408e5bce5326cd0e3e9312ba56195b67d4a1eeccf9f43622e32ff3a075d1d928eee9293c287d4fab9febe0949b4a43632aa77b8b3a9897968be2e4c5be14dc4be9495e6100af64b01379d0fd9acb03af77c1d90948cf39ec18484530fd85c0935dc24b4b96fb006f2a56528130eb44b42132ee1d3452e44b7873ff9cb88506f09ccbc8c48d73915a5698ff7feaa24cb0bffebaf1b4d885a0e4473b5bed5edbdcefee6db303095f8880a683197a5b436415f70893d7cba362b0dc2fdfcce61841177ccab4c1b69047690323dbc427ae5d594bfd60fb1c1c2499a3fa6b5696caf05bd3786531d7233dc80cf0f2384471b47acc5a7a8a44e401361c3d32b6519e25817b7d1e9263108ac1c5a643bdf4f8d976e1283a3703d0ad7a3703d0ad7a3703d0ad7a3cccccccccccccccccccccccccccccccc00000000000000000000000000000080000000000000000000000000000000a0000000000000000000000000000000c8000000000000000000000000000000fa0000000000000000000000000000409c000000000000000000000000000050c3000000000000000000000000000024f4000000000000000000000000008096980000000000000000000000000020bcbe00000000000000000000000000286bee00000000000000000000000000f9029500000000000000000000000040b743ba00000000000000000000000010a5d4e80000000000000000000000002ae78491000000000000000000000080f420e6b50000000000000000000000a031a95fe3000000000000000000000004bfc91b8e0000000000000000000000c52ebca2b10000000000000000000040763a6b0bde00000000000000000000e8890423c78a0000000000000000000062acc5eb78ad000000000000000000807a17b726d7d800000000000000000090ac6e32788687000000000000000000b4570a3f1668a9000000000000000000a1edccce1bc2d30000000000000000a0841440615159840000000000000000c8a51990b9a56fa500000000000000003a0f20f4278fcbce0000000000000040840994f878393f810000000000000050e50bb936d7078fa100000000000000a4de4e6704cdc9f2c9000000000000004d96228145407c6ffc00000000000020f09db5702ba8adc59d000000000000286c05e34c36121937c500000000000032c7c61be0c356df84f60000000000407f3c5c116c3a960b139a0000000000109f4bb31507c97bce97c00000000000d4861e20db48bb1ac2bdf00000000080441413f4880db55099769600000000a055d91731eb50e2a43f14bc0000000008abcf5dfd25e51a8e4f19eb00000000e5caa15abe37cfd0b8d1ef92000000409e3d4af1ad05030527c6abb7000000d005cd9c6d19c743c6b0b796e5000000a2230082e46f5cea7bce327e8f0000808a2c80a2dd8bf3e41a82bf5db3000020ad37200bd56e309ea1622f35e0000034cc22f4264545de02a59d3d218c0000417f2bb17096d695430e058d29af0040115f76dd0c3c4c7bd45146f0f3da00c86afb690a88a50fcd24f32b76d888007a457a040dea8e5300eeefb6930eab80d8d6984590a4726880e9aba438d2d55047867f2bdaa64741f071eb6663a38524d9675fb6909099516c4ea6403c0ca76dcf41f7e3b4f4ff6507e2cf504bcfd0a421897a0ef1f8bf9f44ed81128f81820d6a2b19522df7afc7956822d7f221a39044769fa6f8f49b39bb02eb8c6feacbb4d55347d036f202086ac325700be5fe9065942c4262d70145229a1726274f9ff57eb9b7d23a4d42d6aa809deff022c7b2dea7658789e0d28bd5e0842badebf82feb889ff455cc6377850c333b4c939bfb256bc7716bbf3cd5a6cfff491f78c27aef45394e46ef8b8a90c37f1c2716f3'
|
|
869
|
+
// Pre-decode the EL table bytes once and stash in ctx.runtime.
|
|
870
|
+
// src/compile/index.js appends elTable to ctx.runtime.data only when
|
|
871
|
+
// __dec_to_f64 is actually pulled in via deps (lazy, keeps small modules clean).
|
|
872
|
+
let elTableBytes = ''
|
|
873
|
+
for (let i = 0; i < EL_TABLE_HEX.length; i += 2)
|
|
874
|
+
elTableBytes += String.fromCharCode(parseInt(EL_TABLE_HEX.slice(i, i + 2), 16))
|
|
875
|
+
ctx.runtime.elTable = elTableBytes
|
|
876
|
+
|
|
877
|
+
// Register the stdlib function (no data appended here — see compile/index.js hook)
|
|
878
|
+
ctx.core.stdlib['__dec_to_f64'] = DEC_TO_F64_WAT
|
|
879
|
+
|
|
626
880
|
// === Number constants ===
|
|
627
881
|
|
|
882
|
+
// Each folds to inline (f64.const …), no stdlib dep. Written out (not a table
|
|
883
|
+
// loop) to stay within the self-host subset. `NaN` uses the `nan` token (not raw
|
|
884
|
+
// NaN) so it survives self-host IR marshalling — see emitNum.
|
|
628
885
|
ctx.core.emit['Number.MAX_SAFE_INTEGER'] = () => typed(['f64.const', 9007199254740991], 'f64')
|
|
629
886
|
ctx.core.emit['Number.MIN_SAFE_INTEGER'] = () => typed(['f64.const', -9007199254740991], 'f64')
|
|
630
887
|
ctx.core.emit['Number.EPSILON'] = () => typed(['f64.const', 2.220446049250313e-16], 'f64')
|
|
@@ -632,7 +889,7 @@ export default (ctx) => {
|
|
|
632
889
|
ctx.core.emit['Number.MIN_VALUE'] = () => typed(['f64.const', 5e-324], 'f64')
|
|
633
890
|
ctx.core.emit['Number.POSITIVE_INFINITY'] = () => typed(['f64.const', Infinity], 'f64')
|
|
634
891
|
ctx.core.emit['Number.NEGATIVE_INFINITY'] = () => typed(['f64.const', -Infinity], 'f64')
|
|
635
|
-
ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64')
|
|
892
|
+
ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64')
|
|
636
893
|
|
|
637
894
|
// === Number static methods ===
|
|
638
895
|
|
|
@@ -929,9 +1186,6 @@ export default (ctx) => {
|
|
|
929
1186
|
;; Decimal significand. Keep 18 significant decimal digits, track the
|
|
930
1187
|
;; base-10 exponent for skipped digits, and round once before pow10 scaling.
|
|
931
1188
|
${DEC_SIGNIFICAND}
|
|
932
|
-
;; No digits — the literal was a bare sign or stray text ("abc", "+") → NaN.
|
|
933
|
-
;; (Empty / all-whitespace strings already returned +0 above.)
|
|
934
|
-
${FINISH_SIGNIFICAND}
|
|
935
1189
|
;; Scientific notation. 'e'/'E' commits to an ExponentPart — at least one
|
|
936
1190
|
;; digit must follow ("1e", "5e+" are NaN).
|
|
937
1191
|
${sciExponent(`(if (i32.eqz (local.get $expDigits)) (then (return (f64.const nan))))
|
|
@@ -941,8 +1195,9 @@ export default (ctx) => {
|
|
|
941
1195
|
;; Reject trailing non-whitespace ("5px", numeric separators "1_0", …).
|
|
942
1196
|
(local.set $i (call $__skipws (local.get $v) (local.get $i) (local.get $len)))
|
|
943
1197
|
(if (i32.lt_s (local.get $i) (local.get $len)) (then (return (f64.const nan))))
|
|
944
|
-
|
|
945
|
-
|
|
1198
|
+
;; Eisel-Lemire exact rounding; fallback to __pow10 for ambiguous cases.
|
|
1199
|
+
${EL_SCALE}
|
|
1200
|
+
(local.get $result))`
|
|
946
1201
|
|
|
947
1202
|
// NumberToBigInt: a RangeError unless n is an integral Number — finite and
|
|
948
1203
|
// equal to its own truncation. NaN fails the f64.eq integrality test;
|
|
@@ -1068,15 +1323,15 @@ export default (ctx) => {
|
|
|
1068
1323
|
;; Decimal significand. Keep 18 significant decimal digits, track the
|
|
1069
1324
|
;; base-10 exponent for skipped digits, and round once before pow10 scaling.
|
|
1070
1325
|
${DEC_SIGNIFICAND}
|
|
1071
|
-
${FINISH_SIGNIFICAND}
|
|
1072
1326
|
;; Scientific notation.
|
|
1073
1327
|
${sciExponent(`(if (local.get $expDigits)
|
|
1074
1328
|
(then
|
|
1075
1329
|
(if (local.get $expNeg)
|
|
1076
1330
|
(then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
|
|
1077
1331
|
(else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))`)}
|
|
1078
|
-
|
|
1079
|
-
|
|
1332
|
+
;; Eisel-Lemire exact rounding; fallback to __pow10 for ambiguous cases.
|
|
1333
|
+
${EL_SCALE}
|
|
1334
|
+
(local.get $result))`
|
|
1080
1335
|
|
|
1081
1336
|
// ToString(arg) for the string-input builtins. A statically-known boolean must
|
|
1082
1337
|
// render as "true"/"false" (spec step 1: ToString) before parsing — otherwise
|
package/module/object.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { typed, asF64, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, block64, ptrTypeEq, dispatchByPtrType, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore, boolBoxIR } from '../src/ir.js'
|
|
11
11
|
import { emit } from '../src/bridge.js'
|
|
12
|
+
import { staticArrayPtr } from './array.js'
|
|
12
13
|
import { valTypeOf, shapeOf } from '../src/kind.js'
|
|
13
14
|
import { VAL, lookupValType, repOf, updateRep } from '../src/reps.js'
|
|
14
15
|
import { ctx, err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
|
|
@@ -246,6 +247,31 @@ export default (ctx) => {
|
|
|
246
247
|
}
|
|
247
248
|
ctx.core.emit['Object.getOwnPropertyNames'] = ctx.core.emit['Object.keys']
|
|
248
249
|
|
|
250
|
+
// for-in's read-only key enumeration (src/prepare for…in lowering). Identical to
|
|
251
|
+
// Object.keys EXCEPT: when the receiver is a bare variable with a complete static
|
|
252
|
+
// schema, the key list is a compile-time constant, so it pools ONE static-data
|
|
253
|
+
// array (no per-evaluation alloc) — eliminating for-in's hot-loop heap-growth
|
|
254
|
+
// cliff and its unbounded-allocation OOM. The pooled array is shared/read-only,
|
|
255
|
+
// which is sound because for-in only reads ks[i]/ks.length (Object.keys can't pool:
|
|
256
|
+
// user code may `.sort()`/`.reverse()` the result in place). Anything not a static
|
|
257
|
+
// schema bare-var — arrays/strings/HASH/dyn-props/expressions — delegates to
|
|
258
|
+
// Object.keys (evaluates the receiver, full runtime enumeration).
|
|
259
|
+
ctx.core.emit['__keys_ro'] = (obj) => {
|
|
260
|
+
// Pool only when the receiver's enumerable key set is provably the static
|
|
261
|
+
// schema: a bare var with NO computed-key writes (`o[k]=v`) — those are the
|
|
262
|
+
// only writes that add enumerable keys (computed reads / dot-adds don't).
|
|
263
|
+
// `mayHaveDynProps` is too coarse here — it also flags computed-READ receivers,
|
|
264
|
+
// and for-in's own `o[k]` read would otherwise veto its own pooling.
|
|
265
|
+
if (typeof obj === 'string' && !ctx.types.dynWriteVars?.has(obj) && !isHashTyped(obj) && !arrayValType(obj) && !stringValType(obj)) {
|
|
266
|
+
const schema = resolveSchema(obj)
|
|
267
|
+
if (schema) {
|
|
268
|
+
const slots = schema.map(name => extractF64Bits(asF64(emit(['str', name]))))
|
|
269
|
+
if (slots.every(b => b !== null)) return staticArrayPtr(slots)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return ctx.core.emit['Object.keys'](obj)
|
|
273
|
+
}
|
|
274
|
+
|
|
249
275
|
// Object.prototype.hasOwnProperty(key) — own-property presence check.
|
|
250
276
|
// Compile-time fold for literal keys against object literals or variables
|
|
251
277
|
// with known schemas; runtime path delegates to the `in` operator (same
|
|
@@ -764,11 +790,17 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
|
|
|
764
790
|
else allKnown = false
|
|
765
791
|
} else if (Array.isArray(p) && p[0] === ':') addName(p[1])
|
|
766
792
|
}
|
|
767
|
-
// Single unknown spread `{ ...
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
|
|
793
|
+
// Single unknown spread `{ ...src }` → shallow-clone src at runtime, preserving
|
|
794
|
+
// its type (OBJECT→OBJECT, HASH→HASH). Aliasing src (the old shortcut) leaked
|
|
795
|
+
// every later write to the result back into the source — a real correctness bug
|
|
796
|
+
// (jz's own narrow.js had to hand-route around it). __obj_clone keys off the
|
|
797
|
+
// box's runtime schemaId, so it copies static-segment sources too; the schema
|
|
798
|
+
// table it reads must exist, so declare + force it (assemble.js).
|
|
799
|
+
if (!allKnown && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...') {
|
|
800
|
+
inc('__obj_clone')
|
|
801
|
+
if (!ctx.scope.globals.has('__schema_tbl')) declGlobal('__schema_tbl', 'i32')
|
|
802
|
+
return typed(['call', '$__obj_clone', asF64(emit(props[0][1]))], 'f64')
|
|
803
|
+
}
|
|
772
804
|
if (!allKnown) return emitDynamicSpread(props)
|
|
773
805
|
|
|
774
806
|
const schemaId = ctx.schema.register(allNames)
|