82 lines
2.9 KiB
Markdown
82 lines
2.9 KiB
Markdown
|
```
|
||
|
title = "Part 3: Basic Evaluation"
|
||
|
tags = ["build a programming language", "rust"]
|
||
|
date = "2021-04-15 17:00:42 -0400"
|
||
|
short_desc = "A bad calculator."
|
||
|
slug = "evaluation"
|
||
|
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[^1] some code[^2], so let's do that.
|
||
|
|
||
|
[^1]: evaluate
|
||
|
|
||
|
[^2]: a single expression
|
||
|
|
||
|
<!-- 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.
|
||
|
|