3.7 KiB
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.
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.
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:
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.
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.
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 context1 and the main eval
function creates a context before evaluating each statement.
With that, declaration statements can be evaluated just by calling the declare_variable
method on the context:
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 now2:
fn main() {
let code = "let foo = 1; dbg(foo)";
let tokens = tokenize(&code);
let statements = parse(&tokens);
eval(&statements);
}
$ cargo run
Integer(1)