Add error handling around evaluation
parent
d80650ddab
commit
4c49512624
238
src/eval.rs
238
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<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"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
12
src/lib.rs
12
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;
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
|
|
Loading…
Reference in New Issue