From bc66f0a34c2fe90830f54c23cedefe56419709f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sun, 1 Feb 2026 15:16:20 +0100 Subject: [PATCH] Feat: adding logrand and exprand --- crates/forth/src/ops.rs | 2 ++ crates/forth/src/vm.rs | 22 +++++++++++++ crates/forth/src/words.rs | 22 +++++++++++++ tests/forth/randomness.rs | 66 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index cacb1f1..109f657 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -53,6 +53,8 @@ pub enum Op { Set, GetContext(String), Rand, + ExpRand, + LogRand, Seed, Cycle, PCycle, diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 5344552..a918fd1 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -460,6 +460,28 @@ impl Forth { } } } + Op::ExpRand => { + let hi = stack.pop().ok_or("stack underflow")?.as_float()?; + let lo = stack.pop().ok_or("stack underflow")?.as_float()?; + if lo <= 0.0 || hi <= 0.0 { + return Err("exprand requires positive values".into()); + } + let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) }; + let u: f64 = self.rng.lock().unwrap().gen(); + let val = lo * (hi / lo).powf(u); + stack.push(Value::Float(val, None)); + } + Op::LogRand => { + let hi = stack.pop().ok_or("stack underflow")?.as_float()?; + let lo = stack.pop().ok_or("stack underflow")?.as_float()?; + if lo <= 0.0 || hi <= 0.0 { + return Err("logrand requires positive values".into()); + } + let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) }; + let u: f64 = self.rng.lock().unwrap().gen(); + let val = hi * (lo / hi).powf(u); + stack.push(Value::Float(val, None)); + } Op::Seed => { let s = stack.pop().ok_or("stack underflow")?.as_int()?; *self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64); diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index a34a4cf..7400268 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -473,6 +473,26 @@ pub const WORDS: &[Word] = &[ compile: Simple, varargs: false, }, + Word { + name: "exprand", + aliases: &[], + category: "Probability", + stack: "(lo hi -- f)", + desc: "Exponential random biased toward lo. Both args must be positive", + example: "1.0 100.0 exprand => 3.7", + compile: Simple, + varargs: false, + }, + Word { + name: "logrand", + aliases: &[], + category: "Probability", + stack: "(lo hi -- f)", + desc: "Exponential random biased toward hi. Both args must be positive", + example: "1.0 100.0 logrand => 87.2", + compile: Simple, + varargs: false, + }, Word { name: "seed", aliases: &[], @@ -2508,6 +2528,8 @@ pub(super) fn simple_op(name: &str) -> Option { "sound" => Op::NewCmd, "." => Op::Emit, "rand" => Op::Rand, + "exprand" => Op::ExpRand, + "logrand" => Op::LogRand, "seed" => Op::Seed, "cycle" => Op::Cycle, "pcycle" => Op::PCycle, diff --git a/tests/forth/randomness.rs b/tests/forth/randomness.rs index 8160653..bd5208a 100644 --- a/tests/forth/randomness.rs +++ b/tests/forth/randomness.rs @@ -105,3 +105,69 @@ fn ftom_880() { fn mtof_ftom_roundtrip() { expect_float("60 mtof ftom", 60.0); } + +#[test] +fn exprand_in_range() { + let f = forth_seeded(12345); + f.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap(); + let val = stack_float(&f); + assert!(val >= 1.0 && val <= 100.0, "exprand {} not in [1, 100]", val); +} + +#[test] +fn exprand_deterministic() { + let f1 = forth_seeded(99); + let f2 = forth_seeded(99); + f1.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap(); + f2.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap(); + assert_eq!(f1.stack(), f2.stack()); +} + +#[test] +fn exprand_swapped_args() { + let f1 = forth_seeded(42); + let f2 = forth_seeded(42); + f1.evaluate("1.0 100.0 exprand", &default_ctx()).unwrap(); + f2.evaluate("100.0 1.0 exprand", &default_ctx()).unwrap(); + assert_eq!(f1.stack(), f2.stack()); +} + +#[test] +fn exprand_requires_positive() { + expect_error("0.0 10.0 exprand", "exprand requires positive values"); + expect_error("-1.0 10.0 exprand", "exprand requires positive values"); + expect_error("1.0 0.0 exprand", "exprand requires positive values"); +} + +#[test] +fn logrand_in_range() { + let f = forth_seeded(12345); + f.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap(); + let val = stack_float(&f); + assert!(val >= 1.0 && val <= 100.0, "logrand {} not in [1, 100]", val); +} + +#[test] +fn logrand_deterministic() { + let f1 = forth_seeded(99); + let f2 = forth_seeded(99); + f1.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap(); + f2.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap(); + assert_eq!(f1.stack(), f2.stack()); +} + +#[test] +fn logrand_swapped_args() { + let f1 = forth_seeded(42); + let f2 = forth_seeded(42); + f1.evaluate("1.0 100.0 logrand", &default_ctx()).unwrap(); + f2.evaluate("100.0 1.0 logrand", &default_ctx()).unwrap(); + assert_eq!(f1.stack(), f2.stack()); +} + +#[test] +fn logrand_requires_positive() { + expect_error("0.0 10.0 logrand", "logrand requires positive values"); + expect_error("-1.0 10.0 logrand", "logrand requires positive values"); + expect_error("1.0 0.0 logrand", "logrand requires positive values"); +}