From 0a1bdde25feaa6128cacd2a5f28fb2d123580637 Mon Sep 17 00:00:00 2001 From: Oleksandr Kozachuk Date: Wed, 15 Apr 2026 20:31:48 +0200 Subject: [PATCH] =?UTF-8?q?Add=20RANDOM=20/=20RND-SEED=20=E2=80=94=20xorsh?= =?UTF-8?q?ift64=20PRNG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/core/src/outer.rs | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/crates/core/src/outer.rs b/crates/core/src/outer.rs index 20f502d..949e74c 100644 --- a/crates/core/src/outer.rs +++ b/crates/core/src/outer.rs @@ -259,6 +259,8 @@ pub struct ForthVM { search_order: Arc>>, /// Next wordlist ID to allocate (shared). next_wid: Arc>, + /// xorshift64 PRNG state for RANDOM / RND-SEED. + rng_state: Arc>, } impl ForthVM { @@ -326,6 +328,14 @@ impl ForthVM { 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 ForthVM { // 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 ForthVM { 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" -- 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::::new().unwrap(); + vm.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap(); + let first = vm.data_stack().to_vec(); + + let mut vm2 = ForthVM::::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::::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::::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 // ===================================================================