mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(spec): bit-exact PCG port of Go math/rand/v2
This commit is contained in:
1 parent
307213ef45
commit
231f96f5a5
1 file changed
+96
@@ -0,0 +1,96 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user