diff --git a/justfile b/justfile index 77b2d2f..dc8a87d 100644 --- a/justfile +++ b/justfile @@ -96,8 +96,8 @@ serve-web: build-wasm cd www && bun run dev [group: 'test'] -test: - cargo test +test *args: + cargo test -- {{ args }} [group: 'test'] test-release-workflow: diff --git a/src/analyzer.rs b/src/analyzer.rs new file mode 100644 index 0000000..7788617 --- /dev/null +++ b/src/analyzer.rs @@ -0,0 +1,386 @@ +use super::*; + +pub struct Analyzer<'a> { + environment: Environment<'a>, + inside_function: bool, + inside_loop: bool, +} + +impl<'a> Analyzer<'a> { + pub fn new(environment: Environment<'a>) -> Self { + Self { + environment, + inside_function: false, + inside_loop: false, + } + } + + pub fn analyze(&mut self, ast: &Spanned>) -> Vec { + let (program, _) = ast; + + match program { + Program::Statements(statements) => { + let mut errors = Vec::new(); + + for statement in statements { + errors.extend(self.analyze_statement(statement)); + } + + errors + } + } + } + + fn analyze_statement( + &mut self, + statement: &Spanned>, + ) -> Vec { + let (statement, span) = statement; + + let mut errors = Vec::new(); + + match statement { + Statement::Assignment(lhs, rhs) => { + let is_valid_lvalue = matches!( + &lhs.0, + Expression::Identifier(_) | Expression::ListAccess(_, _) + ); + + if !is_valid_lvalue { + errors.push(Error::new( + lhs.1, + "Left-hand side of assignment must be a variable or list access", + )); + } + + errors.extend(self.analyze_expression(rhs)); + } + Statement::Block(statements) => { + for statement in statements { + errors.extend(self.analyze_statement(statement)); + } + } + Statement::Break => { + if !self.inside_loop { + errors + .push(Error::new(*span, "Cannot use 'break' outside of a loop")); + } + } + Statement::Continue => { + if !self.inside_loop { + errors + .push(Error::new(*span, "Cannot use 'continue' outside of a loop")); + } + } + Statement::Expression(expr) => { + errors.extend(self.analyze_expression(expr)); + } + Statement::Function(name, parameters, body) => { + let mut parameter_names = HashSet::new(); + + for &name in parameters { + if !parameter_names.insert(name) { + errors.push(Error::new( + *span, + format!("Duplicate parameter name `{}`", name), + )); + } + } + + let function = Value::Function( + name, + parameters.clone(), + body.clone(), + self.environment.clone(), + ); + + self + .environment + .add_function(name, Function::UserDefined(function.clone())); + + let old_environment = self.environment.clone(); + let old_inside_function = self.inside_function; + + self.environment = Environment::with_parent(self.environment.clone()); + self.inside_function = true; + + for ¶meter in parameters { + self.environment.add_variable(parameter, Value::Null); + } + + for statement in body { + errors.extend(self.analyze_statement(statement)); + } + + self.environment = old_environment; + self.inside_function = old_inside_function; + } + Statement::If(condition, then_branch, else_branch) => { + errors.extend(self.analyze_expression(condition)); + + for statement in then_branch { + errors.extend(self.analyze_statement(statement)); + } + + if let Some(else_statements) = else_branch { + for statement in else_statements { + errors.extend(self.analyze_statement(statement)); + } + } + } + Statement::Loop(body) => { + let parent_env = self.environment.clone(); + let old_inside_loop = self.inside_loop; + + self.environment = Environment::with_parent(parent_env.clone()); + self.inside_loop = true; + + for statement in body { + errors.extend(self.analyze_statement(statement)); + } + + self.environment = parent_env; + self.inside_loop = old_inside_loop; + } + Statement::Return(expr) => { + if let Some(expr) = expr { + errors.extend(self.analyze_expression(expr)); + } + } + Statement::While(condition, body) => { + errors.extend(self.analyze_expression(condition)); + + let parent_env = self.environment.clone(); + let old_inside_loop = self.inside_loop; + + self.environment = Environment::with_parent(parent_env.clone()); + self.inside_loop = true; + + for statement in body { + errors.extend(self.analyze_statement(statement)); + } + + self.environment = parent_env; + self.inside_loop = old_inside_loop; + } + } + + errors + } + + fn analyze_expression( + &self, + expression: &Spanned>, + ) -> Vec { + let (expr, span) = expression; + + let mut errors = Vec::new(); + + match expr { + Expression::BinaryOp(_, lhs, rhs) => { + errors.extend(self.analyze_expression(lhs)); + errors.extend(self.analyze_expression(rhs)); + } + Expression::UnaryOp(_, expr) => { + errors.extend(self.analyze_expression(expr)); + } + Expression::FunctionCall(name, arguments) => { + if !self.environment.has_symbol(name) { + errors + .push(Error::new(*span, format!("Undefined function `{}`", name))); + } else if let Some(Function::UserDefined(Value::Function( + _, + parameters, + _, + _, + ))) = self.environment.functions.get(name) + { + if arguments.len() != parameters.len() { + errors.push(Error::new( + *span, + format!( + "Function `{}` expects {} arguments, got {}", + name, + parameters.len(), + arguments.len() + ), + )); + } + } + + for argument in arguments { + errors.extend(self.analyze_expression(argument)); + } + } + Expression::Identifier(_) => {} + Expression::List(items) => { + for item in items { + errors.extend(self.analyze_expression(item)); + } + } + Expression::ListAccess(list, index) => { + errors.extend(self.analyze_expression(list)); + errors.extend(self.analyze_expression(index)); + } + _ => {} + } + + errors + } +} + +#[cfg(test)] +mod tests { + use {super::*, anyhow::anyhow}; + + #[derive(Debug)] + struct Test { + program: String, + errors: Vec, + } + + impl Test { + fn new() -> Self { + Self { + program: String::new(), + errors: Vec::new(), + } + } + + fn errors(self, errors: &[&str]) -> Self { + Self { + errors: errors + .iter() + .map(|s| s.to_string()) + .collect::>(), + ..self + } + } + + fn program(self, program: &str) -> Self { + Self { + program: program.to_owned(), + ..self + } + } + + fn run(self) -> Result { + let ast = match parse(&self.program) { + Ok(ast) => ast, + Err(errors) => { + return Err(anyhow!("Failed to parse program: {:?}", errors)); + } + }; + + let environment = Environment::new(Config::default()); + + let mut analyzer = Analyzer::new(environment); + + let analysis_errors = analyzer.analyze(&ast); + + assert_eq!( + analysis_errors.len(), + self.errors.len(), + "Expected {} error(s), got {}:\n{:?}", + self.errors.len(), + analysis_errors.len(), + analysis_errors, + ); + + for (i, error) in analysis_errors.iter().enumerate() { + if !error.message.contains(&self.errors[i]) { + return Err(anyhow!( + "Error {} expected to contain '{}', got '{}'", + i, + self.errors[i], + error.message + )); + } + } + + Ok(()) + } + } + + #[test] + fn invalid_lvalues() -> Result { + Test::new().program("a = 10").errors(&[]).run()?; + + Test::new().program("a = [1, 2, 3]; a[0] = 10").run()?; + + Test::new() + .program("\"foo\" = 10") + .errors(&[ + "Left-hand side of assignment must be a variable or list access", + ]) + .run()?; + + Test::new() + .program("5 = 10") + .errors(&[ + "Left-hand side of assignment must be a variable or list access", + ]) + .run() + } + + #[test] + fn function_parameters() -> Result { + Test::new() + .program("fn add(a, b) { return a + b; }") + .run()?; + + Test::new() + .program("fn add(a, a) { return a + a; }") + .errors(&["Duplicate parameter name `a`"]) + .run()?; + + Test::new() + .program("fn add(a, b, a, c, b) { return a + b + c; }") + .errors(&[ + "Duplicate parameter name `a`", + "Duplicate parameter name `b`", + ]) + .run() + } + + #[test] + fn function_calls() -> Result { + Test::new().program("sin(3.14)").errors(&[]).run()?; + + Test::new() + .program("fn greet() { return 'Hello'; }; greet()") + .errors(&[]) + .run()?; + + Test::new() + .program("undefined_function()") + .errors(&["Undefined function `undefined_function`"]) + .run() + } + + #[test] + fn break_outside_of_loop() -> Result { + Test::new() + .program("a = 0; while (a < 100) { if (i == 25) { break }; a = a + 1 }") + .run()?; + + Test::new() + .program("break") + .errors(&["Cannot use 'break' outside of a loop"]) + .run() + } + + #[test] + fn continue_outside_of_loop() -> Result { + Test::new() + .program( + "a = 0; while (a < 100) { if (i == 25) { continue }; a = a + 1 }", + ) + .run()?; + + Test::new() + .program("continue") + .errors(&["Cannot use 'continue' outside of a loop"]) + .run() + } +} diff --git a/src/arguments.rs b/src/arguments.rs index 17a8738..ef2d653 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -59,7 +59,6 @@ pub struct Arguments { )] pub stack_size: usize, } - impl Arguments { pub fn run(self) -> Result { match (&self.filename, &self.expression) { @@ -83,22 +82,40 @@ impl Arguments { let filename = filename.to_string_lossy().to_string(); - let mut evaluator = Evaluator::from(Environment::new(Config { - precision: self.precision, - rounding_mode: self.rounding_mode.into(), - })); - match parse(&content) { - Ok(ast) => match evaluator.eval(&ast) { - Ok(_) => Ok(()), - Err(error) => { - error - .report(&filename) - .eprint((filename.as_str(), Source::from(content)))?; + Ok(ast) => { + let environment = Environment::new(Config { + precision: self.precision, + rounding_mode: self.rounding_mode.into(), + }); + + let mut analyzer = Analyzer::new(environment.clone()); + + let analysis_errors = analyzer.analyze(&ast); + + if !analysis_errors.is_empty() { + for error in analysis_errors { + error + .report(&filename) + .eprint((filename.as_str(), Source::from(&content)))?; + } process::exit(1); } - }, + + let mut evaluator = Evaluator::from(environment); + + match evaluator.eval(&ast) { + Ok(_) => Ok(()), + Err(error) => { + error + .report(&filename) + .eprint((filename.as_str(), Source::from(content)))?; + + process::exit(1); + } + } + } Err(errors) => { for error in errors { error diff --git a/src/environment.rs b/src/environment.rs index 5f3881e..1e0f068 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -1,14 +1,14 @@ use super::*; #[derive(Clone, Debug, Default)] -pub struct Environment<'src> { +pub struct Environment<'a> { pub config: Config, - pub functions: HashMap<&'src str, Function<'src>>, - pub parent: Option>>, - pub variables: HashMap<&'src str, Value<'src>>, + pub functions: HashMap<&'a str, Function<'a>>, + pub parent: Option>>, + pub variables: HashMap<&'a str, Value<'a>>, } -impl<'src> Environment<'src> { +impl<'a> Environment<'a> { pub fn new(config: Config) -> Self { let mut env = Self { config: config.clone(), @@ -1163,20 +1163,20 @@ impl<'src> Environment<'src> { env } - pub fn add_function(&mut self, name: &'src str, function: Function<'src>) { + pub fn add_function(&mut self, name: &'a str, function: Function<'a>) { self.functions.insert(name, function); } - pub fn add_variable(&mut self, name: &'src str, value: Value<'src>) { + pub fn add_variable(&mut self, name: &'a str, value: Value<'a>) { self.variables.insert(name, value); } pub fn call_function( &self, name: &str, - arguments: Vec>, + arguments: Vec>, span: Span, - ) -> Result, Error> { + ) -> Result, Error> { if let Some(function) = self.functions.get(name) { match function { Function::Builtin(function) => function(BuiltinFunctionPayload { @@ -1245,7 +1245,18 @@ impl<'src> Environment<'src> { } } - pub fn resolve_symbol(&self, symbol: &str) -> Option<&Value<'src>> { + pub fn has_symbol(&self, symbol: &str) -> bool { + let contains = self.functions.contains_key(symbol) + || self.variables.contains_key(symbol); + + if let Some(parent) = &self.parent { + contains || parent.has_symbol(symbol) + } else { + contains + } + } + + pub fn resolve_symbol(&self, symbol: &str) -> Option<&Value<'a>> { if let Some(value) = self.variables.get(symbol) { Some(value) } else if let Some(function) = self.functions.get(symbol) { @@ -1260,7 +1271,7 @@ impl<'src> Environment<'src> { } } - pub fn with_parent(parent: Environment<'src>) -> Self { + pub fn with_parent(parent: Environment<'a>) -> Self { Self { config: parent.config.clone(), functions: parent.functions.clone(), diff --git a/src/error.rs b/src/error.rs index b598c40..9fd21ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Error { pub span: Span, pub message: String, diff --git a/src/evaluator.rs b/src/evaluator.rs index 45739fc..b34017f 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -155,6 +155,7 @@ impl<'a> Evaluator<'a> { "Cannot use 'break' outside of a loop", )); } + Ok(EvalResult::Break) } Statement::Continue => { diff --git a/src/highlighter.rs b/src/highlighter.rs index fd11ace..c96b721 100644 --- a/src/highlighter.rs +++ b/src/highlighter.rs @@ -10,16 +10,16 @@ const COLOR_OPERATOR: &str = "\x1b[36m"; // Cyan const COLOR_RESET: &str = "\x1b[0m"; const COLOR_STRING: &str = "\x1b[32m"; // Green -pub struct TreeHighlighter<'src> { - content: &'src str, +pub struct TreeHighlighter<'a> { + content: &'a str, } -impl<'src> TreeHighlighter<'src> { - pub fn new(content: &'src str) -> Self { +impl<'a> TreeHighlighter<'a> { + pub fn new(content: &'a str) -> Self { Self { content } } - pub fn highlight(&self) -> Cow<'src, str> { + pub fn highlight(&self) -> Cow<'a, str> { match parse(self.content) { Ok(ast) => self.colorize_ast(&ast), Err(_) => { @@ -28,17 +28,14 @@ impl<'src> TreeHighlighter<'src> { } } - fn colorize_ast(&self, program: &Spanned>) -> Cow<'src, str> { + fn colorize_ast(&self, program: &Spanned>) -> Cow<'a, str> { let mut color_spans = Vec::new(); self.collect_color_spans(program, &mut color_spans); color_spans.sort_by_key(|span| span.0); self.apply_color_spans(&color_spans) } - fn apply_color_spans( - &self, - spans: &[(usize, usize, &str)], - ) -> Cow<'src, str> { + fn apply_color_spans(&self, spans: &[(usize, usize, &str)]) -> Cow<'a, str> { if spans.is_empty() { return Cow::Borrowed(self.content); } @@ -69,7 +66,7 @@ impl<'src> TreeHighlighter<'src> { fn collect_color_spans( &self, - program: &Spanned>, + program: &Spanned>, spans: &mut Vec<(usize, usize, &'static str)>, ) { let (node, _) = program; @@ -85,7 +82,7 @@ impl<'src> TreeHighlighter<'src> { fn collect_statement_spans( &self, - statement: &Spanned>, + statement: &Spanned>, spans: &mut Vec<(usize, usize, &'static str)>, ) { let (node, span) = statement; @@ -286,7 +283,7 @@ impl<'src> TreeHighlighter<'src> { fn collect_expression_spans( &self, - expression: &Spanned>, + expression: &Spanned>, spans: &mut Vec<(usize, usize, &'static str)>, ) { let (node, span) = expression; @@ -454,8 +451,8 @@ impl<'src> TreeHighlighter<'src> { fn find_operator( &self, op: &str, - lhs: &Spanned>, - rhs: &Spanned>, + lhs: &Spanned>, + rhs: &Spanned>, ) -> Option { let (start, end) = (lhs.1.end, rhs.1.start); diff --git a/src/lib.rs b/src/lib.rs index fe97285..05c8d52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ pub(crate) use { chumsky::prelude::*, clap::Parser as Clap, std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::{self, Display, Formatter}, fs, ops::Range, @@ -31,6 +31,7 @@ pub(crate) use { }; pub use crate::{ + analyzer::Analyzer, arguments::Arguments, ast::{BinaryOp, Expression, Program, Statement, UnaryOp}, config::Config, @@ -56,6 +57,7 @@ pub mod arguments; #[cfg(not(target_family = "wasm"))] mod highlighter; +mod analyzer; mod ast; mod config; mod environment; diff --git a/src/parser.rs b/src/parser.rs index c51cfab..36ed64b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -42,29 +42,7 @@ fn statement_parser<'a>() .collect::>() .delimited_by(just('{').padded(), just('}').padded()); - let simple_ident = text::ident().padded().map_with(|name, e| { - let span = e.span(); - (Expression::Identifier(name), span) - }); - - let indexed_ident = simple_ident.foldl( - expression - .clone() - .delimited_by(just('['), just(']')) - .padded() - .map_with(|expression, e| (expression, e.span())) - .repeated(), - |base, (index, span)| { - let span = (base.1.start..span.end).into(); - - let expression = - Expression::ListAccess(Box::new(base), Box::new(index)); - - (expression, span) - }, - ); - - let assignment_statement = indexed_ident + let assignment_statement = expression_parser() .then_ignore(just('=').padded()) .then(expression.clone()) .map(|(lhs, rhs)| Statement::Assignment(lhs, rhs)) diff --git a/src/value.rs b/src/value.rs index 6dfa51c..2796ed9 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,18 +1,18 @@ use super::*; #[derive(Clone, Debug)] -pub enum Value<'src> { +pub enum Value<'a> { Boolean(bool), Function( - &'src str, - Vec<&'src str>, - Vec>>, - Environment<'src>, + &'a str, + Vec<&'a str>, + Vec>>, + Environment<'a>, ), List(Vec), Null, Number(Float), - String(&'src str), + String(&'a str), } impl Display for Value<'_> {