From 983a7d8b34fd5685bcd69018845d2519b25b8bf3 Mon Sep 17 00:00:00 2001 From: 0264408 Date: Tue, 16 Jun 2026 16:56:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=A0=87=E5=87=86?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/builtins/mod.rs | 2 + src/interpreter/interpreter.rs | 681 +++++++++++++++++++++++++++++ src/lexer/lexer.rs | 324 ++++++++++++++ src/parser/parser.rs | 745 +++++++++++++++++++++++++++++++- 4 files changed, 1751 insertions(+), 1 deletion(-) diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index 9f5ff94..018f913 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -25,4 +25,6 @@ pub fn register_all(env: &Rc>) { let mut os = HashMap::new(); os.insert("clock".into(), Value::NativeFunction(os::clock)); e.define("os".into(), Value::Object(Rc::new(RefCell::new(os)))); + // clock can also be used as a global function for convenience + e.define("clock".into(), Value::NativeFunction(os::clock)); } diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index bc209bd..fcbe6c2 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -487,3 +487,684 @@ 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), + } + } + + // ========================================================================= + // 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), + } + } + + // ========================================================================= + // 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); + } + + // ========================================================================= + // 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); + } + + // ========================================================================= + // 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_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 error_type_mismatch_add_number_and_string() { + let result = std::panic::catch_unwind(|| { + eval_expr("1 + \"hello\""); + }); + assert!(result.is_err(), "Expected error for adding number + string"); + } + + #[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"); + } +} + diff --git a/src/lexer/lexer.rs b/src/lexer/lexer.rs index f12145d..413dcbc 100644 --- a/src/lexer/lexer.rs +++ b/src/lexer/lexer.rs @@ -309,3 +309,327 @@ fn is_ident_start(c: char) -> bool { 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()), + ]); + } + + // ----- 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 fn if else while for break continue return true false nil"); + assert_eq!(kinds, vec![ + TokenKind::Let, TokenKind::Fn, + TokenKind::If, TokenKind::Else, + TokenKind::While, TokenKind::For, + 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 7b7f645..ed71857 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -552,6 +552,12 @@ impl Parser { } fn synchronize(&mut self) { + // Always consume at least one token to guarantee forward progress, + // preventing infinite loops when previous() is already a semicolon. + if !self.is_at_end() { + self.advance(); + } + while !self.is_at_end() { if self.current > 0 && matches!(self.previous().kind, TokenKind::Semicolon) { return; @@ -572,4 +578,741 @@ impl Parser { } } } -} \ No newline at end of file +} + +// ======================================================================== +// 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), + } + } + + // ----- 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_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_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