use crate::eval::value::Value; /// Errors that can occur running primitive operations in the evaluators. #[derive(Clone, Debug, PartialEq, 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(&'static str, 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), } // 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 { "+" => $left .checked_add($right) .ok_or(PrimOpError::MathFailure("+")) .map(Into::into), "-" => $left .checked_sub($right) .ok_or(PrimOpError::MathFailure("-")) .map(Into::into), "*" => $left .checked_mul($right) .ok_or(PrimOpError::MathFailure("*")) .map(Into::into), "/" => $left .checked_div($right) .ok_or(PrimOpError::MathFailure("/")) .map(Into::into), _ => Err(PrimOpError::UnknownPrimOp($op.to_string())), } }; } impl Value { fn binary_op(operation: &str, left: &Value, right: &Value) -> Result { match left { // for now we only have one type, but in the future this is // going to be very irritating. Value::I64(x) => match right { Value::I64(y) => run_op!(operation, x, *y), // _ => Err(PrimOpError::TypeMismatch( // operation.to_string(), // left.clone(), // right.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 { if values.len() == 2 { Value::binary_op(operation, &values[0], &values[1]) } else { Err(PrimOpError::BadArgCount( operation.to_string(), values.len(), )) } } }