Crafting Interpreters in Rust: Adding nil to the grammar
In the previous post, we wrote a simple grammar that parses true or false as a boolean:
// lox.lalrpop
grammar;
pub Bool: bool = {
"true" => true,
"false" => false,
};
In the book, Lox’s primary rule looks like this:
primary → NUMBER | STRING | "true" | "false" | "nil"
| "(" expression ")" ;
So, ignoring expression for now, and starting with the easiest, let’s add nil.
Introducing Primary
First, though, we need to introduce the Primary type:
// ast.rs
#[derive(Debug)]
pub enum Primary {
Bool(bool)
}
Then we can add it to the grammar, with Bool as one of its variants:
use crate::ast::*;
grammar;
pub Primary: Primary = {
Bool => Primary::Bool(<>)
};
Bool: bool = {
"true" => true,
"false" => false,
};
We also have to change main.rs to look like this:
// main.rs
use lalrpop_util::lalrpop_mod;
lalrpop_mod!(lox);
mod ast;
fn main() {
let parser = lox::PrimaryParser::new();
println!("{:?}", parser.parse("true"));
println!("{:?}", parser.parse("false"));
}
Now when we run it, we get the following:
$ cargo run
Ok(Bool(true))
Ok(Bool(false))
Tests
This ad-hoc testing is making me feel slightly grimy, so I’m going to write some initial tests, and then do the rest of this as TDD.
// main.rs
// ...
#[cfg(test)]
mod tests {
#[test]
fn meh() {
assert!(false)
}
}
Always start with a failing test. I may have taken that a bit literally. This one’s just to make sure that I’m running
cargo test properly:
$ cargo test
test tests::meh ... FAILED
Let’s replace it with something more sensible:
#[cfg(test)]
mod tests {
use crate::*;
use crate::ast::*;
#[test]
fn true_() {
let parser = lox::PrimaryParser::new();
assert_eq!(parser.parse("true").unwrap(), Primary::Bool(true));
}
#[test]
fn false_() {
let parser = lox::PrimaryParser::new();
assert_eq!(parser.parse("false").unwrap(), Primary::Bool(false));
}
}
To get this to compile, we need to add PartialEq to Primary:
#[derive(Debug, PartialEq)]
pub enum Primary {
Bool(bool)
}
Adding nil
Those pass, so we can add a failing test for nil:
#[test]
fn nil() {
let parser = lox::PrimaryParser::new();
assert_eq!(parser.parse("nil").unwrap(), Primary::Nil);
}
That requires us to add Primary::Nil:
#[derive(Debug, PartialEq)]
pub enum Primary {
Nil,
Bool(bool)
}
And we need to update the grammar:
// ...
pub Primary: Primary = {
Nil => Primary::Nil,
Bool => Primary::Bool(<>)
};
Nil = "nil";
// ...
And now the tests pass:
$ cargo test
running 3 tests
test tests::nil ... ok
test tests::false_ ... ok
test tests::true_ ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
We’ve extended the grammar to include the nil literal. In the next post, we’ll look at adding numbers to the grammar.