Files

297 lines
8.2 KiB
Rust
Raw Permalink Normal View History

//! 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`.
2026-07-06 10:43:17 +02:00
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`.
2026-07-06 10:43:17 +02:00
fn parse(src: &str) -> Result<Vec<AstNode>, 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.
2026-07-06 10:43:17 +02:00
fn parse_ok(src: &str) -> Vec<AstNode> {
parse(src).expect("expected source to lex and parse")
}
2026-07-06 10:43:17 +02:00
/// 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
}
2026-07-06 10:43:17 +02:00
/// 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 {
2026-07-06 10:43:17 +02:00
single_stmt(src)
}
// ---------------------------------------------------------------------------
// Expressions
// ---------------------------------------------------------------------------
#[test]
fn lexes_and_parses_number_literal() {
match single_expr("42;") {
2026-07-06 10:43:17 +02:00
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;") {
2026-07-06 10:43:17 +02:00
Expr::VarDeclaration {
name, initializer, ..
} => {
assert_eq!(name, "x");
assert!(initializer.is_some());
}
other => panic!("expected var declaration, got {other:?}"),
}
}
#[test]
fn parses_print_statement() {
2026-07-06 10:43:17 +02:00
assert!(matches!(single_stmt("print 1;"), Expr::Print { .. }));
}
#[test]
fn parses_block_with_multiple_statements() {
match single_stmt("do print 1; print 2; end") {
2026-07-06 10:43:17 +02:00
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;") {
2026-07-06 10:43:17 +02:00
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"),
2026-07-06 10:43:17 +02:00
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") {
2026-07-06 10:43:17 +02:00
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() {
2026-07-06 10:43:17 +02:00
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!(
2026-07-06 10:43:17 +02:00
&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());
}