【问题标题】:Why would I use a lexer and not directly parse code?为什么我要使用词法分析器而不是直接解析代码?
【发布时间】:2021-12-23 10:07:02
【问题描述】:

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

【问题讨论】:

  • 对于几乎所有现实世界的语言,无扫描仪解析需要无限的前瞻/回溯。这样就排除了许多解析算法,并且可能无法为您提供所需的性能。如果没有词法分析器,空格处理也会变得更加麻烦。正确区分关键字和标识符也是如此。如果您不想用“意外的字母 'i'; 预期的 'a' 或 'v'”替换“意外的关键字 'if'; 预期的 'as' 或 'var'”,好的错误消息也会变得更难。也就是说,无扫描仪解析是一回事,可能很适合您的用例。
  • 不清楚您所说的“使用词法分析器”是什么意思。如果您没有使用词法分析器生成器(例如 flex),那么您想到的两种选择之间可能没有太大区别。

标签: parsing compiler-construction interpreter lexer


【解决方案1】:

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

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

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

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

如果您认为这不是一个好方法,显而易见的解决方法是删除所有重复项:将每个标记的代码放在该标记的子例程中,并在每个解析器位置调用标记的子例程。此时,从某种意义上说,您已经有了一个词法分析器:用于识别标记的隔离代码集合。 You can code perfectly fine recursive descent parsers this way.

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

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

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

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

【讨论】:

    猜你喜欢
    • 2016-08-07
    • 2014-01-16
    • 1970-01-01
    • 1970-01-01
    • 2012-08-19
    • 1970-01-01
    • 2012-01-31
    • 2012-03-05
    • 1970-01-01
    相关资源
    最近更新 更多