Add error handling around evaluation

master
Nick Krichevsky 2024-05-16 13:02:07 -04:00
parent d80650ddab
commit 4c49512624
3 changed files with 190 additions and 63 deletions

View File

@ -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<EvaluatedValue> for $output_type {
type Error = AssumedTypeError;
fn try_from(value: EvaluatedValue) -> Result<Self, Self::Error> {
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<EvaluatedValue, ScriptError> {
Interpreter.visit_expr(expr)
}
macro_rules! assume_number {
($value: expr) => {
match $value {
EvaluatedValue::Number(n) => n,
_ => panic!("value was not a number"),
}
};
}
impl Visitor<EvaluatedValue> for Interpreter {
fn visit_literal(&mut self, value: &LiteralValue) -> EvaluatedValue {
value.into()
impl Visitor<Result<EvaluatedValue, ScriptError>> for Interpreter {
fn visit_literal(&mut self, value: &LiteralValue) -> Result<EvaluatedValue, ScriptError> {
Ok(value.into())
}
fn visit_grouping(&mut self, expr: &Expr) -> EvaluatedValue {
fn visit_grouping(&mut self, expr: &Expr) -> Result<EvaluatedValue, ScriptError> {
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<EvaluatedValue, ScriptError> {
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<EvaluatedValue> 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<EvaluatedValue, ScriptError> {
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<EvaluatedValue> 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<EvaluatedValue, ScriptError> {
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<F: FnOnce(f64, f64) -> EvaluatedValue>(
(left_operand, right_operand): (EvaluatedValue, EvaluatedValue),
operator: &Token,
make_output: F,
) -> Result<EvaluatedValue, ScriptError> {
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<EvaluatedValue, ScriptError> {
match operand {
EvaluatedValue::Number(n) => Ok(EvaluatedValue::Number(-n)),
_ => Err(ScriptError {
line: operator.line(),
location: String::new(),
message: format!("operand must be a number"),
}),
}
}

View File

@ -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;

View File

@ -132,8 +132,7 @@ fn parse_primary<I: Iterator<Item = Token>>(iter: &mut Peekable<I>) -> Result<Ex
}),
TokenKind::String(_) => {
// 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),
}),