From 6605f567569d200149bc0b0197c936d71e05bd73 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 22 Feb 2026 09:08:16 +0100 Subject: [PATCH] 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. --- Cargo.lock | 1 + Cargo.toml | 1 + src/ast/environment.rs | 2 + src/ast/rtl/core.rs | 22 +- src/integration_test.rs | 499 ++++++++++++++++++++++------------------ 5 files changed, 299 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1e0fac..4b1cdad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1915,6 +1915,7 @@ dependencies = [ "chrono", "clap", "eframe", + "fastrand", "insta", "regex", ] diff --git a/Cargo.toml b/Cargo.toml index b2208c1..fb1bb97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 626aaf6..d859eb5 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -23,6 +23,7 @@ pub struct Environment { pub global_types: Rc>>, pub global_purity: Rc>>, pub global_values: Rc>>, + pub prng: Rc>, pub function_registry: Rc>>, pub typed_function_registry: Rc>>, pub monomorph_cache: Rc>, @@ -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())), diff --git a/src/ast/rtl/core.rs b/src/ast/rtl/core.rs index f438b9e..4fcced3 100644 --- a/src/ast/rtl/core.rs +++ b/src/ast/rtl/core.rs @@ -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 }); } diff --git a/src/integration_test.rs b/src/integration_test.rs index 190e5ad..ce3470a 100644 --- a/src/integration_test.rs +++ b/src/integration_test.rs @@ -1,219 +1,280 @@ -#[cfg(test)] -mod tests { - use crate::ast::environment::Environment; - use crate::ast::nodes::UntypedKind; - use crate::ast::parser::Parser; - use crate::ast::types::Value; - - #[test] - fn test_parse_integer_constant() { - let source = "123"; - let mut parser = Parser::new(source).expect("Failed to create parser"); - let ast = parser.parse_expression().expect("Failed to parse"); - - if let UntypedKind::Constant(Value::Int(val)) = ast.kind { - assert_eq!(val, 123); - } else { - panic!("Expected Integer constant, got {:?}", ast.kind); - } - } - - #[test] - fn test_parse_negative_integer() { - let source = "-42"; - let mut parser = Parser::new(source).expect("Failed to create parser"); - let ast = parser.parse_expression().expect("Failed to parse"); - - if let UntypedKind::Constant(Value::Int(val)) = ast.kind { - assert_eq!(val, -42); - } else { - panic!("Expected Integer constant, got {:?}", ast.kind); - } - } - - #[test] - fn test_parse_float_constant() { - let source = "123.45"; - let mut parser = Parser::new(source).expect("Failed to create parser"); - let ast = parser.parse_expression().expect("Failed to parse"); - - if let UntypedKind::Constant(Value::Float(val)) = ast.kind { - assert_eq!(val, 123.45); - } else { - panic!("Expected Float constant, got {:?}", ast.kind); - } - } - - #[test] - fn test_parse_negative_float() { - let source = "-10.5"; - let mut parser = Parser::new(source).expect("Failed to create parser"); - let ast = parser.parse_expression().expect("Failed to parse"); - - if let UntypedKind::Constant(Value::Float(val)) = ast.kind { - assert_eq!(val, -10.5); - } else { - panic!("Expected Float constant, got {:?}", ast.kind); - } - } - - #[test] - fn test_closure_modification_from_source() { - let source = r#" - (do - (def x 10) - (def f (fn [] (assign x 20))) - (f) - x - ) - "#; - - let env = Environment::new(); - let compiled = env.compile(source).expect("Failed to compile"); - let linked = env.link(compiled); - let result = env.run(&linked); - - match result { - Ok(Value::Int(val)) => assert_eq!(val, 20, "Closure should modify outer variable"), - Ok(val) => panic!("Expected Int(20), got {:?}", val), - Err(e) => panic!("VM Error: {}", e), - } - } - - #[test] - fn test_examples() { - for opt in [false, true] { - let results = crate::utils::tester::run_functional_tests_with_optimization(opt); - for res in results { - assert!( - res.success, - "Example {} failed at opt {}: {}", - res.name, opt, res.message - ); - } - } - } - - #[test] - fn test_debug_mode_logging() { - let mut env = Environment::new(); - env.optimization = false; - let source = "(+ 10 20)"; - let result = env.run_debug(source).expect("Failed to run debug"); - - let (val, logs) = result; - - // 1. Check value - match val { - Ok(Value::Int(30)) => (), - _ => panic!("Expected Int(30), got {:?}", val), - } - - // 2. Check logs (should have entries for + and constants) - assert!(!logs.is_empty(), "Logs should not be empty"); - - // Look for typical trace patterns - let has_call = logs.iter().any(|l| l.contains("CALL")); - let has_const = logs.iter().any(|l| l.contains("CONST(10)")); - let has_result = logs.iter().any(|l| l.contains("} -> 30")); - - assert!(has_call, "Logs should contain CALL"); - assert!(has_const, "Logs should contain CONST(10)"); - assert!(has_result, "Logs should contain result 30"); - } - - #[test] - fn test_rtl_operators() { - let env = Environment::new(); - - // --- Arithmetic --- - 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("(* 10 20)").unwrap()), "200"); - 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()), "2"); // 20 % 3 = 2 - - // --- Logic / Bitwise --- - assert_eq!( - format!("{}", env.run_script("(and true false)").unwrap()), - "false" - ); - assert_eq!( - format!("{}", env.run_script("(or true false)").unwrap()), - "true" - ); - assert_eq!( - format!("{}", env.run_script("(xor true false)").unwrap()), - "true" - ); - assert_eq!( - format!("{}", env.run_script("(not true)").unwrap()), - "false" - ); - - 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("(and 3 1)").unwrap()), "1"); // 3 & 1 = 1 - - // --- Comparison --- - 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()), "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 10)").unwrap()), "true"); - assert_eq!(format!("{}", env.run_script("(>= 10 10)").unwrap()), "true"); - - // --- NaN --- - assert_eq!(format!("{}", env.run_script("NaN").unwrap()), "NaN"); - } - - #[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"); - } -} +#[cfg(test)] +mod tests { + use crate::ast::environment::Environment; + use crate::ast::nodes::UntypedKind; + use crate::ast::parser::Parser; + use crate::ast::types::Value; + + #[test] + fn test_parse_integer_constant() { + let source = "123"; + let mut parser = Parser::new(source).expect("Failed to create parser"); + let ast = parser.parse_expression().expect("Failed to parse"); + + if let UntypedKind::Constant(Value::Int(val)) = ast.kind { + assert_eq!(val, 123); + } else { + panic!("Expected Integer constant, got {:?}", ast.kind); + } + } + + #[test] + fn test_parse_negative_integer() { + let source = "-42"; + let mut parser = Parser::new(source).expect("Failed to create parser"); + let ast = parser.parse_expression().expect("Failed to parse"); + + if let UntypedKind::Constant(Value::Int(val)) = ast.kind { + assert_eq!(val, -42); + } else { + panic!("Expected Integer constant, got {:?}", ast.kind); + } + } + + #[test] + fn test_parse_float_constant() { + let source = "123.45"; + let mut parser = Parser::new(source).expect("Failed to create parser"); + let ast = parser.parse_expression().expect("Failed to parse"); + + if let UntypedKind::Constant(Value::Float(val)) = ast.kind { + assert_eq!(val, 123.45); + } else { + panic!("Expected Float constant, got {:?}", ast.kind); + } + } + + #[test] + fn test_parse_negative_float() { + let source = "-10.5"; + let mut parser = Parser::new(source).expect("Failed to create parser"); + let ast = parser.parse_expression().expect("Failed to parse"); + + if let UntypedKind::Constant(Value::Float(val)) = ast.kind { + assert_eq!(val, -10.5); + } else { + panic!("Expected Float constant, got {:?}", ast.kind); + } + } + + #[test] + fn test_closure_modification_from_source() { + let source = r#" + (do + (def x 10) + (def f (fn [] (assign x 20))) + (f) + x + ) + "#; + + let env = Environment::new(); + let compiled = env.compile(source).expect("Failed to compile"); + let linked = env.link(compiled); + let result = env.run(&linked); + + match result { + Ok(Value::Int(val)) => assert_eq!(val, 20, "Closure should modify outer variable"), + Ok(val) => panic!("Expected Int(20), got {:?}", val), + Err(e) => panic!("VM Error: {}", e), + } + } + + #[test] + fn test_examples() { + for opt in [false, true] { + let results = crate::utils::tester::run_functional_tests_with_optimization(opt); + for res in results { + assert!( + res.success, + "Example {} failed at opt {}: {}", + res.name, opt, res.message + ); + } + } + } + + #[test] + fn test_debug_mode_logging() { + let mut env = Environment::new(); + env.optimization = false; + let source = "(+ 10 20)"; + let result = env.run_debug(source).expect("Failed to run debug"); + + let (val, logs) = result; + + // 1. Check value + match val { + Ok(Value::Int(30)) => (), + _ => panic!("Expected Int(30), got {:?}", val), + } + + // 2. Check logs (should have entries for + and constants) + assert!(!logs.is_empty(), "Logs should not be empty"); + + // Look for typical trace patterns + let has_call = logs.iter().any(|l| l.contains("CALL")); + let has_const = logs.iter().any(|l| l.contains("CONST(10)")); + let has_result = logs.iter().any(|l| l.contains("} -> 30")); + + assert!(has_call, "Logs should contain CALL"); + assert!(has_const, "Logs should contain CONST(10)"); + assert!(has_result, "Logs should contain result 30"); + } + + #[test] + fn test_rtl_operators() { + let env = Environment::new(); + + // --- Arithmetic --- + 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("(* 10 20)").unwrap()), "200"); + 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()), "2"); // 20 % 3 = 2 + + // --- Logic / Bitwise --- + assert_eq!( + format!("{}", env.run_script("(and true false)").unwrap()), + "false" + ); + assert_eq!( + format!("{}", env.run_script("(or true false)").unwrap()), + "true" + ); + assert_eq!( + format!("{}", env.run_script("(xor true false)").unwrap()), + "true" + ); + assert_eq!( + format!("{}", env.run_script("(not true)").unwrap()), + "false" + ); + + 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("(and 3 1)").unwrap()), "1"); // 3 & 1 = 1 + + // --- Comparison --- + 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()), "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 10)").unwrap()), "true"); + assert_eq!(format!("{}", env.run_script("(>= 10 10)").unwrap()), "true"); + + // --- NaN --- + 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(); + 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"); + } +}