rand() and .rand()

see wiki for details
This commit is contained in:
jess 2026-05-28 03:09:36 -07:00
parent 24958d4896
commit 2111fcac62
2 changed files with 156 additions and 0 deletions

View File

@ -1671,6 +1671,40 @@ pub struct TableWrite {
const MAX_ITERATIONS: usize = 10_000;
const MAX_CALL_DEPTH: u32 = 256;
thread_local! {
static RNG_STATE: std::cell::Cell<u64> = std::cell::Cell::new(seed_from_time());
}
fn seed_from_time() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64).unwrap_or(0x9E3779B97F4A7C15);
nanos.wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEFCAFEBABE
}
/// xorshift64* — pulls the next u64 from the thread-local PRNG.
fn rng_next_u64() -> u64 {
RNG_STATE.with(|s| {
let mut x = s.get();
if x == 0 { x = 0x9E3779B97F4A7C15; }
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
s.set(x);
x.wrapping_mul(0x2545F4914F6CDD1D)
})
}
/// uniform f64 in [0, 1).
fn rng_next_unit() -> f64 {
(rng_next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
/// seeds the thread-local PRNG to a deterministic value.
pub fn seed_rng(seed: u64) {
RNG_STATE.with(|s| s.set(if seed == 0 { 0x9E3779B97F4A7C15 } else { seed }));
}
impl Interpreter {
pub fn new() -> Self {
Interpreter {
@ -2403,6 +2437,52 @@ impl Interpreter {
};
return Ok(retag_spice(Value::Number(result), unit));
}
"rand" => {
match args.len() {
0 => return Ok(Value::Number(rng_next_unit())),
1 => {
let v = self.eval_expr(&args[0], depth)?;
match v {
Value::Number(n) => {
if n <= 0.0 { return Err("rand(n) expects n > 0".into()); }
return Ok(Value::Number((rng_next_unit() * n).floor()));
}
Value::Array(a) => {
if a.is_empty() { return Err("rand(array) on empty array".into()); }
let idx = (rng_next_unit() * a.len() as f64) as usize;
return Ok(a[idx.min(a.len() - 1)].clone());
}
_ => return Err("rand(x) expects a number or array".into()),
}
}
2 => {
let lo = match self.eval_expr(&args[0], depth)? {
Value::Number(n) => n,
_ => return Err("rand(lo, hi) expects numbers".into()),
};
let hi = match self.eval_expr(&args[1], depth)? {
Value::Number(n) => n,
_ => return Err("rand(lo, hi) expects numbers".into()),
};
if hi <= lo { return Err("rand(lo, hi) requires hi > lo".into()); }
return Ok(Value::Number(lo + rng_next_unit() * (hi - lo)));
}
_ => return Err("rand() takes 0, 1, or 2 arguments".into()),
}
}
"seed" => {
if args.len() != 1 {
return Err("seed() expects 1 argument".into());
}
let v = self.eval_expr(&args[0], depth)?;
match v {
Value::Number(n) => {
seed_rng(n.to_bits());
return Ok(Value::Void);
}
_ => return Err("seed() expects a number".into()),
}
}
"floor" | "ceil" | "round" => {
if args.is_empty() || args.len() > 2 {
return Err(format!("{}() expects 1 or 2 arguments", name));
@ -6084,6 +6164,80 @@ fn find(arr, target) {
assert!(matches!(v, Value::Number(n) if n == 3.0));
}
#[test]
fn rand_unit_range() {
let mut i = Interpreter::new();
i.exec_line("seed(42)").unwrap();
for _ in 0..20 {
let v = i.eval_expr_str("rand()").unwrap();
match v {
Value::Number(n) => assert!(n >= 0.0 && n < 1.0, "rand() = {}", n),
_ => panic!("rand() should return Number"),
}
}
}
#[test]
fn rand_integer_bound() {
let mut i = Interpreter::new();
i.exec_line("seed(7)").unwrap();
for _ in 0..30 {
let v = i.eval_expr_str("rand(10)").unwrap();
match v {
Value::Number(n) => assert!(n >= 0.0 && n < 10.0 && n == n.trunc()),
_ => panic!(),
}
}
}
#[test]
fn rand_lo_hi_range() {
let mut i = Interpreter::new();
i.exec_line("seed(99)").unwrap();
for _ in 0..30 {
let v = i.eval_expr_str("rand(5, 8)").unwrap();
match v {
Value::Number(n) => assert!(n >= 5.0 && n < 8.0),
_ => panic!(),
}
}
}
#[test]
fn rand_picks_from_array() {
let mut i = Interpreter::new();
i.exec_line("seed(1)").unwrap();
let v = i.eval_expr_str("rand([10, 20, 30])").unwrap();
match v {
Value::Number(n) => assert!(n == 10.0 || n == 20.0 || n == 30.0),
_ => panic!(),
}
}
#[test]
fn rand_seed_is_deterministic() {
let mut a = Interpreter::new();
a.exec_line("seed(12345)").unwrap();
let va = a.eval_expr_str("rand()").unwrap();
a.exec_line("seed(12345)").unwrap();
let vb = a.eval_expr_str("rand()").unwrap();
match (va, vb) {
(Value::Number(x), Value::Number(y)) => assert_eq!(x.to_bits(), y.to_bits()),
_ => panic!(),
}
}
#[test]
fn rand_method_call_on_array() {
let mut i = Interpreter::new();
i.exec_line("seed(3)").unwrap();
let v = i.eval_expr_str("[1, 2, 3, 4, 5].rand()").unwrap();
match v {
Value::Number(n) => assert!((1.0..=5.0).contains(&n)),
_ => panic!(),
}
}
#[test]
fn for_loop_does_not_leak_var() {
let mut i = Interpreter::new();

View File

@ -728,6 +728,8 @@ fn is_cordial_builtin(w: &str) -> bool {
| "ring" | "iter" | "peek" | "history"
// aggregates
| "sum" | "avg" | "min" | "max" | "count" | "std_devp" | "std_devs"
// random
| "rand" | "seed"
// constants
| "pi"
)