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.
Implementing the DebugWithDb
trait
For now, unfortunately, you have to implement the DebugWithDb
trait manually, as we do not provide a derive.
This is tedious but not difficult. Here is an example of implementing the trait for Expression
:
#![allow(unused)] fn main() { impl DebugWithDb<dyn crate::Db + '_> for Expression { fn fmt(&self, f: &mut std::fmt::Formatter<'_>, db: &dyn crate::Db) -> std::fmt::Result { match self.data(db) { ExpressionData::Op(a, b, c) => f .debug_tuple("ExpressionData::Op") .field(&a.debug(db)) // use `a.debug(db)` for interned things .field(&b.debug(db)) .field(&c.debug(db)) .finish(), ExpressionData::Number(a) => { f.debug_tuple("Number") .field(a) // use just `a` otherwise .finish() } ExpressionData::Variable(a) => f.debug_tuple("Variable").field(&a.debug(db)).finish(), ExpressionData::Call(a, b) => f .debug_tuple("Call") .field(&a.debug(db)) .field(&b.debug(db)) .finish(), } } } }
Some things to note:
- The
data
method gives access to the full enum from the database. - The
Formatter
methods (e.g.,debug_tuple
) can be used to provide consistent output. - When printing the value of a field, use
.field(&a.debug(db))
for fields that are themselves interned or entities, and use.field(&a)
for fields that just implement the ordinaryDebug
trait.
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() { impl DebugWithDb<dyn crate::Db + '_> for Op { fn fmt(&self, f: &mut std::fmt::Formatter<'_>, _db: &dyn crate::Db) -> std::fmt::Result { write!(f, "{:?}", self) } } impl DebugWithDb<dyn crate::Db + '_> for Diagnostic { fn fmt(&self, f: &mut std::fmt::Formatter<'_>, _db: &dyn crate::Db) -> std::fmt::Result { write!(f, "{:?}", self) } } #[salsa::tracked] pub struct Function { #[id] name: FunctionId, args: Vec<VariableId>, body: Expression, } #[salsa::accumulator] pub struct Diagnostics(Diagnostic); #[derive(Clone, Debug)] pub struct Diagnostic { pub position: usize, pub message: String, } }
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::debug::DebugWithDb; // Create the database let mut db = crate::db::Database::default(); // Create the source program let source_program = SourceProgram::new(&mut db, source_text.to_string()); // Invoke the parser let statements = parse_statements(&db, source_program); // Read out any diagnostics let accumulated = parse_statements::accumulated::<Diagnostics>(&db, source_program); // Format the result as a string and return it format!("{:#?}", (statements, accumulated).debug(&db)) } }
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#" ( [ ExpressionData::Op( Number( OrderedFloat( 1.0, ), ), Add, Number( OrderedFloat( 2.0, ), ), ), ], [], )"#]]; expected.assert_eq(&actual); } }