diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index 0ca5e20..dd2775b 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -607,1272 +607,6 @@ impl Interpreter { } } -// ============================================================================ -// Tests -// ============================================================================ - #[cfg(test)] -mod tests { - use super::*; - use crate::lexer::Lexer; - use crate::parser::Parser; - - /// Full pipeline: source → tokens → ast → interpret → last expression value. - /// Wraps in `let __result = ;` so we can read the value back. - fn eval_expr(input: &str) -> Value { - let wrapped = format!("let __result = {};", input); - let (tokens, _) = Lexer::new(&wrapped).tokenize(); - let mut parser = Parser::new(tokens); - let (stmts, errors) = parser.parse(); - assert!(errors.is_empty(), "Parse errors: {:?}", errors); - let mut interpreter = Interpreter::new(); - interpreter.interpret(stmts).expect("Runtime error"); - interpreter.env.borrow().get("__result").expect("No __result in env") - } - - /// Full pipeline for multiple statements. Returns the interpreter for env inspection. - fn run(input: &str) -> Interpreter { - let (tokens, _) = Lexer::new(input).tokenize(); - let mut parser = Parser::new(tokens); - let (stmts, errors) = parser.parse(); - assert!(errors.is_empty(), "Parse errors: {:?}", errors); - let mut interpreter = Interpreter::new(); - interpreter.interpret(stmts).expect("Runtime error"); - interpreter - } - - /// Helper: get a variable from the interpreter's environment. - fn get_var(interp: &Interpreter, name: &str) -> Value { - interp.env.borrow().get(name).unwrap_or(Value::Nil) - } - - /// Helper: assert a number value. - fn assert_num(val: &Value, expected: f64) { - match val { - Value::Number(n) => assert!((n - expected).abs() < f64::EPSILON, - "Expected {}, got {}", expected, n), - _ => panic!("Expected Number({}), got {:?}", expected, val), - } - } - - /// Helper: assert a bool value. - fn assert_bool(val: &Value, expected: bool) { - match val { - Value::Bool(b) => assert_eq!(*b, expected), - _ => panic!("Expected Bool({}), got {:?}", expected, val), - } - } - - /// Helper: assert nil. - fn assert_nil(val: &Value) { - match val { - Value::Nil => {} - _ => panic!("Expected Nil, got {:?}", val), - } - } - - // ========================================================================= - // Literals - // ========================================================================= - - #[test] - fn eval_number() { - assert_num(&eval_expr("42"), 42.0); - } - - #[test] - fn eval_string() { - match eval_expr("\"hello\"") { - Value::String(s) => assert_eq!(s, "hello"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_true() { - assert_bool(&eval_expr("true"), true); - } - - #[test] - fn eval_false() { - assert_bool(&eval_expr("false"), false); - } - - #[test] - fn eval_nil() { - assert_nil(&eval_expr("nil")); - } - - // ========================================================================= - // Arithmetic - // ========================================================================= - - #[test] - fn eval_add() { - assert_num(&eval_expr("1 + 2"), 3.0); - } - - #[test] - fn eval_sub() { - assert_num(&eval_expr("5 - 3"), 2.0); - } - - #[test] - fn eval_mul() { - assert_num(&eval_expr("4 * 3"), 12.0); - } - - #[test] - fn eval_div() { - assert_num(&eval_expr("10 / 4"), 2.5); - } - - #[test] - fn eval_mod() { - assert_num(&eval_expr("7 % 3"), 1.0); - } - - #[test] - fn eval_complex_arithmetic() { - assert_num(&eval_expr("1 + 2 * 3"), 7.0); - assert_num(&eval_expr("(1 + 2) * 3"), 9.0); - } - - // ========================================================================= - // String concatenation - // ========================================================================= - - #[test] - fn eval_string_concat() { - match eval_expr("\"hello \" + \"world\"") { - Value::String(s) => assert_eq!(s, "hello world"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_single_quoted_string() { - match eval_expr("'hello'") { - Value::String(s) => assert_eq!(s, "hello"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_mixed_quotes_concat() { - match eval_expr("\"a\" + 'b'") { - Value::String(s) => assert_eq!(s, "ab"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_single_quote_containing_double() { - match eval_expr("'he\"llo'") { - Value::String(s) => assert_eq!(s, "he\"llo"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_string_plus_number() { - match eval_expr("\"a\" + 42") { - Value::String(s) => assert_eq!(s, "a42"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_number_plus_string() { - match eval_expr("42 + \"a\"") { - Value::String(s) => assert_eq!(s, "42a"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_string_plus_bool() { - match eval_expr("\"t\" + true") { - Value::String(s) => assert_eq!(s, "ttrue"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_bool_plus_string() { - match eval_expr("false + \"x\"") { - Value::String(s) => assert_eq!(s, "falsex"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_string_plus_nil() { - match eval_expr("\"x\" + nil") { - Value::String(s) => assert_eq!(s, "xnil"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_plusequal_string_concat() { - let interp = run("let s = \"hello\"; s += \" world\"; s += 42;"); - match get_var(&interp, "s") { - Value::String(s) => assert_eq!(s, "hello world42"), - v => panic!("Expected String, got {:?}", v), - } - } - - // ========================================================================= - // Comparison - // ========================================================================= - - #[test] - fn eval_equal_numbers() { - assert_bool(&eval_expr("1 == 1"), true); - assert_bool(&eval_expr("1 == 2"), false); - } - - #[test] - fn eval_not_equal() { - assert_bool(&eval_expr("1 != 2"), true); - assert_bool(&eval_expr("1 != 1"), false); - } - - #[test] - fn eval_greater_less() { - assert_bool(&eval_expr("5 > 3"), true); - assert_bool(&eval_expr("3 > 5"), false); - assert_bool(&eval_expr("5 >= 5"), true); - assert_bool(&eval_expr("3 < 5"), true); - assert_bool(&eval_expr("5 < 3"), false); - assert_bool(&eval_expr("3 <= 3"), true); - } - - #[test] - fn eval_equal_strings() { - assert_bool(&eval_expr("\"a\" == \"a\""), true); - assert_bool(&eval_expr("\"a\" == \"b\""), false); - } - - #[test] - fn eval_equal_bools() { - assert_bool(&eval_expr("true == true"), true); - assert_bool(&eval_expr("true == false"), false); - } - - #[test] - fn eval_nil_equals_nil() { - assert_bool(&eval_expr("nil == nil"), true); - } - - // ========================================================================= - // Logical operators - // ========================================================================= - - #[test] - fn eval_logical_and_short_circuit() { - assert_bool(&eval_expr("true && true"), true); - assert_bool(&eval_expr("true && false"), false); - assert_bool(&eval_expr("false && true"), false); - // Short-circuit: false && returns false (the left value) - match eval_expr("false && 999") { - Value::Bool(false) => {} - v => panic!("Expected Bool(false), got {:?}", v), - } - } - - #[test] - fn eval_logical_or_short_circuit() { - assert_bool(&eval_expr("true || false"), true); - assert_bool(&eval_expr("false || true"), true); - assert_bool(&eval_expr("false || false"), false); - // Short-circuit: true || returns true (the left value) - match eval_expr("true || 999") { - Value::Bool(true) => {} - v => panic!("Expected Bool(true), got {:?}", v), - } - } - - // ========================================================================= - // Ternary - // ========================================================================= - - #[test] - fn eval_ternary_true_condition() { - assert_num(&eval_expr("true ? 1 : 2"), 1.0); - } - - #[test] - fn eval_ternary_false_condition() { - assert_num(&eval_expr("false ? 1 : 2"), 2.0); - } - - #[test] - fn eval_ternary_truthy_condition() { - // Non-nil, non-false values are truthy - assert_num(&eval_expr("1 ? 42 : 99"), 42.0); - } - - #[test] - fn eval_ternary_nil_condition() { - assert_num(&eval_expr("nil ? 1 : 2"), 2.0); - } - - #[test] - fn eval_ternary_nested() { - let interp = run("let x = 5; let r = x > 3 ? (x > 10 ? 100 : 50) : 0;"); - assert_num(&get_var(&interp, "r"), 50.0); - } - - #[test] - fn eval_ternary_with_computed_branches() { - let interp = run("let x = 10; let r = x > 5 ? x * 2 : x / 2;"); - assert_num(&get_var(&interp, "r"), 20.0); - } - - #[test] - fn eval_ternary_short_circuit_then() { - // When condition is false, then_branch is not evaluated - let interp = run("let x = 0; let r = false ? (x = 999) : (x = 42);"); - assert_num(&get_var(&interp, "x"), 42.0); - } - - #[test] - fn eval_ternary_short_circuit_else() { - // When condition is true, else_branch is not evaluated - let interp = run("let x = 0; let r = true ? (x = 42) : (x = 999);"); - assert_num(&get_var(&interp, "x"), 42.0); - } - - // ========================================================================= - // Unary - // ========================================================================= - - #[test] - fn eval_negation() { - assert_num(&eval_expr("-5"), -5.0); - assert_num(&eval_expr("--5"), 5.0); - } - - #[test] - fn eval_not() { - assert_bool(&eval_expr("!true"), false); - assert_bool(&eval_expr("!false"), true); - assert_bool(&eval_expr("!nil"), true); - } - - // ========================================================================= - // Assignment and variables - // ========================================================================= - - #[test] - fn eval_variable_definition_and_use() { - let interp = run("let x = 10;"); - assert_num(&get_var(&interp, "x"), 10.0); - } - - #[test] - fn eval_simple_assignment() { - let interp = run("let x = 5; x = 10;"); - assert_num(&get_var(&interp, "x"), 10.0); - } - - #[test] - fn eval_compound_plus_equal() { - let interp = run("let x = 5; x += 3;"); - assert_num(&get_var(&interp, "x"), 8.0); - } - - #[test] - fn eval_compound_minus_equal() { - let interp = run("let x = 10; x -= 3;"); - assert_num(&get_var(&interp, "x"), 7.0); - } - - #[test] - fn eval_compound_star_equal() { - let interp = run("let x = 4; x *= 3;"); - assert_num(&get_var(&interp, "x"), 12.0); - } - - #[test] - fn eval_compound_slash_equal() { - let interp = run("let x = 10; x /= 2;"); - assert_num(&get_var(&interp, "x"), 5.0); - } - - #[test] - fn eval_compound_percent_equal() { - let interp = run("let x = 7; x %= 3;"); - assert_num(&get_var(&interp, "x"), 1.0); - } - - // ========================================================================= - // Const - // ========================================================================= - - #[test] - fn eval_const_definition() { - let interp = run("const x = 42;"); - assert_num(&get_var(&interp, "x"), 42.0); - } - - #[test] - fn error_const_reassignment() { - let result = std::panic::catch_unwind(|| { - run("const x = 5; x = 10;"); - }); - assert!(result.is_err(), "Expected error for reassigning const"); - } - - #[test] - fn error_const_compound_assignment() { - let result = std::panic::catch_unwind(|| { - run("const x = 5; x += 1;"); - }); - assert!(result.is_err(), "Expected error for compound assignment to const"); - } - - #[test] - fn eval_const_shadowing() { - // Inner block can shadow outer const with its own binding - let interp = run("const x = 10; { let x = 20; }"); - assert_num(&get_var(&interp, "x"), 10.0); // Outer unchanged - } - - #[test] - fn eval_const_object_mutation() { - // const prevents rebinding, not property mutation - let interp = run("const obj = { a: 1 }; obj.a = 2;"); - match get_var(&interp, "obj") { - Value::Object(obj) => { - let obj = obj.borrow(); - assert_num(obj.get("a").unwrap(), 2.0); - } - v => panic!("Expected Object, got {:?}", v), - } - } - - #[test] - fn eval_const_array_mutation() { - // const prevents rebinding, not index mutation - let interp = run("const arr = [1, 2]; arr[0] = 99;"); - match get_var(&interp, "arr") { - Value::Array(arr) => { - assert_num(&arr.borrow()[0], 99.0); - } - v => panic!("Expected Array, got {:?}", v), - } - } - - // ========================================================================= - // Scope - // ========================================================================= - - #[test] - fn eval_block_scope_isolation() { - let interp = run("{ let x = 42; }"); - let val = get_var(&interp, "x"); - assert_nil(&val); // x should not exist outside the block - } - - #[test] - fn eval_block_can_access_outer() { - let interp = run("let x = 10; { let y = x + 1; }"); - assert_num(&get_var(&interp, "x"), 10.0); - } - - #[test] - fn eval_shadowing() { - let interp = run("let x = 10; { let x = 20; }"); - assert_num(&get_var(&interp, "x"), 10.0); // Outer x unchanged - } - - #[test] - fn eval_assign_outer_from_inner() { - let interp = run("let x = 10; { x = 20; }"); - assert_num(&get_var(&interp, "x"), 20.0); - } - - // ========================================================================= - // Control flow: if - // ========================================================================= - - #[test] - fn eval_if_true_branch() { - let interp = run("let x = 0; if (true) { x = 1; }"); - assert_num(&get_var(&interp, "x"), 1.0); - } - - #[test] - fn eval_if_false_branch_skipped() { - let interp = run("let x = 0; if (false) { x = 1; }"); - assert_num(&get_var(&interp, "x"), 0.0); - } - - #[test] - fn eval_if_else_true() { - let interp = run("let x = 0; if (true) { x = 1; } else { x = 2; }"); - assert_num(&get_var(&interp, "x"), 1.0); - } - - #[test] - fn eval_if_else_false() { - let interp = run("let x = 0; if (false) { x = 1; } else { x = 2; }"); - assert_num(&get_var(&interp, "x"), 2.0); - } - - #[test] - fn eval_if_truthy() { - // Non-nil, non-false values are truthy - let interp = run("let x = 0; if (1) { x = 42; }"); - assert_num(&get_var(&interp, "x"), 42.0); - } - - // ========================================================================= - // Control flow: while - // ========================================================================= - - #[test] - fn eval_while_loop() { - let interp = run("let i = 0; while (i < 5) { i = i + 1; }"); - assert_num(&get_var(&interp, "i"), 5.0); - } - - #[test] - fn eval_while_never_runs() { - let interp = run("let x = 0; while (false) { x = 1; }"); - assert_num(&get_var(&interp, "x"), 0.0); - } - - // ========================================================================= - // Control flow: for - // ========================================================================= - - #[test] - fn eval_for_loop() { - let interp = run("let sum = 0; for (let i = 0; i < 5; i = i + 1) { sum = sum + i; }"); - assert_num(&get_var(&interp, "sum"), 10.0); // 0+1+2+3+4 - } - - #[test] - fn eval_for_empty_clauses() { - // Infinite loop with break — just test it parses and doesn't crash on one iter - let interp = run("let x = 0; for (;;) { x = 1; break; }"); - assert_num(&get_var(&interp, "x"), 1.0); - } - - // ========================================================================= - // Control flow: break / continue - // ========================================================================= - - #[test] - fn eval_break() { - let interp = run("let i = 0; while (i < 100) { if (i == 5) { break; } i = i + 1; }"); - assert_num(&get_var(&interp, "i"), 5.0); - } - - #[test] - fn eval_continue() { - let interp = run("let sum = 0; let j = 0; while (j < 5) { j = j + 1; if (j == 3) { continue; } sum = sum + j; }"); - assert_num(&get_var(&interp, "sum"), 12.0); // 1+2+4+5 = 12 (skips 3) - } - - #[test] - fn eval_break_in_for() { - let interp = run("let found = -1; for (let k = 0; k < 100; k = k + 1) { if (k * k > 50) { found = k; break; } }"); - assert_num(&get_var(&interp, "found"), 8.0); // 8*8=64 > 50 - } - - // ========================================================================= - // Functions - // ========================================================================= - - #[test] - fn eval_function_call() { - let interp = run("fn add(a, b) { return a + b; } let result = add(3, 4);"); - assert_num(&get_var(&interp, "result"), 7.0); - } - - #[test] - fn eval_function_no_return() { - let interp = run("fn foo() { 42; } let result = foo();"); - assert_nil(&get_var(&interp, "result")); // No explicit return → nil - } - - #[test] - fn eval_recursion() { - let interp = run("fn fib(n) { if (n <= 1) { return n; } return fib(n-1) + fib(n-2); } let f = fib(10);"); - assert_num(&get_var(&interp, "f"), 55.0); - } - - #[test] - fn eval_nested_function_calls() { - let interp = run("fn double(x) { return x * 2; } let r = double(double(3));"); - assert_num(&get_var(&interp, "r"), 12.0); - } - - // ========================================================================= - // Closures - // ========================================================================= - - #[test] - fn eval_simple_closure() { - let interp = run(" - fn make_adder(x) { return fn(y) { return x + y; }; } - let add10 = make_adder(10); - let r = add10(5); - "); - assert_num(&get_var(&interp, "r"), 15.0); - } - - #[test] - fn eval_closure_captures_by_value_semantics() { - // Each call to make_adder creates an independent closure - let interp = run(" - fn make_adder(x) { return fn(y) { return x + y; }; } - let add5 = make_adder(5); - let add10 = make_adder(10); - let r1 = add5(0); - let r2 = add10(0); - "); - assert_num(&get_var(&interp, "r1"), 5.0); - assert_num(&get_var(&interp, "r2"), 10.0); - } - - // ========================================================================= - // Arrays - // ========================================================================= - - #[test] - fn eval_array_creation() { - match eval_expr("[1, 2, 3]") { - Value::Array(arr) => { - let arr = arr.borrow(); - assert_eq!(arr.len(), 3); - assert_num(&arr[0], 1.0); - assert_num(&arr[1], 2.0); - assert_num(&arr[2], 3.0); - } - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn eval_empty_array() { - match eval_expr("[]") { - Value::Array(arr) => assert_eq!(arr.borrow().len(), 0), - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn eval_array_index_access() { - let interp = run("let arr = [10, 20, 30]; let x = arr[0]; let y = arr[2];"); - assert_num(&get_var(&interp, "x"), 10.0); - assert_num(&get_var(&interp, "y"), 30.0); - } - - #[test] - fn eval_array_index_assignment() { - let interp = run("let arr = [1, 2, 3]; arr[0] = 99;"); - match get_var(&interp, "arr") { - Value::Array(arr) => assert_num(&arr.borrow()[0], 99.0), - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn eval_array_compound_index_assignment() { - let interp = run("let arr = [1, 2, 3]; arr[0] += 10;"); - match get_var(&interp, "arr") { - Value::Array(arr) => assert_num(&arr.borrow()[0], 11.0), - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn eval_nested_arrays() { - let interp = run("let arr = [[1, 2], [3, 4]]; let x = arr[1][0];"); - assert_num(&get_var(&interp, "x"), 3.0); - } - - #[test] - fn eval_array_equality() { - assert_bool(&eval_expr("[1, 2, 3] == [1, 2, 3]"), true); - assert_bool(&eval_expr("[1, 2] == [3, 4]"), false); - assert_bool(&eval_expr("[1] == [1, 2]"), false); - } - - // ========================================================================= - // Object equality - // ========================================================================= - - #[test] - fn eval_object_equality_same() { - let interp = run("let a = {x: 1, y: 2}; let b = {x: 1, y: 2}; let r = a == b;"); - assert_bool(&get_var(&interp, "r"), true); - } - - #[test] - fn eval_object_equality_different_values() { - let interp = run("let a = {x: 1}; let b = {x: 2}; let r = a == b;"); - assert_bool(&get_var(&interp, "r"), false); - } - - #[test] - fn eval_object_equality_different_keys() { - let interp = run("let a = {x: 1}; let b = {y: 1}; let r = a == b;"); - assert_bool(&get_var(&interp, "r"), false); - } - - #[test] - fn eval_object_equality_different_size() { - let interp = run("let a = {x: 1}; let b = {x: 1, y: 2}; let r = a == b;"); - assert_bool(&get_var(&interp, "r"), false); - } - - #[test] - fn eval_object_equality_empty() { - let interp = run("let a = {}; let b = {}; let r = a == b;"); - assert_bool(&get_var(&interp, "r"), true); - } - - // ========================================================================= - // Strings as indexable - // ========================================================================= - - #[test] - fn eval_string_index() { - match eval_expr("\"hello\"[0]") { - Value::String(s) => assert_eq!(s, "h"), - v => panic!("Expected String for 'h', got {:?}", v), - } - } - - #[test] - fn eval_string_index_last() { - match eval_expr("\"abc\"[2]") { - Value::String(s) => assert_eq!(s, "c"), - v => panic!("Expected String for 'c', got {:?}", v), - } - } - - // ========================================================================= - // Objects - // ========================================================================= - - #[test] - fn eval_object_creation() { - match eval_expr("{ name: \"aster\", version: 1 }") { - Value::Object(obj) => { - let obj = obj.borrow(); - match obj.get("name") { - Some(Value::String(s)) => assert_eq!(s, "aster"), - v => panic!("Expected String 'aster', got {:?}", v), - } - match obj.get("version") { - Some(Value::Number(n)) => assert!((n - 1.0).abs() < f64::EPSILON), - v => panic!("Expected Number 1, got {:?}", v), - } - } - v => panic!("Expected Object, got {:?}", v), - } - } - - #[test] - fn eval_object_property_access() { - let interp = run("let obj = { name: \"aster\" }; let n = obj.name;"); - match get_var(&interp, "n") { - Value::String(s) => assert_eq!(s, "aster"), - v => panic!("Expected String 'aster', got {:?}", v), - } - } - - #[test] - fn eval_object_property_assignment() { - let interp = run("let obj = { name: \"old\" }; obj.name = \"new\";"); - match get_var(&interp, "obj") { - Value::Object(obj) => { - match obj.borrow().get("name") { - Some(Value::String(s)) => assert_eq!(s, "new"), - v => panic!("Expected String 'new', got {:?}", v), - } - } - v => panic!("Expected Object, got {:?}", v), - } - } - - #[test] - fn eval_object_compound_property_assignment() { - let interp = run("let obj = { age: 26 }; obj.age -= 2;"); - match get_var(&interp, "obj") { - Value::Object(obj) => { - assert_num(obj.borrow().get("age").unwrap(), 24.0); - } - v => panic!("Expected Object, got {:?}", v), - } - } - - // ========================================================================= - // Built-in functions - // ========================================================================= - - #[test] - fn eval_builtin_clock() { - // clock() returns a number (Unix timestamp in seconds) - match eval_expr("clock()") { - Value::Number(n) => assert!(n > 0.0, "clock should return positive number"), - v => panic!("Expected Number from clock(), got {:?}", v), - } - } - - #[test] - fn eval_builtin_os_clock() { - match eval_expr("os.clock()") { - Value::Number(n) => assert!(n > 0.0), - v => panic!("Expected Number from os.clock(), got {:?}", v), - } - } - - #[test] - fn eval_print_returns_nil() { - // print() returns nil - assert_nil(&eval_expr("print(\"test\")")); - } - - // ========================================================================= - // Runtime errors - // ========================================================================= - - #[test] - fn error_undefined_variable() { - let result = std::panic::catch_unwind(|| { - run("let x = y;"); - }); - // run() asserts no errors, so this should panic - assert!(result.is_err(), "Expected error for undefined variable"); - } - - #[test] - fn error_division_by_zero() { - let result = std::panic::catch_unwind(|| { - eval_expr("1 / 0"); - }); - assert!(result.is_err(), "Expected error for division by zero"); - } - - #[test] - fn error_modulo_by_zero() { - let result = std::panic::catch_unwind(|| { - eval_expr("1 % 0"); - }); - assert!(result.is_err(), "Expected error for modulo by zero"); - } - - #[test] - fn error_type_mismatch_negate_string() { - let result = std::panic::catch_unwind(|| { - eval_expr("-\"hello\""); - }); - assert!(result.is_err(), "Expected error for negating a string"); - } - - #[test] - fn eval_number_plus_string_concatenates() { - match eval_expr("1 + \"hello\"") { - Value::String(s) => assert_eq!(s, "1hello"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn error_call_non_function() { - let result = std::panic::catch_unwind(|| { - eval_expr("42()"); - }); - assert!(result.is_err(), "Expected error for calling non-function"); - } - - #[test] - fn error_index_out_of_bounds() { - let result = std::panic::catch_unwind(|| { - eval_expr("[1, 2][5]"); - }); - assert!(result.is_err(), "Expected error for index out of bounds"); - } - - #[test] - fn error_property_on_non_object() { - let result = std::panic::catch_unwind(|| { - eval_expr("42.name"); - }); - assert!(result.is_err(), "Expected error for property on non-object"); - } - - #[test] - fn error_index_on_non_array() { - let result = std::panic::catch_unwind(|| { - eval_expr("42[0]"); - }); - assert!(result.is_err(), "Expected error for index on non-array"); - } - - // ========================================================================= - // Object index access: obj[key] - // ========================================================================= - - #[test] - fn eval_object_index_get() { - assert_num(&eval_expr("{a: 42}[\"a\"]"), 42.0); - } - - #[test] - fn eval_object_index_get_nested() { - assert_num(&eval_expr("{inner: {val: 7}}[\"inner\"][\"val\"]"), 7.0); - } - - #[test] - fn eval_object_index_get_variable_key() { - let interp = run("let obj = {x: 10, y: 20}; let k = \"y\"; let result = obj[k];"); - assert_num(&get_var(&interp, "result"), 20.0); - } - - #[test] - fn eval_object_index_get_undefined() { - let result = std::panic::catch_unwind(|| { - eval_expr("{}[\"x\"]"); - }); - assert!(result.is_err(), "Expected error for undefined property"); - } - - #[test] - fn eval_object_index_set_existing() { - let interp = run("let obj = {a: 1}; obj[\"a\"] = 99;"); - match get_var(&interp, "obj") { - Value::Object(map) => { - let map = map.borrow(); - assert_num(map.get("a").unwrap(), 99.0); - } - v => panic!("Expected Object, got {:?}", v), - } - } - - #[test] - fn eval_object_index_set_new_key() { - let interp = run("let obj = {}; obj[\"name\"] = \"Aster\";"); - match get_var(&interp, "obj") { - Value::Object(map) => { - let map = map.borrow(); - match map.get("name").unwrap() { - Value::String(s) => assert_eq!(s, "Aster"), - v => panic!("Expected String, got {:?}", v), - } - } - v => panic!("Expected Object, got {:?}", v), - } - } - - #[test] - fn eval_object_index_compound_assign() { - let interp = run("let obj = {count: 10}; obj[\"count\"] += 5;"); - match get_var(&interp, "obj") { - Value::Object(map) => { - let map = map.borrow(); - assert_num(map.get("count").unwrap(), 15.0); - } - v => panic!("Expected Object, got {:?}", v), - } - } - - #[test] - fn error_object_index_non_string() { - let result = std::panic::catch_unwind(|| { - eval_expr("{a: 1}[42]"); - }); - assert!(result.is_err(), "Expected error for non-string object index"); - } - - #[test] - fn error_object_index_set_non_string() { - let result = std::panic::catch_unwind(|| { - eval_expr("let obj = {a: 1}; obj[true] = 2;"); - }); - assert!(result.is_err(), "Expected error for non-string object index set"); - } - - // ========================================================================= - // Builtins: len - // ========================================================================= - - #[test] - fn eval_len_string() { - assert_num(&eval_expr("len(\"hello\")"), 5.0); - } - - #[test] - fn eval_len_empty_string() { - assert_num(&eval_expr("len(\"\")"), 0.0); - } - - #[test] - fn eval_len_array() { - assert_num(&eval_expr("len([1, 2, 3])"), 3.0); - } - - #[test] - fn eval_len_empty_array() { - assert_num(&eval_expr("len([])"), 0.0); - } - - #[test] - fn eval_len_object() { - assert_num(&eval_expr("len({a: 1, b: 2})"), 2.0); - } - - #[test] - fn eval_len_empty_object() { - assert_num(&eval_expr("len({})"), 0.0); - } - - #[test] - fn error_len_no_args() { - let result = std::panic::catch_unwind(|| { - eval_expr("len()"); - }); - assert!(result.is_err(), "Expected error for len() with no args"); - } - - #[test] - fn error_len_number() { - let result = std::panic::catch_unwind(|| { - eval_expr("len(42)"); - }); - assert!(result.is_err(), "Expected error for len(42)"); - } - - // ========================================================================= - // Builtins: typeof - // ========================================================================= - - #[test] - fn eval_typeof_number() { - match eval_expr("typeof(42)") { - Value::String(s) => assert_eq!(s, "number"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_typeof_string() { - match eval_expr("typeof(\"hello\")") { - Value::String(s) => assert_eq!(s, "string"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_typeof_bool() { - match eval_expr("typeof(true)") { - Value::String(s) => assert_eq!(s, "bool"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_typeof_nil() { - match eval_expr("typeof(nil)") { - Value::String(s) => assert_eq!(s, "nil"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_typeof_object() { - match eval_expr("typeof({a: 1})") { - Value::String(s) => assert_eq!(s, "object"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_typeof_array() { - match eval_expr("typeof([1, 2])") { - Value::String(s) => assert_eq!(s, "array"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_typeof_function() { - let interp = run("fn f() {}"); - // f is a variable, not an expression — use get_var - match get_var(&interp, "f") { - Value::Function(_) => {} // just verify it's a function - v => panic!("Expected Function, got {:?}", v), - } - } - - // ========================================================================= - // Builtins: push / pop - // ========================================================================= - - #[test] - fn eval_push_returns_value() { - assert_num(&eval_expr("push([1, 2], 3)"), 3.0); - } - - #[test] - fn eval_push_modifies_array() { - let interp = run("let arr = [1, 2]; push(arr, 3);"); - match get_var(&interp, "arr") { - Value::Array(arr) => { - let arr = arr.borrow(); - assert_eq!(arr.len(), 3); - assert_num(&arr[0], 1.0); - assert_num(&arr[1], 2.0); - assert_num(&arr[2], 3.0); - } - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn eval_push_string_to_array() { - let interp = run("let arr = [\"a\"]; push(arr, \"b\");"); - match get_var(&interp, "arr") { - Value::Array(arr) => { - let arr = arr.borrow(); - assert_eq!(arr.len(), 2); - } - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn eval_pop_returns_last() { - assert_num(&eval_expr("pop([1, 2, 3])"), 3.0); - } - - #[test] - fn eval_pop_modifies_array() { - let interp = run("let arr = [1, 2, 3]; pop(arr);"); - match get_var(&interp, "arr") { - Value::Array(arr) => { - let arr = arr.borrow(); - assert_eq!(arr.len(), 2); - assert_num(&arr[0], 1.0); - assert_num(&arr[1], 2.0); - } - v => panic!("Expected Array, got {:?}", v), - } - } - - #[test] - fn error_pop_empty_array() { - let result = std::panic::catch_unwind(|| { - eval_expr("pop([])"); - }); - assert!(result.is_err(), "Expected error for pop([])"); - } - - #[test] - fn error_push_non_array() { - let result = std::panic::catch_unwind(|| { - eval_expr("push(42, 1)"); - }); - assert!(result.is_err(), "Expected error for push on non-array"); - } - - #[test] - fn error_pop_non_array() { - let result = std::panic::catch_unwind(|| { - eval_expr("pop(42)"); - }); - assert!(result.is_err(), "Expected error for pop on non-array"); - } - - // ========================================================================= - // For-In loops - // ========================================================================= - - #[test] - fn eval_forin_array_elements() { - // Sum elements via for-in - let interp = run("let sum = 0; let arr = [1, 2, 3]; for (x in arr) { sum = sum + x; }"); - assert_num(&get_var(&interp, "sum"), 6.0); - } - - #[test] - fn eval_forin_empty_array() { - let interp = run("let count = 0; let arr = []; for (x in arr) { count = count + 1; }"); - assert_num(&get_var(&interp, "count"), 0.0); - } - - #[test] - fn eval_forin_object_keys() { - let interp = run("let keys = \"\"; let obj = {a: 1, b: 2}; for (k in obj) { keys = keys + k; }"); - match get_var(&interp, "keys") { - Value::String(s) => { - // Object iteration order is not guaranteed, so check length - assert_eq!(s.len(), 2); - assert!(s.contains('a')); - assert!(s.contains('b')); - } - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_forin_string_chars() { - let interp = run("let result = \"\"; for (c in \"ab\") { result = result + c; }"); - match get_var(&interp, "result") { - Value::String(s) => assert_eq!(s, "ab"), - v => panic!("Expected String, got {:?}", v), - } - } - - #[test] - fn eval_forin_break() { - let interp = run("let sum = 0; for (x in [1, 2, 3, 4]) { if (x == 3) { break; } sum = sum + x; }"); - assert_num(&get_var(&interp, "sum"), 3.0); // 1 + 2 - } - - #[test] - fn eval_forin_continue() { - let interp = run("let sum = 0; for (x in [1, 2, 3]) { if (x == 2) { continue; } sum = sum + x; }"); - assert_num(&get_var(&interp, "sum"), 4.0); // 1 + 3 - } - - #[test] - fn eval_forin_loop_var_doesnt_leak() { - // Loop variable should not be accessible outside the loop - let interp = run("for (x in [1]) { let _ = x; }"); - // x should not exist in the outer scope - match get_var(&interp, "x") { - Value::Nil => {} // Expected: x is not defined - v => panic!("Expected Nil (undefined), got {:?}", v), - } - } - - #[test] - fn eval_forin_with_object_array() { - // Iterate over an array of objects - let interp = run(" - let arr = [{v: 10}, {v: 20}]; - let total = 0; - for (obj in arr) { total = total + obj.v; } - "); - assert_num(&get_var(&interp, "total"), 30.0); - } - - #[test] - fn error_forin_non_iterable() { - let result = std::panic::catch_unwind(|| { - run("for (x in 42) { let _ = x; }"); // Can't iterate a number - }); - assert!(result.is_err(), "Expected error for for-in on non-iterable"); - } -} - +#[path = "tests.rs"] +mod tests; diff --git a/src/interpreter/tests.rs b/src/interpreter/tests.rs new file mode 100644 index 0000000..b2fe298 --- /dev/null +++ b/src/interpreter/tests.rs @@ -0,0 +1,1261 @@ +use super::*; +use crate::lexer::Lexer; +use crate::parser::Parser; + +/// Full pipeline: source → tokens → ast → interpret → last expression value. +/// Wraps in `let __result = ;` so we can read the value back. +fn eval_expr(input: &str) -> Value { + let wrapped = format!("let __result = {};", input); + let (tokens, _) = Lexer::new(&wrapped).tokenize(); + let mut parser = Parser::new(tokens); + let (stmts, errors) = parser.parse(); + assert!(errors.is_empty(), "Parse errors: {:?}", errors); + let mut interpreter = Interpreter::new(); + interpreter.interpret(stmts).expect("Runtime error"); + interpreter.env.borrow().get("__result").expect("No __result in env") +} + +/// Full pipeline for multiple statements. Returns the interpreter for env inspection. +fn run(input: &str) -> Interpreter { + let (tokens, _) = Lexer::new(input).tokenize(); + let mut parser = Parser::new(tokens); + let (stmts, errors) = parser.parse(); + assert!(errors.is_empty(), "Parse errors: {:?}", errors); + let mut interpreter = Interpreter::new(); + interpreter.interpret(stmts).expect("Runtime error"); + interpreter +} + +/// Helper: get a variable from the interpreter's environment. +fn get_var(interp: &Interpreter, name: &str) -> Value { + interp.env.borrow().get(name).unwrap_or(Value::Nil) +} + +/// Helper: assert a number value. +fn assert_num(val: &Value, expected: f64) { + match val { + Value::Number(n) => assert!((n - expected).abs() < f64::EPSILON, + "Expected {}, got {}", expected, n), + _ => panic!("Expected Number({}), got {:?}", expected, val), + } +} + +/// Helper: assert a bool value. +fn assert_bool(val: &Value, expected: bool) { + match val { + Value::Bool(b) => assert_eq!(*b, expected), + _ => panic!("Expected Bool({}), got {:?}", expected, val), + } +} + +/// Helper: assert nil. +fn assert_nil(val: &Value) { + match val { + Value::Nil => {} + _ => panic!("Expected Nil, got {:?}", val), + } +} + +// ========================================================================= +// Literals +// ========================================================================= + +#[test] +fn eval_number() { + assert_num(&eval_expr("42"), 42.0); +} + +#[test] +fn eval_string() { + match eval_expr("\"hello\"") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_true() { + assert_bool(&eval_expr("true"), true); +} + +#[test] +fn eval_false() { + assert_bool(&eval_expr("false"), false); +} + +#[test] +fn eval_nil() { + assert_nil(&eval_expr("nil")); +} + +// ========================================================================= +// Arithmetic +// ========================================================================= + +#[test] +fn eval_add() { + assert_num(&eval_expr("1 + 2"), 3.0); +} + +#[test] +fn eval_sub() { + assert_num(&eval_expr("5 - 3"), 2.0); +} + +#[test] +fn eval_mul() { + assert_num(&eval_expr("4 * 3"), 12.0); +} + +#[test] +fn eval_div() { + assert_num(&eval_expr("10 / 4"), 2.5); +} + +#[test] +fn eval_mod() { + assert_num(&eval_expr("7 % 3"), 1.0); +} + +#[test] +fn eval_complex_arithmetic() { + assert_num(&eval_expr("1 + 2 * 3"), 7.0); + assert_num(&eval_expr("(1 + 2) * 3"), 9.0); +} + +// ========================================================================= +// String concatenation +// ========================================================================= + +#[test] +fn eval_string_concat() { + match eval_expr("\"hello \" + \"world\"") { + Value::String(s) => assert_eq!(s, "hello world"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_single_quoted_string() { + match eval_expr("'hello'") { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_mixed_quotes_concat() { + match eval_expr("\"a\" + 'b'") { + Value::String(s) => assert_eq!(s, "ab"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_single_quote_containing_double() { + match eval_expr("'he\"llo'") { + Value::String(s) => assert_eq!(s, "he\"llo"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_plus_number() { + match eval_expr("\"a\" + 42") { + Value::String(s) => assert_eq!(s, "a42"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_number_plus_string() { + match eval_expr("42 + \"a\"") { + Value::String(s) => assert_eq!(s, "42a"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_plus_bool() { + match eval_expr("\"t\" + true") { + Value::String(s) => assert_eq!(s, "ttrue"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_bool_plus_string() { + match eval_expr("false + \"x\"") { + Value::String(s) => assert_eq!(s, "falsex"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_string_plus_nil() { + match eval_expr("\"x\" + nil") { + Value::String(s) => assert_eq!(s, "xnil"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_plusequal_string_concat() { + let interp = run("let s = \"hello\"; s += \" world\"; s += 42;"); + match get_var(&interp, "s") { + Value::String(s) => assert_eq!(s, "hello world42"), + v => panic!("Expected String, got {:?}", v), + } +} + +// ========================================================================= +// Comparison +// ========================================================================= + +#[test] +fn eval_equal_numbers() { + assert_bool(&eval_expr("1 == 1"), true); + assert_bool(&eval_expr("1 == 2"), false); +} + +#[test] +fn eval_not_equal() { + assert_bool(&eval_expr("1 != 2"), true); + assert_bool(&eval_expr("1 != 1"), false); +} + +#[test] +fn eval_greater_less() { + assert_bool(&eval_expr("5 > 3"), true); + assert_bool(&eval_expr("3 > 5"), false); + assert_bool(&eval_expr("5 >= 5"), true); + assert_bool(&eval_expr("3 < 5"), true); + assert_bool(&eval_expr("5 < 3"), false); + assert_bool(&eval_expr("3 <= 3"), true); +} + +#[test] +fn eval_equal_strings() { + assert_bool(&eval_expr("\"a\" == \"a\""), true); + assert_bool(&eval_expr("\"a\" == \"b\""), false); +} + +#[test] +fn eval_equal_bools() { + assert_bool(&eval_expr("true == true"), true); + assert_bool(&eval_expr("true == false"), false); +} + +#[test] +fn eval_nil_equals_nil() { + assert_bool(&eval_expr("nil == nil"), true); +} + +// ========================================================================= +// Logical operators +// ========================================================================= + +#[test] +fn eval_logical_and_short_circuit() { + assert_bool(&eval_expr("true && true"), true); + assert_bool(&eval_expr("true && false"), false); + assert_bool(&eval_expr("false && true"), false); + // Short-circuit: false && returns false (the left value) + match eval_expr("false && 999") { + Value::Bool(false) => {} + v => panic!("Expected Bool(false), got {:?}", v), + } +} + +#[test] +fn eval_logical_or_short_circuit() { + assert_bool(&eval_expr("true || false"), true); + assert_bool(&eval_expr("false || true"), true); + assert_bool(&eval_expr("false || false"), false); + // Short-circuit: true || returns true (the left value) + match eval_expr("true || 999") { + Value::Bool(true) => {} + v => panic!("Expected Bool(true), got {:?}", v), + } +} + +// ========================================================================= +// Ternary +// ========================================================================= + +#[test] +fn eval_ternary_true_condition() { + assert_num(&eval_expr("true ? 1 : 2"), 1.0); +} + +#[test] +fn eval_ternary_false_condition() { + assert_num(&eval_expr("false ? 1 : 2"), 2.0); +} + +#[test] +fn eval_ternary_truthy_condition() { + // Non-nil, non-false values are truthy + assert_num(&eval_expr("1 ? 42 : 99"), 42.0); +} + +#[test] +fn eval_ternary_nil_condition() { + assert_num(&eval_expr("nil ? 1 : 2"), 2.0); +} + +#[test] +fn eval_ternary_nested() { + let interp = run("let x = 5; let r = x > 3 ? (x > 10 ? 100 : 50) : 0;"); + assert_num(&get_var(&interp, "r"), 50.0); +} + +#[test] +fn eval_ternary_with_computed_branches() { + let interp = run("let x = 10; let r = x > 5 ? x * 2 : x / 2;"); + assert_num(&get_var(&interp, "r"), 20.0); +} + +#[test] +fn eval_ternary_short_circuit_then() { + // When condition is false, then_branch is not evaluated + let interp = run("let x = 0; let r = false ? (x = 999) : (x = 42);"); + assert_num(&get_var(&interp, "x"), 42.0); +} + +#[test] +fn eval_ternary_short_circuit_else() { + // When condition is true, else_branch is not evaluated + let interp = run("let x = 0; let r = true ? (x = 42) : (x = 999);"); + assert_num(&get_var(&interp, "x"), 42.0); +} + +// ========================================================================= +// Unary +// ========================================================================= + +#[test] +fn eval_negation() { + assert_num(&eval_expr("-5"), -5.0); + assert_num(&eval_expr("--5"), 5.0); +} + +#[test] +fn eval_not() { + assert_bool(&eval_expr("!true"), false); + assert_bool(&eval_expr("!false"), true); + assert_bool(&eval_expr("!nil"), true); +} + +// ========================================================================= +// Assignment and variables +// ========================================================================= + +#[test] +fn eval_variable_definition_and_use() { + let interp = run("let x = 10;"); + assert_num(&get_var(&interp, "x"), 10.0); +} + +#[test] +fn eval_simple_assignment() { + let interp = run("let x = 5; x = 10;"); + assert_num(&get_var(&interp, "x"), 10.0); +} + +#[test] +fn eval_compound_plus_equal() { + let interp = run("let x = 5; x += 3;"); + assert_num(&get_var(&interp, "x"), 8.0); +} + +#[test] +fn eval_compound_minus_equal() { + let interp = run("let x = 10; x -= 3;"); + assert_num(&get_var(&interp, "x"), 7.0); +} + +#[test] +fn eval_compound_star_equal() { + let interp = run("let x = 4; x *= 3;"); + assert_num(&get_var(&interp, "x"), 12.0); +} + +#[test] +fn eval_compound_slash_equal() { + let interp = run("let x = 10; x /= 2;"); + assert_num(&get_var(&interp, "x"), 5.0); +} + +#[test] +fn eval_compound_percent_equal() { + let interp = run("let x = 7; x %= 3;"); + assert_num(&get_var(&interp, "x"), 1.0); +} + +// ========================================================================= +// Const +// ========================================================================= + +#[test] +fn eval_const_definition() { + let interp = run("const x = 42;"); + assert_num(&get_var(&interp, "x"), 42.0); +} + +#[test] +fn error_const_reassignment() { + let result = std::panic::catch_unwind(|| { + run("const x = 5; x = 10;"); + }); + assert!(result.is_err(), "Expected error for reassigning const"); +} + +#[test] +fn error_const_compound_assignment() { + let result = std::panic::catch_unwind(|| { + run("const x = 5; x += 1;"); + }); + assert!(result.is_err(), "Expected error for compound assignment to const"); +} + +#[test] +fn eval_const_shadowing() { + // Inner block can shadow outer const with its own binding + let interp = run("const x = 10; { let x = 20; }"); + assert_num(&get_var(&interp, "x"), 10.0); // Outer unchanged +} + +#[test] +fn eval_const_object_mutation() { + // const prevents rebinding, not property mutation + let interp = run("const obj = { a: 1 }; obj.a = 2;"); + match get_var(&interp, "obj") { + Value::Object(obj) => { + let obj = obj.borrow(); + assert_num(obj.get("a").unwrap(), 2.0); + } + v => panic!("Expected Object, got {:?}", v), + } +} + +#[test] +fn eval_const_array_mutation() { + // const prevents rebinding, not index mutation + let interp = run("const arr = [1, 2]; arr[0] = 99;"); + match get_var(&interp, "arr") { + Value::Array(arr) => { + assert_num(&arr.borrow()[0], 99.0); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +// ========================================================================= +// Scope +// ========================================================================= + +#[test] +fn eval_block_scope_isolation() { + let interp = run("{ let x = 42; }"); + let val = get_var(&interp, "x"); + assert_nil(&val); // x should not exist outside the block +} + +#[test] +fn eval_block_can_access_outer() { + let interp = run("let x = 10; { let y = x + 1; }"); + assert_num(&get_var(&interp, "x"), 10.0); +} + +#[test] +fn eval_shadowing() { + let interp = run("let x = 10; { let x = 20; }"); + assert_num(&get_var(&interp, "x"), 10.0); // Outer x unchanged +} + +#[test] +fn eval_assign_outer_from_inner() { + let interp = run("let x = 10; { x = 20; }"); + assert_num(&get_var(&interp, "x"), 20.0); +} + +// ========================================================================= +// Control flow: if +// ========================================================================= + +#[test] +fn eval_if_true_branch() { + let interp = run("let x = 0; if (true) { x = 1; }"); + assert_num(&get_var(&interp, "x"), 1.0); +} + +#[test] +fn eval_if_false_branch_skipped() { + let interp = run("let x = 0; if (false) { x = 1; }"); + assert_num(&get_var(&interp, "x"), 0.0); +} + +#[test] +fn eval_if_else_true() { + let interp = run("let x = 0; if (true) { x = 1; } else { x = 2; }"); + assert_num(&get_var(&interp, "x"), 1.0); +} + +#[test] +fn eval_if_else_false() { + let interp = run("let x = 0; if (false) { x = 1; } else { x = 2; }"); + assert_num(&get_var(&interp, "x"), 2.0); +} + +#[test] +fn eval_if_truthy() { + // Non-nil, non-false values are truthy + let interp = run("let x = 0; if (1) { x = 42; }"); + assert_num(&get_var(&interp, "x"), 42.0); +} + +// ========================================================================= +// Control flow: while +// ========================================================================= + +#[test] +fn eval_while_loop() { + let interp = run("let i = 0; while (i < 5) { i = i + 1; }"); + assert_num(&get_var(&interp, "i"), 5.0); +} + +#[test] +fn eval_while_never_runs() { + let interp = run("let x = 0; while (false) { x = 1; }"); + assert_num(&get_var(&interp, "x"), 0.0); +} + +// ========================================================================= +// Control flow: for +// ========================================================================= + +#[test] +fn eval_for_loop() { + let interp = run("let sum = 0; for (let i = 0; i < 5; i = i + 1) { sum = sum + i; }"); + assert_num(&get_var(&interp, "sum"), 10.0); // 0+1+2+3+4 +} + +#[test] +fn eval_for_empty_clauses() { + // Infinite loop with break — just test it parses and doesn't crash on one iter + let interp = run("let x = 0; for (;;) { x = 1; break; }"); + assert_num(&get_var(&interp, "x"), 1.0); +} + +// ========================================================================= +// Control flow: break / continue +// ========================================================================= + +#[test] +fn eval_break() { + let interp = run("let i = 0; while (i < 100) { if (i == 5) { break; } i = i + 1; }"); + assert_num(&get_var(&interp, "i"), 5.0); +} + +#[test] +fn eval_continue() { + let interp = run("let sum = 0; let j = 0; while (j < 5) { j = j + 1; if (j == 3) { continue; } sum = sum + j; }"); + assert_num(&get_var(&interp, "sum"), 12.0); // 1+2+4+5 = 12 (skips 3) +} + +#[test] +fn eval_break_in_for() { + let interp = run("let found = -1; for (let k = 0; k < 100; k = k + 1) { if (k * k > 50) { found = k; break; } }"); + assert_num(&get_var(&interp, "found"), 8.0); // 8*8=64 > 50 +} + +// ========================================================================= +// Functions +// ========================================================================= + +#[test] +fn eval_function_call() { + let interp = run("fn add(a, b) { return a + b; } let result = add(3, 4);"); + assert_num(&get_var(&interp, "result"), 7.0); +} + +#[test] +fn eval_function_no_return() { + let interp = run("fn foo() { 42; } let result = foo();"); + assert_nil(&get_var(&interp, "result")); // No explicit return → nil +} + +#[test] +fn eval_recursion() { + let interp = run("fn fib(n) { if (n <= 1) { return n; } return fib(n-1) + fib(n-2); } let f = fib(10);"); + assert_num(&get_var(&interp, "f"), 55.0); +} + +#[test] +fn eval_nested_function_calls() { + let interp = run("fn double(x) { return x * 2; } let r = double(double(3));"); + assert_num(&get_var(&interp, "r"), 12.0); +} + +// ========================================================================= +// Closures +// ========================================================================= + +#[test] +fn eval_simple_closure() { + let interp = run(" + fn make_adder(x) { return fn(y) { return x + y; }; } + let add10 = make_adder(10); + let r = add10(5); + "); + assert_num(&get_var(&interp, "r"), 15.0); +} + +#[test] +fn eval_closure_captures_by_value_semantics() { + // Each call to make_adder creates an independent closure + let interp = run(" + fn make_adder(x) { return fn(y) { return x + y; }; } + let add5 = make_adder(5); + let add10 = make_adder(10); + let r1 = add5(0); + let r2 = add10(0); + "); + assert_num(&get_var(&interp, "r1"), 5.0); + assert_num(&get_var(&interp, "r2"), 10.0); +} + +// ========================================================================= +// Arrays +// ========================================================================= + +#[test] +fn eval_array_creation() { + match eval_expr("[1, 2, 3]") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 3); + assert_num(&arr[0], 1.0); + assert_num(&arr[1], 2.0); + assert_num(&arr[2], 3.0); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_empty_array() { + match eval_expr("[]") { + Value::Array(arr) => assert_eq!(arr.borrow().len(), 0), + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_array_index_access() { + let interp = run("let arr = [10, 20, 30]; let x = arr[0]; let y = arr[2];"); + assert_num(&get_var(&interp, "x"), 10.0); + assert_num(&get_var(&interp, "y"), 30.0); +} + +#[test] +fn eval_array_index_assignment() { + let interp = run("let arr = [1, 2, 3]; arr[0] = 99;"); + match get_var(&interp, "arr") { + Value::Array(arr) => assert_num(&arr.borrow()[0], 99.0), + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_array_compound_index_assignment() { + let interp = run("let arr = [1, 2, 3]; arr[0] += 10;"); + match get_var(&interp, "arr") { + Value::Array(arr) => assert_num(&arr.borrow()[0], 11.0), + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_nested_arrays() { + let interp = run("let arr = [[1, 2], [3, 4]]; let x = arr[1][0];"); + assert_num(&get_var(&interp, "x"), 3.0); +} + +#[test] +fn eval_array_equality() { + assert_bool(&eval_expr("[1, 2, 3] == [1, 2, 3]"), true); + assert_bool(&eval_expr("[1, 2] == [3, 4]"), false); + assert_bool(&eval_expr("[1] == [1, 2]"), false); +} + +// ========================================================================= +// Object equality +// ========================================================================= + +#[test] +fn eval_object_equality_same() { + let interp = run("let a = {x: 1, y: 2}; let b = {x: 1, y: 2}; let r = a == b;"); + assert_bool(&get_var(&interp, "r"), true); +} + +#[test] +fn eval_object_equality_different_values() { + let interp = run("let a = {x: 1}; let b = {x: 2}; let r = a == b;"); + assert_bool(&get_var(&interp, "r"), false); +} + +#[test] +fn eval_object_equality_different_keys() { + let interp = run("let a = {x: 1}; let b = {y: 1}; let r = a == b;"); + assert_bool(&get_var(&interp, "r"), false); +} + +#[test] +fn eval_object_equality_different_size() { + let interp = run("let a = {x: 1}; let b = {x: 1, y: 2}; let r = a == b;"); + assert_bool(&get_var(&interp, "r"), false); +} + +#[test] +fn eval_object_equality_empty() { + let interp = run("let a = {}; let b = {}; let r = a == b;"); + assert_bool(&get_var(&interp, "r"), true); +} + +// ========================================================================= +// Strings as indexable +// ========================================================================= + +#[test] +fn eval_string_index() { + match eval_expr("\"hello\"[0]") { + Value::String(s) => assert_eq!(s, "h"), + v => panic!("Expected String for 'h', got {:?}", v), + } +} + +#[test] +fn eval_string_index_last() { + match eval_expr("\"abc\"[2]") { + Value::String(s) => assert_eq!(s, "c"), + v => panic!("Expected String for 'c', got {:?}", v), + } +} + +// ========================================================================= +// Objects +// ========================================================================= + +#[test] +fn eval_object_creation() { + match eval_expr("{ name: \"aster\", version: 1 }") { + Value::Object(obj) => { + let obj = obj.borrow(); + match obj.get("name") { + Some(Value::String(s)) => assert_eq!(s, "aster"), + v => panic!("Expected String 'aster', got {:?}", v), + } + match obj.get("version") { + Some(Value::Number(n)) => assert!((n - 1.0).abs() < f64::EPSILON), + v => panic!("Expected Number 1, got {:?}", v), + } + } + v => panic!("Expected Object, got {:?}", v), + } +} + +#[test] +fn eval_object_property_access() { + let interp = run("let obj = { name: \"aster\" }; let n = obj.name;"); + match get_var(&interp, "n") { + Value::String(s) => assert_eq!(s, "aster"), + v => panic!("Expected String 'aster', got {:?}", v), + } +} + +#[test] +fn eval_object_property_assignment() { + let interp = run("let obj = { name: \"old\" }; obj.name = \"new\";"); + match get_var(&interp, "obj") { + Value::Object(obj) => { + match obj.borrow().get("name") { + Some(Value::String(s)) => assert_eq!(s, "new"), + v => panic!("Expected String 'new', got {:?}", v), + } + } + v => panic!("Expected Object, got {:?}", v), + } +} + +#[test] +fn eval_object_compound_property_assignment() { + let interp = run("let obj = { age: 26 }; obj.age -= 2;"); + match get_var(&interp, "obj") { + Value::Object(obj) => { + assert_num(obj.borrow().get("age").unwrap(), 24.0); + } + v => panic!("Expected Object, got {:?}", v), + } +} + +// ========================================================================= +// Built-in functions +// ========================================================================= + +#[test] +fn eval_builtin_clock() { + // clock() returns a number (Unix timestamp in seconds) + match eval_expr("clock()") { + Value::Number(n) => assert!(n > 0.0, "clock should return positive number"), + v => panic!("Expected Number from clock(), got {:?}", v), + } +} + +#[test] +fn eval_builtin_os_clock() { + match eval_expr("os.clock()") { + Value::Number(n) => assert!(n > 0.0), + v => panic!("Expected Number from os.clock(), got {:?}", v), + } +} + +#[test] +fn eval_print_returns_nil() { + // print() returns nil + assert_nil(&eval_expr("print(\"test\")")); +} + +// ========================================================================= +// Runtime errors +// ========================================================================= + +#[test] +fn error_undefined_variable() { + let result = std::panic::catch_unwind(|| { + run("let x = y;"); + }); + // run() asserts no errors, so this should panic + assert!(result.is_err(), "Expected error for undefined variable"); +} + +#[test] +fn error_division_by_zero() { + let result = std::panic::catch_unwind(|| { + eval_expr("1 / 0"); + }); + assert!(result.is_err(), "Expected error for division by zero"); +} + +#[test] +fn error_modulo_by_zero() { + let result = std::panic::catch_unwind(|| { + eval_expr("1 % 0"); + }); + assert!(result.is_err(), "Expected error for modulo by zero"); +} + +#[test] +fn error_type_mismatch_negate_string() { + let result = std::panic::catch_unwind(|| { + eval_expr("-\"hello\""); + }); + assert!(result.is_err(), "Expected error for negating a string"); +} + +#[test] +fn eval_number_plus_string_concatenates() { + match eval_expr("1 + \"hello\"") { + Value::String(s) => assert_eq!(s, "1hello"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn error_call_non_function() { + let result = std::panic::catch_unwind(|| { + eval_expr("42()"); + }); + assert!(result.is_err(), "Expected error for calling non-function"); +} + +#[test] +fn error_index_out_of_bounds() { + let result = std::panic::catch_unwind(|| { + eval_expr("[1, 2][5]"); + }); + assert!(result.is_err(), "Expected error for index out of bounds"); +} + +#[test] +fn error_property_on_non_object() { + let result = std::panic::catch_unwind(|| { + eval_expr("42.name"); + }); + assert!(result.is_err(), "Expected error for property on non-object"); +} + +#[test] +fn error_index_on_non_array() { + let result = std::panic::catch_unwind(|| { + eval_expr("42[0]"); + }); + assert!(result.is_err(), "Expected error for index on non-array"); +} + +// ========================================================================= +// Object index access: obj[key] +// ========================================================================= + +#[test] +fn eval_object_index_get() { + assert_num(&eval_expr("{a: 42}[\"a\"]"), 42.0); +} + +#[test] +fn eval_object_index_get_nested() { + assert_num(&eval_expr("{inner: {val: 7}}[\"inner\"][\"val\"]"), 7.0); +} + +#[test] +fn eval_object_index_get_variable_key() { + let interp = run("let obj = {x: 10, y: 20}; let k = \"y\"; let result = obj[k];"); + assert_num(&get_var(&interp, "result"), 20.0); +} + +#[test] +fn eval_object_index_get_undefined() { + let result = std::panic::catch_unwind(|| { + eval_expr("{}[\"x\"]"); + }); + assert!(result.is_err(), "Expected error for undefined property"); +} + +#[test] +fn eval_object_index_set_existing() { + let interp = run("let obj = {a: 1}; obj[\"a\"] = 99;"); + match get_var(&interp, "obj") { + Value::Object(map) => { + let map = map.borrow(); + assert_num(map.get("a").unwrap(), 99.0); + } + v => panic!("Expected Object, got {:?}", v), + } +} + +#[test] +fn eval_object_index_set_new_key() { + let interp = run("let obj = {}; obj[\"name\"] = \"Aster\";"); + match get_var(&interp, "obj") { + Value::Object(map) => { + let map = map.borrow(); + match map.get("name").unwrap() { + Value::String(s) => assert_eq!(s, "Aster"), + v => panic!("Expected String, got {:?}", v), + } + } + v => panic!("Expected Object, got {:?}", v), + } +} + +#[test] +fn eval_object_index_compound_assign() { + let interp = run("let obj = {count: 10}; obj[\"count\"] += 5;"); + match get_var(&interp, "obj") { + Value::Object(map) => { + let map = map.borrow(); + assert_num(map.get("count").unwrap(), 15.0); + } + v => panic!("Expected Object, got {:?}", v), + } +} + +#[test] +fn error_object_index_non_string() { + let result = std::panic::catch_unwind(|| { + eval_expr("{a: 1}[42]"); + }); + assert!(result.is_err(), "Expected error for non-string object index"); +} + +#[test] +fn error_object_index_set_non_string() { + let result = std::panic::catch_unwind(|| { + eval_expr("let obj = {a: 1}; obj[true] = 2;"); + }); + assert!(result.is_err(), "Expected error for non-string object index set"); +} + +// ========================================================================= +// Builtins: len +// ========================================================================= + +#[test] +fn eval_len_string() { + assert_num(&eval_expr("len(\"hello\")"), 5.0); +} + +#[test] +fn eval_len_empty_string() { + assert_num(&eval_expr("len(\"\")"), 0.0); +} + +#[test] +fn eval_len_array() { + assert_num(&eval_expr("len([1, 2, 3])"), 3.0); +} + +#[test] +fn eval_len_empty_array() { + assert_num(&eval_expr("len([])"), 0.0); +} + +#[test] +fn eval_len_object() { + assert_num(&eval_expr("len({a: 1, b: 2})"), 2.0); +} + +#[test] +fn eval_len_empty_object() { + assert_num(&eval_expr("len({})"), 0.0); +} + +#[test] +fn error_len_no_args() { + let result = std::panic::catch_unwind(|| { + eval_expr("len()"); + }); + assert!(result.is_err(), "Expected error for len() with no args"); +} + +#[test] +fn error_len_number() { + let result = std::panic::catch_unwind(|| { + eval_expr("len(42)"); + }); + assert!(result.is_err(), "Expected error for len(42)"); +} + +// ========================================================================= +// Builtins: typeof +// ========================================================================= + +#[test] +fn eval_typeof_number() { + match eval_expr("typeof(42)") { + Value::String(s) => assert_eq!(s, "number"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_typeof_string() { + match eval_expr("typeof(\"hello\")") { + Value::String(s) => assert_eq!(s, "string"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_typeof_bool() { + match eval_expr("typeof(true)") { + Value::String(s) => assert_eq!(s, "bool"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_typeof_nil() { + match eval_expr("typeof(nil)") { + Value::String(s) => assert_eq!(s, "nil"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_typeof_object() { + match eval_expr("typeof({a: 1})") { + Value::String(s) => assert_eq!(s, "object"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_typeof_array() { + match eval_expr("typeof([1, 2])") { + Value::String(s) => assert_eq!(s, "array"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_typeof_function() { + let interp = run("fn f() {}"); + // f is a variable, not an expression — use get_var + match get_var(&interp, "f") { + Value::Function(_) => {} // just verify it's a function + v => panic!("Expected Function, got {:?}", v), + } +} + +// ========================================================================= +// Builtins: push / pop +// ========================================================================= + +#[test] +fn eval_push_returns_value() { + assert_num(&eval_expr("push([1, 2], 3)"), 3.0); +} + +#[test] +fn eval_push_modifies_array() { + let interp = run("let arr = [1, 2]; push(arr, 3);"); + match get_var(&interp, "arr") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 3); + assert_num(&arr[0], 1.0); + assert_num(&arr[1], 2.0); + assert_num(&arr[2], 3.0); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_push_string_to_array() { + let interp = run("let arr = [\"a\"]; push(arr, \"b\");"); + match get_var(&interp, "arr") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 2); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn eval_pop_returns_last() { + assert_num(&eval_expr("pop([1, 2, 3])"), 3.0); +} + +#[test] +fn eval_pop_modifies_array() { + let interp = run("let arr = [1, 2, 3]; pop(arr);"); + match get_var(&interp, "arr") { + Value::Array(arr) => { + let arr = arr.borrow(); + assert_eq!(arr.len(), 2); + assert_num(&arr[0], 1.0); + assert_num(&arr[1], 2.0); + } + v => panic!("Expected Array, got {:?}", v), + } +} + +#[test] +fn error_pop_empty_array() { + let result = std::panic::catch_unwind(|| { + eval_expr("pop([])"); + }); + assert!(result.is_err(), "Expected error for pop([])"); +} + +#[test] +fn error_push_non_array() { + let result = std::panic::catch_unwind(|| { + eval_expr("push(42, 1)"); + }); + assert!(result.is_err(), "Expected error for push on non-array"); +} + +#[test] +fn error_pop_non_array() { + let result = std::panic::catch_unwind(|| { + eval_expr("pop(42)"); + }); + assert!(result.is_err(), "Expected error for pop on non-array"); +} + +// ========================================================================= +// For-In loops +// ========================================================================= + +#[test] +fn eval_forin_array_elements() { + // Sum elements via for-in + let interp = run("let sum = 0; let arr = [1, 2, 3]; for (x in arr) { sum = sum + x; }"); + assert_num(&get_var(&interp, "sum"), 6.0); +} + +#[test] +fn eval_forin_empty_array() { + let interp = run("let count = 0; let arr = []; for (x in arr) { count = count + 1; }"); + assert_num(&get_var(&interp, "count"), 0.0); +} + +#[test] +fn eval_forin_object_keys() { + let interp = run("let keys = \"\"; let obj = {a: 1, b: 2}; for (k in obj) { keys = keys + k; }"); + match get_var(&interp, "keys") { + Value::String(s) => { + // Object iteration order is not guaranteed, so check length + assert_eq!(s.len(), 2); + assert!(s.contains('a')); + assert!(s.contains('b')); + } + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_forin_string_chars() { + let interp = run("let result = \"\"; for (c in \"ab\") { result = result + c; }"); + match get_var(&interp, "result") { + Value::String(s) => assert_eq!(s, "ab"), + v => panic!("Expected String, got {:?}", v), + } +} + +#[test] +fn eval_forin_break() { + let interp = run("let sum = 0; for (x in [1, 2, 3, 4]) { if (x == 3) { break; } sum = sum + x; }"); + assert_num(&get_var(&interp, "sum"), 3.0); // 1 + 2 +} + +#[test] +fn eval_forin_continue() { + let interp = run("let sum = 0; for (x in [1, 2, 3]) { if (x == 2) { continue; } sum = sum + x; }"); + assert_num(&get_var(&interp, "sum"), 4.0); // 1 + 3 +} + +#[test] +fn eval_forin_loop_var_doesnt_leak() { + // Loop variable should not be accessible outside the loop + let interp = run("for (x in [1]) { let _ = x; }"); + // x should not exist in the outer scope + match get_var(&interp, "x") { + Value::Nil => {} // Expected: x is not defined + v => panic!("Expected Nil (undefined), got {:?}", v), + } +} + +#[test] +fn eval_forin_with_object_array() { + // Iterate over an array of objects + let interp = run(" + let arr = [{v: 10}, {v: 20}]; + let total = 0; + for (obj in arr) { total = total + obj.v; } + "); + assert_num(&get_var(&interp, "total"), 30.0); +} + +#[test] +fn error_forin_non_iterable() { + let result = std::panic::catch_unwind(|| { + run("for (x in 42) { let _ = x; }"); // Can't iterate a number + }); + assert!(result.is_err(), "Expected error for for-in on non-iterable"); +} diff --git a/src/lexer/lexer.rs b/src/lexer/lexer.rs index c9a8a6d..a79f0de 100644 --- a/src/lexer/lexer.rs +++ b/src/lexer/lexer.rs @@ -346,417 +346,6 @@ fn is_ident_part(c: char) -> bool { is_ident_start(c) || c.is_ascii_digit() } -// ============================================================================ -// Tests -// ============================================================================ - #[cfg(test)] -mod tests { - use super::*; - - /// Helper: tokenize input and return only TokenKinds (drop EOF and positions). - fn tokenize(input: &str) -> Vec { - let (tokens, _errors) = Lexer::new(input).tokenize(); - tokens.into_iter().filter_map(|t| { - if matches!(t.kind, TokenKind::EOF) { None } else { Some(t.kind) } - }).collect() - } - - /// Helper: tokenize and return full tokens (with positions). - fn tokenize_full(input: &str) -> (Vec, Vec) { - Lexer::new(input).tokenize() - } - - // ----- single-character tokens ----- - - #[test] - fn single_char_tokens() { - let kinds = tokenize("(){},;.:"); - assert_eq!(kinds, vec![ - TokenKind::LeftParen, TokenKind::RightParen, - TokenKind::LeftBrace, TokenKind::RightBrace, - TokenKind::Comma, TokenKind::Semicolon, - TokenKind::Dot, TokenKind::Colon, - ]); - } - - #[test] - fn brackets() { - let kinds = tokenize("[]"); - assert_eq!(kinds, vec![TokenKind::LeftBracket, TokenKind::RightBracket]); - } - - // ----- operators ----- - - #[test] - fn arithmetic_operators() { - let kinds = tokenize("+ - * / %"); - assert_eq!(kinds, vec![ - TokenKind::Plus, TokenKind::Minus, TokenKind::Star, - TokenKind::Slash, TokenKind::Percent, - ]); - } - - #[test] - fn comparison_operators() { - let kinds = tokenize("< <= > >="); - assert_eq!(kinds, vec![ - TokenKind::Less, TokenKind::LessEqual, - TokenKind::Greater, TokenKind::GreaterEqual, - ]); - } - - #[test] - fn equality_operators() { - let kinds = tokenize("== !="); - assert_eq!(kinds, vec![TokenKind::EqualEqual, TokenKind::BangEqual]); - } - - #[test] - fn logical_operators() { - let kinds = tokenize("&& || !"); - assert_eq!(kinds, vec![TokenKind::AndAnd, TokenKind::OrOr, TokenKind::Bang]); - } - - #[test] - fn assignment_operators() { - let kinds = tokenize("= += -= *= /= %="); - assert_eq!(kinds, vec![ - TokenKind::Equal, - TokenKind::PlusEqual, TokenKind::MinusEqual, - TokenKind::StarEqual, TokenKind::SlashEqual, - TokenKind::PercentEqual, - ]); - } - - #[test] - fn operator_proximity_no_spaces() { - let kinds = tokenize("a+b-c*d/e%f"); - // identifiers a, b, c, d, e, f interleaved with operators - let ids: Vec<_> = kinds.iter().filter(|k| matches!(k, TokenKind::Identifier(_))).collect(); - assert_eq!(ids.len(), 6); - } - - // ----- numbers ----- - - #[test] - fn integer_literal() { - let kinds = tokenize("42"); - assert_eq!(kinds, vec![TokenKind::Number(42.0)]); - } - - #[test] - fn float_literal() { - let kinds = tokenize("3.14"); - assert_eq!(kinds, vec![TokenKind::Number(3.14)]); - } - - #[test] - fn zero_and_negative() { - // Note: `-` is a separate unary operator, but `0` should lex correctly - let kinds = tokenize("0"); - assert_eq!(kinds, vec![TokenKind::Number(0.0)]); - } - - // ----- strings ----- - - #[test] - fn string_literal() { - let kinds = tokenize("\"hello\""); - assert_eq!(kinds, vec![TokenKind::String("hello".into())]); - } - - #[test] - fn empty_string() { - let kinds = tokenize("\"\""); - assert_eq!(kinds, vec![TokenKind::String("".into())]); - } - - #[test] - fn string_with_spaces() { - let kinds = tokenize("\"hello world\""); - assert_eq!(kinds, vec![TokenKind::String("hello world".into())]); - } - - #[test] - fn multiple_strings() { - let kinds = tokenize("\"a\" \"b\""); - assert_eq!(kinds, vec![ - TokenKind::String("a".into()), - TokenKind::String("b".into()), - ]); - } - - #[test] - fn string_escape_newline() { - let kinds = tokenize("\"a\\nb\""); - assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]); - } - - #[test] - fn string_escape_tab() { - let kinds = tokenize("\"a\\tb\""); - assert_eq!(kinds, vec![TokenKind::String("a\tb".into())]); - } - - #[test] - fn string_escape_quote() { - let kinds = tokenize("\"he\\\"llo\""); - assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]); - } - - #[test] - fn string_escape_backslash() { - let kinds = tokenize("\"a\\\\b\""); - assert_eq!(kinds, vec![TokenKind::String("a\\b".into())]); - } - - #[test] - fn string_escape_carriage_return() { - let kinds = tokenize("\"a\\rb\""); - assert_eq!(kinds, vec![TokenKind::String("a\rb".into())]); - } - - #[test] - fn string_unknown_escape_is_error() { - let (_, errors) = tokenize_full("\"\\x\""); - assert_eq!(errors.len(), 1); - assert!(errors[0].to_string().contains("Unknown escape")); - } - - // ----- single-quoted strings ----- - - #[test] - fn single_quoted_string() { - let kinds = tokenize("'hello'"); - assert_eq!(kinds, vec![TokenKind::String("hello".into())]); - } - - #[test] - fn single_quoted_empty_string() { - let kinds = tokenize("''"); - assert_eq!(kinds, vec![TokenKind::String("".into())]); - } - - #[test] - fn single_quoted_with_escape() { - let kinds = tokenize("'a\\'b'"); - assert_eq!(kinds, vec![TokenKind::String("a'b".into())]); - } - - #[test] - fn single_quoted_with_newline_escape() { - let kinds = tokenize("'a\\nb'"); - assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]); - } - - #[test] - fn mixed_quote_strings() { - let kinds = tokenize("\"double\" 'single'"); - assert_eq!(kinds, vec![ - TokenKind::String("double".into()), - TokenKind::String("single".into()), - ]); - } - - #[test] - fn single_quote_can_contain_double_quote() { - let kinds = tokenize("'he\"llo'"); - assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]); - } - - #[test] - fn double_quote_can_contain_single_quote() { - let kinds = tokenize("\"it's\""); - assert_eq!(kinds, vec![TokenKind::String("it's".into())]); - } - - #[test] - fn unterminated_single_quoted_string() { - let (_, errors) = tokenize_full("'unterminated"); - assert_eq!(errors.len(), 1); - assert!(errors[0].to_string().contains("Unterminated")); - } - - // ----- identifiers and keywords ----- - - #[test] - fn identifiers() { - let kinds = tokenize("foo bar _private x1"); - let ids: Vec<_> = kinds.iter().filter_map(|k| { - if let TokenKind::Identifier(s) = k { Some(s.clone()) } else { None } - }).collect(); - assert_eq!(ids, vec!["foo", "bar", "_private", "x1"]); - } - - #[test] - fn keywords() { - let kinds = tokenize("let const fn if else while for in break continue return true false nil"); - assert_eq!(kinds, vec![ - TokenKind::Let, TokenKind::Const, TokenKind::Fn, - TokenKind::If, TokenKind::Else, - TokenKind::While, TokenKind::For, TokenKind::In, - TokenKind::Break, TokenKind::Continue, - TokenKind::Return, - TokenKind::True, TokenKind::False, - TokenKind::Nil, - ]); - } - - #[test] - fn identifier_looks_like_keyword_prefix() { - // "lettuce" should be identifier, not "let" + error - let kinds = tokenize("lettuce"); - assert_eq!(kinds, vec![TokenKind::Identifier("lettuce".into())]); - } - - // ----- comments ----- - - #[test] - fn line_comment_ignored() { - let kinds = tokenize("// this is a comment\n42"); - assert_eq!(kinds, vec![TokenKind::Number(42.0)]); - } - - #[test] - fn comment_at_end_of_file() { - let kinds = tokenize("42 // trailing comment"); - assert_eq!(kinds, vec![TokenKind::Number(42.0)]); - } - - #[test] - fn comment_only_line() { - let kinds = tokenize("// just a comment"); - assert!(kinds.is_empty()); - } - - #[test] - fn comment_between_tokens() { - let kinds = tokenize("1 // comment\n2"); - assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]); - } - - // ----- whitespace ----- - - #[test] - fn mixed_whitespace() { - let kinds = tokenize(" \t 42\r\n 84"); - assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(84.0)]); - } - - #[test] - fn only_whitespace() { - let kinds = tokenize(" \n\t \n "); - assert!(kinds.is_empty()); - } - - // ----- errors ----- - - #[test] - fn unterminated_string() { - let (_, errors) = tokenize_full("\"no closing quote"); - assert_eq!(errors.len(), 1); - assert!(errors[0].to_string().contains("Unterminated")); - } - - #[test] - fn bare_ampersand() { - let (_, errors) = tokenize_full("&"); - assert_eq!(errors.len(), 1); - assert!(errors[0].to_string().contains("Unexpected '&'")); - } - - #[test] - fn bare_pipe() { - let (_, errors) = tokenize_full("|"); - assert_eq!(errors.len(), 1); - assert!(errors[0].to_string().contains("Unexpected '|'")); - } - - #[test] - fn unexpected_character() { - let (_, errors) = tokenize_full("@"); - assert_eq!(errors.len(), 1); - assert!(errors[0].to_string().contains("Unexpected character")); - } - - #[test] - fn error_does_not_block_rest() { - // Bare `&` produces an error, but tokenization continues - let (tokens, errors) = tokenize_full("& 42"); - assert_eq!(errors.len(), 1); - let kinds: Vec<_> = tokens.iter().filter(|t| !matches!(t.kind, TokenKind::EOF)).map(|t| t.kind.clone()).collect(); - assert_eq!(kinds, vec![TokenKind::Number(42.0)]); - } - - // ----- position tracking ----- - - #[test] - fn positions_single_line() { - let (tokens, _) = tokenize_full("let x = 5"); - // let @ 1:1 - // x @ 1:5 - // = @ 1:7 - // 5 @ 1:9 - // EOF - let token_kinds: Vec<_> = tokens.iter().map(|t| (&t.kind, t.line, t.column)).collect(); - // Check positions for the first token (let) - assert_eq!(token_kinds[0], (&TokenKind::Let, 1, 1)); - // Check positions for fourth token (5) - assert_eq!(token_kinds[3], (&TokenKind::Number(5.0), 1, 9)); - } - - #[test] - fn positions_multi_line() { - let (tokens, _) = tokenize_full("let\nx\n="); - // let @ 1:1 - // x @ 2:1 - // = @ 3:1 - let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect(); - assert_eq!(token_kinds[0], (1, 1)); // let - assert_eq!(token_kinds[1], (2, 1)); // x - assert_eq!(token_kinds[2], (3, 1)); // = - } - - #[test] - fn position_after_string() { - let (tokens, _) = tokenize_full("\"hi\" 42"); - // "hi" @ 1:1 - // 42 @ 1:6 (after "hi" which is 4 chars + space) - let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect(); - assert_eq!(token_kinds[0], (1, 1)); // "hi" (the opening quote position) - assert_eq!(token_kinds[1], (1, 6)); // 42 - } - - // ----- edge cases ----- - - #[test] - fn empty_input() { - let kinds = tokenize(""); - assert!(kinds.is_empty()); - } - - #[test] - fn complex_expression() { - let kinds = tokenize("if (x >= 0 && y < 10) { return x + y; }"); - assert_eq!(kinds, vec![ - TokenKind::If, - TokenKind::LeftParen, - TokenKind::Identifier("x".into()), - TokenKind::GreaterEqual, - TokenKind::Number(0.0), - TokenKind::AndAnd, - TokenKind::Identifier("y".into()), - TokenKind::Less, - TokenKind::Number(10.0), - TokenKind::RightParen, - TokenKind::LeftBrace, - TokenKind::Return, - TokenKind::Identifier("x".into()), - TokenKind::Plus, - TokenKind::Identifier("y".into()), - TokenKind::Semicolon, - TokenKind::RightBrace, - ]); - } -} +#[path = "tests.rs"] +mod tests; diff --git a/src/lexer/tests.rs b/src/lexer/tests.rs new file mode 100644 index 0000000..7720971 --- /dev/null +++ b/src/lexer/tests.rs @@ -0,0 +1,407 @@ +use super::*; + +/// Helper: tokenize input and return only TokenKinds (drop EOF and positions). +fn tokenize(input: &str) -> Vec { + let (tokens, _errors) = Lexer::new(input).tokenize(); + tokens.into_iter().filter_map(|t| { + if matches!(t.kind, TokenKind::EOF) { None } else { Some(t.kind) } + }).collect() +} + +/// Helper: tokenize and return full tokens (with positions). +fn tokenize_full(input: &str) -> (Vec, Vec) { + Lexer::new(input).tokenize() +} + +// ----- single-character tokens ----- + +#[test] +fn single_char_tokens() { + let kinds = tokenize("(){},;.:"); + assert_eq!(kinds, vec![ + TokenKind::LeftParen, TokenKind::RightParen, + TokenKind::LeftBrace, TokenKind::RightBrace, + TokenKind::Comma, TokenKind::Semicolon, + TokenKind::Dot, TokenKind::Colon, + ]); +} + +#[test] +fn brackets() { + let kinds = tokenize("[]"); + assert_eq!(kinds, vec![TokenKind::LeftBracket, TokenKind::RightBracket]); +} + +// ----- operators ----- + +#[test] +fn arithmetic_operators() { + let kinds = tokenize("+ - * / %"); + assert_eq!(kinds, vec![ + TokenKind::Plus, TokenKind::Minus, TokenKind::Star, + TokenKind::Slash, TokenKind::Percent, + ]); +} + +#[test] +fn comparison_operators() { + let kinds = tokenize("< <= > >="); + assert_eq!(kinds, vec![ + TokenKind::Less, TokenKind::LessEqual, + TokenKind::Greater, TokenKind::GreaterEqual, + ]); +} + +#[test] +fn equality_operators() { + let kinds = tokenize("== !="); + assert_eq!(kinds, vec![TokenKind::EqualEqual, TokenKind::BangEqual]); +} + +#[test] +fn logical_operators() { + let kinds = tokenize("&& || !"); + assert_eq!(kinds, vec![TokenKind::AndAnd, TokenKind::OrOr, TokenKind::Bang]); +} + +#[test] +fn assignment_operators() { + let kinds = tokenize("= += -= *= /= %="); + assert_eq!(kinds, vec![ + TokenKind::Equal, + TokenKind::PlusEqual, TokenKind::MinusEqual, + TokenKind::StarEqual, TokenKind::SlashEqual, + TokenKind::PercentEqual, + ]); +} + +#[test] +fn operator_proximity_no_spaces() { + let kinds = tokenize("a+b-c*d/e%f"); + // identifiers a, b, c, d, e, f interleaved with operators + let ids: Vec<_> = kinds.iter().filter(|k| matches!(k, TokenKind::Identifier(_))).collect(); + assert_eq!(ids.len(), 6); +} + +// ----- numbers ----- + +#[test] +fn integer_literal() { + let kinds = tokenize("42"); + assert_eq!(kinds, vec![TokenKind::Number(42.0)]); +} + +#[test] +fn float_literal() { + let kinds = tokenize("3.14"); + assert_eq!(kinds, vec![TokenKind::Number(3.14)]); +} + +#[test] +fn zero_and_negative() { + // Note: `-` is a separate unary operator, but `0` should lex correctly + let kinds = tokenize("0"); + assert_eq!(kinds, vec![TokenKind::Number(0.0)]); +} + +// ----- strings ----- + +#[test] +fn string_literal() { + let kinds = tokenize("\"hello\""); + assert_eq!(kinds, vec![TokenKind::String("hello".into())]); +} + +#[test] +fn empty_string() { + let kinds = tokenize("\"\""); + assert_eq!(kinds, vec![TokenKind::String("".into())]); +} + +#[test] +fn string_with_spaces() { + let kinds = tokenize("\"hello world\""); + assert_eq!(kinds, vec![TokenKind::String("hello world".into())]); +} + +#[test] +fn multiple_strings() { + let kinds = tokenize("\"a\" \"b\""); + assert_eq!(kinds, vec![ + TokenKind::String("a".into()), + TokenKind::String("b".into()), + ]); +} + +#[test] +fn string_escape_newline() { + let kinds = tokenize("\"a\\nb\""); + assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]); +} + +#[test] +fn string_escape_tab() { + let kinds = tokenize("\"a\\tb\""); + assert_eq!(kinds, vec![TokenKind::String("a\tb".into())]); +} + +#[test] +fn string_escape_quote() { + let kinds = tokenize("\"he\\\"llo\""); + assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]); +} + +#[test] +fn string_escape_backslash() { + let kinds = tokenize("\"a\\\\b\""); + assert_eq!(kinds, vec![TokenKind::String("a\\b".into())]); +} + +#[test] +fn string_escape_carriage_return() { + let kinds = tokenize("\"a\\rb\""); + assert_eq!(kinds, vec![TokenKind::String("a\rb".into())]); +} + +#[test] +fn string_unknown_escape_is_error() { + let (_, errors) = tokenize_full("\"\\x\""); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unknown escape")); +} + +// ----- single-quoted strings ----- + +#[test] +fn single_quoted_string() { + let kinds = tokenize("'hello'"); + assert_eq!(kinds, vec![TokenKind::String("hello".into())]); +} + +#[test] +fn single_quoted_empty_string() { + let kinds = tokenize("''"); + assert_eq!(kinds, vec![TokenKind::String("".into())]); +} + +#[test] +fn single_quoted_with_escape() { + let kinds = tokenize("'a\\'b'"); + assert_eq!(kinds, vec![TokenKind::String("a'b".into())]); +} + +#[test] +fn single_quoted_with_newline_escape() { + let kinds = tokenize("'a\\nb'"); + assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]); +} + +#[test] +fn mixed_quote_strings() { + let kinds = tokenize("\"double\" 'single'"); + assert_eq!(kinds, vec![ + TokenKind::String("double".into()), + TokenKind::String("single".into()), + ]); +} + +#[test] +fn single_quote_can_contain_double_quote() { + let kinds = tokenize("'he\"llo'"); + assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]); +} + +#[test] +fn double_quote_can_contain_single_quote() { + let kinds = tokenize("\"it's\""); + assert_eq!(kinds, vec![TokenKind::String("it's".into())]); +} + +#[test] +fn unterminated_single_quoted_string() { + let (_, errors) = tokenize_full("'unterminated"); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unterminated")); +} + +// ----- identifiers and keywords ----- + +#[test] +fn identifiers() { + let kinds = tokenize("foo bar _private x1"); + let ids: Vec<_> = kinds.iter().filter_map(|k| { + if let TokenKind::Identifier(s) = k { Some(s.clone()) } else { None } + }).collect(); + assert_eq!(ids, vec!["foo", "bar", "_private", "x1"]); +} + +#[test] +fn keywords() { + let kinds = tokenize("let const fn if else while for in break continue return true false nil"); + assert_eq!(kinds, vec![ + TokenKind::Let, TokenKind::Const, TokenKind::Fn, + TokenKind::If, TokenKind::Else, + TokenKind::While, TokenKind::For, TokenKind::In, + TokenKind::Break, TokenKind::Continue, + TokenKind::Return, + TokenKind::True, TokenKind::False, + TokenKind::Nil, + ]); +} + +#[test] +fn identifier_looks_like_keyword_prefix() { + // "lettuce" should be identifier, not "let" + error + let kinds = tokenize("lettuce"); + assert_eq!(kinds, vec![TokenKind::Identifier("lettuce".into())]); +} + +// ----- comments ----- + +#[test] +fn line_comment_ignored() { + let kinds = tokenize("// this is a comment\n42"); + assert_eq!(kinds, vec![TokenKind::Number(42.0)]); +} + +#[test] +fn comment_at_end_of_file() { + let kinds = tokenize("42 // trailing comment"); + assert_eq!(kinds, vec![TokenKind::Number(42.0)]); +} + +#[test] +fn comment_only_line() { + let kinds = tokenize("// just a comment"); + assert!(kinds.is_empty()); +} + +#[test] +fn comment_between_tokens() { + let kinds = tokenize("1 // comment\n2"); + assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]); +} + +// ----- whitespace ----- + +#[test] +fn mixed_whitespace() { + let kinds = tokenize(" \t 42\r\n 84"); + assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(84.0)]); +} + +#[test] +fn only_whitespace() { + let kinds = tokenize(" \n\t \n "); + assert!(kinds.is_empty()); +} + +// ----- errors ----- + +#[test] +fn unterminated_string() { + let (_, errors) = tokenize_full("\"no closing quote"); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unterminated")); +} + +#[test] +fn bare_ampersand() { + let (_, errors) = tokenize_full("&"); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unexpected '&'")); +} + +#[test] +fn bare_pipe() { + let (_, errors) = tokenize_full("|"); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unexpected '|'")); +} + +#[test] +fn unexpected_character() { + let (_, errors) = tokenize_full("@"); + assert_eq!(errors.len(), 1); + assert!(errors[0].to_string().contains("Unexpected character")); +} + +#[test] +fn error_does_not_block_rest() { + // Bare `&` produces an error, but tokenization continues + let (tokens, errors) = tokenize_full("& 42"); + assert_eq!(errors.len(), 1); + let kinds: Vec<_> = tokens.iter().filter(|t| !matches!(t.kind, TokenKind::EOF)).map(|t| t.kind.clone()).collect(); + assert_eq!(kinds, vec![TokenKind::Number(42.0)]); +} + +// ----- position tracking ----- + +#[test] +fn positions_single_line() { + let (tokens, _) = tokenize_full("let x = 5"); + // let @ 1:1 + // x @ 1:5 + // = @ 1:7 + // 5 @ 1:9 + // EOF + let token_kinds: Vec<_> = tokens.iter().map(|t| (&t.kind, t.line, t.column)).collect(); + // Check positions for the first token (let) + assert_eq!(token_kinds[0], (&TokenKind::Let, 1, 1)); + // Check positions for fourth token (5) + assert_eq!(token_kinds[3], (&TokenKind::Number(5.0), 1, 9)); +} + +#[test] +fn positions_multi_line() { + let (tokens, _) = tokenize_full("let\nx\n="); + // let @ 1:1 + // x @ 2:1 + // = @ 3:1 + let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect(); + assert_eq!(token_kinds[0], (1, 1)); // let + assert_eq!(token_kinds[1], (2, 1)); // x + assert_eq!(token_kinds[2], (3, 1)); // = +} + +#[test] +fn position_after_string() { + let (tokens, _) = tokenize_full("\"hi\" 42"); + // "hi" @ 1:1 + // 42 @ 1:6 (after "hi" which is 4 chars + space) + let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect(); + assert_eq!(token_kinds[0], (1, 1)); // "hi" (the opening quote position) + assert_eq!(token_kinds[1], (1, 6)); // 42 +} + +// ----- edge cases ----- + +#[test] +fn empty_input() { + let kinds = tokenize(""); + assert!(kinds.is_empty()); +} + +#[test] +fn complex_expression() { + let kinds = tokenize("if (x >= 0 && y < 10) { return x + y; }"); + assert_eq!(kinds, vec![ + TokenKind::If, + TokenKind::LeftParen, + TokenKind::Identifier("x".into()), + TokenKind::GreaterEqual, + TokenKind::Number(0.0), + TokenKind::AndAnd, + TokenKind::Identifier("y".into()), + TokenKind::Less, + TokenKind::Number(10.0), + TokenKind::RightParen, + TokenKind::LeftBrace, + TokenKind::Return, + TokenKind::Identifier("x".into()), + TokenKind::Plus, + TokenKind::Identifier("y".into()), + TokenKind::Semicolon, + TokenKind::RightBrace, + ]); +} diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 4fe9a62..596904f 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -617,830 +617,6 @@ impl Parser { } } -// ======================================================================== -// Tests -// ======================================================================== - #[cfg(test)] -mod tests { - use super::*; - use crate::lexer::Lexer; - - /// Helper: lex + parse, return (stmts, errors). - fn parse(input: &str) -> (Vec, Vec) { - let (tokens, _) = Lexer::new(input).tokenize(); - Parser::new(tokens).parse() - } - - /// Helper: lex + parse single expression (wraps in `let _ = ;`). - fn parse_expr(input: &str) -> Expr { - let (stmts, errors) = parse(&format!("let __x = {};", input)); - assert!(errors.is_empty(), "Parse errors: {:?}", errors); - match &stmts[0] { - Stmt::Let { initializer, .. } => initializer.clone(), - _ => panic!("Expected Let statement"), - } - } - - /// Helper: lex + parse single statement. - fn parse_stmt(input: &str) -> Stmt { - let (stmts, errors) = parse(input); - assert!(errors.is_empty(), "Parse errors: {:?}", errors); - assert_eq!(stmts.len(), 1); - stmts[0].clone() - } - - // ----- literals ----- - - #[test] - fn parse_number() { - match parse_expr("42") { - Expr::Literal(Literal::Number(42.0)) => {} - e => panic!("Expected Number(42.0), got {:?}", e), - } - } - - #[test] - fn parse_float() { - match parse_expr("3.14") { - Expr::Literal(Literal::Number(n)) => assert!((n - 3.14).abs() < 0.001), - e => panic!("Expected Number, got {:?}", e), - } - } - - #[test] - fn parse_string() { - match parse_expr("\"hello\"") { - Expr::Literal(Literal::String(s)) => assert_eq!(s, "hello"), - e => panic!("Expected String, got {:?}", e), - } - } - - #[test] - fn parse_bool_true() { - match parse_expr("true") { - Expr::Literal(Literal::Bool(true)) => {} - e => panic!("Expected Bool(true), got {:?}", e), - } - } - - #[test] - fn parse_bool_false() { - match parse_expr("false") { - Expr::Literal(Literal::Bool(false)) => {} - e => panic!("Expected Bool(false), got {:?}", e), - } - } - - #[test] - fn parse_nil() { - match parse_expr("nil") { - Expr::Literal(Literal::Nil) => {} - e => panic!("Expected Nil, got {:?}", e), - } - } - - // ----- variables ----- - - #[test] - fn parse_variable() { - match parse_expr("x") { - Expr::Variable(name) => assert_eq!(name, "x"), - e => panic!("Expected Variable, got {:?}", e), - } - } - - // ----- unary ----- - - #[test] - fn parse_negation() { - match parse_expr("-5") { - Expr::Unary { op: UnaryOp::Negate, right } => { - match *right { - Expr::Literal(Literal::Number(5.0)) => {} - e => panic!("Expected Number(5), got {:?}", e), - } - } - e => panic!("Expected Unary(Negate, Number), got {:?}", e), - } - } - - #[test] - fn parse_not() { - match parse_expr("!true") { - Expr::Unary { op: UnaryOp::Not, right } => { - match *right { - Expr::Literal(Literal::Bool(true)) => {} - e => panic!("Expected Bool(true), got {:?}", e), - } - } - e => panic!("Expected Unary(Not, Bool), got {:?}", e), - } - } - - #[test] - fn parse_double_negation() { - match parse_expr("!!true") { - Expr::Unary { op: UnaryOp::Not, right } => { - match *right { - Expr::Unary { op: UnaryOp::Not, .. } => {} - e => panic!("Expected nested Unary, got {:?}", e), - } - } - e => panic!("Expected Unary(Not, ...), got {:?}", e), - } - } - - // ----- binary precedence ----- - - #[test] - fn parse_addition() { - match parse_expr("1 + 2") { - Expr::Binary { op: BinaryOp::Add, left, right } => { - match (*left, *right) { - (Expr::Literal(Literal::Number(1.0)), Expr::Literal(Literal::Number(2.0))) => {} - e => panic!("Expected (1, 2), got {:?}", e), - } - } - e => panic!("Expected Binary(Add), got {:?}", e), - } - } - - #[test] - fn parse_multiplication_precedence() { - // 1 + 2 * 3 => 1 + (2 * 3) - match parse_expr("1 + 2 * 3") { - Expr::Binary { op: BinaryOp::Add, left, right } => { - match *left { - Expr::Literal(Literal::Number(1.0)) => {} - e => panic!("Expected left=1, got {:?}", e), - } - match *right { - Expr::Binary { op: BinaryOp::Mul, .. } => {} - e => panic!("Expected right=Binary(Mul), got {:?}", e), - } - } - e => panic!("Expected Binary(Add), got {:?}", e), - } - } - - #[test] - fn parse_modulo() { - match parse_expr("7 % 3") { - Expr::Binary { op: BinaryOp::Mod, .. } => {} - e => panic!("Expected Binary(Mod), got {:?}", e), - } - } - - // ----- comparison ----- - - #[test] - fn parse_comparison_chain() { - // a < b == c - match parse_expr("a < b == c") { - Expr::Binary { op: BinaryOp::Equal, left, right } => { - match *left { - Expr::Binary { op: BinaryOp::Less, .. } => {} - e => panic!("Expected left=Binary(Less), got {:?}", e), - } - match *right { - Expr::Variable(name) => assert_eq!(name, "c"), - e => panic!("Expected right=Variable(c), got {:?}", e), - } - } - e => panic!("Expected Binary(Equal), got {:?}", e), - } - } - - // ----- logical ----- - - #[test] - fn parse_logical_and() { - match parse_expr("true && false") { - Expr::Logical { op: LogicalOp::And, .. } => {} - e => panic!("Expected Logical(And), got {:?}", e), - } - } - - #[test] - fn parse_logical_or() { - match parse_expr("true || false") { - Expr::Logical { op: LogicalOp::Or, .. } => {} - e => panic!("Expected Logical(Or), got {:?}", e), - } - } - - #[test] - fn parse_logical_precedence() { - // a || b && c => a || (b && c) - match parse_expr("a || b && c") { - Expr::Logical { op: LogicalOp::Or, left, right } => { - match *left { - Expr::Variable(name) => assert_eq!(name, "a"), - e => panic!("Expected left=a, got {:?}", e), - } - match *right { - Expr::Logical { op: LogicalOp::And, .. } => {} - e => panic!("Expected right=Logical(And), got {:?}", e), - } - } - e => panic!("Expected Logical(Or), got {:?}", e), - } - } - - // ----- ternary ----- - - #[test] - fn parse_ternary_simple() { - match parse_expr("a ? 1 : 2") { - Expr::Ternary { condition, then_branch, else_branch } => { - assert!(matches!(*condition, Expr::Variable(_))); - assert!(matches!(*then_branch, Expr::Literal(Literal::Number(1.0)))); - assert!(matches!(*else_branch, Expr::Literal(Literal::Number(2.0)))); - } - e => panic!("Expected Ternary, got {:?}", e), - } - } - - #[test] - fn parse_ternary_nested_right_assoc() { - // a ? b : c ? d : e → a ? b : (c ? d : e) - match parse_expr("a ? b : c ? d : e") { - Expr::Ternary { condition, then_branch, else_branch } => { - assert!(matches!(*condition, Expr::Variable(_))); - assert!(matches!(*then_branch, Expr::Variable(_))); - assert!(matches!(*else_branch, Expr::Ternary { .. })); - } - e => panic!("Expected Ternary with nested Ternary else, got {:?}", e), - } - } - - #[test] - fn parse_ternary_with_assignment_in_else() { - // a ? b : c = d → a ? b : (c = d) - match parse_expr("a ? b : c = 5") { - Expr::Ternary { else_branch, .. } => { - assert!(matches!(*else_branch, Expr::Assign { .. })); - } - e => panic!("Expected Ternary with Assign else, got {:?}", e), - } - } - - // ----- assignment ----- - - #[test] - fn parse_simple_assignment() { - match parse_expr("x = 5") { - Expr::Assign { name, op: AssignOp::Equal, value } => { - assert_eq!(name, "x"); - match *value { - Expr::Literal(Literal::Number(5.0)) => {} - e => panic!("Expected Number(5), got {:?}", e), - } - } - e => panic!("Expected Assign, got {:?}", e), - } - } - - #[test] - fn parse_compound_assignment() { - match parse_expr("x += 2") { - Expr::Assign { name, op: AssignOp::PlusEqual, value } => { - assert_eq!(name, "x"); - match *value { - Expr::Literal(Literal::Number(2.0)) => {} - e => panic!("Expected Number(2), got {:?}", e), - } - } - e => panic!("Expected Assign(PlusEqual), got {:?}", e), - } - } - - #[test] - fn parse_all_compound_ops() { - let ops = [ - ("+=", AssignOp::PlusEqual), - ("-=", AssignOp::MinusEqual), - ("*=", AssignOp::StarEqual), - ("/=", AssignOp::SlashEqual), - ("%=", AssignOp::PercentEqual), - ]; - for (op_str, expected_op) in &ops { - let expr = parse_expr(&format!("x {} 1", op_str)); - match expr { - Expr::Assign { op, .. } => { - assert_eq!(std::mem::discriminant(&op), std::mem::discriminant(expected_op)); - } - e => panic!("Expected Assign for '{}', got {:?}", op_str, e), - } - } - } - - // ----- property access ----- - - #[test] - fn parse_property_get() { - match parse_expr("obj.name") { - Expr::Get { object, name } => { - match *object { - Expr::Variable(n) => assert_eq!(n, "obj"), - e => panic!("Expected Variable(obj), got {:?}", e), - } - assert_eq!(name, "name"); - } - e => panic!("Expected Get, got {:?}", e), - } - } - - #[test] - fn parse_property_set() { - match parse_expr("obj.name = 5") { - Expr::Set { object, name, op: AssignOp::Equal, value } => { - match *object { - Expr::Variable(n) => assert_eq!(n, "obj"), - e => panic!("Expected Variable(obj), got {:?}", e), - } - assert_eq!(name, "name"); - match *value { - Expr::Literal(Literal::Number(5.0)) => {} - e => panic!("Expected Number(5), got {:?}", e), - } - } - e => panic!("Expected Set, got {:?}", e), - } - } - - #[test] - fn parse_chained_property_access() { - match parse_expr("a.b.c") { - Expr::Get { object, name } => { - assert_eq!(name, "c"); - match *object { - Expr::Get { name: ref inner, .. } => assert_eq!(inner, "b"), - e => panic!("Expected chained Get, got {:?}", e), - } - } - e => panic!("Expected Get, got {:?}", e), - } - } - - // ----- index access ----- - - #[test] - fn parse_index_get() { - match parse_expr("arr[0]") { - Expr::IndexGet { array, index } => { - match *array { - Expr::Variable(n) => assert_eq!(n, "arr"), - e => panic!("Expected Variable(arr), got {:?}", e), - } - match *index { - Expr::Literal(Literal::Number(0.0)) => {} - e => panic!("Expected Number(0), got {:?}", e), - } - } - e => panic!("Expected IndexGet, got {:?}", e), - } - } - - #[test] - fn parse_index_set() { - match parse_expr("arr[i] = 42") { - Expr::IndexSet { array, index, op: AssignOp::Equal, value } => { - match *array { - Expr::Variable(n) => assert_eq!(n, "arr"), - e => panic!("Expected Variable(arr), got {:?}", e), - } - match *index { - Expr::Variable(n) => assert_eq!(n, "i"), - e => panic!("Expected Variable(i), got {:?}", e), - } - match *value { - Expr::Literal(Literal::Number(42.0)) => {} - e => panic!("Expected Number(42), got {:?}", e), - } - } - e => panic!("Expected IndexSet, got {:?}", e), - } - } - - #[test] - fn parse_nested_index() { - match parse_expr("a[1][2]") { - Expr::IndexGet { array, .. } => { - match *array { - Expr::IndexGet { .. } => {} - e => panic!("Expected nested IndexGet, got {:?}", e), - } - } - e => panic!("Expected IndexGet, got {:?}", e), - } - } - - // ----- function calls ----- - - #[test] - fn parse_call_no_args() { - match parse_expr("f()") { - Expr::Call { callee, arguments } => { - match *callee { - Expr::Variable(n) => assert_eq!(n, "f"), - e => panic!("Expected Variable(f), got {:?}", e), - } - assert!(arguments.is_empty()); - } - e => panic!("Expected Call, got {:?}", e), - } - } - - #[test] - fn parse_call_with_args() { - match parse_expr("add(1, 2, 3)") { - Expr::Call { callee, arguments } => { - match *callee { - Expr::Variable(n) => assert_eq!(n, "add"), - e => panic!("Expected Variable(add), got {:?}", e), - } - assert_eq!(arguments.len(), 3); - } - e => panic!("Expected Call, got {:?}", e), - } - } - - #[test] - fn parse_chained_calls() { - match parse_expr("f()()") { - Expr::Call { callee, .. } => { - match *callee { - Expr::Call { callee, .. } => { - match *callee { - Expr::Variable(n) => assert_eq!(n, "f"), - e => panic!("Expected Variable(f), got {:?}", e), - } - } - e => panic!("Expected inner Call, got {:?}", e), - } - } - e => panic!("Expected Call, got {:?}", e), - } - } - - #[test] - fn parse_method_style_call() { - // obj.method() - match parse_expr("obj.method()") { - Expr::Call { callee, .. } => { - match *callee { - Expr::Get { .. } => {} - e => panic!("Expected Get, got {:?}", e), - } - } - e => panic!("Expected Call, got {:?}", e), - } - } - - // ----- lambda ----- - - #[test] - fn parse_simple_lambda() { - match parse_expr("fn(x, y) { return x + y; }") { - Expr::Lambda { params, body } => { - assert_eq!(params, vec!["x", "y"]); - assert_eq!(body.len(), 1); - } - e => panic!("Expected Lambda, got {:?}", e), - } - } - - #[test] - fn parse_lambda_no_params() { - match parse_expr("fn() { return 42; }") { - Expr::Lambda { params, body } => { - assert!(params.is_empty()); - assert_eq!(body.len(), 1); - } - e => panic!("Expected Lambda, got {:?}", e), - } - } - - // ----- object literal ----- - - #[test] - fn parse_empty_object() { - match parse_expr("{}") { - Expr::ObjectLiteral { properties } => assert!(properties.is_empty()), - e => panic!("Expected ObjectLiteral, got {:?}", e), - } - } - - #[test] - fn parse_object_with_properties() { - match parse_expr("{ name: \"aster\", version: 1 }") { - Expr::ObjectLiteral { properties } => { - assert_eq!(properties.len(), 2); - assert_eq!(properties[0].0, "name"); - assert_eq!(properties[1].0, "version"); - } - e => panic!("Expected ObjectLiteral, got {:?}", e), - } - } - - // ----- array literal ----- - - #[test] - fn parse_empty_array() { - match parse_expr("[]") { - Expr::ArrayLiteral { elements } => assert!(elements.is_empty()), - e => panic!("Expected ArrayLiteral, got {:?}", e), - } - } - - #[test] - fn parse_array_with_elements() { - match parse_expr("[1, 2, 3]") { - Expr::ArrayLiteral { elements } => assert_eq!(elements.len(), 3), - e => panic!("Expected ArrayLiteral, got {:?}", e), - } - } - - #[test] - fn parse_nested_arrays() { - match parse_expr("[[1, 2], [3, 4]]") { - Expr::ArrayLiteral { elements } => { - assert_eq!(elements.len(), 2); - for e in &elements { - assert!(matches!(e, Expr::ArrayLiteral { .. })); - } - } - e => panic!("Expected ArrayLiteral, got {:?}", e), - } - } - - // ----- statements ----- - - #[test] - fn parse_let_statement() { - match parse_stmt("let x = 5;") { - Stmt::Let { name, initializer, .. } => { - assert_eq!(name, "x"); - assert!(matches!(initializer, Expr::Literal(Literal::Number(5.0)))); - } - e => panic!("Expected Let, got {:?}", e), - } - } - - #[test] - fn parse_let_without_semicolon() { - // Semicolons are optional - match parse_stmt("let x = 5") { - Stmt::Let { name, .. } => assert_eq!(name, "x"), - e => panic!("Expected Let, got {:?}", e), - } - } - - #[test] - fn parse_const_statement() { - match parse_stmt("const x = 5;") { - Stmt::Let { name, mutable, .. } => { - assert_eq!(name, "x"); - assert!(!mutable, "const should set mutable=false"); - } - e => panic!("Expected Let (const), got {:?}", e), - } - } - - #[test] - fn parse_expression_statement() { - match parse_stmt("x;") { - Stmt::ExprStmt(Expr::Variable(name)) => assert_eq!(name, "x"), - e => panic!("Expected ExprStmt(Variable), got {:?}", e), - } - } - - #[test] - fn parse_if_statement() { - match parse_stmt("if (x) { 1; }") { - Stmt::If { condition, then_branch, else_branch } => { - assert!(matches!(condition, Expr::Variable(_))); - assert!(matches!(*then_branch, Stmt::Block(_))); - assert!(else_branch.is_none()); - } - e => panic!("Expected If, got {:?}", e), - } - } - - #[test] - fn parse_if_else_statement() { - match parse_stmt("if (x) { 1; } else { 2; }") { - Stmt::If { else_branch, .. } => { - assert!(else_branch.is_some()); - } - e => panic!("Expected If with else, got {:?}", e), - } - } - - #[test] - fn parse_while_statement() { - match parse_stmt("while (x < 10) { x = x + 1; }") { - Stmt::While { condition, body } => { - assert!(matches!(condition, Expr::Binary { .. })); - assert!(matches!(*body, Stmt::Block(_))); - } - e => panic!("Expected While, got {:?}", e), - } - } - - #[test] - fn parse_for_statement() { - match parse_stmt("for (let i = 0; i < 10; i = i + 1) { }") { - Stmt::For { initializer, condition, step, body } => { - assert!(initializer.is_some()); - assert!(condition.is_some()); - assert!(step.is_some()); - assert!(matches!(*body, Stmt::Block(_))); - } - e => panic!("Expected For, got {:?}", e), - } - } - - #[test] - fn parse_for_empty_clauses() { - match parse_stmt("for (;;) { }") { - Stmt::For { initializer, condition, step, .. } => { - assert!(initializer.is_none()); - assert!(condition.is_none()); - assert!(step.is_none()); - } - e => panic!("Expected For, got {:?}", e), - } - } - - #[test] - fn parse_for_in_array() { - match parse_stmt("for (x in arr) { print(x); }") { - Stmt::ForIn { var_name, iterable, body } => { - assert_eq!(var_name, "x"); - assert!(matches!(iterable, Expr::Variable(_))); - assert!(matches!(*body, Stmt::Block(_))); - } - e => panic!("Expected ForIn, got {:?}", e), - } - } - - #[test] - fn parse_for_in_with_object_literal() { - match parse_stmt("for (k in {a:1,b:2}) { print(k); }") { - Stmt::ForIn { var_name, iterable, .. } => { - assert_eq!(var_name, "k"); - assert!(matches!(iterable, Expr::ObjectLiteral { .. })); - } - e => panic!("Expected ForIn, got {:?}", e), - } - } - - #[test] - fn parse_for_in_with_expression() { - match parse_stmt("for (v in get_list()) { }") { - Stmt::ForIn { var_name, .. } => { - assert_eq!(var_name, "v"); - } - e => panic!("Expected ForIn, got {:?}", e), - } - } - - #[test] - fn parse_for_in_distinct_from_c_style() { - // "for (x = 0; ...)" is C-style for, not for-in - match parse_stmt("for (x = 0; x < 10; x = x + 1) { }") { - Stmt::For { .. } => {} - e => panic!("Expected C-style For, got {:?}", e), - } - } - - #[test] - fn parse_block() { - match parse_stmt("{ let x = 1; x; }") { - Stmt::Block(stmts) => assert_eq!(stmts.len(), 2), - e => panic!("Expected Block, got {:?}", e), - } - } - - #[test] - fn parse_return_statement() { - match parse_stmt("return 42;") { - Stmt::Return(Some(expr)) => { - assert!(matches!(expr, Expr::Literal(Literal::Number(42.0)))); - } - e => panic!("Expected Return(42), got {:?}", e), - } - } - - #[test] - fn parse_return_nil() { - match parse_stmt("return;") { - Stmt::Return(None) => {} - e => panic!("Expected Return(None), got {:?}", e), - } - } - - #[test] - fn parse_break_statement() { - match parse_stmt("break;") { - Stmt::Break => {} - e => panic!("Expected Break, got {:?}", e), - } - } - - #[test] - fn parse_continue_statement() { - match parse_stmt("continue;") { - Stmt::Continue => {} - e => panic!("Expected Continue, got {:?}", e), - } - } - - #[test] - fn parse_function_declaration() { - match parse_stmt("fn add(a, b) { return a + b; }") { - Stmt::Function { name, params, body } => { - assert_eq!(name, "add"); - assert_eq!(params, vec!["a", "b"]); - assert_eq!(body.len(), 1); - } - e => panic!("Expected Function, got {:?}", e), - } - } - - #[test] - fn parse_function_no_params() { - match parse_stmt("fn greet() { print(\"hi\"); }") { - Stmt::Function { name, params, .. } => { - assert_eq!(name, "greet"); - assert!(params.is_empty()); - } - e => panic!("Expected Function, got {:?}", e), - } - } - - // ----- error recovery ----- - - #[test] - fn error_recovery_skips_past_semicolon() { - // First statement is invalid, second should parse fine - let (stmts, errors) = parse("let = 5; let y = 10;"); - assert!(!errors.is_empty(), "Should have parse errors"); - // Should have at least the second statement - assert!(stmts.len() >= 1); - // The second statement should be a valid Let for y - match &stmts.last().unwrap() { - Stmt::Let { name, .. } => assert_eq!(name, "y"), - e => panic!("Expected Let(y), got {:?}", e), - } - } - - #[test] - fn error_recovery_at_statement_boundary() { - let (stmts, errors) = parse("let x = @@@; let y = 10;"); - assert!(!errors.is_empty()); - assert!(stmts.len() >= 1); - } - - // ----- parse errors ----- - - #[test] - fn error_invalid_assignment_target() { - let (_stmts, errors) = parse("5 = 10;"); - assert!(!errors.is_empty()); - } - - #[test] - fn error_missing_closing_paren() { - let (_stmts, errors) = parse("if (x { 1; }"); - assert!(!errors.is_empty()); - } - - #[test] - fn error_missing_semicolon_after_for_condition() { - // for(;;){} is valid — test something actually invalid - let (_stmts, _errors) = parse("for (let i = 0 i < 10) { }"); - assert!(!_errors.is_empty()); - } - - // ----- parenthesized expression ----- - - #[test] - fn parse_parenthesized_expression() { - match parse_expr("(1 + 2)") { - Expr::Binary { op: BinaryOp::Add, .. } => {} - e => panic!("Expected Binary(Add), got {:?}", e), - } - } - - #[test] - fn parse_nested_parentheses() { - match parse_expr("((42))") { - Expr::Literal(Literal::Number(42.0)) => {} - e => panic!("Expected Number(42), got {:?}", e), - } - } - } \ No newline at end of file +#[path = "tests.rs"] +mod tests; diff --git a/src/parser/tests.rs b/src/parser/tests.rs new file mode 100644 index 0000000..face204 --- /dev/null +++ b/src/parser/tests.rs @@ -0,0 +1,820 @@ + use super::*; + use crate::lexer::Lexer; + + /// Helper: lex + parse, return (stmts, errors). + fn parse(input: &str) -> (Vec, Vec) { + let (tokens, _) = Lexer::new(input).tokenize(); + Parser::new(tokens).parse() + } + + /// Helper: lex + parse single expression (wraps in `let _ = ;`). + fn parse_expr(input: &str) -> Expr { + let (stmts, errors) = parse(&format!("let __x = {};", input)); + assert!(errors.is_empty(), "Parse errors: {:?}", errors); + match &stmts[0] { + Stmt::Let { initializer, .. } => initializer.clone(), + _ => panic!("Expected Let statement"), + } + } + + /// Helper: lex + parse single statement. + fn parse_stmt(input: &str) -> Stmt { + let (stmts, errors) = parse(input); + assert!(errors.is_empty(), "Parse errors: {:?}", errors); + assert_eq!(stmts.len(), 1); + stmts[0].clone() + } + + // ----- literals ----- + + #[test] + fn parse_number() { + match parse_expr("42") { + Expr::Literal(Literal::Number(42.0)) => {} + e => panic!("Expected Number(42.0), got {:?}", e), + } + } + + #[test] + fn parse_float() { + match parse_expr("3.14") { + Expr::Literal(Literal::Number(n)) => assert!((n - 3.14).abs() < 0.001), + e => panic!("Expected Number, got {:?}", e), + } + } + + #[test] + fn parse_string() { + match parse_expr("\"hello\"") { + Expr::Literal(Literal::String(s)) => assert_eq!(s, "hello"), + e => panic!("Expected String, got {:?}", e), + } + } + + #[test] + fn parse_bool_true() { + match parse_expr("true") { + Expr::Literal(Literal::Bool(true)) => {} + e => panic!("Expected Bool(true), got {:?}", e), + } + } + + #[test] + fn parse_bool_false() { + match parse_expr("false") { + Expr::Literal(Literal::Bool(false)) => {} + e => panic!("Expected Bool(false), got {:?}", e), + } + } + + #[test] + fn parse_nil() { + match parse_expr("nil") { + Expr::Literal(Literal::Nil) => {} + e => panic!("Expected Nil, got {:?}", e), + } + } + + // ----- variables ----- + + #[test] + fn parse_variable() { + match parse_expr("x") { + Expr::Variable(name) => assert_eq!(name, "x"), + e => panic!("Expected Variable, got {:?}", e), + } + } + + // ----- unary ----- + + #[test] + fn parse_negation() { + match parse_expr("-5") { + Expr::Unary { op: UnaryOp::Negate, right } => { + match *right { + Expr::Literal(Literal::Number(5.0)) => {} + e => panic!("Expected Number(5), got {:?}", e), + } + } + e => panic!("Expected Unary(Negate, Number), got {:?}", e), + } + } + + #[test] + fn parse_not() { + match parse_expr("!true") { + Expr::Unary { op: UnaryOp::Not, right } => { + match *right { + Expr::Literal(Literal::Bool(true)) => {} + e => panic!("Expected Bool(true), got {:?}", e), + } + } + e => panic!("Expected Unary(Not, Bool), got {:?}", e), + } + } + + #[test] + fn parse_double_negation() { + match parse_expr("!!true") { + Expr::Unary { op: UnaryOp::Not, right } => { + match *right { + Expr::Unary { op: UnaryOp::Not, .. } => {} + e => panic!("Expected nested Unary, got {:?}", e), + } + } + e => panic!("Expected Unary(Not, ...), got {:?}", e), + } + } + + // ----- binary precedence ----- + + #[test] + fn parse_addition() { + match parse_expr("1 + 2") { + Expr::Binary { op: BinaryOp::Add, left, right } => { + match (*left, *right) { + (Expr::Literal(Literal::Number(1.0)), Expr::Literal(Literal::Number(2.0))) => {} + e => panic!("Expected (1, 2), got {:?}", e), + } + } + e => panic!("Expected Binary(Add), got {:?}", e), + } + } + + #[test] + fn parse_multiplication_precedence() { + // 1 + 2 * 3 => 1 + (2 * 3) + match parse_expr("1 + 2 * 3") { + Expr::Binary { op: BinaryOp::Add, left, right } => { + match *left { + Expr::Literal(Literal::Number(1.0)) => {} + e => panic!("Expected left=1, got {:?}", e), + } + match *right { + Expr::Binary { op: BinaryOp::Mul, .. } => {} + e => panic!("Expected right=Binary(Mul), got {:?}", e), + } + } + e => panic!("Expected Binary(Add), got {:?}", e), + } + } + + #[test] + fn parse_modulo() { + match parse_expr("7 % 3") { + Expr::Binary { op: BinaryOp::Mod, .. } => {} + e => panic!("Expected Binary(Mod), got {:?}", e), + } + } + + // ----- comparison ----- + + #[test] + fn parse_comparison_chain() { + // a < b == c + match parse_expr("a < b == c") { + Expr::Binary { op: BinaryOp::Equal, left, right } => { + match *left { + Expr::Binary { op: BinaryOp::Less, .. } => {} + e => panic!("Expected left=Binary(Less), got {:?}", e), + } + match *right { + Expr::Variable(name) => assert_eq!(name, "c"), + e => panic!("Expected right=Variable(c), got {:?}", e), + } + } + e => panic!("Expected Binary(Equal), got {:?}", e), + } + } + + // ----- logical ----- + + #[test] + fn parse_logical_and() { + match parse_expr("true && false") { + Expr::Logical { op: LogicalOp::And, .. } => {} + e => panic!("Expected Logical(And), got {:?}", e), + } + } + + #[test] + fn parse_logical_or() { + match parse_expr("true || false") { + Expr::Logical { op: LogicalOp::Or, .. } => {} + e => panic!("Expected Logical(Or), got {:?}", e), + } + } + + #[test] + fn parse_logical_precedence() { + // a || b && c => a || (b && c) + match parse_expr("a || b && c") { + Expr::Logical { op: LogicalOp::Or, left, right } => { + match *left { + Expr::Variable(name) => assert_eq!(name, "a"), + e => panic!("Expected left=a, got {:?}", e), + } + match *right { + Expr::Logical { op: LogicalOp::And, .. } => {} + e => panic!("Expected right=Logical(And), got {:?}", e), + } + } + e => panic!("Expected Logical(Or), got {:?}", e), + } + } + + // ----- ternary ----- + + #[test] + fn parse_ternary_simple() { + match parse_expr("a ? 1 : 2") { + Expr::Ternary { condition, then_branch, else_branch } => { + assert!(matches!(*condition, Expr::Variable(_))); + assert!(matches!(*then_branch, Expr::Literal(Literal::Number(1.0)))); + assert!(matches!(*else_branch, Expr::Literal(Literal::Number(2.0)))); + } + e => panic!("Expected Ternary, got {:?}", e), + } + } + + #[test] + fn parse_ternary_nested_right_assoc() { + // a ? b : c ? d : e → a ? b : (c ? d : e) + match parse_expr("a ? b : c ? d : e") { + Expr::Ternary { condition, then_branch, else_branch } => { + assert!(matches!(*condition, Expr::Variable(_))); + assert!(matches!(*then_branch, Expr::Variable(_))); + assert!(matches!(*else_branch, Expr::Ternary { .. })); + } + e => panic!("Expected Ternary with nested Ternary else, got {:?}", e), + } + } + + #[test] + fn parse_ternary_with_assignment_in_else() { + // a ? b : c = d → a ? b : (c = d) + match parse_expr("a ? b : c = 5") { + Expr::Ternary { else_branch, .. } => { + assert!(matches!(*else_branch, Expr::Assign { .. })); + } + e => panic!("Expected Ternary with Assign else, got {:?}", e), + } + } + + // ----- assignment ----- + + #[test] + fn parse_simple_assignment() { + match parse_expr("x = 5") { + Expr::Assign { name, op: AssignOp::Equal, value } => { + assert_eq!(name, "x"); + match *value { + Expr::Literal(Literal::Number(5.0)) => {} + e => panic!("Expected Number(5), got {:?}", e), + } + } + e => panic!("Expected Assign, got {:?}", e), + } + } + + #[test] + fn parse_compound_assignment() { + match parse_expr("x += 2") { + Expr::Assign { name, op: AssignOp::PlusEqual, value } => { + assert_eq!(name, "x"); + match *value { + Expr::Literal(Literal::Number(2.0)) => {} + e => panic!("Expected Number(2), got {:?}", e), + } + } + e => panic!("Expected Assign(PlusEqual), got {:?}", e), + } + } + + #[test] + fn parse_all_compound_ops() { + let ops = [ + ("+=", AssignOp::PlusEqual), + ("-=", AssignOp::MinusEqual), + ("*=", AssignOp::StarEqual), + ("/=", AssignOp::SlashEqual), + ("%=", AssignOp::PercentEqual), + ]; + for (op_str, expected_op) in &ops { + let expr = parse_expr(&format!("x {} 1", op_str)); + match expr { + Expr::Assign { op, .. } => { + assert_eq!(std::mem::discriminant(&op), std::mem::discriminant(expected_op)); + } + e => panic!("Expected Assign for '{}', got {:?}", op_str, e), + } + } + } + + // ----- property access ----- + + #[test] + fn parse_property_get() { + match parse_expr("obj.name") { + Expr::Get { object, name } => { + match *object { + Expr::Variable(n) => assert_eq!(n, "obj"), + e => panic!("Expected Variable(obj), got {:?}", e), + } + assert_eq!(name, "name"); + } + e => panic!("Expected Get, got {:?}", e), + } + } + + #[test] + fn parse_property_set() { + match parse_expr("obj.name = 5") { + Expr::Set { object, name, op: AssignOp::Equal, value } => { + match *object { + Expr::Variable(n) => assert_eq!(n, "obj"), + e => panic!("Expected Variable(obj), got {:?}", e), + } + assert_eq!(name, "name"); + match *value { + Expr::Literal(Literal::Number(5.0)) => {} + e => panic!("Expected Number(5), got {:?}", e), + } + } + e => panic!("Expected Set, got {:?}", e), + } + } + + #[test] + fn parse_chained_property_access() { + match parse_expr("a.b.c") { + Expr::Get { object, name } => { + assert_eq!(name, "c"); + match *object { + Expr::Get { name: ref inner, .. } => assert_eq!(inner, "b"), + e => panic!("Expected chained Get, got {:?}", e), + } + } + e => panic!("Expected Get, got {:?}", e), + } + } + + // ----- index access ----- + + #[test] + fn parse_index_get() { + match parse_expr("arr[0]") { + Expr::IndexGet { array, index } => { + match *array { + Expr::Variable(n) => assert_eq!(n, "arr"), + e => panic!("Expected Variable(arr), got {:?}", e), + } + match *index { + Expr::Literal(Literal::Number(0.0)) => {} + e => panic!("Expected Number(0), got {:?}", e), + } + } + e => panic!("Expected IndexGet, got {:?}", e), + } + } + + #[test] + fn parse_index_set() { + match parse_expr("arr[i] = 42") { + Expr::IndexSet { array, index, op: AssignOp::Equal, value } => { + match *array { + Expr::Variable(n) => assert_eq!(n, "arr"), + e => panic!("Expected Variable(arr), got {:?}", e), + } + match *index { + Expr::Variable(n) => assert_eq!(n, "i"), + e => panic!("Expected Variable(i), got {:?}", e), + } + match *value { + Expr::Literal(Literal::Number(42.0)) => {} + e => panic!("Expected Number(42), got {:?}", e), + } + } + e => panic!("Expected IndexSet, got {:?}", e), + } + } + + #[test] + fn parse_nested_index() { + match parse_expr("a[1][2]") { + Expr::IndexGet { array, .. } => { + match *array { + Expr::IndexGet { .. } => {} + e => panic!("Expected nested IndexGet, got {:?}", e), + } + } + e => panic!("Expected IndexGet, got {:?}", e), + } + } + + // ----- function calls ----- + + #[test] + fn parse_call_no_args() { + match parse_expr("f()") { + Expr::Call { callee, arguments } => { + match *callee { + Expr::Variable(n) => assert_eq!(n, "f"), + e => panic!("Expected Variable(f), got {:?}", e), + } + assert!(arguments.is_empty()); + } + e => panic!("Expected Call, got {:?}", e), + } + } + + #[test] + fn parse_call_with_args() { + match parse_expr("add(1, 2, 3)") { + Expr::Call { callee, arguments } => { + match *callee { + Expr::Variable(n) => assert_eq!(n, "add"), + e => panic!("Expected Variable(add), got {:?}", e), + } + assert_eq!(arguments.len(), 3); + } + e => panic!("Expected Call, got {:?}", e), + } + } + + #[test] + fn parse_chained_calls() { + match parse_expr("f()()") { + Expr::Call { callee, .. } => { + match *callee { + Expr::Call { callee, .. } => { + match *callee { + Expr::Variable(n) => assert_eq!(n, "f"), + e => panic!("Expected Variable(f), got {:?}", e), + } + } + e => panic!("Expected inner Call, got {:?}", e), + } + } + e => panic!("Expected Call, got {:?}", e), + } + } + + #[test] + fn parse_method_style_call() { + // obj.method() + match parse_expr("obj.method()") { + Expr::Call { callee, .. } => { + match *callee { + Expr::Get { .. } => {} + e => panic!("Expected Get, got {:?}", e), + } + } + e => panic!("Expected Call, got {:?}", e), + } + } + + // ----- lambda ----- + + #[test] + fn parse_simple_lambda() { + match parse_expr("fn(x, y) { return x + y; }") { + Expr::Lambda { params, body } => { + assert_eq!(params, vec!["x", "y"]); + assert_eq!(body.len(), 1); + } + e => panic!("Expected Lambda, got {:?}", e), + } + } + + #[test] + fn parse_lambda_no_params() { + match parse_expr("fn() { return 42; }") { + Expr::Lambda { params, body } => { + assert!(params.is_empty()); + assert_eq!(body.len(), 1); + } + e => panic!("Expected Lambda, got {:?}", e), + } + } + + // ----- object literal ----- + + #[test] + fn parse_empty_object() { + match parse_expr("{}") { + Expr::ObjectLiteral { properties } => assert!(properties.is_empty()), + e => panic!("Expected ObjectLiteral, got {:?}", e), + } + } + + #[test] + fn parse_object_with_properties() { + match parse_expr("{ name: \"aster\", version: 1 }") { + Expr::ObjectLiteral { properties } => { + assert_eq!(properties.len(), 2); + assert_eq!(properties[0].0, "name"); + assert_eq!(properties[1].0, "version"); + } + e => panic!("Expected ObjectLiteral, got {:?}", e), + } + } + + // ----- array literal ----- + + #[test] + fn parse_empty_array() { + match parse_expr("[]") { + Expr::ArrayLiteral { elements } => assert!(elements.is_empty()), + e => panic!("Expected ArrayLiteral, got {:?}", e), + } + } + + #[test] + fn parse_array_with_elements() { + match parse_expr("[1, 2, 3]") { + Expr::ArrayLiteral { elements } => assert_eq!(elements.len(), 3), + e => panic!("Expected ArrayLiteral, got {:?}", e), + } + } + + #[test] + fn parse_nested_arrays() { + match parse_expr("[[1, 2], [3, 4]]") { + Expr::ArrayLiteral { elements } => { + assert_eq!(elements.len(), 2); + for e in &elements { + assert!(matches!(e, Expr::ArrayLiteral { .. })); + } + } + e => panic!("Expected ArrayLiteral, got {:?}", e), + } + } + + // ----- statements ----- + + #[test] + fn parse_let_statement() { + match parse_stmt("let x = 5;") { + Stmt::Let { name, initializer, .. } => { + assert_eq!(name, "x"); + assert!(matches!(initializer, Expr::Literal(Literal::Number(5.0)))); + } + e => panic!("Expected Let, got {:?}", e), + } + } + + #[test] + fn parse_let_without_semicolon() { + // Semicolons are optional + match parse_stmt("let x = 5") { + Stmt::Let { name, .. } => assert_eq!(name, "x"), + e => panic!("Expected Let, got {:?}", e), + } + } + + #[test] + fn parse_const_statement() { + match parse_stmt("const x = 5;") { + Stmt::Let { name, mutable, .. } => { + assert_eq!(name, "x"); + assert!(!mutable, "const should set mutable=false"); + } + e => panic!("Expected Let (const), got {:?}", e), + } + } + + #[test] + fn parse_expression_statement() { + match parse_stmt("x;") { + Stmt::ExprStmt(Expr::Variable(name)) => assert_eq!(name, "x"), + e => panic!("Expected ExprStmt(Variable), got {:?}", e), + } + } + + #[test] + fn parse_if_statement() { + match parse_stmt("if (x) { 1; }") { + Stmt::If { condition, then_branch, else_branch } => { + assert!(matches!(condition, Expr::Variable(_))); + assert!(matches!(*then_branch, Stmt::Block(_))); + assert!(else_branch.is_none()); + } + e => panic!("Expected If, got {:?}", e), + } + } + + #[test] + fn parse_if_else_statement() { + match parse_stmt("if (x) { 1; } else { 2; }") { + Stmt::If { else_branch, .. } => { + assert!(else_branch.is_some()); + } + e => panic!("Expected If with else, got {:?}", e), + } + } + + #[test] + fn parse_while_statement() { + match parse_stmt("while (x < 10) { x = x + 1; }") { + Stmt::While { condition, body } => { + assert!(matches!(condition, Expr::Binary { .. })); + assert!(matches!(*body, Stmt::Block(_))); + } + e => panic!("Expected While, got {:?}", e), + } + } + + #[test] + fn parse_for_statement() { + match parse_stmt("for (let i = 0; i < 10; i = i + 1) { }") { + Stmt::For { initializer, condition, step, body } => { + assert!(initializer.is_some()); + assert!(condition.is_some()); + assert!(step.is_some()); + assert!(matches!(*body, Stmt::Block(_))); + } + e => panic!("Expected For, got {:?}", e), + } + } + + #[test] + fn parse_for_empty_clauses() { + match parse_stmt("for (;;) { }") { + Stmt::For { initializer, condition, step, .. } => { + assert!(initializer.is_none()); + assert!(condition.is_none()); + assert!(step.is_none()); + } + e => panic!("Expected For, got {:?}", e), + } + } + + #[test] + fn parse_for_in_array() { + match parse_stmt("for (x in arr) { print(x); }") { + Stmt::ForIn { var_name, iterable, body } => { + assert_eq!(var_name, "x"); + assert!(matches!(iterable, Expr::Variable(_))); + assert!(matches!(*body, Stmt::Block(_))); + } + e => panic!("Expected ForIn, got {:?}", e), + } + } + + #[test] + fn parse_for_in_with_object_literal() { + match parse_stmt("for (k in {a:1,b:2}) { print(k); }") { + Stmt::ForIn { var_name, iterable, .. } => { + assert_eq!(var_name, "k"); + assert!(matches!(iterable, Expr::ObjectLiteral { .. })); + } + e => panic!("Expected ForIn, got {:?}", e), + } + } + + #[test] + fn parse_for_in_with_expression() { + match parse_stmt("for (v in get_list()) { }") { + Stmt::ForIn { var_name, .. } => { + assert_eq!(var_name, "v"); + } + e => panic!("Expected ForIn, got {:?}", e), + } + } + + #[test] + fn parse_for_in_distinct_from_c_style() { + // "for (x = 0; ...)" is C-style for, not for-in + match parse_stmt("for (x = 0; x < 10; x = x + 1) { }") { + Stmt::For { .. } => {} + e => panic!("Expected C-style For, got {:?}", e), + } + } + + #[test] + fn parse_block() { + match parse_stmt("{ let x = 1; x; }") { + Stmt::Block(stmts) => assert_eq!(stmts.len(), 2), + e => panic!("Expected Block, got {:?}", e), + } + } + + #[test] + fn parse_return_statement() { + match parse_stmt("return 42;") { + Stmt::Return(Some(expr)) => { + assert!(matches!(expr, Expr::Literal(Literal::Number(42.0)))); + } + e => panic!("Expected Return(42), got {:?}", e), + } + } + + #[test] + fn parse_return_nil() { + match parse_stmt("return;") { + Stmt::Return(None) => {} + e => panic!("Expected Return(None), got {:?}", e), + } + } + + #[test] + fn parse_break_statement() { + match parse_stmt("break;") { + Stmt::Break => {} + e => panic!("Expected Break, got {:?}", e), + } + } + + #[test] + fn parse_continue_statement() { + match parse_stmt("continue;") { + Stmt::Continue => {} + e => panic!("Expected Continue, got {:?}", e), + } + } + + #[test] + fn parse_function_declaration() { + match parse_stmt("fn add(a, b) { return a + b; }") { + Stmt::Function { name, params, body } => { + assert_eq!(name, "add"); + assert_eq!(params, vec!["a", "b"]); + assert_eq!(body.len(), 1); + } + e => panic!("Expected Function, got {:?}", e), + } + } + + #[test] + fn parse_function_no_params() { + match parse_stmt("fn greet() { print(\"hi\"); }") { + Stmt::Function { name, params, .. } => { + assert_eq!(name, "greet"); + assert!(params.is_empty()); + } + e => panic!("Expected Function, got {:?}", e), + } + } + + // ----- error recovery ----- + + #[test] + fn error_recovery_skips_past_semicolon() { + // First statement is invalid, second should parse fine + let (stmts, errors) = parse("let = 5; let y = 10;"); + assert!(!errors.is_empty(), "Should have parse errors"); + // Should have at least the second statement + assert!(stmts.len() >= 1); + // The second statement should be a valid Let for y + match &stmts.last().unwrap() { + Stmt::Let { name, .. } => assert_eq!(name, "y"), + e => panic!("Expected Let(y), got {:?}", e), + } + } + + #[test] + fn error_recovery_at_statement_boundary() { + let (stmts, errors) = parse("let x = @@@; let y = 10;"); + assert!(!errors.is_empty()); + assert!(stmts.len() >= 1); + } + + // ----- parse errors ----- + + #[test] + fn error_invalid_assignment_target() { + let (_stmts, errors) = parse("5 = 10;"); + assert!(!errors.is_empty()); + } + + #[test] + fn error_missing_closing_paren() { + let (_stmts, errors) = parse("if (x { 1; }"); + assert!(!errors.is_empty()); + } + + #[test] + fn error_missing_semicolon_after_for_condition() { + // for(;;){} is valid — test something actually invalid + let (_stmts, _errors) = parse("for (let i = 0 i < 10) { }"); + assert!(!_errors.is_empty()); +} + + // ----- parenthesized expression ----- + + #[test] + fn parse_parenthesized_expression() { + match parse_expr("(1 + 2)") { + Expr::Binary { op: BinaryOp::Add, .. } => {} + e => panic!("Expected Binary(Add), got {:?}", e), + } + } + + #[test] + fn parse_nested_parentheses() { + match parse_expr("((42))") { + Expr::Literal(Literal::Number(42.0)) => {} + e => panic!("Expected Number(42), got {:?}", e), + } + }