# Seeded pseudo-random numbers: xoshiro128**, unbiased ranges, floats and shuffles. Not for cryptography. import Base # A pure generator: each draw returns the value beside the next state. # import ./random/random.bend as Rand # (x, r) = Rand.next(Rand.seed(42)) # xoshiro128** state (https://prng.di.unimi.it/xoshiro128starstar.c). It is never all zero. type Rng is Data: Rng{a: U32, b: U32, c: U32, d: U32} def rotl(+x: U32, l: Nat, r: Nat) -> U32: ((x << l) .|. (x >> r) : U32) # splitmix32's output mixer, a bijection on U32. def mix(+z: U32) -> U32: +y = ((z .^. (z >> 16n)) * 569420461 : U32) +w = ((y .^. (y >> 15n)) * 1935289751 : U32) (w .^. (w >> 15n) : U32) # The first four splitmix32 outputs from s. Distinct seeds give distinct states. def seed(+s: U32) -> Rng: Rng{mix((s + 2654435769 : U32)), mix((s + 1013904242 : U32)), mix((s + 3668340011 : U32)), mix((s + 2027808484 : U32))} def from_state.if(zero: Bool, a: U32, b: U32, c: U32, d: U32) -> Rng: match zero: case True{}: seed(0) case False{}: Rng{a, b, c, d} # The state (a, b, c, d) as is, to replay a reference stream. All zero gives seed(0). def from_state(+a: U32, +b: U32, +c: U32, +d: U32) -> Rng: from_state.if(U32.is_zero((a .|. b .|. c .|. d : U32)), a, b, c, d) # One xoshiro128** step: a uniform U32 and the next state. def next(r: Rng) -> U32 & Rng: match r: case Rng{+a, +b, +c, +d}: +c1 = (c .^. a : U32) +d1 = (d .^. b : U32) ((rotl((b * 5 : U32), 7n, 25n) * 9 : U32), Rng{(a .^. d1 : U32), (b .^. c1 : U32), (c1 .^. (b << 9n) : U32), rotl(d1, 11n, 21n)}) def Bool.case(-A: Type, b: Bool, t: Unit -> A, f: Unit -> A) -> A: match b: case True{}: t(Unit{}) case False{}: f(Unit{}) # Rejects draws below t = 2^32 mod n, as OpenBSD's arc4random_uniform does. def below.go(fuel: Nat, +n: U32, +t: U32, xr: U32 & Rng) -> U32 & Rng: match fuel: case 0n: (x, r) = xr ((x % n : U32), r) case 1n+p: (+x, +r) = xr Bool.case(U32 & Rng, U32.is_ge(x, t), u => ((x % n : U32), r), u => below.go(p, n, t, next(r))) def below.if(small: Bool, +n: U32, r: Rng) -> U32 & Rng: match small: case True{}: (0, r) case False{}: below.go(64n, n, ((0 - n : U32) % n : U32), next(r)) # A uniform U32 in [0, n). Each draw is rejected with odds under 1/2, so after # 64 rejections, odds 2^-64, it takes x mod n. For n < 2 it is 0 and r is unchanged. def below(+n: U32, r: Rng) -> U32 & Rng: below.if(U32.is_lt(n, 2), n, r) def range.fin(lo: U32, xr: U32 & Rng) -> U32 & Rng: (x, r) = xr ((lo + x : U32), r) # A uniform U32 in [lo, hi). For hi <= lo + 1 it is lo and r is unchanged. def range(+lo: U32, hi: U32, r: Rng) -> U32 & Rng: range.fin(lo, below((hi - lo : U32), r)) def unit.fin(xr: U32 & Rng) -> F32 & Rng: (x, r) = xr (F32.div(U32.to_f32((x >> 8n : U32)), 16777216.0), r) # A uniform F32 in [0, 1), from the draw's top 24 bits. def unit(r: Rng) -> F32 & Rng: unit.fin(next(r)) def shuffle.fill(~T: Data, xs: List<&2, T>, +i: U32, a: Array) -> Array: match xs: case Nil{}: a case Con{h, t}: shuffle.fill(~T, t, (i + 1 : U32), Array.set(T, a, i, h)) def shuffle.swap.set(~T: Data, +i: U32, +j: U32, xi: T, ax: Array & T) -> Array: (a, xj) = ax Array.set(T, Array.set(T, a, i, xj), j, xi) def shuffle.swap(~T: Data, +i: U32, +j: U32, ax: Array & T) -> Array: (a, xi) = ax shuffle.swap.set(~T, i, j, xi, Array.get(T, a, j)) # Fisher-Yates: at slot i, jr is below(i + 1); slot i swaps with slot j. # At k = 0, i is 0 and below(1) drew nothing. def shuffle.go(~T: Data, k: Nat, +i: U32, a: Array, jr: U32 & Rng) -> Array & Rng: match k: case 0n: (j, r) = jr (a, r) case 1n+p: (+j, r) = jr shuffle.go(~T, p, (i - 1 : U32), shuffle.swap(~T, i, j, Array.get(T, a, i)), below(i, r)) # Slots [0, k] of the array, with ax the read of slot k, prepended to acc. def shuffle.read(~T: Data, k: Nat, ax: Array & T, acc: List<&2, T>) -> List<&2, T>: match k: case 0n: (a, x) = ax x <> acc case 1n+(+p): (a, x) = ax shuffle.read(~T, p, Array.get(T, a, U32.from_nat(p)), x <> acc) def shuffle.fin(~T: Data, +m: U32, ar: Array & Rng) -> List<&2, T> & Rng: (a, r) = ar (shuffle.read(~T, U32.to_nat(m), Array.get(T, a, m), Nil{}), r) def shuffle.run(~T: Data, +h: T, +xs: List<&2, T>, r: Rng) -> List<&2, T> & Rng: +m = (U32.from_nat(List.length(&2, T, xs)) - 1 : U32) shuffle.fin(~T, m, shuffle.go(~T, U32.to_nat(m), m, shuffle.fill(~T, xs, 0, Array.new(T, Nat.add(1n, U32.log2(m)), h)), below((m + 1 : U32), r))) # A uniform permutation of xs. Every draw is below(i + 1), so a list of n # items uses n - 1 draws. def shuffle(~T: Data, xs: List<&2, T>, r: Rng) -> List<&2, T> & Rng: match xs: case Nil{}: (Nil{}, r) case Con{+h, t}: shuffle.run(~T, h, h <> t, r) # 16 bytes from the OS (getentropy, or crypto.getRandomValues in JS). def entropy() -> IO(Result<&1, &1, U32 & String, U32 & U32 & U32 & U32>): import "./effs/random.c" import "./effs/random.js" def from_os.fin(e: Result<&1, &1, U32 & String, U32 & U32 & U32 & U32>) -> Result<&1, &1, U32 & String, Rng>: match e: case Fail{err}: Fail{err} case Done{(a, b, c, d)}: Done{from_state(a, b, c, d)} # A generator seeded from OS entropy, for when runs need not repeat. def from_os() -> IO(Result<&1, &1, U32 & String, Rng>): do IO>: e : Result<&1, &1, U32 & String, U32 & U32 & U32 & U32> <- entropy() return from_os.fin(e)