shadowfacts.net/site/posts/2021-04-15-evaluation.md

2.9 KiB

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.

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.

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:

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.

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.

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:

fn main() {
	let tokens = tokenize("1 + 2 + 3");
	if let Some(node) = parse(tokens) {
		println!("result: {:?}", eval(&node));
	}
}
$ cargo run
result: Integer(6)

Next time, I'll add some more operators and actually get around to operator precedence.