diff --git a/random/_pcg32.ts b/random/_pcg32.ts index b963e6ab0712..6c0d62262e2a 100644 --- a/random/_pcg32.ts +++ b/random/_pcg32.ts @@ -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(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(); + } } diff --git a/random/_pcg32_test.ts b/random/_pcg32_test.ts index 949623b6901f..c2be8e0c5515 100644 --- a/random/_pcg32_test.ts +++ b/random/_pcg32_test.ts @@ -1,13 +1,17 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { assertEquals } from "../assert/equals.ts"; -import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.ts"; +import { assert, assertEquals, assertNotEquals } from "@std/assert"; +import { Pcg32 } from "./_pcg32.ts"; +import { seedBytesFromUint64 } from "./_seed_bytes_from_uint64.ts"; +import { nextFloat64 } from "./next_float_64.ts"; +import { mockLittleEndian } from "./_test_utils.ts"; +import { platform } from "./_platform.ts"; -Deno.test("seedFromU64() generates seeds from bigints", async (t) => { +Deno.test("seedBytesFromUint64() generates seeds from bigints", async (t) => { await t.step("first 10 16-bit seeds are same as rand crate", async (t) => { /** * Expected results obtained by copying the Rust code from - * https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388 + * https://github.com/rust-random/rand/blob/f7bbcca/rand_core/src/lib.rs#L359-L388 * but directly returning `seed` instead of `Self::from_seed(seed)` */ // deno-fmt-ignore @@ -26,7 +30,9 @@ Deno.test("seedFromU64() generates seeds from bigints", async (t) => { for (const [i, expected] of expectedResults.entries()) { await t.step(`With seed ${i}n`, () => { - const actual = Array.from(seedFromU64(BigInt(i), 16)); + const actual = Array.from( + seedBytesFromUint64(BigInt(i), new Uint8Array(16)), + ); assertEquals(actual, expected); }); } @@ -42,7 +48,9 @@ Deno.test("seedFromU64() generates seeds from bigints", async (t) => { const slice = expectedBytes.slice(0, i + 1); await t.step(`With length ${i + 1}`, () => { - const actual = Array.from(seedFromU64(1n, i + 1)); + const actual = Array.from( + seedBytesFromUint64(1n, new Uint8Array(i + 1)), + ); assertEquals(actual, slice); }); } @@ -53,51 +61,62 @@ Deno.test("seedFromU64() generates seeds from bigints", async (t) => { await t.step("wraps bigint input to u64", async (t) => { await t.step("exact multiple of U64_CEIL", () => { - const expected = Array.from(seedFromU64(BigInt(0n), 16)); - const actual = Array.from(seedFromU64(U64_CEIL * 99n, 16)); + const expected = Array.from( + seedBytesFromUint64(BigInt(0n), new Uint8Array(16)), + ); + const actual = Array.from( + seedBytesFromUint64(U64_CEIL * 99n, new Uint8Array(16)), + ); assertEquals(actual, expected); }); await t.step("multiple of U64_CEIL + 1", () => { - const expected = Array.from(seedFromU64(1n, 16)); - const actual = Array.from(seedFromU64(1n + U64_CEIL * 3n, 16)); + const expected = Array.from(seedBytesFromUint64(1n, new Uint8Array(16))); + const actual = Array.from( + seedBytesFromUint64(1n + U64_CEIL * 3n, new Uint8Array(16)), + ); assertEquals(actual, expected); }); await t.step("multiple of U64_CEIL - 1", () => { - const expected = Array.from(seedFromU64(-1n, 16)); - const actual = Array.from(seedFromU64(U64_CEIL - 1n, 16)); + const expected = Array.from(seedBytesFromUint64(-1n, new Uint8Array(16))); + const actual = Array.from( + seedBytesFromUint64(U64_CEIL - 1n, new Uint8Array(16)), + ); assertEquals(actual, expected); }); await t.step("negative multiple of U64_CEIL", () => { - const expected = Array.from(seedFromU64(0n, 16)); - const actual = Array.from(seedFromU64(U64_CEIL * -3n, 16)); + const expected = Array.from(seedBytesFromUint64(0n, new Uint8Array(16))); + const actual = Array.from( + seedBytesFromUint64(U64_CEIL * -3n, new Uint8Array(16)), + ); assertEquals(actual, expected); }); await t.step("negative multiple of U64_CEIL", () => { - const expected = Array.from(seedFromU64(0n, 16)); - const actual = Array.from(seedFromU64(U64_CEIL * -3n, 16)); + const expected = Array.from(seedBytesFromUint64(0n, new Uint8Array(16))); + const actual = Array.from( + seedBytesFromUint64(U64_CEIL * -3n, new Uint8Array(16)), + ); assertEquals(actual, expected); }); }); }); -Deno.test("nextU32() generates random 32-bit integers", async (t) => { +Deno.test("nextUint32() generates random 32-bit integers", () => { /** - * Expected results obtained from the Rust `rand` crate as follows: * ```rs * use rand_pcg::rand_core::{RngCore, SeedableRng}; * use rand_pcg::Lcg64Xsh32; * * let mut rng = Lcg64Xsh32::seed_from_u64(0); * for _ in 0..10 { - * println!("{}", rng.next_u32()); + * println!("{},", rng.next_u32()); * } * ``` */ - const expectedResults = [ + const rustRandSamples = [ 298703107, 4236525527, 336081875, @@ -110,13 +129,159 @@ Deno.test("nextU32() generates random 32-bit integers", async (t) => { 2362354238, ]; - const pgc = fromSeed(seedFromU64(0n, 16)); - const next = () => nextU32(pgc); + const pcg = new Pcg32(0n); + for (const sample of rustRandSamples) { + assertEquals(pcg.nextUint32(), sample); + } +}); - for (const [i, expected] of expectedResults.entries()) { - await t.step(`#${i + 1} generated uint32`, () => { - const actual = next(); - assertEquals(actual, expected); +Deno.test("getRandomValues() writes bytes", () => { + const pcg = new Pcg32(0n); + + const a = new Uint8Array(10); + const b = a.subarray(3, 8); + const c = pcg.getRandomValues(b); + + assert(b === c); + assertEquals(Array.from(b), [3, 217, 205, 17, 215]); + assertEquals(Array.from(a), [0, 0, 0, 3, 217, 205, 17, 215, 0, 0]); +}); + +Deno.test("getRandomValues() gives correct results for multi-byte typed arrays in both endiannesses", async (t) => { + for ( + const numberType of [ + "Int8", + "Int16", + "Int32", + "BigInt64", + "Uint8", + "Uint16", + "Uint32", + "BigUint64", + ] as const + ) { + await t.step(numberType, () => { + const platformLittleEndian = platform.littleEndian; + + const length = 10; + const TypedArray = + globalThis[`${numberType}Array`] as BigInt64ArrayConstructor; + const { BYTES_PER_ELEMENT } = TypedArray; + + const native = new Pcg32(0n).getRandomValues(new TypedArray(length)); + const u8 = new Pcg32(0n).getRandomValues( + new Uint8Array(length * BYTES_PER_ELEMENT), + ); + const dv = new DataView(u8.buffer); + const fromDv = TypedArray.from({ length }, (_, i) => { + return dv[`get${numberType}`](i * BYTES_PER_ELEMENT, true) as bigint; + }); + + assertEquals(native, fromDv); + + for (const littleEndian of [false, true]) { + using _ = mockLittleEndian(littleEndian); + const TypedArray = + globalThis[`${numberType}Array`] as BigInt64ArrayConstructor; + const mocked = new Pcg32(0n).getRandomValues(new TypedArray(length)); + + assertEquals(mocked, native); + if (BYTES_PER_ELEMENT > 1 && littleEndian !== platformLittleEndian) { + assertNotEquals(u8, new Uint8Array(mocked.buffer)); + } + } }); } }); + +Deno.test("nextFloat64() generates the same random numbers as rust rand crate", () => { + /** + * ```rs + * use rand::prelude::*; + * use rand_pcg::Lcg64Xsh32; + * fn main() -> () { + * let mut rng = Lcg64Xsh32::seed_from_u64(0); + * for _ in 0..10 { + * let val: f64 = rng.random(); + * println!("{val},"); + * } + * } + * ``` + */ + const rustRandSamples = [ + 0.986392965323652, + 0.24601264253217958, + 0.37644842389200484, + 0.6668384108033093, + 0.5500284577750535, + 0.027211583252904847, + 0.4610097964014602, + 0.24912787257622104, + 0.10493815385866834, + 0.4625920669083482, + ]; + + const pcg = new Pcg32(0n); + for (const sample of rustRandSamples) { + assertEquals(nextFloat64(pcg.getRandomValues.bind(pcg)), sample); + } +}); + +Deno.test("getRandomValues() can be used to generate the same arbitrary numeric types as rust rand crate", async (t) => { + await t.step("u8", () => { + /** + * ```rs + * use rand::prelude::*; + * use rand_pcg::Lcg64Xsh32; + * fn main() -> () { + * let mut rng = Lcg64Xsh32::seed_from_u64(0); + * for _ in 0..10 { + * let val: u8 = rng.random(); + * println!("{val},"); + * } + * } + * ``` + */ + const rustRandSamples = [3, 215, 211, 62, 155, 133, 142, 14, 192, 62]; + + const pcg = new Pcg32(0n); + for (const sample of rustRandSamples) { + const b = pcg.getRandomValues(new Uint8Array(1)); + assertEquals(b[0], sample); + } + }); + + await t.step("i64", () => { + /** + * ```rs + * use rand::prelude::*; + * use rand_pcg::Lcg64Xsh32; + * fn main() -> () { + * let mut rng = Lcg64Xsh32::seed_from_u64(0); + * for _ in 0..10 { + * let val: u64 = rng.random(); + * println!("{val}n,"); + * } + * } + * ``` + */ + const rustRandSamples = [ + -251005486276683517n, + 4538132255688111059n, + 6944247732487142299n, + -6145746571101709170n, + -8300509879875978816n, + 501965112106777777n, + 8504129729690683813n, + 4595598107041274030n, + 1935767267798412705n, + 8533317468786625891n, + ]; + + const pcg = new Pcg32(0n); + for (const sample of rustRandSamples) { + const b = pcg.getRandomValues(new Uint8Array(8)); + assertEquals(new DataView(b.buffer).getBigInt64(0, true), sample); + } + }); +}); diff --git a/random/_platform.ts b/random/_platform.ts new file mode 100644 index 000000000000..0cebef7f90cb --- /dev/null +++ b/random/_platform.ts @@ -0,0 +1,6 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +export const platform = { + // settable property for testing purposes + littleEndian: new Uint8Array(new Uint16Array([1]).buffer)[0] === 1, +}; diff --git a/random/_seed_bytes_from_uint64.ts b/random/_seed_bytes_from_uint64.ts new file mode 100644 index 000000000000..77985370db68 --- /dev/null +++ b/random/_seed_bytes_from_uint64.ts @@ -0,0 +1,20 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { Pcg32 } from "./_pcg32.ts"; + +/** Initial increment for the PCG32 algorithm. Only used during seeding. */ +const INITIAL_INCREMENT = 11634580027462260723n; + +/** + * 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/f7bbcca/rand_core/src/lib.rs#L359-L388 + */ +export function seedBytesFromUint64( + u64: bigint, + bytes: Uint8Array, +): Uint8Array { + return new Pcg32({ state: u64, increment: INITIAL_INCREMENT }) + // We advance the state first (to get away from the input value, + // in case it has low Hamming Weight). + .step() + .getRandomValues(bytes); +} diff --git a/random/_test_utils.ts b/random/_test_utils.ts new file mode 100644 index 000000000000..9045e6b6e90f --- /dev/null +++ b/random/_test_utils.ts @@ -0,0 +1,125 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// deno-lint-ignore-file no-explicit-any +import { platform } from "./_platform.ts"; + +/** + * ```ts + * type _NumberType = { [K in keyof DataView]: K extends `get${infer T}` ? T : never } + * type NumberTypeName = _NumberType[keyof _NumberType] + * ``` + */ +const numberTypes = [ + "Int8", + "Int16", + "Int32", + "BigInt64", + "Uint8", + "Uint16", + "Uint32", + "BigUint64", + "Float16", + "Float32", + "Float64", +] as const; + +export function mockLittleEndian(littleEndian: boolean) { + // partial `DisposableStack` polyfill + const stack = { + disposables: [] as (() => void)[], + defer(fn: () => void) { + this.disposables.push(fn); + }, + [Symbol.dispose]() { + for (let i = this.disposables.length - 1; i >= 0; --i) { + this.disposables[i]!(); + } + }, + }; + + const originalLittleEndian = platform.littleEndian; + + stack.defer(() => { + platform.littleEndian = originalLittleEndian; + }); + + for (const type of numberTypes) { + const TypedArray = globalThis[`${type}Array`]; + + const MockTypedArray = class extends TypedArray { + // @ts-ignore missing super() call + constructor(...args: any[]) { + const target = new TypedArray(...args); + const dv = new DataView( + target.buffer, + target.byteOffset, + target.byteLength, + ); + + const proxy: any = new Proxy(target, { + get(target, prop) { + if (prop === Symbol.iterator) { + return Array.prototype[Symbol.iterator]; + } + + if (typeof prop === "symbol" || /\D/.test(prop)) { + const val = Reflect.get(target, prop, target); + if (typeof val === "function") { + return val.bind(target); + } + return val; + } + + const i = Number(prop); + return dv[`get${type}`](i * target.BYTES_PER_ELEMENT, littleEndian); + }, + set(target, prop, val: number & bigint) { + if (typeof prop === "symbol" || /\D/.test(prop)) { + return Reflect.set(target, prop, val, target); + } + + const i = Number(prop); + dv[`set${type}`](i * target.BYTES_PER_ELEMENT, val, littleEndian); + return true; + }, + }); + + proxy[Symbol.for("nodejs.util.inspect.custom")] = ( + _: any, + args: any, + inspect: typeof Deno.inspect, + ) => { + return `${ + littleEndian ? "LE" : "BE" + }_${TypedArray.name}(${proxy.length}) ${inspect([...proxy], args)}`; + }; + + if ( + typeof args[0] === "object" && + (Symbol.iterator in args[0] || "length" in args[0]) + ) { + for (let i = 0; i < target.length; ++i) { + proxy[i] = target[i]; + } + } + + return proxy; + } + + static from(args: any[]) { + return new this(Array.from(args)); + } + }; + + // @ts-ignore mock + globalThis[`${type}Array`] = MockTypedArray; + + stack.defer(() => { + // @ts-ignore restore mock + globalThis[`${type}Array`] = TypedArray; + }); + } + + platform.littleEndian = littleEndian; + + return stack; +} diff --git a/random/_types.ts b/random/_types.ts index 10f5c1a26ce6..1f4846f3db0f 100644 --- a/random/_types.ts +++ b/random/_types.ts @@ -12,6 +12,27 @@ */ export type Prng = typeof Math.random; +/** An integer typed array */ +export type IntegerTypedArray = + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint16Array + | Uint32Array + | Uint8ClampedArray + | BigInt64Array + | BigUint64Array; + +/** + * A pseudo-random number generator implementing the same contract as + * `crypto.getRandomValues`, i.e. taking a typed array and mutating it by + * filling it with random bytes, returning the mutated typed array instance. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + */ +export type RandomValueGenerator = (array: T) => T; + /** * Options for random number generation. * diff --git a/random/deno.json b/random/deno.json index 711f03a40bb5..8a1efa31e7bf 100644 --- a/random/deno.json +++ b/random/deno.json @@ -4,7 +4,9 @@ "exports": { ".": "./mod.ts", "./between": "./between.ts", + "./get-random-values-seeded": "./get_random_values_seeded.ts", "./integer-between": "./integer_between.ts", + "./next-float-64": "./next_float_64.ts", "./sample": "./sample.ts", "./seeded": "./seeded.ts", "./shuffle": "./shuffle.ts" diff --git a/random/get_random_values_seeded.ts b/random/get_random_values_seeded.ts new file mode 100644 index 000000000000..09ff3c7ece4c --- /dev/null +++ b/random/get_random_values_seeded.ts @@ -0,0 +1,32 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { Pcg32 } from "./_pcg32.ts"; +import type { RandomValueGenerator } from "./_types.ts"; +export type { IntegerTypedArray, RandomValueGenerator } from "./_types.ts"; + +/** + * Creates a pseudo-random value generator that populates typed arrays, + * based on the given seed. The algorithm used for generation is + * {@link https://www.pcg-random.org/download.html | PCG32}. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @param seed The seed used to initialize the random number generator's state. + * @returns A pseudo-random value generator function, which will generate + * different values on each call. + * + * @example Usage + * ```ts + * import { getRandomValuesSeeded } from "@std/random"; + * import { assertEquals } from "@std/assert"; + * + * const getRandomValues = getRandomValuesSeeded(1n); + * assertEquals(getRandomValues(new Uint8Array(5)), new Uint8Array([230, 11, 167, 51, 238])); + * ``` + */ +export function getRandomValuesSeeded( + seed: bigint, +): RandomValueGenerator { + const pcg = new Pcg32(seed); + return pcg.getRandomValues.bind(pcg); +} diff --git a/random/mod.ts b/random/mod.ts index b78ec5e3d8e4..822deee481af 100644 --- a/random/mod.ts +++ b/random/mod.ts @@ -21,6 +21,8 @@ export * from "./between.ts"; export * from "./integer_between.ts"; +export * from "./get_random_values_seeded.ts"; +export * from "./next_float_64.ts"; export * from "./sample.ts"; export * from "./seeded.ts"; export * from "./shuffle.ts"; diff --git a/random/next_float_64.ts b/random/next_float_64.ts new file mode 100644 index 000000000000..738216f922de --- /dev/null +++ b/random/next_float_64.ts @@ -0,0 +1,46 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import type { RandomValueGenerator } from "./_types.ts"; + +const b8 = new Uint8Array(8); +const dv8 = new DataView(b8.buffer); + +// 0x1.0p-53 +const FLOAT_64_MULTIPLIER = 2 ** -53; +// assert(1 / FLOAT_64_MULTIPLIER === Number.MAX_SAFE_INTEGER + 1) + +/** + * Get a float64 in the range `[0, 1)` from a random value generator. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @param getRandomValues A function that fills a typed array with random values. + * @returns A float64 in the range `[0, 1)`. + * + * @example With a seeded value generator + * ```ts + * import { nextFloat64, getRandomValuesSeeded } from "@std/random"; + * import { assertEquals } from "@std/assert"; + * + * const getRandomValues = getRandomValuesSeeded(1n); + * assertEquals(nextFloat64(getRandomValues), 0.49116444173310125); + * assertEquals(nextFloat64(getRandomValues), 0.06903754193160427); + * assertEquals(nextFloat64(getRandomValues), 0.16063206851777034); + * ``` + * + * @example With an arbitrary value generator + * ```ts + * import { nextFloat64 } from "@std/random"; + * import { assertLess, assertGreaterOrEqual } from "@std/assert"; + * + * const val = nextFloat64(crypto.getRandomValues.bind(crypto)); // example: 0.8928746327842533 + * assertGreaterOrEqual(val, 0); + * assertLess(val, 1); + * ``` + */ +export function nextFloat64(getRandomValues: RandomValueGenerator): number { + getRandomValues(b8); + const int53 = Number(dv8.getBigUint64(0, true) >> 11n); + // assert(int53 <= Number.MAX_SAFE_INTEGER) + return int53 * FLOAT_64_MULTIPLIER; +} diff --git a/random/next_float_64_test.ts b/random/next_float_64_test.ts new file mode 100644 index 000000000000..52f04661daaa --- /dev/null +++ b/random/next_float_64_test.ts @@ -0,0 +1,29 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { + getRandomValuesSeeded, + type RandomValueGenerator, +} from "./get_random_values_seeded.ts"; +import { nextFloat64 } from "./next_float_64.ts"; +import { assertEquals, assertGreaterOrEqual, assertLess } from "@std/assert"; + +Deno.test("nextFloat64() gets floats from a seeded value generator", () => { + const getRandomValues = getRandomValuesSeeded(1n); + assertEquals(nextFloat64(getRandomValues), 0.49116444173310125); + assertEquals(nextFloat64(getRandomValues), 0.06903754193160427); + assertEquals(nextFloat64(getRandomValues), 0.16063206851777034); +}); + +Deno.test("nextFloat64() gets floats that are always in the [0, 1) range", () => { + // deno-lint-ignore no-explicit-any + const zeroValueGenerator: RandomValueGenerator = (b) => (b as any).fill(0); + assertEquals(nextFloat64(zeroValueGenerator), 0); + + // deno-lint-ignore no-explicit-any + const maxValueGenerator: RandomValueGenerator = (b) => (b as any).fill(-1); + assertEquals(nextFloat64(maxValueGenerator), 0.9999999999999999); + + const val = nextFloat64(crypto.getRandomValues.bind(crypto)); + + assertGreaterOrEqual(val, 0); + assertLess(val, 1); +}); diff --git a/random/seeded.ts b/random/seeded.ts index 46238c270f5d..469e3437fdd6 100644 --- a/random/seeded.ts +++ b/random/seeded.ts @@ -1,12 +1,13 @@ // Copyright 2018-2025 the Deno authors. MIT license. // This module is browser compatible. -import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.ts"; +import { Pcg32 } from "./_pcg32.ts"; import type { Prng } from "./_types.ts"; +export type { Prng } from "./_types.ts"; /** * Creates a pseudo-random number generator that generates random numbers in - * the range `[0, 1)`, based on the given seed. The algorithm used for - * generation is {@link https://www.pcg-random.org/download.html | PCG32}. + * the range `[0, 1)`, based on the given seed, with 32 bits of entropy. + * The algorithm used for generation is {@link https://www.pcg-random.org/download.html | PCG32}. * * @experimental **UNSTABLE**: New API, yet to be vetted. * @@ -27,15 +28,6 @@ import type { Prng } from "./_types.ts"; * ``` */ export function randomSeeded(seed: bigint): Prng { - const pcg = fromSeed(seedFromU64(seed, 16)); - return () => uint32ToFloat64(nextU32(pcg)); -} - -/** - * Convert a 32-bit unsigned integer to a float64 in the range `[0, 1)`. - * This operation is lossless, i.e. it's always possible to get the original - * value back by multiplying by 2 ** 32. - */ -function uint32ToFloat64(u32: number): number { - return u32 / 2 ** 32; + const pcg = new Pcg32(seed); + return () => pcg.nextUint32() / 2 ** 32; } diff --git a/random/seeded_test.ts b/random/seeded_test.ts index 89a04163ffe1..643f478047a4 100644 --- a/random/seeded_test.ts +++ b/random/seeded_test.ts @@ -1,5 +1,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { randomSeeded } from "./seeded.ts"; +import { getRandomValuesSeeded } from "./get_random_values_seeded.ts"; +import { nextFloat64 } from "./next_float_64.ts"; +import { type Prng, randomSeeded } from "./seeded.ts"; import { assertAlmostEquals, assertEquals } from "@std/assert"; Deno.test("randomSeeded() generates random numbers", () => { @@ -10,8 +12,13 @@ Deno.test("randomSeeded() generates random numbers", () => { assertEquals(prng(), 0.7924694607499987); }); -Deno.test("randomSeeded() gives relatively uniform distribution of random numbers", async (t) => { - const prng = randomSeeded(1n); +Deno.test("getRandomValuesSeeded() with nextFloat64() gives relatively uniform distribution of random numbers", async (t) => { + function randomSeeded53Bit(seed: bigint): Prng { + const getRandomValues = getRandomValuesSeeded(seed); + return () => nextFloat64(getRandomValues); + } + + const prng = randomSeeded53Bit(1n); const results = Array.from({ length: 1e4 }, prng); await t.step("all results are in [0, 1)", () => { @@ -61,3 +68,11 @@ Deno.test("randomSeeded() gives relatively uniform distribution of random number }, ); }); + +Deno.test("getRandomValuesSeeded() generates bytes", () => { + const prng = getRandomValuesSeeded(1n); + assertEquals( + prng(new Uint8Array(5)), + new Uint8Array([230, 11, 167, 51, 238]), + ); +});