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"); + } +}