flan/test/programs/rand.flan
Joseph Ferano a0278378ab Five randomness functions, and a draw wide enough to answer them
rand-int, rand, rand-bool, rand-int-range and rand-float-range, at the widths
the author ruled: a u64 draw and an f64 in [0, 1). rand-seed and rand-state
keep their names. The four old names are not names, and each is refused by the
one that is, with a call that compiles — in both the call and the bare-name
position, because a Lisp-1 makes the second a real thing to write.

The generator's step is untouched, so a seed means what it meant. Its output
function is not: PCG-XSH-RR folded the state to 32 bits, and no honest u64 or
53-bit f64 comes out of 32 bits without a second step. PCG-RXS-M-XS 64 answers
64 from the same one, so all five still cost exactly one draw and a seeded run
is reproducible. The price, written where it lives: the permutation is a
bijection of the state, which is what 64 output bits from 64 state bits costs.

The sequence is therefore a different one, and programs/rand.flan pins it —
reproducibility across the five, the single-draw cost of each, the half-open
boundaries, and rand-bool's count over a thousand flips.

sand.flan is not touched. It calls rand-f32, so the cases that compile or
re-evaluate it skip themselves on the fixture rather than on a comment: fix
its two calls and every one of them runs again. Its hash is the old
generator's grid and gets re-taken then.
2026-09-21 10:57:18 +07:00

103 lines
4.1 KiB
Plaintext

;;;; The five randomness functions, pinned.
;;;;
;;;; Three properties are asserted here and each is asserted as a *number* off
;;;; a fixed seed rather than as a range, because a range would still pass if
;;;; the derivation changed under it:
;;;;
;;;; 1. A seed fixes the sequence. The same five calls after the same seed
;;;; print the same five lines.
;;;; 2. A call that answers a number costs exactly one draw. Each block seeds,
;;;; makes one call, and prints the *next* rand-int: all five print the
;;;; same number, which is only true if each consumed one step of the
;;;; generator. The one call that is not a draw is the one that is not a
;;;; number — a range with nothing in it answers lo and leaves the
;;;; generator where it was, which is asserted under 3.
;;;; 3. The ranges are half-open and never divide by zero: [5, 5) and a
;;;; reversed range answer lo, a range of one always answers lo, a loop
;;;; over [0, 4) never produces 4, and the whole of i64 is a range like any
;;;; other — its span only exists by wrapping.
;;;;
;;;; And rand-bool over a thousand draws, which is the only one of the five
;;;; whose distribution a single number can check.
(defn main [] i32
;; 1. Reproducibility: two identical blocks, and therefore two identical
;; pairs of lines in the expected output.
(dotimes [round 2]
(rand-seed 20260921)
(print (rand-int)) (print " ")
(print (rand)) (print " ")
(print (rand-bool)) (print " ")
(print (rand-int-range -10 10)) (print " ")
(print (rand-float-range 2.0 3.0))
(println ""))
;; 2. One draw each. The first line is the second draw after seed 1, and
;; every line under it is that same draw reached through a different one of
;; the five.
(rand-seed 1) (rand-int) (println (rand-int))
(rand-seed 1) (rand) (println (rand-int))
(rand-seed 1) (rand-bool) (println (rand-int))
(rand-seed 1) (rand-int-range 0 100) (println (rand-int))
(rand-seed 1) (rand-float-range 0.0 1.0) (println (rand-int))
;; 3. The boundaries. An empty range, a reversed one, and a range of one
;; answer lo without dividing; a range at the extremes of i64 still lands
;; inside it.
(print (rand-int-range 5 5)) (print " ")
(print (rand-int-range 5 -5)) (print " ")
(let [ones 0]
(dotimes [i 20]
(when (= (rand-int-range 7 8) 7) (set ones (+ ones 1))))
(print ones))
(println "")
;; And an empty range is not a draw: two of them either side of a rand-int
;; leave it the number it would have been on its own. Seeded twice so the
;; two lines are the same number, which is the whole assertion.
(rand-seed 1) (println (rand-int))
(rand-seed 1)
(rand-int-range 5 5)
(rand-int-range 9 -9)
(println (rand-int))
;; And the widest range there is. hi - lo is 2^64 - 1 here and only reaches
;; that by wrapping, which is what the u64 it is read as makes right: the
;; answer has to be an i64 and not a number outside one.
(rand-seed 3)
(println (rand-int-range -9223372036854775808 9223372036854775807))
;; hi is excluded, and lo is reachable: over 2000 draws on [0, 4) the top
;; value seen is 3 and the bottom is 0.
(let [top 0
bottom 3
fours 0]
(dotimes [i 2000]
(let [v (i32 (rand-int-range 0 4))]
(set top (max top v))
(set bottom (min bottom v))
(when (= v 4) (set fours (+ fours 1)))))
(print top) (print " ") (print bottom) (print " ") (print fours)
(println ""))
;; The float range is half-open for the same reason: rand never reaches 1.0,
;; so nothing here reaches hi.
(let [over 0]
(dotimes [i 2000]
(let [v (rand-float-range -1.0 1.0)]
(when (or (>= v 1.0) (< v -1.0)) (set over (+ over 1)))))
(print over)
(println ""))
;; rand-bool over a thousand draws. The count is pinned, and a fair coin puts
;; it near 500 — a generator that had lost a bit would sit at 0 or 1000, and
;; one biased enough to matter would miss the band.
(rand-seed 99)
(let [heads 0]
(dotimes [i 1000]
(when (rand-bool) (set heads (+ heads 1))))
(print heads) (print " ")
(print (and (> heads 450) (< heads 550)))
(println ""))
0)