diff --git a/site/posts/2021-05-09-variable-declarations.md b/site/posts/2021-05-09-variable-declarations.md new file mode 100644 index 0000000..a2865db --- /dev/null +++ b/site/posts/2021-05-09-variable-declarations.md @@ -0,0 +1,109 @@ +``` +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 = `

This post is part of a series about learning Rust and building a small programming language.


` +``` + +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. + +```rust +fn parse_statement<'a, I: Iterator>(it: &mut Peekable<'a, I>) -> Option { + // ... + 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, +} +``` + +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.