1. 1. Introduction
  2. 2. 宏,彻底剖析
    1. 2.1. 语法扩展
      1. 2.1.1. 源码解析过程
      2. 2.1.2. AST中的宏
      3. 2.1.3. 展开
    2. 2.2. macro_rules!
    3. 2.3. 细枝末节
      1. 2.3.1. 再探捕获与展开
      2. 2.3.2. 卫生性
      3. 2.3.3. 不是标识符的标识符
      4. 2.3.4. 调试
      5. 2.3.5. 作用域
      6. 2.3.6. 导入/导出
  3. 3. 宏,实践介绍
  4. 4. 常用模式
    1. 4.1. 回调
    2. 4.2. 标记树撕咬机
    3. 4.3. 内用规则
    4. 4.4. 下推累积
    5. 4.5. 重复替代
    6. 4.6. 尾部分隔符
    7. 4.7. 标记树聚束
    8. 4.8. 可见性
    9. 4.9. 临时措施
  5. 5. 轮子
    1. 5.1. AST强转
    2. 5.2. 计数
    3. 5.3. 枚举解析
  6. 6. 实例注解
    1. 6.1. Ook!

展开

展开相对简单。编译器在生成AST之后,对程序进行语义理解之前的某个时间点,将会对所有宏进行展开。

这一过程包括,遍历AST,定位所有宏调用,并将它们用其展开进行替换。在非宏的语法扩展情境中,此过程具体如何发生根据具体情境各有不同。但所有语法扩展在展开完成之后所经历的历程都与宏所经历的相同。

每当编译器遇见一个语法扩展,都会根据上下文决定一个语法元素集。该语法扩展的展开结果应能被顺利解析为集合中的某个元素。举例来说,如果在模组作用域内调用了宏,那么编译器就会尝试将该宏的展开结果解析为一个表示某项条目(item)的AST节点。如果在需要表达式的位置调用了宏,那么编译器就会尝试将该宏的展开结果解析为一个表示表达式的AST节点。

事实上,语义扩展能够被转换成以下任意一种:

换句话讲,宏调用所在的位置,决定了该宏展开之后的结果被解读的方式。

编译器将把AST中表示宏调用的节点用其宏展开的输出节点完全替换。这一替换是结构性(structural)的,而非织构性(textural)的。

举例来说:

let eight = 2 * four!();Run

我们可将这部分AST表示为:

┌─────────────┐
│ Let         │
│ name: eight │   ┌─────────┐
│ init: ◌     │╶─╴│ BinOp   │
└─────────────┘   │ op: Mul │
                ┌╴│ lhs: ◌  │
     ┌────────┐ │ │ rhs: ◌  │╶┐ ┌────────────┐
     │ LitInt │╶┘ └─────────┘ └╴│ Macro      │
     │ val: 2 │                 │ name: four │
     └────────┘                 │ body: ()   │
                                └────────────┘

根据上下文,four!()必须展开成一个表达式 (初始化语句只可能是表达式)。因此,无论实际展开结果如何,它都将被解读成一个完整的表达式。此处我们假设,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);Run

注意到虽然表达式本身不包含括号,我们仍加上了它们。这是因为,编译器总是将宏展开结果作为完整的AST节点对待,而不是仅仅作为一列标记。换句话说,即便不显式地把复杂的表达式用括号包起来,编译器也不可能“错意”宏替换的结果,或者改变求值顺序。

理解这一点——宏展开被当作AST节点看待——非常重要,它表明:

有关展开还有一条值得注意:如果某个语法扩展的展开结果包含了另一次语法扩展调用,那会怎么样?例如,上述four!如果被展开成了1 + three!(),会发生什么?

let x = four!();Run

展开成:

let x = 1 + three!();Run

编译器将会检查扩展结果中是否包含更多的宏调用;如果有,它们将被进一步展开。因此,上述AST节点将被再次展开成:

let x = 1 + 3;Run

此处我们了解到,展开是按“趟”发生的;要多少趟才能完全展开所有调用,那就会展开多少趟。

嗯,也不全是如此。事实上,编译器为此设置了一个上限。它被称作宏递归上限,默认值为32.如果第32次展开结果仍然包含宏调用,编译器将会终止并返回一个递归上限溢出的错误信息。

此上限可通过属性 #![recursion_limit="…"]被改写,但这种改写必须是crate级别的。 一般来讲,可能的话最好还是尽量让宏展开递归次数保持在默认值以下。