From 7c66110cc989c189812b96544c684ba480649dfe Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 4 May 2026 21:33:21 -0400 Subject: [PATCH 1/4] Extract assignment targets into dedicated syntax nodes --- crates/val-wasm/src/ast_node.rs | 28 +++++ crates/val-wasm/src/lib.rs | 3 +- src/ast.rs | 31 +++++- src/evaluator.rs | 178 +++++++++++++++++++------------- src/highlighter.rs | 38 ++++++- src/lib.rs | 2 +- src/parser.rs | 49 +++++---- tests/integration.rs | 15 +++ 8 files changed, 252 insertions(+), 92 deletions(-) diff --git a/crates/val-wasm/src/ast_node.rs b/crates/val-wasm/src/ast_node.rs index 6280e06..508460d 100644 --- a/crates/val-wasm/src/ast_node.rs +++ b/crates/val-wasm/src/ast_node.rs @@ -162,6 +162,34 @@ impl From<(&Statement<'_>, &Span)> for AstNode { } } +impl From<(&AssignmentTarget<'_>, &Span)> for AstNode { + fn from(value: (&AssignmentTarget<'_>, &Span)) -> Self { + let (target, span) = value; + + let range = Range::from(span); + + let mut children = Vec::new(); + + match target { + AssignmentTarget::Identifier(_) => Self { + kind: target.kind(), + range, + children, + }, + AssignmentTarget::ListAccess(list, index) => { + children.push(Self::from((&list.0, &list.1))); + children.push(Self::from((&index.0, &index.1))); + + Self { + kind: target.kind(), + range, + children, + } + } + } + } +} + impl From<(&Expression<'_>, &Span)> for AstNode { fn from(value: (&Expression<'_>, &Span)) -> Self { let (expression, span) = value; diff --git a/crates/val-wasm/src/lib.rs b/crates/val-wasm/src/lib.rs index 3f7754c..4553075 100644 --- a/crates/val-wasm/src/lib.rs +++ b/crates/val-wasm/src/lib.rs @@ -7,7 +7,8 @@ use { serde::Serialize, serde_wasm_bindgen::to_value, val::{ - Environment, Evaluator, Expression, Program, RoundingMode, Span, Statement, + AssignmentTarget, Environment, Evaluator, Expression, Program, + RoundingMode, Span, Statement, }, wasm_bindgen::prelude::*, }; diff --git a/src/ast.rs b/src/ast.rs index cc37f6a..4547477 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -34,7 +34,7 @@ impl Program<'_> { #[derive(Debug, Clone)] pub enum Statement<'a> { - Assignment(Spanned>, Spanned>), + Assignment(Spanned>, Spanned>), Block(Vec>>), Break, Continue, @@ -179,6 +179,35 @@ impl Statement<'_> { } } +#[derive(Debug, Clone)] +pub enum AssignmentTarget<'a> { + Identifier(&'a str), + ListAccess(Box>, Box>>), +} + +impl Display for AssignmentTarget<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + AssignmentTarget::Identifier(identifier) => { + write!(f, "identifier({identifier})") + } + AssignmentTarget::ListAccess(list, index) => { + write!(f, "list_access({}, {})", list.0, index.0) + } + } + } +} + +impl AssignmentTarget<'_> { + #[must_use] + pub fn kind(&self) -> String { + String::from(match self { + AssignmentTarget::Identifier(_) => "identifier", + AssignmentTarget::ListAccess(_, _) => "list_access", + }) + } +} + #[derive(Debug, Clone)] pub enum UnaryOp { Negate, diff --git a/src/evaluator.rs b/src/evaluator.rs index 4ae5d72..bf4a6de 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -85,76 +85,7 @@ impl<'a> Evaluator<'a> { Statement::Assignment(lhs, rhs) => { let value = self.eval_expression(rhs)?; - match &lhs.0 { - Expression::Identifier(name) => { - self.environment.add_symbol(name, value.clone()); - } - Expression::ListAccess(base_box, index_box) => { - let (list_name, list_span) = match &base_box.0 { - Expression::Identifier(name) => (*name, base_box.1), - _ => { - return Err(Error::new( - base_box.1, - "left‑hand side must be a variable or list element", - )); - } - }; - - let mut list = match self.environment.resolve_symbol(list_name) { - Some(Value::List(items)) => items, - Some(other) => { - return Err(Error::new( - list_span, - format!( - "'{}' is not a list (found {})", - list_name, - other.type_name() - ), - )); - } - None => { - return Err(Error::new( - list_span, - format!("Undefined variable `{list_name}`"), - )); - } - }; - - let Some(index) = self - .eval_expression(index_box)? - .number(index_box.1)? - .to_f64(self.environment.config.rounding_mode) - .and_then(finite_non_negative_usize) - else { - return Err(Error::new( - index_box.1, - "List index must be a non-negative finite number", - )); - }; - - if index >= list.len() { - return Err(Error::new( - lhs.1, - format!( - "Index {} out of bounds for list of length {}", - index, - list.len() - ), - )); - } - - list[index] = value.clone(); - - self.environment.add_symbol(list_name, Value::List(list)); - } - - _ => { - return Err(Error::new( - lhs.1, - "left‑hand side must be a variable or list element", - )); - } - } + self.assign(lhs, value.clone())?; Ok(Completion::Value(value)) } @@ -328,6 +259,113 @@ impl<'a> Evaluator<'a> { } } + fn assign( + &mut self, + target: &Spanned>, + value: Value<'a>, + ) -> Result<(), Error> { + match &target.0 { + AssignmentTarget::Identifier(name) => { + self.environment.add_symbol(name, value); + Ok(()) + } + AssignmentTarget::ListAccess(_, _) => { + let (name, name_span) = Self::assignment_root(target); + let mut indices = Vec::new(); + Self::assignment_indices(target, &mut indices); + + let Some(root) = self.environment.resolve_symbol(name) else { + return Err(Error::new( + name_span, + format!("Undefined variable `{name}`"), + )); + }; + + let root = + self.assign_indices(name, root, &indices, value, target.1)?; + self.environment.add_symbol(name, root); + Ok(()) + } + } + } + + fn assignment_root( + target: &Spanned>, + ) -> (&'a str, Span) { + match &target.0 { + AssignmentTarget::Identifier(name) => (name, target.1), + AssignmentTarget::ListAccess(base, _) => Self::assignment_root(base), + } + } + + fn assignment_indices<'target>( + target: &'target Spanned>, + indices: &mut Vec<&'target Spanned>>, + ) { + match &target.0 { + AssignmentTarget::Identifier(_) => {} + AssignmentTarget::ListAccess(base, index) => { + Self::assignment_indices(base, indices); + indices.push(index); + } + } + } + + fn assign_indices( + &mut self, + name: &'a str, + value: Value<'a>, + indices: &[&Spanned>], + assigned: Value<'a>, + span: Span, + ) -> Result, Error> { + let Some((index, rest)) = indices.split_first() else { + return Ok(assigned); + }; + + let mut list = match value { + Value::List(items) => items, + other => { + return Err(Error::new( + index.1, + format!("'{}' is not a list (found {})", name, other.type_name()), + )); + } + }; + + let index = self.list_index(index)?; + + if index >= list.len() { + return Err(Error::new( + span, + format!( + "Index {} out of bounds for list of length {}", + index, + list.len() + ), + )); + } + + list[index] = + self.assign_indices(name, list[index].clone(), rest, assigned, span)?; + + Ok(Value::List(list)) + } + + fn list_index( + &mut self, + index: &Spanned>, + ) -> Result { + self + .eval_expression(index)? + .number(index.1)? + .to_f64(self.environment.config.rounding_mode) + .and_then(finite_non_negative_usize) + .ok_or_else(|| { + Error::new(index.1, "List index must be a non-negative finite number") + }) + } + fn eval_expression( &mut self, ast: &Spanned>, diff --git a/src/highlighter.rs b/src/highlighter.rs index 29ad5d1..fa58ce6 100644 --- a/src/highlighter.rs +++ b/src/highlighter.rs @@ -94,7 +94,7 @@ impl<'src> TreeHighlighter<'src> { match node { Statement::Assignment(lhs, rhs) => { - self.collect_expression_spans(lhs, spans); + self.collect_assignment_target_spans(lhs, spans); if let Some(eq_pos) = self.content[start..end].find('=') { spans.push((start + eq_pos, start + eq_pos + 1, COLOR_OPERATOR)); @@ -309,6 +309,42 @@ impl<'src> TreeHighlighter<'src> { } } + fn collect_assignment_target_spans( + &self, + target: &Spanned>, + spans: &mut Vec<(usize, usize, &'static str)>, + ) { + let (node, span) = target; + + match node { + AssignmentTarget::Identifier(identifier) => { + spans.push(( + span.start, + span.start + identifier.len(), + COLOR_IDENTIFIER, + )); + } + AssignmentTarget::ListAccess(list, index) => { + self.collect_assignment_target_spans(list, spans); + self.collect_expression_spans(index, spans); + + if let Some(open_bracket) = + self.content[list.1.end..index.1.start].find('[') + { + let start = list.1.end + open_bracket; + spans.push((start, start + 1, COLOR_OPERATOR)); + } + + if let Some(close_bracket) = + self.content[index.1.end..span.end].find(']') + { + let start = index.1.end + close_bracket; + spans.push((start, start + 1, COLOR_OPERATOR)); + } + } + } + } + fn collect_expression_spans( &self, expression: &Spanned>, diff --git a/src/lib.rs b/src/lib.rs index 6bea7a0..4fa3d2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub(crate) use { pub use crate::{ arguments::Arguments, - ast::{BinaryOp, Expression, Program, Statement, UnaryOp}, + ast::{AssignmentTarget, BinaryOp, Expression, Program, Statement, UnaryOp}, builtin::{Builtin, BuiltinFunction, BuiltinFunctionPayload}, completion::Completion, config::Config, diff --git a/src/parser.rs b/src/parser.rs index ed492e6..0f064d0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -31,6 +31,24 @@ fn program_parser<'a>() .map_with(|ast, e| (ast, e.span())) } +fn index_parser<'a, P>( + expression: P, +) -> impl Parser< + 'a, + &'a str, + (Spanned>, SimpleSpan), + extra::Err>, +> + Clone +where + P: Parser<'a, &'a str, Spanned>, extra::Err>> + + Clone, +{ + expression + .delimited_by(just('['), just(']')) + .padded() + .map_with(|expression, e| (expression, e.span())) +} + fn statement_parser<'a>() -> impl Parser<'a, &'a str, Spanned>, extra::Err>> + Clone { @@ -47,27 +65,22 @@ fn statement_parser<'a>() let simple_ident = text::ident().padded().map_with(|name, e| { let span = e.span(); - (Expression::Identifier(name), span) + (AssignmentTarget::Identifier(name), span) }); - let indexed_ident = simple_ident.foldl( - expression - .clone() - .delimited_by(just('['), just(']')) - .padded() - .map_with(|expression, e| (expression, e.span())) - .repeated(), + let assignment_target = simple_ident.foldl( + index_parser(expression.clone()).repeated(), |base, (index, span)| { let span = (base.1.start..span.end).into(); - let expression = - Expression::ListAccess(Box::new(base), Box::new(index)); + let target = + AssignmentTarget::ListAccess(Box::new(base), Box::new(index)); - (expression, span) + (target, span) }, ); - let assignment_statement = indexed_ident + let assignment_statement = assignment_target .then_ignore(just('=').padded()) .then(expression.clone()) .map(|(lhs, rhs)| Statement::Assignment(lhs, rhs)) @@ -246,12 +259,7 @@ fn expression_parser<'a>() .padded(); let list_access = atom.clone().foldl( - expression - .clone() - .delimited_by(just('['), just(']')) - .padded() - .map_with(|expression, e| (expression, e.span())) - .repeated(), + index_parser(expression.clone()).repeated(), |list: Spanned>, (index, span): (Spanned>, SimpleSpan)| { let span = (list.1.start..span.end).into(); @@ -468,6 +476,11 @@ mod tests { .program("x = 5") .ast("statements(assignment(identifier(x), number(5)))") .run(); + + Test::new() + .program("foo[0][1] = bar") + .ast("statements(assignment(list_access(list_access(identifier(foo), number(0)), number(1)), identifier(bar)))") + .run(); } #[test] diff --git a/tests/integration.rs b/tests/integration.rs index 9fd4d4b..9b88bd3 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1964,6 +1964,21 @@ fn list_element_assignment_updates_value() -> Result { .run() } +#[test] +fn nested_list_element_assignment_updates_value() -> Result { + Test::new()? + .program(indoc! { + " + nums = [[1, 2], [3, 4]] + nums[1][0] = 5 + println(nums) + " + }) + .expected_status(0) + .expected_stdout(Exact("[[1, 2], [5, 4]]\n")) + .run() +} + #[test] fn list_literals() -> Result { Test::new()? From add6e2f83bf7e3bf06fb0466b9e0ea19b4abf2db Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 4 May 2026 21:47:43 -0400 Subject: [PATCH 2/4] Tweak --- benches/main.rs | 9 +- crates/val-wasm/src/lib.rs | 2 +- justfile | 6 +- rustfmt.toml | 1 - src/arguments.rs | 12 +- src/ast.rs | 20 ++ src/completion.rs | 14 +- src/evaluator.rs | 590 ++++++++++++++++++------------------- src/function.rs | 4 +- 9 files changed, 328 insertions(+), 330 deletions(-) diff --git a/benches/main.rs b/benches/main.rs index aeb54a7..3880ad2 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -14,7 +14,8 @@ fn bench_increment_value(criterion: &mut Criterion) { group.bench_function(format!("n = {number}"), |bencher| { bencher.iter(|| { - black_box(Evaluator::from(Environment::default()).eval(&ast)).unwrap(); + black_box(Evaluator::from(Environment::default()).evaluate(&ast)) + .unwrap(); }); }); } @@ -70,7 +71,8 @@ fn bench_prime_count(criterion: &mut Criterion) { group.bench_function(format!("n = {number}"), |bencher| { bencher.iter(|| { - black_box(Evaluator::from(Environment::default()).eval(&ast)).unwrap(); + black_box(Evaluator::from(Environment::default()).evaluate(&ast)) + .unwrap(); }); }); } @@ -90,7 +92,8 @@ fn bench_recursive_factorial(criterion: &mut Criterion) { group.bench_function(format!("n = {number}"), |bencher| { bencher.iter(|| { - black_box(Evaluator::from(Environment::default()).eval(&ast)).unwrap(); + black_box(Evaluator::from(Environment::default()).evaluate(&ast)) + .unwrap(); }); }); } diff --git a/crates/val-wasm/src/lib.rs b/crates/val-wasm/src/lib.rs index 4553075..0aa4975 100644 --- a/crates/val-wasm/src/lib.rs +++ b/crates/val-wasm/src/lib.rs @@ -51,7 +51,7 @@ pub fn evaluate(input: &str) -> Result { rounding_mode: RoundingMode::FromZero.into(), })); - match evaluator.eval(&ast) { + match evaluator.evaluate(&ast) { Ok(value) => Ok(to_value(&value.to_string()).unwrap()), Err(error) => Err( to_value(&[ValError { diff --git a/justfile b/justfile index 77b2d2f..02b3b24 100644 --- a/justfile +++ b/justfile @@ -34,7 +34,7 @@ check: [group: 'check'] ci: test clippy forbid - cargo +nightly fmt --all -- --check + cargo fmt --all -- --check cargo update --locked --package val [group: 'check'] @@ -43,11 +43,11 @@ clippy: [group: 'format'] fmt: - cargo +nightly fmt + cargo fmt [group: 'format'] fmt-check: - cargo +nightly fmt --all -- --check + cargo fmt --all -- --check [group: 'format'] fmt-web: diff --git a/rustfmt.toml b/rustfmt.toml index 8f09c6b..4f292ee 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,4 @@ edition = "2018" -imports_granularity = "One" max_width = 80 newline_style = "Unix" tab_spaces = 2 diff --git a/src/arguments.rs b/src/arguments.rs index e29c48d..d1680ed 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -72,7 +72,7 @@ impl Arguments { })); match parse(&content) { - Ok(ast) => match evaluator.eval(&ast) { + Ok(ast) => match evaluator.evaluate(&ast) { Ok(_) => Ok(()), Err(error) => { error @@ -94,14 +94,14 @@ impl Arguments { } } - fn eval_expression(&self, value: String) -> Result { + fn evaluate_expression(&self, value: String) -> Result { let mut evaluator = Evaluator::from(Environment::new(Config { precision: self.precision, rounding_mode: self.rounding_mode.into(), })); match parse(&value) { - Ok(ast) => match evaluator.eval(&ast) { + Ok(ast) => match evaluator.evaluate(&ast) { Ok(value) => { if let Value::Null = value { return Ok(()); @@ -162,7 +162,7 @@ impl Arguments { let filename = filename.to_string_lossy().to_string(); match parse(content) { - Ok(ast) => match evaluator.eval(&ast) { + Ok(ast) => match evaluator.evaluate(&ast) { Ok(_) => {} Err(error) => { error @@ -194,7 +194,7 @@ impl Arguments { let line: &'static str = Box::leak(line.into_boxed_str()); match parse(line) { - Ok(ast) => match evaluator.eval(&ast) { + Ok(ast) => match evaluator.evaluate(&ast) { Ok(value) if !matches!(value, Value::Null) => println!("{value}"), Ok(_) => {} Err(error) => error @@ -215,7 +215,7 @@ impl Arguments { pub fn run(self) -> Result { match (&self.filename, &self.expression) { (Some(filename), _) => self.eval(filename), - (_, Some(expression)) => self.eval_expression(expression.clone()), + (_, Some(expression)) => self.evaluate_expression(expression.clone()), _ => { #[cfg(not(target_family = "wasm"))] { diff --git a/src/ast.rs b/src/ast.rs index 4547477..dc44b80 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -208,6 +208,26 @@ impl AssignmentTarget<'_> { } } +impl<'a> AssignmentTarget<'a> { + pub(crate) fn indices(&self) -> Vec<&Spanned>> { + match self { + AssignmentTarget::Identifier(_) => Vec::new(), + AssignmentTarget::ListAccess(base, index) => { + let mut indices = base.0.indices(); + indices.push(index); + indices + } + } + } + + pub(crate) fn root(&self, span: Span) -> (&'a str, Span) { + match self { + AssignmentTarget::Identifier(name) => (name, span), + AssignmentTarget::ListAccess(base, _) => base.0.root(base.1), + } + } +} + #[derive(Debug, Clone)] pub enum UnaryOp { Negate, diff --git a/src/completion.rs b/src/completion.rs index 69e5c05..8191b11 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -10,20 +10,8 @@ pub enum Completion<'a> { impl<'a> Completion<'a> { pub(crate) fn unwrap(&self) -> Value<'a> { match self { - Completion::Value(v) | Completion::Return(v) => v.clone(), + Completion::Return(value) | Completion::Value(value) => value.clone(), Completion::Break | Completion::Continue => Value::Null, } } - - pub(crate) fn is_return(&self) -> bool { - matches!(self, Completion::Return(_)) - } - - pub(crate) fn is_break(&self) -> bool { - matches!(self, Completion::Break) - } - - pub(crate) fn is_continue(&self) -> bool { - matches!(self, Completion::Continue) - } } diff --git a/src/evaluator.rs b/src/evaluator.rs index bf4a6de..dd48988 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -24,241 +24,6 @@ fn finite_non_negative_usize(number: f64) -> Option { } impl<'a> Evaluator<'a> { - pub(crate) fn enter_function( - &mut self, - f: impl FnOnce(&mut Self) -> Result, - ) -> Result { - self.context.enter_function(); - let result = f(self); - self.context.exit_function(); - result - } - - fn enter_loop( - &mut self, - f: impl FnOnce(&mut Self) -> Result, - ) -> Result { - self.context.enter_loop(); - let result = f(self); - self.context.exit_loop(); - result - } - - /// # Errors - /// - /// Returns an evaluation error when a statement or expression is invalid. - pub fn eval( - &mut self, - ast: &Spanned>, - ) -> Result, Error> { - let (node, _) = ast; - - match node { - Program::Statements(statements) => { - let mut result = Value::Null; - - for statement in statements { - let completion = self.eval_statement(statement)?; - - result = completion.unwrap(); - - if completion.is_return() - || completion.is_break() - || completion.is_continue() - { - break; - } - } - - Ok(result) - } - } - } - - pub(crate) fn eval_statement( - &mut self, - statement: &Spanned>, - ) -> Result, Error> { - let (node, span) = statement; - - match node { - Statement::Assignment(lhs, rhs) => { - let value = self.eval_expression(rhs)?; - - self.assign(lhs, value.clone())?; - - Ok(Completion::Value(value)) - } - Statement::Block(statements) => { - let mut result = Value::Null; - - for statement in statements { - let completion = self.eval_statement(statement)?; - - result = completion.unwrap(); - - if completion.is_return() - || completion.is_break() - || completion.is_continue() - { - return Ok(completion); - } - } - - Ok(Completion::Value(result)) - } - Statement::Break => { - if !self.context.inside_loop() { - return Err(Error::new( - *span, - "Cannot use 'break' outside of a loop", - )); - } - Ok(Completion::Break) - } - Statement::Continue => { - if !self.context.inside_loop() { - return Err(Error::new( - *span, - "Cannot use 'continue' outside of a loop", - )); - } - - Ok(Completion::Continue) - } - Statement::Expression(expression) => { - Ok(Completion::Value(self.eval_expression(expression)?)) - } - Statement::For(name, iterable, body) => { - let list = self.eval_expression(iterable)?.list(iterable.1)?; - let mut result = Value::Null; - - self.enter_loop(|evaluator| { - for item in list { - evaluator.environment.add_symbol(name, item); - - for statement in body { - let completion = evaluator.eval_statement(statement)?; - - result = completion.unwrap(); - - if completion.is_return() { - return Ok(Completion::Return(result)); - } else if completion.is_break() { - return Ok(Completion::Value(result)); - } else if completion.is_continue() { - break; - } - } - } - - Ok(Completion::Value(result)) - }) - } - Statement::Function(name, params, body) => { - let function = Function::UserDefined { - body: body.clone(), - environment: self.environment.clone(), - name, - parameters: params.clone(), - }; - - self.environment.add_function(name, function.clone()); - - Ok(Completion::Value(Value::Function(function))) - } - Statement::If(condition, then_branch, else_branch) => { - if self.eval_expression(condition)?.boolean(condition.1)? { - let mut result = Value::Null; - - for statement in then_branch { - let completion = self.eval_statement(statement)?; - - result = completion.unwrap(); - - if completion.is_return() - || completion.is_break() - || completion.is_continue() - { - return Ok(completion); - } - } - - Ok(Completion::Value(result)) - } else if let Some(else_statements) = else_branch { - let mut result = Value::Null; - - for statement in else_statements { - let completion = self.eval_statement(statement)?; - - result = completion.unwrap(); - - if completion.is_return() - || completion.is_break() - || completion.is_continue() - { - return Ok(completion); - } - } - - Ok(Completion::Value(result)) - } else { - Ok(Completion::Value(Value::Null)) - } - } - Statement::Loop(body) => self.enter_loop(|evaluator| { - loop { - for statement in body { - let completion = evaluator.eval_statement(statement)?; - - let result = completion.unwrap(); - - if completion.is_return() { - return Ok(Completion::Return(result)); - } else if completion.is_break() { - return Ok(Completion::Value(result)); - } else if completion.is_continue() { - break; - } - } - } - }), - Statement::Return(expr) => { - if !self.context.inside_function() { - return Err(Error::new(*span, "Cannot return outside of a function")); - } - - Ok(Completion::Return(match expr { - Some(expr) => self.eval_expression(expr)?, - None => Value::Null, - })) - } - Statement::While(condition, body) => { - let mut result = Value::Null; - - self.enter_loop(|evaluator| { - while evaluator.eval_expression(condition)?.boolean(condition.1)? { - for statement in body { - let completion = evaluator.eval_statement(statement)?; - - result = completion.unwrap(); - - if completion.is_return() { - return Ok(Completion::Return(result)); - } else if completion.is_break() { - return Ok(Completion::Value(result)); - } else if completion.is_continue() { - break; - } - } - } - - Ok(Completion::Value(result)) - }) - } - } - } - fn assign( &mut self, target: &Spanned>, @@ -270,9 +35,9 @@ impl<'a> Evaluator<'a> { Ok(()) } AssignmentTarget::ListAccess(_, _) => { - let (name, name_span) = Self::assignment_root(target); - let mut indices = Vec::new(); - Self::assignment_indices(target, &mut indices); + let (name, name_span) = target.0.root(target.1); + + let indices = target.0.indices(); let Some(root) = self.environment.resolve_symbol(name) else { return Err(Error::new( @@ -283,30 +48,10 @@ impl<'a> Evaluator<'a> { let root = self.assign_indices(name, root, &indices, value, target.1)?; - self.environment.add_symbol(name, root); - Ok(()) - } - } - } - fn assignment_root( - target: &Spanned>, - ) -> (&'a str, Span) { - match &target.0 { - AssignmentTarget::Identifier(name) => (name, target.1), - AssignmentTarget::ListAccess(base, _) => Self::assignment_root(base), - } - } + self.environment.add_symbol(name, root); - fn assignment_indices<'target>( - target: &'target Spanned>, - indices: &mut Vec<&'target Spanned>>, - ) { - match &target.0 { - AssignmentTarget::Identifier(_) => {} - AssignmentTarget::ListAccess(base, index) => { - Self::assignment_indices(base, indices); - indices.push(index); + Ok(()) } } } @@ -333,7 +78,14 @@ impl<'a> Evaluator<'a> { } }; - let index = self.list_index(index)?; + let index = self + .evaluate_expression(index)? + .number(index.1)? + .to_f64(self.environment.config.rounding_mode) + .and_then(finite_non_negative_usize) + .ok_or_else(|| { + Error::new(index.1, "List index must be a non-negative finite number") + })?; if index >= list.len() { return Err(Error::new( @@ -352,21 +104,58 @@ impl<'a> Evaluator<'a> { Ok(Value::List(list)) } - fn list_index( + pub(crate) fn enter_function( &mut self, - index: &Spanned>, - ) -> Result { - self - .eval_expression(index)? - .number(index.1)? - .to_f64(self.environment.config.rounding_mode) - .and_then(finite_non_negative_usize) - .ok_or_else(|| { - Error::new(index.1, "List index must be a non-negative finite number") - }) + f: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.context.enter_function(); + let result = f(self); + self.context.exit_function(); + result } - fn eval_expression( + fn enter_loop( + &mut self, + f: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.context.enter_loop(); + let result = f(self); + self.context.exit_loop(); + result + } + + /// # Errors + /// + /// Returns an evaluation error when a statement or expression is invalid. + pub fn evaluate( + &mut self, + ast: &Spanned>, + ) -> Result, Error> { + let (node, _) = ast; + + match node { + Program::Statements(statements) => { + let mut result = Value::Null; + + for statement in statements { + let completion = self.evaluate_statement(statement)?; + + result = completion.unwrap(); + + if matches!( + &completion, + Completion::Return(_) | Completion::Break | Completion::Continue + ) { + break; + } + } + + Ok(result) + } + } + } + + fn evaluate_expression( &mut self, ast: &Spanned>, ) -> Result, Error> { @@ -374,8 +163,10 @@ impl<'a> Evaluator<'a> { match node { Expression::BinaryOp(BinaryOp::Add, lhs, rhs) => { - let (lhs_val, rhs_val) = - (self.eval_expression(lhs)?, self.eval_expression(rhs)?); + let (lhs_val, rhs_val) = ( + self.evaluate_expression(lhs)?, + self.evaluate_expression(rhs)?, + ); match (&lhs_val, &rhs_val) { (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a.add( @@ -405,8 +196,10 @@ impl<'a> Evaluator<'a> { } } Expression::BinaryOp(BinaryOp::Divide, lhs, rhs) => { - let (lhs_val, rhs_val) = - (self.eval_expression(lhs)?, self.eval_expression(rhs)?); + let (lhs_val, rhs_val) = ( + self.evaluate_expression(lhs)?, + self.evaluate_expression(rhs)?, + ); let (lhs_num, rhs_num) = (lhs_val.number(lhs.1)?, rhs_val.number(rhs.1)?); @@ -422,7 +215,7 @@ impl<'a> Evaluator<'a> { ))) } Expression::BinaryOp(BinaryOp::Equal, lhs, rhs) => Ok(Value::Boolean( - self.eval_expression(lhs)? == self.eval_expression(rhs)?, + self.evaluate_expression(lhs)? == self.evaluate_expression(rhs)?, )), Expression::BinaryOp( op @ (BinaryOp::LessThan @@ -432,8 +225,10 @@ impl<'a> Evaluator<'a> { lhs, rhs, ) => { - let (lhs_val, rhs_val) = - (self.eval_expression(lhs)?, self.eval_expression(rhs)?); + let (lhs_val, rhs_val) = ( + self.evaluate_expression(lhs)?, + self.evaluate_expression(rhs)?, + ); match (&lhs_val, &rhs_val) { (Value::Number(a), Value::Number(b)) => { @@ -467,19 +262,21 @@ impl<'a> Evaluator<'a> { } Expression::BinaryOp(BinaryOp::LogicalAnd, lhs, rhs) => { Ok(Value::Boolean( - self.eval_expression(lhs)?.boolean(lhs.1)? - && self.eval_expression(rhs)?.boolean(rhs.1)?, + self.evaluate_expression(lhs)?.boolean(lhs.1)? + && self.evaluate_expression(rhs)?.boolean(rhs.1)?, )) } Expression::BinaryOp(BinaryOp::LogicalOr, lhs, rhs) => { Ok(Value::Boolean( - self.eval_expression(lhs)?.boolean(lhs.1)? - || self.eval_expression(rhs)?.boolean(rhs.1)?, + self.evaluate_expression(lhs)?.boolean(lhs.1)? + || self.evaluate_expression(rhs)?.boolean(rhs.1)?, )) } Expression::BinaryOp(BinaryOp::Modulo, lhs, rhs) => { - let (lhs_val, rhs_val) = - (self.eval_expression(lhs)?, self.eval_expression(rhs)?); + let (lhs_val, rhs_val) = ( + self.evaluate_expression(lhs)?, + self.evaluate_expression(rhs)?, + ); let (lhs_num, rhs_num) = (lhs_val.number(lhs.1)?, rhs_val.number(rhs.1)?); @@ -511,18 +308,20 @@ impl<'a> Evaluator<'a> { Ok(Value::Number(remainder)) } Expression::BinaryOp(BinaryOp::Multiply, lhs, rhs) => Ok(Value::Number( - self.eval_expression(lhs)?.number(lhs.1)?.mul( - &self.eval_expression(rhs)?.number(rhs.1)?, + self.evaluate_expression(lhs)?.number(lhs.1)?.mul( + &self.evaluate_expression(rhs)?.number(rhs.1)?, self.environment.config.precision, self.environment.config.rounding_mode, ), )), Expression::BinaryOp(BinaryOp::NotEqual, lhs, rhs) => Ok(Value::Boolean( - self.eval_expression(lhs)? != self.eval_expression(rhs)?, + self.evaluate_expression(lhs)? != self.evaluate_expression(rhs)?, )), Expression::BinaryOp(BinaryOp::Power, lhs, rhs) => { - let (lhs_val, rhs_val) = - (self.eval_expression(lhs)?, self.eval_expression(rhs)?); + let (lhs_val, rhs_val) = ( + self.evaluate_expression(lhs)?, + self.evaluate_expression(rhs)?, + ); let (lhs_num, rhs_num) = (lhs_val.number(lhs.1)?, rhs_val.number(rhs.1)?); @@ -539,8 +338,8 @@ impl<'a> Evaluator<'a> { Ok(Value::Number(result)) } Expression::BinaryOp(BinaryOp::Subtract, lhs, rhs) => Ok(Value::Number( - self.eval_expression(lhs)?.number(lhs.1)?.sub( - &self.eval_expression(rhs)?.number(rhs.1)?, + self.evaluate_expression(lhs)?.number(lhs.1)?.sub( + &self.evaluate_expression(rhs)?.number(rhs.1)?, self.environment.config.precision, self.environment.config.rounding_mode, ), @@ -550,7 +349,7 @@ impl<'a> Evaluator<'a> { let mut evaluated_arguments = Vec::with_capacity(arguments.len()); for argument in arguments { - evaluated_arguments.push(self.eval_expression(argument)?); + evaluated_arguments.push(self.evaluate_expression(argument)?); } self @@ -569,13 +368,13 @@ impl<'a> Evaluator<'a> { let mut evaluated_list = Vec::with_capacity(list.len()); for item in list { - evaluated_list.push(self.eval_expression(item)?); + evaluated_list.push(self.evaluate_expression(item)?); } Ok(Value::List(evaluated_list)) } Expression::ListAccess(list, index) => { - let list_value = self.eval_expression(list)?; + let list_value = self.evaluate_expression(list)?; let list = match &list_value { Value::List(items) => items.clone(), @@ -588,7 +387,7 @@ impl<'a> Evaluator<'a> { }; let Some(index) = self - .eval_expression(index)? + .evaluate_expression(index)? .number(index.1)? .to_f64(self.environment.config.rounding_mode) .and_then(finite_non_negative_usize) @@ -615,11 +414,200 @@ impl<'a> Evaluator<'a> { Expression::Null => Ok(Value::Null), Expression::Number(number) => Ok(Value::Number(number.clone())), Expression::String(string) => Ok(Value::String(string)), - Expression::UnaryOp(UnaryOp::Negate, rhs) => { - Ok(Value::Number(-self.eval_expression(rhs)?.number(rhs.1)?)) + Expression::UnaryOp(UnaryOp::Negate, rhs) => Ok(Value::Number( + -self.evaluate_expression(rhs)?.number(rhs.1)?, + )), + Expression::UnaryOp(UnaryOp::Not, rhs) => Ok(Value::Boolean( + !self.evaluate_expression(rhs)?.boolean(rhs.1)?, + )), + } + } + + pub(crate) fn evaluate_statement( + &mut self, + statement: &Spanned>, + ) -> Result, Error> { + let (node, span) = statement; + + match node { + Statement::Assignment(lhs, rhs) => { + let value = self.evaluate_expression(rhs)?; + + self.assign(lhs, value.clone())?; + + Ok(Completion::Value(value)) + } + Statement::Block(statements) => { + let mut result = Value::Null; + + for statement in statements { + let completion = self.evaluate_statement(statement)?; + + result = completion.unwrap(); + + if matches!( + &completion, + Completion::Return(_) | Completion::Break | Completion::Continue + ) { + return Ok(completion); + } + } + + Ok(Completion::Value(result)) + } + Statement::Break => { + if !self.context.inside_loop() { + return Err(Error::new( + *span, + "Cannot use 'break' outside of a loop", + )); + } + + Ok(Completion::Break) + } + Statement::Continue => { + if !self.context.inside_loop() { + return Err(Error::new( + *span, + "Cannot use 'continue' outside of a loop", + )); + } + + Ok(Completion::Continue) + } + Statement::Expression(expression) => { + Ok(Completion::Value(self.evaluate_expression(expression)?)) } - Expression::UnaryOp(UnaryOp::Not, rhs) => { - Ok(Value::Boolean(!self.eval_expression(rhs)?.boolean(rhs.1)?)) + Statement::For(name, iterable, body) => { + let list = self.evaluate_expression(iterable)?.list(iterable.1)?; + + let mut result = Value::Null; + + self.enter_loop(|evaluator| { + for item in list { + evaluator.environment.add_symbol(name, item); + + for statement in body { + let completion = evaluator.evaluate_statement(statement)?; + + result = completion.unwrap(); + + if matches!(&completion, Completion::Return(_)) { + return Ok(Completion::Return(result)); + } else if matches!(&completion, Completion::Break) { + return Ok(Completion::Value(result)); + } else if matches!(&completion, Completion::Continue) { + break; + } + } + } + + Ok(Completion::Value(result)) + }) + } + Statement::Function(name, params, body) => { + let function = Function::UserDefined { + body: body.clone(), + environment: self.environment.clone(), + name, + parameters: params.clone(), + }; + + self.environment.add_function(name, function.clone()); + + Ok(Completion::Value(Value::Function(function))) + } + Statement::If(condition, then_branch, else_branch) => { + if self.evaluate_expression(condition)?.boolean(condition.1)? { + let mut result = Value::Null; + + for statement in then_branch { + let completion = self.evaluate_statement(statement)?; + + result = completion.unwrap(); + + if matches!( + &completion, + Completion::Return(_) | Completion::Break | Completion::Continue + ) { + return Ok(completion); + } + } + + Ok(Completion::Value(result)) + } else if let Some(else_statements) = else_branch { + let mut result = Value::Null; + + for statement in else_statements { + let completion = self.evaluate_statement(statement)?; + + result = completion.unwrap(); + + if matches!( + &completion, + Completion::Return(_) | Completion::Break | Completion::Continue + ) { + return Ok(completion); + } + } + + Ok(Completion::Value(result)) + } else { + Ok(Completion::Value(Value::Null)) + } + } + Statement::Loop(body) => self.enter_loop(|evaluator| { + loop { + for statement in body { + let completion = evaluator.evaluate_statement(statement)?; + + let result = completion.unwrap(); + + if matches!(&completion, Completion::Return(_)) { + return Ok(Completion::Return(result)); + } else if matches!(&completion, Completion::Break) { + return Ok(Completion::Value(result)); + } else if matches!(&completion, Completion::Continue) { + break; + } + } + } + }), + Statement::Return(expr) => { + if !self.context.inside_function() { + return Err(Error::new(*span, "Cannot return outside of a function")); + } + + Ok(Completion::Return(match expr { + Some(expr) => self.evaluate_expression(expr)?, + None => Value::Null, + })) + } + Statement::While(condition, body) => { + let mut result = Value::Null; + + self.enter_loop(|evaluator| { + while evaluator + .evaluate_expression(condition)? + .boolean(condition.1)? + { + for statement in body { + let completion = evaluator.evaluate_statement(statement)?; + + result = completion.unwrap(); + + if matches!(&completion, Completion::Return(_)) { + return Ok(Completion::Return(result)); + } else if matches!(&completion, Completion::Break) { + return Ok(Completion::Value(result)); + } else if matches!(&completion, Completion::Continue) { + break; + } + } + } + + Ok(Completion::Value(result)) + }) } } } diff --git a/src/function.rs b/src/function.rs index e39579a..1d2c8f3 100644 --- a/src/function.rs +++ b/src/function.rs @@ -60,9 +60,9 @@ impl<'src> Function<'src> { } for statement in body { - let result = evaluator.eval_statement(statement)?; + let result = evaluator.evaluate_statement(statement)?; - if result.is_return() { + if matches!(&result, Completion::Return(_)) { return Ok(result.unwrap()); } } From 151e8a58f56ec6078730dccd922c9f61bb497e93 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 4 May 2026 21:52:34 -0400 Subject: [PATCH 3/4] Tweak --- src/evaluator.rs | 66 +++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/evaluator.rs b/src/evaluator.rs index dd48988..332c3be 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -14,15 +14,6 @@ impl<'a> From> for Evaluator<'a> { } } -fn finite_non_negative_usize(number: f64) -> Option { - if number.is_finite() && number >= 0.0 { - let number = number.trunc(); - format!("{number:.0}").parse().ok() - } else { - None - } -} - impl<'a> Evaluator<'a> { fn assign( &mut self, @@ -82,7 +73,14 @@ impl<'a> Evaluator<'a> { .evaluate_expression(index)? .number(index.1)? .to_f64(self.environment.config.rounding_mode) - .and_then(finite_non_negative_usize) + .and_then(|number| { + if number.is_finite() && number >= 0.0 { + let number = number.trunc(); + format!("{number:.0}").parse::().ok() + } else { + None + } + }) .ok_or_else(|| { Error::new(index.1, "List index must be a non-negative finite number") })?; @@ -390,7 +388,14 @@ impl<'a> Evaluator<'a> { .evaluate_expression(index)? .number(index.1)? .to_f64(self.environment.config.rounding_mode) - .and_then(finite_non_negative_usize) + .and_then(|number| { + if number.is_finite() && number >= 0.0 { + let number = number.trunc(); + format!("{number:.0}").parse::().ok() + } else { + None + } + }) else { return Err(Error::new( index.1, @@ -492,12 +497,11 @@ impl<'a> Evaluator<'a> { result = completion.unwrap(); - if matches!(&completion, Completion::Return(_)) { - return Ok(Completion::Return(result)); - } else if matches!(&completion, Completion::Break) { - return Ok(Completion::Value(result)); - } else if matches!(&completion, Completion::Continue) { - break; + match completion { + Completion::Break => return Ok(Completion::Value(result)), + Completion::Continue => break, + Completion::Return(_) => return Ok(Completion::Return(result)), + Completion::Value(_) => {} } } } @@ -563,23 +567,22 @@ impl<'a> Evaluator<'a> { let result = completion.unwrap(); - if matches!(&completion, Completion::Return(_)) { - return Ok(Completion::Return(result)); - } else if matches!(&completion, Completion::Break) { - return Ok(Completion::Value(result)); - } else if matches!(&completion, Completion::Continue) { - break; + match completion { + Completion::Break => return Ok(Completion::Value(result)), + Completion::Continue => break, + Completion::Return(_) => return Ok(Completion::Return(result)), + Completion::Value(_) => {} } } } }), - Statement::Return(expr) => { + Statement::Return(expression) => { if !self.context.inside_function() { return Err(Error::new(*span, "Cannot return outside of a function")); } - Ok(Completion::Return(match expr { - Some(expr) => self.evaluate_expression(expr)?, + Ok(Completion::Return(match expression { + Some(expression) => self.evaluate_expression(expression)?, None => Value::Null, })) } @@ -596,12 +599,11 @@ impl<'a> Evaluator<'a> { result = completion.unwrap(); - if matches!(&completion, Completion::Return(_)) { - return Ok(Completion::Return(result)); - } else if matches!(&completion, Completion::Break) { - return Ok(Completion::Value(result)); - } else if matches!(&completion, Completion::Continue) { - break; + match completion { + Completion::Break => return Ok(Completion::Value(result)), + Completion::Continue => break, + Completion::Return(_) => return Ok(Completion::Return(result)), + Completion::Value(_) => {} } } } From 79fb8ce3c654bf90ca8918799b24969f46de2ad9 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 4 May 2026 21:55:00 -0400 Subject: [PATCH 4/4] Tweak --- src/evaluator.rs | 4 ++-- src/lib.rs | 51 ++++++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/evaluator.rs b/src/evaluator.rs index 332c3be..be51933 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,8 +1,8 @@ use {super::*, crate::context::Context}; pub struct Evaluator<'a> { - context: Context, - pub environment: Environment<'a>, + pub(crate) context: Context, + pub(crate) environment: Environment<'a>, } impl<'a> From> for Evaluator<'a> { diff --git a/src/lib.rs b/src/lib.rs index 4fa3d2d..f8c7b78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -mod consts; - pub(crate) use { crate::{builtins::BUILTINS, consts::with_consts}, ariadne::{Color, Label, Report, ReportKind, Source}, @@ -34,38 +32,15 @@ pub(crate) use { std::borrow::Cow::{self, Owned}, }; -pub use crate::{ - arguments::Arguments, - ast::{AssignmentTarget, BinaryOp, Expression, Program, Statement, UnaryOp}, - builtin::{Builtin, BuiltinFunction, BuiltinFunctionPayload}, - completion::Completion, - config::Config, - environment::Environment, - error::Error, - evaluator::Evaluator, - float_ext::FloatExt, - function::Function, - parser::parse, - rounding_mode::RoundingMode, - value::Value, -}; - -pub type Span = SimpleSpan; - type Result = std::result::Result; type Spanned = (T, Span); -#[doc(hidden)] -pub mod arguments; - -#[cfg(not(target_family = "wasm"))] -mod highlighter; - mod ast; mod builtin; mod builtins; mod completion; mod config; +mod consts; mod context; mod environment; mod error; @@ -75,3 +50,27 @@ mod function; mod parser; mod rounding_mode; mod value; + +#[doc(hidden)] +pub mod arguments; + +#[cfg(not(target_family = "wasm"))] +mod highlighter; + +pub use crate::{ + arguments::Arguments, + ast::{AssignmentTarget, BinaryOp, Expression, Program, Statement, UnaryOp}, + builtin::{Builtin, BuiltinFunction, BuiltinFunctionPayload}, + completion::Completion, + config::Config, + environment::Environment, + error::Error, + evaluator::Evaluator, + float_ext::FloatExt, + function::Function, + parser::parse, + rounding_mode::RoundingMode, + value::Value, +}; + +pub type Span = SimpleSpan;