use crate::eval::primtype::PrimitiveType; use crate::eval::value::Value; use super::primtype::{UnknownPrimType, ValuePrimitiveTypeError}; /// Errors that can occur running primitive operations in the evaluators. #[derive(Clone, Debug, thiserror::Error)] pub enum PrimOpError { #[error("Math error (underflow or overflow) computing {0} operator")] MathFailure(&'static str), /// This particular variant covers the case in which a primitive /// operator takes two arguments that are supposed to be the same, /// but they differ. (So, like, all the math operators.) #[error("Type mismatch ({1} vs {2}) computing {0} operator")] TypeMismatch(String, Value, Value), /// This variant covers when an operator must take a particular /// type, but the user has provided a different one. #[error("Bad type for operator {0}: {1}")] BadTypeFor(String, Value), /// Probably obvious from the name, but just to be very clear: this /// happens when you pass three arguments to a two argument operator, /// etc. Technically that's a type error of some sort, but we split /// it out. #[error("Illegal number of arguments for {0}: {1} arguments found")] BadArgCount(String, usize), #[error("Unknown primitive operation {0}")] UnknownPrimOp(String), #[error("Unsafe cast from {from} to {to}")] UnsafeCast { from: PrimitiveType, to: PrimitiveType, }, #[error(transparent)] UnknownPrimType(#[from] UnknownPrimType), #[error(transparent)] ValuePrimitiveTypeError(#[from] ValuePrimitiveTypeError), } impl PartialEq> for PrimOpError { fn eq(&self, other: &PrimOpError) -> bool { match (self, other) { (PrimOpError::MathFailure(a), PrimOpError::MathFailure(b)) => a == b, (PrimOpError::TypeMismatch(a, b, c), PrimOpError::TypeMismatch(x, y, z)) => { a == x && b.strip() == y.strip() && c.strip() == z.strip() } (PrimOpError::BadTypeFor(a, b), PrimOpError::BadTypeFor(x, y)) => { a == x && b.strip() == y.strip() } (PrimOpError::BadArgCount(a, b), PrimOpError::BadArgCount(x, y)) => a == x && b == y, (PrimOpError::UnknownPrimOp(a), PrimOpError::UnknownPrimOp(x)) => a == x, ( PrimOpError::UnsafeCast { from: a, to: b }, PrimOpError::UnsafeCast { from: x, to: y }, ) => a == x && b == y, (PrimOpError::UnknownPrimType(a), PrimOpError::UnknownPrimType(x)) => a == x, (PrimOpError::ValuePrimitiveTypeError(a), PrimOpError::ValuePrimitiveTypeError(x)) => { a == x } _ => false, } } } // Implementing primitives in an interpreter like this is *super* tedious, // and the only way to make it even somewhat manageable is to use macros. // This particular macro works for binary operations, and assumes that // you've already worked out that the `calculate` call provided two arguments. // // In those cases, it will rul the operations we know about, and error if // it doesn't. // // This macro then needs to be instantiated for every type, which is super // fun. macro_rules! run_op { ($op: ident, $left: expr, $right: expr) => { match $op { "+" => Ok($left.wrapping_add($right).into()), "-" => Ok($left.wrapping_sub($right).into()), "*" => Ok($left.wrapping_mul($right).into()), "/" if $right == 0 => Err(PrimOpError::MathFailure("/")), "/" => Ok($left.wrapping_div($right).into()), _ => Err(PrimOpError::UnknownPrimOp($op.to_string())), } }; } impl Value { fn unary_op(operation: &str, value: &Value) -> Result, PrimOpError> { match operation { "-" => match value { Value::I8(x) => Ok(Value::I8(x.wrapping_neg())), Value::I16(x) => Ok(Value::I16(x.wrapping_neg())), Value::I32(x) => Ok(Value::I32(x.wrapping_neg())), Value::I64(x) => Ok(Value::I64(x.wrapping_neg())), _ => Err(PrimOpError::BadTypeFor("-".to_string(), value.clone())), }, _ => Err(PrimOpError::BadArgCount(operation.to_owned(), 1)), } } fn binary_op( operation: &str, left: &Value, right: &Value, ) -> Result, PrimOpError> { match left { Value::I8(x) => match right { Value::I8(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::I16(x) => match right { Value::I16(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::I32(x) => match right { Value::I32(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::I64(x) => match right { Value::I64(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::U8(x) => match right { Value::U8(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::U16(x) => match right { Value::U16(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::U32(x) => match right { Value::U32(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::U64(x) => match right { Value::U64(y) => run_op!(operation, x, *y), _ => Err(PrimOpError::TypeMismatch( operation.to_string(), left.clone(), right.clone(), )), }, Value::Closure(_, _, _, _) | Value::Void => { Err(PrimOpError::BadTypeFor(operation.to_string(), left.clone())) } } } /// Calculate the result of running the given primitive on the given arguments. /// /// This can cause errors in a whole mess of ways, so be careful about your /// inputs. For example, addition only works when the two values have the exact /// same type, so expect an error if you try to do so. In addition, this /// implementation catches and raises an error on overflow or underflow, so /// its worth being careful to make sure that your inputs won't cause either /// condition. pub fn calculate( operation: &str, values: Vec>, ) -> Result, PrimOpError> { match values.len() { 1 => Value::unary_op(operation, &values[0]), 2 => Value::binary_op(operation, &values[0], &values[1]), x => Err(PrimOpError::BadArgCount(operation.to_string(), x)), } } }