v6/site/posts/2021-05-09-variable-declara...

114 lines
3.7 KiB
Markdown

```
title = "Part 10: Variable Declarations"
tags = ["build a programming language", "rust"]
date = "2021-05-09 19:14:42 -0400"
slug = "variable-declarations"
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>'
```
Now that the parser can handle multiple statements and the usage of variables, let's add the ability to actually declare variables.
<!-- excerpt-end -->
First off, the lexer now lexes a couple new things: the `let` keyword and the equals sign.
When the parser tries to parse a statement and sees a let token, it knows it's looking at a variable declaration. After the let token it expects to find a identifier (the variable name), an equals sign, and then an expression for the initial value of the variable. The variable name and the initial value expression then make up a `Declare` AST node.
```rust
fn parse_statement<'a, I: Iterator<Item = &'a Token>>(it: &mut Peekable<'a, I>) -> Option<Statement> {
// ...
let node = match token {
Token::Let => {
let name: String;
if let Some(Token::Identifier(s)) = it.peek() {
name = s.clone();
it.next();
} else {
panic!("expected identifier after let");
}
expect_token!(it, Equals, "expected equals after identifier after let");
let value = parse_expression(it).expect("initial value in let statement");
Some(Statement::Declare { name, value })
}
// ...
};
// ...
}
```
`expect_token!` is a simple macro I wrote to handle expecting to see a specific token in the stream and panicking if it's not there, since that's a pattern that was coming up frequently:
```rust
macro_rules! expect_token {
($stream:ident, $token:ident, $msg:expr) => {
if let Some(Token::$token) = $stream.peek() {
$stream.next();
} else {
panic!($msg);
}
};
}
```
Next, to actually evaluate variable declarations, the evaulator needs to have some concept of a context. Right now, every expression can be evaluated without any external state. But, when a variable is declared, we want it to be accessible later on in the code, so there needs to be somewhere to store that information.
For now, the only information we need is the map of variable names to their values.
```rust
struct Context {
variables: HashMap<String, Value>,
}
```
There are also a few methods for `Context`, one to construct a new context and one to declare a variable with an initial value.
```rust
impl Context {
fn new() -> Self {
Self {
variables: HashMap::new(),
}
}
fn declare_variable(&mut self, name: &str, value: Value) {
if self.variables.contains_key(name) {
panic!("cannot re-declare variable {}", name);
} else {
self.variables.insert(name.into(), value);
}
}
}
```
Every `eval_` function has also changed to take a reference to the current context[^1] and the main `eval` function creates a context before evaluating each statement.
[^1]: For now a simple mutable reference is fine, because there's only ever one context: the global one. But, in the future, this will need to be something a bit more complicated.
With that, declaration statements can be evaluated just by calling the `declare_variable` method on the context:
```rust
fn eval_declare_variable(name: &str, value: &Node, context: &mut Context) {
let val = eval_expr(value, context);
context.declare_variable(name, val);
}
```
And we can actually set and read variables now[^2]:
```rust
fn main() {
let code = "let foo = 1; dbg(foo)";
let tokens = tokenize(&code);
let statements = parse(&tokens);
eval(&statements);
}
```
```sh
$ cargo run
Integer(1)
```
[^2]: The `dbg` function is a builtin I added that prints out the Rust version of the `Value` it's passed.