UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

276 lines (275 loc) 13.9 kB
/** * # fair/detmath — vendored, bit-deterministic `exp` / `log` for the certified-grade path * * ## Why this file exists * A certified Grade must be **byte-identical across every runtime we grade on** — V8 on Node, * V8 on Cloudflare Workers, and JSC on Bun. JavaScript's `+ − × ÷` and `Math.sqrt` are pinned * by IEEE-754 (correctly-rounded binary64) and reproduce bit-for-bit everywhere. The *library* * transcendentals `Math.exp` and `Math.log` are **not** pinned — the ECMAScript spec explicitly * allows implementation-defined results, so V8 and JSC can (and do) return answers that differ * in the last ulp. On the certified-grade path that is a cross-runtime float divergence: two * engines could stamp two different grades on the same tape. * * This module removes that risk by **vendoring a fixed, portable double-precision `exp`/`log`** * (the SunPro / fdlibm `__ieee754_exp` and `__ieee754_log`, the same code musl and the BSDs * ship) reimplemented in terms of *only* the pinned IEEE-754 primitives. Because every operation * inside is a correctly-rounded binary64 op, the result is the same bits on every conforming * engine — which the checked-in cross-engine golden bit-vectors (`tests/detmath.golden.test.ts`) * assert. * * These are drop-in replacements for `Math.exp` / `Math.log` on the pricing + fill-hazard path * ({@link ../fair/black76.ts}, {@link ../fill/model.ts}). They are pure (no I/O, no clock, no * RNG — RUNTIME §0); the shared scratch buffer is single-threaded mutable state used only to read * a double's raw words, never observable output. * * ## Provenance * Ported from FreeBSD/SunPro `msun` `e_exp.c` and `e_log.c` (fdlibm). Original notice: * ``` * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. * Developed at SunPro, a Sun Microsystems, Inc. business. Permission to use, copy, modify, and * distribute this software is freely granted, provided that this notice is preserved. * ``` * The algorithm, constants and comments are Sun's; only the surface syntax is TypeScript. */ // ───────────────────────────────────────────────────────────────────────────── // Raw double ⇄ 32-bit word access (the fdlibm GET/SET_HIGH_WORD primitives) // // A single 8-byte scratch buffer aliased as a float64 and two uint32s lets us read/write the // high and low 32-bit halves of a double. `HI`/`LO` are resolved once from the platform's // endianness so the port is correct on both LE and (hypothetical) BE hosts — the words are what // matters, never their byte order. // ───────────────────────────────────────────────────────────────────────────── const _buf = new ArrayBuffer(8); const _f64 = new Float64Array(_buf); const _u32 = new Uint32Array(_buf); // 1.0 === 0x3FF0000000000000: the word holding 0x3ff00000 is the HIGH word. _f64[0] = 1.0; const HI = _u32[1] === 0x3ff00000 ? 1 : 0; const LO = 1 - HI; /** High 32 bits of `x` as an unsigned word (fdlibm `GET_HIGH_WORD`). */ function getHighWord(x) { _f64[0] = x; return _u32[HI] >>> 0; } /** Low 32 bits of `x` as an unsigned word (fdlibm `GET_LOW_WORD`). */ function getLowWord(x) { _f64[0] = x; return _u32[LO] >>> 0; } /** Return `x` with its high 32 bits replaced by `hw` (fdlibm `SET_HIGH_WORD`). */ function withHighWord(x, hw) { _f64[0] = x; _u32[HI] = hw >>> 0; return _f64[0]; } /** Compose a double from a high and low word (fdlibm `INSERT_WORDS`). */ function fromWords(hw, lw) { _u32[HI] = hw >>> 0; _u32[LO] = lw >>> 0; return _f64[0]; } /** * The one NaN this module is allowed to *return*: quiet NaN with a clear sign bit, built from its * bits rather than obtained from the hardware. * * fdlibm mints NaN by evaluating an invalid operation — `(x-x)/0`, `0/0`, `Inf-Inf` — and lets the * FPU hand back its default NaN. That is fine in C, where only NaN-*ness* is promised, and it is * **wrong here**: IEEE-754 does not specify the default NaN's sign bit, and the architectures we * grade on disagree. arm64 delivers `7ff8000000000000`; x86_64 delivers `fff8000000000000`. Both * are quiet NaNs, both satisfy `Number.isNaN`, and both are invisible to `===` — but this module's * whole contract is that its output is the *same bits* everywhere (the golden compares hex, not * values), so a sign bit that tracks the CPU is a cross-engine divergence on the certified-grade * path. It is the exact defect the vendoring exists to prevent, one level below the ulps. * * The JS `NaN` literal is no fix: its bit pattern is implementation-defined too (ECMAScript lets * an engine pick any NaN, observable precisely through the typed-array reads above), so `return NaN` * would trade one unpinned pattern for another. Only writing the words settles it. * * `7ff8000000000000` — the sign bit clear — is chosen because it is the pattern already recorded in * `tests/golden/detmath.golden.json`, and it is the canonical qNaN in the IEEE-754 sense. * * Do not "simplify" this away. Know which half CI defends: an arithmetic mint IS caught — the golden * goes red on x86_64. `return NaN` is NOT — every engine we currently test happens to materialize the * literal as `7ff8000000000000`, so CI would stay green. That is luck, not a guarantee: the paragraph * above is the reason, and it holds by spec reasoning and review, not by the golden. Writing the words * is the only form that does not rest on an engine choice we cannot pin. */ const CANONICAL_NAN = fromWords(0x7ff80000, 0x00000000); // ───────────────────────────────────────────────────────────────────────────── // exp — SunPro/fdlibm `__ieee754_exp` // // exp(x) is computed by argument reduction x = k·ln2 + r (|r| <= 0.5·ln2), a degree-5 minimax // remez polynomial for expm1 on r, then reassembly exp(x) = 2^k · exp(r). Accurate to < 1 ulp. // ───────────────────────────────────────────────────────────────────────────── const one = 1.0; const halF = [0.5, -0.5]; const o_threshold = 7.09782712893383973096e2; // 0x40862E42 0xFEFA39EF — overflow above this const u_threshold = -7.45133219101941108420e2; // 0xC0874910 0xD52D3051 — underflow below this const ln2HI = [6.93147180369123816490e-1, -6.93147180369123816490e-1]; // 0x3FE62E42 0xFEE00000 const ln2LO = [1.90821492927058770002e-10, -1.90821492927058770002e-10]; // 0x3DEA39EF 0x35793C76 const invln2 = 1.44269504088896341351; // 0x3FF71547 0x652B82FE const P1 = 1.66666666666666019037e-1; // 0x3FC55555 0x5555553E const P2 = -2.77777777770155933842e-3; // 0xBF66C16C 0x16BEBD93 const P3 = 6.61375632143793436117e-5; // 0x3F11566A 0xAF25DE2C const P4 = -1.65339022054652515390e-6; // 0xBEBBBD41 0xC5D26BF1 const P5 = 4.13813679705723846039e-8; // 0x3E663769 0x72BEA4D0 const huge = 1.0e300; const twom1000 = 9.33263618503218878990e-302; // 2**-1000 = 0x01700000 0x00000000 /** * Bit-deterministic `Math.exp`. Same value on every IEEE-754 engine (< 1 ulp of the true exp), * used everywhere `Math.exp` sat on the certified-grade path. Edge cases match the library: * `exp(NaN)=NaN`, `exp(+Inf)=+Inf`, `exp(-Inf)=0`, overflow → `+Inf`, underflow → `0`. */ export function dexp(x) { let hi = 0.0; let lo = 0.0; let k = 0; let hx = getHighWord(x); const xsb = (hx >>> 31) & 1; // sign bit of x hx &= 0x7fffffff; // high word of |x| // Filter out non-finite / overflow / underflow arguments. if (hx >= 0x40862e42) { // |x| >= 709.78... if (hx >= 0x7ff00000) { const lx = getLowWord(x); if (((hx & 0xfffff) | lx) !== 0) return x + x; // NaN return xsb === 0 ? x : 0.0; // exp(+-inf) = {inf, 0} } if (x > o_threshold) return huge * huge; // overflow → +Inf if (x < u_threshold) return twom1000 * twom1000; // underflow → 0 } // Argument reduction: x = k·ln2 + (hi - lo). if (hx > 0x3fd62e42) { // |x| > 0.5·ln2 if (hx < 0x3ff0a2b2) { // |x| < 1.5·ln2 hi = x - ln2HI[xsb]; lo = ln2LO[xsb]; k = 1 - xsb - xsb; } else { k = (invln2 * x + halF[xsb]) | 0; // truncate toward zero (C int cast) const t = k; hi = x - t * ln2HI[0]; // t·ln2HI is exact here lo = t * ln2LO[0]; } x = hi - lo; } else if (hx < 0x3e300000) { // |x| < 2**-28: exp(x) ≈ 1 + x if (huge + x > one) return one + x; } else { k = 0; } // x is now in the primary range [-0.5·ln2, 0.5·ln2]. const t = x * x; const c = x - t * (P1 + t * (P2 + t * (P3 + t * (P4 + t * P5)))); let y; if (k === 0) return one - ((x * c) / (c - 2.0) - x); y = one - ((lo - (x * c) / (2.0 - c)) - hi); // Reassemble exp(x) = 2^k · y by adding k to y's exponent word. if (k >= -1021) { const hy = getHighWord(y); return withHighWord(y, (hy + (k << 20)) >>> 0); } const hy = getHighWord(y); return withHighWord(y, (hy + ((k + 1000) << 20)) >>> 0) * twom1000; } // ───────────────────────────────────────────────────────────────────────────── // log — SunPro/fdlibm `__ieee754_log` // // log(x): reduce x = 2^k · (1 + f) with sqrt(2)/2 < 1+f < sqrt(2), then log(1+f) via the odd // series in s = f/(2+f). Accurate to < 1 ulp. // ───────────────────────────────────────────────────────────────────────────── const ln2_hi = 6.93147180369123816490e-1; // 0x3FE62E42 0xFEE00000 const ln2_lo = 1.90821492927058770002e-10; // 0x3DEA39EF 0x35793C76 const two54 = 1.80143985094819840000e16; // 0x43500000 0x00000000 const Lg1 = 6.666666666666735130e-1; // 0x3FE55555 0x55555593 const Lg2 = 3.999999999940941908e-1; // 0x3FD99999 0x9997FA04 const Lg3 = 2.857142874366239149e-1; // 0x3FD24924 0x94229359 const Lg4 = 2.222219843214978396e-1; // 0x3FCC71C5 0x1D8E78B0 const Lg5 = 1.818357216161805012e-1; // 0x3FC74664 0x96CB03DE const Lg6 = 1.531383769920937332e-1; // 0x3FC39A09 0xD078C69F const Lg7 = 1.479819860511658591e-1; // 0x3FC2F112 0xDF3E5244 const zero = 0.0; /** * Bit-deterministic `Math.log` (natural log). Same value on every IEEE-754 engine (< 1 ulp), * used everywhere `Math.log` sat on the certified-grade path. Edge cases match the library: * `log(+-0)=-Inf`, `log(x<0)=NaN`, `log(NaN)=NaN`, `log(+Inf)=+Inf`. */ export function dlog(x) { let hx = getHighWord(x) | 0; // signed high word const lx = getLowWord(x); let k = 0; if (hx < 0x00100000) { // x < 2**-1022 (subnormal, zero, or negative) if (((hx & 0x7fffffff) | lx) === 0) return -two54 / zero; // log(+-0) = -Inf // log(-#) = NaN. fdlibm writes `(x-x)/zero` here; we return the constructed qNaN instead, because // the hardware's default NaN has an architecture-defined sign bit. See {@link CANONICAL_NAN}. if (hx < 0) return CANONICAL_NAN; k -= 54; x *= two54; // scale subnormal up hx = getHighWord(x) | 0; } if (hx >= 0x7ff00000) return x + x; // +Inf or NaN k += (hx >> 20) - 1023; hx &= 0x000fffff; const i = (hx + 0x95f64) & 0x100000; x = withHighWord(x, (hx | (i ^ 0x3ff00000)) >>> 0); // normalize x or x/2 into [sqrt(1/2), sqrt(2)) k += i >> 20; const f = x - 1.0; if ((0x000fffff & (2 + hx)) < 3) { // -2**-20 <= f < 2**-20: f is tiny, use the short series. if (f === zero) { if (k === 0) return zero; const dk0 = k; return dk0 * ln2_hi + dk0 * ln2_lo; } const R0 = f * f * (0.5 - 0.33333333333333333 * f); if (k === 0) return f - R0; const dk1 = k; return dk1 * ln2_hi - (R0 - dk1 * ln2_lo - f); } const s = f / (2.0 + f); const dk = k; const z = s * s; let i2 = hx - 0x6147a; const w = z * z; const j = 0x6b851 - hx; const t1 = w * (Lg2 + w * (Lg4 + w * Lg6)); const t2 = z * (Lg1 + w * (Lg3 + w * (Lg5 + w * Lg7))); i2 |= j; const R = t2 + t1; if (i2 > 0) { const hfsq = 0.5 * f * f; if (k === 0) return f - (hfsq - s * (hfsq + R)); return dk * ln2_hi - (hfsq - (s * (hfsq + R) + dk * ln2_lo) - f); } if (k === 0) return f - s * (f - R); return dk * ln2_hi - (s * (f - R) - dk * ln2_lo - f); } /** Raw IEEE-754 bits of a double as a stable 16-hex-digit string (big-endian, high word first). * The canonical form the cross-engine golden bit-vectors are recorded and compared in — two * engines agree iff these strings are equal. */ export function f64ToHex(x) { const hi = getHighWord(x).toString(16).padStart(8, "0"); const lo = getLowWord(x).toString(16).padStart(8, "0"); return hi + lo; } /** Inverse of {@link f64ToHex}: reconstruct the double a golden hex string encodes. */ export function hexToF64(hex) { return fromWords(parseInt(hex.slice(0, 8), 16), parseInt(hex.slice(8, 16), 16)); }