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!

源码解析过程

Rust程序编译过程的第一阶段是标记解析(tokenization)。在这一过程中,源代码将被转换成一系列的标记(token,即无法被分割的词法单元;在编程语言世界中等价于“单词”)。Rust包含多种标记,比如:

…等等。

上面的叙述中有些地方值得注意。

首先,self既是一个标识符又是一个关键词。几乎在所有情况下它都被视作是一个关键词,但它有可能被视为标识符。我们稍后会(带着咒骂)提到这种情况。

其次,关键词里列有一些可疑的家伙,比如yieldmacro。它们在当前的Rust语言中并没有任何含义,但编译器的确会把它们视作关键词进行解析。这些词语被保留作语言未来扩充时使用。

第三,符号里列有一些未被当前语言使用的条目。比如<-,这是历史残留:目前它被移除了Rust语法,但词法分析器仍然没丢掉它。

最后,注意::被视作一个独立的标记,而非两个连续的:。这一规则适用于Rust中所有的多字符符号标记。1

作为对比,某些语言的宏系统正扎根于这一阶段。Rust并非如此。举例来说,从效果来看,C/C++的宏就是在这里得到处理的。2这也正是下列代码能够运行的原因:3

#define SUB void
#define BEGIN {
#define END }

SUB main() BEGIN
    printf("Oh, the horror!\n");
END

编译过程的下一个阶段是语法解析(parsing)。这一过程中,一系列的标记将被转换成一棵抽象语法树(Abstract Syntax Tree, AST)。此过程将在内存中建立起程序的语法结构。举例来说,标记序列1+2将被转换成某种类似于:

┌─────────┐   ┌─────────┐
│ BinOp   │ ┌╴│ LitInt  │
│ op: Add │ │ │ val: 1  │
│ lhs: ◌  │╶┘ └─────────┘
│ rhs: ◌  │╶┐ ┌─────────┐
└─────────┘ └╴│ LitInt  │
              │ val: 2  │
              └─────────┘

的东西。生成出的AST将包含整个程序的结构,但这一结构仅包含词法信息。举例来讲,在这个阶段编译器虽然可能知道某个表达式提及了某个名为a的变量,但它并没有办法知道a究竟是什么,或者它在哪儿。

在AST生成之后,宏处理过程才开始。但在讨论宏处理过程之前,我们需要先谈谈标记树(token tree)。

标记树

标记树是介于标记与AST之间的东西。首先明确一点,几乎所有标记都构成标记树。具体来说,它们可被看作标记树叶节点。另有一类存在可被看作标记树叶节点,我们将在稍后提到它。

只有一种基础标记不是标记树叶节点,“分组”标记:(...)[...]{...}。这三者属于标记树内节点, 正是它们给标记树带来了树状的结构。给个具体的例子,这列标记:

a + b + (c + d[0]) + eRun

将被转换为这样的标记树:

«a» «+» «b» «+» «(   )» «+» «e»
          ╭────────┴──────────╮
           «c» «+» «d» «[   ]»
                        ╭─┴─╮
                         «0»

注意它跟最后生成的AST并没有关联。AST将仅有一个根节点,而这棵标记树有九(原文如此)个。作为参照,最后生成的AST应该是这样:

              ┌─────────┐
              │ BinOp   │
              │ op: Add │
            ┌╴│ lhs: ◌  │
┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
│ Var     │╶┘ └─────────┘ └╴│ BinOp   │
│ name: a │                 │ op: Add │
└─────────┘               ┌╴│ lhs: ◌  │
              ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
              │ Var     │╶┘ └─────────┘ └╴│ BinOp   │
              │ name: b │                 │ op: Add │
              └─────────┘               ┌╴│ lhs: ◌  │
                            ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
                            │ BinOp   │╶┘ └─────────┘ └╴│ Var     │
                            │ op: Add │                 │ name: e │
                          ┌╴│ lhs: ◌  │                 └─────────┘
              ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
              │ Var     │╶┘ └─────────┘ └╴│ Index   │
              │ name: c │               ┌╴│ arr: ◌  │
              └─────────┘   ┌─────────┐ │ │ ind: ◌  │╶┐ ┌─────────┐
                            │ Var     │╶┘ └─────────┘ └╴│ LitInt  │
                            │ name: d │                 │ val: 0  │
                            └─────────┘                 └─────────┘

理解AST与标记树间的区别至关重要。写宏时,你将同时与这两者打交道。

还有一条需要注意:不可能出现不匹配的小/中/大括号,也不可能存在包含错误嵌套结构的标记树。


  1. @是有意义的,虽然大多数人好像都已经完全不记得它了。它用于在模式中把某个模式的非终结部分绑定给一个名称。甚至一位Rust核心团队的成员,在审阅这一章节并特意提起这一小节时,都没记起它的含义。可怜,真是可怜。 

  2. 事实上,C的预处理器采用了跟C语言本身不同的词法结构。但总体来说个中区别并不相干。 

  3. 它是不是真该奏效又是另外一码事了。