From 29c86d278ddd8af39bfc763beb73d70445980e25 Mon Sep 17 00:00:00 2001 From: Giulio Agostini Date: Mon, 29 Jun 2026 20:47:59 +0200 Subject: [PATCH] Make environment generic and adapt closures Added unit test --- .gitignore | 0 Cargo.toml | 0 README.md | 0 examples/fail_error.lox | 0 examples/test_closure.lox | 15 +++ examples/test_example.lox | 0 examples/test_function_call.lox | 0 examples/test_source_tracking.lox | 0 src/backend/environment.rs | 124 ++++++++++++++++-- src/backend/interpreter.rs | 148 ++++++++++++++++++++- src/backend/mod.rs | 1 + src/backend/variable_resolution.rs | 204 +++++++++++++++++++++++++++++ src/common/ast.rs | 0 src/common/base_value.rs | 5 +- src/common/lox_result.rs | 6 +- src/common/mod.rs | 0 src/frontend/lexer.rs | 188 ++++++++++++++++++++++++++ src/frontend/mod.rs | 0 src/frontend/parser.rs | 159 +++++++++++++++++++++- src/frontend/source_registry.rs | 0 src/frontend/tokens.rs | 0 src/lib.rs | 0 src/logging/display_ast.rs | 0 src/logging/display_token.rs | 0 src/logging/mod.rs | 0 src/main.rs | 0 src/middleend/crawler.rs | 0 src/middleend/mod.rs | 0 tests/number_operations.rs | 0 tests/test_example.rs | 0 30 files changed, 820 insertions(+), 30 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 Cargo.toml mode change 100644 => 100755 README.md mode change 100644 => 100755 examples/fail_error.lox mode change 100644 => 100755 examples/test_closure.lox mode change 100644 => 100755 examples/test_example.lox mode change 100644 => 100755 examples/test_function_call.lox mode change 100644 => 100755 examples/test_source_tracking.lox mode change 100644 => 100755 src/backend/environment.rs mode change 100644 => 100755 src/backend/interpreter.rs mode change 100644 => 100755 src/backend/mod.rs create mode 100755 src/backend/variable_resolution.rs mode change 100644 => 100755 src/common/ast.rs mode change 100644 => 100755 src/common/base_value.rs mode change 100644 => 100755 src/common/lox_result.rs mode change 100644 => 100755 src/common/mod.rs mode change 100644 => 100755 src/frontend/lexer.rs mode change 100644 => 100755 src/frontend/mod.rs mode change 100644 => 100755 src/frontend/parser.rs mode change 100644 => 100755 src/frontend/source_registry.rs mode change 100644 => 100755 src/frontend/tokens.rs mode change 100644 => 100755 src/lib.rs mode change 100644 => 100755 src/logging/display_ast.rs mode change 100644 => 100755 src/logging/display_token.rs mode change 100644 => 100755 src/logging/mod.rs mode change 100644 => 100755 src/main.rs mode change 100644 => 100755 src/middleend/crawler.rs mode change 100644 => 100755 src/middleend/mod.rs mode change 100644 => 100755 tests/number_operations.rs mode change 100644 => 100755 tests/test_example.rs diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/examples/fail_error.lox b/examples/fail_error.lox old mode 100644 new mode 100755 diff --git a/examples/test_closure.lox b/examples/test_closure.lox old mode 100644 new mode 100755 index 2e288c0..719f093 --- a/examples/test_closure.lox +++ b/examples/test_closure.lox @@ -10,3 +10,18 @@ end internal := closure(); internal(); + +a := "Global"; +b := a; +c := b; +c = b = a; +d := c = b = a; +do + showA :: fn() do + print a; + end + + showA(); + a := "Local"; + showA(); +end diff --git a/examples/test_example.lox b/examples/test_example.lox old mode 100644 new mode 100755 diff --git a/examples/test_function_call.lox b/examples/test_function_call.lox old mode 100644 new mode 100755 diff --git a/examples/test_source_tracking.lox b/examples/test_source_tracking.lox old mode 100644 new mode 100755 diff --git a/src/backend/environment.rs b/src/backend/environment.rs old mode 100644 new mode 100755 index 97166ec..640a45d --- a/src/backend/environment.rs +++ b/src/backend/environment.rs @@ -1,34 +1,45 @@ -use std::collections::HashMap; - use crate::{ - common::{ - base_value::BaseValue, - lox_result::{runtime_error, LoxResult}, - }, + common::lox_result::{runtime_error, LoxResult}, frontend::source_registry::SourceSlice, }; +use std::collections::HashMap; +use std::fmt::Debug; -#[derive(Debug, Clone, PartialEq)] -pub struct EnvironmentStack { - stack: Vec>, +pub type Environment = HashMap; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct EnvironmentStack { + stack: Vec>, } -impl EnvironmentStack { +impl EnvironmentStack { pub fn new() -> Self { EnvironmentStack { stack: vec![HashMap::new()], } } + pub fn is_empty(&self) -> bool { + self.stack.is_empty() + } + pub fn push_new_scope(&mut self) { self.stack.push(HashMap::new()); } + pub fn push_existing_scope(&mut self, scope: HashMap) { + self.stack.push(scope); + } + pub fn pop_scope(&mut self) { self.stack.pop(); } - pub fn get(&self, name: &str) -> LoxResult { + pub fn clone_last_scope(&mut self) -> HashMap { + self.stack.last().unwrap().clone() + } + + pub fn get(&self, name: &str) -> LoxResult { let size = self.stack.len(); for i in (0..size).rev() { @@ -42,12 +53,12 @@ impl EnvironmentStack { ) } - pub fn declare(&mut self, name: String, value: BaseValue) -> LoxResult { + pub fn declare(&mut self, name: String, value: T) -> LoxResult { self.stack.last_mut().unwrap().insert(name, value.clone()); Ok(value) } - pub fn set(&mut self, name: String, value: BaseValue) -> LoxResult { + pub fn set(&mut self, name: String, value: T) -> LoxResult { let size = self.stack.len(); for i in (0..size).rev() { @@ -69,3 +80,90 @@ impl EnvironmentStack { ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn declare_and_get() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.declare("a".to_string(), 1).unwrap(); + assert_eq!(env.get("a").unwrap(), 1); + } + + #[test] + fn get_undefined_is_error() { + let env: EnvironmentStack = EnvironmentStack::new(); + assert!(env.get("nope").is_err()); + } + + #[test] + fn declare_overwrites_in_same_scope() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.declare("a".to_string(), 1).unwrap(); + env.declare("a".to_string(), 2).unwrap(); + assert_eq!(env.get("a").unwrap(), 2); + } + + #[test] + fn inner_scope_shadows_outer_and_unshadows_on_pop() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.declare("a".to_string(), 1).unwrap(); + env.push_new_scope(); + env.declare("a".to_string(), 2).unwrap(); + assert_eq!(env.get("a").unwrap(), 2); + env.pop_scope(); + assert_eq!(env.get("a").unwrap(), 1); + } + + #[test] + fn get_falls_through_to_outer_scope() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.declare("a".to_string(), 1).unwrap(); + env.push_new_scope(); + assert_eq!(env.get("a").unwrap(), 1); + } + + #[test] + fn set_updates_existing_value_in_outer_scope() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.declare("a".to_string(), 1).unwrap(); + env.push_new_scope(); + env.set("a".to_string(), 9).unwrap(); + env.pop_scope(); + assert_eq!(env.get("a").unwrap(), 9); + } + + #[test] + fn set_undefined_is_error() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + assert!(env.set("a".to_string(), 1).is_err()); + } + + #[test] + fn pop_scope_discards_inner_declarations() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.push_new_scope(); + env.declare("temp".to_string(), 5).unwrap(); + env.pop_scope(); + assert!(env.get("temp").is_err()); + } + + #[test] + fn push_existing_scope_makes_values_visible() { + let mut scope = HashMap::new(); + scope.insert("k".to_string(), 7); + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.push_existing_scope(scope); + assert_eq!(env.get("k").unwrap(), 7); + } + + #[test] + fn clone_last_scope_returns_top_scope() { + let mut env: EnvironmentStack = EnvironmentStack::new(); + env.declare("a".to_string(), 1).unwrap(); + let scope = env.clone_last_scope(); + assert_eq!(scope.get("a"), Some(&1)); + } +} diff --git a/src/backend/interpreter.rs b/src/backend/interpreter.rs old mode 100644 new mode 100755 index 360d8ec..74b39de --- a/src/backend/interpreter.rs +++ b/src/backend/interpreter.rs @@ -10,14 +10,14 @@ use crate::{ use std::fmt::{Debug, Display}; pub struct Interpreter { - enviorment: EnvironmentStack, + enviorment: EnvironmentStack, } pub trait EvaluateInterpreter { fn evaluate(&mut self, stmt: T) -> LoxResult; } -impl<'a, R: AstNodeKind + Clone + Debug + Display> EvaluateInterpreter> for Interpreter +impl EvaluateInterpreter> for Interpreter where Interpreter: EvaluateInterpreter, { @@ -36,8 +36,6 @@ impl EvaluateInterpreter for Interpreter { Expr::Literal { value } => match value { BaseValue::Function(mut func) => { func.closure = Some(self.enviorment.clone()); - println!("Closure created"); - println!("current enviorment: {:?}", self.enviorment); Ok(BaseValue::Function(func)) } _ => Ok(value), @@ -128,9 +126,7 @@ impl Interpreter { match function { BaseValue::NativeFunction(func) => Ok((func.function)(&evaluated_arguments)), BaseValue::Function(func) => { - // Save the current environment let saved_env = self.enviorment.clone(); - // If the function captured a closure, use it as the base environment if let Some(closure_env) = func.closure { self.enviorment = closure_env; @@ -348,3 +344,143 @@ impl Interpreter { result } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::frontend::lexer::Lexer; + use crate::frontend::parser::Parser; + + /// Lex, parse and interpret `src`, returning the value of the last statement. + fn eval(src: &str) -> LoxResult { + let tokens = Lexer::new(src.to_string(), 0) + .scans_tokens() + .expect("source should lex without errors"); + let statements = Parser::new(tokens) + .parse() + .expect("source should parse without errors"); + let mut interpreter = Interpreter::new(); + let mut last = BaseValue::Nil; + for stmt in statements { + last = interpreter.evaluate(stmt)?; + } + Ok(last) + } + + #[test] + fn evaluates_arithmetic_with_precedence() { + assert_eq!(eval("1 + 2 * 3;").unwrap().to_string(), "7"); + } + + #[test] + fn grouping_changes_precedence() { + assert_eq!(eval("(1 + 2) * 3;").unwrap().to_string(), "9"); + } + + #[test] + fn division_and_subtraction() { + assert_eq!(eval("10 - 4 / 2;").unwrap().to_string(), "8"); + } + + #[test] + fn comparison_yields_boolean() { + assert_eq!(eval("1 < 2;").unwrap(), BaseValue::Boolean(true)); + assert_eq!(eval("2 < 1;").unwrap(), BaseValue::Boolean(false)); + } + + #[test] + fn equality_operators() { + assert_eq!(eval("1 == 1;").unwrap(), BaseValue::Boolean(true)); + assert_eq!(eval("1 != 2;").unwrap(), BaseValue::Boolean(true)); + } + + #[test] + fn unary_negation() { + assert_eq!(eval("-5;").unwrap().to_string(), "-5"); + } + + #[test] + fn logical_not() { + assert_eq!(eval("!true;").unwrap(), BaseValue::Boolean(false)); + } + + #[test] + fn concatenates_strings() { + match eval("\"a\" + \"b\";").unwrap() { + BaseValue::String(s) => assert!(s.contains('a') && s.contains('b')), + other => panic!("expected string, got {:?}", other), + } + } + + #[test] + fn declares_and_reads_variable() { + assert_eq!(eval("var x: Int = 10; x + 5;").unwrap().to_string(), "15"); + } + + #[test] + fn assignment_updates_variable() { + assert_eq!( + eval("var x: Int = 1; x = 42; x;").unwrap().to_string(), + "42" + ); + } + + #[test] + fn if_takes_then_branch_when_true() { + assert_eq!( + eval("var x: Int = 0; if true then x = 1; x;") + .unwrap() + .to_string(), + "1" + ); + } + + #[test] + fn if_takes_else_branch_when_false() { + assert_eq!( + eval("var x: Int = 0; if false then x = 1; else x = 2; x;") + .unwrap() + .to_string(), + "2" + ); + } + + #[test] + fn while_loop_runs_until_condition_false() { + let src = "var i: Int = 0; while i < 3 do i = i + 1; end i;"; + assert_eq!(eval(src).unwrap().to_string(), "3"); + } + + #[test] + fn block_scope_does_not_leak_outer_assignment() { + // `set` walks outer scopes, so the outer `x` is updated from inside the block. + let src = "var x: Int = 1; do x = 5; end x;"; + assert_eq!(eval(src).unwrap().to_string(), "5"); + } + + #[test] + fn defines_and_calls_a_function() { + let src = "add :: fn (a, b) do return a + b; end add(2, 3);"; + assert_eq!(eval(src).unwrap().to_string(), "5"); + } + + #[test] + fn clock_native_function_returns_a_number() { + assert!(matches!(eval("clock();").unwrap(), BaseValue::Number(..))); + } + + #[test] + fn undefined_variable_is_runtime_error() { + assert!(eval("missing;").is_err()); + } + + #[test] + fn calling_a_non_function_is_runtime_error() { + assert!(eval("var x: Int = 5; x();").is_err()); + } + + #[test] + fn adding_incompatible_types_is_runtime_error() { + assert!(eval("1 + true;").is_err()); + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs old mode 100644 new mode 100755 index ad68c27..a6f3b79 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,2 +1,3 @@ pub mod environment; pub mod interpreter; +pub mod variable_resolution; diff --git a/src/backend/variable_resolution.rs b/src/backend/variable_resolution.rs new file mode 100755 index 0000000..1c9d515 --- /dev/null +++ b/src/backend/variable_resolution.rs @@ -0,0 +1,204 @@ +use crate::{ + backend::environment::EnvironmentStack, + common::{ + ast::{AstNode, AstNodeKind, Expr, Stmt}, + lox_result::{runtime_error, LoxError, LoxResult}, + }, + frontend::source_registry::SourceSlice, +}; + +use std::fmt::{Debug, Display}; +struct Resolver { + scopes: EnvironmentStack, +} + +impl Resolver { + pub fn new() -> Self { + Resolver { + scopes: EnvironmentStack::new(), + } + } + + fn declare(&mut self, name: &String) { + if self.scopes.is_empty() { + return; + } + let _ = self.scopes.set(name.clone(), false); + } + + fn define(&mut self, name: &String) { + if self.scopes.is_empty() { + return; + } + let _ = self.scopes.set(name.clone(), true); + } +} + +pub trait StaticAnalyzer { + fn resolve(&mut self, node: &T) -> LoxResult; +} + +impl StaticAnalyzer> for Resolver +where + Resolver: StaticAnalyzer, +{ + fn resolve(&mut self, node: &AstNode) -> LoxResult { + self.resolve(&node.node) + } +} + +impl StaticAnalyzer for Resolver { + fn resolve(&mut self, node: &Stmt) -> LoxResult { + match node { + Stmt::Expression { expression, .. } => { + // Visit the expression + self.resolve(expression.as_ref()) + } + Stmt::Print { expression, .. } => { + // Visit the expression + self.resolve(expression.as_ref()) + } + Stmt::VarDeclaration { + initializer, name, .. + } => { + self.declare(name); + // Visit the initializer if present + if let Some(init) = initializer { + self.resolve(init.as_ref())?; + } + self.define(name); + Ok(true) + } + Stmt::VarAssigment { value, name, .. } => { + match self.scopes.get(name) { + Ok(true) => (), + Ok(false) => { + return Err(LoxError::RuntimeError { + source_slice: SourceSlice::default(), + message: "Cant read loac variable in it own lintilizer".to_string(), + }) + } + Err(err) => return Err(err), + } + self.resolve(value.as_ref()) + } + Stmt::Return { expression, .. } => { + // Visit the return expression + self.resolve(expression.as_ref()) + } + Stmt::Block { statements, .. } => { + self.scopes.push_new_scope(); + // Visit all statements in the block + for stmt in statements.iter() { + self.resolve(stmt)?; + } + self.scopes.pop_scope(); + Ok(true) + } + Stmt::If { + condition, + then_branch, + elif_branch, + else_branch, + .. + } => { + // Visit the condition + self.resolve(condition.as_ref())?; + + // Visit the then branch + self.resolve(then_branch.as_ref())?; + + // Visit all elif branches + for (elif_condition, elif_stmt) in elif_branch.iter() { + self.resolve(elif_condition.as_ref())?; + self.resolve(elif_stmt.as_ref())?; + } + + // Visit the else branch if present + if let Some(else_stmt) = else_branch { + self.resolve(else_stmt.as_ref())?; + } + + Ok(true) + } + Stmt::While { + condition, body, .. + } => { + // Visit the condition + self.resolve(condition.as_ref())?; + + // Visit the body + self.resolve(body.as_ref())?; + Ok(true) + } + Stmt::For { + variable, + condition, + increment, + body, + .. + } => { + // Visit the variable initialization + self.resolve(variable.as_ref())?; + + // Visit the condition + self.resolve(condition.as_ref())?; + + // Visit the increment + self.resolve(increment.as_ref())?; + + // Visit the body + self.resolve(body.as_ref())?; + Ok(true) + } + } + } +} + +impl StaticAnalyzer for Resolver { + fn resolve(&mut self, node: &Expr) -> LoxResult { + match node { + Expr::Literal { .. } => { + // Leaf node - no children to visit + Ok(true) + } + Expr::Binary { left, right, .. } => { + // Visit left operand + self.resolve(left.as_ref())?; + + // Visit right operand + self.resolve(right.as_ref()) + } + Expr::Unary { operand, .. } => { + // Visit the operand + self.resolve(operand.as_ref()) + } + Expr::Grouping { expression } => { + // Visit the grouped expression + self.resolve(expression.as_ref()) + } + Expr::Identifier { name, .. } => { + if !self.scopes.is_empty() && self.scopes.get(name).is_ok() { + return Err(LoxError::ParseError { + source_slice: SourceSlice::default(), + message: "Cant read local varialbe in it own initializer".to_string(), + }); + } + Ok(true) + } + Expr::Call { + callee, arguments, .. + } => { + // Visit the callee + self.resolve(callee.as_ref())?; + + // Visit all arguments + for arg in arguments.iter() { + self.resolve(arg)?; + } + + Ok(true) + } + } + } +} diff --git a/src/common/ast.rs b/src/common/ast.rs old mode 100644 new mode 100755 diff --git a/src/common/base_value.rs b/src/common/base_value.rs old mode 100644 new mode 100755 index 6eb2976..1929a4d --- a/src/common/base_value.rs +++ b/src/common/base_value.rs @@ -2,6 +2,7 @@ use core::fmt; use std::fmt::Display; use std::ops::{Add, Div, Mul, Not, Rem, Sub}; +use crate::backend::environment::Environment; use crate::common::lox_result::{runtime_error, LoxResult}; use crate::{ backend::environment::EnvironmentStack, @@ -312,7 +313,7 @@ pub struct LoxFunction { pub parameters: Vec<(String, String)>, pub return_type: Option, pub body: AstNode, - pub closure: Option, + pub closure: Option>, pub guard: Option>, } @@ -320,7 +321,7 @@ impl LoxFunction { pub fn new( parameters: Vec<(String, String)>, body: AstNode, - closure: Option, + closure: Option>, guard: Option>, ) -> Self { LoxFunction { diff --git a/src/common/lox_result.rs b/src/common/lox_result.rs old mode 100644 new mode 100755 index dce78e0..63327b4 --- a/src/common/lox_result.rs +++ b/src/common/lox_result.rs @@ -106,11 +106,7 @@ impl LoxError { LoxError::ParseError { source_slice, .. } => Some(source_slice.clone()), LoxError::IoError { .. } => None, LoxError::TypeMismatch { source_slice, .. } => Some(source_slice.clone()), - LoxError::Return { - value, - return_label, - .. - } => None, + LoxError::Return { .. } => None, } } diff --git a/src/common/mod.rs b/src/common/mod.rs old mode 100644 new mode 100755 diff --git a/src/frontend/lexer.rs b/src/frontend/lexer.rs old mode 100644 new mode 100755 index 6751c21..5befaad --- a/src/frontend/lexer.rs +++ b/src/frontend/lexer.rs @@ -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 { + 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 { + 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()); + } +} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs old mode 100644 new mode 100755 diff --git a/src/frontend/parser.rs b/src/frontend/parser.rs old mode 100644 new mode 100755 index 29e8dad..1cc73be --- a/src/frontend/parser.rs +++ b/src/frontend/parser.rs @@ -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>> { + 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> { + parse_source(src).expect("expected source to parse") + } + + /// Extract the inner expression of an expression statement. + fn expression_of(stmt: &AstNode) -> &AstNode { + 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()); + } +} diff --git a/src/frontend/source_registry.rs b/src/frontend/source_registry.rs old mode 100644 new mode 100755 diff --git a/src/frontend/tokens.rs b/src/frontend/tokens.rs old mode 100644 new mode 100755 diff --git a/src/lib.rs b/src/lib.rs old mode 100644 new mode 100755 diff --git a/src/logging/display_ast.rs b/src/logging/display_ast.rs old mode 100644 new mode 100755 diff --git a/src/logging/display_token.rs b/src/logging/display_token.rs old mode 100644 new mode 100755 diff --git a/src/logging/mod.rs b/src/logging/mod.rs old mode 100644 new mode 100755 diff --git a/src/main.rs b/src/main.rs old mode 100644 new mode 100755 diff --git a/src/middleend/crawler.rs b/src/middleend/crawler.rs old mode 100644 new mode 100755 diff --git a/src/middleend/mod.rs b/src/middleend/mod.rs old mode 100644 new mode 100755 diff --git a/tests/number_operations.rs b/tests/number_operations.rs old mode 100644 new mode 100755 diff --git a/tests/test_example.rs b/tests/test_example.rs old mode 100644 new mode 100755