#[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); let ast = parser.parse_expression(); 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); let ast = parser.parse_expression(); 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); let ast = parser.parse_expression(); 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); let ast = parser.parse_expression(); 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) .into_result() .expect("Failed to compile"); let linked = env.link(compiled); let func = env.instantiate(linked); let result: Result = Ok((func.func)(&[])); match result { Ok(Value::Int(20)) => (), 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. Create a seeded generator in env1 env1.run_script("(def rand (make-random 123))").unwrap(); let val1_a = env1.run_script("(rand)").unwrap(); // 2. env2 should have its own default seed state for its generators env2.run_script("(def rand (make-random))").unwrap(); let val2_a = env2.run_script("(rand)").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. Create another generator in env2 with the same seed env2.run_script("(def rand-same (make-random 123))") .unwrap(); let val2_b = env2.run_script("(rand-same)").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("(def rand1 (make-random 42))").unwrap(); let val1 = env.run_script("(rand1)").unwrap(); // 2. Second run with same seed 42 env.run_script("(def rand2 (make-random 42))").unwrap(); let val2 = env.run_script("(rand2)").unwrap(); assert_eq!( val1, val2, "Random results must be identical for the same seed" ); // 3. Third run with different seed env.run_script("(def rand3 (make-random 123))").unwrap(); let val3 = env.run_script("(rand3)").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] #[should_panic(expected = "'again' is only allowed in tail position to avoid dead code.")] fn test_again_non_tail_panic() { let env = Environment::new(); let source = "(do (def f (fn [x] (do (again (- x 1)) x))) (f 5))"; // This will trigger the TCO pass which contains the validation logic let _ = env.run_script(source); } #[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"); } #[test] fn test_nested_destructuring_optimization() { let env = Environment::new(); // 1. Tuple-to-Tuple let source_tuple = "((fn [[x y]] (+ x y)) [10 20])"; assert_eq!(format!("{}", env.run_script(source_tuple).unwrap()), "30"); let dump_tuple = env.dump_ast(source_tuple).unwrap(); assert!( dump_tuple.contains("Constant: 30"), "Nested tuple should be folded to 30. Dump:\n{}", dump_tuple ); } #[test] fn test_def_destructuring() { let env = Environment::new(); // 1. Global destructuring let source_global = "(do (def [a b] [1 2]) (+ a b))"; assert_eq!(format!("{}", env.run_script(source_global).unwrap()), "3"); // 2. Local nested destructuring inside a function let source_local = "((fn [x] (do (def [a [[b c] d]] x) (+ a (+ b (+ c d))))) [1 [[2 3] 4]])"; assert_eq!(format!("{}", env.run_script(source_local).unwrap()), "10"); // 3. Verify 'def' returns the assigned value let source_return = "(def [x y] [7 8])"; let res = env.run_script(source_return).unwrap(); if let Value::Tuple(vals) = res { assert_eq!(vals.len(), 2); assert_eq!(format!("{}", vals[0]), "7"); assert_eq!(format!("{}", vals[1]), "8"); } else { panic!("Expected tuple return from def, got {:?}", res); } } #[test] fn test_assign_destructuring() { // 1. Simple assignment destructuring { let env = Environment::new(); let source_simple = "(do (def a 0) (def b 0) (assign [a b] [10 20]) (+ a b))"; assert_eq!(format!("{}", env.run_script(source_simple).unwrap()), "30"); } // 2. Nested assignment destructuring { let env = Environment::new(); let source_nested = "(do (def a 0) (def b 0) (def c 0) (assign [a [b c]] [1 [2 3]]) (+ a (+ b c)))"; assert_eq!(format!("{}", env.run_script(source_nested).unwrap()), "6"); } // 3. Assignment returns the assigned value { let env = Environment::new(); let source_return = "(do (def a 0) (def b 0) (assign [a b] [5 6]))"; let res = env.run_script(source_return).unwrap(); if let Value::Tuple(vals) = res { assert_eq!(vals.len(), 2); assert_eq!(format!("{}", vals[0]), "5"); assert_eq!(format!("{}", vals[1]), "6"); } else { panic!("Expected tuple return from assign, got {:?}", res); } } } #[test] fn test_pipeline_optional_type() { let env = Environment::new(); // The lambda uses an `if` without an `else` returning a float constant. // The TypeChecker should deduce `Optional(Float)` for the lambda body, // and correctly unwrap it to `Series(Float)` for the pipeline output. let source = "(do (def src (create-random-ohlc 42 10)) (def filtered (pipe [src] (fn [tick] (if true 42.0 ) ) ) ) filtered )"; let res = env.run_script(source); if let Err(e) = &res { panic!("Script failed to compile/run: {:?}", e); } let val = res.unwrap(); if let crate::ast::types::Value::Object(obj) = val { assert_eq!(obj.type_name(), "PipelineNode"); } else { panic!("Expected an Object(PipelineNode)"); } } #[test] fn test_multi_level_destructuring() { let env = Environment::new(); let source = "(do (def process_data (fn [conf] (do (def [str s] conf) (def [f ss] s) [\"Symbol:\" str \"field:\" f \"id:\" ss] ) ) ) (process_data [\"btc\" [:close \"cls\"]]))"; let res = env.run_script(source).unwrap(); assert_eq!( format!("{}", res), "[\"Symbol:\" \"btc\" \"field:\" :close \"id:\" \"cls\"]" ); } #[test] fn test_closure_reassignment_optimization_bug() { let env = Environment::new(); // This test case reproduces a bug where the optimizer aggressively inlined a function // ('f') even though its parameters ('x') were being assigned to in the body (by inner lambda). // The fix ensures that such functions are NOT inlined. let source = "(do (def f (fn [[x y]] (fn [] (assign x (+ x y))))) ((f [1 2])))"; let res = env.run_script(source); assert_eq!(format!("{}", res.unwrap()), "3"); } #[test] fn test_macro_inlining_identity_collision() { let source = r#" (do (macro wrap [f] `(fn [x] (~f x))) (def add1 (fn [x] (+ x 1))) (def add2 (fn [x] (+ x 2))) (def w1 (wrap add1)) (def w2 (wrap add2)) (w1 (w2 10))) "#; // 1. Verify the result is correct let env_run = Environment::new(); let res = env_run.run_script(source).expect("Failed to run script"); assert_eq!(format!("{}", res), "13"); // 2. Verify that it was actually folded into a constant by the optimizer let env_dump = Environment::new(); let dump = env_dump.dump_ast(source).expect("Failed to dump AST"); assert!( dump.contains("Constant: 13"), "Macro-wrapped calls should be fully folded to 13. Dump:\n{}", dump ); // The definitions add1, add2, w1, w2 should be gone after dead code elimination assert!( !dump.contains("Define Variable"), "Definitions should be removed by DCE" ); } #[test] fn test_optimizer_upvalue_inlining_bug_repro() { let env = Environment::new(); let source = r#" (do (def make-counter (fn [init] (do (def val init) { :inc (fn [] (assign val (+ val 1))) :get (fn [] val) }))) (def c (make-counter 10)) ((.inc c)) ((.get c))) "#; let res = env.run_script(source); assert!(res.is_ok(), "Optimizer bug triggered: {:?}", res.err()); assert_eq!(format!("{}", res.unwrap()), "11"); } #[test] fn test_optimizer_destructuring_inlining_and_mutation() { let env = Environment::new(); // 1. Test: Destructuring definition should allow inlining if not mutated let source_inline = "(do (def [x y] [10 20]) (+ x y))"; let res_inline = env.run_script(source_inline).unwrap(); assert_eq!(format!("{}", res_inline), "30"); // 2. Test: Destructuring definition should NOT be inlined if mutated (Regression Test) let source_mutation = r#" (do (def [a b] [1 2]) (def f (fn [] (assign a (+ a b)))) (f) a) "#; let res_mutation = env.run_script(source_mutation).unwrap(); assert_eq!(format!("{}", res_mutation), "3"); } #[test] fn test_record_basics() { let env = Environment::new(); let source = r#" ((fn [user] [(.name user) (.age user)]) {:name "Alice" :age 30}) "#; let res = env.run_script(source).unwrap(); assert_eq!(format!("{}", res), "[\"Alice\" 30]"); } #[test] fn test_record_optimized_access() { let env = Environment::new(); let source_eval = "(.price {:id 1 :price 99.5})"; // 1. Check result (will be fully folded to a constant by the new optimization) let res = env.run_script(source_eval).unwrap(); assert_eq!(format!("{}", res), "99.5"); // 2. Verify optimization to GET_FIELD when the record contains non-constants let source_ast = "(fn [id] (.price {:id id :price 99.5}))"; let dump = env.dump_ast(source_ast).unwrap(); assert!( dump.contains("GetField: .price"), "Should be optimized to GetField. Dump:\n{}", dump ); } #[test] fn test_first_class_field_accessor() { let env = Environment::new(); let source = r#" (do (def get-name .name) ; Dynamic call to field accessor (get-name {:name "Alice"})) "#; let res = env.run_script(source).unwrap(); assert_eq!(format!("{}", res), "\"Alice\""); } #[test] fn test_record_constant_folding() { let env = Environment::new(); // Optimizer should fold (.x {:x 10}) into 10 let source = "(.x {:x 10 :y 20})"; let dump = env.dump_ast(source).unwrap(); assert!( dump.contains("Constant: 10"), "Should evaluate GetField at compile time." ); } #[test] fn test_record_literal_constant_folding() { let env = Environment::new(); let source = " {:a 1 :b 2} "; let dump = env.dump_ast(source).unwrap(); // Ensure the record definition itself is folded into a Constant. assert!( dump.contains("Constant: {:a 1, :b 2}"), "Should transform a pure record literal into a constant value." ); assert!( !dump.contains("Record {"), "Should not leave a runtime Record node in the AST." ); } #[test] fn test_record_inlining_in_while_loop() { let env_ast = Environment::new(); // Ensure that constants (like the config record) are inherited into inner lambda scopes (like the body of a while loop). let source = r#" (do (def loop-config {:start 0 :limit 10}) (def loop-idx 0) (while (< loop-idx (.limit loop-config)) (assign loop-idx (+ loop-idx 1))) loop-idx ) "#; let dump = env_ast.dump_ast(source).unwrap(); // The optimizer should inline `loop-config`, resolving `(.limit loop-config)` to `10`. // The GetField and the Get for 'loop-config' should vanish inside the condition. assert!( !dump.contains("GetField: .limit"), "The record field should be completely inlined." ); assert!( dump.contains("Constant: 10"), "The limit should be resolved to a constant 10." ); // The result of running it should obviously still be correct. let env_run = Environment::new(); let res = env_run.run_script(source).unwrap(); assert_eq!(format!("{}", res), "10"); } #[test] fn test_record_errors() { let env = Environment::new(); // Both cases are reported by the TypeChecker since it can't resolve the call // for a missing field or a non-record argument. // 1. Missing field let res_missing = env.run_script("(.missing {:a 1})"); assert!(res_missing.is_err()); assert!(res_missing.unwrap_err().contains("Invalid arguments")); // 2. Not a record let res_not_rec = env.run_script("(.name 123)"); assert!(res_not_rec.is_err()); assert!(res_not_rec.unwrap_err().contains("Invalid arguments")); } #[test] fn test_record_layout_interning() { let env = Environment::new(); let source = r#" (do (def r1 {:a 1 :b 2}) (def r2 {:a 10 :b 20}) ; Identical layouts result in identical types (= r1 r2)) "#; // This will be false because values differ, but let's just check if it compiles and runs. // To really test interning, we'd need a way to check if layouts are the same Arc. let res = env.run_script(source).unwrap(); assert_eq!(format!("{}", res), "false"); } #[test] fn test_error_recovery_parser() { let env = Environment::new(); // Syntax error: mismatched bracket/paren let source = "(do (def a [1 2 ) )"; let result = env.compile(source); assert!(result.diagnostics.has_errors(), "Expected parser errors"); let error_msgs: Vec<_> = result .diagnostics .items .iter() .map(|d| d.message.as_str()) .collect(); // It should complain about finding ) instead of ] assert!( error_msgs.iter().any(|m| m.contains("RightParen")), "Expected error about RightParen" ); // AST should still be partially built (not None) assert!( result.ast.is_some(), "AST should be partially built despite parser errors" ); } #[test] fn test_error_recovery_binder() { let env = Environment::new(); // Semantic error: undefined variable let source = "(do (def a 10) (def b unknown_var) (+ a 5))"; let result = env.compile(source); assert!(result.diagnostics.has_errors(), "Expected binder errors"); let error_msgs: Vec<_> = result .diagnostics .items .iter() .map(|d| d.message.as_str()) .collect(); assert!( error_msgs .iter() .any(|m| m.contains("Undefined variable 'unknown_var'")), "Expected undefined variable error" ); assert!(result.ast.is_some(), "AST should be built with Error nodes"); } #[test] fn test_error_recovery_type_checker() { let env = Environment::new(); // Semantic error: Type mismatch in function call let source = "(do (def a 10) (def b (not \"text\")) (- a 2))"; let result = env.compile(source); assert!( result.diagnostics.has_errors(), "Expected type checker errors" ); let error_msgs: Vec<_> = result .diagnostics .items .iter() .map(|d| d.message.as_str()) .collect(); assert!( error_msgs .iter() .any(|m| m.contains("Invalid arguments for function call")), "Expected invalid arguments error" ); assert!(result.ast.is_some(), "AST should be built with Error nodes"); } #[test] fn test_error_recovery_multiple_errors() { let env = Environment::new(); // A script with multiple errors that should all be collected let source = r#" (do (def a undefined_var) (def b (not "text")) ) "#; let result = env.compile(source); assert!(result.diagnostics.has_errors()); assert!( result.diagnostics.items.len() >= 2, "Expected multiple errors to be collected" ); let error_msgs: Vec<_> = result .diagnostics .items .iter() .map(|d| d.message.as_str()) .collect(); assert!( error_msgs .iter() .any(|m| m.contains("Undefined variable 'undefined_var'")) ); assert!( error_msgs .iter() .any(|m| m.contains("Invalid arguments for function call")) ); } }