Add Part 3: Basic Evaluation

This commit is contained in:
Shadowfacts 2021-04-15 19:39:20 -04:00
parent 8ba029c5a4
commit 9296bb1ddc
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
1 changed files with 76 additions and 0 deletions

View File

@ -0,0 +1,76 @@
```
metadata.title = "Part 3: Basic Evaluation"
metadata.tags = ["build a programming language", "rust"]
metadata.date = "2021-04-15 17:00:42 -0400"
metadata.shortDesc = "A bad calculator."
metadata.slug = "evaluation"
metadata.preamble = `<p style="font-style: italic;">This post is part of a <a href="/build-a-programming-language/" data-link="/build-a-programming-language/">series</a> about learning Rust and building a small programming language.</p><hr>`
```
Last time I said operator precedence was going to be next. Well, if you've read the title, you know that's not the case. I decided I really wanted to see this actually run^[evaluate] some code^[a single expression], so let's do that.
<!-- excerpt-end -->
First, there needs to be something to actually store values during the evaluation process. For this, I used yet another enum. It only has one case for now because we can currently only lex and parse integer values and one arithmetic operator.
```rust
enum Value {
Integer(i64),
}
```
There's also a helper function to extract the underlying integer from a value in places where we're certain it's going to be an integer:
```rust
impl Value {
fn expect_integer(&self, &msg) -> i64 {
match self {
Value::Integer(n) => *n,
_ => panic!("{}", msg),
}
}
}
```
The compiler warns about the unreachable match arm, but it'll be useful once there are more types of values. (Once again, actual error reporting will wait.)
The actual evaulation starts in the `eval` function which takes a reference to the node to evaluate and returns a `Value` representing its result.
For integer nodes, the value of the AST node is wrapped in a Value and returned directly. For binary operator (i.e. addition) nodes the left- and right-hand values are extracted and another function is called to perform the operation.
```rust
fn eval(node: &Node) -> Value {
match node {
Node::Integer(n) => Value::Integer(*n),
Node::BinaryOp { left, right } => eval_binary_op(left, right),
}
}
```
This `eval_binary_op` function takes each of the nodes and calls `eval` with it. By doing this, it recurses through the the AST evaluating each node in a depth-first manner. It then turns each value into an integer (panicking if either isn't what it expects) and returns a new Value with the values added together.
```rust
fn eval_binary_op(left: &Node, right: &Node) -> Value {
let left = eval(left).expect_integer("left hand side of binary operator must be an integer");
let right = eval(right).expect_integer("right hand side of binary operator must be an integer");
Value::Integer(left + right)
}
```
And with that surpisingly small amount of code, I've got a very dumb calculator that can perform arbitrary additions:
```rust
fn main() {
let tokens = tokenize("1 + 2 + 3");
if let Some(node) = parse(tokens) {
println!("result: {:?}", eval(&node));
}
}
```
```sh
$ cargo run
result: Integer(6)
```
Next time, I'll add some more operators and actually get around to operator precedence.