如何解决词法分析器的规格是什么?
有人问我以下问题:
Given two token rules:
A := aaa
B := aaaa
The output for text 'aaaaaaaaa' (nine a's) is
B (aaaa); B (aaaa); unkNown (a)
Rather than
A (aaa); A (aaa); A (aaa)
我知道这是最大咀嚼原则的结果。但是我如何恰当地陈述词法分析器的正确行为呢?具体来说,有人问我:“为什么输出应该是第一个输出,而不是看起来更合理的第二个输出?”
我试图寻找许多文献,但是它们仅描述了最大的munch算法是如何工作的。即便如此,如果有人可以提出一些建议也会很有帮助。
解决方法
这是一个很好的问题。
没有词法分析器的正式定义;这是pattern,而不是标准。使用最大嚼嘴是一种启发式方法,而不是绝对规则。但这是一个很好的启发式方法:最大的nch饮几乎总是要做的事。
但是,“几乎总是”不是“总是”,并且许多语言都有最大的mu断的例外。
(至少)可以追溯到Modula-2的一个非常常见的示例是范围运算符..
,通常与整数操作数一起使用,因此0..99
表示从0到99的范围。 (语义因语言而异。)在大多数语言中,最大的munch会将0..99
解释为两个浮点常量(0.
和.99
),而不是构成范围的三个标记。 ({0
,..
和99
)。因此,必须将其作为最大mu餐的例外。
C ++还有另一个例外。在该语言中添加了二合字母之后,就可以将[
写为<:
了;与三合一字母不同,二合字母仅是记号,因此应使用最大字符数。但是,::
在C ++中用作名称空间分隔符,并且::name
表示“ name
在顶级名称空间中”(与由于using
而激活的名称空间相反)宣言)。许多已经存在的程序都包含使用这种语法的模板扩展-Template<::classname>
-如果将<:
突然解释为[
的语法等效项,那么这些将成为语法错误。因此,对序列<::
的最大修改产生了一个例外,该序列必须是两个标记<
::
除非,下一个字符是>
或:
。因此,我们有<::a
⇒<
::
a
,但是<:::a
⇒<:
::
a
和{{1 }}⇒<::>
<:
(即:>
)。
还有其他示例。
(F)lex通过使用稍微更灵活的“ matching”(匹配)定义来处理这些异常,该定义仍取决于最大的munch启发式算法。
(f)lex模式可以包含一个(无括号)[]
运算符。 /
通常称为“跟踪上下文”运算符,但这并不精确。 /
不会更改匹配的字符;它的作用是从匹配的令牌中排除/
之后的匹配部分。因此,适用于C ++的(f)lex扫描仪可能包含以下规则:
/
结果,输入< { return T_LT; }
<: { return T_LT_COLON; }
</::[^:>] { return T_LT; }
将匹配所有三个模式。由于最大的限制,最后一个规则将获胜,但是从该匹配项返回的令牌只是<::a
,而不是完整的四个字符的匹配项。这意味着下一次扫描将从重新扫描<
开始(导致检测到::a
令牌)。另一方面,输入::
将不匹配最后一个模式,因此扫描仪将必须回溯到实际匹配的最长模式:<:::a
。
可以使用类似的技巧来确保<:
范围内的语言(例如,Swift)与浮点模式不匹配:
2..3
同样,[[:digit:]]+"."[[:digit:]] { ... return T_FLOAT; }
[[:digit:]]+/".." { ... return T_INTEGER; }
的最长匹配项是第二个匹配项,因此,最大号选择。但是返回的令牌只是2..3
;重新扫描尾随上下文以获取下一个标记。
但是,还有另一类最大的munch异常,只能通过使词法分析依赖于语法上下文来处理。例如,EcmaScript(和其他语言,包括Awk)允许以2
字符开头的正则表达式文字。但是,/
也可能是除法运算符或/
运算符的第一个字符。 (它也可能会开始注释,但是由于正则表达式不能以/=
开头,因此这种情况是没有问题的。)语言语法有效地定义了两个互斥的上下文:
- 在“运算符”上下文中,可以使用中缀或后缀运算符,而不能使用变量或文字之类的操作数。
- 在“操作数”上下文中,可以使用操作数或前缀运算符,而其他运算符则不能。
因此 parser 始终知道下一个标记是否可能是除法运算符,以及何时可能是正则表达式。但这不是将事实传达给词法分析器的好方法。 (解析器依赖于先行标记的事实使情况进一步复杂化:为了使解析器调用词法分析器并指出是使用运算符还是使用正则表达式规则,解析器需要先知道 读取前瞻令牌。)
最大蒙克语的另一种明显的语法异常集是模棱两可的双括号,其中最臭名昭著的是C ++模板参数。首次将模板添加到C ++时,我们需要注意避免关闭两个开放的模板专业化而没有中间空格,以避免*
被视为右移运算符。那很烦人,也没有必要。 >>
可以用两种不同的方式解释的表达式很少,在几乎所有这样的表达式中,其中一种可能性是非常不可思议的。因此,为了使大多数C ++程序员感到宽慰(以及C ++词法分析器作者的烦恼),最终更改了标准,以允许>>
关闭两个开放的模板专业化,而不是强制将其解释为语法上的解释(或语义上)无效的右移。 (请注意,这种情况不必由词法分析器处理。词法分析器可以返回单个>>
令牌,将其留给解析器以使用该单个令牌来关闭两个开放的专业化词。这会使语法a复杂化。一点,但并非难以忍受。)
因此,最大的吃货不是很普遍的。而且,不时提出针对词法分析器的建议,这些词法分析器可以通过产生多个可能的令牌流来处理歧义词库,这与GLR / GLL解析器可以产生多个可能的分析树的方式非常相似。确实,一种经常被建议的可能性是“无扫描器”解析器,其中词法分析被简单地集成到语法中。如果有GLR / GLL解析器可用,那么词法分析往往会破坏对超前需求的事实不再是制止者。 (据称,无扫描器解析也可以与PEG语法一起很好地工作。)
另一方面,处理并行令牌流可能会大大增加解析时间,甚至达到要求输入长度为平方时间的程度(尽管在实际语法中这不会发生)。而且我认为这使我们回到了原始问题中的示例。
上面给出的所有示例都显示了涉及两个连续标记的与最大munch的偏差(在某种程度上,它们是与最大munch的偏差),这增加了词法分析的复杂性,但对计算复杂性没有太大影响。但是在操作规范中建议的语法中,将>>
识别为以aaaaaaaaa
标记而不是aaa
开头,需要预先考虑多个标记(因为aaaa
和{{ 1}}应该以{{1}}开头。
除了对解析器可能提出额外的计算要求外,这种非本地性的歧义还会给人类读者(或作家)带来认知负担,而阅读器(或作者)通常会得到解析器的最佳服务,其功能易于预测,甚至如果它有时找不到潜在的解析。
想到aaaaaaaa
的C例子;有时会出现一个问题,为什么不将其解析为aaaaaaaaaa
,这是唯一可能的有意义的解析。我认为这里的答案不是“因为最大的吃嚼子”,而是“因为大多数代码读者不会看到该解析”。超越最大计算量的最大优点之一是它易于解释和预测。
实际上非常简单。当词法分析器(实现最长匹配/最大修改率原则)在处理输入字符时,它会针对每个令牌与其他令牌进行相互依赖决策。这意味着,在发现第一个最长可能匹配为aaaa
之后,解析器便可以处理第一个令牌。然后,词法分析器再次开始搜索另一个标记,并再次找到aaaa
。此后,由于剩余的输入字符串不足以匹配任何令牌,因此单个剩余的a
不会被识别为令牌。
如果词法分析器在整个输入中搜索可能的最佳匹配项,以将其转换为令牌,而不是正在讨论的最长匹配项,则可能会出现第二个结果。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。