Defining the parser: debug impls and testing
As the final part of the parser, we need to write some tests.
To do so, we will create a database, set the input source text, run the parser, and check the result.
Before we can do that, though, we have to address one question: how do we inspect the value of an interned type like Expression
?
The DebugWithDb
trait
Because an interned type like Expression
just stores an integer, the traditional Debug
trait is not very useful.
To properly print a Expression
, you need to access the Salsa database to find out what its value is.
To solve this, salsa
provides a DebugWithDb
trait that acts like the regular Debug
, but takes a database as argument.
For types that implement this trait, you can invoke the debug
method.
This returns a temporary that implements the ordinary Debug
trait, allowing you to write something like
#![allow(unused)] fn main() { eprintln!("Expression = {:?}", expr.debug(db)); }
and get back the output you expect.
The DebugWithDb
trait is automatically derived for all #[input]
, #[interned]
, and #[tracked]
structs.
Forwarding to the ordinary Debug
trait
For consistency, it is sometimes useful to have a DebugWithDb
implementation even for types, like Op
, that are just ordinary enums. You can do that like so:
#![allow(unused)] fn main() { }
Writing the unit test
Now that we have our DebugWithDb
impls in place, we can write a simple unit test harness.
The parse_string
function below creates a database, sets the source text, and then invokes the parser:
#![allow(unused)] fn main() { /// Create a new database with the given source text and parse the result. /// Returns the statements and the diagnostics generated. #[cfg(test)] fn parse_string(source_text: &str) -> String { use salsa::Database as _; crate::db::Database::default().attach(|db| { // Create the source program let source_program = SourceProgram::new(db, source_text.to_string()); // Invoke the parser let statements = parse_statements(db, source_program); // Read out any diagnostics let accumulated = parse_statements::accumulated::<Diagnostic>(db, source_program); // Format the result as a string and return it format!("{:#?}", (statements, accumulated)) }) } }
Combined with the expect-test
crate, we can then write unit tests like this one:
#![allow(unused)] fn main() { #[test] fn parse_print() { let actual = parse_string("print 1 + 2"); let expected = expect_test::expect![[r#" ( Program { [salsa id]: Id(0), statements: [ Statement { span: Span { [salsa id]: Id(4), start: 0, end: 11, }, data: Print( Expression { span: Span { [salsa id]: Id(3), start: 6, end: 11, }, data: Op( Expression { span: Span { [salsa id]: Id(0), start: 6, end: 7, }, data: Number( OrderedFloat( 1.0, ), ), }, Add, Expression { span: Span { [salsa id]: Id(2), start: 10, end: 11, }, data: Number( OrderedFloat( 2.0, ), ), }, ), }, ), }, ], }, [], )"#]]; expected.assert_eq(&actual); } }