//! End-to-end frontend tests: source text -> lexer -> parser -> AST. //! //! These exercise the lexer and parser together through the public API and //! assert on the resulting AST, complementing the per-module unit tests inside //! `src/frontend/{lexer,parser}.rs`. use rlox::common::ast::{AstNode, Expr}; use rlox::common::base_value::{BaseValue, Number}; use rlox::frontend::lexer::Lexer; use rlox::frontend::parser::Parser; use rlox::frontend::tokens::TokenType; /// Run the full frontend pipeline on `src`. fn parse(src: &str) -> Result, String> { let tokens = Lexer::new(src.to_string(), 0) .scans_tokens() .map_err(|e| format!("lex error: {e}"))?; Parser::new(tokens) .parse() .map_err(|e| format!("parse error: {e}")) } /// Parse `src`, panicking if the frontend reports any error. fn parse_ok(src: &str) -> Vec { parse(src).expect("expected source to lex and parse") } /// Parse `src` and return the single node it should produce. /// /// Statements and expressions share the unified `Expr`/`AstNode` type, so this /// simply returns the node of the sole top-level statement. fn single_stmt(src: &str) -> Expr { let mut stmts = parse_ok(src); assert_eq!(stmts.len(), 1, "expected exactly one statement for {src:?}"); stmts.remove(0).node } /// Parse `src` and return its single expression-statement's expression. /// /// Expression statements aren't wrapped in a dedicated variant anymore; the /// node itself is the expression. fn single_expr(src: &str) -> Expr { single_stmt(src) } // --------------------------------------------------------------------------- // Expressions // --------------------------------------------------------------------------- #[test] fn lexes_and_parses_number_literal() { match single_expr("42;") { Expr::Literal { value } => assert_eq!(*value, BaseValue::Number(Number::I32(42))), other => panic!("expected literal, got {other:?}"), } } #[test] fn parses_string_and_boolean_literals() { assert!(matches!(single_expr("true;"), Expr::Literal { .. })); assert!(matches!(single_expr("\"hi\";"), Expr::Literal { .. })); } #[test] fn multiplication_binds_tighter_than_addition() { // 1 + 2 * 3 => (+ 1 (* 2 3)) match single_expr("1 + 2 * 3;") { Expr::Binary { operator, right, .. } => { assert_eq!(operator, TokenType::Plus); assert!(matches!( right.node, Expr::Binary { operator: TokenType::Star, .. } )); } other => panic!("expected binary, got {other:?}"), } } #[test] fn grouping_overrides_precedence() { // (1 + 2) * 3 => (* (group (+ 1 2)) 3) match single_expr("(1 + 2) * 3;") { Expr::Binary { operator, left, .. } => { assert_eq!(operator, TokenType::Star); assert!(matches!(left.node, Expr::Grouping { .. })); } other => panic!("expected binary, got {other:?}"), } } #[test] fn parses_unary_operators() { assert!(matches!( single_expr("-5;"), Expr::Unary { operator: TokenType::Minus, .. } )); assert!(matches!( single_expr("!true;"), Expr::Unary { operator: TokenType::Bang, .. } )); } #[test] fn parses_comparison_and_equality() { assert!(matches!( single_expr("1 < 2;"), Expr::Binary { operator: TokenType::Less, .. } )); assert!(matches!( single_expr("1 == 2;"), Expr::Binary { operator: TokenType::EqualEqual, .. } )); } #[test] fn parses_logical_operators() { assert!(matches!( single_expr("true or false;"), Expr::Binary { operator: TokenType::Or, .. } )); assert!(matches!( single_expr("true and false;"), Expr::Binary { operator: TokenType::And, .. } )); } #[test] fn parses_call_with_arguments() { match single_expr("add(1, 2);") { Expr::Call { callee, arguments } => { assert!(matches!(callee.node, Expr::Identifier { .. })); assert_eq!(arguments.len(), 2); } other => panic!("expected call, got {other:?}"), } } // --------------------------------------------------------------------------- // Assignment (now an expression) // --------------------------------------------------------------------------- #[test] fn assignment_is_an_expression_statement() { match single_expr("x = 5;") { Expr::Assign { name, .. } => assert_eq!(name, "x"), other => panic!("expected assign, got {other:?}"), } } #[test] fn assignment_is_right_associative() { // a = b = c => (assign a (assign b c)) match single_expr("a = b = c;") { Expr::Assign { name, value } => { assert_eq!(name, "a"); match value.node { Expr::Assign { name, .. } => assert_eq!(name, "b"), other => panic!("expected nested assign, got {other:?}"), } } other => panic!("expected assign, got {other:?}"), } } #[test] fn assignment_to_non_identifier_is_an_error() { assert!(parse("1 = 2;").is_err()); } // --------------------------------------------------------------------------- // Statements // --------------------------------------------------------------------------- #[test] fn parses_var_declaration() { match single_stmt("x := 5;") { Expr::VarDeclaration { name, initializer, .. } => { assert_eq!(name, "x"); assert!(initializer.is_some()); } other => panic!("expected var declaration, got {other:?}"), } } #[test] fn parses_print_statement() { assert!(matches!(single_stmt("print 1;"), Expr::Print { .. })); } #[test] fn parses_block_with_multiple_statements() { match single_stmt("do print 1; print 2; end") { Expr::Block { statements, .. } => assert_eq!(statements.len(), 2), other => panic!("expected block, got {other:?}"), } } #[test] fn parses_if_else() { match single_stmt("if true then print 1; else print 2;") { Expr::If { else_branch: Some(_), .. } => {} other => panic!("expected if/else, got {other:?}"), } } #[test] fn parses_while_loop() { assert!(matches!( single_stmt("while true do print 1; end"), Expr::While { .. } )); } #[test] fn parses_for_loop_with_assignment_increment() { match single_stmt("for i := 0; i <= 2; i = i + 1; do print i; end") { Expr::For { increment, .. } => { // The increment is an assignment expression. assert!(matches!(increment.node, Expr::Assign { .. })) } other => panic!("expected for loop, got {other:?}"), } } #[test] fn parses_function_declaration() { match single_stmt("add :: fn (a, b): Number do return a + b; end;") { Expr::VarDeclaration { name, initializer, .. } => { assert_eq!(name, "add"); match initializer { Some(init) => assert!(matches!( &init.node, Expr::Literal { value } if matches!(&**value, BaseValue::Function(_)) )), None => panic!("expected a function initializer"), } } other => panic!("expected function declaration, got {other:?}"), } } // --------------------------------------------------------------------------- // Whole-program / error handling // --------------------------------------------------------------------------- #[test] fn parses_a_multi_statement_program() { let stmts = parse_ok("x := 1; y := 2; print x + y;"); assert_eq!(stmts.len(), 3); } #[test] fn lexer_errors_surface_through_the_frontend() { // Unterminated string is a lexical error reported by the lexer stage. assert!(parse("\"unterminated;").is_err()); } #[test] fn missing_semicolon_is_a_parse_error() { assert!(parse("print 1").is_err()); } #[test] fn unclosed_grouping_is_a_parse_error() { assert!(parse("(1 + 2;").is_err()); }