为什么我要使用词法分析器而不直接解析代码?

zen*_*net 7 compiler-construction parsing interpreter lexer

我正在尝试从头开始创建一种简单的编程语言(解释器),但我想知道为什么我应该使用词法分析器。对我来说,创建一个直接解析代码的解析器似乎会更容易。我在忽略什么?

Ira*_*ter 7

我想你会同意大多数语言(可能包括你正在实现的语言)都有概念标记:

  • 运算符,例如 *(通常是乘法)、'('、')'、;
  • 关键字,例如“IF”、“GOTO”
  • 标识符,例如 FOO、计数……
  • 数字,例如 0、-527.23E-41
  • 注释,例如 /* 该文本在您的文件中被忽略 */
  • 空白,例如,被忽略的空白、制表符和换行符序列

实际上,需要一段特定的代码来扫描/收集构成每个单独标记的字符。对于您的语言所具有的每种类型的标记,您都需要这样的代码块。

如果您编写一个没有词法分析器的解析器,则在解析器尝试决定接下来发生什么的每个点上,您都必须拥有识别解析中该点可能出现的标记的所有代码。在下一个解析器点,您将需要所有代码来识别那里可能的标记。这会给你带来大量的代码重复;您希望空白代码在解析器中出现多少次?

如果您认为这不是一个好方法,那么明显的解决方法是删除所有重复:将每个标记的代码放置在该标记的子例程中,并在每个解析器位置调用标记的子例程。此时,从某种意义上说,您已经有了一个词法分析器:用于识别标记的独立代码集合。 您可以通过这种方式编写完美的递归下降解析器

您将发现的下一件事是您在每个解析器点调用许多标记的标记子例程。即使这看起来也需要大量的工作和重复。因此,将所有调用替换为单个“GetNextToken”调用,该调用本身调用所有令牌的令牌识别代码,并返回标识遇到的特定令牌的枚举。现在您的解析器开始看起来合理:在每个解析器点,它对 GetNextToken 进行一次调用,然后在返回的枚举上进行分支。这基本上是人们标准化为“词法分析器”的接口。

您会发现的一件事是令牌词法分析器有时会遇到重叠问题;关键字和标识符通常有这个麻烦。实际上,将所有令牌识别器合并到单个有限状态机中更容易,这样就可以更轻松地区分令牌。事实证明,在处理编程语言源文本时,速度也非常快。您的玩具语言可能永远不会解析超过 100 行,但真正的编译器每天处理数百万行代码,其中大部分时间都花在进行标记识别(“词法分析”)上。空白抑制。

您可以手动编写此状态机代码。这并不难,但相当乏味。或者,您可以使用像 FLEX 这样的工具来为您完成此操作,这只是为了方便。随着您的语言中不同种类的标记数量的增加,FLEX 解决方案变得越来越有吸引力。

TLDR:如果您使用词法分析器,您的解析器会更容易编写,并且体积更小。此外,如果您将各个词位编译到状态机中(手动或使用“词法分析器生成器”),它将运行得更快,这一点很重要。