展开
展开相对简单。在生成 AST 之后,和编译器对程序进行语义理解之前,编译器将会对所有语法拓展进行展开。
这一过程包括:遍历 AST,确定所有语法拓展调用的位置,并把它们替换成展开的内容。
每当编译器遇见一个语法扩展,都会根据上下文解析成有限语法元素集中的一个。
举例来说,如果在模块作用域内调用语法拓展,那么编译器就将它的展开结果解析为表示某项条目 (item) 的 AST 节点;如果在表达式的位置上调用语法拓展,那么编译器就将它的展开结果解析为表示表达式的 AST 节点。
事实上,一个语义扩展的展开结果会变成以下一种情况:
- 一个表达式
- 一个模式
- 一个类型
- 零或多个条目(包括的
impl
块) - 零或多个语句
换句话讲,语法拓展调用所在的位置,决定了该语法拓展展开结果被解读的方式。
编译器会操作 AST 节点,把语法拓展调用处的节点完全替换成输出的节点。这一替换是结构性 (structural) 的,而非织构性 (textural) 的。
比如思考以下代码:
let eight = 2 * four!();
我们可将这部分 AST 可视化地表示为:
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────────┐
│ LitInt │╶┘ └─────────┘ └╴│ Macro │
│ val: 2 │ │ name: four │
└────────┘ │ body: () │
└────────────┘
根据上下文,four!()
必须展开成一个表达式(initializer
只可能是表达式)。因此,无论实际展开的结果如何,它都将被解读成一个完整的表达式。
此处假设 four!
成其展开结果为表达式 1 + 3
。故而,展开后将 AST 变为:
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ LitInt │╶┘ └─────────┘ └╴│ BinOp │
│ val: 2 │ │ op: Add │
└────────┘ ┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────┐
│ LitInt │╶┘ └─────────┘ └╴│ LitInt │
│ val: 1 │ │ val: 3 │
└────────┘ └────────┘
这又能被重写成:
let eight = 2 * (1 + 3);
注意,虽然表达式本身不包含括号,但这里仍然加上了它们。这是因为,编译器总是将语法拓展的展开结果看作完整的 AST 节点,而不是仅仅把它视为一列标记。
换句话说,即便不显式地把复杂的表达式用括号包起来,编译器也不可能“错意”语法拓展替换的结果,或者改变求值顺序。
语法拓展被当作 AST 节点展开,这一观点非常重要,它造成两大影响:
- 语法拓展不仅调用位置有限制,其展开结果也只能跟语法解析器在该位置所预期的 AST 节点种类一致。
- 因此,语法拓展必定无法展开成不完整或不合语法的结构。
有关展开还有一点值得注意:如果某语法扩展的展开结果包含另一个语法扩展调用,那会怎么样?
例如,上述 four!
如果被展开成了 1 + three!()
,会发生什么?
let x = four!();
展开成:
let x = 1 + three!();
编译器将会检查扩展结果中是否包含更多的语法拓展调用;如果有,它们将被进一步展开。
因此,上述 AST 节点将被再次展开成:
let x = 1 + 3;
这里的要点是,语法拓展展开发生在“传递”过程中;要完全展开所有调用,就需要同样多的传递。
嗯,也不全是如此。事实上,编译器为此设置了一个上限。它被称作语法拓展的递归上限,默认值为 128。如果第 128 次展开结果仍然包含语法拓展调用,编译器将会终止并返回一个递归上限溢出的错误信息。
此上限可通过 #![recursion_limit="…"]
来提高,但这种改写必须是 crate
级别的。 一般来讲,可能的话最好还是尽量让语法拓展展开递归次数保持在默认值以下,因为会影响编译时间。