1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-03-20 06:21:09 +02:00

Rework the error handling exercise to be based on the expression evaluator exercise (#2521)

This commit is contained in:
Nicole L 2024-12-16 14:36:35 -08:00 committed by GitHub
parent 644f849bf8
commit 4663ec838e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 99 additions and 204 deletions

View File

@ -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<Item=Result<Token, TokenizerError>>` 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}}
```
<detail>
- 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.
</details>

View File

@ -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<Expression>, Op, Box<Expression>),
/// An operation on two subexpressions.
Op { op: Operation, left: Box<Expression>, right: Box<Expression> },
/// A literal value
Value(i64),
}
#[derive(PartialEq, Eq, Debug)]
struct DivideByZeroError;
// ANCHOR_END: types
fn tokenize(input: &str) -> Tokenizer {
return Tokenizer(input.chars().peekable());
}
#[derive(Debug, Error)]
enum TokenizerError {
#[error("Unexpected character '{0}' in input")]
UnexpectedCharacter(char),
}
struct Tokenizer<'a>(Peekable<Chars<'a>>);
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<Token, TokenizerError>;
fn next(&mut self) -> Option<Result<Token, TokenizerError>> {
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<Expression, ParserError> {
let mut tokens = tokenize(input);
fn parse_expr<'a>(
tokens: &mut Tokenizer<'a>,
) -> Result<Expression, ParserError> {
let tok = tokens.next().ok_or(ParserError::UnexpectedEOF)??;
let expr = match tok {
Token::Number(num) => {
let v = num.parse()?;
Expression::Number(v)
/*
// 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::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)),
})
}
Expression::Value(v) => v,
}
parse_expr(&mut tokens)
}
// ANCHOR_END: eval
*/
fn main() -> anyhow::Result<()> {
let expr = parse("10+foo+20-30")?;
println!("{expr:?}");
Ok(())
// ANCHOR: solution
fn eval(e: Expression) -> Result<i64, DivideByZeroError> {
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
/*
// ANCHOR: panics
fn tokenize(input: &str) -> Tokenizer {
return Tokenizer(input.chars().peekable());
}
struct Tokenizer<'a>(Peekable<Chars<'a>>);
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 = Token;
fn next(&mut self) -> Option<Token> {
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}"),
}
}
}
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

View File

@ -1,7 +1,9 @@
# Solution
<!-- compile_fail because `mdbook test` does not allow use of `thiserror` -->
```rust,editable
{{#include exercise.rs:types}}
```rust,editable,compile_fail
{{#include exercise.rs:solution}}
{{#include exercise.rs:tests}}
```