diff options
Diffstat (limited to 'lib/std/rand.zig')
| -rw-r--r-- | lib/std/rand.zig | 1105 |
1 files changed, 1105 insertions, 0 deletions
diff --git a/lib/std/rand.zig b/lib/std/rand.zig new file mode 100644 index 0000000000..e14a60e2ae --- /dev/null +++ b/lib/std/rand.zig @@ -0,0 +1,1105 @@ +// The engines provided here should be initialized from an external source. For now, randomBytes +// from the crypto package is the most suitable. Be sure to use a CSPRNG when required, otherwise using +// a normal PRNG will be faster and use substantially less stack space. +// +// ``` +// var buf: [8]u8 = undefined; +// try std.crypto.randomBytes(buf[0..]); +// const seed = mem.readIntSliceLittle(u64, buf[0..8]); +// +// var r = DefaultPrng.init(seed); +// +// const s = r.random.int(u64); +// ``` +// +// TODO(tiehuis): Benchmark these against other reference implementations. + +const std = @import("std.zig"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const expect = std.testing.expect; +const expectEqual = std.testing.expectEqual; +const mem = std.mem; +const math = std.math; +const ziggurat = @import("rand/ziggurat.zig"); +const maxInt = std.math.maxInt; + +// When you need fast unbiased random numbers +pub const DefaultPrng = Xoroshiro128; + +// When you need cryptographically secure random numbers +pub const DefaultCsprng = Isaac64; + +pub const Random = struct { + fillFn: fn (r: *Random, buf: []u8) void, + + /// Read random bytes into the specified buffer until full. + pub fn bytes(r: *Random, buf: []u8) void { + r.fillFn(r, buf); + } + + pub fn boolean(r: *Random) bool { + return r.int(u1) != 0; + } + + /// Returns a random int `i` such that `0 <= i <= maxInt(T)`. + /// `i` is evenly distributed. + pub fn int(r: *Random, comptime T: type) T { + const UnsignedT = @IntType(false, T.bit_count); + const ByteAlignedT = @IntType(false, @divTrunc(T.bit_count + 7, 8) * 8); + + var rand_bytes: [@sizeOf(ByteAlignedT)]u8 = undefined; + r.bytes(rand_bytes[0..]); + + // use LE instead of native endian for better portability maybe? + // TODO: endian portability is pointless if the underlying prng isn't endian portable. + // TODO: document the endian portability of this library. + const byte_aligned_result = mem.readIntSliceLittle(ByteAlignedT, rand_bytes); + const unsigned_result = @truncate(UnsignedT, byte_aligned_result); + return @bitCast(T, unsigned_result); + } + + /// Constant-time implementation off ::uintLessThan. + /// The results of this function may be biased. + pub fn uintLessThanBiased(r: *Random, comptime T: type, less_than: T) T { + comptime assert(T.is_signed == false); + comptime assert(T.bit_count <= 64); // TODO: workaround: LLVM ERROR: Unsupported library call operation! + assert(0 < less_than); + if (T.bit_count <= 32) { + return @intCast(T, limitRangeBiased(u32, r.int(u32), less_than)); + } else { + return @intCast(T, limitRangeBiased(u64, r.int(u64), less_than)); + } + } + + /// Returns an evenly distributed random unsigned integer `0 <= i < less_than`. + /// This function assumes that the underlying ::fillFn produces evenly distributed values. + /// Within this assumption, the runtime of this function is exponentially distributed. + /// If ::fillFn were backed by a true random generator, + /// the runtime of this function would technically be unbounded. + /// However, if ::fillFn is backed by any evenly distributed pseudo random number generator, + /// this function is guaranteed to return. + /// If you need deterministic runtime bounds, use `::uintLessThanBiased`. + pub fn uintLessThan(r: *Random, comptime T: type, less_than: T) T { + comptime assert(T.is_signed == false); + comptime assert(T.bit_count <= 64); // TODO: workaround: LLVM ERROR: Unsupported library call operation! + assert(0 < less_than); + // Small is typically u32 + const Small = @IntType(false, @divTrunc(T.bit_count + 31, 32) * 32); + // Large is typically u64 + const Large = @IntType(false, Small.bit_count * 2); + + // adapted from: + // http://www.pcg-random.org/posts/bounded-rands.html + // "Lemire's (with an extra tweak from me)" + var x: Small = r.int(Small); + var m: Large = Large(x) * Large(less_than); + var l: Small = @truncate(Small, m); + if (l < less_than) { + // TODO: workaround for https://github.com/ziglang/zig/issues/1770 + // should be: + // var t: Small = -%less_than; + var t: Small = @bitCast(Small, -%@bitCast(@IntType(true, Small.bit_count), Small(less_than))); + + if (t >= less_than) { + t -= less_than; + if (t >= less_than) { + t %= less_than; + } + } + while (l < t) { + x = r.int(Small); + m = Large(x) * Large(less_than); + l = @truncate(Small, m); + } + } + return @intCast(T, m >> Small.bit_count); + } + + /// Constant-time implementation off ::uintAtMost. + /// The results of this function may be biased. + pub fn uintAtMostBiased(r: *Random, comptime T: type, at_most: T) T { + assert(T.is_signed == false); + if (at_most == maxInt(T)) { + // have the full range + return r.int(T); + } + return r.uintLessThanBiased(T, at_most + 1); + } + + /// Returns an evenly distributed random unsigned integer `0 <= i <= at_most`. + /// See ::uintLessThan, which this function uses in most cases, + /// for commentary on the runtime of this function. + pub fn uintAtMost(r: *Random, comptime T: type, at_most: T) T { + assert(T.is_signed == false); + if (at_most == maxInt(T)) { + // have the full range + return r.int(T); + } + return r.uintLessThan(T, at_most + 1); + } + + /// Constant-time implementation off ::intRangeLessThan. + /// The results of this function may be biased. + pub fn intRangeLessThanBiased(r: *Random, comptime T: type, at_least: T, less_than: T) T { + assert(at_least < less_than); + if (T.is_signed) { + // Two's complement makes this math pretty easy. + const UnsignedT = @IntType(false, T.bit_count); + const lo = @bitCast(UnsignedT, at_least); + const hi = @bitCast(UnsignedT, less_than); + const result = lo +% r.uintLessThanBiased(UnsignedT, hi -% lo); + return @bitCast(T, result); + } else { + // The signed implementation would work fine, but we can use stricter arithmetic operators here. + return at_least + r.uintLessThanBiased(T, less_than - at_least); + } + } + + /// Returns an evenly distributed random integer `at_least <= i < less_than`. + /// See ::uintLessThan, which this function uses in most cases, + /// for commentary on the runtime of this function. + pub fn intRangeLessThan(r: *Random, comptime T: type, at_least: T, less_than: T) T { + assert(at_least < less_than); + if (T.is_signed) { + // Two's complement makes this math pretty easy. + const UnsignedT = @IntType(false, T.bit_count); + const lo = @bitCast(UnsignedT, at_least); + const hi = @bitCast(UnsignedT, less_than); + const result = lo +% r.uintLessThan(UnsignedT, hi -% lo); + return @bitCast(T, result); + } else { + // The signed implementation would work fine, but we can use stricter arithmetic operators here. + return at_least + r.uintLessThan(T, less_than - at_least); + } + } + + /// Constant-time implementation off ::intRangeAtMostBiased. + /// The results of this function may be biased. + pub fn intRangeAtMostBiased(r: *Random, comptime T: type, at_least: T, at_most: T) T { + assert(at_least <= at_most); + if (T.is_signed) { + // Two's complement makes this math pretty easy. + const UnsignedT = @IntType(false, T.bit_count); + const lo = @bitCast(UnsignedT, at_least); + const hi = @bitCast(UnsignedT, at_most); + const result = lo +% r.uintAtMostBiased(UnsignedT, hi -% lo); + return @bitCast(T, result); + } else { + // The signed implementation would work fine, but we can use stricter arithmetic operators here. + return at_least + r.uintAtMostBiased(T, at_most - at_least); + } + } + + /// Returns an evenly distributed random integer `at_least <= i <= at_most`. + /// See ::uintLessThan, which this function uses in most cases, + /// for commentary on the runtime of this function. + pub fn intRangeAtMost(r: *Random, comptime T: type, at_least: T, at_most: T) T { + assert(at_least <= at_most); + if (T.is_signed) { + // Two's complement makes this math pretty easy. + const UnsignedT = @IntType(false, T.bit_count); + const lo = @bitCast(UnsignedT, at_least); + const hi = @bitCast(UnsignedT, at_most); + const result = lo +% r.uintAtMost(UnsignedT, hi -% lo); + return @bitCast(T, result); + } else { + // The signed implementation would work fine, but we can use stricter arithmetic operators here. + return at_least + r.uintAtMost(T, at_most - at_least); + } + } + + /// TODO: deprecated. use ::boolean or ::int instead. + pub fn scalar(r: *Random, comptime T: type) T { + return if (T == bool) r.boolean() else r.int(T); + } + + /// TODO: deprecated. renamed to ::intRangeLessThan + pub fn range(r: *Random, comptime T: type, start: T, end: T) T { + return r.intRangeLessThan(T, start, end); + } + + /// Return a floating point value evenly distributed in the range [0, 1). + pub fn float(r: *Random, comptime T: type) T { + // Generate a uniform value between [1, 2) and scale down to [0, 1). + // Note: The lowest mantissa bit is always set to 0 so we only use half the available range. + switch (T) { + f32 => { + const s = r.int(u32); + const repr = (0x7f << 23) | (s >> 9); + return @bitCast(f32, repr) - 1.0; + }, + f64 => { + const s = r.int(u64); + const repr = (0x3ff << 52) | (s >> 12); + return @bitCast(f64, repr) - 1.0; + }, + else => @compileError("unknown floating point type"), + } + } + + /// Return a floating point value normally distributed with mean = 0, stddev = 1. + /// + /// To use different parameters, use: floatNorm(...) * desiredStddev + desiredMean. + pub fn floatNorm(r: *Random, comptime T: type) T { + const value = ziggurat.next_f64(r, ziggurat.NormDist); + switch (T) { + f32 => return @floatCast(f32, value), + f64 => return value, + else => @compileError("unknown floating point type"), + } + } + + /// Return an exponentially distributed float with a rate parameter of 1. + /// + /// To use a different rate parameter, use: floatExp(...) / desiredRate. + pub fn floatExp(r: *Random, comptime T: type) T { + const value = ziggurat.next_f64(r, ziggurat.ExpDist); + switch (T) { + f32 => return @floatCast(f32, value), + f64 => return value, + else => @compileError("unknown floating point type"), + } + } + + /// Shuffle a slice into a random order. + pub fn shuffle(r: *Random, comptime T: type, buf: []T) void { + if (buf.len < 2) { + return; + } + + var i: usize = 0; + while (i < buf.len - 1) : (i += 1) { + const j = r.intRangeLessThan(usize, i, buf.len); + mem.swap(T, &buf[i], &buf[j]); + } + } +}; + +/// Convert a random integer 0 <= random_int <= maxValue(T), +/// into an integer 0 <= result < less_than. +/// This function introduces a minor bias. +pub fn limitRangeBiased(comptime T: type, random_int: T, less_than: T) T { + comptime assert(T.is_signed == false); + const T2 = @IntType(false, T.bit_count * 2); + + // adapted from: + // http://www.pcg-random.org/posts/bounded-rands.html + // "Integer Multiplication (Biased)" + var m: T2 = T2(random_int) * T2(less_than); + return @intCast(T, m >> T.bit_count); +} + +const SequentialPrng = struct { + const Self = @This(); + random: Random, + next_value: u8, + + pub fn init() Self { + return Self{ + .random = Random{ .fillFn = fill }, + .next_value = 0, + }; + } + + fn fill(r: *Random, buf: []u8) void { + const self = @fieldParentPtr(Self, "random", r); + for (buf) |*b| { + b.* = self.next_value; + } + self.next_value +%= 1; + } +}; + +test "Random int" { + testRandomInt(); + comptime testRandomInt(); +} +fn testRandomInt() void { + var r = SequentialPrng.init(); + + expect(r.random.int(u0) == 0); + + r.next_value = 0; + expect(r.random.int(u1) == 0); + expect(r.random.int(u1) == 1); + expect(r.random.int(u2) == 2); + expect(r.random.int(u2) == 3); + expect(r.random.int(u2) == 0); + + r.next_value = 0xff; + expect(r.random.int(u8) == 0xff); + r.next_value = 0x11; + expect(r.random.int(u8) == 0x11); + + r.next_value = 0xff; + expect(r.random.int(u32) == 0xffffffff); + r.next_value = 0x11; + expect(r.random.int(u32) == 0x11111111); + + r.next_value = 0xff; + expect(r.random.int(i32) == -1); + r.next_value = 0x11; + expect(r.random.int(i32) == 0x11111111); + + r.next_value = 0xff; + expect(r.random.int(i8) == -1); + r.next_value = 0x11; + expect(r.random.int(i8) == 0x11); + + r.next_value = 0xff; + expect(r.random.int(u33) == 0x1ffffffff); + r.next_value = 0xff; + expect(r.random.int(i1) == -1); + r.next_value = 0xff; + expect(r.random.int(i2) == -1); + r.next_value = 0xff; + expect(r.random.int(i33) == -1); +} + +test "Random boolean" { + testRandomBoolean(); + comptime testRandomBoolean(); +} +fn testRandomBoolean() void { + var r = SequentialPrng.init(); + expect(r.random.boolean() == false); + expect(r.random.boolean() == true); + expect(r.random.boolean() == false); + expect(r.random.boolean() == true); +} + +test "Random intLessThan" { + @setEvalBranchQuota(10000); + testRandomIntLessThan(); + comptime testRandomIntLessThan(); +} +fn testRandomIntLessThan() void { + var r = SequentialPrng.init(); + r.next_value = 0xff; + expect(r.random.uintLessThan(u8, 4) == 3); + expect(r.next_value == 0); + expect(r.random.uintLessThan(u8, 4) == 0); + expect(r.next_value == 1); + + r.next_value = 0; + expect(r.random.uintLessThan(u64, 32) == 0); + + // trigger the bias rejection code path + r.next_value = 0; + expect(r.random.uintLessThan(u8, 3) == 0); + // verify we incremented twice + expect(r.next_value == 2); + + r.next_value = 0xff; + expect(r.random.intRangeLessThan(u8, 0, 0x80) == 0x7f); + r.next_value = 0xff; + expect(r.random.intRangeLessThan(u8, 0x7f, 0xff) == 0xfe); + + r.next_value = 0xff; + expect(r.random.intRangeLessThan(i8, 0, 0x40) == 0x3f); + r.next_value = 0xff; + expect(r.random.intRangeLessThan(i8, -0x40, 0x40) == 0x3f); + r.next_value = 0xff; + expect(r.random.intRangeLessThan(i8, -0x80, 0) == -1); + + r.next_value = 0xff; + expect(r.random.intRangeLessThan(i3, -4, 0) == -1); + r.next_value = 0xff; + expect(r.random.intRangeLessThan(i3, -2, 2) == 1); +} + +test "Random intAtMost" { + @setEvalBranchQuota(10000); + testRandomIntAtMost(); + comptime testRandomIntAtMost(); +} +fn testRandomIntAtMost() void { + var r = SequentialPrng.init(); + r.next_value = 0xff; + expect(r.random.uintAtMost(u8, 3) == 3); + expect(r.next_value == 0); + expect(r.random.uintAtMost(u8, 3) == 0); + + // trigger the bias rejection code path + r.next_value = 0; + expect(r.random.uintAtMost(u8, 2) == 0); + // verify we incremented twice + expect(r.next_value == 2); + + r.next_value = 0xff; + expect(r.random.intRangeAtMost(u8, 0, 0x7f) == 0x7f); + r.next_value = 0xff; + expect(r.random.intRangeAtMost(u8, 0x7f, 0xfe) == 0xfe); + + r.next_value = 0xff; + expect(r.random.intRangeAtMost(i8, 0, 0x3f) == 0x3f); + r.next_value = 0xff; + expect(r.random.intRangeAtMost(i8, -0x40, 0x3f) == 0x3f); + r.next_value = 0xff; + expect(r.random.intRangeAtMost(i8, -0x80, -1) == -1); + + r.next_value = 0xff; + expect(r.random.intRangeAtMost(i3, -4, -1) == -1); + r.next_value = 0xff; + expect(r.random.intRangeAtMost(i3, -2, 1) == 1); + + expect(r.random.uintAtMost(u0, 0) == 0); +} + +test "Random Biased" { + var r = DefaultPrng.init(0); + // Not thoroughly checking the logic here. + // Just want to execute all the paths with different types. + + expect(r.random.uintLessThanBiased(u1, 1) == 0); + expect(r.random.uintLessThanBiased(u32, 10) < 10); + expect(r.random.uintLessThanBiased(u64, 20) < 20); + + expect(r.random.uintAtMostBiased(u0, 0) == 0); + expect(r.random.uintAtMostBiased(u1, 0) <= 0); + expect(r.random.uintAtMostBiased(u32, 10) <= 10); + expect(r.random.uintAtMostBiased(u64, 20) <= 20); + + expect(r.random.intRangeLessThanBiased(u1, 0, 1) == 0); + expect(r.random.intRangeLessThanBiased(i1, -1, 0) == -1); + expect(r.random.intRangeLessThanBiased(u32, 10, 20) >= 10); + expect(r.random.intRangeLessThanBiased(i32, 10, 20) >= 10); + expect(r.random.intRangeLessThanBiased(u64, 20, 40) >= 20); + expect(r.random.intRangeLessThanBiased(i64, 20, 40) >= 20); + + // uncomment for broken module error: + //expect(r.random.intRangeAtMostBiased(u0, 0, 0) == 0); + expect(r.random.intRangeAtMostBiased(u1, 0, 1) >= 0); + expect(r.random.intRangeAtMostBiased(i1, -1, 0) >= -1); + expect(r.random.intRangeAtMostBiased(u32, 10, 20) >= 10); + expect(r.random.intRangeAtMostBiased(i32, 10, 20) >= 10); + expect(r.random.intRangeAtMostBiased(u64, 20, 40) >= 20); + expect(r.random.intRangeAtMostBiased(i64, 20, 40) >= 20); +} + +// Generator to extend 64-bit seed values into longer sequences. +// +// The number of cycles is thus limited to 64-bits regardless of the engine, but this +// is still plenty for practical purposes. +const SplitMix64 = struct { + s: u64, + + pub fn init(seed: u64) SplitMix64 { + return SplitMix64{ .s = seed }; + } + + pub fn next(self: *SplitMix64) u64 { + self.s +%= 0x9e3779b97f4a7c15; + + var z = self.s; + z = (z ^ (z >> 30)) *% 0xbf58476d1ce4e5b9; + z = (z ^ (z >> 27)) *% 0x94d049bb133111eb; + return z ^ (z >> 31); + } +}; + +test "splitmix64 sequence" { + var r = SplitMix64.init(0xaeecf86f7878dd75); + + const seq = [_]u64{ + 0x5dbd39db0178eb44, + 0xa9900fb66b397da3, + 0x5c1a28b1aeebcf5c, + 0x64a963238f776912, + 0xc6d4177b21d1c0ab, + 0xb2cbdbdb5ea35394, + }; + + for (seq) |s| { + expect(s == r.next()); + } +} + +// PCG32 - http://www.pcg-random.org/ +// +// PRNG +pub const Pcg = struct { + const default_multiplier = 6364136223846793005; + + random: Random, + + s: u64, + i: u64, + + pub fn init(init_s: u64) Pcg { + var pcg = Pcg{ + .random = Random{ .fillFn = fill }, + .s = undefined, + .i = undefined, + }; + + pcg.seed(init_s); + return pcg; + } + + fn next(self: *Pcg) u32 { + const l = self.s; + self.s = l *% default_multiplier +% (self.i | 1); + + const xor_s = @truncate(u32, ((l >> 18) ^ l) >> 27); + const rot = @intCast(u32, l >> 59); + + return (xor_s >> @intCast(u5, rot)) | (xor_s << @intCast(u5, (0 -% rot) & 31)); + } + + fn seed(self: *Pcg, init_s: u64) void { + // Pcg requires 128-bits of seed. + var gen = SplitMix64.init(init_s); + self.seedTwo(gen.next(), gen.next()); + } + + fn seedTwo(self: *Pcg, init_s: u64, init_i: u64) void { + self.s = 0; + self.i = (init_s << 1) | 1; + self.s = self.s *% default_multiplier +% self.i; + self.s +%= init_i; + self.s = self.s *% default_multiplier +% self.i; + } + + fn fill(r: *Random, buf: []u8) void { + const self = @fieldParentPtr(Pcg, "random", r); + + var i: usize = 0; + const aligned_len = buf.len - (buf.len & 7); + + // Complete 4 byte segments. + while (i < aligned_len) : (i += 4) { + var n = self.next(); + comptime var j: usize = 0; + inline while (j < 4) : (j += 1) { + buf[i + j] = @truncate(u8, n); + n >>= 8; + } + } + + // Remaining. (cuts the stream) + if (i != buf.len) { + var n = self.next(); + while (i < buf.len) : (i += 1) { + buf[i] = @truncate(u8, n); + n >>= 4; + } + } + } +}; + +test "pcg sequence" { + var r = Pcg.init(0); + const s0: u64 = 0x9394bf54ce5d79de; + const s1: u64 = 0x84e9c579ef59bbf7; + r.seedTwo(s0, s1); + + const seq = [_]u32{ + 2881561918, + 3063928540, + 1199791034, + 2487695858, + 1479648952, + 3247963454, + }; + + for (seq) |s| { + expect(s == r.next()); + } +} + +// Xoroshiro128+ - http://xoroshiro.di.unimi.it/ +// +// PRNG +pub const Xoroshiro128 = struct { + random: Random, + + s: [2]u64, + + pub fn init(init_s: u64) Xoroshiro128 { + var x = Xoroshiro128{ + .random = Random{ .fillFn = fill }, + .s = undefined, + }; + + x.seed(init_s); + return x; + } + + fn next(self: *Xoroshiro128) u64 { + const s0 = self.s[0]; + var s1 = self.s[1]; + const r = s0 +% s1; + + s1 ^= s0; + self.s[0] = math.rotl(u64, s0, u8(55)) ^ s1 ^ (s1 << 14); + self.s[1] = math.rotl(u64, s1, u8(36)); + + return r; + } + + // Skip 2^64 places ahead in the sequence + fn jump(self: *Xoroshiro128) void { + var s0: u64 = 0; + var s1: u64 = 0; + + const table = [_]u64{ + 0xbeac0467eba5facb, + 0xd86b048b86aa9922, + }; + + inline for (table) |entry| { + var b: usize = 0; + while (b < 64) : (b += 1) { + if ((entry & (u64(1) << @intCast(u6, b))) != 0) { + s0 ^= self.s[0]; + s1 ^= self.s[1]; + } + _ = self.next(); + } + } + + self.s[0] = s0; + self.s[1] = s1; + } + + fn seed(self: *Xoroshiro128, init_s: u64) void { + // Xoroshiro requires 128-bits of seed. + var gen = SplitMix64.init(init_s); + + self.s[0] = gen.next(); + self.s[1] = gen.next(); + } + + fn fill(r: *Random, buf: []u8) void { + const self = @fieldParentPtr(Xoroshiro128, "random", r); + + var i: usize = 0; + const aligned_len = buf.len - (buf.len & 7); + + // Complete 8 byte segments. + while (i < aligned_len) : (i += 8) { + var n = self.next(); + comptime var j: usize = 0; + inline while (j < 8) : (j += 1) { + buf[i + j] = @truncate(u8, n); + n >>= 8; + } + } + + // Remaining. (cuts the stream) + if (i != buf.len) { + var n = self.next(); + while (i < buf.len) : (i += 1) { + buf[i] = @truncate(u8, n); + n >>= 8; + } + } + } +}; + +test "xoroshiro sequence" { + var r = Xoroshiro128.init(0); + r.s[0] = 0xaeecf86f7878dd75; + r.s[1] = 0x01cd153642e72622; + + const seq1 = [_]u64{ + 0xb0ba0da5bb600397, + 0x18a08afde614dccc, + 0xa2635b956a31b929, + 0xabe633c971efa045, + 0x9ac19f9706ca3cac, + 0xf62b426578c1e3fb, + }; + + for (seq1) |s| { + expect(s == r.next()); + } + + r.jump(); + + const seq2 = [_]u64{ + 0x95344a13556d3e22, + 0xb4fb32dafa4d00df, + 0xb2011d9ccdcfe2dd, + 0x05679a9b2119b908, + 0xa860a1da7c9cd8a0, + 0x658a96efe3f86550, + }; + + for (seq2) |s| { + expect(s == r.next()); + } +} + +// ISAAC64 - http://www.burtleburtle.net/bob/rand/isaacafa.html +// +// CSPRNG +// +// Follows the general idea of the implementation from here with a few shortcuts. +// https://doc.rust-lang.org/rand/src/rand/prng/isaac64.rs.html +pub const Isaac64 = struct { + random: Random, + + r: [256]u64, + m: [256]u64, + a: u64, + b: u64, + c: u64, + i: usize, + + pub fn init(init_s: u64) Isaac64 { + var isaac = Isaac64{ + .random = Random{ .fillFn = fill }, + .r = undefined, + .m = undefined, + .a = undefined, + .b = undefined, + .c = undefined, + .i = undefined, + }; + + // seed == 0 => same result as the unseeded reference implementation + isaac.seed(init_s, 1); + return isaac; + } + + fn step(self: *Isaac64, mix: u64, base: usize, comptime m1: usize, comptime m2: usize) void { + const x = self.m[base + m1]; + self.a = mix +% self.m[base + m2]; + + const y = self.a +% self.b +% self.m[@intCast(usize, (x >> 3) % self.m.len)]; + self.m[base + m1] = y; + + self.b = x +% self.m[@intCast(usize, (y >> 11) % self.m.len)]; + self.r[self.r.len - 1 - base - m1] = self.b; + } + + fn refill(self: *Isaac64) void { + const midpoint = self.r.len / 2; + + self.c +%= 1; + self.b +%= self.c; + + { + var i: usize = 0; + while (i < midpoint) : (i += 4) { + self.step(~(self.a ^ (self.a << 21)), i + 0, 0, midpoint); + self.step(self.a ^ (self.a >> 5), i + 1, 0, midpoint); + self.step(self.a ^ (self.a << 12), i + 2, 0, midpoint); + self.step(self.a ^ (self.a >> 33), i + 3, 0, midpoint); + } + } + + { + var i: usize = 0; + while (i < midpoint) : (i += 4) { + self.step(~(self.a ^ (self.a << 21)), i + 0, midpoint, 0); + self.step(self.a ^ (self.a >> 5), i + 1, midpoint, 0); + self.step(self.a ^ (self.a << 12), i + 2, midpoint, 0); + self.step(self.a ^ (self.a >> 33), i + 3, midpoint, 0); + } + } + + self.i = 0; + } + + fn next(self: *Isaac64) u64 { + if (self.i >= self.r.len) { + self.refill(); + } + + const value = self.r[self.i]; + self.i += 1; + return value; + } + + fn seed(self: *Isaac64, init_s: u64, comptime rounds: usize) void { + // We ignore the multi-pass requirement since we don't currently expose full access to + // seeding the self.m array completely. + mem.set(u64, self.m[0..], 0); + self.m[0] = init_s; + + // prescrambled golden ratio constants + var a = [_]u64{ + 0x647c4677a2884b7c, + 0xb9f8b322c73ac862, + 0x8c0ea5053d4712a0, + 0xb29b2e824a595524, + 0x82f053db8355e0ce, + 0x48fe4a0fa5a09315, + 0xae985bf2cbfc89ed, + 0x98f5704f6c44c0ab, + }; + + comptime var i: usize = 0; + inline while (i < rounds) : (i += 1) { + var j: usize = 0; + while (j < self.m.len) : (j += 8) { + comptime var x1: usize = 0; + inline while (x1 < 8) : (x1 += 1) { + a[x1] +%= self.m[j + x1]; + } + + a[0] -%= a[4]; + a[5] ^= a[7] >> 9; + a[7] +%= a[0]; + a[1] -%= a[5]; + a[6] ^= a[0] << 9; + a[0] +%= a[1]; + a[2] -%= a[6]; + a[7] ^= a[1] >> 23; + a[1] +%= a[2]; + a[3] -%= a[7]; + a[0] ^= a[2] << 15; + a[2] +%= a[3]; + a[4] -%= a[0]; + a[1] ^= a[3] >> 14; + a[3] +%= a[4]; + a[5] -%= a[1]; + a[2] ^= a[4] << 20; + a[4] +%= a[5]; + a[6] -%= a[2]; + a[3] ^= a[5] >> 17; + a[5] +%= a[6]; + a[7] -%= a[3]; + a[4] ^= a[6] << 14; + a[6] +%= a[7]; + + comptime var x2: usize = 0; + inline while (x2 < 8) : (x2 += 1) { + self.m[j + x2] = a[x2]; + } + } + } + + mem.set(u64, self.r[0..], 0); + self.a = 0; + self.b = 0; + self.c = 0; + self.i = self.r.len; // trigger refill on first value + } + + fn fill(r: *Random, buf: []u8) void { + const self = @fieldParentPtr(Isaac64, "random", r); + + var i: usize = 0; + const aligned_len = buf.len - (buf.len & 7); + + // Fill complete 64-byte segments + while (i < aligned_len) : (i += 8) { + var n = self.next(); + comptime var j: usize = 0; + inline while (j < 8) : (j += 1) { + buf[i + j] = @truncate(u8, n); + n >>= 8; + } + } + + // Fill trailing, ignoring excess (cut the stream). + if (i != buf.len) { + var n = self.next(); + while (i < buf.len) : (i += 1) { + buf[i] = @truncate(u8, n); + n >>= 8; + } + } + } +}; + +test "isaac64 sequence" { + var r = Isaac64.init(0); + + // from reference implementation + const seq = [_]u64{ + 0xf67dfba498e4937c, + 0x84a5066a9204f380, + 0xfee34bd5f5514dbb, + 0x4d1664739b8f80d6, + 0x8607459ab52a14aa, + 0x0e78bc5a98529e49, + 0xfe5332822ad13777, + 0x556c27525e33d01a, + 0x08643ca615f3149f, + 0xd0771faf3cb04714, + 0x30e86f68a37b008d, + 0x3074ebc0488a3adf, + 0x270645ea7a2790bc, + 0x5601a0a8d3763c6a, + 0x2f83071f53f325dd, + 0xb9090f3d42d2d2ea, + }; + + for (seq) |s| { + expect(s == r.next()); + } +} + +/// Sfc64 pseudo-random number generator from Practically Random. +/// Fastest engine of pracrand and smallest footprint. +/// See http://pracrand.sourceforge.net/ +pub const Sfc64 = struct { + random: Random, + + a: u64 = undefined, + b: u64 = undefined, + c: u64 = undefined, + counter: u64 = undefined, + + const Rotation = 24; + const RightShift = 11; + const LeftShift = 3; + + pub fn init(init_s: u64) Sfc64 { + var x = Sfc64{ + .random = Random{ .fillFn = fill }, + }; + + x.seed(init_s); + return x; + } + + fn next(self: *Sfc64) u64 { + const tmp = self.a +% self.b +% self.counter; + self.counter += 1; + self.a = self.b ^ (self.b >> RightShift); + self.b = self.c +% (self.c << LeftShift); + self.c = math.rotl(u64, self.c, Rotation) +% tmp; + return tmp; + } + + fn seed(self: *Sfc64, init_s: u64) void { + self.a = init_s; + self.b = init_s; + self.c = init_s; + self.counter = 1; + var i: u32 = 0; + while (i < 12) : (i += 1) { + _ = self.next(); + } + } + + fn fill(r: *Random, buf: []u8) void { + const self = @fieldParentPtr(Sfc64, "random", r); + + var i: usize = 0; + const aligned_len = buf.len - (buf.len & 7); + + // Complete 8 byte segments. + while (i < aligned_len) : (i += 8) { + var n = self.next(); + comptime var j: usize = 0; + inline while (j < 8) : (j += 1) { + buf[i + j] = @truncate(u8, n); + n >>= 8; + } + } + + // Remaining. (cuts the stream) + if (i != buf.len) { + var n = self.next(); + while (i < buf.len) : (i += 1) { + buf[i] = @truncate(u8, n); + n >>= 8; + } + } + } +}; + +test "Sfc64 sequence" { + // Unfortunately there does not seem to be an official test sequence. + var r = Sfc64.init(0); + + const seq = [_]u64{ + 0x3acfa029e3cc6041, + 0xf5b6515bf2ee419c, + 0x1259635894a29b61, + 0xb6ae75395f8ebd6, + 0x225622285ce302e2, + 0x520d28611395cb21, + 0xdb909c818901599d, + 0x8ffd195365216f57, + 0xe8c4ad5e258ac04a, + 0x8f8ef2c89fdb63ca, + 0xf9865b01d98d8e2f, + 0x46555871a65d08ba, + 0x66868677c6298fcd, + 0x2ce15a7e6329f57d, + 0xb2f1833ca91ca79, + 0x4b0890ac9bf453ca, + }; + + for (seq) |s| { + expectEqual(s, r.next()); + } +} + +// Actual Random helper function tests, pcg engine is assumed correct. +test "Random float" { + var prng = DefaultPrng.init(0); + + var i: usize = 0; + while (i < 1000) : (i += 1) { + const val1 = prng.random.float(f32); + expect(val1 >= 0.0); + expect(val1 < 1.0); + + const val2 = prng.random.float(f64); + expect(val2 >= 0.0); + expect(val2 < 1.0); + } +} + +test "Random shuffle" { + var prng = DefaultPrng.init(0); + + var seq = [_]u8{ 0, 1, 2, 3, 4 }; + var seen = [_]bool{false} ** 5; + + var i: usize = 0; + while (i < 1000) : (i += 1) { + prng.random.shuffle(u8, seq[0..]); + seen[seq[0]] = true; + expect(sumArray(seq[0..]) == 10); + } + + // we should see every entry at the head at least once + for (seen) |e| { + expect(e == true); + } +} + +fn sumArray(s: []const u8) u32 { + var r: u32 = 0; + for (s) |e| + r += e; + return r; +} + +test "Random range" { + var prng = DefaultPrng.init(0); + testRange(&prng.random, -4, 3); + testRange(&prng.random, -4, -1); + testRange(&prng.random, 10, 14); + testRange(&prng.random, -0x80, 0x7f); +} + +fn testRange(r: *Random, start: i8, end: i8) void { + testRangeBias(r, start, end, true); + testRangeBias(r, start, end, false); +} +fn testRangeBias(r: *Random, start: i8, end: i8, biased: bool) void { + const count = @intCast(usize, i32(end) - i32(start)); + var values_buffer = [_]bool{false} ** 0x100; + const values = values_buffer[0..count]; + var i: usize = 0; + while (i < count) { + const value: i32 = if (biased) r.intRangeLessThanBiased(i8, start, end) else r.intRangeLessThan(i8, start, end); + const index = @intCast(usize, value - start); + if (!values[index]) { + i += 1; + values[index] = true; + } + } +} |
