[翻译]编译器(6)-标识符

原文在此
————翻译分隔线————

编译器(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 函数就可以进行计算错误是在哪里发生的,并且报告这个位置。

总结

关于标识符所涵盖的内容大约就这么多。如我所承诺的,不会对相关的代码进行太多的解释。

我建议经常回顾一下这些代码。一旦你明白了它们是怎么协同工作的,它看起来将会有意义得多。这个库将在编译器中大量使用,因此我们将会不断的提到它。

Join the Conversation

5 Comments

  1. “这个文件可能需要你花点时间来思考。”中“这个文件”应该有一个超链接

Leave a comment

Your email address will not be published. Required fields are marked *