forked from shadowfacts/shadowfacts.net
110 lines
3.7 KiB
Markdown
110 lines
3.7 KiB
Markdown
|
```
|
||
|
metadata.title = "Part 10: Variable Declarations"
|
||
|
metadata.tags = ["build a programming language", "rust"]
|
||
|
metadata.date = "2021-05-09 19:14:42 -0400"
|
||
|
metadata.shortDesc = ""
|
||
|
metadata.slug = "variable-declarations"
|
||
|
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>`
|
||
|
```
|
||
|
|
||
|
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) {
|
||
|
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.
|