diff --git a/src/backend/environment.rs b/src/backend/environment.rs index b161b57..f6ed87b 100755 --- a/src/backend/environment.rs +++ b/src/backend/environment.rs @@ -48,7 +48,7 @@ impl EnvironmentStack { } } runtime_error( - SourceSlice::default(), // todo change this to the actual source slice + SourceSlice::synthetic(), // todo change this to the actual source slice format!("Undefined variable '{}'", name), ) } @@ -75,7 +75,7 @@ impl EnvironmentStack { } } runtime_error( - SourceSlice::default(), // todo change this to the actual source slice + SourceSlice::synthetic(), // todo change this to the actual source slice format!("Undefined variable '{}'", name), ) } diff --git a/src/backend/interpreter.rs b/src/backend/interpreter.rs index 74b39de..bc6bb1a 100755 --- a/src/backend/interpreter.rs +++ b/src/backend/interpreter.rs @@ -56,6 +56,10 @@ impl EvaluateInterpreter for Interpreter { } Expr::Grouping { expression } => self.evaluate(*expression), Expr::Call { callee, arguments } => self.evaluate_call(callee, arguments), + Expr::Assign { name, value } => { + let value = self.evaluate(*value)?; + self.enviorment.set(name, value) + } } } } @@ -69,9 +73,7 @@ impl EvaluateInterpreter> for Interpreter { Err(LoxError::RuntimeError { message, source_slice, - }) if source_slice == SourceSlice::default() => { - runtime_error(node.source_slice.clone(), message) - } + }) if source_slice.is_synthetic() => runtime_error(node.source_slice.clone(), message), Err(err) => Err(err), } } @@ -180,7 +182,7 @@ impl Interpreter { TokenType::And => Ok(BaseValue::Boolean(left.is_truthy() && right.is_truthy())), TokenType::Or => Ok(BaseValue::Boolean(left.is_truthy() || right.is_truthy())), _ => Err(LoxError::RuntimeError { - source_slice: SourceSlice::default(), + source_slice: SourceSlice::synthetic(), message: format!("Unsupported binary operator: {:?}", operator), }), } @@ -191,13 +193,13 @@ impl Interpreter { TokenType::Minus => match operand { BaseValue::Number(n) => Ok(BaseValue::Number(n.neg())), _ => Err(LoxError::RuntimeError { - source_slice: SourceSlice::default(), + source_slice: SourceSlice::synthetic(), message: "Cannot negate non-numeric value".to_string(), }), }, TokenType::Bang => Ok(!operand), _ => Err(LoxError::RuntimeError { - source_slice: SourceSlice::default(), + source_slice: SourceSlice::synthetic(), message: format!("Unsupported unary operator: {:?}", operator), }), } @@ -222,10 +224,7 @@ impl Interpreter { }; self.enviorment.declare(name.clone(), value) } - Stmt::VarAssigment { name, value, .. } => { - let result = self.evaluate(*value)?; - self.enviorment.set(name.clone(), result) - } + Stmt::If { condition, then_branch, @@ -298,7 +297,7 @@ impl Interpreter { BaseValue::Boolean(false) => continue, _ => { return Err(LoxError::TypeMismatch { - source_slice: SourceSlice::default(), // todo change this to the actual source slice + source_slice: SourceSlice::synthetic(), // todo change this to the actual source slice expected: "boolean".to_string(), found: condition.to_string(), }); @@ -312,7 +311,7 @@ impl Interpreter { } } _ => Err(LoxError::TypeMismatch { - source_slice: SourceSlice::default(), // todo change this to the actual source slice + source_slice: SourceSlice::synthetic(), // todo change this to the actual source slice expected: "boolean".to_string(), found: condition.to_string(), }), diff --git a/src/common/ast.rs b/src/common/ast.rs index a796a28..48c6381 100755 --- a/src/common/ast.rs +++ b/src/common/ast.rs @@ -3,6 +3,25 @@ use crate::{ frontend::{source_registry::SourceSlice, tokens::TokenType}, }; use std::fmt::{Debug, Display}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// A unique identity for an AST node, assigned once at construction. +/// +/// Unlike a [`SourceSlice`] (which describes *where* a node is, for +/// diagnostics), a `NodeId` describes *which* node it is. It is stable across +/// clones, so analyses such as the resolver can key per-reference data on it +/// without relying on source positions being unique. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeId(pub usize); + +static NEXT_NODE_ID: AtomicUsize = AtomicUsize::new(0); + +impl NodeId { + /// Allocate the next globally-unique node id. + pub fn next() -> Self { + NodeId(NEXT_NODE_ID.fetch_add(1, Ordering::Relaxed)) + } +} /* * grammar: * program -> statement* EOF @@ -10,7 +29,6 @@ use std::fmt::{Debug, Display}; * | print_statement * | var_statement * | block_statement - * | assignment_statement * | if_statement * * expression_statement -> expression ";" @@ -18,7 +36,6 @@ use std::fmt::{Debug, Display}; * var_statement -> ("var"|"dyn"|"mut")? IDENTIFIER (":" IDENTIFIER)? (("=" expression)? ";")| function_declaration * function_declaration -> "::" "(" parameters? ")" ("->" IDENTIFIER)? block_statement * parameters -> ( IDENTIFIER (":" IDENTIFIER)? ("," IDENTIFIER (":" IDENTIFIER)? )* ) - * assignment_statement -> IDENTIFIER "=" expression ";" * block_statement -> "do" statement* "end" * if_statement -> "if" expression "then" statement ("elif" expression "then" statement)* ("else" statement)? "end" * while_statement -> "while" expression "do" statement "end" @@ -61,6 +78,10 @@ pub enum Expr { callee: Box>, arguments: Vec>, }, + Assign { + name: String, + value: Box>, + }, } // Implementazione Display per Expr (per debugging) @@ -86,6 +107,7 @@ impl std::fmt::Display for Expr { .collect::>() .join(", ") ), + Expr::Assign { name, value } => write!(f, "Assign ({} = {})", name, value.node), } } } @@ -112,6 +134,7 @@ impl Debug for Expr { .collect::>() .join(", ") ), + Expr::Assign { name, value } => write!(f, "(assign {:?} {:?})", name, value.node), } } } @@ -131,11 +154,6 @@ pub enum Stmt { initializer: Option>>, return_value: Box, }, - VarAssigment { - name: String, - value: Box>, - return_value: Box, - }, Return { expression: Box>, label: String, @@ -208,15 +226,6 @@ impl Display for Stmt { Some(init) => write!(f, "Var({} = {}) -> {:?};", name, init.node, return_value), None => write!(f, "Var({}) -> {:?};", name, return_value), }, - Stmt::VarAssigment { - name, - value, - return_value, - } => write!( - f, - "Assign({} = {}) -> {:?};", - name, value.node, return_value - ), Stmt::Return { expression, return_value, @@ -319,17 +328,6 @@ impl Debug for Stmt { ), None => write!(f, "Var({:?}) -> {:?};", name, return_value), }, - Stmt::VarAssigment { - name, - value, - return_value, - } => { - write!( - f, - "Assign({:?} = {:?}) -> {:?};", - name, value.node, return_value - ) - } Stmt::Return { expression, return_value, @@ -397,6 +395,7 @@ impl AstNodeKind for Expr { Expr::Grouping { .. } => "grouping expression", Expr::Identifier { .. } => "variable expression", Expr::Call { .. } => "call expression", + Expr::Assign { .. } => "assignment expression", } } } @@ -406,7 +405,6 @@ impl AstNodeKind for Stmt { match self { Stmt::Expression { .. } => "expression", Stmt::VarDeclaration { .. } => "variable declaration", - Stmt::VarAssigment { .. } => "assignment", Stmt::Return { .. } => "return statement", Stmt::Block { .. } => "block statement", Stmt::If { .. } => "if statement", @@ -417,12 +415,22 @@ impl AstNodeKind for Stmt { } } -#[derive(Clone, PartialEq, Default)] +#[derive(Clone)] pub struct AstNode { + /// Stable identity, assigned at construction and preserved across clones. + pub id: NodeId, pub node: T, pub source_slice: SourceSlice, } +// Identity (`id`) deliberately does not participate in equality: two nodes are +// equal when their content and location match, regardless of node id. +impl PartialEq for AstNode { + fn eq(&self, other: &Self) -> bool { + self.node == other.node && self.source_slice == other.source_slice + } +} + impl Display for AstNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -445,6 +453,10 @@ impl Debug for AstNode { impl AstNode { pub fn new(node: T, source_slice: SourceSlice) -> Self { - AstNode { node, source_slice } + AstNode { + id: NodeId::next(), + node, + source_slice, + } } } diff --git a/src/common/base_value.rs b/src/common/base_value.rs index 36e996f..bc6ed9c 100755 --- a/src/common/base_value.rs +++ b/src/common/base_value.rs @@ -398,7 +398,7 @@ impl Add for BaseValue { Ok(BaseValue::String(format!("{}{}", a, b))) } _ => runtime_error( - SourceSlice::default(), + SourceSlice::synthetic(), "Cannot add non-numeric values".to_string(), ), } @@ -428,7 +428,7 @@ impl Sub for BaseValue { match (self, other) { (BaseValue::Number(a), BaseValue::Number(b)) => Ok(BaseValue::Number(a.sub(b))), _ => runtime_error( - SourceSlice::default(), + SourceSlice::synthetic(), "Cannot subtract non-numeric values".to_string(), ), } @@ -442,10 +442,10 @@ impl Div for BaseValue { match (self, other) { (BaseValue::Number(a), BaseValue::Number(b)) => match a.div(b) { Some(result) => Ok(BaseValue::Number(result)), - None => runtime_error(SourceSlice::default(), "Division by zero".to_string()), + None => runtime_error(SourceSlice::synthetic(), "Division by zero".to_string()), }, _ => runtime_error( - SourceSlice::default(), + SourceSlice::synthetic(), "Cannot divide non-numeric values".to_string(), ), } @@ -459,7 +459,7 @@ impl Mul for BaseValue { match (self, other) { (BaseValue::Number(a), BaseValue::Number(b)) => Ok(BaseValue::Number(a.mul(b))), _ => runtime_error( - SourceSlice::default(), + SourceSlice::synthetic(), "Cannot multiply non-numeric values".to_string(), ), } @@ -473,10 +473,10 @@ impl Rem for BaseValue { match (self, other) { (BaseValue::Number(a), BaseValue::Number(b)) => match a.rem(b) { Some(result) => Ok(BaseValue::Number(result)), - None => runtime_error(SourceSlice::default(), "Division by zero".to_string()), + None => runtime_error(SourceSlice::synthetic(), "Division by zero".to_string()), }, _ => runtime_error( - SourceSlice::default(), + SourceSlice::synthetic(), "Cannot divide non-numeric values".to_string(), ), } diff --git a/src/frontend/lexer.rs b/src/frontend/lexer.rs index 5befaad..5c65137 100755 --- a/src/frontend/lexer.rs +++ b/src/frontend/lexer.rs @@ -5,8 +5,9 @@ use crate::frontend::tokens::{Token, TokenType}; pub struct Lexer { input: String, - start_char: usize, - current_char: usize, + // Byte offsets into `input` (not char counts) so slicing is UTF-8 correct. + start: usize, + current: usize, start_pos: SourcePosition, end_pos: SourcePosition, source_id: SourceId, @@ -48,28 +49,18 @@ impl Lexer { pub fn new(input: String, source_id: SourceId) -> Lexer { Lexer { input, - start_char: 0, - current_char: 0, + start: 0, + current: 0, start_pos: SourcePosition::default(), end_pos: SourcePosition::default(), source_id, } } - fn advance_column(&mut self) { - self.current_char += 1; - self.end_pos.column += 1; - } - - fn advance_line(&mut self) { - self.end_pos.line += 1; - self.end_pos.column = 0; - } - pub fn scans_tokens(&mut self) -> LoxResult> { let mut tokens = Vec::new(); while !self.is_at_end() { - self.start_char = self.current_char; + self.start = self.current; self.start_pos = self.end_pos.clone(); match self.scan_token() { Ok(Some(token)) => tokens.push(token), @@ -82,32 +73,31 @@ impl Lexer { } fn is_at_end(&self) -> bool { - self.current_char >= self.input.len() + self.current >= self.input.len() } fn advance(&mut self) -> char { - self.advance_column(); - self.input.chars().nth(self.current_char - 1).unwrap() + let c = self.input[self.current..].chars().next().unwrap(); + self.current += c.len_utf8(); + if c == '\n' { + self.end_pos.line += 1; + self.end_pos.column = 0; + } else { + self.end_pos.column += 1; + } + c } fn peek(&self) -> char { - if self.is_at_end() { - '\0' - } else { - self.input.chars().nth(self.current_char).unwrap() - } + self.input[self.current..].chars().next().unwrap_or('\0') } fn peek_next(&self) -> char { - if self.current_char + 1 >= self.input.len() { - '\0' - } else { - self.input.chars().nth(self.current_char + 1).unwrap() - } + self.input[self.current..].chars().nth(1).unwrap_or('\0') } fn make_token(&self, token_type: TokenType) -> Token { - let text = self.input[self.start_char..self.current_char].to_string(); + let text = self.input[self.start..self.current].to_string(); Token::new( token_type, text, @@ -119,7 +109,7 @@ impl Lexer { ) } fn make_token_with_literal(&self, token_type: TokenType, literal: BaseValue) -> Token { - let text = self.input[self.start_char..self.current_char].to_string(); + let text = self.input[self.start..self.current].to_string(); Token::new_complete( token_type, text, @@ -179,9 +169,6 @@ impl Lexer { ('/', '*') => { // Commento multi-line while (self.peek() != '*' || self.peek_next() != '/') && !self.is_at_end() { - if self.peek() == '\n' { - self.advance_line(); - } self.advance(); } if self.is_at_end() { @@ -201,10 +188,7 @@ impl Lexer { } ('/', _) => Ok(Some(self.make_token(TokenType::Slash))), (' ', _) | ('\r', _) | ('\t', _) => Ok(None), - ('\n', _) => { - self.advance_line(); - Ok(None) - } + ('\n', _) => Ok(None), ('"', _) => self.string(), (c, _) if c.is_digit(10) => self.number(), (c, _) if c.is_alphanumeric() || c == '_' => self.identifier(), @@ -236,7 +220,7 @@ impl Lexer { self.advance(); Ok(Some(self.make_token_with_literal( TokenType::String, - BaseValue::String(self.input[self.start_char..self.current_char].to_string()), + BaseValue::String(self.input[self.start..self.current].to_string()), ))) } @@ -266,7 +250,7 @@ impl Lexer { None }; - let num_str = &self.input[self.start_char..self.current_char]; + let num_str = &self.input[self.start..self.current]; let num_str_without_suffix = if suffix.is_some() { &num_str[..num_str.len() - 1] } else { @@ -303,7 +287,7 @@ impl Lexer { while self.peek().is_alphanumeric() || self.peek() == '_' { self.advance(); } - let text = self.input[self.start_char..self.current_char].to_string(); + let text = self.input[self.start..self.current].to_string(); match get_keyword_token(&text) { Some(TokenType::True) => Ok(Some( self.make_token_with_literal(TokenType::True, BaseValue::Boolean(true)), @@ -509,4 +493,25 @@ mod tests { let result = Lexer::new("@".to_string(), 0).scans_tokens(); assert!(result.is_err()); } + + #[test] + fn handles_multibyte_identifier() { + // `é` is two UTF-8 bytes; the old char-counted slicing would panic or + // slice mid-codepoint here. Byte offsets make this correct. + let tokens = lex("café"); + assert_eq!(tokens[0].token_type, TokenType::Identifier); + assert_eq!( + tokens[0].literal, + Some(BaseValue::String("café".to_string())) + ); + } + + #[test] + fn tracks_line_and_column_across_newlines() { + // "1\n22": the second token sits at the start of line 1. + let tokens = lex("1\n22"); + assert_eq!(tokens[1].lexeme, "22"); + assert_eq!(tokens[1].source_slice.start_position.line, 1); + assert_eq!(tokens[1].source_slice.start_position.column, 0); + } } diff --git a/src/frontend/parser.rs b/src/frontend/parser.rs index a7a047f..1a279c0 100755 --- a/src/frontend/parser.rs +++ b/src/frontend/parser.rs @@ -53,7 +53,6 @@ impl Parser { self.var_statement() } (TokenType::Identifier, TokenType::Colon) => self.var_statement(), - (TokenType::Identifier, TokenType::Equal) => self.assignment_statement(), (TokenType::If, _) => self.if_statement(), (TokenType::While, _) => self.while_statement(), (TokenType::For, _) => self.for_statement(), @@ -68,7 +67,7 @@ impl Parser { let variable = self.var_statement()?; let condition = self.expression()?; self.consume(TokenType::Semicolon, "Expected ';' after for condition")?; - let increment = self.assignment_statement()?; + let increment = self.expression_statement()?; let body = self.statement(Some("loop".to_string()))?; let end_slice = self.previous().source_slice.clone(); @@ -260,8 +259,8 @@ impl Parser { } let body = self.statement(None)?; - let node = AstNode { - node: Expr::Literal { + let node = AstNode::new( + Expr::Literal { value: BaseValue::Function(LoxFunction { parameters, return_type: None, @@ -270,8 +269,8 @@ impl Parser { guard, }), }, - source_slice: combine_position.clone(), - }; + combine_position.clone(), + ); Ok(AstNode::new( Stmt::VarDeclaration { name: name_lexeme, @@ -294,12 +293,12 @@ impl Parser { } // todo: make type annotation } - let mut value = AstNode { - node: Expr::Literal { + let mut value = AstNode::new( + Expr::Literal { value: BaseValue::Nil, }, - source_slice: start_slice.clone(), - }; + start_slice.clone(), + ); if self.peek().token_type == TokenType::Equal { self.advance(); value = self.expression()?; @@ -323,31 +322,6 @@ impl Parser { combined_slice, )) } - fn assignment_statement(&mut self) -> LoxResult> { - let start_slice = self.peek().source_slice.clone(); - let name = self.consume(TokenType::Identifier, "Expect variable name.")?; - let name_lexeme = name.lexeme.clone(); - self.consume(TokenType::Equal, "Expect '=' after variable name.")?; - let value = self.expression()?; - let semicolon = self.consume( - TokenType::Semicolon, - "Expect ';' after variable declaration.", - )?; - let end_slice = semicolon.source_slice.clone(); - let combined_slice = SourceSlice::from_positions( - start_slice.source_id, - start_slice.start_position, - end_slice.end_position, - ); - Ok(AstNode::new( - Stmt::VarAssigment { - name: name_lexeme, - value: Box::new(value), - return_value: Box::new(BaseValue::Nil), - }, - combined_slice, - )) - } fn print_statement(&mut self) -> LoxResult> { // consume the print keyword @@ -441,12 +415,12 @@ impl Parser { source_slice, }) => { if message == "Expect expression." { - AstNode { - node: Expr::Literal { + AstNode::new( + Expr::Literal { value: BaseValue::Nil, }, - source_slice: start_slice.clone(), - } + start_slice.clone(), + ) } else { return Err(LoxError::ParseError { message, @@ -479,7 +453,34 @@ impl Parser { } fn expression(&mut self) -> LoxResult> { - self.or_and() + self.assignment() + } + + fn assignment(&mut self) -> LoxResult> { + let expr = self.or_and()?; + if self.peek().token_type == TokenType::Equal { + self.advance(); // consume '=' + // Right-associative: `a = b = c` parses as `a = (b = c)`. + let value = self.assignment()?; + let combined_slice = SourceSlice::from_positions( + expr.source_slice.source_id, + expr.source_slice.start_position.clone(), + value.source_slice.end_position.clone(), + ); + let target_slice = expr.source_slice.clone(); + match expr.node { + Expr::Identifier { name } => Ok(AstNode::new( + Expr::Assign { + name, + value: Box::new(value), + }, + combined_slice, + )), + _ => parse_error(target_slice, "Invalid assignment target."), + } + } else { + Ok(expr) + } } fn or_and(&mut self) -> LoxResult> { @@ -978,8 +979,11 @@ mod tests { 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), + Stmt::Expression { expression, .. } => match &expression.node { + Expr::Assign { name, .. } => assert_eq!(name, "x"), + other => panic!("expected assign expression, got {:?}", other), + }, + other => panic!("expected expression statement, got {:?}", other), } } diff --git a/src/frontend/source_registry.rs b/src/frontend/source_registry.rs index 27ae1ef..7c245fa 100755 --- a/src/frontend/source_registry.rs +++ b/src/frontend/source_registry.rs @@ -51,7 +51,7 @@ impl SourceFile { } } -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct SourcePosition { pub line: usize, pub column: usize, @@ -63,7 +63,7 @@ impl Display for SourcePosition { } } -#[derive(Clone, PartialEq, Default)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct SourceSlice { pub source_id: SourceId, pub start_position: SourcePosition, @@ -91,6 +91,25 @@ impl Display for SourceSlice { } impl SourceSlice { + /// An explicit, non-located placeholder span. + /// + /// Use this only when a real span is genuinely unavailable. Unlike the old + /// `Default` impl, it is greppable and obviously intentional, so it can't be + /// produced by accident. + pub fn synthetic() -> Self { + Self { + source_id: 0, + start_position: SourcePosition::default(), + end_position: SourcePosition::default(), + } + } + + /// Whether this span is the non-located placeholder produced by + /// [`SourceSlice::synthetic`]. + pub fn is_synthetic(&self) -> bool { + *self == Self::synthetic() + } + pub fn from_positions( source_id: SourceId, start_position: SourcePosition, diff --git a/src/logging/display_ast.rs b/src/logging/display_ast.rs index 1418c50..04775c0 100755 --- a/src/logging/display_ast.rs +++ b/src/logging/display_ast.rs @@ -234,6 +234,20 @@ impl PrettyPrint for Expr { write!(f, "{}Identifier {{ name: {:?} }}", indent, name) } } + Expr::Assign { name, value } => { + if ctx.config.compact { + let value_str = + pretty_print_with_config(value.as_ref(), &PrettyConfig::compact()); + write!(f, "{} = {}", name, value_str) + } else { + writeln!(f, "{}Assign {{", indent)?; + writeln!(f, "{}name: {:?},", ctx.child_context(false).indent(), name)?; + write!(f, "{}value: ", ctx.child_context(true).indent())?; + value.pretty_print(&ctx.child_context(true), f)?; + writeln!(f)?; + write!(f, "{}}}", indent) + } + } Expr::Call { callee, arguments } => { if ctx.config.compact { write!(f, "{}", callee) @@ -317,20 +331,7 @@ impl PrettyPrint for Stmt { write!(f, "{}}}", indent) } } - Stmt::VarAssigment { name, value, .. } => { - if ctx.config.compact { - let value_str = - pretty_print_with_config(value.as_ref(), &PrettyConfig::compact()); - write!(f, "{} = {};", name, value_str) - } else { - writeln!(f, "{}Assign {{", indent)?; - writeln!(f, "{}name: {:?},", ctx.child_context(false).indent(), name)?; - write!(f, "{}value: ", ctx.child_context(true).indent())?; - value.pretty_print(&ctx.child_context(true), f)?; - writeln!(f)?; - write!(f, "{}}}", indent) - } - } + Stmt::Return { expression, .. } => { if ctx.config.compact { let expr_str = diff --git a/src/middleend/variable_resolution.rs b/src/middleend/variable_resolution.rs index 71cfb6c..32467b5 100755 --- a/src/middleend/variable_resolution.rs +++ b/src/middleend/variable_resolution.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::{ backend::environment::EnvironmentStack, common::{ - ast::{AstNode, Expr, Stmt}, + ast::{AstNode, Expr, NodeId, Stmt}, lox_result::{LoxError, LoxResult}, }, frontend::source_registry::SourceSlice, @@ -12,7 +12,7 @@ use crate::{ struct Resolver { scopes: EnvironmentStack, - locals: HashMap, + locals: HashMap, } impl Resolver { @@ -27,6 +27,7 @@ impl Resolver { if self.scopes.is_empty() { return; } + let scope = self.scopes.peek(); let _ = self.scopes.set(name.clone(), false); } @@ -37,13 +38,16 @@ impl Resolver { let _ = self.scopes.set(name.clone(), true); } - fn resolve_local(&mut self, name: &String) { + fn resolve_local(&mut self, id: NodeId, name: &String) { let depth = self.scopes.depth(); for i in (0..depth).rev() { if self.scopes.scope_contains(i, name) { - self.locals.insert(, i); + // Distance = how many scopes up from the innermost the binding lives. + self.locals.insert(id, depth - 1 - i); + return; } } + // Not found in any tracked scope: assume global, record nothing. } } @@ -57,20 +61,8 @@ impl Visitor for Resolver { self.define(name); Ok(()) } - Stmt::VarAssigment { name, .. } => { - match self.scopes.get(name) { - Ok(true) => (), - Ok(false) => { - return Err(LoxError::RuntimeError { - source_slice: SourceSlice::default(), - message: "Cant read local variable in it own lintilizer".to_string(), - }) - } - Err(err) => return Err(err), - } - // `walk_stmt` resolves the assigned value. - walk_stmt(self, stmt) - } + // NOTE: assignment is now an `Expr::Assign`, not a statement. + // Add an `Expr::Assign` arm to `visit_expr` to resolve assignments. Stmt::Block { .. } => { self.scopes.push_new_scope(); walk_stmt(self, stmt)?; @@ -87,11 +79,15 @@ impl Visitor for Resolver { Expr::Identifier { name, .. } => { if !self.scopes.is_empty() && self.scopes.get(name).is_ok() { return Err(LoxError::ParseError { - source_slice: SourceSlice::default(), + source_slice: SourceSlice::synthetic(), message: "Cant read local varialbe in it own initializer".to_string(), }); } - Ok(()) + walk_expr(self, expr) + } + Expr::Assign { name, .. } => { + self.resolve_local(expr.id, name); + walk_expr(self, expr) } _ => walk_expr(self, expr), } diff --git a/src/middleend/visit_ast.rs b/src/middleend/visit_ast.rs index 1b1594f..35471d6 100644 --- a/src/middleend/visit_ast.rs +++ b/src/middleend/visit_ast.rs @@ -73,7 +73,6 @@ pub fn walk_stmt(visitor: &mut V, stmt: &AstNode) -> LoxResult } Ok(()) } - Stmt::VarAssigment { value, .. } => visitor.visit_expr(value), Stmt::Return { expression, .. } => visitor.visit_expr(expression), Stmt::Block { statements, .. } => { for statement in statements.iter() { @@ -137,6 +136,7 @@ pub fn walk_expr(visitor: &mut V, expr: &AstNode) -> LoxResult visitor.visit_expr(right) } Expr::Unary { operand, .. } => visitor.visit_expr(operand), + Expr::Assign { value, .. } => visitor.visit_expr(value), Expr::Grouping { expression } => visitor.visit_expr(expression), Expr::Call { callee, arguments, .. diff --git a/tests/frontend.rs b/tests/frontend.rs new file mode 100644 index 0000000..5ee5103 --- /dev/null +++ b/tests/frontend.rs @@ -0,0 +1,298 @@ +//! 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>, 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> { + 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()); +}