Make environment generic and adapt closures
Added unit test
This commit is contained in:
Regular → Executable
+188
@@ -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
Regular → Executable
+155
-4
@@ -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
Regular → Executable
Reference in New Issue
Block a user