diff --git a/src/error-handling/exercise.md b/src/error-handling/exercise.md index 84159db3648a..97676776312a 100644 --- a/src/error-handling/exercise.md +++ b/src/error-handling/exercise.md @@ -1,23 +1,27 @@ --- -minutes: 30 +minutes: 20 --- # Exercise: Rewriting with Result -The following implements a very simple parser for an expression language. -However, it handles errors by panicking. Rewrite it to instead use idiomatic -error handling and propagate errors to a return from `main`. Feel free to use -[`thiserror`] and [`anyhow`]. - -[`thiserror`]: https://docs.rs/thiserror -[`anyhow`]: https://docs.rs/anyhow - -> **Hint:** start by fixing error handling in the `parse` function. Once that is -> working correctly, update `Tokenizer` to implement -> `Iterator>` and handle that in the parser. +In this exercise we're revisiting the expression evaluator exercise that we did +in day 2. Our initial solution ignores a possible error case: Dividing by zero! +Rewrite `eval` to instead use idiomatic error handling to handle this error case +and return an error when it occurs. We provide a simple `DivideByZeroError` type +to use as the error type for `eval`. ```rust,editable {{#include exercise.rs:types}} -{{#include exercise.rs:panics}} +{{#include exercise.rs:eval}} + +{{#include exercise.rs:tests}} ``` + + + +- The starting code here isn't exactly the same as the previous exercise's + solution: We've added in an explicit panic to show students where the error + case is. Point this out if students get confused. + + diff --git a/src/error-handling/exercise.rs b/src/error-handling/exercise.rs index 62aac85eced1..395773806141 100644 --- a/src/error-handling/exercise.rs +++ b/src/error-handling/exercise.rs @@ -12,212 +12,101 @@ // See the License for the specific language governing permissions and // limitations under the License. -// ANCHOR: solution -use thiserror::Error; +#![allow(dead_code)] // ANCHOR: types -use std::iter::Peekable; -use std::str::Chars; - -/// An arithmetic operator. -#[derive(Debug, PartialEq, Clone, Copy)] -enum Op { +/// An operation to perform on two subexpressions. +#[derive(Debug)] +enum Operation { Add, Sub, + Mul, + Div, } -/// A token in the expression language. -#[derive(Debug, PartialEq)] -enum Token { - Number(String), - Identifier(String), - Operator(Op), -} - -/// An expression in the expression language. -#[derive(Debug, PartialEq)] +/// An expression, in tree form. +#[derive(Debug)] enum Expression { - /// A reference to a variable. - Var(String), - /// A literal number. - Number(u32), - /// A binary operation. - Operation(Box, Op, Box), -} -// ANCHOR_END: types + /// An operation on two subexpressions. + Op { op: Operation, left: Box, right: Box }, -fn tokenize(input: &str) -> Tokenizer { - return Tokenizer(input.chars().peekable()); + /// A literal value + Value(i64), } -#[derive(Debug, Error)] -enum TokenizerError { - #[error("Unexpected character '{0}' in input")] - UnexpectedCharacter(char), -} - -struct Tokenizer<'a>(Peekable>); - -impl<'a> Tokenizer<'a> { - fn collect_number(&mut self, first_char: char) -> Token { - let mut num = String::from(first_char); - while let Some(&c @ '0'..='9') = self.0.peek() { - num.push(c); - self.0.next(); - } - Token::Number(num) - } - - fn collect_identifier(&mut self, first_char: char) -> Token { - let mut ident = String::from(first_char); - while let Some(&c @ ('a'..='z' | '_' | '0'..='9')) = self.0.peek() { - ident.push(c); - self.0.next(); - } - Token::Identifier(ident) - } -} - -impl<'a> Iterator for Tokenizer<'a> { - type Item = Result; - - fn next(&mut self) -> Option> { - let c = self.0.next()?; - match c { - '0'..='9' => Some(Ok(self.collect_number(c))), - 'a'..='z' | '_' => Some(Ok(self.collect_identifier(c))), - '+' => Some(Ok(Token::Operator(Op::Add))), - '-' => Some(Ok(Token::Operator(Op::Sub))), - _ => Some(Err(TokenizerError::UnexpectedCharacter(c))), - } - } -} - -#[derive(Debug, Error)] -enum ParserError { - #[error("Tokenizer error: {0}")] - TokenizerError(#[from] TokenizerError), - #[error("Unexpected end of input")] - UnexpectedEOF, - #[error("Unexpected token {0:?}")] - UnexpectedToken(Token), - #[error("Invalid number")] - InvalidNumber(#[from] std::num::ParseIntError), -} - -fn parse(input: &str) -> Result { - let mut tokens = tokenize(input); - - fn parse_expr<'a>( - tokens: &mut Tokenizer<'a>, - ) -> Result { - let tok = tokens.next().ok_or(ParserError::UnexpectedEOF)??; - let expr = match tok { - Token::Number(num) => { - let v = num.parse()?; - Expression::Number(v) - } - Token::Identifier(ident) => Expression::Var(ident), - Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)), - }; - // Look ahead to parse a binary operation if present. - Ok(match tokens.next() { - None => expr, - Some(Ok(Token::Operator(op))) => Expression::Operation( - Box::new(expr), - op, - Box::new(parse_expr(tokens)?), - ), - Some(Err(e)) => return Err(e.into()), - Some(Ok(tok)) => return Err(ParserError::UnexpectedToken(tok)), - }) - } - - parse_expr(&mut tokens) -} - -fn main() -> anyhow::Result<()> { - let expr = parse("10+foo+20-30")?; - println!("{expr:?}"); - Ok(()) -} -// ANCHOR_END: solution +#[derive(PartialEq, Eq, Debug)] +struct DivideByZeroError; +// ANCHOR_END: types /* -// ANCHOR: panics -fn tokenize(input: &str) -> Tokenizer { - return Tokenizer(input.chars().peekable()); -} - -struct Tokenizer<'a>(Peekable>); - -impl<'a> Tokenizer<'a> { - fn collect_number(&mut self, first_char: char) -> Token { - let mut num = String::from(first_char); - while let Some(&c @ '0'..='9') = self.0.peek() { - num.push(c); - self.0.next(); - } - Token::Number(num) - } - - fn collect_identifier(&mut self, first_char: char) -> Token { - let mut ident = String::from(first_char); - while let Some(&c @ ('a'..='z' | '_' | '0'..='9')) = self.0.peek() { - ident.push(c); - self.0.next(); +// ANCHOR: eval +// The original implementation of the expression evaluator. Update this to +// return a `Result` and produce an error when dividing by 0. +fn eval(e: Expression) -> i64 { + match e { + Expression::Op { op, left, right } => { + let left = eval(*left); + let right = eval(*right); + match op { + Operation::Add => left + right, + Operation::Sub => left - right, + Operation::Mul => left * right, + Operation::Div => if right != 0 { + left / right + } else { + panic!("Cannot divide by zero!"); + }, + } } - Token::Identifier(ident) + Expression::Value(v) => v, } } +// ANCHOR_END: eval +*/ -impl<'a> Iterator for Tokenizer<'a> { - type Item = Token; - - fn next(&mut self) -> Option { - let c = self.0.next()?; - match c { - '0'..='9' => Some(self.collect_number(c)), - 'a'..='z' => Some(self.collect_identifier(c)), - '+' => Some(Token::Operator(Op::Add)), - '-' => Some(Token::Operator(Op::Sub)), - _ => panic!("Unexpected character {c}"), +// ANCHOR: solution +fn eval(e: Expression) -> Result { + match e { + Expression::Op { op, left, right } => { + let left = eval(*left)?; + let right = eval(*right)?; + Ok(match op { + Operation::Add => left + right, + Operation::Sub => left - right, + Operation::Mul => left * right, + Operation::Div => { + if right == 0 { + return Err(DivideByZeroError); + } else { + left / right + } + } + }) } + Expression::Value(v) => Ok(v), } } +// ANCHOR_END: solution -fn parse(input: &str) -> Expression { - let mut tokens = tokenize(input); - - fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Expression { - let Some(tok) = tokens.next() else { - panic!("Unexpected end of input"); - }; - let expr = match tok { - Token::Number(num) => { - let v = num.parse().expect("Invalid 32-bit integer"); - Expression::Number(v) - } - Token::Identifier(ident) => Expression::Var(ident), - Token::Operator(_) => panic!("Unexpected token {tok:?}"), - }; - // Look ahead to parse a binary operation if present. - match tokens.next() { - None => expr, - Some(Token::Operator(op)) => Expression::Operation( - Box::new(expr), - op, - Box::new(parse_expr(tokens)), - ), - Some(tok) => panic!("Unexpected token {tok:?}"), - } - } - - parse_expr(&mut tokens) +// ANCHOR: tests +#[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(DivideByZeroError) + ); } fn main() { - let expr = parse("10+foo+20-30"); - println!("{expr:?}"); + let expr = Expression::Op { + op: Operation::Sub, + left: Box::new(Expression::Value(20)), + right: Box::new(Expression::Value(10)), + }; + println!("expr: {expr:?}"); + println!("result: {:?}", eval(expr)); } -// ANCHOR_END: panics -*/ +// ANCHOR_END: tests diff --git a/src/error-handling/solution.md b/src/error-handling/solution.md index 2dc2bc586209..10463390b50a 100644 --- a/src/error-handling/solution.md +++ b/src/error-handling/solution.md @@ -1,7 +1,9 @@ # Solution - +```rust,editable +{{#include exercise.rs:types}} -```rust,editable,compile_fail {{#include exercise.rs:solution}} + +{{#include exercise.rs:tests}} ```