1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-03-20 14:31:15 +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 # Exercise: Rewriting with Result
The following implements a very simple parser for an expression language. In this exercise we're revisiting the expression evaluator exercise that we did
However, it handles errors by panicking. Rewrite it to instead use idiomatic in day 2. Our initial solution ignores a possible error case: Dividing by zero!
error handling and propagate errors to a return from `main`. Feel free to use Rewrite `eval` to instead use idiomatic error handling to handle this error case
[`thiserror`] and [`anyhow`]. and return an error when it occurs. We provide a simple `DivideByZeroError` type
to use as the error type for `eval`.
[`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.
```rust,editable ```rust,editable
{{#include exercise.rs:types}} {{#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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// ANCHOR: solution #![allow(dead_code)]
use thiserror::Error;
// ANCHOR: types // ANCHOR: types
use std::iter::Peekable; /// An operation to perform on two subexpressions.
use std::str::Chars; #[derive(Debug)]
enum Operation {
/// An arithmetic operator.
#[derive(Debug, PartialEq, Clone, Copy)]
enum Op {
Add, Add,
Sub, Sub,
Mul,
Div,
} }
/// A token in the expression language. /// An expression, in tree form.
#[derive(Debug, PartialEq)] #[derive(Debug)]
enum Token {
Number(String),
Identifier(String),
Operator(Op),
}
/// An expression in the expression language.
#[derive(Debug, PartialEq)]
enum Expression { enum Expression {
/// A reference to a variable. /// An operation on two subexpressions.
Var(String), Op { op: Operation, left: Box<Expression>, right: Box<Expression> },
/// A literal number.
Number(u32), /// A literal value
/// A binary operation. Value(i64),
Operation(Box<Expression>, Op, Box<Expression>),
} }
#[derive(PartialEq, Eq, Debug)]
struct DivideByZeroError;
// ANCHOR_END: types // ANCHOR_END: types
fn tokenize(input: &str) -> Tokenizer { /*
return Tokenizer(input.chars().peekable()); // ANCHOR: eval
} // The original implementation of the expression evaluator. Update this to
// return a `Result` and produce an error when dividing by 0.
#[derive(Debug, Error)] fn eval(e: Expression) -> i64 {
enum TokenizerError { match e {
#[error("Unexpected character '{0}' in input")] Expression::Op { op, left, right } => {
UnexpectedCharacter(char), let left = eval(*left);
} let right = eval(*right);
match op {
struct Tokenizer<'a>(Peekable<Chars<'a>>); Operation::Add => left + right,
Operation::Sub => left - right,
impl<'a> Tokenizer<'a> { Operation::Mul => left * right,
fn collect_number(&mut self, first_char: char) -> Token { Operation::Div => if right != 0 {
let mut num = String::from(first_char); left / right
while let Some(&c @ '0'..='9') = self.0.peek() { } else {
num.push(c); panic!("Cannot divide by zero!");
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)
} }
Token::Identifier(ident) => Expression::Var(ident), }
Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)), Expression::Value(v) => v,
};
// 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)
} }
// ANCHOR_END: eval
*/
fn main() -> anyhow::Result<()> { // ANCHOR: solution
let expr = parse("10+foo+20-30")?; fn eval(e: Expression) -> Result<i64, DivideByZeroError> {
println!("{expr:?}"); match e {
Ok(()) 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_END: solution
/* // ANCHOR: tests
// ANCHOR: panics #[test]
fn tokenize(input: &str) -> Tokenizer { fn test_error() {
return Tokenizer(input.chars().peekable()); assert_eq!(
} eval(Expression::Op {
op: Operation::Div,
struct Tokenizer<'a>(Peekable<Chars<'a>>); left: Box::new(Expression::Value(99)),
right: Box::new(Expression::Value(0)),
impl<'a> Tokenizer<'a> { }),
fn collect_number(&mut self, first_char: char) -> Token { Err(DivideByZeroError)
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)
} }
fn main() { fn main() {
let expr = parse("10+foo+20-30"); let expr = Expression::Op {
println!("{expr:?}"); 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 # 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:solution}}
{{#include exercise.rs:tests}}
``` ```