距离上次看书已经过了很久了。这次看了书的 2.6 节,也花了很长时间。其实看书没花太久,因为没怎么看懂,反而实现例程用了很长时间,一方面是我没看懂书,另一方面是因为我的 Rust 写得太少了,最后写出来的代码也比较丑陋……

词法分析 (Lexical Analysis) [§2.6]

整个 2.6 节讲的就是如何进行词法分析,也就是怎么实现词法分析器。包括了剔除空白和注释、预读、字面量(书里写的是“常量”,但我感觉用字面量更好)、关键字和标识符。

词法单元 (Token)

这个是比较抽象的概念,就目前接触到的部分,我感觉词法单元就是组成语句或表达式的基本元素,比如:数字、标识符、关键字等。词法单元再分解就是单个字符了,因此我们的程序需要有能力把词法单元给提取出来。

在实现的时候,一个词法单元至少要有两个部分,其一是标签 (Tag),用来表示这是个啥,另一个是就是这个词法单元的内容,可以是值(当其为数字时)、可以是词素 (lexem) (当其为标识符时)。我在改写例程的时候定义了一个名为 Token 的 trait,然后用 Box<dyn Token> 来表示一个词法单元。代码是写出来了,但我不确定是不是用枚举来表示更好。

决定使用 trait 对象的一个原因是,书中的例程把词法单元的标签和内容(值或词素)声明为 public final,大概意思是可读,但初始化后不可写。而 Rust 没有 final 这个东西,我立马就想到了把 "getter" 公开,而不提供 "setter",只在关联函数 new 里初始化,而枚举好像不能这样。

4月5日更新

参考了一个 Rust 写的 c11 词法分析器 c_lexer,用枚举应该是更好的选择,于是我重新写了一遍这节的练习,也更改了其他一些地方,比如书中对于符号的处理,是直接把字符对应的 ASCII 码作为其标签,我参照 c_lexer 给这一节涉及到的符号添加了枚举变体。

剔除空白和注释 [§2.6.1]

对于空白来讲,其它的都好说,遇到就跳过,但换行需要特别注意。首先是换行需要把行号加一,其次是 Windows 的换行是 \r\n 而不是 \n,需要特殊处理(书中没处理)。

剔除注释在书上没给例程,留作练习 2.6.1 了,在我的代码里也丑陋地实现了。

预读 [§2.6.2]

我们读取代码的方式是一个字符一个字符地读,所以在遇到 <=< 这种符号的时候,必须预读下一个字符才能确定,这很好理解。而在实现的时候,我用了 Rust 里的 Peekable 来帮助我在不移动迭代器的情况下读取下一个字符。而书中也讲了,今后会用到一个缓冲技术来实现预读。

4月5日更新

c_lexer 使用了状态机来做预读,我看了看目录,应该会在第三章讲到,现在就还是用老方法来做。不过我稍微改了一下,增加使用回移“指针”来实现一个缓冲区。

字面量 [§2.6.3]

这节接触到的字面量只有无符号整数,在练习 2.6.3 会要求扩展到浮点数,但我还没做。对于无符号整数,词法单元里没有词素,而是直接保存了它的值。在我的代码里是用了一个结构体,实现 Token 这个 trait 来表示数字。

关键字和标识符 [§2.6.4]

书中用了一个散列表来储存所有关键字和用到的标识符。我遇到的问题主要是 HashMap 的键 (K) 以及值 (V) 中的词素 (lexem) 用什么类型。在 Java 里面太好办了,有垃圾回收,用字符串就好了;但 Rust 里如果用字符串切片的引用,就要考虑生命周期和所有权的问题,到底是键持有字符串,还是值里的词素持有字符串呢?我想了半天没想明白,也不知道怎么实现把字符串移动进 HashMap 的键/值的同时,拿到它的引用,并放到同一项的值/键里……最后我放弃使用字符串切片的引用,让所有地方都持有一个字符串了,等以后知识储备够了再来解决。

4月5日更新

在 c_lexer 里用了 internship 里的 IStr 储存标识符的字符串部分,这就相当于有一个全局的 HashSet<Rc<str>>,但只需当作 String 用就行了。如此一来,就不需要用 HashMap 来储存非关键字的标识符,因为其字符串部分已经用 IStr 实现了全局(线程内)唯一。

另一个改变是对于关键字,不用 HashMap,而用了一个完美散列 phfMap。好处是有一个宏 phf_map 可以方便地添加所有关键字,而不需要在初始化时一次次调用 reserve 来储存。

词法分析器 (Lexical Analyzer) [§2.6.5]

英文名也叫 lexer,就是这一节的内容合起来实现的东西。读书的时候我是没读懂,写代码的时候也似懂非懂,写笔记的时候对照代码再顺了几遍稍微有点理解了。

代码

代码在此:lexer.rs

目前代码只做到了例程(书中图 2-34 + 图 2-35)和练习 2.6.1 的内容。之后再继续把练习 2.6.2 和 2.6.3 也做了,就直接更新 gist 了。

4月5日更新

新的代码实现了所有练习,就不放在 gist 了,我搞了个仓库:dragon-book-lexer。跟之前的比起来,多了 lex 方法,可以一次次调用 scan,最终生成词法单元的向量。