TT Lab
开始
学习 学习路径 课程

编译器 — 从头到尾亲手打造一门小语言

词法分析器为什么要记住位置

在 TT Lab 中继续学习

一句话总结

词法分析器把字符序列变成词法单元(token,带有种类的单词)序列,同时滤掉空白和注释,并为每个词法单元记下它来自哪里(行、列)。有了这些,语法分析器看到的就不再是字符,而是有意义的片段,后面所有错误信息也都继承这个位置。

为什么需要它

假设语法分析器直接读取字符。let x=10;、let x = 10 ; 和 let x /* 설명 */ = 10;(注释内容为韩文,意为“说明”) 是同一条语句,但语法分析器的每条规则都得各自带一段跳过空白和注释的代码。每遇到一次 <=,每条规则都要检查“下一个字符是不是 =”,而且每条规则还得小心 letter 是以关键字 let 开头这个事实。

词法分析器把这些事集中到一处。语法分析器看到的只有 let、IDENT(x)、=、INT(10)、;、EOF 六个。此外它还多做一件事——记住位置。能对用户说“第 3 行第 17 列的 @ 不认识”,而不是笼统地说“有地方错了”,是因为最前面的阶段在数字符的时候,把行和列一起数了下来。语法分析器的“这里应该是 ;”、语义分析的“未声明的名字”,乃至运行中的“除以 0”,全都使用同一套坐标系。

工作原理

词法分析器用一个游标把字符扫描一遍。每走一步,先看当前字符,决定从哪种词法单元开始,然后一直吞到这个词法单元结束。

 let  x1 <= 007 ; // 끝
 ^^^  ^^ ^^ ^^^ ^
 let  IDENT  <=  INT  ;   (주석과 공백은 버리고) EOF
 1:2  1:7  1:10 1:13 1:17                        1:23

有四条规则必须遵守。

最长的优先(maximal munch,最长匹配)。能读成 <= 的时候,就不要切成 < 和 =。先检查两个字符的符号,不行再退回到一个字符。名字也一样——letter 要把字母读完之后,再看它是不是关键字。如果先把前三个字符当作 let 切下来,就会变成 let + ter。

列按字符计数。文件是 UTF-8 字节,但人看到的列是字符。在 /* 한글 */ x(注释内是两个韩文字符)中,x 是第 10 个字符,却是第 14 个字节。按字节数,含有韩文注释的那一行里所有错误位置都会错位。跨过换行之后,列回到 1。

错误报告它开始的位置。没有闭合的 /* 要到文件末尾才会被发现,但应该报告的位置是注释开始的地方。如果报告结束位置,用户就会在文件末尾迷路。超出 64 位的整数,同样报告该数字的第一列。

把结尾也当作词法单元。最后总是放一个 EOF 词法单元。这样语法分析器不必另外检查“还有没有词法单元”,而“这里应该是 },文件却结束了”这样的错误,也能准确地标在 EOF 的位置(最后一个字符的正后方)。

只扫描一遍也很重要。如果每次都像 src = src[1:] 那样把剩余字符切下来重新生成,那么每读一个字符就要复制整个文件,文件大小翻倍,时间就会变成四倍。游标应该只是移动一个位置编号。

在现场相遇的样子

下一项实验要做什么

在 lexer.py 中分五块做出 mini 语言的词法分析器——数行和列的游标、跳过空白和注释、整数与名字和关键字、按最长优先切分的符号,以及把它们连起来的 tokenize。评分器会把每个词法单元的行和列都与参考词法分析器对照,并在夹杂韩文的注释、超过 64 位的整数、没有闭合的注释这几种情况下检查连错误文字也一致;最后把输入放大到四倍,测量时间是否线性增长。