Skip to content

Commit

Permalink
feat(random/unstable): basic randomization functions (denoland#5626)
Browse files Browse the repository at this point in the history
Co-authored-by: Yoshiya Hinosawa <[email protected]>
Co-authored-by: Asher Gomez <[email protected]>
  • Loading branch information
3 people authored Sep 5, 2024
1 parent 4ac8373 commit 149839b
Show file tree
Hide file tree
Showing 20 changed files with 1,058 additions and 0 deletions.
2 changes: 2 additions & 0 deletions _tools/check_circular_package_dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Mod =
| "msgpack"
| "net"
| "path"
| "random"
| "regexp"
| "semver"
| "streams"
Expand Down Expand Up @@ -107,6 +108,7 @@ const ENTRYPOINTS: Record<Mod, string[]> = {
msgpack: ["mod.ts"],
net: ["mod.ts"],
path: ["mod.ts"],
random: ["mod.ts"],
regexp: ["mod.ts"],
semver: ["mod.ts"],
streams: ["mod.ts"],
Expand Down
1 change: 1 addition & 0 deletions _tools/check_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const ENTRY_POINTS = [
"../path/mod.ts",
"../path/posix/mod.ts",
"../path/windows/mod.ts",
"../random/mod.ts",
"../regexp/mod.ts",
"../semver/mod.ts",
"../streams/mod.ts",
Expand Down
1 change: 1 addition & 0 deletions browser-compat.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"./msgpack",
"./net",
"./path",
"./random",
"./regexp",
"./semver",
"./streams",
Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"./msgpack",
"./net",
"./path",
"./random",
"./regexp",
"./semver",
"./streams",
Expand Down
1 change: 1 addition & 0 deletions import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@std/net": "jsr:@std/net@^1.0.2",
"@std/path": "jsr:@std/path@^1.0.4",
"@std/regexp": "jsr:@std/regexp@^1.0.0",
"@std/random": "jsr:@std/random@^0.1.0",
"@std/semver": "jsr:@std/semver@^1.0.3",
"@std/streams": "jsr:@std/streams@^1.0.4",
"@std/tar": "jsr:@std/tar@^0.1.0",
Expand Down
100 changes: 100 additions & 0 deletions random/_pcg32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. 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;

// 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;
};

/**
* 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 `pcg` by advancing `pcg.state`.
*/
function step(pgc: PcgMutableState) {
pgc.state = BigInt.asUintN(64, pgc.state * MUL + (pgc.inc | 1n));
}

/**
* 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;
}

/**
* 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));
}

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

/**
* 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);

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);

for (let i = 0; i < Math.floor(numBytes / 4); ++i) {
new DataView(seed.buffer).setUint32(i * 4, nextU32(pgc), true);
}

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);
}

return seed;
}
122 changes: 122 additions & 0 deletions random/_pcg32_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../assert/equals.ts";
import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.ts";

Deno.test("seedFromU64() 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
* but directly returning `seed` instead of `Self::from_seed(seed)`
*/
// deno-fmt-ignore
const expectedResults = [
[236, 242, 115, 249, 129, 181, 205, 69, 135, 240, 70, 115, 6, 173, 108, 173],
[234, 216, 29, 114, 93, 38, 16, 78, 137, 156, 59, 248, 66, 206, 120, 46],
[77, 209, 16, 204, 177, 124, 55, 30, 237, 239, 68, 142, 238, 125, 215, 7],
[108, 90, 247, 27, 160, 186, 6, 71, 76, 124, 221, 142, 87, 133, 92, 175],
[197, 166, 196, 87, 44, 68, 69, 62, 55, 32, 34, 218, 130, 107, 171, 170],
[60, 64, 172, 11, 74, 188, 224, 128, 161, 112, 220, 75, 85, 212, 145, 251],
[177, 93, 150, 16, 48, 3, 23, 51, 155, 104, 76, 121, 82, 134, 239, 107],
[200, 12, 64, 59, 208, 32, 108, 9, 55, 166, 59, 111, 242, 79, 37, 30],
[222, 11, 88, 159, 202, 89, 63, 215, 36, 57, 0, 156, 63, 131, 114, 90],
[21, 119, 90, 241, 241, 191, 180, 229, 150, 199, 126, 251, 25, 141, 7, 4],
];

for (const [i, expected] of expectedResults.entries()) {
await t.step(`With seed ${i}n`, () => {
const actual = Array.from(seedFromU64(BigInt(i), 16));
assertEquals(actual, expected);
});
}
});

await t.step(
"generates arbitrary-length seed data from a single bigint",
async (t) => {
// deno-fmt-ignore
const expectedBytes = [234, 216, 29, 114, 93, 38, 16, 78, 137, 156, 59, 248, 66, 206, 120, 46, 186];

for (const i of expectedBytes.keys()) {
const slice = expectedBytes.slice(0, i + 1);

await t.step(`With length ${i + 1}`, () => {
const actual = Array.from(seedFromU64(1n, i + 1));
assertEquals(actual, slice);
});
}
},
);

const U64_CEIL = 2n ** 64n;

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));
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));
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));
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));
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));
assertEquals(actual, expected);
});
});
});

Deno.test("nextU32() generates random 32-bit integers", async (t) => {
/**
* 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());
* }
* ```
*/
const expectedResults = [
298703107,
4236525527,
336081875,
1056616254,
1060453275,
1616833669,
501767310,
2864049166,
56572352,
2362354238,
];

const pgc = fromSeed(seedFromU64(0n, 16));
const next = () => nextU32(pgc);

for (const [i, expected] of expectedResults.entries()) {
await t.step(`#${i + 1} generated uint32`, () => {
const actual = next();
assertEquals(actual, expected);
});
}
});
27 changes: 27 additions & 0 deletions random/_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.

/**
* A pseudo-random number generator implementing the same contract as
* `Math.random`, i.e. taking zero arguments and returning a random number in
* the range `[0, 1)`. The behavior of a function that accepts a `Prng` an
* option may be customized by passing a `Prng` with different behavior from
* `Math.random`, for example it may be seeded.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export type Prng = typeof Math.random;

/**
* Options for random number generation.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export type RandomOptions = {
/**
* A pseudo-random number generator returning a random number in the range
* `[0, 1)`, used for randomization.
* @default {Math.random}
*/
prng?: Prng;
};
51 changes: 51 additions & 0 deletions random/between.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import type { Prng, RandomOptions } from "./_types.ts";
export type { Prng, RandomOptions };

/**
* Generates a random number between the provided minimum and maximum values.
*
* The number is in the range `[min, max)`, i.e. `min` is included but `max` is excluded.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param min The minimum value (inclusive)
* @param max The maximum value (exclusive)
* @param options The options for the random number generator
* @returns A random number between the provided minimum and maximum values
*
* @example Usage
* ```ts no-assert
* import { randomBetween } from "@std/random";
*
* randomBetween(1, 10); // 6.688009464410508
* randomBetween(1, 10); // 3.6267118101712006
* randomBetween(1, 10); // 7.853320239013774
* ```
*/
export function randomBetween(
min: number,
max: number,
options?: RandomOptions,
): number {
if (!Number.isFinite(min)) {
throw new RangeError(
`Cannot generate a random number: min cannot be ${min}`,
);
}
if (!Number.isFinite(max)) {
throw new RangeError(
`Cannot generate a random number: max cannot be ${max}`,
);
}
if (max < min) {
throw new RangeError(
`Cannot generate a random number as max must be greater than or equal to min: max=${max}, min=${min}`,
);
}

const x = (options?.prng ?? Math.random)();
const y = min * (1 - x) + max * x;
return y >= min && y < max ? y : min;
}
Loading

0 comments on commit 149839b

Please sign in to comment.