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:
parent
644f849bf8
commit
4663ec838e
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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}}
|
||||
```
|
||||
|
Loading…
x
Reference in New Issue
Block a user