From 2111fcac6272c70013ef78d0389ce379b95abcdb Mon Sep 17 00:00:00 2001 From: jess Date: Thu, 28 May 2026 03:09:36 -0700 Subject: [PATCH] rand() and .rand() see wiki for details --- core/src/interp.rs | 154 +++++++++++++++++++++++++++++++++++++++++ viewport/src/syntax.rs | 2 + 2 files changed, 156 insertions(+) diff --git a/core/src/interp.rs b/core/src/interp.rs index ea87ff6..4787442 100644 --- a/core/src/interp.rs +++ b/core/src/interp.rs @@ -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 = 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(); diff --git a/viewport/src/syntax.rs b/viewport/src/syntax.rs index ead1cfb..b401903 100644 --- a/viewport/src/syntax.rs +++ b/viewport/src/syntax.rs @@ -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" )