From 4c495126249cc881f5d4df2bac7a0190bf2fc8b4 Mon Sep 17 00:00:00 2001 From: Nick Krichevsky Date: Thu, 16 May 2024 13:02:07 -0400 Subject: [PATCH] Add error handling around evaluation --- src/eval.rs | 238 +++++++++++++++++++++++++++++++++++++++------------ src/lib.rs | 12 +-- src/parse.rs | 3 +- 3 files changed, 190 insertions(+), 63 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index 000f850..389b734 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,8 +1,12 @@ -use std::fmt::{self, Display, Formatter}; +use std::{ + error, + fmt::{self, Display, Formatter}, +}; use crate::{ ast::{Expr, LiteralValue, Visitor}, lex::{Token, TokenKind}, + ScriptError, }; #[derive(Debug, Clone, PartialEq)] @@ -13,6 +17,43 @@ pub enum EvaluatedValue { Nil, } +#[derive(thiserror::Error, Debug)] +#[error("{0}")] +pub struct AssumedTypeError(String); + +macro_rules! impl_value_try_from { + ($output_type: ty, $pattern: pat => $val: expr, $err_msg: literal) => { + impl TryFrom for $output_type { + type Error = AssumedTypeError; + + fn try_from(value: EvaluatedValue) -> Result { + match value { + $pattern => Ok($val), + _ => Err(AssumedTypeError($err_msg.to_string())), + } + } + } + }; +} + +impl_value_try_from!( + f64, + EvaluatedValue::Number(n) => n, + "value is not of type number" +); + +impl_value_try_from!( + String, + EvaluatedValue::String(s) => s, + "value is not of type string" +); + +impl_value_try_from!( + bool, + EvaluatedValue::Boolean(b) => b, + "value is not of type boolean" +); + impl EvaluatedValue { fn is_truthy(&self) -> bool { match self { @@ -49,33 +90,28 @@ impl Display for EvaluatedValue { struct Interpreter; -pub fn evaluate(expr: &Expr) -> EvaluatedValue { +pub fn evaluate(expr: &Expr) -> Result { Interpreter.visit_expr(expr) } -macro_rules! assume_number { - ($value: expr) => { - match $value { - EvaluatedValue::Number(n) => n, - _ => panic!("value was not a number"), - } - }; -} - -impl Visitor for Interpreter { - fn visit_literal(&mut self, value: &LiteralValue) -> EvaluatedValue { - value.into() +impl Visitor> for Interpreter { + fn visit_literal(&mut self, value: &LiteralValue) -> Result { + Ok(value.into()) } - fn visit_grouping(&mut self, expr: &Expr) -> EvaluatedValue { + fn visit_grouping(&mut self, expr: &Expr) -> Result { self.visit_expr(expr) } - fn visit_unary(&mut self, expr: &Expr, operator: &Token) -> EvaluatedValue { - let operand = self.visit_expr(expr); + fn visit_unary( + &mut self, + expr: &Expr, + operator: &Token, + ) -> Result { + let operand = self.visit_expr(expr)?; match operator.kind() { - TokenKind::Minus => EvaluatedValue::Number(-assume_number!(operand)), - TokenKind::Bang => EvaluatedValue::Boolean(operand.is_truthy()), + TokenKind::Minus => evaluate_negation(operand, operator), + TokenKind::Bang => Ok(EvaluatedValue::Boolean(!operand.is_truthy())), _ => unreachable!( "attempted to evaluate non-unary operator as unary operator: {:?}", operator @@ -83,52 +119,71 @@ impl Visitor for Interpreter { } } - fn visit_binary(&mut self, left: &Expr, operator: &Token, right: &Expr) -> EvaluatedValue { - let left_operand = self.visit_expr(left); - let right_operand = self.visit_expr(right); + fn visit_binary( + &mut self, + left: &Expr, + operator: &Token, + right: &Expr, + ) -> Result { + let left_operand = self.visit_expr(left)?; + let right_operand = self.visit_expr(right)?; + match operator.kind() { - TokenKind::Minus => { - EvaluatedValue::Number(assume_number!(left_operand) - assume_number!(right_operand)) - } + TokenKind::Plus => evaluate_addition((left_operand, right_operand), operator), - TokenKind::Slash => { - EvaluatedValue::Number(assume_number!(left_operand) / assume_number!(right_operand)) - } - - TokenKind::Star => { - EvaluatedValue::Number(assume_number!(left_operand) * assume_number!(right_operand)) - } - - TokenKind::Plus => match (left_operand, right_operand) { - (EvaluatedValue::Number(left_num), EvaluatedValue::Number(right_num)) => { - EvaluatedValue::Number(left_num + right_num) - } - - (EvaluatedValue::String(left_str), EvaluatedValue::String(right_str)) => { - EvaluatedValue::String(left_str + right_str.as_ref()) - } - - _ => panic!("incompatible addition types"), // TODO: handle this better - }, - - TokenKind::Greater => EvaluatedValue::Boolean( - assume_number!(left_operand) > assume_number!(right_operand), + TokenKind::Minus => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Number(left_value - right_value), ), - TokenKind::GreaterEqual => EvaluatedValue::Boolean( - assume_number!(left_operand) >= assume_number!(right_operand), + TokenKind::Slash => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Number(left_value / right_value), ), - TokenKind::Less => EvaluatedValue::Boolean( - assume_number!(left_operand) < assume_number!(right_operand), + TokenKind::Star => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Number(left_value * right_value), ), - TokenKind::LessEqual => EvaluatedValue::Boolean( - assume_number!(left_operand) <= assume_number!(right_operand), + TokenKind::Greater => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Boolean(left_value > right_value), ), - TokenKind::EqualEqual => EvaluatedValue::Boolean(left_operand == right_operand), - TokenKind::BangEqual => EvaluatedValue::Boolean(left_operand != right_operand), + TokenKind::GreaterEqual => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Boolean(left_value >= right_value), + ), + + TokenKind::Less => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Boolean(left_value < right_value), + ), + + TokenKind::LessEqual => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Boolean(left_value <= right_value), + ), + + TokenKind::EqualEqual => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Boolean(left_value == right_value), + ), + + TokenKind::BangEqual => evaluate_binary_arithmetic( + (left_operand, right_operand), + operator, + |left_value, right_value| EvaluatedValue::Boolean(left_value != right_value), + ), _ => unreachable!( "attempted to evaluate non-binary operator as binary operator: {:?}", @@ -137,3 +192,76 @@ impl Visitor for Interpreter { } } } + +fn convert_arithmetic_operands( + (left, right): (EvaluatedValue, EvaluatedValue), + operator: &Token, +) -> Result<(f64, f64), ScriptError> { + let make_err = |message: String| ScriptError { + message, + line: operator.line(), + location: String::new(), + }; + match (f64::try_from(left), f64::try_from(right)) { + (Ok(left_val), Ok(right_val)) => Ok((left_val, right_val)), + + (Err(AssumedTypeError(msg)), Ok(_right)) => { + Err(make_err(format!("left operand has invalid type: {msg}"))) + } + + (Ok(_left), Err(AssumedTypeError(msg))) => { + Err(make_err(format!("right operand has invalid type: {msg}"))) + } + + (Err(AssumedTypeError(left_msg)), Err(AssumedTypeError(right_msg))) => Err(make_err( + format!("left and right operand must both both have invalid types; Left: {left_msg}; Right: {right_msg}"), + )), + } +} + +fn evaluate_addition( + (left, right): (EvaluatedValue, EvaluatedValue), + operator: &Token, +) -> Result { + match (left, right) { + (EvaluatedValue::Number(left_value), EvaluatedValue::Number(right_value)) => { + Ok(EvaluatedValue::Number(left_value + right_value)) + } + + (EvaluatedValue::String(left_value), EvaluatedValue::String(right_value)) => { + Ok(EvaluatedValue::String(left_value + right_value.as_ref())) + } + + // TODO: we could improve this error (and others) to include the types + _ => Err(ScriptError { + line: operator.line(), + location: String::new(), + message: format!("left and right operands must be both numbers or both strings"), + }), + } +} + +fn evaluate_binary_arithmetic EvaluatedValue>( + (left_operand, right_operand): (EvaluatedValue, EvaluatedValue), + operator: &Token, + make_output: F, +) -> Result { + let (left_value, right_value) = + convert_arithmetic_operands((left_operand, right_operand), operator)?; + + Ok(make_output(left_value, right_value)) +} + +fn evaluate_negation( + operand: EvaluatedValue, + operator: &Token, +) -> Result { + match operand { + EvaluatedValue::Number(n) => Ok(EvaluatedValue::Number(-n)), + _ => Err(ScriptError { + line: operator.line(), + location: String::new(), + message: format!("operand must be a number"), + }), + } +} diff --git a/src/lib.rs b/src/lib.rs index 52874c7..08d5916 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,13 +47,13 @@ pub fn run(script: &str) -> Result<(), ScriptErrors> { return Err(errors.into()); } - println!( - "{} => {}", - ASTPrinter.visit_expr(&parsed), - eval::evaluate(&parsed) - ); + let eval_res = eval::evaluate(&parsed); + match eval_res { + Ok(ref value) => println!("{} => {}", ASTPrinter.visit_expr(&parsed), value,), + Err(_) => println!("{} => Error", ASTPrinter.visit_expr(&parsed)), + } - Ok(()) + eval_res.map(|_| ()).map_err(|err| ScriptErrors(vec![err])) } struct ASTPrinter; diff --git a/src/parse.rs b/src/parse.rs index 680dd56..13b9554 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -132,8 +132,7 @@ fn parse_primary>(iter: &mut Peekable) -> Result { // special case to avoid cloning - let next_token = iter.next().unwrap(); - match next_token.into_kind() { + match token.into_kind() { TokenKind::String(string) => Ok(Expr::Literal { value: LiteralValue::String(string), }),