299 lines
8.3 KiB
Rust
299 lines
8.3 KiB
Rust
//! 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, Stmt};
|
||
|
|
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<Vec<AstNode<Stmt>>, 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<AstNode<Stmt>> {
|
||
|
|
parse(src).expect("expected source to lex and parse")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Parse `src` and return the single statement it should produce.
|
||
|
|
fn single_stmt(src: &str) -> Stmt {
|
||
|
|
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 the expression of its single expression-statement.
|
||
|
|
fn single_expr(src: &str) -> Expr {
|
||
|
|
match single_stmt(src) {
|
||
|
|
Stmt::Expression { expression, .. } => expression.node,
|
||
|
|
other => panic!("expected expression statement, got {other:?}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 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;") {
|
||
|
|
Stmt::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;"), Stmt::Print { .. }));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_block_with_multiple_statements() {
|
||
|
|
match single_stmt("do print 1; print 2; end") {
|
||
|
|
Stmt::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;") {
|
||
|
|
Stmt::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"),
|
||
|
|
Stmt::While { .. }
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_for_loop_with_assignment_increment() {
|
||
|
|
match single_stmt("for i := 0; i <= 2; i = i + 1; do print i; end") {
|
||
|
|
Stmt::For { increment, .. } => match increment.node {
|
||
|
|
// The increment desugars to an expression statement holding an assignment.
|
||
|
|
Stmt::Expression { expression, .. } => {
|
||
|
|
assert!(matches!(expression.node, Expr::Assign { .. }))
|
||
|
|
}
|
||
|
|
other => panic!("expected expression-statement increment, got {other:?}"),
|
||
|
|
},
|
||
|
|
other => panic!("expected for loop, got {other:?}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn parses_function_declaration() {
|
||
|
|
match single_stmt("add :: fn (a, b) do return a + b; end") {
|
||
|
|
Stmt::VarDeclaration {
|
||
|
|
name, initializer, ..
|
||
|
|
} => {
|
||
|
|
assert_eq!(name, "add");
|
||
|
|
match initializer {
|
||
|
|
Some(init) => assert!(matches!(
|
||
|
|
init.node,
|
||
|
|
Expr::Literal {
|
||
|
|
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());
|
||
|
|
}
|