源码解析过程
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
既是一个标识符又是一个关键词。几乎在所有情况下它都被视作是一个关键词,但它有可能被视为标识符。我们稍后会(带着咒骂)提到这种情况。
其次,关键词里列有一些可疑的家伙,比如yield
和macro
。它们在当前的Rust语言中并没有任何含义,但编译器的确会把它们视作关键词进行解析。这些词语被保留作语言未来扩充时使用。
第三,符号里也列有一些未被当前语言使用的条目。比如<-
,这是历史残留:目前它被移除了Rust语法,但词法分析器仍然没丢掉它。
最后,注意::
被视作一个独立的标记,而非两个连续的:
。这一规则适用于Rust中所有的多字符符号标记。^逝去的@
作为对比,某些语言的宏系统正扎根于这一阶段。Rust并非如此。举例来说,从效果来看,C/C++的宏就是在这里得到处理的。^其实不是这也正是下列代码能够运行的原因:^这看起来不错
#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]) + e
将被转换为这样的标记树:
«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与标记树间的区别至关重要。写宏时,你将同时与这两者打交道。
还有一条需要注意:不可能出现不匹配的小/中/大括号,也不可能存在包含错误嵌套结构的标记树。