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 a salsa::tracked
type like Program?
The salsa::Database::attach method
Because a tracked type like Program just stores an integer, the traditional Debug trait is not very useful.
To properly print a Program, you need to access the Salsa database to find out what its value is.
To solve this, salsa provides the debug option when declaring tracked structs: #[salsa::tracked(debug)] which
creates an implementation of Debug that can access the values of Program in
the salsa database:
#![allow(unused)] fn main() { #[salsa::tracked(debug)] pub struct Program<'db> { #[tracked] #[returns(ref)] pub statements: Vec<Statement<'db>>, } }
Specifying the debug option allows us to use our types in formatted strings,
but it's not enough to get the full value. Simply writing this code:
#![allow(unused)] fn main() { let db: CalcDatabaseImpl = Default::default(); let surce_text = "print 1 + 2"; let source_program = SourceProgram::new(db, source_text.to_string()); let statements = parse_statements(db, source_program); println!("{:#?}", statements); }
gives us this output:
Program {
[salsa id]: Id(800),
}
And that is because when println! calls Debug::fmt on our statements variable
of type Program, the Debug::fmt implementation has no access to the Salsa database
to inspect the values.
In order to allow Debug::fmt to access the database, we can call it inside a
function passed to Database::attach which sets a thread-local variable to the
Database value it was called on, allowing the debug implementation to access it
and inspect values:
#![allow(unused)] fn main() { let db: CalcDatabaseImpl = Default::default(); db.attach(|db| { let surce_text = "print 1 + 2"; let source_program = SourceProgram::new(db, source_text.to_string()); let statements = parse_statements(db, source_program); println!("{:#?}", statements); }) }
Now we should see something like this:
Program {
[salsa id]: Id(800),
statements: [
Statement {
span: Span {
[salsa id]: Id(404),
start: 0,
end: 11,
},
data: Print(
Expression {
span: Span {
[salsa id]: Id(403),
start: 6,
end: 11,
},
data: Op(
Expression {
span: Span {
[salsa id]: Id(400),
start: 6,
end: 7,
},
data: Number(
1.0,
),
},
Add,
Expression {
span: Span {
[salsa id]: Id(402),
start: 10,
end: 11,
},
data: Number(
2.0,
),
},
),
},
),
},
],
}
Writing the unit test
Now that we know how to inspect all the values of Salsa structs, 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
to get the statements and creates a formatted string with all the values:
#![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; use crate::db::CalcDatabaseImpl; CalcDatabaseImpl::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(800), statements: [ Statement { span: Span { [salsa id]: Id(404), start: 0, end: 11, }, data: Print( Expression { span: Span { [salsa id]: Id(403), start: 6, end: 11, }, data: Op( Expression { span: Span { [salsa id]: Id(400), start: 6, end: 7, }, data: Number( 1.0, ), }, Add, Expression { span: Span { [salsa id]: Id(402), start: 10, end: 11, }, data: Number( 2.0, ), }, ), }, ), }, ], }, [], )"#]]; expected.assert_eq(&actual); } }