2026-06-30 14:05:46 +02:00
|
|
|
//! 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};
|
2026-06-30 14:05:46 +02:00
|
|
|
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> {
|
2026-06-30 14:05:46 +02:00
|
|
|
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> {
|
2026-06-30 14:05:46 +02:00
|
|
|
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 {
|
2026-06-30 14:05:46 +02:00
|
|
|
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.
|
2026-06-30 14:05:46 +02:00
|
|
|
fn single_expr(src: &str) -> Expr {
|
2026-07-06 10:43:17 +02:00
|
|
|
single_stmt(src)
|
2026-06-30 14:05:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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))),
|
2026-06-30 14:05:46 +02:00
|
|
|
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 {
|
2026-06-30 14:05:46 +02:00
|
|
|
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 { .. }));
|
2026-06-30 14:05:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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),
|
2026-06-30 14:05:46 +02:00
|
|
|
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 {
|
2026-06-30 14:05:46 +02:00
|
|
|
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 { .. }
|
2026-06-30 14:05:46 +02:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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 { .. }))
|
|
|
|
|
}
|
2026-06-30 14:05:46 +02:00
|
|
|
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 {
|
2026-06-30 14:05:46 +02:00
|
|
|
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(_))
|
2026-06-30 14:05:46 +02:00
|
|
|
)),
|
|
|
|
|
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());
|
|
|
|
|
}
|