定义解析器:Debug 实现与测试

作为解析器部分的最后一步,我们需要编写一些测试。 为此,我们会创建一个数据库,设置输入源文本,运行解析器并检查结果。 不过在这之前,还有一个问题必须先解决: 我们要如何查看像 Expression 这种 interned 类型所代表的值?

DebugWithDb trait

Expression 这种 interned 类型本质上只存了一个整数, 所以传统的 Debug trait 并不怎么有用。 要正确打印一个 Expression, 你需要访问 Salsa 数据库,才能知道这个整数实际对应的值是什么。

为了解决这个问题,salsa 提供了 DebugWithDb trait。 它的行为类似普通的 Debug, 但额外接收一个数据库参数。 对实现了这个 trait 的类型,你可以调用 debug 方法。 它会返回一个临时值, 这个临时值实现了普通的 Debug trait, 于是你就可以写出下面这样的代码:

#![allow(unused)]
fn main() {
eprintln!("Expression = {:?}", expr.debug(db));
}

并得到你真正想看到的输出。

DebugWithDb 会自动为所有 #[input]#[interned]#[tracked] 结构体派生。

转发到普通 Debug trait

为了保持一致性, 即使对于像 Op 这种普通枚举, 有时也希望它实现 DebugWithDb。 你可以像下面这样做:

#![allow(unused)]

fn main() {
}

编写单元测试

现在 DebugWithDb 的实现已经准备好了, 我们就可以写一个简单的单元测试辅助函数。 下面的 parse_string 会创建一个数据库、设置源文本,然后调用解析器:

#![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))
    })
}
}

再配合 expect-test crate, 我们就能写出这样的单元测试:

#![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);
}
}