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