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);
}
}