Add RANDOM / RND-SEED — xorshift64 PRNG
Non-standard but ubiquitous in gforth/SwiftForth/VFX. Adds a shared rng_state on ForthVM, seeded from nanosecond wall-clock at boot. `RANDOM ( -- u )` returns a 32-bit pseudo-random cell; `RND-SEED ( u -- )` reseeds, with 0 forced to a nonzero constant to avoid xorshift's fixed point. Three tests cover determinism after seeding, distinct-value spread across 1000 pulls, and the zero-seed safeguard.
This commit is contained in:
@@ -259,6 +259,8 @@ pub struct ForthVM<R: Runtime> {
|
||||
search_order: Arc<Mutex<Vec<u32>>>,
|
||||
/// Next wordlist ID to allocate (shared).
|
||||
next_wid: Arc<Mutex<u32>>,
|
||||
/// xorshift64 PRNG state for RANDOM / RND-SEED.
|
||||
rng_state: Arc<Mutex<u64>>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> ForthVM<R> {
|
||||
@@ -326,6 +328,14 @@ impl<R: Runtime> ForthVM<R> {
|
||||
substitutions: Arc::new(Mutex::new(HashMap::new())),
|
||||
search_order: Arc::new(Mutex::new(vec![1])),
|
||||
next_wid: Arc::new(Mutex::new(2)),
|
||||
rng_state: {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let seed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0xDEAD_BEEF_CAFE_BABE);
|
||||
Arc::new(Mutex::new(if seed == 0 { 0xDEAD_BEEF_CAFE_BABE } else { seed }))
|
||||
},
|
||||
};
|
||||
|
||||
vm.register_primitives()?;
|
||||
@@ -2580,6 +2590,9 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// UTIME ( -- ud ) microseconds since epoch as double-cell
|
||||
self.register_utime()?;
|
||||
|
||||
// RANDOM ( -- u ), RND-SEED ( u -- )
|
||||
self.register_random()?;
|
||||
|
||||
// HOLDS
|
||||
// HOLDS: defined in boot.fth
|
||||
|
||||
@@ -5094,6 +5107,42 @@ impl<R: Runtime> ForthVM<R> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// RANDOM ( -- u ) return a 32-bit pseudo-random cell (xorshift64).
|
||||
/// RND-SEED ( u -- ) reseed the PRNG; seed=0 is forced to a nonzero constant.
|
||||
fn register_random(&mut self) -> anyhow::Result<()> {
|
||||
let state = Arc::clone(&self.rng_state);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let mut s = state.lock().unwrap();
|
||||
let mut x = *s;
|
||||
if x == 0 {
|
||||
x = 0xDEAD_BEEF_CAFE_BABE;
|
||||
}
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
*s = x;
|
||||
drop(s);
|
||||
let sp = ctx.get_dsp();
|
||||
let new_sp = sp - CELL_SIZE;
|
||||
ctx.mem_write_i32(new_sp as u32, x as i32);
|
||||
ctx.set_dsp(new_sp);
|
||||
Ok(())
|
||||
});
|
||||
self.register_host_primitive("RANDOM", false, func)?;
|
||||
|
||||
let state = Arc::clone(&self.rng_state);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let seed = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
||||
ctx.set_dsp(sp + CELL_SIZE);
|
||||
let mut s = state.lock().unwrap();
|
||||
*s = if seed == 0 { 0xDEAD_BEEF_CAFE_BABE } else { seed };
|
||||
Ok(())
|
||||
});
|
||||
self.register_host_primitive("RND-SEED", false, func)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// PARSE ( char "ccc<char>" -- c-addr u ) as inline host function.
|
||||
fn register_parse_host(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
@@ -7626,6 +7675,48 @@ mod tests {
|
||||
assert_eq!(vm.take_output(), "test");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// New words: RANDOM / RND-SEED
|
||||
// ===================================================================
|
||||
|
||||
#[test]
|
||||
fn test_random_deterministic_after_seed() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap();
|
||||
let first = vm.data_stack().to_vec();
|
||||
|
||||
let mut vm2 = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm2.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap();
|
||||
let second = vm2.data_stack().to_vec();
|
||||
|
||||
assert_eq!(first, second, "same seed must produce same sequence");
|
||||
assert_eq!(first.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_distinct_values() {
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate("1 RND-SEED").unwrap();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..1000 {
|
||||
vm.evaluate("RANDOM").unwrap();
|
||||
let v = vm.pop_data_stack().unwrap();
|
||||
seen.insert(v);
|
||||
}
|
||||
// xorshift64's low-32 sequence repeats after a long period; 1000 pulls
|
||||
// should hit at least 900 unique cells.
|
||||
assert!(seen.len() >= 900, "only {} distinct out of 1000", seen.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rnd_seed_zero_forced_nonzero() {
|
||||
// xorshift with state 0 is a fixed point; seeding with 0 must avoid that.
|
||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||
vm.evaluate("0 RND-SEED RANDOM RANDOM").unwrap();
|
||||
let stack = vm.data_stack();
|
||||
assert!(stack[0] != 0 || stack[1] != 0, "seed-0 must not freeze the stream");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// New words: COUNT
|
||||
// ===================================================================
|
||||
|
||||
Reference in New Issue
Block a user