jz 0.7.0 → 0.8.1

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.
Files changed (55) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1366 -1101
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +49 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +167 -117
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +295 -171
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +40 -35
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +324 -34
  28. package/src/compile/index.js +204 -61
  29. package/src/compile/infer.js +8 -1
  30. package/src/compile/loop-divmod.js +12 -58
  31. package/src/compile/loop-model.js +91 -0
  32. package/src/compile/loop-recurrence.js +167 -0
  33. package/src/compile/loop-square.js +102 -0
  34. package/src/compile/narrow.js +180 -34
  35. package/src/compile/peel-stencil.js +18 -64
  36. package/src/compile/plan/common.js +29 -0
  37. package/src/compile/plan/index.js +4 -1
  38. package/src/compile/plan/inline.js +176 -21
  39. package/src/compile/plan/literals.js +93 -19
  40. package/src/ctx.js +51 -12
  41. package/src/helper-counters.js +137 -0
  42. package/src/ir.js +102 -13
  43. package/src/kind-traits.js +7 -3
  44. package/src/kind.js +14 -1
  45. package/src/op-policy.js +5 -2
  46. package/src/ops.js +119 -0
  47. package/src/optimize/index.js +1125 -136
  48. package/src/optimize/recurse.js +182 -0
  49. package/src/optimize/vectorize.js +1302 -144
  50. package/src/prepare/index.js +29 -12
  51. package/src/reps.js +4 -1
  52. package/src/type.js +53 -45
  53. package/src/wat/assemble.js +92 -9
  54. package/src/widen.js +21 -0
  55. package/src/wat/optimize.js +0 -3938
package/module/math.js CHANGED
@@ -37,7 +37,7 @@ export default (ctx) => {
37
37
  'math.log2': ['math.log'],
38
38
  'math.log1p': ['math.log'],
39
39
  'math.pow': ['math.exp', 'math.log'],
40
- 'math.asin': ['math.atan'],
40
+ 'math.asin': [],
41
41
  'math.acos': ['math.asin'],
42
42
  'math.atan2': ['math.atan'],
43
43
  'math.sinh': ['math.exp'],
@@ -46,7 +46,8 @@ export default (ctx) => {
46
46
  'math.asinh': ['math.isFinite', 'math.log'],
47
47
  'math.acosh': ['math.log'],
48
48
  'math.atanh': ['math.log'],
49
- 'math.cbrt': ['math.isFinite', 'math.pow'],
49
+ 'math.cbrt': ['math.isFinite'],
50
+ 'math.fifthroot': ['math.isFinite'],
50
51
  'math.sumPrecise': ['__ptr_offset', '__len', '__alloc'],
51
52
  })
52
53
  // Helpers: all math ops take f64 and return f64. Args go through ToNumber
@@ -176,7 +177,9 @@ export default (ctx) => {
176
177
  n[0] === 'f64.const' ? (typeof n[1] === 'number' && n[1] >= 0) : false
177
178
  const sqrtIR = (a) => { const ir = f('f64.sqrt', a); return nonNegF64(ir[1]) ? ir : canon(ir) }
178
179
 
179
- // Constants
180
+ // Constants — each folds to its `(f64.const …)` inline (no stdlib dep, hence
181
+ // direct emit rather than reg). Written out (not a `Math[name]` loop) because the
182
+ // self-host subset can't resolve dynamic access on the `Math` compile-time namespace.
180
183
  ctx.core.emit['math.PI'] = () => typed(['f64.const', Math.PI], 'f64')
181
184
  ctx.core.emit['math.E'] = () => typed(['f64.const', Math.E], 'f64')
182
185
  ctx.core.emit['math.LN2'] = () => typed(['f64.const', Math.LN2], 'f64')
@@ -358,6 +361,40 @@ export default (ctx) => {
358
361
  // We emit, check, and if not foldable, the emitted IR is used by the fallthrough paths.
359
362
  const irA = toNumF64(a, emit(a)), irB = toNumF64(b, emit(b))
360
363
  if (isLit(irA) && isLit(irB)) return typed(['f64.const', Math.pow(litVal(irA), litVal(irB))], 'f64')
364
+ // Constant non-integer exponent c: inline Math.pow(x,c) as exp(c·log(x)) — that IS $math.pow's
365
+ // own non-integer tail (the final line of $math.pow below), so it is BIT-IDENTICAL to the call
366
+ // for every finite x and for x ∈ {±0, +∞, NaN}: log+exp carry the edges (log(NaN)=NaN,
367
+ // log(0)=−∞, log(<0)=NaN, log(+∞)=+∞; exp(±∞)=∞/0). Skipping the ~15-branch pow special-case
368
+ // ladder + the call frame is a per-pixel win on the gamma curves (v**0.45, a**(1/2.4)) that
369
+ // dominate tone-mapping, and a program whose only pow is folded this way never pulls $math.pow.
370
+ // The ONE divergence is x=−∞: this yields NaN where Math.pow gives ±∞ — the same deliberate
371
+ // boundary trade jz already makes for `(−∞)**0.5` (see the sqrt fold above); −∞ is never a real
372
+ // tone-map/gamma base. Integers stay on $math.pow (its square-and-multiply path is bit-different
373
+ // from exp/log); ±0.5 stays sqrt / the exact pow path (handled above).
374
+ if (isLit(irB)) {
375
+ const c = litVal(irB)
376
+ // Constant exponent with denominator 5 and < 5 (the sRGB / Rec.709 decode gammas 2.4, 2.2,
377
+ // and any k/5): x^(k/5) = x^p · fifthroot(x^r), p=⌊c⌋, r=5c−5p ∈ 1..4 — algebraic, no exp/log,
378
+ // ~3.6e-10 rel err over x ∈ (0,1]. Beats exp(c·log x): the pow microbench drops below V8's
379
+ // own Math.pow intrinsic. x<0 → NaN to match Math.pow on a non-integer exponent (the exp·log
380
+ // form's log(<0)=NaN). x=0/+∞/NaN all carry correctly through the power + fifthroot.
381
+ if (Number.isFinite(c) && c > 0 && c < 5 && !Number.isInteger(c) && Number.isInteger(c * 5)) {
382
+ inc('math.fifthroot')
383
+ const t = temp('pw'), g = get(t)
384
+ const ipow = (k) => k === 1 ? g : k === 2 ? ['f64.mul', g, g]
385
+ : k === 3 ? ['f64.mul', ['f64.mul', g, g], g] : ['f64.mul', ['f64.mul', g, g], ['f64.mul', g, g]] // k ∈ 1..4
386
+ const p = Math.floor(c), r = Math.round(c * 5) - p * 5
387
+ const root = ['call', '$math.fifthroot', ipow(r)]
388
+ const body = p === 0 ? root : ['f64.mul', ipow(p), root]
389
+ return typed(['block', ['result', 'f64'],
390
+ ['local.set', `$${t}`, irA],
391
+ ['if', ['result', 'f64'], ['f64.lt', g, ['f64.const', 0]],
392
+ ['then', ['f64.const', 'nan']], ['else', body]]], 'f64')
393
+ }
394
+ if (Number.isFinite(c) && !Number.isInteger(c) && c !== 0.5 && c !== -0.5)
395
+ return (inc('math.exp'), inc('math.log'),
396
+ typed(['call', '$math.exp', ['f64.mul', irB, ['call', '$math.log', irA]]], 'f64'))
397
+ }
361
398
  // base 2 → dedicated 2^y (exp2 is exact for integer y, and skips exp's ×ln2/÷ln2).
362
399
  // Every other literal base keeps $math.pow: `exp(y·ln base)` would lose ulps and,
363
400
  // worse, miss Math.pow's integer-exponent semantics — e.g. `16 ** flen` with a
@@ -526,10 +563,6 @@ export default (ctx) => {
526
563
  const horner2 = (cs, v = '$r2') => cs.reduceRight((acc, c, i) =>
527
564
  i === cs.length - 1 ? splat(c)
528
565
  : `(f64x2.add ${splat(c)} (f64x2.mul (local.get ${v}) ${acc}))`, '')
529
- // fdlibm log's even/odd split, as f64x2 coefficient arrays (the scalar $math.log inlines them):
530
- // t1 = w·(L3 + w·(L5 + w·L7)), t2 = z·(L2 + w·(L4 + w·(L6 + w·L8))). Vectorized log_v reuses these.
531
- const LOG_T1 = [0.3999999999940941908, 0.2222219843214978396, 0.1531383769920937332]
532
- const LOG_T2 = [0.6666666666666735130, 0.2857142874366239149, 0.1818357216161805012, 0.1479819860511658591]
533
566
  // Shared reduce → r ∈ [−π/2,π/2] in $r, quadrant parity in $q (branchless, 2 passes).
534
567
  const reduce2 = `
535
568
  (local.set $q (f64x2.nearest (f64x2.mul (local.get $x) ${splat(INV_PI)})))
@@ -579,7 +612,7 @@ export default (ctx) => {
579
612
  // 2^52 magic-add, identical to convert_i32_s for |k|≤1075). Any other lane (≤0/∞/NaN/denormal)
580
613
  // routes BOTH lanes to the scalar fallback → bit-exact by construction, edges never lose precision.
581
614
  wat('math.log_v', `(func $math.log_v (param $x v128) (result v128)
582
- (local $k v128) (local $m v128) (local $mask v128) (local $f v128) (local $s v128) (local $z v128) (local $w v128) (local $hfsq v128)
615
+ (local $k v128) (local $m v128) (local $mask v128) (local $s v128) (local $z v128)
583
616
  (if (result v128)
584
617
  (i64x2.all_true (v128.and
585
618
  (f64x2.ge (local.get $x) (f64x2.splat (f64.const 0x1p-1022)))
@@ -593,18 +626,20 @@ export default (ctx) => {
593
626
  (local.set $mask (f64x2.ge (local.get $m) (f64x2.splat (f64.const 1.4142135623730951))))
594
627
  (local.set $m (v128.bitselect (f64x2.mul (local.get $m) (f64x2.splat (f64.const 0.5))) (local.get $m) (local.get $mask)))
595
628
  (local.set $k (f64x2.add (local.get $k) (v128.and (local.get $mask) (f64x2.splat (f64.const 1.0)))))
596
- (local.set $f (f64x2.sub (local.get $m) (f64x2.splat (f64.const 1.0))))
597
- (local.set $s (f64x2.div (local.get $f) (f64x2.add (local.get $f) (f64x2.splat (f64.const 2.0)))))
629
+ ;; mirrors scalar $math.log op-for-op (same constants/order) bit-exact lanes
630
+ (local.set $s (f64x2.div (f64x2.sub (local.get $m) (f64x2.splat (f64.const 1.0))) (f64x2.add (local.get $m) (f64x2.splat (f64.const 1.0)))))
598
631
  (local.set $z (f64x2.mul (local.get $s) (local.get $s)))
599
- (local.set $w (f64x2.mul (local.get $z) (local.get $z)))
600
- (local.set $hfsq (f64x2.mul (f64x2.splat (f64.const 0.5)) (f64x2.mul (local.get $f) (local.get $f))))
601
632
  (f64x2.add
602
633
  (f64x2.mul (local.get $k) (f64x2.splat (f64.const ${Math.LN2})))
603
- (f64x2.add (f64x2.sub (local.get $f) (local.get $hfsq))
604
- (f64x2.mul (local.get $s) (f64x2.add (local.get $hfsq)
605
- (f64x2.add
606
- (f64x2.mul (local.get $w) ${horner2(LOG_T1, '$w')})
607
- (f64x2.mul (local.get $z) ${horner2(LOG_T2, '$w')})))))))
634
+ (f64x2.mul (f64x2.mul (f64x2.splat (f64.const 2.0)) (local.get $s))
635
+ (f64x2.add (f64x2.splat (f64.const 1.0))
636
+ (f64x2.mul (local.get $z)
637
+ (f64x2.add (f64x2.splat (f64.const 0.33333333283005556))
638
+ (f64x2.mul (local.get $z)
639
+ (f64x2.add (f64x2.splat (f64.const 0.20000059590510924))
640
+ (f64x2.mul (local.get $z)
641
+ (f64x2.add (f64x2.splat (f64.const 0.14275490984342690))
642
+ (f64x2.mul (local.get $z) (f64x2.splat (f64.const 0.11663796426848184)))))))))))))
608
643
  (else
609
644
  (f64x2.replace_lane 1
610
645
  (f64x2.splat (call $math.log (f64x2.extract_lane 0 (local.get $x))))
@@ -695,7 +730,6 @@ export default (ctx) => {
695
730
  // Edge cases: NaN→NaN, ≤0 distinguishes 0→-Inf, <0→NaN; +Inf passes through.
696
731
  wat('math.log', `(func $math.log (param $x f64) (result f64)
697
732
  (local $bits i64) (local $k i32) (local $m f64) (local $s f64) (local $z f64)
698
- (local $f f64) (local $w f64) (local $t1 f64) (local $t2 f64) (local $hfsq f64)
699
733
  (if (f64.ne (local.get $x) (local.get $x))
700
734
  (then (return (local.get $x))))
701
735
  (if (f64.le (local.get $x) (f64.const 0.0))
@@ -726,27 +760,22 @@ export default (ctx) => {
726
760
  (then
727
761
  (local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
728
762
  (local.set $k (i32.add (local.get $k) (i32.const 1)))))
729
- ;; s = f/(2+f) with f = m−1 (= (m−1)/(m+1)); then the fdlibm even/odd-split
730
- ;; polynomial. Two parallel Horner chains (t1 over even powers, t2 over odd)
731
- ;; cut the dependency chain ~in half vs one 9-deep Horner more ILP, fewer
732
- ;; terms and reconstruct log(m) = f hfsq + (hfsq + t1 + t2). ~1 ulp.
733
- (local.set $f (f64.sub (local.get $m) (f64.const 1.0)))
734
- (local.set $s (f64.div (local.get $f) (f64.add (local.get $f) (f64.const 2.0))))
763
+ ;; s = (m−1)/(m+1) (|s| 3−2√2 0.172); log(m) = 2s·(1 + z·G(z)), z = s², G a degree-3
764
+ ;; minimax in z (Remez, equioscillation 5.8e-10). One short Horner replaces fdlibm's 7-term
765
+ ;; even/odd split ~40% fewer ops, max rel err 1.7e-11 (jz transcendentals target ~1e-9).
766
+ (local.set $s (f64.div (f64.sub (local.get $m) (f64.const 1.0)) (f64.add (local.get $m) (f64.const 1.0))))
735
767
  (local.set $z (f64.mul (local.get $s) (local.get $s)))
736
- (local.set $w (f64.mul (local.get $z) (local.get $z)))
737
- (local.set $t1 (f64.mul (local.get $w) (f64.add (f64.const 0.3999999999940941908)
738
- (f64.mul (local.get $w) (f64.add (f64.const 0.2222219843214978396)
739
- (f64.mul (local.get $w) (f64.const 0.1531383769920937332)))))))
740
- (local.set $t2 (f64.mul (local.get $z) (f64.add (f64.const 0.6666666666666735130)
741
- (f64.mul (local.get $w) (f64.add (f64.const 0.2857142874366239149)
742
- (f64.mul (local.get $w) (f64.add (f64.const 0.1818357216161805012)
743
- (f64.mul (local.get $w) (f64.const 0.1479819860511658591)))))))))
744
- (local.set $hfsq (f64.mul (f64.const 0.5) (f64.mul (local.get $f) (local.get $f))))
745
768
  (f64.add
746
769
  (f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))
747
- (f64.add (f64.sub (local.get $f) (local.get $hfsq))
748
- (f64.mul (local.get $s) (f64.add (local.get $hfsq)
749
- (f64.add (local.get $t1) (local.get $t2))))))))`)
770
+ (f64.mul (f64.mul (f64.const 2.0) (local.get $s))
771
+ (f64.add (f64.const 1.0)
772
+ (f64.mul (local.get $z)
773
+ (f64.add (f64.const 0.33333333283005556)
774
+ (f64.mul (local.get $z)
775
+ (f64.add (f64.const 0.20000059590510924)
776
+ (f64.mul (local.get $z)
777
+ (f64.add (f64.const 0.14275490984342690)
778
+ (f64.mul (local.get $z) (f64.const 0.11663796426848184)))))))))))))`)
750
779
 
751
780
  wat('math.log2', `(func $math.log2 (param $x f64) (result f64)
752
781
  (f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN2})))`)
@@ -915,82 +944,71 @@ export default (ctx) => {
915
944
  // fdlibm atan: 4-region argument reduction onto |r| ≤ tan(π/16), then an
916
945
  // 11-term odd polynomial split into even/odd parts. Accurate to <1 ulp —
917
946
  // the old Taylor series was ~2e-6 off near |x|=0.5. Drives asin/acos/atan2.
947
+ // Fast atan: sign symmetry (work on |x|), two-stage reduction onto [0, tan(π/8)] — |x|>1 →
948
+ // π/2−atan(1/x), then t>tan(π/8) → π/8+atan((t−C)/(1+Ct)) — then a degree-5 minimax t·P(t²).
949
+ // Replaces the fdlibm 4-way / 11-term / extended-precision form (correctly-rounded but ~3× the
950
+ // ops). Max rel err 6e-10 over all of ℝ — well within jz's ~1e-9 transcendental budget. asin =
951
+ // atan(x/√(1−x²)) and acos = π/2−asin inherit it, so all three drop from ~1.6–2.3× to under V8.
918
952
  wat('math.atan', `(func $math.atan (param $x f64) (result f64)
919
- (local $abs_x f64) (local $id i32) (local $r f64) (local $z f64) (local $w f64)
920
- (local $s1 f64) (local $s2 f64) (local $ahi f64) (local $alo f64) (local $res f64)
921
- ;; NaN passes through unchanged.
953
+ (local $t f64) (local $u f64) (local $r f64) (local $off f64) (local $flip i32)
954
+ ;; NaN passes through; ±0 returns x (preserves sign of zero); ±Inf flows through (1/Inf=0 π/2).
922
955
  (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
923
- (local.set $abs_x (f64.abs (local.get $x)))
924
- ;; |x| >= 2^66: atan saturates to ±π/2.
925
- (if (f64.ge (local.get $abs_x) (f64.const 7.378697629483821e19))
926
- (then (return (f64.copysign (f64.const 1.5707963267948966) (local.get $x)))))
927
- (if (f64.lt (local.get $abs_x) (f64.const 0.4375))
956
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
957
+ (local.set $t (f64.abs (local.get $x)))
958
+ (local.set $off (f64.const 0.0))
959
+ (local.set $flip (i32.const 0))
960
+ (if (f64.gt (local.get $t) (f64.const 1.0))
961
+ (then (local.set $t (f64.div (f64.const 1.0) (local.get $t))) (local.set $flip (i32.const 1))))
962
+ (if (f64.gt (local.get $t) (f64.const 0.41421356237309503))
928
963
  (then
929
- ;; |x| < 2^-27: atan(x) ≈ x (also preserves sign of zero).
930
- (if (f64.lt (local.get $abs_x) (f64.const 7.450580596923828e-9))
931
- (then (return (local.get $x))))
932
- (local.set $id (i32.const -1))
933
- (local.set $r (local.get $x)))
934
- (else
935
- (local.set $r (local.get $abs_x))
936
- (if (f64.lt (local.get $abs_x) (f64.const 1.1875))
937
- (then
938
- (if (f64.lt (local.get $abs_x) (f64.const 0.6875))
939
- (then ;; id=0: r = (2x-1)/(2+x)
940
- (local.set $id (i32.const 0))
941
- (local.set $r (f64.div (f64.sub (f64.mul (f64.const 2.0) (local.get $r)) (f64.const 1.0))
942
- (f64.add (f64.const 2.0) (local.get $r)))))
943
- (else ;; id=1: r = (x-1)/(x+1)
944
- (local.set $id (i32.const 1))
945
- (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.0))
946
- (f64.add (local.get $r) (f64.const 1.0)))))))
947
- (else
948
- (if (f64.lt (local.get $abs_x) (f64.const 2.4375))
949
- (then ;; id=2: r = (x-1.5)/(1+1.5x)
950
- (local.set $id (i32.const 2))
951
- (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.5))
952
- (f64.add (f64.const 1.0) (f64.mul (f64.const 1.5) (local.get $r))))))
953
- (else ;; id=3: r = -1/x
954
- (local.set $id (i32.const 3))
955
- (local.set $r (f64.div (f64.const -1.0) (local.get $r)))))))))
956
- (local.set $z (f64.mul (local.get $r) (local.get $r)))
957
- (local.set $w (f64.mul (local.get $z) (local.get $z)))
958
- (local.set $s1 (f64.mul (local.get $z)
959
- (f64.add (f64.const 0.3333333333333293)
960
- (f64.mul (local.get $w) (f64.add (f64.const 0.14285714272503466)
961
- (f64.mul (local.get $w) (f64.add (f64.const 0.09090887133436507)
962
- (f64.mul (local.get $w) (f64.add (f64.const 0.06661073137387531)
963
- (f64.mul (local.get $w) (f64.add (f64.const 0.049768779946159324)
964
- (f64.mul (local.get $w) (f64.const 0.016285820115365782)))))))))))))
965
- (local.set $s2 (f64.mul (local.get $w)
966
- (f64.add (f64.const -0.19999999999876483)
967
- (f64.mul (local.get $w) (f64.add (f64.const -0.11111110405462356)
968
- (f64.mul (local.get $w) (f64.add (f64.const -0.0769187620504483)
969
- (f64.mul (local.get $w) (f64.add (f64.const -0.058335701337905735)
970
- (f64.mul (local.get $w) (f64.const -0.036531572744216916)))))))))))
971
- ;; |x| < 0.4375: result = r - r*(s1+s2), sign carried by r itself.
972
- (if (i32.lt_s (local.get $id) (i32.const 0))
973
- (then (return (f64.sub (local.get $r) (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2)))))))
974
- ;; Reconstruct: z = atanhi[id] - ((r*(s1+s2) - atanlo[id]) - r), sign of x.
975
- (if (i32.eq (local.get $id) (i32.const 0))
976
- (then (local.set $ahi (f64.const 0.4636476090008061)) (local.set $alo (f64.const 2.2698777452961687e-17)))
977
- (else (if (i32.eq (local.get $id) (i32.const 1))
978
- (then (local.set $ahi (f64.const 0.7853981633974483)) (local.set $alo (f64.const 3.061616997868383e-17)))
979
- (else (if (i32.eq (local.get $id) (i32.const 2))
980
- (then (local.set $ahi (f64.const 0.982793723247329)) (local.set $alo (f64.const 1.3903311031230998e-17)))
981
- (else (local.set $ahi (f64.const 1.5707963267948966)) (local.set $alo (f64.const 6.123233995736766e-17))))))))
982
- (local.set $res (f64.sub (local.get $ahi)
983
- (f64.sub (f64.sub (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2))) (local.get $alo))
984
- (local.get $r))))
985
- (f64.copysign (local.get $res) (local.get $x)))`)
964
+ (local.set $t (f64.div (f64.sub (local.get $t) (f64.const 0.41421356237309503))
965
+ (f64.add (f64.const 1.0) (f64.mul (f64.const 0.41421356237309503) (local.get $t)))))
966
+ (local.set $off (f64.const 0.39269908169872414))))
967
+ (local.set $u (f64.mul (local.get $t) (local.get $t)))
968
+ (local.set $r (f64.add (local.get $off)
969
+ (f64.mul (local.get $t)
970
+ (f64.add (f64.const 0.99999999939667072)
971
+ (f64.mul (local.get $u)
972
+ (f64.add (f64.const -0.33333307625846248)
973
+ (f64.mul (local.get $u)
974
+ (f64.add (f64.const 0.19998216947828790)
975
+ (f64.mul (local.get $u)
976
+ (f64.add (f64.const -0.14240083011830104)
977
+ (f64.mul (local.get $u)
978
+ (f64.add (f64.const 0.10573479828448784)
979
+ (f64.mul (local.get $u) (f64.const -0.060347904072425573))))))))))))))
980
+ (if (local.get $flip) (then (local.set $r (f64.sub (f64.const 1.5707963267948966) (local.get $r)))))
981
+ (f64.copysign (local.get $r) (local.get $x)))`)
986
982
 
983
+ // Fast asin: small-argument poly a + a·u·R(u) (u=a²) with the standard half-angle reduction for
984
+ // |x|>0.5 — a = sqrt((1−|x|)/2) maps the singular end to the smooth domain, asin = π/2 − 2·poly.
985
+ // One sqrt only on the upper half, no atan/div. R is a degree-6 minimax on [0,0.25]; max rel err
986
+ // ~2.6e-10. Replaces asin = atan(x/√(1−x²)) (which paid a div + atan's own reductions).
987
987
  wat('math.asin', `(func $math.asin (param $x f64) (result f64)
988
- ;; Domain is [-1, 1]; outside it (including ±Infinity), Math.asin returns NaN.
989
- ;; sin/cos output is clamped to [-1, 1] by sin_core/cos_core, so no tolerance needed here.
990
- (if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 1.0))
991
- (then (f64.const nan))
992
- (else (call $math.atan (f64.div (local.get $x)
993
- (f64.sqrt (f64.sub (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))))`)
988
+ (local $ax f64) (local $a f64) (local $u f64) (local $r f64)
989
+ ;; |x|>1 NaN (covers ±Inf); NaN propagates (the >1 test is false, poly carries NaN through).
990
+ (if (f64.gt (f64.abs (local.get $x)) (f64.const 1.0)) (then (return (f64.const nan))))
991
+ (local.set $ax (f64.abs (local.get $x)))
992
+ (if (f64.le (local.get $ax) (f64.const 0.5))
993
+ (then (local.set $a (local.get $ax)))
994
+ (else (local.set $a (f64.sqrt (f64.mul (f64.const 0.5) (f64.sub (f64.const 1.0) (local.get $ax)))))))
995
+ (local.set $u (f64.mul (local.get $a) (local.get $a)))
996
+ (local.set $r (f64.add (local.get $a) (f64.mul (f64.mul (local.get $a) (local.get $u))
997
+ (f64.add (f64.const 0.16666666715486264)
998
+ (f64.mul (local.get $u)
999
+ (f64.add (f64.const 0.074999892151409259)
1000
+ (f64.mul (local.get $u)
1001
+ (f64.add (f64.const 0.044648555271317079)
1002
+ (f64.mul (local.get $u)
1003
+ (f64.add (f64.const 0.030259196387355945)
1004
+ (f64.mul (local.get $u)
1005
+ (f64.add (f64.const 0.023661273034955098)
1006
+ (f64.mul (local.get $u)
1007
+ (f64.add (f64.const 0.010472588920432560)
1008
+ (f64.mul (local.get $u) (f64.const 0.031028862087420162))))))))))))))))
1009
+ (if (f64.gt (local.get $ax) (f64.const 0.5))
1010
+ (then (local.set $r (f64.sub (f64.const 1.5707963267948966) (f64.mul (f64.const 2.0) (local.get $r))))))
1011
+ (f64.copysign (local.get $r) (local.get $x)))`)
994
1012
 
995
1013
  wat('math.acos', `(func $math.acos (param $x f64) (result f64)
996
1014
  (f64.sub (f64.const ${HALF_PI}) (call $math.asin (local.get $x))))`)
@@ -1057,19 +1075,51 @@ export default (ctx) => {
1057
1075
  (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
1058
1076
  (f64.mul (f64.const 0.5) (call $math.log (f64.div (f64.add (f64.const 1.0) (local.get $x)) (f64.sub (f64.const 1.0) (local.get $x))))))`)
1059
1077
 
1078
+ // Bit-hack initial guess (divide the IEEE exponent by 3 via an integer divide of the raw bits,
1079
+ // plus a magic bias) then 3 Newton steps t = (2t + a/t²)/3 — quadratic convergence, max rel err
1080
+ // ~1e-12 over the whole f64 range. Replaces the old `pow(x,1/3)` seed: no exp/log call, ~3-4×
1081
+ // faster (the colorconv / Oklab hot path is 3 cbrt per pixel), and a program whose only
1082
+ // transcendental is cbrt no longer pulls the pow/exp/log stdlib. Not bit-identical to V8's fdlibm
1083
+ // cbrt (neither was the pow form) — jz's transcendentals are fast minimax/Newton approximations.
1060
1084
  wat('math.cbrt', `(func $math.cbrt (param $x f64) (result f64)
1061
- (local $y f64)
1062
- ;; ±Infinity and NaN pass through; preserve sign of zero.
1085
+ (local $a f64) (local $t f64) (local $s f64)
1086
+ ;; NaN / ±Infinity / ±0 pass through unchanged (sign of zero preserved).
1063
1087
  (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
1064
1088
  (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
1065
- (if (result f64) (f64.lt (local.get $x) (f64.const 0.0))
1066
- (then (f64.neg (call $math.cbrt (f64.neg (local.get $x)))))
1067
- (else
1068
- ;; Initial guess via pow, then Newton-Raphson: y = (2y + x/y²)/3
1069
- (local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
1070
- (local.set $y (f64.div (f64.add (f64.mul (f64.const 2.0) (local.get $y)) (f64.div (local.get $x) (f64.mul (local.get $y) (local.get $y)))) (f64.const 3.0)))
1071
- (local.set $y (f64.div (f64.add (f64.mul (f64.const 2.0) (local.get $y)) (f64.div (local.get $x) (f64.mul (local.get $y) (local.get $y)))) (f64.const 3.0)))
1072
- (local.get $y))))`)
1089
+ (local.set $a (f64.abs (local.get $x)))
1090
+ (local.set $s (f64.const 1.0))
1091
+ ;; subnormal |x| < 2^-1022: scale up by 2^60 so the exponent split is valid; cbrt(2^60) = 2^20.
1092
+ (if (f64.lt (local.get $a) (f64.const 2.2250738585072014e-308))
1093
+ (then (local.set $a (f64.mul (local.get $a) (f64.const 1152921504606846976.0)))
1094
+ (local.set $s (f64.const 9.5367431640625e-07))))
1095
+ (local.set $t (f64.reinterpret_i64
1096
+ (i64.add (i64.div_u (i64.reinterpret_f64 (local.get $a)) (i64.const 3)) (i64.const 0x2A9F7893BF800000))))
1097
+ (local.set $t (f64.mul (f64.add (f64.add (local.get $t) (local.get $t)) (f64.div (local.get $a) (f64.mul (local.get $t) (local.get $t)))) (f64.const 0.3333333333333333)))
1098
+ (local.set $t (f64.mul (f64.add (f64.add (local.get $t) (local.get $t)) (f64.div (local.get $a) (f64.mul (local.get $t) (local.get $t)))) (f64.const 0.3333333333333333)))
1099
+ (local.set $t (f64.mul (f64.add (f64.add (local.get $t) (local.get $t)) (f64.div (local.get $a) (f64.mul (local.get $t) (local.get $t)))) (f64.const 0.3333333333333333)))
1100
+ (local.set $t (f64.mul (local.get $t) (local.get $s)))
1101
+ (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $t))) (else (local.get $t))))`)
1102
+
1103
+ // Fifth root of v ≥ 0 — same bit-hack seed (÷5 of the raw bits) + 3 Newton steps t=(4t+v/t⁴)/5.
1104
+ // Caller (constant-exponent pow with denominator 5, e.g. the sRGB 2.4 gamma) guarantees v ≥ 0.
1105
+ wat('math.fifthroot', `(func $math.fifthroot (param $v f64) (result f64)
1106
+ (local $t f64) (local $s f64) (local $q f64)
1107
+ (if (i32.eqz (call $math.isFinite (local.get $v))) (then (return (local.get $v))))
1108
+ (if (f64.eq (local.get $v) (f64.const 0.0)) (then (return (f64.const 0.0))))
1109
+ (local.set $s (f64.const 1.0))
1110
+ ;; subnormal: scale by 2^100 = (2^20)^5; fifthroot(2^100) = 2^20
1111
+ (if (f64.lt (local.get $v) (f64.const 2.2250738585072014e-308))
1112
+ (then (local.set $v (f64.mul (local.get $v) (f64.const 1.2676506002282294e30)))
1113
+ (local.set $s (f64.const 9.5367431640625e-07))))
1114
+ (local.set $t (f64.reinterpret_i64
1115
+ (i64.add (i64.div_u (i64.reinterpret_f64 (local.get $v)) (i64.const 5)) (i64.const 0x3325E66666666800))))
1116
+ (local.set $q (f64.mul (local.get $t) (local.get $t)))
1117
+ (local.set $t (f64.mul (f64.add (f64.mul (f64.const 4.0) (local.get $t)) (f64.div (local.get $v) (f64.mul (local.get $q) (local.get $q)))) (f64.const 0.2)))
1118
+ (local.set $q (f64.mul (local.get $t) (local.get $t)))
1119
+ (local.set $t (f64.mul (f64.add (f64.mul (f64.const 4.0) (local.get $t)) (f64.div (local.get $v) (f64.mul (local.get $q) (local.get $q)))) (f64.const 0.2)))
1120
+ (local.set $q (f64.mul (local.get $t) (local.get $t)))
1121
+ (local.set $t (f64.mul (f64.add (f64.mul (f64.const 4.0) (local.get $t)) (f64.div (local.get $v) (f64.mul (local.get $q) (local.get $q)))) (f64.const 0.2)))
1122
+ (f64.mul (local.get $t) (local.get $s)))`)
1073
1123
 
1074
1124
  // Small finite-test helper (NaN→0, ±Inf→0, finite→1). Used by transcendental
1075
1125
  // functions that need to short-circuit on infinite inputs.