// Bit-exact port of Go's math/rand/v2 PCG source wrapped in rand.Rand. // // This MUST match `rand.New(rand.NewPCG(hi, lo))` draw-for-draw, because every // random decision the action picker makes is shared between the goja verifier // and the V8 web runtime. Divergence here breaks reproducibility across engines. // // The golden fixture in test/fixtures/pcg-golden.json is generated by // internal/_oracle/main.go and pins the exact sequences asserted in pcg.test.ts. const MASK64 = (1n << 64n) - 1n; const MUL_HI = 2549297995355413924n; const MUL_LO = 4865540595714422341n; const INC_HI = 6364136223846793005n; const INC_LO = 1442695040888963407n; const CHEAP_MUL = 0xda942042e4dd58b5n; const POW53 = 1n << 53n; // mul64 returns the 128-bit product of two uint64 values split into high and // low 64-bit halves, matching Go's math/bits.Mul64. function mul64(a: bigint, b: bigint): { hi: bigint; lo: bigint } { const product = a * b; return { hi: (product >> 64n) & MASK64, lo: product & MASK64 }; } export class Pcg { private hi: bigint; private lo: bigint; constructor(hi: bigint, lo: bigint) { this.hi = hi & MASK64; this.lo = lo & MASK64; } // next advances the 128-bit LCG state and returns the new (hi, lo) pair. private next(): { hi: bigint; lo: bigint } { // 128-bit multiply of state by the 128-bit multiplier, keeping 128 bits. let { hi, lo } = mul64(this.lo, MUL_LO); hi = (hi + this.hi * MUL_LO + this.lo * MUL_HI) & MASK64; // 128-bit add of the increment with carry propagation. const loSum = lo + INC_LO; const carry = loSum >> 64n; lo = loSum & MASK64; hi = (hi + INC_HI + carry) & MASK64; this.lo = lo; this.hi = hi; return { hi, lo }; } // uint64 mirrors PCG.Uint64: the DXSM output permutation over next(). uint64(): bigint { let { hi, lo } = this.next(); hi ^= hi >> 32n; hi = (hi * CHEAP_MUL) & MASK64; hi ^= hi >> 48n; hi = (hi * (lo | 1n)) & MASK64; return hi & MASK64; } // float64 mirrors rand.Rand.Float64: float64(Uint64()<<11>>11) / (1<<53), // which keeps the low 53 bits and divides by 2^53. float64(): number { const bits = this.uint64() & (POW53 - 1n); return Number(bits) / Number(POW53); } // intN mirrors rand.Rand.IntN via uint64n (Lemire with a rejection threshold). // The host arch is 64-bit, so the 32-bit fast path never applies. intN(n: number | bigint): number { const bound = typeof n === "bigint" ? n : BigInt(n); if (bound <= 0n) { throw new Error("invalid argument to intN"); } return Number(this.uint64n(bound)); } private uint64n(n: bigint): bigint { if ((n & (n - 1n)) === 0n) { // n is a power of two: mask the low bits. return this.uint64() & (n - 1n); } let { hi, lo } = mul64(this.uint64(), n); if (lo < n) { // Go computes thresh := -n % n in uint64 math, where -n wraps to 2^64 - n. const threshold = ((1n << 64n) - n) % n; while (lo < threshold) { ({ hi, lo } = mul64(this.uint64(), n)); } } return hi; } }