基本结构

在使用 Salsa 之前,让我们先来讨论一下 calc 编译器的基本结构。

Salsa 所设计的一部分内容是,你能够编写感觉“非常接近”正常 Rust 程序的程序。

示例程序

这是我们的示例 calc 程序:

x = 5
y = 10
z = x + y * 3
print z

解析器

calc 编译器接受由字符串表示的程序作为输入:


#![allow(unused)]
fn main() {
struct ProgramSource {
    text: String
}
}

它做的第一件事是将该字符串解析为一系列语句,这些语句看起来类似于下面的伪 Rust 代码1


#![allow(unused)]
fn main() {
enum Statement {
    /// Defines `fn <name>(<args>) = <body>`
    Function(Function),
    /// Defines `print <expr>`
    Print(Expression),
}

/// Defines `fn <name>(<args>) = <body>`
struct Function {
    name: FunctionId,
    args: Vec<VariableId>,
    body: Expression
}
}

其中,表达式类似于这样(伪 Rust 代码,因为 Expression 枚举是递归的):


#![allow(unused)]
fn main() {
enum Expression {
    Op(Expression, Op, Expression),
    Number(f64),
    Variable(VariableId),
    Call(FunctionId, Vec<Expression>),
}

enum Op {
    Add,
    Subtract,
    Multiply,
    Divide,
}
}

最后,对于函数/变量名, FunctionIdVariableId 类型将是被驻留的 (interned) 字符串:


#![allow(unused)]
fn main() {
type FunctionId = /* interned string */;
type VariableId = /* interned string */;
}
1

因为 calc 非常简单,所以我们不必费心将词法分析器 (lexer) 和解析器 (parser) 分开。

检查器

“检查器” (checker) 的任务是确保用户只引用已定义的变量。

我们将以一种“无上下文”的风格编写检查器,这有点不直观,但允许更多的增量复用。

其思想是为给定的表达式计算它引用的变量。然后有一个函数 check,它确保这些变量是已经定义的变量的子集。

翻译器

翻译器 (interpreter) 将执行程序并打印结果。这里不需要太多的增量复用,尽管这当然是可能的。