Skip to content
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

fix #17467 1st call to rand is now non-skewed; allow seed == 0 #17468

Merged
merged 7 commits into from
May 11, 2021
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
16 changes: 11 additions & 5 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
- Nim compiler now adds ASCII unit separator `\31` before a newline for every generated
message (potentially multiline), so tooling can tell when messages start and end.

- `random.initRand(seed)` now produces non-skewed values for the 1st call to `rand()` after
initialization with a small (< 30000) seed. Use `-d:nimLegacyRandomInitRand` to restore
previous behavior for a transition time, see PR #17467.


## Standard library additions and changes
- Added support for parenthesized expressions in `strformat`
Expand Down Expand Up @@ -117,6 +121,13 @@
- Added `randState` template that exposes the default random number generator.
Useful for library authors.

- Added `random.initRand()` overload with no argument which uses the current time as a seed.

- `random.initRand(seed)` now allows `seed == 0`.

- Added `std/sysrand` module to get random numbers from a secure source
provided by the operating system.

- Added `std/enumutils` module. Added `genEnumCaseStmt` macro that generates case statement to parse string to enum.
Added `items` for enums with holes.
Added `symbolName` to return the enum symbol name ignoring the human readable name.
Expand Down Expand Up @@ -203,9 +214,6 @@

- Deprecated `any`. See https://github.com/nim-lang/RFCs/issues/281

- Added `std/sysrand` module to get random numbers from a secure source
provided by the operating system.

- Added optional `options` argument to `copyFile`, `copyFileToDir`, and
`copyFileWithPermissions`. By default, on non-Windows OSes, symlinks are
followed (copy files symlinks point to); on Windows, `options` argument is
Expand All @@ -223,8 +231,6 @@
- Added `os.isAdmin` to tell whether the caller's process is a member of the
Administrators local group (on Windows) or a root (on POSIX).

- Added `random.initRand()` overload with no argument which uses the current time as a seed.

- Added experimental `linenoise.readLineStatus` to get line and status (e.g. ctrl-D or ctrl-C).

- Added `compilesettings.SingleValueSetting.libPath`.
Expand Down
93 changes: 36 additions & 57 deletions lib/pure/random.nim
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,10 @@ proc next*(r: var Rand): uint64 =
## that accepts a slice
## * `rand proc<#rand,typedesc[T]>`_ that accepts an integer or range type
## * `skipRandomNumbers proc<#skipRandomNumbers,Rand>`_
runnableExamples:
runnableExamples("-r:off"):
var r = initRand(2019)
doAssert r.next() == 138_744_656_611_299'u64
doAssert r.next() == 979_810_537_855_049_344'u64
doAssert r.next() == 3_628_232_584_225_300_704'u64
assert r.next() == 13223559681708962501'u64 # implementation defined
assert r.next() == 7229677234260823147'u64 # ditto

let s0 = r.a0
var s1 = r.a1
Expand Down Expand Up @@ -214,11 +213,9 @@ proc rand*(r: var Rand; max: Natural): int {.benign.} =
## * `rand proc<#rand,Rand,HSlice[T: Ordinal or float or float32 or float64,T: Ordinal or float or float32 or float64]>`_
## that accepts a slice
## * `rand proc<#rand,typedesc[T]>`_ that accepts an integer or range type
runnableExamples:
runnableExamples("-r:off"):
var r = initRand(123)
doAssert r.rand(100) == 0
doAssert r.rand(100) == 96
doAssert r.rand(100) == 66
assert r.rand(100) == 96 # implementation defined

if max == 0: return
while true:
Expand All @@ -241,11 +238,9 @@ proc rand*(max: int): int {.benign.} =
## * `rand proc<#rand,HSlice[T: Ordinal or float or float32 or float64,T: Ordinal or float or float32 or float64]>`_
## that accepts a slice
## * `rand proc<#rand,typedesc[T]>`_ that accepts an integer or range type
runnableExamples:
runnableExamples("-r:off"):
randomize(123)
doAssert rand(100) == 0
doAssert rand(100) == 96
doAssert rand(100) == 66
assert [rand(100), rand(100)] == [96, 63] # implementation defined

rand(state, max)

Expand Down Expand Up @@ -305,10 +300,8 @@ proc rand*[T: Ordinal or SomeFloat](r: var Rand; x: HSlice[T, T]): T =
## * `rand proc<#rand,typedesc[T]>`_ that accepts an integer or range type
runnableExamples:
var r = initRand(345)
doAssert r.rand(1..6) == 4
doAssert r.rand(1..6) == 4
doAssert r.rand(1..6) == 6
let f = r.rand(-1.0 .. 1.0) # 0.8741183448756229
assert r.rand(1..5) <= 5
assert r.rand(-1.1 .. 1.2) >= -1.1
assert x.a <= x.b
when T is SomeFloat:
result = rand(r, x.b - x.a) + x.a
Expand All @@ -333,9 +326,7 @@ proc rand*[T: Ordinal or SomeFloat](x: HSlice[T, T]): T =
## * `rand proc<#rand,typedesc[T]>`_ that accepts an integer or range type
runnableExamples:
randomize(345)
doAssert rand(1..6) == 4
doAssert rand(1..6) == 4
doAssert rand(1..6) == 6
assert rand(1..6) <= 6

result = rand(state, x)

Expand All @@ -354,16 +345,12 @@ proc rand*[T: SomeInteger](t: typedesc[T]): T =
## that accepts a slice
runnableExamples:
randomize(567)
doAssert rand(int8) == 55
doAssert rand(int8) == -42
doAssert rand(int8) == 43
doAssert rand(uint32) == 578980729'u32
doAssert rand(uint32) == 4052940463'u32
doAssert rand(uint32) == 2163872389'u32
doAssert rand(range[1..16]) == 11
doAssert rand(range[1..16]) == 4
doAssert rand(range[1..16]) == 16

if false: # implementation defined
assert rand(int8) == -42
assert rand(uint32) == 578980729'u32
assert rand(range[1..16]) == 11
# pending csources >= 1.4.0 or fixing https://github.com/timotheecour/Nim/issues/251#issuecomment-831599772,
# use `runnableExamples("-r:off")` instead of `if false`
when T is range:
result = rand(state, low(T)..high(T))
else:
Expand All @@ -380,9 +367,7 @@ proc sample*[T](r: var Rand; s: set[T]): T =
runnableExamples:
var r = initRand(987)
let s = {1, 3, 5, 7, 9}
doAssert r.sample(s) == 5
doAssert r.sample(s) == 7
doAssert r.sample(s) == 1
assert r.sample(s) in s

assert card(s) != 0
var i = rand(r, card(s) - 1)
Expand All @@ -406,9 +391,7 @@ proc sample*[T](s: set[T]): T =
runnableExamples:
randomize(987)
let s = {1, 3, 5, 7, 9}
doAssert sample(s) == 5
doAssert sample(s) == 7
doAssert sample(s) == 1
assert sample(s) in s

sample(state, s)

Expand All @@ -423,9 +406,7 @@ proc sample*[T](r: var Rand; a: openArray[T]): T =
runnableExamples:
let marbles = ["red", "blue", "green", "yellow", "purple"]
var r = initRand(456)
doAssert r.sample(marbles) == "blue"
doAssert r.sample(marbles) == "yellow"
doAssert r.sample(marbles) == "red"
assert r.sample(marbles) in marbles

result = a[r.rand(a.low..a.high)]

Expand All @@ -445,9 +426,7 @@ proc sample*[T](a: openArray[T]): T =
runnableExamples:
let marbles = ["red", "blue", "green", "yellow", "purple"]
randomize(456)
doAssert sample(marbles) == "blue"
doAssert sample(marbles) == "yellow"
doAssert sample(marbles) == "red"
assert sample(marbles) in marbles

result = a[rand(a.low..a.high)]

Expand Down Expand Up @@ -476,9 +455,7 @@ proc sample*[T, U](r: var Rand; a: openArray[T]; cdf: openArray[U]): T =
let count = [1, 6, 8, 3, 4]
let cdf = count.cumsummed
var r = initRand(789)
doAssert r.sample(marbles, cdf) == "red"
doAssert r.sample(marbles, cdf) == "green"
doAssert r.sample(marbles, cdf) == "blue"
assert r.sample(marbles, cdf) in marbles

assert(cdf.len == a.len) # Two basic sanity checks.
assert(float(cdf[^1]) > 0.0)
Expand Down Expand Up @@ -512,9 +489,7 @@ proc sample*[T, U](a: openArray[T]; cdf: openArray[U]): T =
let count = [1, 6, 8, 3, 4]
let cdf = count.cumsummed
randomize(789)
doAssert sample(marbles, cdf) == "red"
doAssert sample(marbles, cdf) == "green"
doAssert sample(marbles, cdf) == "blue"
assert sample(marbles, cdf) in marbles

state.sample(a, cdf)

Expand Down Expand Up @@ -547,10 +522,10 @@ proc gauss*(mu = 0.0, sigma = 1.0): float {.since: (1, 3).} =
proc initRand*(seed: int64): Rand =
## Initializes a new `Rand <#Rand>`_ state using the given seed.
##
## `seed` must not be zero. Providing a specific seed will produce
## the same results for that seed each time.
## Providing a specific seed will produce the same results for that seed each time.
##
## The resulting state is independent of the default RNG's state.
## The resulting state is independent of the default RNG's state. When `seed == 0`,
## we internally set the seed to an implementation defined non-zero value.
##
## **See also:**
## * `initRand proc<#initRand>`_ that uses the current time
Expand All @@ -560,20 +535,22 @@ proc initRand*(seed: int64): Rand =
from std/times import getTime, toUnix, nanosecond

var r1 = initRand(123)

let now = getTime()
var r2 = initRand(now.toUnix * 1_000_000_000 + now.nanosecond)

doAssert seed != 0 # 0 causes `rand(int)` to always return 0 for example.
const seedFallback0 = int32.high # arbitrary
let seed = if seed != 0: seed else: seedFallback0 # because 0 is a fixed point
result.a0 = Ui(seed shr 16)
result.a1 = Ui(seed and 0xffff)
when not defined(nimLegacyRandomInitRand):
# calling `discard next(result)` (even a few times) would still produce
# skewed numbers for the 1st call to `rand()`.
skipRandomNumbers(result)
discard next(result)

proc randomize*(seed: int64) {.benign.} =
## Initializes the default random number generator with the given seed.
##
## `seed` must not be zero. Providing a specific seed will produce
## the same results for that seed each time.
## Providing a specific seed will produce the same results for that seed each time.
##
## **See also:**
## * `initRand proc<#initRand,int64>`_ that initializes a Rand state
Expand All @@ -600,7 +577,8 @@ proc shuffle*[T](r: var Rand; x: var openArray[T]) =
var cards = ["Ace", "King", "Queen", "Jack", "Ten"]
var r = initRand(678)
r.shuffle(cards)
doAssert cards == ["King", "Ace", "Queen", "Ten", "Jack"]
import std/algorithm
assert cards.sorted == @["Ace", "Jack", "King", "Queen", "Ten"]

for i in countdown(x.high, 1):
let j = r.rand(i)
Expand All @@ -620,7 +598,8 @@ proc shuffle*[T](x: var openArray[T]) =
var cards = ["Ace", "King", "Queen", "Jack", "Ten"]
randomize(678)
shuffle(cards)
doAssert cards == ["King", "Ace", "Queen", "Ten", "Jack"]
import std/algorithm
assert cards.sorted == @["Ace", "Jack", "King", "Queen", "Ten"]

shuffle(state, x)

Expand Down
2 changes: 1 addition & 1 deletion testament/important_packages.nim
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pkg "itertools", "nim doc src/itertools.nim"
pkg "iterutils"
pkg "jstin"
pkg "karax", "nim c -r tests/tester.nim"
pkg "kdtree", "nimble test", "https://github.com/jblindsay/kdtree"
pkg "kdtree", "nimble test -d:nimLegacyRandomInitRand", "https://github.com/jblindsay/kdtree"
pkg "loopfusion"
pkg "macroutils"
pkg "manu"
Expand Down
31 changes: 27 additions & 4 deletions tests/stdlib/trandom.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
discard """
joinable: false
joinable: false # to avoid messing with global rand state
targets: "c js"
"""

Expand Down Expand Up @@ -37,11 +37,14 @@ main()

block:
when not defined(js):
doAssert almostEqual(rand(12.5), 4.012897747078944)
doAssert almostEqual(rand(2233.3322), 879.702755321298)
doAssert almostEqual(rand(12.5), 7.355175342026979)
doAssert almostEqual(rand(2233.3322), 499.342386778917)

type DiceRoll = range[0..6]
doAssert rand(DiceRoll).int == 4
when not defined(js):
doAssert rand(DiceRoll).int == 3
else:
doAssert rand(DiceRoll).int == 6

var rs: RunningStat
for j in 1..5:
Expand Down Expand Up @@ -164,3 +167,23 @@ block: # random sample
let stdDev = sqrt(n * p * (1.0 - p))
# NOTE: like unnormalized int CDF test, P(wholeTestFails) =~ 0.01.
doAssert abs(float(histo[values[i]]) - expected) <= 3.0 * stdDev

block:
# 0 is a valid seed
var r = initRand(0)
doAssert r.rand(1.0) != r.rand(1.0)
r = initRand(10)
doAssert r.rand(1.0) != r.rand(1.0)
# changing the seed changes the sequence
var r1 = initRand(123)
var r2 = initRand(124)
doAssert r1.rand(1.0) != r2.rand(1.0)

block: # bug #17467
let n = 1000
for i in -n .. n:
var r = initRand(i)
let x = r.rand(1.0)
doAssert x > 1e-4, $(x, i)
# This used to fail for each i in 0..<26844, i.e. the 1st produced value
# was predictable and < 1e-4, skewing distributions.