Exercise: Expression Evaluation
Let’s write a simple recursive evaluator for arithmetic expressions. Start with an enum defining the binary operations:
#![allow(unused)] fn main() { /// An operation to perform on two subexpressions. #[derive(Debug)] enum Operation { Add, Sub, Mul, Div, } /// An expression, in tree form. #[derive(Debug)] enum Expression { /// An operation on two subexpressions. Op { op: Operation, left: Box<Expression>, right: Box<Expression>, }, /// A literal value Value(i64), } /// The result of evaluating an expression. #[derive(Debug, PartialEq, Eq)] enum Res { /// Evaluation was successful, with the given result. Ok(i64), /// Evaluation failed, with the given error message. Err(String), } // Allow `Ok` and `Err` as shorthands for `Res::Ok` and `Res::Err`. use Res::{Err, Ok}; fn eval(e: Expression) -> Res { todo!() } #[test] fn test_value() { assert_eq!(eval(Expression::Value(19)), Ok(19)); } #[test] fn test_sum() { assert_eq!( eval(Expression::Op { op: Operation::Add, left: Box::new(Expression::Value(10)), right: Box::new(Expression::Value(20)), }), Ok(30) ); } #[test] fn test_recursion() { let term1 = Expression::Op { op: Operation::Mul, left: Box::new(Expression::Value(10)), right: Box::new(Expression::Value(9)), }; let term2 = Expression::Op { op: Operation::Mul, left: Box::new(Expression::Op { op: Operation::Sub, left: Box::new(Expression::Value(3)), right: Box::new(Expression::Value(4)), }), right: Box::new(Expression::Value(5)), }; assert_eq!( eval(Expression::Op { op: Operation::Add, left: Box::new(term1), right: Box::new(term2), }), Ok(85) ); } #[test] fn test_error() { assert_eq!( eval(Expression::Op { op: Operation::Div, left: Box::new(Expression::Value(99)), right: Box::new(Expression::Value(0)), }), Err(String::from("division by zero")) ); } }
The Box
type here is a smart pointer, and will be covered in detail later in
the course. An expression can be “boxed” with Box::new
as seen in the tests.
To evaluate a boxed expression, use the deref operator to “unbox” it:
eval(*boxed_expr)
.
Some expressions cannot be evaluated and will return an error. The Res
type represents either a successful value or an error with a message. This is
very similar to the standard-library Result
which we will see later.
Copy and paste the code into the Rust playground, and begin implementing
eval
. The final product should pass the tests. It may be helpful to use
todo!()
and get the tests to pass one-by-one. You can also skip a test
temporarily with
#[ignore]
:
#[test]
#[ignore]
fn test_value() { .. }
If you finish early, try writing a test that results in an integer overflow.
How could you handle this with Res::Err
instead of a panic?