Skip to content

feat(random/unstable): allow generating seeded random bytes and 53-bit-entropy floats in [0, 1) (add getRandomValuesSeeded and nextFloat64) #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

Merged
merged 15 commits into from
May 27, 2025
Merged
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
210 changes: 134 additions & 76 deletions random/_pcg32.ts
Original file line number Diff line number Diff line change
@@ -1,100 +1,158 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// 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;
/** Initial increment for the PCG32 algorithm. Only used during seeding. */
const INC: bigint = 11634580027462260723n;
import { platform } from "./_platform.ts";
import { seedBytesFromUint64 } from "./_seed_bytes_from_uint64.ts";
import type { IntegerTypedArray } from "./_types.ts";

// 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
const b4 = new Uint8Array(4);
const dv4 = new DataView(b4.buffer);

/**
* Internal state for the PCG32 algorithm.
* `state` prop is mutated by each step, whereas `inc` prop remains constant.
*/
type PcgMutableState = {
state: bigint;
inc: bigint;
};
abstract class Prng32 {
/** Generates a pseudo-random 32-bit unsigned integer. */
abstract nextUint32(): number;

/**
* 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);
}
/**
* Mutates the provided typed array with pseudo-random values.
* @returns The same typed array, now populated with random values.
*/
getRandomValues<T extends IntegerTypedArray>(arr: T): T {
const { buffer, byteLength, byteOffset } = arr;
const rem = byteLength % 4;
const cutoffLen = byteLength - rem;

/**
* Mutates `pcg` by advancing `pcg.state`.
*/
function step(pgc: PcgMutableState) {
pgc.state = BigInt.asUintN(64, pgc.state * MUL + (pgc.inc | 1n));
}
const dv = new DataView(buffer, byteOffset, byteLength);
for (let i = 0; i < cutoffLen; i += 4) {
dv.setUint32(i, this.nextUint32(), true);
}

/**
* 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;
if (rem !== 0) {
dv4.setUint32(0, this.nextUint32(), true);
for (let i = 0; i < rem; ++i) {
dv.setUint8(cutoffLen + i, b4[i]!);
}
}

if (arr.BYTES_PER_ELEMENT !== 1 && !platform.littleEndian) {
const bits = arr.BYTES_PER_ELEMENT * 8;
const name = bits > 32
? `BigUint${bits as 64}` as const
: `Uint${bits as 16 | 32}` as const;
for (let i = 0; i < arr.length; ++i) {
const idx = i * arr.BYTES_PER_ELEMENT;
dv[`set${name}`](idx, dv[`get${name}`](idx, true) as never, false);
}
}

return arr;
}
}

/**
* 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.
* Modified from https://github.com/rust-random/rand/blob/f7bbcca/rand_pcg/src/pcg64.rs#L140-L153
*/
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 {
/** Multiplier for the PCG32 algorithm. */
// deno-lint-ignore deno-style-guide/naming-convention
static readonly MULTIPLIER = 6364136223846793005n;
// Constants are for 64-bit state, 32-bit output
// deno-lint-ignore deno-style-guide/naming-convention
static readonly ROTATE = 59n; // 64 - 5
// deno-lint-ignore deno-style-guide/naming-convention
static readonly XSHIFT = 18n; // (5 + 32) / 2
// deno-lint-ignore deno-style-guide/naming-convention
static readonly SPARE = 27n; // 64 - 32 - 5

// `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;
}
#state = new BigUint64Array(2);
get state() {
return this.#state[0]!;
}
protected set state(val) {
this.#state[0] = val;
}
get increment() {
return this.#state[1]!;
}
protected set increment(val) {
// https://www.pcg-random.org/posts/critiquing-pcg-streams.html#changing-the-increment
// > Increments have just one rule: they must be odd.
// We OR the increment with 1 upon setting to ensure this.
this.#state[1] = val | 1n;
}

/**
* 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);
/**
* Creates a new `Pcg32` instance with entropy generated from the seed.
* @param seed A 64-bit unsigned integer used to seed the generator.
*/
constructor(seed: bigint);
/**
* Creates a new `Pcg32` instance with the given `state` and `increment` values.
* @param state The current state of the generator.
* @param increment The increment value used in the generator.
*
* > [!NOTE]
* > It is typically better to use the constructor that takes a single `seed` value.
* > However, this constructor can be useful for resuming from a saved state.
*/
constructor({ state, increment }: { state: bigint; increment: bigint });
constructor(arg: bigint | { state: bigint; increment: bigint }) {
if (typeof arg === "bigint") {
return Pcg32.#seedFromUint64(arg);
}

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);
super();
this.state = arg.state;
this.increment = arg.increment;
}

for (let i = 0; i < Math.floor(numBytes / 4); ++i) {
new DataView(seed.buffer).setUint32(i * 4, nextU32(pgc), true);
/** @returns The next pseudo-random 32-bit integer. */
nextUint32(): number {
// Output function XSH RR: xorshift high (bits), followed by a random rotate
const rot = this.state >> Pcg32.ROTATE;
const xsh = BigInt.asUintN(
32,
(this.state >> Pcg32.XSHIFT ^ this.state) >> Pcg32.SPARE,
);
this.step();
return Number(this.#rotateRightUint32(xsh, rot));
}

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);
/** Mutates `pcg` by advancing `pcg.state`. */
step(): this {
this.state = this.state * Pcg32.MULTIPLIER + this.increment;
return this;
}

return seed;
// `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;
}

static #seedFromUint64(seed: bigint): Pcg32 {
return this.#fromSeed(seedBytesFromUint64(seed, new Uint8Array(16)));
}

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbcca/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),
);
}

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbcca/rand_pcg/src/pcg64.rs#L99-L105
*/
static #fromStateIncr(state: bigint, increment: bigint): Pcg32 {
// Move state away from initial value
return new Pcg32({ state: state + increment, increment }).step();
}
}
Loading
Loading