forked from shadowfacts/shadowfacts.net
Add Part 10: Variable Declarations
This commit is contained in:
parent
0b913d434a
commit
2abf6344d3
|
@ -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 = `<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.
|
Loading…
Reference in New Issue