jz 0.8.0 → 0.9.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.
Files changed (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
package/module/math.js CHANGED
@@ -14,10 +14,15 @@
14
14
 
15
15
  import { typed, asF64, asI32, toI32, toNumF64, temp, arrayLoop, isLit, litVal, isPureIR } from '../src/ir.js'
16
16
  import { emit, emitter, reg, deps, dual, tag, wat, hostImport } from '../src/bridge.js'
17
- import { inc, declGlobal } from '../src/ctx.js'
18
- import { repOf } from '../src/reps.js'
17
+ import { inc, declGlobal, err } from '../src/ctx.js'
18
+ import { repOf, VAL } from '../src/reps.js'
19
+ import { valTypeOf } from '../src/kind.js'
19
20
 
20
21
  export default (ctx) => {
22
+ // `**`/Math.pow kernel select — see the single authoritative comment block just above
23
+ // `emitPow` (below) for full crPow/approxPow semantics. Read once here; every other site
24
+ // (deps table, pow_core/pow_fold/pow_fold_v dual bodies) just branches on this.
25
+ const crPow = !!ctx.transform.optimize?.crPow
21
26
  // Math.random seeding. DEFAULT: entropy-seeded once from the host on first use (crypto under
22
27
  // host:'js', `random_get` under WASI), so randomness "just works" and isn't silently reproducible.
23
28
  // `randomSeed: <n>` picks a fixed seed for a reproducible sequence; `true` forces entropy explicitly.
@@ -36,8 +41,17 @@ export default (ctx) => {
36
41
  'math.expm1': ['math.exp'],
37
42
  'math.log2': ['math.log'],
38
43
  'math.log1p': ['math.log'],
39
- 'math.pow': ['math.exp', 'math.log'],
40
- 'math.asin': ['math.atan'],
44
+ 'math.pow': ['math.pow_core'],
45
+ 'math.pow_core': crPow ? ['math.pow_transcend'] : ['math.pow_scalbn'],
46
+ 'math.pow_scalbn': [],
47
+ // math.pow_transcend/math.pow_fold only exist (are registered as wat() templates below) when
48
+ // optimize.crPow is set — see the authoritative comment above emitPow. Declaring their deps
49
+ // unconditionally here is harmless when crPow is off: nothing ever inc()s 'math.pow_fold' in
50
+ // that mode (emitPow's const-exponent branch calls $math.exp/$math.log instead), so this edge
51
+ // is simply never traversed.
52
+ 'math.pow_transcend': ['math.pow_scalbn'],
53
+ 'math.pow_fold': ['math.pow_transcend'],
54
+ 'math.asin': [],
41
55
  'math.acos': ['math.asin'],
42
56
  'math.atan2': ['math.atan'],
43
57
  'math.sinh': ['math.exp'],
@@ -46,7 +60,8 @@ export default (ctx) => {
46
60
  'math.asinh': ['math.isFinite', 'math.log'],
47
61
  'math.acosh': ['math.log'],
48
62
  'math.atanh': ['math.log'],
49
- 'math.cbrt': ['math.isFinite', 'math.pow'],
63
+ 'math.cbrt': ['math.isFinite'],
64
+ 'math.fifthroot': ['math.isFinite'],
50
65
  'math.sumPrecise': ['__ptr_offset', '__len', '__alloc'],
51
66
  })
52
67
  // Helpers: all math ops take f64 and return f64. Args go through ToNumber
@@ -247,6 +262,8 @@ export default (ctx) => {
247
262
  ], 'f64')
248
263
  }
249
264
  ctx.core.emit['math.fround'] = a => typed(['f64.promote_f32', ['f32.demote_f64', toNumF64(a, emit(a))]], 'f64')
265
+ // ES2025 Math.f16round — no wasm f16 ops, so round in software (exactly).
266
+ reg('math.f16round', ['math.f16round'], a => fn('math.f16round', a))
250
267
 
251
268
  // Sign
252
269
  reg('math.sign', ['math.sign'], a => fn('math.sign', a))
@@ -349,6 +366,10 @@ export default (ctx) => {
349
366
  const exp2Call = emitter(['math.exp2'], (exp) => typed(['call', '$math.exp2', toNumF64(exp, emit(exp))], 'f64'))
350
367
  // Shared pow/** lowering.
351
368
  const emitPow = (a, b, allowExpPos) => {
369
+ // BigInt ** is real JS (2n ** 3n === 8n) but unimplemented — the f64 pow
370
+ // pipeline would reinterpret raw i64 bits. Reject instead of silent garbage.
371
+ if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
372
+ err('BigInt exponentiation (`**`) not supported — use a multiply loop or Number(x)')
352
373
  const n = constInt(b)
353
374
  if (n !== null && Math.abs(n) <= POW_FOLD_MAX) return foldPow(a, n)
354
375
  if (constNum(b) === 0.5) { const ir = typed(['f64.sqrt', toNumF64(a, emit(a))], 'f64'); return nonNegF64(ir[1]) ? ir : canon(ir) }
@@ -360,21 +381,74 @@ export default (ctx) => {
360
381
  // We emit, check, and if not foldable, the emitted IR is used by the fallthrough paths.
361
382
  const irA = toNumF64(a, emit(a)), irB = toNumF64(b, emit(b))
362
383
  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).
384
+ // Constant non-integer exponent c: inline Math.pow(x,c) as a fast fold instead of the
385
+ // general $math.pow. Skipping the ~15-branch pow special-case ladder (only the x-dependent
386
+ // slice NaN/±Inf/0/negative is needed; every y-branch is statically dead since c is a
387
+ // known finite non-0/1/±0.5/integer literal) + the call frame is still a per-pixel win on
388
+ // the gamma curves (v**0.45, a**(1/2.4)) that dominate tone-mapping, and a program whose
389
+ // only pow is folded this way never pulls the general $math.pow/pow_core. Integers stay on
390
+ // $math.pow (its square-and-multiply path is exact, not transcendental); ±0.5 stays sqrt
391
+ // (also exact, correctly rounded by hardware).
392
+ //
393
+ // KERNEL SELECT — `optimize.crPow` (default OFF) picks how a constant non-integer exponent
394
+ // lowers:
395
+ // OFF (DEFAULT, today's shipped behavior, bit-for-bit): exp(c·log(x)) — that IS $math.pow's
396
+ // own non-integer tail (the final line of $math.pow below), so it is BIT-IDENTICAL to the
397
+ // call for every finite x and for x ∈ {±0, +∞, NaN}: log+exp carry the edges (log(NaN)=NaN,
398
+ // log(0)=−∞, log(<0)=NaN, log(+∞)=+∞; exp(±∞)=∞/0). The ONE divergence is x=−∞: this
399
+ // yields NaN where Math.pow gives ±∞ — the same deliberate boundary trade jz already makes
400
+ // for `(−∞)**0.5` (see the sqrt fold above); −∞ is never a real tone-map/gamma base. The
401
+ // k/5-exponent gammas (sRGB/Rec.709 decode, 2.4/2.2/…) skip log/exp entirely via an
402
+ // UNCONDITIONAL algebraic fifthroot fold (x^(k/5) = x^p·fifthroot(x^r), p=⌊c⌋, r=5c−5p ∈
403
+ // 1..4, ~3.6e-10 rel err, not correctly rounded, measured worst case ~473ulp
404
+ // test/fifthroot-ulp.js) — this was always the plain-build behavior and stays so.
405
+ // ON: the constant exponent instead routes through $math.pow_fold, which shares
406
+ // $math.pow_transcend's two-phase Ziv dd/td kernel with the runtime-y path $math.pow_core
407
+ // uses when crPow is on (see $math.pow_transcend's own comment for the algorithm) —
408
+ // CORRECTLY ROUNDED (0 misrounds on the 5152-vector CORE-MATH-class gate,
409
+ // test/pow-cr.js). c needs no pre-split: the shared kernel's multiply is a twoProd-based
410
+ // exact product (Dekker-splits BOTH operands internally), so the call is just x and the
411
+ // f64.const literal c. HONEST COST: measured ~13x the default exp∘log fold's runtime on
412
+ // the gamma-heavy color benches (colorpq 13.6x behind V8 under crPow, vs ~1x today) —
413
+ // correctness has a real price, so this stays opt-in rather than default
414
+ // (`{ optimize: { crPow: true } }`). Under crPow, the fifthroot fast path is ALSO opt-in
415
+ // rather than automatic (`{ optimize: { approxPow: true } }`, default OFF): correctness
416
+ // wins by default once crPow has opted into the correctly-rounded kernel family — a
417
+ // caller who wants both speed AND crPow's runtime-y correctness sets both flags.
373
418
  if (isLit(irB)) {
374
419
  const c = litVal(irB)
375
- if (Number.isFinite(c) && !Number.isInteger(c) && c !== 0.5 && c !== -0.5)
420
+ // Finite x<0 NaN to match Math.pow on a non-integer exponent (the exp·log form's
421
+ // log(<0)=NaN). x=-Infinity is its OWN case, not "negative": |x|=Infinity means Math.pow
422
+ // ignores the sign for a non-integer exponent (c > 0 in this branch's guard, so the result
423
+ // is +Infinity). x=+0/-0/+∞/NaN carry correctly through power + fifthroot.
424
+ const fifthrootGate = crPow ? ctx.transform.optimize?.approxPow : true
425
+ if (fifthrootGate && Number.isFinite(c) && c > 0 && c < 5 && !Number.isInteger(c) && Number.isInteger(c * 5)) {
426
+ inc('math.fifthroot')
427
+ const t = temp('pw'), g = get(t)
428
+ const ipow = (k) => k === 1 ? g : k === 2 ? ['f64.mul', g, g]
429
+ : k === 3 ? ['f64.mul', ['f64.mul', g, g], g] : ['f64.mul', ['f64.mul', g, g], ['f64.mul', g, g]] // k ∈ 1..4
430
+ const p = Math.floor(c), r = Math.round(c * 5) - p * 5
431
+ const root = ['call', '$math.fifthroot', ipow(r)]
432
+ const body = p === 0 ? root : ['f64.mul', ipow(p), root]
433
+ return typed(['block', ['result', 'f64'],
434
+ ['local.set', `$${t}`, irA],
435
+ ['if', ['result', 'f64'], ['f64.eq', g, ['f64.const', '-inf']],
436
+ ['then', ['f64.const', 'inf']],
437
+ ['else', ['if', ['result', 'f64'], ['f64.lt', g, ['f64.const', 0]],
438
+ ['then', ['f64.const', 'nan']], ['else', body]]]]], 'f64')
439
+ }
440
+ if (crPow) {
441
+ if (Number.isFinite(c) && !Number.isInteger(c) && c !== 0.5 && c !== -0.5) {
442
+ inc('math.pow_fold')
443
+ // c needs no hi/lo pre-split: $math.pow_fold shares $math.pow_transcend's kernel,
444
+ // which exact-multiplies via twoProd (Dekker split done ON BOTH operands inside the
445
+ // kernel) rather than fdlibm's manual y1/y2 chop — so a single f64.const suffices.
446
+ return typed(['call', '$math.pow_fold', irA, ['f64.const', c]], 'f64')
447
+ }
448
+ } else if (Number.isFinite(c) && !Number.isInteger(c) && c !== 0.5 && c !== -0.5) {
376
449
  return (inc('math.exp'), inc('math.log'),
377
450
  typed(['call', '$math.exp', ['f64.mul', irB, ['call', '$math.log', irA]]], 'f64'))
451
+ }
378
452
  }
379
453
  // base 2 → dedicated 2^y (exp2 is exact for integer y, and skips exp's ×ln2/÷ln2).
380
454
  // Every other literal base keeps $math.pow: `exp(y·ln base)` would lose ulps and,
@@ -428,6 +502,30 @@ export default (ctx) => {
428
502
  // WAT stdlib implementations
429
503
  // ============================================
430
504
 
505
+ // Round-to-nearest-f16 without double rounding: add-then-subtract s = 1.5·2^(52+k)
506
+ // makes the f64 adder itself round |x| to a multiple of the f16 quantum 2^k,
507
+ // ties-to-even (sum stays in s's binade, so the subtraction is exact). k comes
508
+ // from |x|'s exponent: eu-10 for f16 normals (eu ≥ -14), -24 in the subnormal
509
+ // range. Overflow boundary: |x| ≥ 65520 (= 65504 + half-ulp) → ±∞, per spec.
510
+ wat('math.f16round', `(func $math.f16round (param $x f64) (result f64)
511
+ (local $abs i64) (local $eu i32) (local $s f64)
512
+ (local.set $abs (i64.and (i64.reinterpret_f64 (local.get $x)) (i64.const 0x7FFFFFFFFFFFFFFF)))
513
+ ;; NaN, ±Infinity, ±0 pass through
514
+ (if (i64.ge_u (local.get $abs) (i64.const 0x7FF0000000000000)) (then (return (local.get $x))))
515
+ (if (i64.eqz (local.get $abs)) (then (return (local.get $x))))
516
+ (if (f64.ge (f64.reinterpret_i64 (local.get $abs)) (f64.const 65520))
517
+ (then (return (f64.copysign (f64.const inf) (local.get $x)))))
518
+ (local.set $eu (i32.sub (i32.wrap_i64 (i64.shr_u (local.get $abs) (i64.const 52))) (i32.const 1023)))
519
+ (local.set $s (f64.reinterpret_i64 (i64.or
520
+ (i64.shl (i64.extend_i32_s (i32.add
521
+ (select (i32.sub (local.get $eu) (i32.const 10)) (i32.const -24)
522
+ (i32.ge_s (local.get $eu) (i32.const -14)))
523
+ (i32.const 1075))) (i64.const 52))
524
+ (i64.const 0x0008000000000000))))
525
+ (f64.copysign
526
+ (f64.sub (f64.add (f64.reinterpret_i64 (local.get $abs)) (local.get $s)) (local.get $s))
527
+ (local.get $x)))`)
528
+
431
529
  wat('math.sign', `(func $math.sign (param $x f64) (result f64)
432
530
  ;; sign(NaN) = NaN, sign(±0) = ±0 — both pass x through unchanged.
433
531
  (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
@@ -449,15 +547,11 @@ export default (ctx) => {
449
547
  // 2^f over the reduced range f ∈ [-0.5, 0.5] for $math.exp2 (rel. err ≤ 6e-9). Lets the
450
548
  // base-2 power `2**y` skip the ×ln2 / ÷ln2 round-trip exp(y·ln2) pays — see $math.exp2.
451
549
  const EXP2_C = [1, 0.6931472000619209, 0.24022650999918949, 0.05550340682450019, 0.009618048870444599, 0.0013395279077191057, 0.00015463102004723134]
452
- // Range-reduction constants embedded as exact round-trip decimal STRINGS, not
453
- // `${Math.PI}`/`${1/Math.PI}` number interpolation. These multiply the (possibly
454
- // astronomically large) argument, so any lost digit wrecks large-arg reduction. Under
455
- // self-host the kernel formats `${number}` through __ftoa — a 9-significant-digit dtoa
456
- // (module/number.js) which would bake "3.14159265"/"0.318309886" into the WAT and
457
- // throw the reduced quadrant off (Δ≈0.2 at x≈2267). A string interpolates verbatim, so
458
- // watr parses the full-precision f64 in both legs. Values are native toString's shortest
459
- // round-trip reprs of Math.PI, 1/Math.PI, Math.PI/2 — byte-identical to the old output.
460
- const PI = '3.141592653589793', INV_PI = '0.3183098861837907', HALF_PI = '1.5707963267948966'
550
+ // Range-reduction constants via plain number interpolation: `${number}` now formats
551
+ // through the Ryū shortest-round-trip __ftoa in BOTH legs (host and self-hosted
552
+ // kernel), so the full-precision f64 bakes into the WAT verbatim — the former
553
+ // string-literal workaround for the kernel's 9-digit dtoa is obsolete.
554
+ const PI = Math.PI, INV_PI = 1 / Math.PI, HALF_PI = Math.PI / 2
461
555
 
462
556
  // Round-to-nearest reduction r = x − q·π ∈ [−π/2, π/2], in pure f64 — no int conversion,
463
557
  // so it never traps and never saturates. A SECOND pass folds the q·π rounding error back
@@ -544,15 +638,6 @@ export default (ctx) => {
544
638
  const horner2 = (cs, v = '$r2') => cs.reduceRight((acc, c, i) =>
545
639
  i === cs.length - 1 ? splat(c)
546
640
  : `(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
641
  // Shared reduce → r ∈ [−π/2,π/2] in $r, quadrant parity in $q (branchless, 2 passes).
557
642
  const reduce2 = `
558
643
  (local.set $q (f64x2.nearest (f64x2.mul (local.get $x) ${splat(INV_PI)})))
@@ -583,6 +668,27 @@ export default (ctx) => {
583
668
  (f64x2.splat (call $math.pow (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
584
669
  (call $math.pow (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.pow'])
585
670
 
671
+ // $math.pow_fold_v — SIMD twin of $math.pow_fold, ONLY registered under optimize.crPow (that
672
+ // fold itself only exists then — see the authoritative comment above emitPow). Per-lane scalar
673
+ // repack — BIT-EXACT by construction, no cheap 2-lane polynomial for the branchy fdlibm-style
674
+ // dd/td kernel — and it keeps a constant-exponent-pow-bearing pixel kernel's surrounding f64x2
675
+ // arithmetic vectorized exactly like pow2/atan2_2/hypot_2/cbrt_v/fifthroot_v already do for
676
+ // their own callees. c arrives as v128 (every PPC_CALL2 arg is lifted through the generic splat
677
+ // path — see src/optimize/vectorize.js), but every lane holds the SAME compile-time constant,
678
+ // so extracting lane 0 for both scalar calls is exact. Off crPow, the vectorizer's own
679
+ // const-exponent lift (vectorize.js) uses $math.exp_v/$math.log_v directly instead — no mirror
680
+ // needed here, matching the default exp(c·log(x)) fold's own shape.
681
+ if (crPow) {
682
+ wat('math.pow_fold_v', `(func $math.pow_fold_v (param $x v128) (param $c v128) (result v128)
683
+ (f64x2.replace_lane 1
684
+ (f64x2.splat (call $math.pow_fold
685
+ (f64x2.extract_lane 0 (local.get $x))
686
+ (f64x2.extract_lane 0 (local.get $c))))
687
+ (call $math.pow_fold
688
+ (f64x2.extract_lane 1 (local.get $x))
689
+ (f64x2.extract_lane 1 (local.get $c)))))`, ['math.pow_fold'])
690
+ }
691
+
586
692
  // atan2/hypot/log have no cheap 2-lane polynomial (multi-`return` fdlibm bodies), so — like pow2 —
587
693
  // each f64x2 mirror computes both lanes with the SCALAR helper and repacks: BIT-EXACT by
588
694
  // construction. The per-pixel-color pass only emits these when a truly-2-wide op (sin2/cos2/sqrt)
@@ -596,13 +702,24 @@ export default (ctx) => {
596
702
  (f64x2.replace_lane 1
597
703
  (f64x2.splat (call $math.hypot (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
598
704
  (call $math.hypot (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.hypot'])
705
+ // cbrt/fifthroot: same per-lane scalar repack (their scalar bodies are branchy exponent-split +
706
+ // Newton, no cheap 2-lane poly). BIT-EXACT by construction. Unlocks the Oklab/OkLCh path (3 cbrt
707
+ // per pixel) and the sRGB/Rec.709 `x**(k/5)` gamma so their surrounding f64x2 arithmetic vectorizes.
708
+ wat('math.cbrt_v', `(func $math.cbrt_v (param $x v128) (result v128)
709
+ (f64x2.replace_lane 1
710
+ (f64x2.splat (call $math.cbrt (f64x2.extract_lane 0 (local.get $x))))
711
+ (call $math.cbrt (f64x2.extract_lane 1 (local.get $x)))))`, ['math.cbrt'])
712
+ wat('math.fifthroot_v', `(func $math.fifthroot_v (param $x v128) (result v128)
713
+ (f64x2.replace_lane 1
714
+ (f64x2.splat (call $math.fifthroot (f64x2.extract_lane 0 (local.get $x))))
715
+ (call $math.fifthroot (f64x2.extract_lane 1 (local.get $x)))))`, ['math.fifthroot'])
599
716
  // True f64x2 log — both lanes through one fdlibm poly (≈2× over two scalar calls). The HOT path
600
717
  // (both lanes a normal finite x>0) mirrors $math.log's normal branch op-for-op: bit-exact (the
601
718
  // sqrt2-center conditional becomes a per-lane bitselect; the i32 exponent k becomes an f64 via the
602
719
  // 2^52 magic-add, identical to convert_i32_s for |k|≤1075). Any other lane (≤0/∞/NaN/denormal)
603
720
  // routes BOTH lanes to the scalar fallback → bit-exact by construction, edges never lose precision.
604
721
  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)
722
+ (local $k v128) (local $m v128) (local $mask v128) (local $s v128) (local $z v128)
606
723
  (if (result v128)
607
724
  (i64x2.all_true (v128.and
608
725
  (f64x2.ge (local.get $x) (f64x2.splat (f64.const 0x1p-1022)))
@@ -616,18 +733,20 @@ export default (ctx) => {
616
733
  (local.set $mask (f64x2.ge (local.get $m) (f64x2.splat (f64.const 1.4142135623730951))))
617
734
  (local.set $m (v128.bitselect (f64x2.mul (local.get $m) (f64x2.splat (f64.const 0.5))) (local.get $m) (local.get $mask)))
618
735
  (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)))))
736
+ ;; mirrors scalar $math.log op-for-op (same constants/order) bit-exact lanes
737
+ (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)))))
621
738
  (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
739
  (f64x2.add
625
740
  (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')})))))))
741
+ (f64x2.mul (f64x2.mul (f64x2.splat (f64.const 2.0)) (local.get $s))
742
+ (f64x2.add (f64x2.splat (f64.const 1.0))
743
+ (f64x2.mul (local.get $z)
744
+ (f64x2.add (f64x2.splat (f64.const 0.33333333283005556))
745
+ (f64x2.mul (local.get $z)
746
+ (f64x2.add (f64x2.splat (f64.const 0.20000059590510924))
747
+ (f64x2.mul (local.get $z)
748
+ (f64x2.add (f64x2.splat (f64.const 0.14275490984342690))
749
+ (f64x2.mul (local.get $z) (f64x2.splat (f64.const 0.11663796426848184)))))))))))))
631
750
  (else
632
751
  (f64x2.replace_lane 1
633
752
  (f64x2.splat (call $math.log (f64x2.extract_lane 0 (local.get $x))))
@@ -718,7 +837,6 @@ export default (ctx) => {
718
837
  // Edge cases: NaN→NaN, ≤0 distinguishes 0→-Inf, <0→NaN; +Inf passes through.
719
838
  wat('math.log', `(func $math.log (param $x f64) (result f64)
720
839
  (local $bits i64) (local $k i32) (local $m f64) (local $s f64) (local $z f64)
721
- (local $f f64) (local $w f64) (local $t1 f64) (local $t2 f64) (local $hfsq f64)
722
840
  (if (f64.ne (local.get $x) (local.get $x))
723
841
  (then (return (local.get $x))))
724
842
  (if (f64.le (local.get $x) (f64.const 0.0))
@@ -749,27 +867,22 @@ export default (ctx) => {
749
867
  (then
750
868
  (local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
751
869
  (local.set $k (i32.add (local.get $k) (i32.const 1)))))
752
- ;; s = f/(2+f) with f = m−1 (= (m−1)/(m+1)); then the fdlibm even/odd-split
753
- ;; polynomial. Two parallel Horner chains (t1 over even powers, t2 over odd)
754
- ;; cut the dependency chain ~in half vs one 9-deep Horner more ILP, fewer
755
- ;; terms and reconstruct log(m) = f hfsq + (hfsq + t1 + t2). ~1 ulp.
756
- (local.set $f (f64.sub (local.get $m) (f64.const 1.0)))
757
- (local.set $s (f64.div (local.get $f) (f64.add (local.get $f) (f64.const 2.0))))
870
+ ;; s = (m−1)/(m+1) (|s| 3−2√2 0.172); log(m) = 2s·(1 + z·G(z)), z = s², G a degree-3
871
+ ;; minimax in z (Remez, equioscillation 5.8e-10). One short Horner replaces fdlibm's 7-term
872
+ ;; even/odd split ~40% fewer ops, max rel err 1.7e-11 (jz transcendentals target ~1e-9).
873
+ (local.set $s (f64.div (f64.sub (local.get $m) (f64.const 1.0)) (f64.add (local.get $m) (f64.const 1.0))))
758
874
  (local.set $z (f64.mul (local.get $s) (local.get $s)))
759
- (local.set $w (f64.mul (local.get $z) (local.get $z)))
760
- (local.set $t1 (f64.mul (local.get $w) (f64.add (f64.const 0.3999999999940941908)
761
- (f64.mul (local.get $w) (f64.add (f64.const 0.2222219843214978396)
762
- (f64.mul (local.get $w) (f64.const 0.1531383769920937332)))))))
763
- (local.set $t2 (f64.mul (local.get $z) (f64.add (f64.const 0.6666666666666735130)
764
- (f64.mul (local.get $w) (f64.add (f64.const 0.2857142874366239149)
765
- (f64.mul (local.get $w) (f64.add (f64.const 0.1818357216161805012)
766
- (f64.mul (local.get $w) (f64.const 0.1479819860511658591)))))))))
767
- (local.set $hfsq (f64.mul (f64.const 0.5) (f64.mul (local.get $f) (local.get $f))))
768
875
  (f64.add
769
876
  (f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))
770
- (f64.add (f64.sub (local.get $f) (local.get $hfsq))
771
- (f64.mul (local.get $s) (f64.add (local.get $hfsq)
772
- (f64.add (local.get $t1) (local.get $t2))))))))`)
877
+ (f64.mul (f64.mul (f64.const 2.0) (local.get $s))
878
+ (f64.add (f64.const 1.0)
879
+ (f64.mul (local.get $z)
880
+ (f64.add (f64.const 0.33333333283005556)
881
+ (f64.mul (local.get $z)
882
+ (f64.add (f64.const 0.20000059590510924)
883
+ (f64.mul (local.get $z)
884
+ (f64.add (f64.const 0.14275490984342690)
885
+ (f64.mul (local.get $z) (f64.const 0.11663796426848184)))))))))))))`)
773
886
 
774
887
  wat('math.log2', `(func $math.log2 (param $x f64) (result f64)
775
888
  (f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN2})))`)
@@ -860,6 +973,487 @@ export default (ctx) => {
860
973
  (f64.mul (call $math.log (local.get $u)) (local.get $x))
861
974
  (f64.sub (local.get $u) (f64.const 1.0))))`)
862
975
 
976
+
977
+ // The entire correctly-rounded kernel below (codegen helpers, breakpoint tables, and the
978
+ // $math.pow_transcend registration itself) is built and registered ONLY when `optimize.crPow`
979
+ // is set — see the authoritative crPow/approxPow comment above `emitPow` for why it's opt-in
980
+ // (honest cost: ~13x the old fold's runtime on gamma-heavy color kernels). Gating the whole
981
+ // section (not just the wat() registration) means a plain build pays zero JS-side cost for
982
+ // table-hex construction / codegen generation, and $math.pow_transcend never enters
983
+ // ctx.core.stdlib at all — so it can't accidentally leak into a default-build's includes set.
984
+ if (crPow) {
985
+ // ============================================
986
+ // Correctly-rounded pow: two-phase Ziv dd/td kernel
987
+ // ============================================
988
+ // $math.pow_transcend(x,y) — x>0 finite, y finite nonzero (the transcendental tail both
989
+ // $math.pow_core (runtime y) and $math.pow_fold (compile-time-constant y) delegate to, once
990
+ // their own special-case ladders rule out NaN/±Inf/±0/x<0/y==0/±1/integer-in-i32-range/y==
991
+ // ±0.5). Ported from a from-scratch double-double/triple-double design (NOT fdlibm's e_pow.c
992
+ // — that algorithm targets ~1ulp, not correct rounding, and the earlier fdlibm-ported
993
+ // $math.pow_core missed 8.2% of the CR vector gate; see test/pow-cr.js), using a Ziv rounding
994
+ // test to promote from cheap double-double (phase 1) to triple-double (phase 2) only when
995
+ // phase 1's own error bound can't certify the final rounding. Design + derivation fully
996
+ // worked out and differentially validated (5152/5152 gate vectors + 26k targeted adversarial
997
+ // + 150k general-random cases, 0 misrounds) in scratchpad/pow/ before this port — see
998
+ // pow_dd.mjs (the reference prototype every WAT line here mirrors 1:1) and its measurement
999
+ // scripts (measure_log2_abs.py, measure_exp2_unscaled.py) for the error-bound derivations
1000
+ // cited below.
1001
+ //
1002
+ // ALGORITHM (both phases share this shape, at k=2 (dd) or k=3 (td) limbs):
1003
+ // 1. log2(x) to k-limb precision: bit-extract x=m·2^kexp (m∈[1,2)), look up the table
1004
+ // breakpoint m0_j nearest m (top-8-mantissa-bit index j, LOG2_TABLE — 256 entries ×
1005
+ // k-limb log2(1+j/256), injected as a linear-memory data table, see injectTable in
1006
+ // src/wat/assemble.js — same mechanism as module/number.js's Eisel-Lemire/Ryū tables),
1007
+ // then log2(x) = kexp + log2(m0_j) + log2(1+r) where r=(m-m0_j)/m0_j (|r|<2^-8, so a
1008
+ // short Horner series converges fast — LOG_SERIES, Mercator ln(1+r) coefficients).
1009
+ // CANCELLATION FIX: when m>=1.5 (j>=128), regroup as (kexp+1)+(log2(m0_j)-1)+log2(1+r)
1010
+ // instead of kexp+log2(m0_j)+log2(1+r) — both are the same value (subtracting the exact
1011
+ // integer 1 from a k-limb value is lossless), but the regrouped form never lets two O(1)
1012
+ // quantities nearly cancel down to a near-zero log2(x) (x close to a power of 2): the
1013
+ // naive form loses up to ~50 bits there since the k-limb fold's error is bounded
1014
+ // relative to the DISCARDED O(1) input magnitude, not the tiny post-cancellation output.
1015
+ // 2. Multiply by y (exact-ish via twoProd, which Dekker-splits BOTH operands — no manual
1016
+ // y1/y2 pre-split needed, unlike fdlibm/the old $math.pow_fold's c1/c2 params).
1017
+ // 3. 2^L via the same shape: round to nearest integer n (f64.nearest — IEEE round-ties-to-
1018
+ // even, spliced back in via $math.pow_scalbn), then a 256-point sub-table (EXP2_TABLE,
1019
+ // 2^(idx/256) for idx∈[-128,127]) plus a short Horner series (EXP_SERIES, e^u
1020
+ // coefficients with ln2 powers folded in) on the doubly-reduced fraction.
1021
+ // 4. ROUNDING TEST: eps = |y|·LOG2_ABS_ERR[k]·ln2 + EXP2_REL_ERR[k], applied as
1022
+ // |result_hi|·eps to the k-limb result. LOG2_ABS_ERR is an ABSOLUTE bound on step 1's
1023
+ // error (measured empirically, ~uniform over x — dd 2^-77.15, td 2^-148.2, before an
1024
+ // 8+ bit margin), scaled by |y| because step 2 turns a fixed absolute log2(x) error
1025
+ // into an absolute error in L=y·log2(x) that GROWS WITH |y| — this is the term a naive
1026
+ // "eps=|result|*E" misses: when x is adversarially close to a power of 2 (log2(x) tiny)
1027
+ // and y is huge, |L| can stay modest even as |y|→huge, so bounding eps off the RESULT's
1028
+ // own magnitude alone silently understates the true uncertainty by a factor of
1029
+ // |y|·log2(x)/L. (Confirmed the hard way: x=1-2^-53, y=1e18 missed by 8 ulps under the
1030
+ // naive formula; 0 misses with this one, across every stress set above.) EXP2_REL_ERR
1031
+ // is step 3's own relative error (dd 2^-72.55, td 2^-156.9, unscaled — the final ·2^n
1032
+ // splice via $math.pow_scalbn is a separate, already-exact staged multiply, musl
1033
+ // scalbn.c, adding none). d(2^L)/dL = 2^L·ln2 converts L's absolute error to the
1034
+ // result's relative error, hence the ln2 factor. PHASE-1 COST: the dd Horner series
1035
+ // (steps 1 and 3) uses a CHEAP HYBRID — the dominant correction term is kept at full dd
1036
+ // precision (one extra mulExt) but the rest of the series runs in plain f64 on the
1037
+ // leading limb, since a fully-plain series measured only ~2^-69 (too loose for
1038
+ // colorpq's own PQ exponents — ~47% phase-2 escalation there) while a fully-rigorous
1039
+ // dd Horner chain (every term compensated) made phase 1 ~28x slower than the fdlibm
1040
+ // kernel it replaced. This hybrid is the measured middle ground — see powLog1pCheapGen's
1041
+ // and the exp2 P-series' own comments below.
1042
+ // 5. If phase 1 (dd) can't certify: recompute at phase 2 (td). Phase 2 is expected to
1043
+ // ALWAYS certify (0 uncertain-after-phase-2 cases across every validation set) — if it
1044
+ // doesn't, this returns its best-effort value rather than nothing (see the mission
1045
+ // note: an uncertain result here would mean the gate found a case beyond what
1046
+ // scratchpad/pow/ discovered, worth its own report, not a silent wrong answer).
1047
+ //
1048
+ // |y| > 1e20 short-circuits BEFORE any of the above: the smallest possible |log2(x)| for
1049
+ // finite x>0,x!=1 is ~1.6e-16 (x adjacent to 1), so |y|>1100/1.6e-16~=6.9e18 already forces
1050
+ // definite overflow/underflow — 1e20 keeps ~15x margin above that while staying far under
1051
+ // ~1.34e300, where twoProd's internal Veltkamp split (SPLITTER·y) would itself overflow to
1052
+ // Infinity and corrupt the multiply. x==1 is handled explicitly there too (pow_fold has no
1053
+ // x==1 pre-check of its own — it relies on log2(1)=0 exactly zeroing the product for ANY y,
1054
+ // which the main kernel already gives it, but the |y|>1e20 short-circuit bypasses the main
1055
+ // kernel entirely so needs its own x==1 case).
1056
+ const POW_LOG2_T = 8, POW_EXP2_T = 8
1057
+ const POW_LOG_N_DD = 9, POW_LOG_N_TD = 18, POW_EXP_N_DD = 8, POW_EXP_N_TD = 15
1058
+ // dd (k=2) bounds measured for the CHEAP-HYBRID phase-1 Horner below (leading correction
1059
+ // term at DD precision via one extra mulExt, tail terms plain-f64): worst dd log2 abs err
1060
+ // 2^-77.15, dd exp2 rel err 2^-72.55 over a 15k+-point sweep incl. subnormals/adversarial
1061
+ // near-power-of-2 x (scratchpad/pow/measure_log2_abs.py, measure_exp2_unscaled.py) — ~9
1062
+ // bits margin below each. An all-plain-tail version (no DD leading-correction term) measured
1063
+ // only 2^-68.97 / 2^-71.27 — too loose for colorpq's own PQ exponents (~47% phase-2
1064
+ // escalation measured there, worse than the expensive full-rigor path it replaced); this
1065
+ // hybrid recovers the needed precision for one extra mulExt (~35 ops) instead of the full
1066
+ // ~(N-1)-deep dd Horner chain (~300+ ops) it replaces. td (k=3) unchanged: phase 2 still
1067
+ // uses the fully-rigorous Horner (powHornerExt).
1068
+ const POW_LOG2_ABS_ERR = { 2: 2 ** -68, 3: 2 ** -138 }
1069
+ const POW_EXP2_REL_ERR = { 2: 2 ** -64, 3: 2 ** -146 }
1070
+ // Mercator ln(1+r) coefficients (r^1..r^18), each a 3-limb (hi,mid,lo) f64 expansion —
1071
+ // uniform 3-limb treatment (not just enough for dd) avoids per-coefficient precision
1072
+ // bookkeeping: a plain-f64 a2..a4 would itself cap the td rounding-test budget at ~2^-114
1073
+ // (worked by hand: a coefficient's contribution to total relative error is
1074
+ // a_i·r^(i-1)·(coefficient's own rel. error), and for i=2..4 with |r|<=2^-8 that leaves only
1075
+ // ~50-70 bits of slack from a plain double) — 3-limb coefficients remove that risk entirely
1076
+ // at zero extra runtime cost (dd just reads the hi limb). Generated by
1077
+ // scratchpad/pow/gen_tables.py (mpmath, 400-bit) — verified against the CR vector gate, not
1078
+ // hand-derived.
1079
+ const POW_LOG_SERIES = [
1080
+ [1, 0, 0],
1081
+ [-0.5, 0, 0],
1082
+ [0.3333333333333333, 1.850371707708594e-17, 1.0271626370065257e-33],
1083
+ [-0.25, 0, 0],
1084
+ [0.2, -1.1102230246251566e-17, 6.162975822039155e-34],
1085
+ [-0.16666666666666666, -9.25185853854297e-18, -5.135813185032629e-34],
1086
+ [0.14285714285714285, 7.93016446160826e-18, 4.4021255871708246e-34],
1087
+ [-0.125, 0, 0],
1088
+ [0.1111111111111111, 6.1679056923619804e-18, 3.423875456688419e-34],
1089
+ [-0.1, 5.551115123125783e-18, -3.0814879110195775e-34],
1090
+ [0.09090909090909091, -2.523234146875356e-18, 7.003381615953585e-35],
1091
+ [-0.08333333333333333, -4.625929269271485e-18, -2.5679065925163143e-34],
1092
+ [0.07692307692307693, -4.270088556250602e-18, 2.370375316168906e-34],
1093
+ [-0.07142857142857142, -3.96508223080413e-18, -2.2010627935854123e-34],
1094
+ [0.06666666666666667, 9.251858538542971e-19, 1.2839532962581572e-35],
1095
+ [-0.0625, 0, 0],
1096
+ [0.058823529411764705, 8.163404592832033e-19, 1.1328999672866093e-35],
1097
+ [-0.05555555555555555, -3.0839528461809902e-18, -1.7119377283442096e-34]]
1098
+ // 2^r2 coefficients (r2^0..r2^15): b_k = ln2^k/k!, so the series is directly in the reduced
1099
+ // fraction r2 (no separate u=r2·ln2 extended multiply needed). Same uniform-3-limb rigor.
1100
+ const POW_EXP_SERIES = [
1101
+ [1, 0, 0],
1102
+ [0.6931471805599453, 2.3190468138462996e-17, 5.707708438416212e-34],
1103
+ [0.24022650695910072, -9.493931253182876e-18, -2.4105486965696903e-34],
1104
+ [0.05550410866482158, -3.1658222903912804e-18, 1.1357423645400287e-34],
1105
+ [0.009618129107628477, 2.8324606784381e-19, 1.85284146980722e-35],
1106
+ [0.0013333558146428443, 1.3928059563172586e-20, -7.148318211080472e-37],
1107
+ [0.0001540353039338161, 1.1783618439907562e-20, 4.5910849836706486e-38],
1108
+ [0.000015252733804059841, -8.027446755055875e-22, -3.3547393057817446e-38],
1109
+ [0.000001321548679014431, -2.0162732323629023e-24, 1.2689094913973184e-40],
1110
+ [1.01780860092397e-7, -1.949520713756723e-24, 9.914912572246126e-41],
1111
+ [7.054911620801123e-9, -2.9110453965609406e-26, 1.2702853147779823e-42],
1112
+ [4.4455382718708116e-10, -1.2731051485060954e-26, 4.420326254448758e-43],
1113
+ [2.5678435993488206e-11, -3.6970912098302563e-28, 1.7132265077294294e-44],
1114
+ [1.3691488853904128e-12, 7.770795328665668e-29, 4.5200006429723875e-45],
1115
+ [6.778726354822545e-14, 5.7164033621144854e-30, 2.4988036368119357e-47],
1116
+ [3.1324367070884287e-15, -3.9318558140598756e-32, -2.0482463830537468e-48],
1117
+ [1.3570247948755148e-16, -1.057117616368963e-32, -1.512313747717571e-49]]
1118
+ const POW_LOG2E = [1.4426950408889634, 2.0355273740931033e-17, -1.0614659956117258e-33] // 1/ln2, 3-limb
1119
+
1120
+ // ---- WAT codegen: EFT (error-free transform) primitives, no FMA (Dekker splits) ----
1121
+ // A Builder accumulates a statement list (nested — if/then/else bodies build with
1122
+ // sub-scopes, `B.sub()`, and splice into the parent as `(then ${sub.stmts.join(' ')})`).
1123
+ //
1124
+ // REGISTER POOL (not one fresh local per intermediate value): `tmp()` used to mint a
1125
+ // brand-new WASM local for every single EFT micro-step — twoSum alone burns 3, twoProd 7,
1126
+ // and a k-limb Horner chains dozens of these per term. For $math.pow_transcend that summed
1127
+ // to ~7000 locals, and both the wasm engine's own compiler and jz's THIS ADD MADE
1128
+ // codegen/optimize passes pay for it: colorpq measured ~15x the old fdlibm kernel's time,
1129
+ // and a pow-using program's OWN compile time went ~0.1s -> ~4.1s. WASM locals are
1130
+ // function-scoped, not block-scoped, so distinct intermediates can share one physical slot
1131
+ // once the earlier one's last use has passed — a classic linear-scan register allocation,
1132
+ // done here as a two-pass token scheme instead of hand-tracking free lists at every call
1133
+ // site (that would be exactly as error-prone as the bug it's fixing):
1134
+ // PASS 1 (this Builder): `tmp()` does NOT pick a real local name. It mints a UNIQUE ID,
1135
+ // emits `(local.set \x01ID\x02 expr)` into the statement stream, and returns
1136
+ // `(local.get \x01ID\x02)` for the caller to embed in later expressions — U+0001/U+0002
1137
+ // control chars so a token can never collide with real WAT text or another token's digits.
1138
+ // Text order here IS execution order (statements append in the order they run; the one
1139
+ // place order gets locally inverted — a `(local.set TARGET expr)` prints TARGET before
1140
+ // expr's own operand reads, though expr evaluates first at runtime — only costs a missed
1141
+ // same-statement reuse opportunity, e.g. `x = a+a` not sharing a's slot with x; it never
1142
+ // causes an early free, because freeing is keyed off each id's PRECOMPUTED true last-use
1143
+ // position, not scan position — see powResolvePool).
1144
+ // PASS 2 (`powResolvePool`, called once on the fully-assembled function body): scan for
1145
+ // every token, resolve each id's real last use, walk the text again allocating a small
1146
+ // per-type register file (separate pools for f64/i32/i64 — a value can only reuse a
1147
+ // same-typed slot), freeing a register the instant its id's last use is seen. Mutable
1148
+ // locals (below) are NOT pooled — they're few, and their whole point is surviving across
1149
+ // sub-scopes, so they keep stable dedicated names exactly as before.
1150
+ const POW_TOK_1 = '\x01', POW_TOK_2 = '\x02'
1151
+ const powMkBuilder = (prefix, shared) => {
1152
+ shared ??= { n: 0, type: {}, mutDecls: [] }
1153
+ const stmts = []
1154
+ const tmp = (expr, type = 'f64') => {
1155
+ const id = shared.n++
1156
+ shared.type[id] = type
1157
+ const tok = `${POW_TOK_1}${id}${POW_TOK_2}`
1158
+ stmts.push(`(local.set ${tok} ${expr})`)
1159
+ return `(local.get ${tok})`
1160
+ }
1161
+ // .set returns the STATEMENT STRING (does not push itself) — a mutable local is
1162
+ // typically declared in one scope but assigned from several (if/then/else sub-scopes),
1163
+ // so the caller must explicitly `.raw()` the result onto whichever scope is active.
1164
+ const mutable = (base, type = 'f64') => {
1165
+ const name = `$${prefix}_${base}${shared.n++}`
1166
+ shared.mutDecls.push(`(local ${name} ${type})`)
1167
+ return { name, get: `(local.get ${name})`, set: (expr) => `(local.set ${name} ${expr})` }
1168
+ }
1169
+ const raw = (s) => stmts.push(s)
1170
+ const sub = (p) => powMkBuilder(p ?? prefix, shared)
1171
+ return { tmp, mutable, raw, sub, stmts, mutDecls: shared.mutDecls, type: shared.type }
1172
+ }
1173
+ // Pass 2 of the register pool (see the Builder comment above): resolve every \x01id\x02
1174
+ // token in `text` to a real, REUSED local name. Returns the extra `(local ...)` decls the
1175
+ // pool needs (concat with the mutable-local decls already collected) and the resolved text.
1176
+ const powResolvePool = (text, typeOf) => {
1177
+ const tokenRe = /local\.(set|get) \x01(\d+)\x02/g
1178
+ const events = []
1179
+ for (let m; (m = tokenRe.exec(text));) events.push({ isSet: m[1] === 'set', id: +m[2], at: m.index })
1180
+ const lastUse = {}
1181
+ for (const e of events) if (!e.isSet) lastUse[e.id] = e.at // last (highest-index) 'get' wins
1182
+ const free = { f64: [], i32: [], i64: [] }, next = { f64: 0, i32: 0, i64: 0 }, regOf = {}
1183
+ for (const e of events) {
1184
+ const type = typeOf[e.id]
1185
+ if (e.isSet) regOf[e.id] = free[type].length ? free[type].pop() : next[type]++
1186
+ else if (e.at === lastUse[e.id]) free[type].push(regOf[e.id])
1187
+ }
1188
+ const resolved = text.replace(/\x01(\d+)\x02/g, (_, idStr) => `$pt_${typeOf[+idStr]}_${regOf[+idStr]}`)
1189
+ const decls = []
1190
+ for (const type of ['f64', 'i32', 'i64']) for (let i = 0; i < next[type]; i++) decls.push(`(local $pt_${type}_${i} ${type})`)
1191
+ return { decls, resolved }
1192
+ }
1193
+ const POW_SPLITTER = '134217729' // 2^27+1, Veltkamp split constant for f64's 53-bit mantissa
1194
+ const powSplit = (B, a) => {
1195
+ const c = B.tmp(`(f64.mul (f64.const ${POW_SPLITTER}) ${a})`)
1196
+ const hi = B.tmp(`(f64.sub ${c} (f64.sub ${c} ${a}))`)
1197
+ const lo = B.tmp(`(f64.sub ${a} ${hi})`)
1198
+ return [hi, lo]
1199
+ }
1200
+ const powTwoSum = (B, a, b) => {
1201
+ const s = B.tmp(`(f64.add ${a} ${b})`)
1202
+ const bb = B.tmp(`(f64.sub ${s} ${a})`)
1203
+ const e = B.tmp(`(f64.add (f64.sub ${a} (f64.sub ${s} ${bb})) (f64.sub ${b} ${bb}))`)
1204
+ return [s, e]
1205
+ }
1206
+ const powTwoProd = (B, a, b) => {
1207
+ const p = B.tmp(`(f64.mul ${a} ${b})`)
1208
+ const [ah, al] = powSplit(B, a), [bh, bl] = powSplit(B, b)
1209
+ const t1 = B.tmp(`(f64.sub (f64.mul ${ah} ${bh}) ${p})`)
1210
+ const t2 = B.tmp(`(f64.add ${t1} (f64.mul ${ah} ${bl}))`)
1211
+ const t3 = B.tmp(`(f64.add ${t2} (f64.mul ${al} ${bh}))`)
1212
+ const e = B.tmp(`(f64.add ${t3} (f64.mul ${al} ${bl}))`)
1213
+ return [p, e]
1214
+ }
1215
+ // absorb: ripple `term` top-down into a k-limb accumulator (array of k expr-refs) via
1216
+ // twoSum; the final carry-out (dropped) is ~2^-53k relative to the LARGEST term folded so
1217
+ // far, so a chain of these absorptions gives a k-limb-equivalent (~53k-bit) result.
1218
+ const powAbsorb = (B, acc, term) => {
1219
+ const next = []
1220
+ let carry = term
1221
+ for (let j = 0; j < acc.length; j++) { const [s, e] = powTwoSum(B, acc[j], carry); next.push(s); carry = e }
1222
+ return next
1223
+ }
1224
+ const powFoldK = (B, terms, k) => { let acc = new Array(k).fill('(f64.const 0)'); for (const t of terms) acc = powAbsorb(B, acc, t); return acc }
1225
+ const powMulExtDouble = (B, A, y, k) => {
1226
+ const terms = []
1227
+ for (let i = 0; i < k; i++) {
1228
+ if (i === k - 1) terms.push(`(f64.mul ${A[i]} ${y})`)
1229
+ else { const [p, e] = powTwoProd(B, A[i], y); terms.push(p, e) }
1230
+ }
1231
+ return powFoldK(B, terms, k)
1232
+ }
1233
+ // k-limb * k-limb, triangular (drop cross terms below k-limb precision — the standard
1234
+ // QD-library dd_mul generalizes cleanly to k limbs this way).
1235
+ const powMulExt = (B, A, Bv, k) => {
1236
+ const terms = []
1237
+ for (let i = 0; i < k; i++) for (let j = 0; j < k; j++) {
1238
+ if (i + j >= k) continue
1239
+ if (i + j === k - 1) terms.push(`(f64.mul ${A[i]} ${Bv[j]})`)
1240
+ else { const [p, e] = powTwoProd(B, A[i], Bv[j]); terms.push(p, e) }
1241
+ }
1242
+ return powFoldK(B, terms, k)
1243
+ }
1244
+ const powAddExt = (B, A, Bv, k) => powFoldK(B, [...A, ...Bv], k)
1245
+ // u/v (plain doubles, u,v exact by construction at every call site — Sterbenz subtraction
1246
+ // against a bit-truncated table breakpoint) to k-limb precision via iterative refinement:
1247
+ // each pass forms the EXACT residual u-s·v (twoProd+twoSum) and divides it again, recovering
1248
+ // ~53 more bits per pass.
1249
+ const powDivExt = (B, u, v, k) => {
1250
+ const terms = []
1251
+ let rHi = u, rLo = '(f64.const 0)'
1252
+ for (let pass = 0; pass < k; pass++) {
1253
+ const s = B.tmp(`(f64.div ${rHi} ${v})`)
1254
+ const [p, e] = powTwoProd(B, s, v)
1255
+ const [t1, t1e] = powTwoSum(B, rHi, `(f64.neg ${p})`)
1256
+ const [t2, t2e] = powTwoSum(B, rLo, `(f64.neg ${e})`)
1257
+ rHi = B.tmp(`(f64.add ${t1} ${t2})`); rLo = B.tmp(`(f64.add ${t1e} ${t2e})`)
1258
+ terms.push(s)
1259
+ }
1260
+ return powFoldK(B, terms, k)
1261
+ }
1262
+ // Horner (highest degree first) over a k-limb variable, coefficients as 3-limb JS rows
1263
+ // (only the first k limbs of each are used).
1264
+ const powHornerExt = (B, coefRows, k, xLimbs) => {
1265
+ const N = coefRows.length
1266
+ let acc = coefRows[N - 1].slice(0, k).map(v => `(f64.const ${v})`)
1267
+ for (let i = N - 2; i >= 0; i--) {
1268
+ acc = powMulExt(B, acc, xLimbs, k)
1269
+ acc = powAddExt(B, acc, coefRows[i].slice(0, k).map(v => `(f64.const ${v})`), k)
1270
+ }
1271
+ return acc
1272
+ }
1273
+
1274
+ // frexp: x>0 finite, in mutable local xLoc (rescaled in-place for subnormals). Returns
1275
+ // {kexp (mutable i32), m (f64 expr, in [1,2)), mHi (i32 expr, high word of m's bit pattern)}.
1276
+ const powFrexpGen = (B, xLoc) => {
1277
+ const bits0 = B.tmp(`(i64.reinterpret_f64 ${xLoc.get})`, 'i64')
1278
+ const hi0 = B.tmp(`(i32.wrap_i64 (i64.shr_u ${bits0} (i64.const 32)))`, 'i32')
1279
+ const kexp = B.mutable('kexp', 'i32')
1280
+ B.raw(kexp.set('(i32.const 0)'))
1281
+ const subB = B.sub('frs')
1282
+ subB.raw(xLoc.set(`(f64.mul ${xLoc.get} (f64.const ${2 ** 54}))`))
1283
+ subB.raw(kexp.set('(i32.const -54)'))
1284
+ B.raw(`(if (i32.eqz (i32.shr_u ${hi0} (i32.const 20))) (then ${subB.stmts.join(' ')}))`)
1285
+ const bits = B.tmp(`(i64.reinterpret_f64 ${xLoc.get})`, 'i64')
1286
+ const hi = B.tmp(`(i32.wrap_i64 (i64.shr_u ${bits} (i64.const 32)))`, 'i32')
1287
+ const lo = B.tmp(`(i32.wrap_i64 ${bits})`, 'i32')
1288
+ B.raw(kexp.set(`(i32.add ${kexp.get} (i32.sub (i32.shr_u ${hi} (i32.const 20)) (i32.const 1023)))`))
1289
+ const mHi = B.tmp(`(i32.or (i32.and ${hi} (i32.const 0x800fffff)) (i32.const 0x3ff00000))`, 'i32')
1290
+ const m = B.tmp(`(f64.reinterpret_i64 (i64.or (i64.shl (i64.extend_i32_u ${mHi}) (i64.const 32)) (i64.extend_i32_u ${lo})))`)
1291
+ return { kexp, m, mHi }
1292
+ }
1293
+
1294
+ // CHEAP-HYBRID phase-1 (dd only) series evaluation: ln(1+r) = r + a2 r^2 + a3 r^3 + ... .
1295
+ // Mercator's series has ALL integer powers of r (unlike atanh's odd-power-only series, the
1296
+ // shape exp2's series shares — see below), so a plain-Horner tail's reduction variable is r
1297
+ // itself, NOT r^2 (an earlier version mistakenly reused the odd-series r^2 pattern here;
1298
+ // confirmed wrong against the mpmath oracle — it silently dropped odd-power siblings of the
1299
+ // r^2 term, landing ~2^-16 absolute error instead of the intended ~2^-69). a2 r^2 is kept at
1300
+ // DD precision (one mulExt for r^2 + one mulExtDouble by the constant) since it's the
1301
+ // dominant correction: a fully-plain series (a2 onward all plain f64, ~2 ops/term) measured
1302
+ // only ~2^-69 dd precision — too loose for colorpq's own PQ exponents (~47% phase-2
1303
+ // escalation measured, worse than the fully-rigorous dd Horner it was meant to replace,
1304
+ // which itself made phase 1 ~28x slower than the fdlibm kernel it replaced). This hybrid —
1305
+ // one extra dd multiply for a2 r^2, plain Horner for a3 r^3 onward (truly O(r^3), tiny) —
1306
+ // measured 2^-77.15 dd absolute error (scratchpad/pow/measure_log2_abs.py), recovering the
1307
+ // needed precision for a fraction of full rigor's ~(logNTerms-1)-deep dd Horner chain cost.
1308
+ const powLog1pCheapGen = (B, r, logNTerms) => {
1309
+ const r0 = r[0]
1310
+ const rSq = powMulExt(B, r, r, 2)
1311
+ const a2Term = powMulExtDouble(B, rSq, `(f64.const ${POW_LOG_SERIES[1][0]})`, 2)
1312
+ let Qtail = `(f64.const ${POW_LOG_SERIES[logNTerms - 1][0]})`
1313
+ for (let i = logNTerms - 2; i >= 2; i--) Qtail = B.tmp(`(f64.add (f64.mul ${Qtail} ${r0}) (f64.const ${POW_LOG_SERIES[i][0]}))`)
1314
+ const tail = B.tmp(`(f64.mul (f64.mul (f64.mul ${r0} ${r0}) ${r0}) ${Qtail})`)
1315
+ return powFoldK(B, [...r, ...a2Term, tail], 2)
1316
+ }
1317
+
1318
+ // log2(x) to k-limb precision — see the header comment for the algorithm and the
1319
+ // cancellation-fix rationale. tblBase: WAT expr for LOG2_TABLE's injected base address.
1320
+ const powLog2ExtGen = (B, xLoc, k, tblBase, logNTerms) => {
1321
+ const { kexp, m, mHi } = powFrexpGen(B, xLoc)
1322
+ const T = POW_LOG2_T
1323
+ const j = B.tmp(`(i32.and (i32.shr_u ${mHi} (i32.const ${20 - T})) (i32.const ${(1 << T) - 1}))`, 'i32')
1324
+ const maskHi = (0xfff00000 | (((1 << T) - 1) << (20 - T))) >>> 0
1325
+ const m0Hi = B.tmp(`(i32.and ${mHi} (i32.const ${maskHi | 0}))`, 'i32')
1326
+ const m0 = B.tmp(`(f64.reinterpret_i64 (i64.shl (i64.extend_i32_u ${m0Hi}) (i64.const 32)))`)
1327
+ const u = B.tmp(`(f64.sub ${m} ${m0})`)
1328
+ const r = powDivExt(B, u, m0, k)
1329
+ const lnP = k === 2 ? powLog1pCheapGen(B, r, logNTerms) : powMulExt(B, powHornerExt(B, POW_LOG_SERIES.slice(0, logNTerms), k, r), r, k)
1330
+ const log2P = powMulExt(B, lnP, POW_LOG2E.slice(0, k).map(v => `(f64.const ${v})`), k)
1331
+ const addr = B.tmp(`(i32.add ${tblBase} (i32.mul ${j} (i32.const 24)))`, 'i32')
1332
+ const tHi = B.tmp(`(f64.load offset=0 ${addr})`)
1333
+ const tMid = k >= 2 ? B.tmp(`(f64.load offset=8 ${addr})`) : null
1334
+ const tLo = k >= 3 ? B.tmp(`(f64.load offset=16 ${addr})`) : null
1335
+ const tableEntryRaw = [tHi, tMid, tLo].slice(0, k)
1336
+ const kexpAdj = B.mutable('kexpadj', 'i32')
1337
+ const teAdj = tableEntryRaw.map((_, i) => B.mutable('te' + i))
1338
+ const elseB = B.sub('lelse')
1339
+ elseB.raw(kexpAdj.set(kexp.get))
1340
+ teAdj.forEach((h, i) => elseB.raw(h.set(tableEntryRaw[i])))
1341
+ const thenB = B.sub('lthen')
1342
+ thenB.raw(kexpAdj.set(`(i32.add ${kexp.get} (i32.const 1))`))
1343
+ const shifted = powFoldK(thenB, [...tableEntryRaw, '(f64.const -1)'], k)
1344
+ shifted.forEach((h, i) => thenB.raw(teAdj[i].set(h)))
1345
+ B.raw(`(if (i32.ge_s ${j} (i32.const ${(1 << T) / 2})) (then ${thenB.stmts.join(' ')}) (else ${elseB.stmts.join(' ')}))`)
1346
+ const kexpF = B.tmp(`(f64.convert_i32_s ${kexpAdj.get})`)
1347
+ return powFoldK(B, [kexpF, ...teAdj.map(h => h.get), ...log2P], k)
1348
+ }
1349
+
1350
+ // 2^L to k-limb precision — Llimbs is a k-limb array. Returns {limbs: k-limb array of the
1351
+ // UNSCALED fractional-part result, n: i32 expr, the exponent $math.pow_scalbn splices in}.
1352
+ const powExp2ExtGen = (B, Llimbs, k, tblBase, expNTerms) => {
1353
+ const n0 = B.tmp(`(f64.nearest ${Llimbs[0]})`) // ties-to-even, IEEE roundTiesToEven
1354
+ const n = B.tmp(`(i32.trunc_f64_s ${n0})`, 'i32')
1355
+ const nF = B.tmp(`(f64.convert_i32_s ${n})`)
1356
+ const negNF = B.tmp(`(f64.neg ${nF})`)
1357
+ const rExp = powFoldK(B, [...Llimbs, negNF], k)
1358
+ const idxF0 = B.tmp(`(f64.nearest (f64.mul ${rExp[0]} (f64.const 256)))`)
1359
+ const idxI0 = B.tmp(`(i32.trunc_f64_s ${idxF0})`, 'i32')
1360
+ const idxLo = B.tmp(`(select (i32.const -128) ${idxI0} (i32.lt_s ${idxI0} (i32.const -128)))`, 'i32')
1361
+ const idx = B.tmp(`(select (i32.const 127) ${idxLo} (i32.gt_s ${idxLo} (i32.const 127)))`, 'i32')
1362
+ const idxF = B.tmp(`(f64.convert_i32_s ${idx})`)
1363
+ const negIdxOver256 = B.tmp(`(f64.neg (f64.div ${idxF} (f64.const 256)))`)
1364
+ const r2 = powFoldK(B, [...rExp, negIdxOver256], k)
1365
+ const addr = B.tmp(`(i32.add ${tblBase} (i32.mul (i32.add ${idx} (i32.const 128)) (i32.const 24)))`, 'i32')
1366
+ const eHi = B.tmp(`(f64.load offset=0 ${addr})`)
1367
+ const eMid = k >= 2 ? B.tmp(`(f64.load offset=8 ${addr})`) : null
1368
+ const eLo = k >= 3 ? B.tmp(`(f64.load offset=16 ${addr})`) : null
1369
+ const tableEntry = [eHi, eMid, eLo].slice(0, k)
1370
+ // CHEAP phase-1 (dd only): 2^r2 = 1 + b1*r2 + b2*r2^2+... . Unlike log's series (odd
1371
+ // powers of r only, naturally a series in r^2), exp2's has BOTH parities of r2, so a
1372
+ // plain tail Horner runs IN r2 (not r2^2). b1*r2 and b2*r2^2 are kept at DD precision
1373
+ // (b1*r2: one mulExt; b2*r2^2: one mulExt for r2^2 + one mulExtDouble by b2) — b1 is the
1374
+ // dominant correction (ln2, not O(r2) small) and b2's term needed the same DD treatment
1375
+ // log2's a2 did (see powLog1pCheapGen's comment: a plain-double b2 alone measured only
1376
+ // ~2^-71 dd precision, too loose for colorpq's own PQ exponents). b3 onward (O(r2^3),
1377
+ // truly small) stay a cheap plain Horner on r2's leading limb. Phase 2 (td) keeps the
1378
+ // fully-rigorous Horner.
1379
+ const P = k === 2 ? (() => {
1380
+ const r2_0 = r2[0]
1381
+ const term1 = powMulExt(B, r2, POW_EXP_SERIES[1].slice(0, 2).map(v => `(f64.const ${v})`), 2)
1382
+ const r2Sq = powMulExt(B, r2, r2, 2)
1383
+ const term2 = powMulExtDouble(B, r2Sq, `(f64.const ${POW_EXP_SERIES[2][0]})`, 2)
1384
+ let Qtail = `(f64.const ${POW_EXP_SERIES[expNTerms - 1][0]})`
1385
+ for (let i = expNTerms - 2; i >= 3; i--) Qtail = B.tmp(`(f64.add (f64.mul ${Qtail} ${r2_0}) (f64.const ${POW_EXP_SERIES[i][0]}))`)
1386
+ const tail = B.tmp(`(f64.mul (f64.mul (f64.mul ${r2_0} ${r2_0}) ${r2_0}) ${Qtail})`)
1387
+ return powFoldK(B, ['(f64.const 1)', ...term1, ...term2, tail], 2)
1388
+ })() : powHornerExt(B, POW_EXP_SERIES.slice(0, expNTerms), k, r2)
1389
+ const result = powMulExt(B, tableEntry, P, k)
1390
+ return { limbs: result, n }
1391
+ }
1392
+
1393
+ // Assemble $math.pow_transcend's full body — see the header comment for the algorithm.
1394
+ const genPowTranscend = () => {
1395
+ const B = powMkBuilder('pt')
1396
+ const xLoc = { get: '(local.get $x)', set: (e) => `(local.set $x ${e})` }
1397
+ const yLoc = { get: '(local.get $y)' }
1398
+ const logTbl = '(global.get $math.pow_log2_tbl)', expTbl = '(global.get $math.pow_exp2_tbl)'
1399
+ B.raw(`(if (f64.gt (f64.abs ${yLoc.get}) (f64.const 1e20))
1400
+ (then
1401
+ (if (f64.eq ${xLoc.get} (f64.const 1.0)) (then (return (f64.const 1.0))))
1402
+ (if (i32.eq (f64.gt ${xLoc.get} (f64.const 1.0)) (f64.gt ${yLoc.get} (f64.const 0.0)))
1403
+ (then (return (f64.const inf)))
1404
+ (else (return (f64.const 0.0))))))`)
1405
+ const epsExpr = (k) => `(f64.add (f64.mul (f64.abs ${yLoc.get}) (f64.const ${POW_LOG2_ABS_ERR[k] * Math.LN2})) (f64.const ${POW_EXP2_REL_ERR[k]}))`
1406
+ const emitPhase = (k, logN, expN, isLast) => {
1407
+ const Bp = B.sub(`p${k}`)
1408
+ const logx = powLog2ExtGen(Bp, xLoc, k, logTbl, logN)
1409
+ const L = powMulExtDouble(Bp, logx, yLoc.get, k)
1410
+ Bp.raw(`(if (f64.gt ${L[0]} (f64.const 1100)) (then (return (f64.const inf))))`)
1411
+ Bp.raw(`(if (f64.lt ${L[0]} (f64.const -1100)) (then (return (f64.const 0.0))))`)
1412
+ const { limbs, n } = powExp2ExtGen(Bp, L, k, expTbl, expN)
1413
+ const hi = limbs[0]
1414
+ const loSum = limbs.length === 2 ? limbs[1] : Bp.tmp(`(f64.add ${limbs[1]} ${limbs[2]})`)
1415
+ const eps = Bp.tmp(`(f64.mul (f64.abs ${hi}) ${epsExpr(k)})`)
1416
+ const lowerU = Bp.tmp(`(f64.add ${hi} (f64.sub ${loSum} ${eps}))`)
1417
+ const upperU = Bp.tmp(`(f64.add ${hi} (f64.add ${loSum} ${eps}))`)
1418
+ const lower = Bp.tmp(`(call $math.pow_scalbn ${lowerU} ${n})`)
1419
+ const upper = Bp.tmp(`(call $math.pow_scalbn ${upperU} ${n})`)
1420
+ if (isLast) {
1421
+ // Phase 2: return best-effort if STILL uncertain rather than nothing — validated 0
1422
+ // occurrences (see header comment), so this is a documented safety net, not a live path.
1423
+ Bp.raw(`(if (f64.eq ${lower} ${upper}) (then (return ${lower})))`)
1424
+ Bp.raw(`(return (call $math.pow_scalbn (f64.add ${hi} ${loSum}) ${n}))`)
1425
+ } else {
1426
+ Bp.raw(`(if (f64.eq ${lower} ${upper}) (then (return ${lower})))`)
1427
+ }
1428
+ B.raw(Bp.stmts.join(' '))
1429
+ }
1430
+ emitPhase(2, POW_LOG_N_DD, POW_EXP_N_DD, false) // phase 1 (dd) — cheap common path
1431
+ emitPhase(3, POW_LOG_N_TD, POW_EXP_N_TD, true) // phase 2 (td) — rare, always returns
1432
+ // Pool resolution runs ONCE over the whole (both-phases) body — phase 2's pool reuses
1433
+ // phase 1's already-declared registers for free (phase 1 has unconditionally returned or
1434
+ // finished by the time phase 2's code runs, so none of its values are still live).
1435
+ const { decls: poolDecls, resolved } = powResolvePool(B.stmts.join(' '), B.type)
1436
+ return `(func $math.pow_transcend (param $x f64) (param $y f64) (result f64)
1437
+ ${B.mutDecls.join(' ')} ${poolDecls.join(' ')}
1438
+ ${resolved})`
1439
+ }
1440
+
1441
+ // LOG2_TABLE / EXP2_TABLE: 256 entries x 24 bytes (3 little-endian f64 limbs each) —
1442
+ // log2(1+j/256) for j=0..255, and 2^(j/256) for j=-128..127 respectively, computed at
1443
+ // 400-bit precision (scratchpad/pow/gen_table_bytes.py) and decomposed into a 3-limb
1444
+ // (hi,mid,lo) expansion. Injected as linear-memory data tables only when
1445
+ // $math.pow_transcend survives reachability pruning — same lazy-table mechanism as
1446
+ // module/number.js's Eisel-Lemire/Ryū tables (src/wat/assemble.js's injectTable).
1447
+ // Char-array + one join, not `s += chr` — the concat form allocates ~n²/2 bytes
1448
+ // of dead strings PER COMPILE (see module/number.js hexToBytes; these two 6 KB
1449
+ // tables alone cost ~38 MB per compile inside the warm self-host kernel).
1450
+ const powHexToBytes = (hex) => { const chars = []; for (let i = 0; i < hex.length; i += 2) chars.push(String.fromCharCode(parseInt(hex.slice(i, i + 2), 16))); return chars.join('') }
1451
+ ctx.runtime.powLog2Table = powHexToBytes('00000000000000000000000000000000000000000000000077ac7a6dc409773f11be24496fb6123c3b347080dd8794b85108efb650fe863f545e00ec8de32f3c1b6e1364c677b7b80c7ba9173136913f5244249b06ed323c2a1c23cc79b1b1382ad2c28596e7963fbd9f7b0776643dbc240d89569f6bd0b8860f85ba63939c3f4fd3c53da09b393c4625f31f589fceb8133413d5d11ca13fb96c05fbba7e22bc4c51f7bfdabcc438265a689430eda33f94c41fb8a513303c793aa68b0ca5de3887fd8e75d3baa63fe15b4b78039b383c47694326e7dbdeb8945149c3bf85a93f84de43ed3828483cf3f7d42fc9c7d5385fab0ab9fa4dac3ff088332c0f0e16bcabfd82b1d22bb438a05332838913af3f941a1251be8c47bcbd52d1caebcbde389b9fa29f38ebb03fb89951d1220c53bcc8f350916b06f7383d5a137e5b4bb23f6982ba324c975dbc5a236e632449ffb8c3f127dd2faab33fcf5af27697fd43bcf1f84223b5a9dd38b73b0336b807b53f962e4b42968645bcc3a726e5b1a7d0381613c9faf663b63f165198e014335f3caa60f0b586a7e4b881a2b896eebeb73ffaa11a19d7204e3cc6f7e155cfe6e4385b33466ea118b93f1793ac6d736354bc7864eca6dc37d7b8948434df1171ba3f2c3dc4e1a7bb34bcc015918a7db6d3b8baabad4042c8bb3f5f46b969b1ec583ceff73abe6978f4b8da825be3341ebd3ff9f2c83cce42533c5c0a5ab00caff838b2a57f11ec72be3ff229dcd9b5bd3c3c269c2dee50a99338a5000b0f6ac6bf3fc8bd1ae7b2ec5bbc650e64f59a00f9b8e479da8c588cc03fabd60be410766abcf915400c2b5407b92e0689b4e134c13fa5990dfe7d4bf9bb7d2ad8d3216153b87b2b5597d1dcc13f580e1e0c15a9673cb48ad47da875053940a6074b2984c23f75f7d6545da26cbcfa5e49a32e63d9381aae78e2e92ac33f4ca775c0f2514f3cce4e3bb09a6ce6b8648a9a6d14d1c33fd6b8a9e091e93fbc75ebf8dbb645deb84df783f9a976c43fb61fb2a0749c583ce9c4c7b3adeec3388a5c7a90ab1bc53ffffd1626022c623ca7ad823d61350ab988d6fb391ac0c53f5d7e7d3bfd1768bc175706c3b68be4b81613c9faf663c63f165198e014336f3caa60f0b586a7f4b87f02efd44207c73f3ebf6b7d0ee0543c8fb96547d9baf2b8df5dd0c7fea9c73f73e823be64033dbce83e96073d3dd2b8b3032fd02b4cc83fb5e2c488db6e11bc056a892605e1b1b86c2b35e8caedc83f93d99a79af8c62bcc54f06e9bdd5fb38df707e07dd8ec93f5cb9486574d6173cacc5ecec97067ab86bb82023632fca3ff0426d6cbde35abce5070ddc7ac5ed3894ecb42d5ecfca3fcae73f34e11e40bc0a755f41905ada38e9955f17cf6ecb3f1fed8b3363fb6a3cd85d40b152cdbfb8ee4dd9cdb60dcc3ffc7326669b38603c4b467ddb2794f7b8c90d773c16accc3f72b8d369cd4e6abc806de7e032de02397059324cee49cd3ffde27e4da5855bbcaa9696fa807cf7b80f48b1e33fe7cd3fbee1328cbd414ebcbcc95491b201bdb84d6a4ee70b84ce3f1e087115061b5cbc60b030556103efb8278f20395320cf3f3b63cc994add613c102efc634c070bb90a6802b916bccf3f78c253cc6cd4513ca5245261b641f9b864064da2ab2bd03f080bd6a4173a71bc2da3b9354c8316390930b0db0a79d03f1755679ac6c07b3ce15b7bbc9e99f4b88f2a547529c6d03f80f4f91638c176bcd26695fc04a30e39760bd3da0713d13fd952c3d2477b6abcfa87cdb933f80339ff08bb76a65fd13f19074f118e5f763c18de3de5e230e03870f091b205acd13f28e3b7e11ca3743c2d27cdb11d061a39138ed8f625f8d13feae6fb6934f4653c90705860f1fc0db93a070eab0744d23fffec16ad916e7fbc9d5879e10b11f0b88326b335ab8fd23fe68b905ae8c0783cee68dcd263041fb9af9a4dfc10dbd23fcf1b638fdec47b3c8c0b388be38e0b3936286b633926d33f84aac40f074779bcbfab01bd18770cb9edcda4ce2471d33f09fcb449663763bc61f47613e486f138fbdca1a0d3bbd33f2e67c98b3c9f7abc0a3c205ca8401d3949041b3b4606d43fcd4809d1bc6c7d3c2a8a0790f35e1239c44fddfe7c50d43f47ce00d897ca7abcd53ea5dd0d110eb98b1bcd4b789ad43f02250b90406d1bbc2107a84e9accb2b86afbe88038e4d43f7e15e9f4380a31bcdcfc34744a96c738b3964cfcbd2dd53f6df28739d8737f3c963cbcdfbdcb1239cb78331b0977d53f1dfbc4941fbe73bcc16aed90a11a173988d6fb391ac0d53f5d7e7d3bfd1778bc175706c3b68bf4b8ae4829b4f108d63f90e6b762fc437abc229eb942275c0139a77b67e48f51d63f6761371287dd5abc3aa3d2812e2dfeb8b8d48c24f599d63fbf2d0d1552317a3cf76c79d8b07900b9de0c9dcd21e2d63fc13dd18254e3753c9b5fdf0e6cdeedb883c1cb37162ad73ffe57d79ac9f27abc298095e3a6b60ab93cfb7ebad271d73f58dbed2a13905bbc7c586bcf6d78acb8c4aa51ac57b9d73f1973fbf4dbd8753c9ba604e122a91c39541c1663a500d83fbd6ffa045b57693c92fc8dbdb01109398e61d833bc47d83f50788d88b51f6cbcdb586a5764280e3926b2e0729c8ed83f59c9cdd666d276bc0d389238c13bf5b872c3b57346d5d83f029394b6bd3075bcace6ccd843b3eb3809171f89ba1bd93f591ea5be52d372bcbabea8db8561f5389c402705f961d93fcae648a198c27cbcd8d85468593e15392f231e3902a8d93f61ca2187b65d6a3cae856f613cdcf5b8e0259b75d6edd93f4d548ec138c97dbc6e4615f85919fab851607f0a7633da3f4f181acaefa078bcce5307c0dc1f1b39f4bef746e178da3f12255c6a81a0743cd9092cebb25310b9491f7f7918beda3f433aa7aa8d5b5ebccfaf2a3d6e78fc383464e0ef1b03db3fbbc3aeb00598793c01fb7d4ab51aef38a18238f7eb47db3f769dc041e4fa76bc1ca17fb58e54fcb87a86f8db888cdb3ffcdad1a788327abc185ec9d84c6917b93190e7e9f2d0db3fa5fb37c7f42e75bcccb6f58b4853e438e6ca246c2a15dc3fdd6b5ada1aed6fbc6cd3c85eb27dfd38565b29ad2f59dc3fccbcbb52b99f7f3c8f98615681a70839b447caf6029ddc3fefc12b2e1c4066bca759ba2e38380db97d583a92a4e0dc3fd32f8ce87a516bbc3256b5de8fea0bb97df20bc81424dd3fd2532ab56f85743cf6de402f466a19390fea32e05367dd3fadaafa661e141cbc41b70b733ebfb638b94f062262aadd3fb7115671a574123cfb166c7b12c998b84c3642d43feddd3f417c4558c6ae733cb148e84eb38906b99872093ded2fde3fab4274ae4ae44b3c4eb0ef05d239e738d254e7a16a72de3f26e2e8253ca3483cead4eb46d1abe938ce5bd147b8b4de3fedf5807972f9743c67dcd35f8f3c1db920e22873d6f6de3f05410e6a80477f3c052b89cc18e01bb941c5bc67c538df3f95a09716e48067bc6c9170aaa87df0b8cf06cb68857adf3f85d7fd3f67f368bcfeab6d06a572f3b80a6802b916bcdf3f78c253cc6cd4613ca5245261b64109b99bff839a79fddf3f1e1df432e08c71bc9c7b522da1d31db9e0647227571fe03fb3d35c08819a5bbc22e19f1dfab4f5387f99978bda3fe03fd7b302321aca793c965b13a96a1a1cb9b24ef2194760e03fe1bf9c8f8f4c7f3c7774560493da00393d707ff29c80e03ffb604507cf96843c14119e661e3d2cb9fce1f834dca0e03f7d89a14939f97fbc8c408a347a691ab9a63ad60005c1e03f97d8a6d49520863cbbd3507e50152e39117c4d7517e1e03f25fd24caabae633c59dbf2681135f9b8eac853b11301e13f39eb9b6b37c78e3c9aab6d4d3bad273907189ed3f920e13fa3e922b068a5763c11edb582d0dcf6b844e5a1fac940e13feda773b5967a88bc144f94a8b57b2ab906e095448460e13fbb9cf58f372b4e3ce14b4924263defb86a9772cf2880e13fcd14455f3aae833c64118711066328392124f3b8b79fe13f560d30d570588c3c1cdd0facd6ea23390ed0951e31bfe13f79322356ec4a6cbca4a92aa782e3efb8a6bb9c1d95dee13ff71dc7864c7a683c06826616e9c1053926810ed3e3fde13f3c11ce515790803c66b2444f756321399ad5b65b1d1de23ffe5085167aa8783c1ce5b612371907b9c82727d4413ce23f271053c3981d503ce95ac3f97f91fd38043db758515be23fd3234640a3a1853ca1ab3dd3bdddcf38f8cb85054c7ae23f7dba8e833be1453cbedcc5d20357c4b8601579f63199e23fcce24577f67c843c172f390d4111e738d17a3f4703b8e23f5d7e7d3bfd1758bc175706c3b68bd4b880135013c0d6e23f00152f8a532a893c1fdc538c184f18b9263feb7568f5e23f129369d18c27643cb276d8e56f160db9f2361b8afc13e33f5ba380201df97c3cd64c3f7a65591639a79cb46a7c32e33ff0de02fd36ca7bbc0c06dc6e9e93f038d9075732e850e33fad9c236c369367bc7f05fdde145dd13862916dfb3f6fe33f367354a8dc11803cde84334f45d82c39095d2fe0838de33f74c573e1f00d743c609be325b09519396721a0fab3abe33f837d8bdfa69a79bcc0d5ca39c6bd12b912ae9064d0c9e33f037548d90a66713c395785b1a416e5b816709f37d9e7e33fb25469eba9b88a3c2480d0cf4fca22b9bcf4388dce05e43f3155354ed01264bcdee718dd34b4e038a96a987eb023e43f13ab5274d8c1893cfdb1b510c36428396521c8247f41e43fba5e598d949384bc7b14b14bad8625b93907a2983a5fe43f4563f60700ca86bc1fec8214717c25b98725d0f2e27ce43f765b20176fbc6cbc592f4b2d8401cfb88b1bcd4b789ae43f02250b90406d2bbc2107a84e9accc2b89597e4bbfab7e43ffc88584319168f3c96bfee6e01c826b9c4ce335b6ad5e43ffd1c66fe7b9b823cc0eb11bc6f3c2a393ef3a941c7f2e43f2a41e0c21fd48d3c4aeb6c47e4ca2439f9a808871110e53fcb6e3a645b0b71bc96b1cdc5ea3a1cb90979e442492de53f872a21e126bd833c0fe501f3ef482c398e43a58c6e4ae53f2f511567cd9383bc4e72080bce8407b92cb0867b8167e53f2e0ceeafe9c58e3cb79888f79702f438349d98268284e53f524bb9163a397ebcf31c237aa74408395c8dbfa470a1e53f128e3e7941e3753cce6c6e1b142201b93514b50c4dbee53f6a77b73f304585bcf8b9740a80fc2c393c41087517dbe53fd1bddaea9170823c246ab064e3a12e39af091ef4cff7e53fa8cce84f39cd81bc3421d8c8d74e133909b131a07614e63f757150e4468c89bc39d6d6a7fea41c394830558f0b31e63f0079c785961a803c93dc40242a991939f09b71d78e4de63f5b7ae1bf03e189bc162fc6fad21e2539cc88478e006ae63f688d375e926879bc951ea4e7f0880a39876f6fc96086e63f0863be0edcc57b3cab6f8436246a02b90a0f5a9eafa2e63fcd17e4e52a13503c63d242f484abe7b8aecd5022edbee63f44d085404f15573ccaeda1d38b17fcb84a19766a19dbe63fed4ae5c0074173bc907c7c6d7869e83818c6c58b34f7e63ff11a37c3035e86bceff8b79f00162a397c6c159b3e13e73f8eec374280e96abca590e5638bf30a39b0c514ad372fe73f6b58d0c874317abc5e8018b59c37fab854074ed61f4be73f9b5717e0438d7cbc6410c80351581cb9ee3d262bf766e73fe3445acca40e79bcb2b3cc56aad4183957a6ddbfbd82e73fa9aa97841ff28e3c6a61907b62d30739200690a8739ee73f359d5b12b6997dbc9627f17d649010b9e40235f918bae73ffe3843fe6ed5533c9fcab70049eaf7b8a478a0c5add5e73f2b94a6ec8e10803c6cb1db0274ad0e3916cf822132f1e73f0aca0044996877bcc710352dacc002b9f94d6920a60ce83fcfe408da2e777bbc69d61732359a1cb97370bed50928e83fc61e9eecfd1172bc579fdd769b4b08397437ca545d43e83f3c9ef37c620c82bcbd53ce5e01582e39267bb2b0a05ee83ff6a164e8fc86603c2d47bb03261b0ab9713b7bfcd379e83fe437c2d7fd6680bc5558ec831c1104398bef064bf794e83fab46f6b1a81b803cec5de7ffc5170239a9d416af0ab0e83f66bc3a4772936ebc67c36537994b04b9be3b4b3b0ecbe83f9139815edd2583bc662c2a03fe40fab861d6230202e6e83fe7fffaf34f058ebce5c2d57efdde2ab9cd020016e600e93fd7e7827bafc02bbcf027ceedad06ceb809171f89ba1be93f591ea5be52d382bcbabea8db856105392faba06d7f36e93f91bc0893e6cd88bccbfceb62785015b9e3e284d53451e93fbdf7d66e1ed3893c9d0c9c36425429b9f6b5acd2da6be93f21b9ceeab61360bccb7764f9653bb7382f38da767186e93f2888c1bea6ac8ebc406dd1df2375283950e0b0d3f8a0e93f1a68a15a46b570bc8d6ba4053bc6ec384dceb5fa70bbe93fcd12e6cdd8f48ebc7e24784e1d692439b31050fdd9d5e93f9f201d485699893c20951ae31ca1163956e9c8ec33f0e93f8c0022383473843cf72a91eb42b429392d114cda7e0aea3fad307e44d1ce69bc702c58ab6e9ffcb877fbe7d6ba24ea3f755665fb7d4685bcd385e0ef5e3e0f391f188ef3e73eea3fb6f4ae7239c377bcac2756dcc9840539641513410659ea3f615ec42b3ff3723c050953d2b0ad0539c8202fd01573ea3fdb7d04707daa80bc5efe989afe291a3945277eb1168dea3f5fbc1e68cd5183bc2702d97631cd2039d31480f508a7ea3f83c81af28d378f3cb388bd7d6b3324393b1399acecc0ea3fa83c6a749e1e743cdf8cb825035f103933c811e7c1daea3f0446284fe95477bc3e1518d1a3add5b8d69217b588f4ea3f40053c5a9c2285bc6a3c4f1b236f16b96cc8bc26410eeb3f657bf258892c74bc9be263b24582f8b88af0f84beb27eb3fe039978dacb5813c39717c469ac9f1388c00a9348741eb3f04094138a34373bc8c5eb6705eb3183966968ff0145beb3f8af0e29f617969bca36a7364a6910bb9da32558f9474eb3f328ecd50dcd2823c24674ff4c13121b909738820068eeb3f2207b1253b3a6d3c7783398bc47a013964499eb369a7eb3f195d29bcfdf05d3c91da8d264765f0380636f257bfc0eb3fc2663452b28a8bbc0b619135c20016b96e7ec61c07daeb3f4b26fb7d48ba82bc99bb8e64fe4c15b9a764441141f3eb3f98120d122ec880bc12d26ec7b7621c39d35d7c446d0cec3f181d57696ab6883ca9fa0f5fd12323b9294866c58b25ec3fe421be5f902085bc5cfda2344d0aecb855a0e1a29c3eec3fd8db24a7c5b4793c6404f112870ff83858b6b5eb9f57ec3f21885cbb346b77bc2c8dd898b28b0839c7e191ae9570ec3f321123c6262f883ca757ad98fd1813398eb50dfa7d89ec3f45b4b56827338ebcc59293d5a4cd05b91633a9dc58a2ec3f845560c871ff7a3ceaed46e7a7e707b9f6fccc6426bbec3f650beb3ea14c84bc8c4b723d8263d1380789caa0e6d3ec3fa99d75b745de7ebc38536df498741d390352dc9e99ecec3fc21d1c8244ea34bcf1276543cc97c2389608266d3f05ed3fea3970d725c68c3c397d4a1509362639f1c3b419d81ded3f618abf846e6c7bbc52967f81e85dd1b8d5317fb26336ed3fc756dad61bbc743cbed589c694ae02392bc66545e24eed3f95b6ab8a134983bc4472bd382730d8380fea32e05367ed3fadaafa661e142cbc41b70b733ebfc6386c2a9b90b87fed3f3e7a5e000ae4813c6816c627539bdd3815663d641098ed3ff4dd5c8afc1d7ebc37fbf11ca96b16b965fba2685bb0ed3f4157db0f051086bc56bfa336022415b96cf53fab99c8ed3fc610b5aece99873c4a43f49e7ece1839a4387339cbe0ed3f076449764fa1893ce078ad79fc952b392caf8620f0f8ed3ff42a1a0fd2fb823c12457dae837b25399674af6d0811ee3f1127236d46f6743c6ad25a250dd512b940010e2e1429ee3f53bce367aaba6f3c365de723eef605393d55ae6e1341ee3f526182b3fbe46a3c726973fdd2e1f3b8ce22883c0659ee3f9bb16e6a4486853c8418f66d2bd0fb386ff87ea4ec70ee3fdbac353308d469bcbb876170a7fc0c39736a62b3c688ee3f22f8030e977785bc4ac7071b50511c393a3cee7594a0ee3fb0136e2f2ac661bc74a9d9e5ec5808b9fb88caf855b8ee3f1e6177ceb8965a3ca19ca5358cfff2b823ec8b480bd0ee3fedcfc0d5f1c3853c77915b9723432eb950a9b371b4e7ee3f22bd9f7c705b8f3c5681bfee5c86fbb8e4d3af8051ffee3ff028ad2566d87cbc75f354baab9716b93076db81e216ef3fb6af1046c9907d3c180dd176e53df5b845b87e81672eef3f944297b35d84553cad04dc0df604f6385506cf8be045ef3f08c13df95693833c866b8ceab3e007b9be36efac4d5def3f50b2c2cc7743873c26d77b68f1bc2039aeafeff0ae74ef3f7c394ed0242f74bca5acdbec3b0b0f39698cce63048cef3f333da705926b8cbc6e234642e55f2bb933c277114ea3ef3f9fd3b5e7a2a4883c99d69a522bdc26b9df44c5058cbaef3f85142fff53d77f3c37b8092cdfbef438f92a7f4cbed1ef3f0d44837388ac803cbadcada742ae0339a0d15bf1e4e8ef3fb42d1722f1ec723c88907f3167a61eb9')
1452
+ ctx.runtime.powExp2Table = powHexToBytes('cd3b7f669ea0e63f5664b21334dd8bbc75c1de3a3e7d25393e1775fa52b0e63f0e9d9a2cf5386a3c186d6259ba6bf938bfda0b7512c0e63f0d0bff67568962bc196c76585cefff384576d4dddccfe63f0973f1b6a97a8c3c9e7de927cb80f8b82f1a653cb2dfe63fab883c683abe5bbc78e7bb8af859ba38e53a599892efe63fb2c81a9e74b980bc90d12acab38d0ab9849451f97dffe63ff60e86250f3c78bc6e954a3f920100b9872ef466740fe73f5fa65ad444d6493ca1a2478fa89cddb8745fece8751fe73f997a8886476e71bcc3a45369796912b98ad0ea86822fe73f722cd62ca00a82bc87af0b7efb8d18b97481a5489a3fe73f3cd5656cd9a880bc68a7f317e22a2839fdcbd735bd4fe73f1c6e8a61fd47803c04b9ad3f03422b39c9674256eb5fe73fd36d3157592480bce8e519fae7f828b9096eabb12470e73ff847911677788b3c93b1e7d1c5b9eeb83f5dde4f6980e73f2d16020ab866883cf7327930424d24b9f61cac38b990e73f3eddaa62a849833c5fbd4fd609b100b98701eb7314a1e73f2f9904ee771574bcd4102d937a21d4b8dbcf76097bb1e73f88dc6884b5eb8bbc6dc00b4b750313b932c13001edc1e73fd64d16d14c128f3c03bbc26c234d2db9f086ff626ad2e73fb4b872fbdbbd813c8c6f94b65eda29b9624ecf36f3e2e73f7e7915ba025d603cdffcf827140ae73891c4918487f3e73fae1193cf117f70bcffb785360b9f1139121a3e542704e83f2b976d62867c82bc6eb1c9710d4e1d39d906d1add214e83f4d1d150d3764843c77d261654d692c3913ce4c998925e83fd83215d41d4c8dbcc1bacb65adf6e038f741b91e4c36e83fd52bdf319a9b893c8b2005ab00511e39adc723461a47e83ffbcd41a384d678bcd1ef165ce19115b9215b9f17f457e83fd016b2f848a74bbc90ee2ee7e658ea38ed92449bd968e83fbaf6d49bf8c68fbc21d98151f6161fb936a431d9ca79e83fbd47dbd2d7d2753c1fa99ec1799607b999668ad9c78ae83f3ab57cf3c294893cde85f33e28612d390f5878a4d09be83f2b20754495538d3cf40038928efd2fb9dba02a42e5ace83f274b8656f1e9863c336383a7440603b97817d6ba05bee83f6e4443fc5ecb8e3c607ef4b4f14e2e398c44b51632cfe83faae3e9325ed560bcd69d83dbb3daf3b8d966085e6ae0e83fe6b2c96f4a1187bca37c34ae62d1213936771599aef1e83f6c97e3a213cc753c6351b8d226bfc3388a2c28d0fe02e93fd23ffe85ca92853c7a400aa6ecaa2939c6ff910b5b14e93f2425582e79d68dbc4a53045285031c39e22faa53c325e93f7fdb39a65f4573bc430b22ab9a041ab9e5c5cdb03737e93fbc7eb581c75f57bcb20dac57e297f638e5985f2bb848e93f992d7d79d6c37dbc0b7e50ed82a614b90f52c8cb445ae93f39f0a5967c4b66bcbb8ba9c95370d0b8b370769add6be93f96c8197f96a54bbc2911a0e23348ec38504ede9f827de93fd1851b7c5b188dbc6f4b14d7b9ed2739a2227ae4338fe93fed784ca2daab6c3c12c377c8c22e0fb9ba07ca70f1a0e93f32e6ce91bd7381bc5f965478985300b90dfe534dbbb2e93f18d5f64d4ed88dbc314b18fee4670f3990f0a38291c4e93fbef271b0467c6c3c5c0843796b370639d5b84b1974d6e93f3382dda3be1685bc98c691ebba1905b92323e31963e8e93f6e4ce678ca24683ce0ba2b082cf9a0389ef2078d5efae93fcefaf1aacea974bc5d7888fce6f0143965e55d7b660cea3f33d51c5d495983bcfbb4514508541339bbb88eed7a1eea3f0ee78bee18668c3cc0782db72f7f2b39332d4aec9b30ea3fab36dc7d5c30863c176dc222fa472539d80a4680c942ea3f20b19f5880a78abc8391d8419e6e2db95d253eb20355ea3fe1418ddb6e2f8dbc483fd6df7afdfbb85260f48a4a67ea3f66036730560f553c750452ee9686eb3858b330139e79ea3fc763c5ca7ecb8b3c51f77631697826b9592ec153fe8bea3fa915bab267f884bc76a16b560d1f2039bffd79556b9eea3f31fdf70ec9fa803cb98c9ee36ab11839ba6e3521e5b0ea3f4545e9da319c783c71d9f0903bf40ab97af3d3bf6bc3ea3fd06ce7ca34927fbcf89676fcdb60fcb874273c3affd5ea3fe5b8b1b63bef873c842b2fd1551a23b9add35a999fe8ea3f81cc5d34cda1873cea75e63abc7f2a39fff222e64cfbea3fcd5e310ffcb284bc302f93a6d252223966b68d29070eeb3f25e4804cf5de8bbc0056c595bb1c143952899a6cce20eb3fcc56074a02dd843c9e8c92f6da8328b9fb154fb8a233eb3f08d784305e8052bcd9a4dd0ebcbaf238b149b7158446eb3f907cdfe93d766fbc6ff5f31c40e5f3b83a59e58d7259eb3fe36dbabbdf718cbcdff71d087074fcb82ac5f1296e6ceb3f6e3f8852f3a8823c6298a998eea11739475efbf2767feb3f3bac547e4f5865bc72abe18144a6fa38e44927f28c92eb3fc665cb5416728bbc672431d91e1115394a06a130b0a5eb3f2e29540ed3fc8ebc673c5091bfd1dab81f6f9ab7e0b8eb3f056269c9d1522fbc882005b899b4b1b8d2c14b901ecceb3f849e2d7ad03d723c58120e0564a1193907a2f3c369dfeb3f525bea6023262cbcff8465c2b82acbb8091ed75bc2f2eb3f739c6b3fcafd8ebcda59cdce817e02393db341612806ec3f34cafba15a8a7dbc9546df0f5dfd06b99c5285dd9b19ec3fdd4850896510713cda285912519e09392c65fad91c2dec3fd7a5c81716e586bcdee5ea370eaf05b97ad0ff5fab40ec3f0ac683e037458b3cf8f470facda6143922fbfa784754ec3fafb59324072f813cac83f5444e6317394bd1572ef167ec3fad3c48ff4d88823cb25c9d324cc41fb933c98889a87bec3f595525bebb767ebc00b57558843902b9b5e706946d8fec3f445c8048bcac613cfab800c1aaedf638dbc4515740a3ec3ff5080dd1bef277bc5a5894cc3352c4386990efdc20b7ec3fdb49e9d1cb03653c2e036b5665870d3975166d2e0fcbec3f939000860f226dbcb3b373e724040e39fac35d550bdfec3f729d82533bd87dbc4920743a07eaeab874ab5b5b15f3ec3f57ff6db8e9088abcf76dba56fe4307b97c89074a2d07ed3f9c7a794337bc8cbcf6a09d0344702eb968c9082b531bed3fee369a213656853c73cd11c6e66c29b9f2890d08872fed3f78859d717b488dbce7faa9b262daf238d6a1caeac843ed3f14165abf53db833cf7567a10bd20243987a4fbdc1858ed3f07375bd702ed723cfc3155b053b0fab8d3e662e8766ced3fa065814a7ae84f3cff759cf50117cb389883c916e380ed3fe8dfed8bc11e81bc5a76c87a4ed00eb97560ff715d95ed3fbef69abb2d058a3c14204926c50d23398532db03e6a9ed3f32b56d6900238c3c15c60e6f24f6273915833ad67cbeed3fe48b6b92f1768bbc2d61e6118f8320b960b401f321d3ed3fc318f07857da823cf31c66adde6c2cb958061c64d5e7ed3f8fba798e52a58cbce15e2b82fdf914395f9b7b3397fced3f5c4b184fcda581bcd6ef44a925722b39177d196b6711ee3f447f5cbd29b562bc2a07a59c3086033929a1f5144626ee3f96147a8127b687bc9a408c8018982bb912ee163b333bee3f8bc6fd31a4f489bc84c79e916db82839f63f8be72e50ee3f8fcca980899e733c78d2c2b32ce91139766d67243965ee3f35b72275f83f76bc18a7b58dfcbd0139834cc7fb517aee3fe28d0cca22d5823ccba9b6b057a728b940b7cd77798fee3fb154b080940881bccbb70358ae061339da90a4a2afa4ee3f93289c17239c8ebcdef3bb42f2c01fb96eca7c86f4b9ee3ff2e493222f83843ce8cb5102c40f29b9f1678e2d48cfee3f8cad11b4f3938cbc3bb444efdfb920b9108518a2aae4ee3f8d5687a48dc6813c247e07b58d1c0339275a61ee1bfaee3fb0b6a486f4c78d3c69ff29d2d56d2f392a41b61c9c0fef3f451d1865002283bc5bb0934211f823b997ba6b372b25ef3f438e0dbfa5a1833c16b57654adc624397472dd48c93aef3fde37d83e5a5a69bc10cce43f180ae43840456e5b7650ef3f8ba1d82de1d3893cf30ec8ff9b0104b9f84488793266ef3f3e3439357ba38f3c139e4c5aa426173914be9cadfd7bef3f0a3506d012bb8dbc4dfa8072cec52539893c2402d891ef3f89f679a7a82e51bc57efc3bf2493f8b8d8909e81c1a7ef3f1e93a5f35348773c51766fc360c0ed3814d59236babdef3fb68e0915736769bc329a8ad2d40c0b39f1718f2bc2d3ef3fe779659674eb523c6cc54e9396f0f238d9232a6bd9e9ef3fe3fd427403a6643cf30a13e885afeb38000000000000f03f00000000000000000000000000000000bfbc5afa1a0bf03f719f60a7b2f684bc083c3f52dd550bb93533fba93d16f03fb7cdb89a29619b3c8709d80780f42b3981023b146821f03fb64ec50f31bf82bc0bff27a73e9529396180773e9a2cf03f5d085b53839071bcd5743d0a5b0819b9ccbb112ed437f03f1ae1adee1168653ce977bd5a3d310139857f6ee81543f03f6ec977191ca390bc40404bf4fb12f9b8b154f6725f4ef03f8dd0a03a79c3843ce2af722b139c2fb9748515d3b059f03f65b475a4e2738d3c7e258d4ff95f1039891f3c0e0a65f03f97c399577bcb95bc984c6cfeade03339def6dd296b70f03f273cb1e2df918cbcab242c2e1fb41f3936a8722bd47bf03f008745543423833c6e3699508d2b09b9c89b75184587f03fff84b24bbe86613c4f416bd920580139e00766f6bd92f03fd13f0a80638096bcf83ed6f89f18043983f3c6ca3e9ef03f366131187848913c59c2fdd1458b34b919391f9bc7a9f03f381d3d876cd1853cdd230277d9f82cb90f89f96c58b5f03f0b61dc4a2ea6983c4cf7ebd69b7c36b9856ce445f1c0f03fef1cd20689f9943c8e3712c4719d0339f747722b92ccf03f714fe216dc1e903ce36f4e56ac8a3e39ec5d39233bd8f03f6a313fe44dc19bbc9c382ec1be9616b9a2d1d332ece3f03f537bc527173a403cdb9d4e9976aae5b8c5a9df5fa5eff03f1b0254bcb99d94bcfcf10371d54a3db91bd3feaf66fbf03f7bbd4ec4ed9b6bbc5942d8491febfab83e23d7283007f13fd5fd9216eb468d3ca875485b01571f39515b12d00113f13f3a9b443910c596bc2d568f988bd52939b62a5eabdb1ef13f72fb03f754a49cbc8825b0214b45f338cc316cc0bd2af13fc7a56cb314b551bc203108428f8df0b8ab04f214a836f13ff0dc48ba8f1067bcaa1ce9344e9a0c39e02da9ae9a42f13f9e36f19abf2f93bc1664c7b47bfe32b92e314f93954ef13fab44bf39e8918bbc327293d6fedd27b9518ea5c8985af13f0aabeeb96a40823c74c47952571b10b9c2c37154a466f13f321aea823bf2583c80f4f9656fecfc387b517d3cb872f13f768ad7b9419081bcf03fa16a40f22439c0bb9586d47ef13f645aace23f9e703c10929cec4cf90039ea8d8c38f98af13f6c0f97d1231091bcc5970b04f02517392f5d37582697f13f087ef185ddaa943c99a3308a6729263975cb6feb5ba3f13fe468497b4c5b8e3ce86a928361d30a391c8a13f899aff13f8092b6a485bf973cd07bbf23c4c215b9d45c0484e0bbf13f07f62e35865399bc8e710395a60c24b96b1c28952fc8f13fc9f810807709903c674ec981b87518b9aab9683187d4f13f3c64a2006e019e3c18b981082da61e392740b45ee7e0f13fdeb68c08d8fd96bc068766819f4530b91dd9fc2250edf13f8cb77b0298df91bc7574c4364d503e394dce3884c1f9f13f5caf97a024f59bbc1c06837fd78627b9d68c62883b06f23f95844a8175c78d3ca41e6fc1db8107b919a87835be12f23fc9aafc2c2d59933caac80902b46416b996dc7d91491ff23feea594947ea9823c6b10b7b3c29326b9d11279a2dd2bf23f9fd67755fb348d3c7830845221871bb93862756e7a38f23f7305c7b67eb0993ce032f59a9fd824b90a1482fb1f45f23f96a91c91cccf8a3c00703c513b4618393fa6b24fce51f23fa4f4f4be55c18a3c97f7dcafc8a9f13875ce1e71855ef23f2c1bc34aa2e1933ccd8b08766eba3539dd7ce265456bf23fd9e9409933bd823c771b463a3977123929df1d340e78f23f6ce7f9057c069e3c8b53e4ceb7d43bb98163f5e1df84f23f7e0d3f8c3a4c9abc7d2de5a2da7f363970bb9175ba91f23fbd1c402872cc82bcc4e4462d90a3d738e1de1ff59d9ef23f5512adafe812863c9046608544e50d39130fd1668aabf23fa7901619435799bcf6b7737e6dde2db990d9dad07fb8f23fa41a38d6dc0a41bcbd6d79b85f88e0382f1b77397ec5f23f2451eba6450195bcd4bf5c425c243eb90b03e4a685d2f23fd541db544702903c0793cbf8d8e91eb98915641f96dff23f98e1bcfbcf169d3c80f75c7ac6ad2a39562f3ea9afecf23f8323d5450fca713c2ad1e6de087b0d396b88bd4ad2f9f23f93da2b53553c65bc9820749c0886023915b7310afe06f33fe48231d26af4863cd9d09cf0b2b71739fdb2eeed3214f33fd1fcf3f3a359893cb5811b07eb1c29b931d84cfc7021f33f7c04188ee79c8a3ce8852b888c771b3932eaa83bb82ef33f18f3b43ce8459cbcb998083ac7783bb9ff1664b2083cf33fa65936842127933c6bfc6ceaa20634b92dfae3666249f33fa4810893755a83bcc96c7b6aaf7504b9f19f925fc556f33f28464e5cee5c8bbcf2d520e524e528b93b88dea23164f33f5eb86ca044318cbc96802341b08b2f39cba93a37a771f33fe2ea42bfea3a96bcfa6b51123e7e38394a751e23267ff33f3cb2ce9ecaf599bcf8127eed6974053966d8056dae8cf33fbd04993c8d959ebc214f40617aa72039ef40711b409af33f34298efca5a999bc7a0b91aef88f31b9f79fe534dba7f33fe3f561d636e475bc96c217ffb1b00939f46cecbf7fb5f33f18ff6fe2664c953c80bee3b2f8683d39e5a813c32dc3f33fc3295d37f8ff9ebc5a39932a3f1421b973e1ed44e5d0f33f714c288cd0e87f3c1559a64e16bac1382234124ca6def33fbc9ef01109da8a3cb78ffa68ba0828b975511cdf70ecf33fca9b8c7b63f68abc26ba49f3debc09b91c80ac0445faf33ff3f956f923d097bc0d2024373e4730b924a067c32208f43f48d0f4b6f8dd8b3ccf9544751c8b22392a2ef7210a16f43f7892301c69f35ebc1865fcea432bd3b88a460927fb23f43fdd14b3c02d4698bc80feff78d9ca31b997a850d9f531f43f99795fe3ddc781bcef5f1996c4032939d4b9843ffa3ff43f03c00497be80883cdf5849ca664929b92d896160084ef43fd080ef047a9b483c22d9e32d31acd0b832d2a742205cf43f8e1ffb82196468bcc85a5ae4395af8b857001ded416af43f768a64d14b949c3c3a1ff24f40df373937328b666d78f43f335744edf0209cbc083b3da2afb61cb9d03cc1b5a286f43ff06290b6a3c1733cc03a74aeeb1e0e39d2ae92e1e194f43fa09e495e89b283bca67223bb055c2f39ded3d7f02aa3f43f56bed1f362cb993cc7e261c776181939d7b76dea7db1f43ff097287fb82581bc2dee116a40241839272a36d5dabff43fe242ecaf97437d3c392b5c74c706ec3814c117b841cef43f5dbd0a69295e903c6778872174972bb90dddfd99b2dcf43f33786abcdbec983c439b5569c9121239ffabd8812debf43f527a5d2e7d2595bc22db115a0e772eb9a72c9d76b2f9f43fe35759d209b394bccd85b6d71faaf1b8ee31457f4108f53f5f46b7499b247a3cf8110a5f6d420fb94266cfa2da16f53fef93bd6985768fbc7d172682710ef938ef4e3fe87d25f53f71efef438d997cbce8d175164a9710b9824f9d562b34f53fad3cb11dbe7a80bc4c211f9533a70f3927adf6f4e242f53f7e5f2d196d92873caa6ba02e782611b90f925dcaa451f53f9be5edef9c688dbc93041b7791c91939d210e9dd7060f53ff0eb8e166efb90bc715c57ae29113939da27b536476ff53fad931d012cbb993cff13a65268f80fb9cfc4e2db277ef53fc4b9578a8cb990bc20aa4f1f89393db9fdc797d4128df53fe81d9a5be195823cc6e4d12ad9262ab9cc07ff27089cf53f0fe667e4cee297bc25bc1b2f770c2d39295448dd07abf53fad4746054c32963cfeda6f50ee4427b9037aa8fb11baf53f1a3e234ca1779bbc004288b1df763439b746598a26c9f53fa28669811b4b3c3c8c97545273c28e38938b999045d8f53f4356b4a8a7d69cbcfbb7d1eccf6d32394821ad156fe7f53f5ee68030f9a69b3cd6a75fb79a5f39b971ebdc20a3f6f53f92cfcde3ddea89bc7974c7fba6e1203909dc76b9e105f63f47de569b42e293bc88252eb9542c13b9f4f6cde62a15f63f274cb84a3e4b9e3cc85a3264caa305b985553ab07e24f63f97b4407ec18393bc91b9cf57e7d80539fd29191ddd33f63fe564b9be1047983cb6dd0b51212e373920c3cc344643f63f33899d753c488cbc0fc4c10040901339b78fbcfeb952f63f093ea7c9d5e39abc1a3c878aa6fd3339252255823862f63f341c598709b69bbc3b0adcf437a33439f63308c7c171f63f34616c5832878ebc25da440de8592f3973a94cd45581f63f653ef744ae38603cff043b630328efb838959eb1f490f63f5d44eb9abd04883caaebbcc8eaf828b9')
1453
+
1454
+ wat('math.pow_transcend', genPowTranscend(), ['math.pow_scalbn'])
1455
+ } // if (crPow)
1456
+
863
1457
  wat('math.pow', `(func $math.pow (param $x f64) (param $y f64) (result f64)
864
1458
  (local $result f64) (local $n i32) (local $neg_base i32) (local $abs_x f64)
865
1459
  ;; y == 0 -> 1 (covers pow(NaN,0), pow(±0,0), pow(±Inf,0))
@@ -933,87 +1527,348 @@ export default (ctx) => {
933
1527
  ;; x < 0, non-integer finite y -> NaN
934
1528
  (if (f64.lt (local.get $x) (f64.const 0.0))
935
1529
  (then (return (f64.const nan))))
936
- (call $math.exp (f64.mul (local.get $y) (call $math.log (local.get $x)))))`)
1530
+ ;; Remaining case: x > 0 finite (≠1), y finite (≠0,≠1) and not an i32-range integer.
1531
+ ;; $math.pow_core below is a correctly-rounded fdlibm port by default (no exp/log double-
1532
+ ;; rounding), or — under optimize.crPow — CORRECTLY ROUNDED in the stronger CORE-MATH sense
1533
+ ;; (two-phase Ziv dd/td kernel, see $math.pow_core's own comment).
1534
+ (call $math.pow_core (local.get $x) (local.get $y)))`)
1535
+
1536
+ // scalbn(x, n) = x * 2^n, correctly rounded even when the result lands in the subnormal
1537
+ // range (a single f64.mul by a bit-constructed 2^n would double-round there). Ported from
1538
+ // musl's src/math/scalbn.c (MIT — https://git.musl-libc.org/cgit/musl/tree/src/math/scalbn.c,
1539
+ // also FreeBSD msun's scalbn.c): splitting the scale into two safe steps, each within the
1540
+ // exact power-of-two range, avoids that double rounding. Only reached from $math.pow_core's
1541
+ // subnormal-result tail, where |n| stays well under 1075 — the >1023 branch and the doubly-
1542
+ // nested steps are dead there but kept for fidelity with the reference.
1543
+ wat('math.pow_scalbn', `(func $math.pow_scalbn (param $x f64) (param $n i32) (result f64)
1544
+ (local $y f64)
1545
+ (local.set $y (local.get $x))
1546
+ (if (i32.gt_s (local.get $n) (i32.const 1023))
1547
+ (then
1548
+ (local.set $y (f64.mul (local.get $y) (f64.const 0x1p1023)))
1549
+ (local.set $n (i32.sub (local.get $n) (i32.const 1023)))
1550
+ (if (i32.gt_s (local.get $n) (i32.const 1023))
1551
+ (then
1552
+ (local.set $y (f64.mul (local.get $y) (f64.const 0x1p1023)))
1553
+ (local.set $n (i32.sub (local.get $n) (i32.const 1023)))
1554
+ (if (i32.gt_s (local.get $n) (i32.const 1023)) (then (local.set $n (i32.const 1023)))))))
1555
+ (else (if (i32.lt_s (local.get $n) (i32.const -1022))
1556
+ (then
1557
+ (local.set $y (f64.mul (local.get $y) (f64.mul (f64.const 0x1p-1022) (f64.const 0x1p53))))
1558
+ (local.set $n (i32.add (local.get $n) (i32.const 969))) ;; 1022-53, staged to dodge subnormal double-rounding
1559
+ (if (i32.lt_s (local.get $n) (i32.const -1022))
1560
+ (then
1561
+ (local.set $y (f64.mul (local.get $y) (f64.mul (f64.const 0x1p-1022) (f64.const 0x1p53))))
1562
+ (local.set $n (i32.add (local.get $n) (i32.const 969)))
1563
+ (if (i32.lt_s (local.get $n) (i32.const -1022)) (then (local.set $n (i32.const -1022))))))))))
1564
+ (f64.mul (local.get $y)
1565
+ (f64.reinterpret_i64 (i64.shl (i64.extend_i32_s (i32.add (local.get $n) (i32.const 1023))) (i64.const 52)))))`)
1566
+
1567
+ // x**y for the case the ladder above can't fast-path: x > 0 finite (≠1), y finite (≠0,≠1) and
1568
+ // not an i32-range integer. y==0.5 is always special-cased to hardware sqrt (correctly
1569
+ // rounded, cheaper than either general kernel below) regardless of crPow. Two kernels, picked
1570
+ // by `optimize.crPow` (see the authoritative comment above `emitPow` for the flag's full
1571
+ // semantics and the measured cost of switching):
1572
+ // OFF (DEFAULT): ported from fdlibm/FreeBSD msun's e_pow.c (Sun Microsystems, freely
1573
+ // licensed — https://raw.githubusercontent.com/freebsd/freebsd-src/main/lib/msun/src/
1574
+ // e_pow.c), the same algorithm V8's base/ieee754.cc ports for Math.pow — so this targets
1575
+ // bit-exactness against the host, not just low ulps (though it is "nearly rounded", not
1576
+ // CORE-MATH-class correctly rounded — see $math.pow_transcend for that). Trimmed to the
1577
+ // x>0 slice: fdlibm's sign/yisint bookkeeping for x<0 is dead weight here (x<0 already
1578
+ // returned NaN above).
1579
+ // 1. log2(x) in double-double (hi+lo): bit-extract the exponent, reduce the mantissa
1580
+ // around 1 or 1.5 (whichever centers it tighter), run the L1..L6 minimax on
1581
+ // s=(m-bp)/(m+bp). |y| ≥ 2^31 skips straight to a 1-term series, valid because the
1582
+ // only way such a y doesn't over/underflow outright is x within 2^-20 of 1.
1583
+ // 2. y*log2(x) in double-double, with an early overflow/underflow return once the
1584
+ // exponent product is unambiguously outside (-1075, 1024).
1585
+ // 3. 2^(that product): round to the nearest integer n — via the high-word bit trick
1586
+ // fdlibm uses, not float rounding, so the fractional remainder stays exact — evaluate
1587
+ // the P1..P5 minimax on the fraction, then splice n back in as a raw exponent-field
1588
+ // add, falling back to $math.pow_scalbn only when that add would underflow the
1589
+ // exponent field.
1590
+ // ON: delegates to the shared two-phase Ziv dd/td kernel — see $math.pow_transcend's own
1591
+ // comment above for the algorithm. CORE-MATH-class correctly rounded (0 misrounds on the
1592
+ // 5152-vector gate, test/pow-cr.js) at a measured ~13x runtime cost on gamma-heavy color
1593
+ // kernels (colorpq), hence opt-in rather than default.
1594
+ wat('math.pow_core', crPow
1595
+ ? `(func $math.pow_core (param $x f64) (param $y f64) (result f64)
1596
+ (if (f64.eq (local.get $y) (f64.const 0.5))
1597
+ (then (return (f64.sqrt (local.get $x)))))
1598
+ (call $math.pow_transcend (local.get $x) (local.get $y)))`
1599
+ : `(func $math.pow_core (param $x f64) (param $y f64) (result f64)
1600
+ (local $ax f64) (local $u f64) (local $v f64) (local $w f64) (local $t f64) (local $r f64)
1601
+ (local $t1 f64) (local $t2 f64) (local $y1 f64) (local $p_h f64) (local $p_l f64) (local $z f64)
1602
+ (local $ss f64) (local $s2 f64) (local $s_h f64) (local $s_l f64) (local $t_h f64) (local $t_l f64)
1603
+ (local $z_h f64) (local $z_l f64) (local $bp_k f64) (local $dp_h_k f64) (local $dp_l_k f64)
1604
+ (local $ix i32) (local $hy i32) (local $iy i32) (local $j i32) (local $i i32) (local $k i32) (local $n i32)
1605
+
1606
+ ;; y == 0.5 exactly (x > 0 here, always a valid sqrt domain): matches fdlibm/V8's own sqrt
1607
+ ;; fast path, and f64.sqrt is correctly rounded so this can only help bit-exactness.
1608
+ (if (f64.eq (local.get $y) (f64.const 0.5))
1609
+ (then (return (f64.sqrt (local.get $x)))))
1610
+
1611
+ (local.set $ax (local.get $x))
1612
+ (local.set $ix (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $x)) (i64.const 32))))
1613
+ (local.set $hy (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $y)) (i64.const 32))))
1614
+ (local.set $iy (i32.and (local.get $hy) (i32.const 0x7fffffff)))
1615
+
1616
+ (if (i32.gt_u (local.get $iy) (i32.const 0x41e00000))
1617
+ (then
1618
+ ;; |y| > 2^31: definite overflow/underflow unless x is within ~2^-20 of 1, in which
1619
+ ;; case log(x) via a short series (x-x^2/2+x^3/3-x^4/4) suffices.
1620
+ (if (i32.gt_u (local.get $iy) (i32.const 0x43f00000))
1621
+ (then
1622
+ (if (i32.le_u (local.get $ix) (i32.const 0x3fefffff))
1623
+ (then (return (select (f64.const inf) (f64.const 0.0) (i32.lt_s (local.get $hy) (i32.const 0))))))
1624
+ (if (i32.ge_u (local.get $ix) (i32.const 0x3ff00000))
1625
+ (then (return (select (f64.const inf) (f64.const 0.0) (i32.gt_s (local.get $hy) (i32.const 0))))))))
1626
+ (if (i32.lt_u (local.get $ix) (i32.const 0x3fefffff))
1627
+ (then (return (select (f64.const inf) (f64.const 0.0) (i32.lt_s (local.get $hy) (i32.const 0))))))
1628
+ (if (i32.gt_u (local.get $ix) (i32.const 0x3ff00000))
1629
+ (then (return (select (f64.const inf) (f64.const 0.0) (i32.gt_s (local.get $hy) (i32.const 0))))))
1630
+ (local.set $t (f64.sub (local.get $ax) (f64.const 1.0)))
1631
+ (local.set $w (f64.mul (f64.mul (local.get $t) (local.get $t))
1632
+ (f64.sub (f64.const 0.5) (f64.mul (local.get $t)
1633
+ (f64.sub (f64.const 3.3333333333333331e-01) (f64.mul (local.get $t) (f64.const 0.25)))))))
1634
+ (local.set $u (f64.mul (f64.const 1.44269502162933349609e+00) (local.get $t)))
1635
+ (local.set $v (f64.sub (f64.mul (local.get $t) (f64.const 1.92596299112661746887e-08))
1636
+ (f64.mul (local.get $w) (f64.const 1.44269504088896338700e+00))))
1637
+ (local.set $t1 (f64.add (local.get $u) (local.get $v)))
1638
+ (local.set $t1 (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $t1)) (i64.const 0xffffffff00000000))))
1639
+ (local.set $t2 (f64.sub (local.get $v) (f64.sub (local.get $t1) (local.get $u)))))
1640
+ (else
1641
+ (local.set $n (i32.const 0))
1642
+ ;; Subnormal x: scale into the normal range and remember the shift.
1643
+ (if (i32.lt_u (local.get $ix) (i32.const 0x00100000))
1644
+ (then
1645
+ (local.set $ax (f64.mul (local.get $ax) (f64.const 9007199254740992.0)))
1646
+ (local.set $n (i32.sub (local.get $n) (i32.const 53)))
1647
+ (local.set $ix (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $ax)) (i64.const 32))))))
1648
+ (local.set $n (i32.add (local.get $n) (i32.sub (i32.shr_u (local.get $ix) (i32.const 20)) (i32.const 0x3ff))))
1649
+ (local.set $j (i32.and (local.get $ix) (i32.const 0x000fffff)))
1650
+ (local.set $ix (i32.or (local.get $j) (i32.const 0x3ff00000)))
1651
+ ;; Interval split: center the reduced mantissa on 1 (k=0, |x|<sqrt(3/2)) or 1.5
1652
+ ;; (k=1, |x|<sqrt(3)) — whichever keeps s=(m-bp[k])/(m+bp[k]) smaller.
1653
+ (if (i32.le_u (local.get $j) (i32.const 0x0003988E))
1654
+ (then (local.set $k (i32.const 0)))
1655
+ (else (if (i32.lt_u (local.get $j) (i32.const 0x000BB67A))
1656
+ (then (local.set $k (i32.const 1)))
1657
+ (else
1658
+ (local.set $k (i32.const 0))
1659
+ (local.set $n (i32.add (local.get $n) (i32.const 1)))
1660
+ (local.set $ix (i32.sub (local.get $ix) (i32.const 0x00100000)))))))
1661
+ (local.set $ax (f64.reinterpret_i64
1662
+ (i64.or (i64.and (i64.reinterpret_f64 (local.get $ax)) (i64.const 0x00000000ffffffff))
1663
+ (i64.shl (i64.extend_i32_u (local.get $ix)) (i64.const 32)))))
1664
+ (local.set $bp_k (select (f64.const 1.5) (f64.const 1.0) (i32.eq (local.get $k) (i32.const 1))))
1665
+ (local.set $dp_h_k (select (f64.const 0.584962487220764160156) (f64.const 0.0) (i32.eq (local.get $k) (i32.const 1))))
1666
+ (local.set $dp_l_k (select (f64.const 1.35003920212974897128e-08) (f64.const 0.0) (i32.eq (local.get $k) (i32.const 1))))
1667
+ (local.set $u (f64.sub (local.get $ax) (local.get $bp_k)))
1668
+ (local.set $v (f64.div (f64.const 1.0) (f64.add (local.get $ax) (local.get $bp_k))))
1669
+ (local.set $ss (f64.mul (local.get $u) (local.get $v)))
1670
+ (local.set $s_h (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $ss)) (i64.const 0xffffffff00000000))))
1671
+ ;; t_h ≈ (ax+bp[k]) with its low 32 bits cleared, built directly from ix's bits (half
1672
+ ;; the exponent+mantissa, plus fdlibm's fixed per-k offsets) rather than an add+round.
1673
+ (local.set $t_h (f64.reinterpret_i64 (i64.shl
1674
+ (i64.extend_i32_u (i32.add (i32.add
1675
+ (i32.or (i32.shr_u (local.get $ix) (i32.const 1)) (i32.const 0x20000000))
1676
+ (i32.const 0x00080000))
1677
+ (i32.shl (local.get $k) (i32.const 18))))
1678
+ (i64.const 32))))
1679
+ (local.set $t_l (f64.sub (local.get $ax) (f64.sub (local.get $t_h) (local.get $bp_k))))
1680
+ (local.set $s_l (f64.mul (local.get $v)
1681
+ (f64.sub (f64.sub (local.get $u) (f64.mul (local.get $s_h) (local.get $t_h))) (f64.mul (local.get $s_h) (local.get $t_l)))))
1682
+ (local.set $s2 (f64.mul (local.get $ss) (local.get $ss)))
1683
+ (local.set $r (f64.mul (f64.mul (local.get $s2) (local.get $s2))
1684
+ (f64.add (f64.const 5.99999999999994648725e-01) (f64.mul (local.get $s2)
1685
+ (f64.add (f64.const 4.28571428578550184252e-01) (f64.mul (local.get $s2)
1686
+ (f64.add (f64.const 3.33333329818377432918e-01) (f64.mul (local.get $s2)
1687
+ (f64.add (f64.const 2.72728123808534006489e-01) (f64.mul (local.get $s2)
1688
+ (f64.add (f64.const 2.30660745775561754067e-01) (f64.mul (local.get $s2) (f64.const 2.06975017800338417784e-01)))))))))))))
1689
+ (local.set $r (f64.add (local.get $r) (f64.mul (local.get $s_l) (f64.add (local.get $s_h) (local.get $ss)))))
1690
+ (local.set $s2 (f64.mul (local.get $s_h) (local.get $s_h)))
1691
+ (local.set $t_h (f64.add (f64.add (f64.const 3.0) (local.get $s2)) (local.get $r)))
1692
+ (local.set $t_h (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $t_h)) (i64.const 0xffffffff00000000))))
1693
+ (local.set $t_l (f64.sub (local.get $r) (f64.sub (f64.sub (local.get $t_h) (f64.const 3.0)) (local.get $s2))))
1694
+ (local.set $u (f64.mul (local.get $s_h) (local.get $t_h)))
1695
+ (local.set $v (f64.add (f64.mul (local.get $s_l) (local.get $t_h)) (f64.mul (local.get $t_l) (local.get $ss))))
1696
+ (local.set $p_h (f64.add (local.get $u) (local.get $v)))
1697
+ (local.set $p_h (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $p_h)) (i64.const 0xffffffff00000000))))
1698
+ (local.set $p_l (f64.sub (local.get $v) (f64.sub (local.get $p_h) (local.get $u))))
1699
+ (local.set $z_h (f64.mul (f64.const 9.61796700954437255859e-01) (local.get $p_h)))
1700
+ (local.set $z_l (f64.add (f64.add
1701
+ (f64.mul (f64.const -7.02846165095275826516e-09) (local.get $p_h))
1702
+ (f64.mul (local.get $p_l) (f64.const 9.61796693925975554329e-01)))
1703
+ (local.get $dp_l_k)))
1704
+ (local.set $t (f64.convert_i32_s (local.get $n)))
1705
+ (local.set $t1 (f64.add (f64.add (f64.add (local.get $z_h) (local.get $z_l)) (local.get $dp_h_k)) (local.get $t)))
1706
+ (local.set $t1 (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $t1)) (i64.const 0xffffffff00000000))))
1707
+ (local.set $t2 (f64.sub (local.get $z_l)
1708
+ (f64.sub (f64.sub (f64.sub (local.get $t1) (local.get $t)) (local.get $dp_h_k)) (local.get $z_h))))))
1709
+
1710
+ ;; Combine: (y1+y2)*(t1+t2) where y1 is y with its low 32 bits cleared, y2=y-y1 — a
1711
+ ;; double-double multiply of y against log2(x).
1712
+ (local.set $y1 (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $y)) (i64.const 0xffffffff00000000))))
1713
+ (local.set $p_l (f64.add (f64.mul (f64.sub (local.get $y) (local.get $y1)) (local.get $t1)) (f64.mul (local.get $y) (local.get $t2))))
1714
+ (local.set $p_h (f64.mul (local.get $y1) (local.get $t1)))
1715
+ (local.set $z (f64.add (local.get $p_l) (local.get $p_h)))
1716
+ (local.set $j (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $z)) (i64.const 32))))
1717
+ (local.set $i (i32.wrap_i64 (i64.reinterpret_f64 (local.get $z))))
1718
+
1719
+ (if (i32.ge_s (local.get $j) (i32.const 0x40900000))
1720
+ (then
1721
+ (if (i32.ne (i32.or (i32.sub (local.get $j) (i32.const 0x40900000)) (local.get $i)) (i32.const 0))
1722
+ (then (return (f64.const inf)))
1723
+ (else (if (f64.gt (f64.add (local.get $p_l) (f64.const 8.0085662595372944372e-17)) (f64.sub (local.get $z) (local.get $p_h)))
1724
+ (then (return (f64.const inf)))))))
1725
+ (else (if (i32.ge_u (i32.and (local.get $j) (i32.const 0x7fffffff)) (i32.const 0x4090cc00))
1726
+ (then
1727
+ (if (i32.ne (i32.or (i32.sub (local.get $j) (i32.const 0xc090cc00)) (local.get $i)) (i32.const 0))
1728
+ (then (return (f64.const 0.0)))
1729
+ (else (if (f64.le (local.get $p_l) (f64.sub (local.get $z) (local.get $p_h)))
1730
+ (then (return (f64.const 0.0))))))))))
1731
+
1732
+ ;; 2^(p_h+p_l): round to nearest integer n (bit trick, not float round, to keep the
1733
+ ;; fractional remainder's low bits exact), evaluate the P1..P5 kernel on it, splice n back
1734
+ ;; in as a raw exponent-field add.
1735
+ (local.set $i (i32.and (local.get $j) (i32.const 0x7fffffff)))
1736
+ (local.set $k (i32.sub (i32.shr_u (local.get $i) (i32.const 20)) (i32.const 0x3ff)))
1737
+ (local.set $n (i32.const 0))
1738
+ (if (i32.gt_u (local.get $i) (i32.const 0x3fe00000))
1739
+ (then
1740
+ (local.set $n (i32.add (local.get $j) (i32.shr_u (i32.const 0x00100000) (i32.add (local.get $k) (i32.const 1)))))
1741
+ (local.set $k (i32.sub (i32.shr_u (i32.and (local.get $n) (i32.const 0x7fffffff)) (i32.const 20)) (i32.const 0x3ff)))
1742
+ (local.set $t (f64.reinterpret_i64 (i64.shl
1743
+ (i64.extend_i32_u (i32.and (local.get $n) (i32.xor (i32.shr_u (i32.const 0x000fffff) (local.get $k)) (i32.const -1))))
1744
+ (i64.const 32))))
1745
+ (local.set $n (i32.shr_u (i32.or (i32.and (local.get $n) (i32.const 0x000fffff)) (i32.const 0x00100000)) (i32.sub (i32.const 20) (local.get $k))))
1746
+ (if (i32.lt_s (local.get $j) (i32.const 0)) (then (local.set $n (i32.sub (i32.const 0) (local.get $n)))))
1747
+ (local.set $p_h (f64.sub (local.get $p_h) (local.get $t)))))
1748
+ (local.set $t (f64.add (local.get $p_l) (local.get $p_h)))
1749
+ (local.set $t (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $t)) (i64.const 0xffffffff00000000))))
1750
+ (local.set $u (f64.mul (local.get $t) (f64.const 6.93147182464599609375e-01)))
1751
+ (local.set $v (f64.add (f64.mul (f64.sub (local.get $p_l) (f64.sub (local.get $t) (local.get $p_h))) (f64.const 6.93147180559945286227e-01))
1752
+ (f64.mul (local.get $t) (f64.const -1.90465429995776804525e-09))))
1753
+ (local.set $z (f64.add (local.get $u) (local.get $v)))
1754
+ (local.set $w (f64.sub (local.get $v) (f64.sub (local.get $z) (local.get $u))))
1755
+ (local.set $t (f64.mul (local.get $z) (local.get $z)))
1756
+ (local.set $t1 (f64.sub (local.get $z) (f64.mul (local.get $t)
1757
+ (f64.add (f64.const 1.66666666666666019037e-01) (f64.mul (local.get $t)
1758
+ (f64.add (f64.const -2.77777777770155933842e-03) (f64.mul (local.get $t)
1759
+ (f64.add (f64.const 6.61375632143793436117e-05) (f64.mul (local.get $t)
1760
+ (f64.add (f64.const -1.65339022054652515390e-06) (f64.mul (local.get $t) (f64.const 4.13813679705723846039e-08))))))))))))
1761
+ (local.set $r (f64.sub
1762
+ (f64.div (f64.mul (local.get $z) (local.get $t1)) (f64.sub (local.get $t1) (f64.const 2.0)))
1763
+ (f64.add (local.get $w) (f64.mul (local.get $z) (local.get $w)))))
1764
+ (local.set $z (f64.sub (f64.const 1.0) (f64.sub (local.get $r) (local.get $z))))
1765
+ (local.set $j (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $z)) (i64.const 32))))
1766
+ (local.set $j (i32.add (local.get $j) (i32.shl (local.get $n) (i32.const 20))))
1767
+ (if (result f64) (i32.le_s (i32.shr_s (local.get $j) (i32.const 20)) (i32.const 0))
1768
+ (then (call $math.pow_scalbn (local.get $z) (local.get $n)))
1769
+ (else (f64.reinterpret_i64 (i64.or (i64.and (i64.reinterpret_f64 (local.get $z)) (i64.const 0x00000000ffffffff))
1770
+ (i64.shl (i64.extend_i32_u (local.get $j)) (i64.const 32)))))))`,
1771
+ crPow ? ['math.pow_transcend'] : ['math.pow_scalbn'])
1772
+
1773
+ // $math.pow_fold — Math.pow(x, C) for a COMPILE-TIME-CONSTANT non-integer exponent C under
1774
+ // optimize.crPow (module/math.js's emitPow const-exponent fold, and its SIMD twin
1775
+ // $math.pow_fold_v above / src/optimize/vectorize.js's PPC_CALL2 entry) — see the authoritative
1776
+ // comment above emitPow for the flag's full semantics. Off crPow, emitPow lowers the same
1777
+ // constant-exponent case to exp(c·log(x)) directly (no separate wat function); this one is
1778
+ // registered ONLY when crPow is on. Shares $math.pow_transcend's kernel with $math.pow_core —
1779
+ // see that function's comment for the algorithm; c needs no hi/lo pre-split (the kernel's
1780
+ // multiply is twoProd-based, Dekker-splitting both operands internally). Bypasses the
1781
+ // $math.pow wrapper's special-case ladder, so it replicates only the x-dependent slice of it
1782
+ // here (NaN/±Inf/±0/x<0) — the y-dependent branches (y==0/NaN/±Inf/±1, integer y) are ALL
1783
+ // statically dead, since emitPow only reaches this fold when c is a finite literal that is
1784
+ // none of those. x==1 needs no case either: log2(1) evaluates to exactly 0 (dd) for any c
1785
+ // (verified by the differential test), so the result is exactly 1.0.
1786
+ if (crPow) {
1787
+ wat('math.pow_fold', `(func $math.pow_fold (param $x f64) (param $c f64) (result f64)
1788
+ ;; NaN propagates (return x itself, preserving payload bits — same as $math.pow's own
1789
+ ;; NaN checks: no arithmetic runs, so nothing mints a non-canonical NaN).
1790
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
1791
+ ;; |x| == Infinity: magnitude is c>0 ? Inf : 0, UNSIGNED — c is never an odd integer here
1792
+ ;; (emitPow's guard excludes every integer c), so the sign never flips, matching
1793
+ ;; Math.pow(±Infinity, non-integer c) exactly.
1794
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf))
1795
+ (then (return (select (f64.const inf) (f64.const 0.0) (f64.gt (local.get $c) (f64.const 0.0))))))
1796
+ ;; x == ±0: the reciprocal selection (c<0 ? Inf : 0), also unsigned for the same reason.
1797
+ (if (f64.eq (local.get $x) (f64.const 0.0))
1798
+ (then (return (select (f64.const inf) (f64.const 0.0) (f64.lt (local.get $c) (f64.const 0.0))))))
1799
+ ;; x < 0 with non-integer c → NaN (matches $math.pow's own x<0 branch).
1800
+ (if (f64.lt (local.get $x) (f64.const 0.0)) (then (return (f64.const nan))))
1801
+ (call $math.pow_transcend (local.get $x) (local.get $c)))`, ['math.pow_transcend'])
1802
+ } // if (crPow)
937
1803
 
938
1804
  // fdlibm atan: 4-region argument reduction onto |r| ≤ tan(π/16), then an
939
1805
  // 11-term odd polynomial split into even/odd parts. Accurate to <1 ulp —
940
1806
  // the old Taylor series was ~2e-6 off near |x|=0.5. Drives asin/acos/atan2.
1807
+ // Fast atan: sign symmetry (work on |x|), two-stage reduction onto [0, tan(π/8)] — |x|>1 →
1808
+ // π/2−atan(1/x), then t>tan(π/8) → π/8+atan((t−C)/(1+Ct)) — then a degree-5 minimax t·P(t²).
1809
+ // Replaces the fdlibm 4-way / 11-term / extended-precision form (correctly-rounded but ~3× the
1810
+ // ops). Max rel err 6e-10 over all of ℝ — well within jz's ~1e-9 transcendental budget. asin =
1811
+ // atan(x/√(1−x²)) and acos = π/2−asin inherit it, so all three drop from ~1.6–2.3× to under V8.
941
1812
  wat('math.atan', `(func $math.atan (param $x f64) (result f64)
942
- (local $abs_x f64) (local $id i32) (local $r f64) (local $z f64) (local $w f64)
943
- (local $s1 f64) (local $s2 f64) (local $ahi f64) (local $alo f64) (local $res f64)
944
- ;; NaN passes through unchanged.
1813
+ (local $t f64) (local $u f64) (local $r f64) (local $off f64) (local $flip i32)
1814
+ ;; NaN passes through; ±0 returns x (preserves sign of zero); ±Inf flows through (1/Inf=0 π/2).
945
1815
  (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
946
- (local.set $abs_x (f64.abs (local.get $x)))
947
- ;; |x| >= 2^66: atan saturates to ±π/2.
948
- (if (f64.ge (local.get $abs_x) (f64.const 7.378697629483821e19))
949
- (then (return (f64.copysign (f64.const 1.5707963267948966) (local.get $x)))))
950
- (if (f64.lt (local.get $abs_x) (f64.const 0.4375))
1816
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
1817
+ (local.set $t (f64.abs (local.get $x)))
1818
+ (local.set $off (f64.const 0.0))
1819
+ (local.set $flip (i32.const 0))
1820
+ (if (f64.gt (local.get $t) (f64.const 1.0))
1821
+ (then (local.set $t (f64.div (f64.const 1.0) (local.get $t))) (local.set $flip (i32.const 1))))
1822
+ (if (f64.gt (local.get $t) (f64.const 0.41421356237309503))
951
1823
  (then
952
- ;; |x| < 2^-27: atan(x) ≈ x (also preserves sign of zero).
953
- (if (f64.lt (local.get $abs_x) (f64.const 7.450580596923828e-9))
954
- (then (return (local.get $x))))
955
- (local.set $id (i32.const -1))
956
- (local.set $r (local.get $x)))
957
- (else
958
- (local.set $r (local.get $abs_x))
959
- (if (f64.lt (local.get $abs_x) (f64.const 1.1875))
960
- (then
961
- (if (f64.lt (local.get $abs_x) (f64.const 0.6875))
962
- (then ;; id=0: r = (2x-1)/(2+x)
963
- (local.set $id (i32.const 0))
964
- (local.set $r (f64.div (f64.sub (f64.mul (f64.const 2.0) (local.get $r)) (f64.const 1.0))
965
- (f64.add (f64.const 2.0) (local.get $r)))))
966
- (else ;; id=1: r = (x-1)/(x+1)
967
- (local.set $id (i32.const 1))
968
- (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.0))
969
- (f64.add (local.get $r) (f64.const 1.0)))))))
970
- (else
971
- (if (f64.lt (local.get $abs_x) (f64.const 2.4375))
972
- (then ;; id=2: r = (x-1.5)/(1+1.5x)
973
- (local.set $id (i32.const 2))
974
- (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.5))
975
- (f64.add (f64.const 1.0) (f64.mul (f64.const 1.5) (local.get $r))))))
976
- (else ;; id=3: r = -1/x
977
- (local.set $id (i32.const 3))
978
- (local.set $r (f64.div (f64.const -1.0) (local.get $r)))))))))
979
- (local.set $z (f64.mul (local.get $r) (local.get $r)))
980
- (local.set $w (f64.mul (local.get $z) (local.get $z)))
981
- (local.set $s1 (f64.mul (local.get $z)
982
- (f64.add (f64.const 0.3333333333333293)
983
- (f64.mul (local.get $w) (f64.add (f64.const 0.14285714272503466)
984
- (f64.mul (local.get $w) (f64.add (f64.const 0.09090887133436507)
985
- (f64.mul (local.get $w) (f64.add (f64.const 0.06661073137387531)
986
- (f64.mul (local.get $w) (f64.add (f64.const 0.049768779946159324)
987
- (f64.mul (local.get $w) (f64.const 0.016285820115365782)))))))))))))
988
- (local.set $s2 (f64.mul (local.get $w)
989
- (f64.add (f64.const -0.19999999999876483)
990
- (f64.mul (local.get $w) (f64.add (f64.const -0.11111110405462356)
991
- (f64.mul (local.get $w) (f64.add (f64.const -0.0769187620504483)
992
- (f64.mul (local.get $w) (f64.add (f64.const -0.058335701337905735)
993
- (f64.mul (local.get $w) (f64.const -0.036531572744216916)))))))))))
994
- ;; |x| < 0.4375: result = r - r*(s1+s2), sign carried by r itself.
995
- (if (i32.lt_s (local.get $id) (i32.const 0))
996
- (then (return (f64.sub (local.get $r) (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2)))))))
997
- ;; Reconstruct: z = atanhi[id] - ((r*(s1+s2) - atanlo[id]) - r), sign of x.
998
- (if (i32.eq (local.get $id) (i32.const 0))
999
- (then (local.set $ahi (f64.const 0.4636476090008061)) (local.set $alo (f64.const 2.2698777452961687e-17)))
1000
- (else (if (i32.eq (local.get $id) (i32.const 1))
1001
- (then (local.set $ahi (f64.const 0.7853981633974483)) (local.set $alo (f64.const 3.061616997868383e-17)))
1002
- (else (if (i32.eq (local.get $id) (i32.const 2))
1003
- (then (local.set $ahi (f64.const 0.982793723247329)) (local.set $alo (f64.const 1.3903311031230998e-17)))
1004
- (else (local.set $ahi (f64.const 1.5707963267948966)) (local.set $alo (f64.const 6.123233995736766e-17))))))))
1005
- (local.set $res (f64.sub (local.get $ahi)
1006
- (f64.sub (f64.sub (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2))) (local.get $alo))
1007
- (local.get $r))))
1008
- (f64.copysign (local.get $res) (local.get $x)))`)
1824
+ (local.set $t (f64.div (f64.sub (local.get $t) (f64.const 0.41421356237309503))
1825
+ (f64.add (f64.const 1.0) (f64.mul (f64.const 0.41421356237309503) (local.get $t)))))
1826
+ (local.set $off (f64.const 0.39269908169872414))))
1827
+ (local.set $u (f64.mul (local.get $t) (local.get $t)))
1828
+ (local.set $r (f64.add (local.get $off)
1829
+ (f64.mul (local.get $t)
1830
+ (f64.add (f64.const 0.99999999939667072)
1831
+ (f64.mul (local.get $u)
1832
+ (f64.add (f64.const -0.33333307625846248)
1833
+ (f64.mul (local.get $u)
1834
+ (f64.add (f64.const 0.19998216947828790)
1835
+ (f64.mul (local.get $u)
1836
+ (f64.add (f64.const -0.14240083011830104)
1837
+ (f64.mul (local.get $u)
1838
+ (f64.add (f64.const 0.10573479828448784)
1839
+ (f64.mul (local.get $u) (f64.const -0.060347904072425573))))))))))))))
1840
+ (if (local.get $flip) (then (local.set $r (f64.sub (f64.const 1.5707963267948966) (local.get $r)))))
1841
+ (f64.copysign (local.get $r) (local.get $x)))`)
1009
1842
 
1843
+ // Fast asin: small-argument poly a + a·u·R(u) (u=a²) with the standard half-angle reduction for
1844
+ // |x|>0.5 — a = sqrt((1−|x|)/2) maps the singular end to the smooth domain, asin = π/2 − 2·poly.
1845
+ // One sqrt only on the upper half, no atan/div. R is a degree-6 minimax on [0,0.25]; max rel err
1846
+ // ~2.6e-10. Replaces asin = atan(x/√(1−x²)) (which paid a div + atan's own reductions).
1010
1847
  wat('math.asin', `(func $math.asin (param $x f64) (result f64)
1011
- ;; Domain is [-1, 1]; outside it (including ±Infinity), Math.asin returns NaN.
1012
- ;; sin/cos output is clamped to [-1, 1] by sin_core/cos_core, so no tolerance needed here.
1013
- (if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 1.0))
1014
- (then (f64.const nan))
1015
- (else (call $math.atan (f64.div (local.get $x)
1016
- (f64.sqrt (f64.sub (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))))`)
1848
+ (local $ax f64) (local $a f64) (local $u f64) (local $r f64)
1849
+ ;; |x|>1 NaN (covers ±Inf); NaN propagates (the >1 test is false, poly carries NaN through).
1850
+ (if (f64.gt (f64.abs (local.get $x)) (f64.const 1.0)) (then (return (f64.const nan))))
1851
+ (local.set $ax (f64.abs (local.get $x)))
1852
+ (if (f64.le (local.get $ax) (f64.const 0.5))
1853
+ (then (local.set $a (local.get $ax)))
1854
+ (else (local.set $a (f64.sqrt (f64.mul (f64.const 0.5) (f64.sub (f64.const 1.0) (local.get $ax)))))))
1855
+ (local.set $u (f64.mul (local.get $a) (local.get $a)))
1856
+ (local.set $r (f64.add (local.get $a) (f64.mul (f64.mul (local.get $a) (local.get $u))
1857
+ (f64.add (f64.const 0.16666666715486264)
1858
+ (f64.mul (local.get $u)
1859
+ (f64.add (f64.const 0.074999892151409259)
1860
+ (f64.mul (local.get $u)
1861
+ (f64.add (f64.const 0.044648555271317079)
1862
+ (f64.mul (local.get $u)
1863
+ (f64.add (f64.const 0.030259196387355945)
1864
+ (f64.mul (local.get $u)
1865
+ (f64.add (f64.const 0.023661273034955098)
1866
+ (f64.mul (local.get $u)
1867
+ (f64.add (f64.const 0.010472588920432560)
1868
+ (f64.mul (local.get $u) (f64.const 0.031028862087420162))))))))))))))))
1869
+ (if (f64.gt (local.get $ax) (f64.const 0.5))
1870
+ (then (local.set $r (f64.sub (f64.const 1.5707963267948966) (f64.mul (f64.const 2.0) (local.get $r))))))
1871
+ (f64.copysign (local.get $r) (local.get $x)))`)
1017
1872
 
1018
1873
  wat('math.acos', `(func $math.acos (param $x f64) (result f64)
1019
1874
  (f64.sub (f64.const ${HALF_PI}) (call $math.asin (local.get $x))))`)
@@ -1080,19 +1935,51 @@ export default (ctx) => {
1080
1935
  (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
1081
1936
  (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))))))`)
1082
1937
 
1938
+ // Bit-hack initial guess (divide the IEEE exponent by 3 via an integer divide of the raw bits,
1939
+ // plus a magic bias) then 3 Newton steps t = (2t + a/t²)/3 — quadratic convergence, max rel err
1940
+ // ~1e-12 over the whole f64 range. Replaces the old `pow(x,1/3)` seed: no exp/log call, ~3-4×
1941
+ // faster (the colorconv / Oklab hot path is 3 cbrt per pixel), and a program whose only
1942
+ // transcendental is cbrt no longer pulls the pow/exp/log stdlib. Not bit-identical to V8's fdlibm
1943
+ // cbrt (neither was the pow form) — jz's transcendentals are fast minimax/Newton approximations.
1083
1944
  wat('math.cbrt', `(func $math.cbrt (param $x f64) (result f64)
1084
- (local $y f64)
1085
- ;; ±Infinity and NaN pass through; preserve sign of zero.
1945
+ (local $a f64) (local $t f64) (local $s f64)
1946
+ ;; NaN / ±Infinity / ±0 pass through unchanged (sign of zero preserved).
1086
1947
  (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
1087
1948
  (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
1088
- (if (result f64) (f64.lt (local.get $x) (f64.const 0.0))
1089
- (then (f64.neg (call $math.cbrt (f64.neg (local.get $x)))))
1090
- (else
1091
- ;; Initial guess via pow, then Newton-Raphson: y = (2y + x/y²)/3
1092
- (local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
1093
- (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)))
1094
- (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)))
1095
- (local.get $y))))`)
1949
+ (local.set $a (f64.abs (local.get $x)))
1950
+ (local.set $s (f64.const 1.0))
1951
+ ;; subnormal |x| < 2^-1022: scale up by 2^60 so the exponent split is valid; cbrt(2^60) = 2^20.
1952
+ (if (f64.lt (local.get $a) (f64.const 2.2250738585072014e-308))
1953
+ (then (local.set $a (f64.mul (local.get $a) (f64.const 1152921504606846976.0)))
1954
+ (local.set $s (f64.const 9.5367431640625e-07))))
1955
+ (local.set $t (f64.reinterpret_i64
1956
+ (i64.add (i64.div_u (i64.reinterpret_f64 (local.get $a)) (i64.const 3)) (i64.const 0x2A9F7893BF800000))))
1957
+ (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)))
1958
+ (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)))
1959
+ (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)))
1960
+ (local.set $t (f64.mul (local.get $t) (local.get $s)))
1961
+ (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $t))) (else (local.get $t))))`)
1962
+
1963
+ // Fifth root of v ≥ 0 — same bit-hack seed (÷5 of the raw bits) + 3 Newton steps t=(4t+v/t⁴)/5.
1964
+ // Caller (constant-exponent pow with denominator 5, e.g. the sRGB 2.4 gamma) guarantees v ≥ 0.
1965
+ wat('math.fifthroot', `(func $math.fifthroot (param $v f64) (result f64)
1966
+ (local $t f64) (local $s f64) (local $q f64)
1967
+ (if (i32.eqz (call $math.isFinite (local.get $v))) (then (return (local.get $v))))
1968
+ (if (f64.eq (local.get $v) (f64.const 0.0)) (then (return (f64.const 0.0))))
1969
+ (local.set $s (f64.const 1.0))
1970
+ ;; subnormal: scale by 2^100 = (2^20)^5; fifthroot(2^100) = 2^20
1971
+ (if (f64.lt (local.get $v) (f64.const 2.2250738585072014e-308))
1972
+ (then (local.set $v (f64.mul (local.get $v) (f64.const 1.2676506002282294e30)))
1973
+ (local.set $s (f64.const 9.5367431640625e-07))))
1974
+ (local.set $t (f64.reinterpret_i64
1975
+ (i64.add (i64.div_u (i64.reinterpret_f64 (local.get $v)) (i64.const 5)) (i64.const 0x3325E66666666800))))
1976
+ (local.set $q (f64.mul (local.get $t) (local.get $t)))
1977
+ (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)))
1978
+ (local.set $q (f64.mul (local.get $t) (local.get $t)))
1979
+ (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)))
1980
+ (local.set $q (f64.mul (local.get $t) (local.get $t)))
1981
+ (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)))
1982
+ (f64.mul (local.get $t) (local.get $s)))`)
1096
1983
 
1097
1984
  // Small finite-test helper (NaN→0, ±Inf→0, finite→1). Used by transcendental
1098
1985
  // functions that need to short-circuit on infinite inputs.