diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index dc09af2..0b13a7a 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -848,39 +848,6 @@ mod tests { env.compile(source).into_result().unwrap() } - #[test] - fn test_destructuring_scalar_error() { - let env = crate::ast::environment::Environment::new(); - let result = env.compile("(def [x] 5)").into_result(); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - "Statement 'def' cannot be used as an expression.\nCannot destructure type int as a tuple/vector" - ); - } - - #[test] - fn test_call_argument_mismatch() { - let env = crate::ast::environment::Environment::new(); - // fn takes 1 arg, called with 0 - let result = env.compile("(do (def f (fn [x] x)) (f))").into_result(); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .contains("Invalid arguments for function call") - ); - } - - #[test] - fn test_destructuring_vector_args() { - let env = crate::ast::environment::Environment::new(); - // ((fn [[x y]] (+ x y)) [10 20]) -> 30 - let result = env.compile("((fn [[x y]] (+ x y)) [10 20])").into_result(); - assert!(result.is_ok()); - assert_eq!(get_ret_type(&result.unwrap()), StaticType::Int); - } - fn get_ret_type(node: &TypedNode) -> StaticType { if let StaticType::Function(sig) = &node.ty { sig.ret.clone() diff --git a/src/integration_test.rs b/src/integration_test.rs deleted file mode 100644 index b023421..0000000 --- a/src/integration_test.rs +++ /dev/null @@ -1,917 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::ast::environment::Environment; - use crate::ast::nodes::SyntaxKind; - 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 SyntaxKind::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 SyntaxKind::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 SyntaxKind::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 SyntaxKind::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 - // We must define it at the true root level (not inside 'do') to make it global - let val1_a = env1.run_script("(do (def rand (make-random 123)) (rand))").unwrap(); - - // 2. env2 should have its own default seed state for its generators - let val2_a = env2.run_script("(do (def rand (make-random)) (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 - let val2_b = env2.run_script("(do (def rand-same (make-random 123)) (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 - let val1 = env.run_script("(do (def rand1 (make-random 42)) (rand1))").unwrap(); - - // 2. Second run with same seed 42 - let val2 = env.run_script("(do (def rand2 (make-random 42)) (rand2))").unwrap(); - - assert_eq!( - val1, val2, - "Random results must be identical for the same seed" - ); - - // 3. Third run with different seed - let val3 = env.run_script("(do (def rand3 (make-random 123)) (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 destructuring result manually - let source_check = "(do (def [x y] [7 8]) [x y])"; - let res = env.run_script(source_check).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 from [x y], 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 value check - { - let env = Environment::new(); - let source_return = "(do (def a 0) (def b 0) (assign [a b] [5 6]) [a b])"; - 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 from [a b], 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(), "StreamNode"); - } else { - panic!("Expected an Object(StreamNode)"); - } - } - - #[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") || dump.contains("Call"), - "Macro-wrapped calls should be properly handled. Dump:\n{}", - dump - ); - } - - #[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` eventually, but in a single pass - // with the new statement rules, it might leave GetField intact if the definition isn't fully removed yet. - assert!( - dump.contains("Constant: 10") || dump.contains("GetField: .limit"), - "The limit should be resolved to a constant 10 or kept as GetField." - ); - - // 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")) - ); - } - - #[test] - fn test_optimizer_inlining_slot_clash_repro() { - let mut env = Environment::new(); - env.optimization = true; - - // This exactly mirrors err.myc structure. - // NOTE: We must use a dynamic call like (series :float) here. - // Simple constants like (def history 100) might be constant-folded or - // inlined as values by the optimizer before the slot-clash can occur. - // By using a series, we force the result into a local slot at runtime, - // which triggers the conflict when the inliner fails to remap slots correctly. - let source = r#" - (do - (def outer (fn [length] - (do - (def history (series 100 :float)) - (fn [val] length) - ) - )) - ((outer 20) 5) - ) - "#; - - let res = env.run_script(source).unwrap(); - assert_eq!(format!("{}", res), "20"); - } - - #[test] - fn test_reproduce_inlining_slot_clash_crash() { - let mut env = Environment::new(); - env.optimization = true; - - let source = r#" -(do - (def MY_SMA - (fn [length] - (do - (def history (series 100 :float)) - (fn [val] - (do - (push history val) - (len history)))))) - - (def src (create-random-ohlc 42 100)) - (def s (.close src)) - - [(pipe [s] (MY_SMA 5))] - [(pipe [s] (SMA 20))]) -"#; - // Note: Using SMA from RTL and MY_SMA locally to mix it up. - // Actually, for full independence, let's use MY_SMA twice. - let source_fixed = source.replace("(SMA 20)", "(MY_SMA 20)"); - - let res = env.run_script(&source_fixed); - assert!(res.is_ok(), "Inlining slot clash triggered: {:?}", res.err()); - } - - #[test] - fn test_repro_placeholder_in_template_failure() { - let env = Environment::new(); - let source = r#" - (do - (macro repeat [var limit body] - `((fn [~var __limit] - (if (< ~var __limit) - (do ~body (again (+ ~var 1) __limit)))) - 0 ~limit)) - (repeat n 10 42)) - "#; - let res = env.run_script(source); - assert!(res.is_ok(), "Expansion failed to strip placeholders: {:?}", res.err()); - } - - // --- Optimizer robustness: destructuring correctness (regression tests) --- - - #[test] - fn test_dead_def_destructuring_correctness() { - // A dead destructuring def must not change the result — with or without optimization. - let source = "(do (def [x y] [1 2]) 42)"; - let mut env_opt = Environment::new(); - env_opt.optimization = true; - let mut env_no = Environment::new(); - env_no.optimization = false; - assert_eq!( - format!("{}", env_opt.run_script(source).unwrap()), - format!("{}", env_no.run_script(source).unwrap()) - ); - } - - #[test] - fn test_captured_slot_destructuring() { - let env = Environment::new(); - // A closure capturing both slots from a destructuring def must work correctly. - let source = r#" - (do - (def [x y] [10 20]) - (def get-sum (fn [] (+ x y))) - (get-sum)) - "#; - assert_eq!(format!("{}", env.run_script(source).unwrap()), "30"); - } - - #[test] - fn test_destructuring_capture_mutation() { - let env = Environment::new(); - // A closure mutating a slot bound via destructuring must see the updated value. - let source = r#" - (do - (def [a b] [1 2]) - (def inc! (fn [] (assign a (+ a b)))) - (inc!) - (inc!) - a) - "#; - // 1+2=3, 3+2=5 - assert_eq!(format!("{}", env.run_script(source).unwrap()), "5"); - } - - #[test] - fn test_inline_two_lambdas_same_slots() { - let env = Environment::new(); - // Two lambdas each binding a local at slot 0 get inlined into the same scope. - // Slot remapping must prevent collision between them. - let source = r#" - (do - (def add1 (fn [x] (+ x 1))) - (def add2 (fn [x] (+ x 2))) - (+ (add1 10) (add2 20))) - "#; - assert_eq!(format!("{}", env.run_script(source).unwrap()), "33"); - let dump = env.dump_ast(source).unwrap(); - assert!( - dump.contains("Constant: 33"), - "Should be fully folded to Constant 33. Dump:\n{}", - dump - ); - } - - #[test] - fn test_multipass_const_propagation() { - let env = Environment::new(); - // A constant chain where each step is only resolvable after the previous has been folded. - // Requires multiple optimizer passes. - let source = r#" - (do - (def a 1) - (def b (+ a 1)) - (def c (+ b 1)) - (+ c 1)) - "#; - assert_eq!(format!("{}", env.run_script(source).unwrap()), "4"); - let dump = env.dump_ast(source).unwrap(); - assert!( - dump.contains("Constant: 4"), - "Multi-pass chain should fold to Constant 4. Dump:\n{}", - dump - ); - } - - #[test] - fn test_dead_destructuring_def_eliminated() { - let env = Environment::new(); - // A dead destructuring def with a pure RHS should be eliminated by the optimizer, - // just like a dead simple def is. Without the fix, the Def node stays in the AST - // and wastes stack slots. - let dump = env.dump_ast("(do (def [x y] [1 2]) 42)").unwrap(); - assert!( - !dump.contains("Def"), - "Dead destructuring def should be eliminated from AST. Dump:\n{}", - dump - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index 5a9d88a..ad62bd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,2 @@ pub mod ast; pub mod utils; - -#[cfg(test)] -mod integration_test; diff --git a/tests/closures.rs b/tests/closures.rs new file mode 100644 index 0000000..6a9ec10 --- /dev/null +++ b/tests/closures.rs @@ -0,0 +1,93 @@ +use myc::ast::environment::Environment; +use myc::ast::types::Value; + +#[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_closure_reassignment_optimization_bug() { + let env = Environment::new(); + // Regression: optimizer inlined a function whose parameters were assigned to + // by an inner lambda. Such functions must NOT be 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_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] +#[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))"; + let _ = env.run_script(source); +} + +#[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; + + match val { + Ok(Value::Int(30)) => (), + _ => panic!("Expected Int(30), got {:?}", val), + } + + assert!(!logs.is_empty(), "Logs should not be empty"); + + 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"); +} diff --git a/tests/destructuring.rs b/tests/destructuring.rs new file mode 100644 index 0000000..37872ac --- /dev/null +++ b/tests/destructuring.rs @@ -0,0 +1,139 @@ +use myc::ast::environment::Environment; +use myc::ast::types::Value; + +#[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(); + + 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 destructuring result manually + let source_check = "(do (def [x y] [7 8]) [x y])"; + let res = env.run_script(source_check).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 from [x y], 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 value check + { + let env = Environment::new(); + let source_return = "(do (def a 0) (def b 0) (assign [a b] [5 6]) [a b])"; + 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 from [a b], got {:?}", res); + } + } +} + +#[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_captured_slot_destructuring() { + let env = Environment::new(); + // A closure capturing both slots from a destructuring def must work correctly. + let source = r#" + (do + (def [x y] [10 20]) + (def get-sum (fn [] (+ x y))) + (get-sum)) + "#; + assert_eq!(format!("{}", env.run_script(source).unwrap()), "30"); +} + +#[test] +fn test_destructuring_capture_mutation() { + let env = Environment::new(); + // A closure mutating a slot bound via destructuring must see the updated value. + let source = r#" + (do + (def [a b] [1 2]) + (def inc! (fn [] (assign a (+ a b)))) + (inc!) + (inc!) + a) + "#; + // 1+2=3, 3+2=5 + assert_eq!(format!("{}", env.run_script(source).unwrap()), "5"); +} diff --git a/tests/error_recovery.rs b/tests/error_recovery.rs new file mode 100644 index 0000000..76885bb --- /dev/null +++ b/tests/error_recovery.rs @@ -0,0 +1,162 @@ +use myc::ast::compiler::bound_nodes::TypedNode; +use myc::ast::environment::Environment; +use myc::ast::types::StaticType; + +// Helper: extracts the return type from a compiled node. +// For a function type, returns the return type; otherwise returns the node type directly. +fn get_ret_type(node: &TypedNode) -> StaticType { + if let StaticType::Function(sig) = &node.ty { + sig.ret.clone() + } else { + node.ty.clone() + } +} + +// --- Type checker errors --- + +#[test] +fn test_destructuring_scalar_error() { + let env = Environment::new(); + let result = env.compile("(def [x] 5)").into_result(); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + "Statement 'def' cannot be used as an expression.\nCannot destructure type int as a tuple/vector" + ); +} + +#[test] +fn test_call_argument_mismatch() { + let env = Environment::new(); + // fn takes 1 arg, called with 0 + let result = env.compile("(do (def f (fn [x] x)) (f))").into_result(); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .contains("Invalid arguments for function call") + ); +} + +#[test] +fn test_destructuring_vector_args() { + let env = Environment::new(); + // ((fn [[x y]] (+ x y)) [10 20]) -> 30 + let result = env.compile("((fn [[x y]] (+ x y)) [10 20])").into_result(); + assert!(result.is_ok()); + assert_eq!(get_ret_type(&result.unwrap()), StaticType::Int); +} + +// --- Error recovery (multi-pass diagnostics) --- + +#[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(); + + assert!( + error_msgs.iter().any(|m| m.contains("RightParen")), + "Expected error about RightParen" + ); + 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")) + ); +} diff --git a/tests/examples.rs b/tests/examples.rs new file mode 100644 index 0000000..ccd81f4 --- /dev/null +++ b/tests/examples.rs @@ -0,0 +1,15 @@ +use myc::utils::tester::run_functional_tests_with_optimization; + +#[test] +fn test_examples() { + for opt in [false, true] { + let results = run_functional_tests_with_optimization(opt); + for res in results { + assert!( + res.success, + "Example {} failed at opt {}: {}", + res.name, opt, res.message + ); + } + } +} diff --git a/tests/macros.rs b/tests/macros.rs new file mode 100644 index 0000000..a808ba4 --- /dev/null +++ b/tests/macros.rs @@ -0,0 +1,47 @@ +use myc::ast::environment::Environment; + +#[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") || dump.contains("Call"), + "Macro-wrapped calls should be properly handled. Dump:\n{}", + dump + ); +} + +#[test] +fn test_repro_placeholder_in_template_failure() { + let env = Environment::new(); + let source = r#" + (do + (macro repeat [var limit body] + `((fn [~var __limit] + (if (< ~var __limit) + (do ~body (again (+ ~var 1) __limit)))) + 0 ~limit)) + (repeat n 10 42)) + "#; + let res = env.run_script(source); + assert!(res.is_ok(), "Expansion failed to strip placeholders: {:?}", res.err()); +} diff --git a/tests/optimizer.rs b/tests/optimizer.rs new file mode 100644 index 0000000..2b22f81 --- /dev/null +++ b/tests/optimizer.rs @@ -0,0 +1,137 @@ +use myc::ast::environment::Environment; + +#[test] +fn test_optimizer_destructuring_inlining_and_mutation() { + let env = Environment::new(); + + // 1. 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. 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_inline_two_lambdas_same_slots() { + let env = Environment::new(); + // Two lambdas each binding a local at slot 0 get inlined into the same scope. + // Slot remapping must prevent collision between them. + let source = r#" + (do + (def add1 (fn [x] (+ x 1))) + (def add2 (fn [x] (+ x 2))) + (+ (add1 10) (add2 20))) + "#; + assert_eq!(format!("{}", env.run_script(source).unwrap()), "33"); + let dump = env.dump_ast(source).unwrap(); + assert!( + dump.contains("Constant: 33"), + "Should be fully folded to Constant 33. Dump:\n{}", + dump + ); +} + +#[test] +fn test_multipass_const_propagation() { + let env = Environment::new(); + // A constant chain where each step is only resolvable after the previous has been folded. + // Requires multiple optimizer passes. + let source = r#" + (do + (def a 1) + (def b (+ a 1)) + (def c (+ b 1)) + (+ c 1)) + "#; + assert_eq!(format!("{}", env.run_script(source).unwrap()), "4"); + let dump = env.dump_ast(source).unwrap(); + assert!( + dump.contains("Constant: 4"), + "Multi-pass chain should fold to Constant 4. Dump:\n{}", + dump + ); +} + +#[test] +fn test_dead_def_destructuring_correctness() { + // A dead destructuring def must not change the result — with or without optimization. + let source = "(do (def [x y] [1 2]) 42)"; + let mut env_opt = Environment::new(); + env_opt.optimization = true; + let mut env_no = Environment::new(); + env_no.optimization = false; + assert_eq!( + format!("{}", env_opt.run_script(source).unwrap()), + format!("{}", env_no.run_script(source).unwrap()) + ); +} + +#[test] +fn test_dead_destructuring_def_eliminated() { + let env = Environment::new(); + // A dead destructuring def with a pure RHS should be eliminated by the optimizer. + let dump = env.dump_ast("(do (def [x y] [1 2]) 42)").unwrap(); + assert!( + !dump.contains("Def"), + "Dead destructuring def should be eliminated from AST. Dump:\n{}", + dump + ); +} + +#[test] +fn test_optimizer_inlining_slot_clash_repro() { + let mut env = Environment::new(); + env.optimization = true; + + let source = r#" + (do + (def outer (fn [length] + (do + (def history (series 100 :float)) + (fn [val] length) + ) + )) + ((outer 20) 5) + ) + "#; + + let res = env.run_script(source).unwrap(); + assert_eq!(format!("{}", res), "20"); +} + +#[test] +fn test_reproduce_inlining_slot_clash_crash() { + let mut env = Environment::new(); + env.optimization = true; + + let source = r#" +(do + (def MY_SMA + (fn [length] + (do + (def history (series 100 :float)) + (fn [val] + (do + (push history val) + (len history)))))) + + (def src (create-random-ohlc 42 100)) + (def s (.close src)) + + [(pipe [s] (MY_SMA 5))] + [(pipe [s] (MY_SMA 20))]) +"#; + + let res = env.run_script(source); + assert!(res.is_ok(), "Inlining slot clash triggered: {:?}", res.err()); +} diff --git a/tests/parser.rs b/tests/parser.rs new file mode 100644 index 0000000..d8de449 --- /dev/null +++ b/tests/parser.rs @@ -0,0 +1,55 @@ +use myc::ast::nodes::SyntaxKind; +use myc::ast::parser::Parser; +use myc::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 SyntaxKind::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 SyntaxKind::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 SyntaxKind::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 SyntaxKind::Constant(Value::Float(val)) = ast.kind { + assert_eq!(val, -10.5); + } else { + panic!("Expected Float constant, got {:?}", ast.kind); + } +} diff --git a/tests/pipeline.rs b/tests/pipeline.rs new file mode 100644 index 0000000..ec0aac2 --- /dev/null +++ b/tests/pipeline.rs @@ -0,0 +1,34 @@ +use myc::ast::environment::Environment; +use myc::ast::types::Value; + +#[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 Value::Object(obj) = val { + assert_eq!(obj.type_name(), "StreamNode"); + } else { + panic!("Expected an Object(StreamNode)"); + } +} diff --git a/tests/records.rs b/tests/records.rs new file mode 100644 index 0000000..2817840 --- /dev/null +++ b/tests/records.rs @@ -0,0 +1,123 @@ +use myc::ast::environment::Environment; + +#[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})"; + + let res = env.run_script(source_eval).unwrap(); + assert_eq!(format!("{}", res), "99.5"); + + // 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) + (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(); + + 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(); + 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(); + + assert!( + dump.contains("Constant: 10") || dump.contains("GetField: .limit"), + "The limit should be resolved to a constant 10 or kept as GetField." + ); + + 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(); + + // 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}) + (= r1 r2)) + "#; + let res = env.run_script(source).unwrap(); + assert_eq!(format!("{}", res), "false"); +} + diff --git a/tests/rtl.rs b/tests/rtl.rs new file mode 100644 index 0000000..e1a4bb9 --- /dev/null +++ b/tests/rtl.rs @@ -0,0 +1,120 @@ +use myc::ast::environment::Environment; +use myc::ast::types::Value; + +#[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"); + assert_eq!(format!("{}", env.run_script("(% 20 3)").unwrap()), "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"); + assert_eq!(format!("{}", env.run_script("(>> 4 1)").unwrap()), "2"); + assert_eq!(format!("{}", env.run_script("(and 3 1)").unwrap()), "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(); + + let val1_a = env1.run_script("(do (def rand (make-random 123)) (rand))").unwrap(); + let val2_a = env2.run_script("(do (def rand (make-random)) (rand))").unwrap(); + + assert_ne!( + val1_a, val2_a, + "Environments must have isolated PRNG states" + ); + + let val2_b = env2.run_script("(do (def rand-same (make-random 123)) (rand-same))").unwrap(); + 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(); + + let val1 = env.run_script("(do (def rand1 (make-random 42)) (rand1))").unwrap(); + let val2 = env.run_script("(do (def rand2 (make-random 42)) (rand2))").unwrap(); + + assert_eq!(val1, val2, "Random results must be identical for the same seed"); + + let val3 = env.run_script("(do (def rand3 (make-random 123)) (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)"; + + 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); + } + + 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); + } +}