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", "chrono",
"clap", "clap",
"eframe", "eframe",
"fastrand",
"insta", "insta",
"regex", "regex",
] ]
+1
View File
@@ -9,6 +9,7 @@ eframe = "0.33.3"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
chrono = "0.4" chrono = "0.4"
regex = "1.10" regex = "1.10"
fastrand = "2.3"
[dev-dependencies] [dev-dependencies]
insta = { version = "1.39", features = ["yaml"] } 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_types: Rc<RefCell<HashMap<u32, StaticType>>>,
pub global_purity: Rc<RefCell<HashMap<u32, Purity>>>, pub global_purity: Rc<RefCell<HashMap<u32, Purity>>>,
pub global_values: Rc<RefCell<Vec<Value>>>, pub global_values: Rc<RefCell<Vec<Value>>>,
pub prng: Rc<RefCell<fastrand::Rng>>,
pub function_registry: Rc<RefCell<HashMap<u32, BoundNode>>>, pub function_registry: Rc<RefCell<HashMap<u32, BoundNode>>>,
pub typed_function_registry: Rc<RefCell<HashMap<u32, TypedNode>>>, pub typed_function_registry: Rc<RefCell<HashMap<u32, TypedNode>>>,
pub monomorph_cache: Rc<RefCell<MonoCache>>, pub monomorph_cache: Rc<RefCell<MonoCache>>,
@@ -89,6 +90,7 @@ impl Environment {
global_types: Rc::new(RefCell::new(HashMap::new())), global_types: Rc::new(RefCell::new(HashMap::new())),
global_purity: Rc::new(RefCell::new(HashMap::new())), global_purity: Rc::new(RefCell::new(HashMap::new())),
global_values: Rc::new(RefCell::new(Vec::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())), function_registry: Rc::new(RefCell::new(HashMap::new())),
typed_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())), monomorph_cache: Rc::new(RefCell::new(HashMap::new())),
+15 -7
View File
@@ -528,13 +528,21 @@ fn register_math(env: &Environment) {
ret: StaticType::Float, ret: StaticType::Float,
})); }));
// Simple LCG (no dependencies required) let prng = env.prng.clone();
static mut SEED: u64 = 0x12345678; env.register_native("random", random_ty, Purity::SideEffectFree, move |_| {
env.register_native("random", random_ty, Purity::SideEffectFree, |_| { Value::Float(prng.borrow_mut().f64())
unsafe { });
SEED = SEED.wrapping_mul(6364136223846793005).wrapping_add(1);
let val = (SEED >> 32) as f64 / (u32::MAX as f64); let seed_ty = StaticType::Function(Box::new(Signature {
Value::Float(val) 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
}); });
} }
+280 -219
View File
@@ -1,219 +1,280 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ast::environment::Environment; use crate::ast::environment::Environment;
use crate::ast::nodes::UntypedKind; use crate::ast::nodes::UntypedKind;
use crate::ast::parser::Parser; use crate::ast::parser::Parser;
use crate::ast::types::Value; use crate::ast::types::Value;
#[test] #[test]
fn test_parse_integer_constant() { fn test_parse_integer_constant() {
let source = "123"; let source = "123";
let mut parser = Parser::new(source).expect("Failed to create parser"); let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse"); let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Int(val)) = ast.kind { if let UntypedKind::Constant(Value::Int(val)) = ast.kind {
assert_eq!(val, 123); assert_eq!(val, 123);
} else { } else {
panic!("Expected Integer constant, got {:?}", ast.kind); panic!("Expected Integer constant, got {:?}", ast.kind);
} }
} }
#[test] #[test]
fn test_parse_negative_integer() { fn test_parse_negative_integer() {
let source = "-42"; let source = "-42";
let mut parser = Parser::new(source).expect("Failed to create parser"); let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse"); let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Int(val)) = ast.kind { if let UntypedKind::Constant(Value::Int(val)) = ast.kind {
assert_eq!(val, -42); assert_eq!(val, -42);
} else { } else {
panic!("Expected Integer constant, got {:?}", ast.kind); panic!("Expected Integer constant, got {:?}", ast.kind);
} }
} }
#[test] #[test]
fn test_parse_float_constant() { fn test_parse_float_constant() {
let source = "123.45"; let source = "123.45";
let mut parser = Parser::new(source).expect("Failed to create parser"); let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse"); let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Float(val)) = ast.kind { if let UntypedKind::Constant(Value::Float(val)) = ast.kind {
assert_eq!(val, 123.45); assert_eq!(val, 123.45);
} else { } else {
panic!("Expected Float constant, got {:?}", ast.kind); panic!("Expected Float constant, got {:?}", ast.kind);
} }
} }
#[test] #[test]
fn test_parse_negative_float() { fn test_parse_negative_float() {
let source = "-10.5"; let source = "-10.5";
let mut parser = Parser::new(source).expect("Failed to create parser"); let mut parser = Parser::new(source).expect("Failed to create parser");
let ast = parser.parse_expression().expect("Failed to parse"); let ast = parser.parse_expression().expect("Failed to parse");
if let UntypedKind::Constant(Value::Float(val)) = ast.kind { if let UntypedKind::Constant(Value::Float(val)) = ast.kind {
assert_eq!(val, -10.5); assert_eq!(val, -10.5);
} else { } else {
panic!("Expected Float constant, got {:?}", ast.kind); panic!("Expected Float constant, got {:?}", ast.kind);
} }
} }
#[test] #[test]
fn test_closure_modification_from_source() { fn test_closure_modification_from_source() {
let source = r#" let source = r#"
(do (do
(def x 10) (def x 10)
(def f (fn [] (assign x 20))) (def f (fn [] (assign x 20)))
(f) (f)
x x
) )
"#; "#;
let env = Environment::new(); let env = Environment::new();
let compiled = env.compile(source).expect("Failed to compile"); let compiled = env.compile(source).expect("Failed to compile");
let linked = env.link(compiled); let linked = env.link(compiled);
let result = env.run(&linked); let result = env.run(&linked);
match result { match result {
Ok(Value::Int(val)) => assert_eq!(val, 20, "Closure should modify outer variable"), Ok(Value::Int(val)) => assert_eq!(val, 20, "Closure should modify outer variable"),
Ok(val) => panic!("Expected Int(20), got {:?}", val), Ok(val) => panic!("Expected Int(20), got {:?}", val),
Err(e) => panic!("VM Error: {}", e), Err(e) => panic!("VM Error: {}", e),
} }
} }
#[test] #[test]
fn test_examples() { fn test_examples() {
for opt in [false, true] { for opt in [false, true] {
let results = crate::utils::tester::run_functional_tests_with_optimization(opt); let results = crate::utils::tester::run_functional_tests_with_optimization(opt);
for res in results { for res in results {
assert!( assert!(
res.success, res.success,
"Example {} failed at opt {}: {}", "Example {} failed at opt {}: {}",
res.name, opt, res.message res.name, opt, res.message
); );
} }
} }
} }
#[test] #[test]
fn test_debug_mode_logging() { fn test_debug_mode_logging() {
let mut env = Environment::new(); let mut env = Environment::new();
env.optimization = false; env.optimization = false;
let source = "(+ 10 20)"; let source = "(+ 10 20)";
let result = env.run_debug(source).expect("Failed to run debug"); let result = env.run_debug(source).expect("Failed to run debug");
let (val, logs) = result; let (val, logs) = result;
// 1. Check value // 1. Check value
match val { match val {
Ok(Value::Int(30)) => (), Ok(Value::Int(30)) => (),
_ => panic!("Expected Int(30), got {:?}", val), _ => panic!("Expected Int(30), got {:?}", val),
} }
// 2. Check logs (should have entries for + and constants) // 2. Check logs (should have entries for + and constants)
assert!(!logs.is_empty(), "Logs should not be empty"); assert!(!logs.is_empty(), "Logs should not be empty");
// Look for typical trace patterns // Look for typical trace patterns
let has_call = logs.iter().any(|l| l.contains("CALL")); let has_call = logs.iter().any(|l| l.contains("CALL"));
let has_const = logs.iter().any(|l| l.contains("CONST(10)")); let has_const = logs.iter().any(|l| l.contains("CONST(10)"));
let has_result = logs.iter().any(|l| l.contains("} -> 30")); let has_result = logs.iter().any(|l| l.contains("} -> 30"));
assert!(has_call, "Logs should contain CALL"); assert!(has_call, "Logs should contain CALL");
assert!(has_const, "Logs should contain CONST(10)"); assert!(has_const, "Logs should contain CONST(10)");
assert!(has_result, "Logs should contain result 30"); assert!(has_result, "Logs should contain result 30");
} }
#[test] #[test]
fn test_rtl_operators() { fn test_rtl_operators() {
let env = Environment::new(); let env = Environment::new();
// --- Arithmetic --- // --- Arithmetic ---
assert_eq!(format!("{}", env.run_script("(+ 10 20)").unwrap()), "30"); assert_eq!(format!("{}", env.run_script("(+ 10 20)").unwrap()), "30");
assert_eq!(format!("{}", env.run_script("(- 20 10)").unwrap()), "10"); assert_eq!(format!("{}", env.run_script("(- 20 10)").unwrap()), "10");
assert_eq!(format!("{}", env.run_script("(* 10 20)").unwrap()), "200"); assert_eq!(format!("{}", env.run_script("(* 10 20)").unwrap()), "200");
assert_eq!(format!("{}", env.run_script("(/ 20 10)").unwrap()), "2"); assert_eq!(format!("{}", env.run_script("(/ 20 10)").unwrap()), "2");
assert_eq!(format!("{}", env.run_script("(// 20 3)").unwrap()), "6"); // 20 // 3 = 6 assert_eq!(format!("{}", env.run_script("(// 20 3)").unwrap()), "6"); // 20 // 3 = 6
assert_eq!(format!("{}", env.run_script("(% 20 3)").unwrap()), "2"); // 20 % 3 = 2 assert_eq!(format!("{}", env.run_script("(% 20 3)").unwrap()), "2"); // 20 % 3 = 2
// --- Logic / Bitwise --- // --- Logic / Bitwise ---
assert_eq!( assert_eq!(
format!("{}", env.run_script("(and true false)").unwrap()), format!("{}", env.run_script("(and true false)").unwrap()),
"false" "false"
); );
assert_eq!( assert_eq!(
format!("{}", env.run_script("(or true false)").unwrap()), format!("{}", env.run_script("(or true false)").unwrap()),
"true" "true"
); );
assert_eq!( assert_eq!(
format!("{}", env.run_script("(xor true false)").unwrap()), format!("{}", env.run_script("(xor true false)").unwrap()),
"true" "true"
); );
assert_eq!( assert_eq!(
format!("{}", env.run_script("(not true)").unwrap()), format!("{}", env.run_script("(not true)").unwrap()),
"false" "false"
); );
assert_eq!(format!("{}", env.run_script("(<< 1 2)").unwrap()), "4"); // 1 << 2 = 4 assert_eq!(format!("{}", env.run_script("(<< 1 2)").unwrap()), "4"); // 1 << 2 = 4
assert_eq!(format!("{}", env.run_script("(>> 4 1)").unwrap()), "2"); // 4 >> 1 = 2 assert_eq!(format!("{}", env.run_script("(>> 4 1)").unwrap()), "2"); // 4 >> 1 = 2
assert_eq!(format!("{}", env.run_script("(and 3 1)").unwrap()), "1"); // 3 & 1 = 1 assert_eq!(format!("{}", env.run_script("(and 3 1)").unwrap()), "1"); // 3 & 1 = 1
// --- Comparison --- // --- Comparison ---
assert_eq!(format!("{}", env.run_script("(= 10 10)").unwrap()), "true"); assert_eq!(format!("{}", env.run_script("(= 10 10)").unwrap()), "true");
assert_eq!(format!("{}", env.run_script("(= 10 20)").unwrap()), "false"); assert_eq!(format!("{}", env.run_script("(= 10 20)").unwrap()), "false");
assert_eq!(format!("{}", env.run_script("(<> 10 20)").unwrap()), "true"); assert_eq!(format!("{}", env.run_script("(<> 10 20)").unwrap()), "true");
assert_eq!(format!("{}", env.run_script("(< 10 20)").unwrap()), "true"); assert_eq!(format!("{}", env.run_script("(< 10 20)").unwrap()), "true");
assert_eq!(format!("{}", env.run_script("(> 10 20)").unwrap()), "false"); assert_eq!(format!("{}", env.run_script("(> 10 20)").unwrap()), "false");
assert_eq!(format!("{}", env.run_script("(<= 10 10)").unwrap()), "true"); assert_eq!(format!("{}", env.run_script("(<= 10 10)").unwrap()), "true");
assert_eq!(format!("{}", env.run_script("(>= 10 10)").unwrap()), "true"); assert_eq!(format!("{}", env.run_script("(>= 10 10)").unwrap()), "true");
// --- NaN --- // --- NaN ---
assert_eq!(format!("{}", env.run_script("NaN").unwrap()), "NaN"); assert_eq!(format!("{}", env.run_script("NaN").unwrap()), "NaN");
} }
#[test] #[test]
fn test_now_function_not_folded() { fn test_random_isolation_between_environments() {
let env = Environment::new(); let env1 = Environment::new();
let source = "(now)"; let env2 = Environment::new();
// 1. Check result type and value plausibility // 1. Seed env1
let result = env.run_script(source).expect("Failed to run script"); env1.run_script("(seed! 123)").unwrap();
if let Value::DateTime(ts) = result { let val1_a = env1.run_script("(random)").unwrap();
let current = chrono::Utc::now().timestamp_millis();
assert!(ts > 0); // 2. env2 should have its own default seed state
assert!(ts <= current); let val2_a = env2.run_script("(random)").unwrap();
} else {
panic!("Expected DateTime, got {:?}", result); // They are highly unlikely to be equal by default,
} // and seeding env1 MUST not have seeded env2.
assert_ne!(
// 2. Verify it's NOT constant folded in the AST dump val1_a, val2_a,
let dump = env.dump_ast(source).expect("Failed to dump AST"); "Environments must have isolated PRNG states"
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);
} // 3. Seed env2 differently
env2.run_script("(seed! 123)").unwrap();
#[test] let val2_b = env2.run_script("(random)").unwrap();
fn test_date_parsing() {
let env = Environment::new(); // After same seeding, they should match (isolated but identical seed)
let res = env.run_script("(date \"2023-01-01\")").unwrap(); assert_eq!(
if let Value::DateTime(_) = res { val1_a, val2_b,
// OK "Different environments with the same seed must produce the same sequence"
} else { );
panic!("Expected DateTime, got {:?}", res); }
}
} #[test]
fn test_random_seeding_determinism() {
#[test] let env = Environment::new();
fn test_dynamic_call_destructuring_underflow() {
let env = Environment::new(); // 1. First run with seed 42
let source = "(do env.run_script("(seed! 42)").unwrap();
(def call-dynamic (fn [f data] (f data))) let val1 = env.run_script("(random)").unwrap();
(def data [10 [20 30]])
(def x (fn [[a [b c]]] (+ a (+ b c)))) // 2. Second run with same seed 42
(call-dynamic x data))"; env.run_script("(seed! 42)").unwrap();
let val2 = env.run_script("(random)").unwrap();
let result = env.run_script(source);
if let Err(e) = &result { assert_eq!(
panic!("Failed: {}", e); val1, val2,
} "Random results must be identical for the same seed"
assert_eq!(format!("{}", result.unwrap()), "60"); );
}
} // 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();
let source = "(now)";
// 1. Check result type and value plausibility
let result = env.run_script(source).expect("Failed to run script");
if let Value::DateTime(ts) = result {
let current = chrono::Utc::now().timestamp_millis();
assert!(ts > 0);
assert!(ts <= current);
} else {
panic!("Expected DateTime, got {:?}", result);
}
// 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
);
}
#[test]
fn test_date_parsing() {
let env = Environment::new();
let res = env.run_script("(date \"2023-01-01\")").unwrap();
if let Value::DateTime(_) = res {
// OK
} else {
panic!("Expected DateTime, got {:?}", res);
}
}
#[test]
fn test_dynamic_call_destructuring_underflow() {
let env = Environment::new();
let source = "(do
(def call-dynamic (fn [f data] (f data)))
(def data [10 [20 30]])
(def x (fn [[a [b c]]] (+ a (+ b c))))
(call-dynamic x data))";
let result = env.run_script(source);
if let Err(e) = &result {
panic!("Failed: {}", e);
}
assert_eq!(format!("{}", result.unwrap()), "60");
}
}