源码解析过程

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

  • 标识符(identifiers):foo, Bambous, self, we_can_dance, LaCaravane, …
  • 整数(integers):42, 72u32, 0_______0, …
  • 关键词(keywords):_, fn, self, match, yield, macro, …
  • 生命周期(lifetimes):'a, 'b, 'a_rare_long_lifetime_name, …
  • 字符串(strings):"", "Leicester", r##"venezuelan beaver"##, …
  • 符号(symbols):[, :, ::, ->, @, <-, …

…等等。

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

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

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

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

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

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

  1. #define SUB void
  2. #define BEGIN {
  3. #define END }
  4. SUB main() BEGIN
  5. printf("Oh, the horror!\n");
  6. END

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

  1. ┌─────────┐ ┌─────────┐
  2. BinOp ┌╴│ LitInt
  3. op: Add val: 1
  4. lhs: │╶┘ └─────────┘
  5. rhs: │╶┐ ┌─────────┐
  6. └─────────┘ └╴│ LitInt
  7. val: 2
  8. └─────────┘

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

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

标记树

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

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

  1. a + b + (c + d[0]) + e

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

  1. «a» «+» «b» «+» «( «+» «e»
  2. ╭────────┴──────────╮
  3. «c» «+» «d» «[
  4. ╭─┴─╮
  5. «0»

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

  1. ┌─────────┐
  2. BinOp
  3. op: Add
  4. ┌╴│ lhs:
  5. ┌─────────┐ rhs: │╶┐ ┌─────────┐
  6. Var │╶┘ └─────────┘ └╴│ BinOp
  7. name: a op: Add
  8. └─────────┘ ┌╴│ lhs:
  9. ┌─────────┐ rhs: │╶┐ ┌─────────┐
  10. Var │╶┘ └─────────┘ └╴│ BinOp
  11. name: b op: Add
  12. └─────────┘ ┌╴│ lhs:
  13. ┌─────────┐ rhs: │╶┐ ┌─────────┐
  14. BinOp │╶┘ └─────────┘ └╴│ Var
  15. op: Add name: e
  16. ┌╴│ lhs: └─────────┘
  17. ┌─────────┐ rhs: │╶┐ ┌─────────┐
  18. Var │╶┘ └─────────┘ └╴│ Index
  19. name: c ┌╴│ arr:
  20. └─────────┘ ┌─────────┐ ind: │╶┐ ┌─────────┐
  21. Var │╶┘ └─────────┘ └╴│ LitInt
  22. name: d val: 0
  23. └─────────┘ └─────────┘

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

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