原文在此。
————翻译分隔线————
编译器(6)-标识符
第一部分:介绍
第二部分:编译、转译和解释
第三部分:编译器设计概览
第四部分:语言设计概述
第五部分:Calc 1 语言规格说明书
在本文中,我们终于可以开始沉浸在代码中去了!
标识符
在之前的内容里,我们已经讨论了语法和需要扫描的标识符的集合。我们定义了表达式、数字和运算符。同时还明确期望有成对的括号。还应当让解析器知道,扫描器什么时候到达文件结尾。
在开始扫描之前,为了让扫描器能够工作之前,需要将代码中的标识符格式化。在编译器所涉及的所有阶段都会用到标识符。如果我们希望开发出像 Go 的 fmt 或 vet 这样的程序,还可能需要复用标识符。
这是第一部分的代码:https://github.com/rthornton128/calc/blob/calc1/token/token.go
那些常量开始的时候看起来会挺有趣的。有一些小写字母开头的、非导出的标识符混合在大写字母开头、导出的之间。非导出标识符可以为我们编写工具函数提供帮助,并且允许在不修改其他任何代码的情况下对语言进行扩展。
https://github.com/rthornton128/calc/blob/calc1/token/token.go#L36
接下来将标识符(Token)映射到字符串。还可以使用一个字符串的数组,不过我没这么做。现在这样写查询函数(Lookup)比较容易。
https://github.com/rthornton128/calc/blob/calc1/token/token.go#L50
其余的部分是工具函数。你可以在 IsLiteral 和 IsOperator 中看到我们的非导出常量派上用场的地方。不论要添加多少新的运算符或文法符号,都无须对这些函数进行修改。方便啊!
https://github.com/rthornton128/calc/blob/calc1/token/token.go#L58
Lookup、String 和 Valid 在生成错误信息的时候提供帮助。
位置
这个文件可能需要你花点时间来思考。我将试着慢慢解释给你听。
在扫描的时候,从流中获得第一个字符开始,从上往下、从左往右的的进行。第一个字符的偏移量是零。
相比而言,当用户希望知道汇报出来的错误是发生在哪行哪列的时候,第一个字符应当在第一行,第一列。因此,需要将字符的位置信息翻译为对最终用户有意义的信息。
位置(Pos)是字符的偏移量加上文件的基数。如果基数是一,字符串的偏移是零,这个字符串的 Pos 是一。
位置为零是非法的,因为这意味着文件之外的地方。同样的,如果一个位置大于文件的基数加上文件的长度,那么也是非法的。
为什么要考虑这么复杂的事情呢?好吧,当你需要解析多个文件的时候,在没有很多支持的情况下,要确定错误信息是从哪个文件中产生的是一件很麻烦的事情。Pos 使事情变得简单。在后面的文章中会有更多关于此的介绍。
Position 类型严格用于错误报告。它允许我们输出清晰的关于哪行、哪列,以及哪个文件发生了错误的信息。在这个阶段,我们只需要处理单独的一个文件,但是将来我们会对这段代码感激不禁。
File
严格意义上说,对于编写一个编译器,File 是完全没有必要的,不过为了清晰的错误消息和生死攸关的导入功能,我还是觉得有它比较好。Go 这方面的工作做得不错,不过其他一些编译器就不一定了。例如,GNU C 编译器,通常都很讨厌。不过这几年它还是改进了不少。
这些代码提供了一个一些将要提供的内容的框架。如果哪天我们需要处理多于一个文件时,我们才会用到它。
核心思想本质来说是为了错误报告,并且与位置码紧密相连。再次提示,由于我们只有一个文件(基数为一),或者说开始的位置是一。它不可能更小,不过将来可能会更大一些。因此现在不要去纠结它。
每次,扫描器检测到一个新行的字符,就需要将这个位置添加到文件的行列表中。这样,Position 函数就可以进行计算错误是在哪里发生的,并且报告这个位置。
总结
关于标识符所涵盖的内容大约就这么多。如我所承诺的,不会对相关的代码进行太多的解释。
我建议经常回顾一下这些代码。一旦你明白了它们是怎么协同工作的,它看起来将会有意义得多。这个库将在编译器中大量使用,因此我们将会不断的提到它。
Leave a Reply