feat: Add fastrand for PRNG support

Replaces the custom LCG implementation with `fastrand` for improved
random number generation.
This commit introduces the `fastrand` crate to the project for robust
and efficient pseudo-random number generation.
The `Environment` struct now includes a `prng` field to hold the random
number generator.
The built-in `random` function now utilizes this PRNG for generating
floating-point random numbers.
A new `seed!` function is added to allow users to seed the PRNG for
deterministic random sequences.
This change enhances the randomness capabilities of the language, making
it suitable for simulations and other applications requiring good
quality random numbers.
This commit is contained in:
Michael Schimmel
2026-02-22 09:08:16 +01:00
parent df06c51205
commit 6605f56756
5 changed files with 299 additions and 226 deletions
Generated
+1
View File
@@ -1915,6 +1915,7 @@ dependencies = [
"chrono",
"clap",
"eframe",
"fastrand",
"insta",
"regex",
]
+1
View File
@@ -9,6 +9,7 @@ eframe = "0.33.3"
clap = { version = "4.5", features = ["derive"] }
chrono = "0.4"
regex = "1.10"
fastrand = "2.3"
[dev-dependencies]
insta = { version = "1.39", features = ["yaml"] }
+2
View File
@@ -23,6 +23,7 @@ pub struct Environment {
pub global_types: Rc<RefCell<HashMap<u32, StaticType>>>,
pub global_purity: Rc<RefCell<HashMap<u32, Purity>>>,
pub global_values: Rc<RefCell<Vec<Value>>>,
pub prng: Rc<RefCell<fastrand::Rng>>,
pub function_registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
pub typed_function_registry: Rc<RefCell<HashMap<u32, TypedNode>>>,
pub monomorph_cache: Rc<RefCell<MonoCache>>,
@@ -89,6 +90,7 @@ impl Environment {
global_types: Rc::new(RefCell::new(HashMap::new())),
global_purity: Rc::new(RefCell::new(HashMap::new())),
global_values: Rc::new(RefCell::new(Vec::new())),
prng: Rc::new(RefCell::new(fastrand::Rng::new())),
function_registry: Rc::new(RefCell::new(HashMap::new())),
typed_function_registry: Rc::new(RefCell::new(HashMap::new())),
monomorph_cache: Rc::new(RefCell::new(HashMap::new())),
+15 -7
View File
@@ -528,13 +528,21 @@ fn register_math(env: &Environment) {
ret: StaticType::Float,
}));
// Simple LCG (no dependencies required)
static mut SEED: u64 = 0x12345678;
env.register_native("random", random_ty, Purity::SideEffectFree, |_| {
unsafe {
SEED = SEED.wrapping_mul(6364136223846793005).wrapping_add(1);
let val = (SEED >> 32) as f64 / (u32::MAX as f64);
Value::Float(val)
let prng = env.prng.clone();
env.register_native("random", random_ty, Purity::SideEffectFree, move |_| {
Value::Float(prng.borrow_mut().f64())
});
let seed_ty = StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Int]),
ret: StaticType::Void,
}));
let prng_seed = env.prng.clone();
env.register_native("seed!", seed_ty, Purity::Impure, move |args| {
if let Value::Int(s) = args[0] {
prng_seed.borrow_mut().seed(s as u64);
}
Value::Void
});
}
+63 -2
View File
@@ -169,6 +169,59 @@ mod tests {
assert_eq!(format!("{}", env.run_script("NaN").unwrap()), "NaN");
}
#[test]
fn test_random_isolation_between_environments() {
let env1 = Environment::new();
let env2 = Environment::new();
// 1. Seed env1
env1.run_script("(seed! 123)").unwrap();
let val1_a = env1.run_script("(random)").unwrap();
// 2. env2 should have its own default seed state
let val2_a = env2.run_script("(random)").unwrap();
// They are highly unlikely to be equal by default,
// and seeding env1 MUST not have seeded env2.
assert_ne!(
val1_a, val2_a,
"Environments must have isolated PRNG states"
);
// 3. Seed env2 differently
env2.run_script("(seed! 123)").unwrap();
let val2_b = env2.run_script("(random)").unwrap();
// After same seeding, they should match (isolated but identical seed)
assert_eq!(
val1_a, val2_b,
"Different environments with the same seed must produce the same sequence"
);
}
#[test]
fn test_random_seeding_determinism() {
let env = Environment::new();
// 1. First run with seed 42
env.run_script("(seed! 42)").unwrap();
let val1 = env.run_script("(random)").unwrap();
// 2. Second run with same seed 42
env.run_script("(seed! 42)").unwrap();
let val2 = env.run_script("(random)").unwrap();
assert_eq!(
val1, val2,
"Random results must be identical for the same seed"
);
// 3. Third run with different seed
env.run_script("(seed! 123)").unwrap();
let val3 = env.run_script("(random)").unwrap();
assert_ne!(val1, val3, "Random results must differ for different seeds");
}
#[test]
fn test_now_function_not_folded() {
let env = Environment::new();
@@ -186,8 +239,16 @@ mod tests {
// 2. Verify it's NOT constant folded in the AST dump
let dump = env.dump_ast(source).expect("Failed to dump AST");
assert!(dump.contains("Call"), "now() should remain a Call, not a Constant. Dump: \n{}", dump);
assert!(!dump.contains("Constant: #"), "now() should NOT be folded into a specific timestamp constant. Dump: \n{}", dump);
assert!(
dump.contains("Call"),
"now() should remain a Call, not a Constant. Dump: \n{}",
dump
);
assert!(
!dump.contains("Constant: #"),
"now() should NOT be folded into a specific timestamp constant. Dump: \n{}",
dump
);
}
#[test]