Skip to content

feat(random/unstable): allow generating seeded random bytes and 53-bit-entropy floats in [0, 1) #6626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 98 additions & 69 deletions random/_pcg32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,128 @@
// Based on Rust `rand` crate (https://github.com/rust-random/rand). Apache-2.0 + MIT license.

/** Multiplier for the PCG32 algorithm. */
const MUL: bigint = 6364136223846793005n;
const MUL = 6364136223846793005n;
/** Initial increment for the PCG32 algorithm. Only used during seeding. */
const INC: bigint = 11634580027462260723n;
const INC = 11634580027462260723n;

// Constants are for 64-bit state, 32-bit output
const ROTATE = 59n; // 64 - 5
const XSHIFT = 18n; // (5 + 32) / 2
const SPARE = 27n; // 64 - 32 - 5

/**
* Internal state for the PCG32 algorithm.
* `state` prop is mutated by each step, whereas `inc` prop remains constant.
*/
type PcgMutableState = {
state: bigint;
inc: bigint;
};
const b4 = new Uint8Array(4);
const dv4 = new DataView(b4.buffer);

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L129-L135
*/
export function fromSeed(seed: Uint8Array) {
const d = new DataView(seed.buffer);
return fromStateIncr(d.getBigUint64(0, true), d.getBigUint64(8, true) | 1n);
}
abstract class Prng32 {
/** Generates a pseudo-random 32-bit unsigned integer. */
abstract nextUint32(): number;

/**
* Mutates `pcg` by advancing `pcg.state`.
*/
function step(pgc: PcgMutableState) {
pgc.state = BigInt.asUintN(64, pgc.state * MUL + (pgc.inc | 1n));
}
/**
* Mutates the provided `Uint8Array` with pseudo-random values.
* @returns The same `Uint8Array`, now populated with random values.
*/
getRandomValues<T extends Uint8Array>(bytes: T): T {
const { buffer, byteLength, byteOffset } = bytes;
const rem = byteLength % 4;
const cutoffLen = byteLength - rem;

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L99-L105
*/
function fromStateIncr(state: bigint, inc: bigint): PcgMutableState {
const pcg: PcgMutableState = { state, inc };
// Move away from initial value
pcg.state = BigInt.asUintN(64, state + inc);
step(pcg);
return pcg;
const dv = new DataView(buffer, byteOffset, byteLength);
for (let i = 0; i < cutoffLen; i += 4) {
dv.setUint32(i, this.nextUint32(), true);
}

if (rem !== 0) {
dv4.setUint32(0, this.nextUint32(), true);
for (let i = 0; i < rem; ++i) {
dv.setUint8(cutoffLen + i, b4[i]!);
}
}

return bytes;
}
}

/**
* Internal PCG32 implementation, used by both the public seeded random
* function and the seed generation algorithm.
*
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L140-L153
*
* `pcg.state` is internally advanced by the function.
*
* @param pcg The state and increment values to use for the PCG32 algorithm.
* @returns The next pseudo-random 32-bit integer.
*/
export function nextU32(pcg: PcgMutableState): number {
const state = pcg.state;
step(pcg);
// Output function XSH RR: xorshift high (bits), followed by a random rotate
const rot = state >> ROTATE;
const xsh = BigInt.asUintN(32, (state >> XSHIFT ^ state) >> SPARE);
return Number(rotateRightU32(xsh, rot));
}
export class Pcg32 extends Prng32 {
state: bigint;
inc: bigint;

// `n`, `rot`, and return val are all u32
function rotateRightU32(n: bigint, rot: bigint): bigint {
const left = BigInt.asUintN(32, n << (-rot & 31n));
const right = n >> rot;
return left | right;
}
constructor(state: bigint, inc: bigint) {
super();
this.state = state;
this.inc = inc;
}

/**
* Convert a scalar bigint seed to a Uint8Array of the specified length.
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388
*/
export function seedFromU64(state: bigint, numBytes: number): Uint8Array {
const seed = new Uint8Array(numBytes);
/** @returns The next pseudo-random 32-bit integer. */
nextUint32(): number {
const state = this.state;
this.step();
// Output function XSH RR: xorshift high (bits), followed by a random rotate
const rot = state >> ROTATE;
const xsh = BigInt.asUintN(32, (state >> XSHIFT ^ state) >> SPARE);
return Number(this.#rotateRightUint32(xsh, rot));
}

const pgc: PcgMutableState = { state: BigInt.asUintN(64, state), inc: INC };
// We advance the state first (to get away from the input value,
// in case it has low Hamming Weight).
step(pgc);
/** Mutates `pcg` by advancing `pcg.state`. */
step(): this {
this.state = BigInt.asUintN(64, this.state * MUL + (this.inc | 1n));
return this;
}

for (let i = 0; i < Math.floor(numBytes / 4); ++i) {
new DataView(seed.buffer).setUint32(i * 4, nextU32(pgc), true);
// `n`, `rot`, and return val are all u32
#rotateRightUint32(n: bigint, rot: bigint): bigint {
const left = BigInt.asUintN(32, n << (-rot & 31n));
const right = n >> rot;
return left | right;
}

const rem = numBytes % 4;
if (rem) {
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, nextU32(pgc), true);
seed.set(bytes.subarray(0, rem), numBytes - rem);
/**
* Creates a new `Pcg32` instance with entropy generated from the given
* `seed`, treated as an unsigned 64-bit integer.
*/
static seedFromUint64(seed: bigint): Pcg32 {
return this.#fromSeed(seedBytesFromUint64(seed, new Uint8Array(16)));
}

return seed;
/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L129-L135
*/
static #fromSeed(seed: Uint8Array) {
const d = new DataView(seed.buffer);
return this.#fromStateIncr(
d.getBigUint64(0, true),
d.getBigUint64(8, true) | 1n,
);
}

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L99-L105
*/
static #fromStateIncr(state: bigint, inc: bigint): Pcg32 {
const pcg = new Pcg32(state, inc);
// Move away from initial value
pcg.state = BigInt.asUintN(64, state + inc);
pcg.step();
return pcg;
}
}

/**
* Write entropy generated from a scalar bigint seed into the provided Uint8Array, for use as a seed.
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388
*/
export function seedBytesFromUint64(
u64: bigint,
bytes: Uint8Array,
): Uint8Array {
return new Pcg32(BigInt.asUintN(64, u64), INC)
// We advance the state first (to get away from the input value,
// in case it has low Hamming Weight).
.step()
.getRandomValues(bytes);
}
Loading
Loading