Make environment generic and adapt closures

Added unit test
This commit is contained in:
Giulio Agostini
2026-06-29 20:50:29 +02:00
parent 4e2645e12d
commit 29c86d278d
30 changed files with 820 additions and 30 deletions
Regular → Executable
+188
View File
@@ -322,3 +322,191 @@ impl Lexer {
}
}
}
#[cfg(test)]
mod tests {
use std::f64;
use super::*;
/// Lex `input` and return the resulting tokens, panicking on lexical errors.
fn lex(input: &str) -> Vec<Token> {
Lexer::new(input.to_string(), 0)
.scans_tokens()
.expect("expected input to lex without errors")
}
/// Lex `input` and collect just the token types (including the trailing EOF).
fn token_types(input: &str) -> Vec<TokenType> {
lex(input).into_iter().map(|t| t.token_type).collect()
}
#[test]
fn scans_single_character_tokens() {
assert_eq!(
token_types("(){}[],.-+;:*%"),
vec![
TokenType::LeftParen,
TokenType::RightParen,
TokenType::LeftBrace,
TokenType::RightBrace,
TokenType::LeftBracket,
TokenType::RightBracket,
TokenType::Comma,
TokenType::Dot,
TokenType::Minus,
TokenType::Plus,
TokenType::Semicolon,
TokenType::Colon,
TokenType::Star,
TokenType::Percent,
TokenType::Eof,
]
);
}
#[test]
fn scans_one_and_two_char_operators() {
assert_eq!(
token_types("! != = == < <= > >= /"),
vec![
TokenType::Bang,
TokenType::BangEqual,
TokenType::Equal,
TokenType::EqualEqual,
TokenType::Less,
TokenType::LessEqual,
TokenType::Greater,
TokenType::GreaterEqual,
TokenType::Slash,
TokenType::Eof,
]
);
}
#[test]
fn keyword_lookup_matches_known_words() {
assert_eq!(get_keyword_token("and"), Some(TokenType::And));
assert_eq!(get_keyword_token("if"), Some(TokenType::If));
assert_eq!(get_keyword_token("then"), Some(TokenType::Then));
assert_eq!(get_keyword_token("while"), Some(TokenType::While));
assert_eq!(get_keyword_token("do"), Some(TokenType::StartBlock));
assert_eq!(get_keyword_token("end"), Some(TokenType::EndBlock));
assert_eq!(get_keyword_token("not_a_keyword"), None);
}
#[test]
fn scans_keywords_in_a_stream() {
assert_eq!(
token_types("if then else while print return"),
vec![
TokenType::If,
TokenType::Then,
TokenType::Else,
TokenType::While,
TokenType::Print,
TokenType::Return,
TokenType::Eof,
]
);
}
#[test]
fn scans_integer_number() {
let tokens = lex("42");
assert_eq!(tokens[0].token_type, TokenType::Number);
assert_eq!(tokens[0].literal, Some(BaseValue::Number(Number::I32(42))));
}
#[test]
fn scans_float_number() {
let tokens = lex("3.141592653589793");
assert_eq!(
tokens[0].literal,
Some(BaseValue::Number(Number::F64(f64::consts::PI)))
);
}
#[test]
fn scans_number_with_float_suffix() {
let tokens = lex("5f");
assert_eq!(tokens[0].literal, Some(BaseValue::Number(Number::F64(5.0))));
}
#[test]
fn scans_number_with_unsigned_suffix() {
let tokens = lex("7u");
assert_eq!(tokens[0].literal, Some(BaseValue::Number(Number::U128(7))));
}
#[test]
fn scans_string_literal_including_quotes() {
// The lexer slices from the opening quote through the closing quote,
// so the literal currently retains the surrounding quotes.
let tokens = lex("\"hello\"");
assert_eq!(tokens[0].token_type, TokenType::String);
assert_eq!(
tokens[0].literal,
Some(BaseValue::String("\"hello\"".to_string()))
);
}
#[test]
fn scans_identifier() {
let tokens = lex("foo_bar");
assert_eq!(tokens[0].token_type, TokenType::Identifier);
assert_eq!(
tokens[0].literal,
Some(BaseValue::String("foo_bar".to_string()))
);
}
#[test]
fn scans_boolean_and_nil_literals() {
let tokens = lex("true false Nil");
assert_eq!(tokens[0].literal, Some(BaseValue::Boolean(true)));
assert_eq!(tokens[1].literal, Some(BaseValue::Boolean(false)));
assert_eq!(tokens[2].literal, Some(BaseValue::Nil));
}
#[test]
fn ignores_line_comments() {
assert_eq!(
token_types("1 // a comment\n2"),
vec![TokenType::Number, TokenType::Number, TokenType::Eof]
);
}
#[test]
fn ignores_block_comments() {
assert_eq!(
token_types("1 /* multi\nline */ 2"),
vec![TokenType::Number, TokenType::Number, TokenType::Eof]
);
}
#[test]
fn always_appends_eof_even_for_empty_input() {
let tokens = lex("");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Eof);
}
#[test]
fn unterminated_string_is_a_lexical_error() {
let result = Lexer::new("\"oops".to_string(), 0).scans_tokens();
assert!(matches!(result, Err(LoxError::LexicalError { .. })));
}
#[test]
fn unterminated_block_comment_is_a_lexical_error() {
let result = Lexer::new("/* never ends".to_string(), 0).scans_tokens();
assert!(matches!(result, Err(LoxError::LexicalError { .. })));
}
#[test]
fn unexpected_character_is_a_lexical_error() {
let result = Lexer::new("@".to_string(), 0).scans_tokens();
assert!(result.is_err());
}
}
Regular → Executable
View File
Regular → Executable
+155 -4
View File
@@ -205,6 +205,10 @@ impl Parser {
let mut _type_annotation = BaseValue::Nil;
self.consume(TokenType::Colon, "Expect column")?;
if self.peek().token_type == TokenType::Identifier {
// TODO manage type annotation
self.consume(TokenType::Identifier, "Expect type name.")?;
}
match self.peek().token_type {
TokenType::Equal | TokenType::Identifier => {
@@ -374,11 +378,7 @@ impl Parser {
while self.peek().token_type != TokenType::EndBlock && !self.is_at_end() {
let stmt = self.statement(label.clone())?;
let should_break = matches!(stmt.node, Stmt::Expression { .. });
statements.push(stmt);
if should_break {
break;
}
}
let end_token = self.consume(TokenType::EndBlock, "Expect 'end' after block.")?;
let end_slice = end_token.source_slice.clone();
@@ -864,3 +864,154 @@ impl Parser {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::base_value::Number;
use crate::frontend::lexer::Lexer;
/// Lex and parse `src`, returning the parser's result.
fn parse_source(src: &str) -> LoxResult<Vec<AstNode<Stmt>>> {
let tokens = Lexer::new(src.to_string(), 0)
.scans_tokens()
.expect("source should lex without errors");
Parser::new(tokens).parse()
}
/// Parse `src`, panicking if parsing fails.
fn parse_ok(src: &str) -> Vec<AstNode<Stmt>> {
parse_source(src).expect("expected source to parse")
}
/// Extract the inner expression of an expression statement.
fn expression_of(stmt: &AstNode<Stmt>) -> &AstNode<Expr> {
match &stmt.node {
Stmt::Expression { expression, .. } => expression,
other => panic!("expected expression statement, got {:?}", other),
}
}
#[test]
fn parses_number_literal_expression() {
let stmts = parse_ok("42;");
assert_eq!(stmts.len(), 1);
match &expression_of(&stmts[0]).node {
Expr::Literal { value } => {
assert_eq!(*value, BaseValue::Number(Number::I32(42)));
}
other => panic!("expected literal, got {:?}", other),
}
}
#[test]
fn respects_multiplication_precedence_over_addition() {
// 1 + 2 * 3 should parse as 1 + (2 * 3)
let stmts = parse_ok("1 + 2 * 3;");
match &expression_of(&stmts[0]).node {
Expr::Binary {
operator, right, ..
} => {
assert_eq!(*operator, TokenType::Plus);
match &right.node {
Expr::Binary { operator, .. } => {
assert_eq!(*operator, TokenType::Star)
}
other => panic!("expected nested binary, got {:?}", other),
}
}
other => panic!("expected binary expression, got {:?}", other),
}
}
#[test]
fn parses_unary_negation() {
let stmts = parse_ok("-5;");
match &expression_of(&stmts[0]).node {
Expr::Unary { operator, .. } => assert_eq!(*operator, TokenType::Minus),
other => panic!("expected unary expression, got {:?}", other),
}
}
#[test]
fn parses_grouping_to_change_precedence() {
// (1 + 2) * 3 should have a grouping on the left of the multiply.
let stmts = parse_ok("(1 + 2) * 3;");
match &expression_of(&stmts[0]).node {
Expr::Binary { operator, left, .. } => {
assert_eq!(*operator, TokenType::Star);
assert!(matches!(left.node, Expr::Grouping { .. }));
}
other => panic!("expected binary expression, got {:?}", other),
}
}
#[test]
fn parses_comparison_expression() {
let stmts = parse_ok("1 < 2;");
match &expression_of(&stmts[0]).node {
Expr::Binary { operator, .. } => assert_eq!(*operator, TokenType::Less),
other => panic!("expected binary expression, got {:?}", other),
}
}
#[test]
fn parses_print_statement() {
let stmts = parse_ok("print 1;");
assert!(matches!(stmts[0].node, Stmt::Print { .. }));
}
#[test]
fn parses_var_declaration_with_initializer() {
let stmts = parse_ok("var x: Int = 5;");
match &stmts[0].node {
Stmt::VarDeclaration {
name, initializer, ..
} => {
assert_eq!(name, "x");
assert!(initializer.is_some());
}
other => panic!("expected var declaration, got {:?}", other),
}
}
#[test]
fn parses_assignment_statement() {
let stmts = parse_ok("x = 5;");
match &stmts[0].node {
Stmt::VarAssigment { name, .. } => assert_eq!(name, "x"),
other => panic!("expected assignment statement, got {:?}", other),
}
}
#[test]
fn parses_block_statement() {
let stmts = parse_ok("do print 1; print 2; end");
match &stmts[0].node {
Stmt::Block { statements, .. } => assert_eq!(statements.len(), 2),
other => panic!("expected block statement, got {:?}", other),
}
}
#[test]
fn parses_if_statement() {
let stmts = parse_ok("if true then print 1;");
assert!(matches!(stmts[0].node, Stmt::If { .. }));
}
#[test]
fn parses_while_statement() {
let stmts = parse_ok("while true do print 1; end");
assert!(matches!(stmts[0].node, Stmt::While { .. }));
}
#[test]
fn errors_on_missing_semicolon_after_print() {
assert!(parse_source("print 1").is_err());
}
#[test]
fn errors_on_unclosed_grouping() {
assert!(parse_source("(1 + 2;").is_err());
}
}
Regular → Executable
View File
Regular → Executable
View File