创建词法分析器最有效的方法是什么?

时间:2018-08-27 18:13:21

标签: compiler-construction lexer finite-automata

我目前正在尝试学习如何手工创建自己的词法分析器。我一直在使用Flex(以及Bison)进行练习和学习它在内部的工作方式,但是我目前至少看到3种不同的解决方案来开发自己的解决方案。

  1. 使用RE列表,遍历每个RE并匹配,只需返回关联的令牌(请参阅有关RE的python文档)
  2. 从RE创建DFA(例如,Flex也是如此:基于RE,创建大型状态机)
  3. 使用大量开关案例或if语句创建我自己的“状态机”(例如,我认为Lua会这样做)

我有信心可以尝试每种解决方案,但是:

  • 是否存在一种解决方案无法解决的情况?
  • 在哪种情况下,您将使用一种解决方案而不是另一种解决方案?
  • 正如标题所说:哪个代码产生的效率最高?

谢谢!

1 个答案:

答案 0 :(得分:5)

当且仅当您设法编写状态机和弹性词法描述而没有任何错误时,第二和第三种选择才等效。 YMMV,但我的经验是,编写(和阅读)弹性词法描述要容易得多。

第一种选择可能并不等效,并且在一般情况下使其等效也并非易事。

问题是,如果多个模式匹配正则表达式,将会发生什么。 (在如上所述编写大量switch语句时,此问题也导致了细微的错误。)在这种情况下,通常接受的词汇策略是使用"maximal munch"规则:选择导致最长匹配的模式,如果存在不止一个这样的模式,请选择在词汇定义中最先出现的模式。

作为此规则为何很重要的简单示例,请考虑一种具有关键字dodouble的语言。观察到理想的行为是:

do {         => First token is keyword do
double d;    => First token is keyword double
doubt = 0.9; => First token is identifier doubt

在标准(f)lex文件中,该文件应实现为:

"do"       {  return T_FOR; }
"double"   {  return T_FOREACH; }
[[:alpha:]_][[:alnum:]_]*  { yyval.str = strdup(yytext); return T_ID; }
如果前两个规则碰巧顺序不同,

(F)lex将产生完全相同的扫描仪,尽管第三个规则肯定必须在末尾。但是,能够对前两个规则进行重新排序就不容易出错。当然,如上所述,有些人会使用字母顺序将关键字写成词法规则,但其他人可能选择通过语法功能来组织关键字,以便将dofor,{{ 1}},while等,以及donedoubleint等。对于后一种组织,程序员将很难确保重叠关键字以任何特定顺序出现,因此flex无关紧要;在这种情况下(与许多其他情况一样),选择最长的匹配肯定是正确的。

如果创建正则表达式列表并仅选择第一个匹配项,则需要确保正则表达式在匹配长度方面是相反的,以便与最长的关键字匹配的正则表达式排在最前面。 (这会将char放在double之前,因此按字母顺序排列的关键字将失败。)

更糟糕的是,哪个正则表达式具有最长的匹配可能不是立即显而易见的。关键字很明显-您可以按长度对文字模式进行反向排序-但在一般情况下,最大的munch规则可能不是正则表达式的部分排序:对于某些令牌,可能是一个正则表达式表达式具有最长的匹配,而另一个正则表达式为不同的标记提供了更长的匹配。

或者,您可以尝试 all 正则表达式,并跟踪匹配时间最长的正则表达式。这样可以正确实现最大的耗时(但请参见下文),但是效率更低,因为每个模式都必须与每个令牌匹配。

您链接到的Python文档中使用的实际代码通过在各种正则表达式之间插入do运算符,从而根据提供的模式实际创建了一个正则表达式。 (这使得不可能使用编号的捕获,但这可能不是问题。)

如果Python正则表达式具有Posix最长匹配语义,则将等同于最大munch,但事实并非如此:Python替换将优先选择第一个匹配项,除非需要后续匹配项以继续正则表达式:< / p>

|

要正确处理此问题,您必须格外小心,以确保正则表达式的顺序正确且不会干扰彼此的匹配。 (并非所有的正则表达式库的工作方式都与Python相同,但很多都一样。您需要检查文档,也许还要做一些试验。)

简而言之,对于一种特定的语言,如果您准备进行一些工作,您将能够手动构建“正确”工作的词法分析器(假设该语言坚持最大的需求,因为大多数标准化语言大多这样做),但这绝对是一件繁琐的事情。不仅限于您:对于想要了解或验证您的代码的任何人来说,这都是额外的工作。

因此,就编写代码的效率(包括调试)而言,我想说像(f)lex这样的词法生成器无疑是赢家。

有一个长期存在的模因,即手工(或开放编码)词法生成器速度更快。如果您想尝试一下,可以尝试使用>>> pat = re.compile("(wee|week)(night|knight)") >>> pat.match("weeknight").group(1) 'wee' >>> pat.match("weekknight").group(1) 'week' ,它会产生高度优化的开放式词法扫描器。 (通过开放代码,我的意思是它们不使用过渡表。)对于给定的一组词法规则,该理论可能正确或不正确,因为基于表的词法分析器(由(f)lex生成)是通常,代码大小要小得多,因此可以更有效地利用处理器缓存。如果选择flex的快速(但较大)表选项,则扫描器的内部循环非常短,并且仅包含一个条件分支。 (但是,对单个分支的分支预测将不会非常有效)。相比之下,开放编码的扫描器在循环中具有大量条件分支的代码,其中大多数条件分支相当容易预测。 (不是执行路径更长;而是内部循环不足以缓存)。

无论哪种方式,我认为可以合理地说,差异不会破坏资金,我的建议始终是使用词法分析器,这对于其他人来说更容易阅读,特别是如果您计划寻求帮助的话在SO:-)