【问题标题】:What's the specification of a lexer?词法分析器的规格是什么?
【发布时间】:2020-08-29 13:06:26
【问题描述】:

我被问到以下问题:

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)

我知道这是最大咀嚼原则的结果。但是我怎样才能恰当地陈述词法分析器的正确行为呢?具体来说,有人问我“为什么输出应该是第一个,而不是第二个似乎更合理?”

我试图查找许多文献,但它们只描述了最大咀嚼算法的工作原理。即便如此,如果有人能提出一些参考,那将是有帮助的。

【问题讨论】:

    标签: lexer lexical-analysis


    【解决方案1】:

    这是个好问题。

    词法分析器没有正式的定义;这是pattern,不是标准。最大咀嚼的使用是一种启发式的,而不是绝对的规则。但这是一个很好的启发式方法:最大咀嚼几乎总是是正确的事情。

    但“几乎总是”不是“总是”,许多语言都有最大咀嚼的例外。

    一个非常常见的例子(至少)可以追溯到 Modula-2,是范围运算符 ..,通常与整数操作数一起使用,因此 0..99 表示从 0 到 99 的范围。(语义不同从语言到语言。)在大多数语言中,maximal munch 会将 0..99 解释为两个浮点常量(0..99),而不是组成一个范围的三个标记(0.. 和 @ 987654329@)。所以这必须作为最大咀嚼的例外处理。

    C++ 还有一个例外。在语言中添加二合字母后,可以将[ 写成<:;与三合字母不同,二合字母只是标记,因此应该应用最大咀嚼。但是,:: 在 C++ 中用作命名空间分隔符,::name 表示“顶级命名空间中的name”(与由于 using 声明而处于活动状态的命名空间相反)。许多已经存在的程序包括使用这种语法的模板扩展——Template<::classname>——如果<:突然被解释为[的语法等价物,这些就会变成语法错误。所以对序列<:: 的最大咀嚼做了一个例外,它必须是两个标记< :: 除非下一个字符是>:。因此,我们有 @987654345 @987654345 @ @ @ @ @ @987654346 @ @987654347 @但是 @987654348 @⇒ @98765449 @98765449 @9876543550 @9876543550 @987444444444444444444444444444444444444444444444444×

    还有其他例子。

    (F)lex 通过使用“匹配”的稍微灵活的定义来处理这些异常,该定义仍然取决于最大咀嚼启发式。

    (f)lex 模式可以包含单个(未加括号的)/ 运算符。 / 通常被称为“尾随上下文”运算符,但这并不准确。 / 不会改变匹配的字符;它的作用是从匹配的令牌中排除/之后的匹配部分。因此 C++ 的 (f)lex 扫描器可能包含以下规则:

    <         { return T_LT; }
    <:        { return T_LT_COLON; }
    </::[^:>] { return T_LT; }
    

    因此,输入 &lt;::a 将匹配所有三种模式。由于最大咀嚼,最后一条规则将获胜,但从该匹配返回的令牌只是&lt;,而不是完整的四字符匹配。这意味着下一次扫描将从重新扫描::a 开始(导致检测到:: 令牌)。另一方面,输入 &lt;:::a 将不匹配最后一个模式,因此扫描器将不得不回溯到实际匹配的最长模式:&lt;:

    可以使用类似的技巧来确保具有范围的语言(例如 Swift)中的 2..3 不匹配浮点模式:

    [[:digit:]]+"."[[:digit:]] { ... return T_FLOAT; }
    [[:digit:]]+/".."          { ... return T_INTEGER; }
    

    同样,2..3 的最长匹配是第二个匹配,因此 maximal munch 选择它。但是返回的令牌只是2;为下一个标记重新扫描尾随上下文。

    但是,还有另一类最大咀嚼异常,只能通过使词法分析依赖于句法上下文来处理。例如,EcmaScript(和其他语言,包括 Awk)允许以 / 字符开头的正则表达式文字。但是,/ 也可能是除法运算符或/= 运算符的第一个字符。 (它也可以开始注释,但由于正则表达式不能以 * 开头,这种情况是没有问题的。)语言语法有效地定义了两个互斥的上下文:

    • 在“运算符”上下文中,可以使用中缀或后缀运算符,而不能使用变量或文字等操作数。
    • 在“操作数”上下文中,可以使用操作数或前缀运算符,但不能使用其他运算符。

    所以 解析器 总是知道下一个标记是否可能是除法运算符,以及何时可能是正则表达式。但是它没有一个很好的方法来将该事实传达给词法分析器。 (解析器依赖于前瞻标记的事实使场景更加复杂:为了让解析器调用词法分析器并指示是使用运算符还是正则表达式规则,解析器需要知道 before em> 它读取前瞻令牌。)

    maximal munch 的另一组明显的语法例外是模棱两可的双右括号,其中最臭名昭著的可能是 C++ 模板参数。当模板首次添加到 C++ 中时,我们需要小心避免在没有中间空格的情况下关闭两个打开的模板特化,以避免&gt;&gt; 被识别为右移运算符。这很烦人,没有必要这样做。很少有表达式可以用两种不同的方式解释&gt;&gt;,而且在几乎所有这样的表达式中,其中一种可能性都是人为的。因此,为了让大多数 C++ 程序员(以及 C++ 词法分析器编写者的烦恼)松了一口气,标准最终被更改为允许 &gt;&gt; 关闭两个打开的模板特化,而不是强制它被解释为语法(或语义)无效的右移。 (请注意,这种情况不必由词法分析器处理。词法分析器可以返回单个 &gt;&gt; 标记,将其留给解析器使用该单个标记来关闭两个打开的特化。这使语法有点复杂,但并非无法忍受。)

    所以,最大咀嚼不是相当普遍的。此外,不时会提出词法分析器的建议,这些词法分析器可以通过生成多个可能的标记流来处理模棱两可的词典,这与 GLR/GLL 解析器可以生成多个可能的解析树的方式非常相似。实际上,一种经常被建议的可能性是“无扫描”解析器,其中词法分析被简单地集成到语法中。如果 GLR/GLL 解析器可用,那么词法分析往往会破坏对前瞻的需求这一事实不再是阻碍。 (据称,无扫描仪解析也适用于 PEG 语法。)

    另一方面,处理并行标记流会显着增加解析时间,甚至达到要求时间与输入长度成二次方的程度(尽管在实际语法中不应该发生这种情况)。我认为这让我们回到了原始问题中的示例。

    上面提供的所有示例都显示了涉及两个连续标记的与最大 munch 的偏差(在某种程度上它们是与最大 munch 的偏差),这增加了词法分析的复杂性,但对计算复杂性应该没有太大影响。但是在 OP 中建议的语法中,将aaaaaaaaa 识别为以aaa 标记而不是aaaa 开始需要向前看多个标记(因为aaaaaaaaaaaaaaaaaa 都应该以aaaa 开头) .

    除了对解析器提出可能的额外计算需求之外,这种非局部消歧还给人类读者(或作者)带来了认知负担,而通常由功能易于预测的解析器提供最好的服务,甚至如果它有时无法找到潜在的解析。

    a+++++b 的 C 示例浮现在脑海中;偶尔会出现一个问题,即为什么不将其解析为a++ + ++b,这是唯一可能的有意义的解析。我认为这里的答案与其说是“因为最大咀嚼”,不如说是“因为大多数代码阅读器不会看到那个解析”。除了计算价值之外,maximal munch 的一大优势在于它易于解释和预测。

    【讨论】:

      【解决方案2】:

      其实很简单。当词法分析器(实现最长匹配/最大咀嚼原则)处理输入字符时,它会根据其他标记相互依赖对每个标记做出决定。这意味着,在找到第一个最长可能匹配为aaaa 之后,第一个标记就可以被解析器处理了。然后词法分析器再次开始搜索另一个标记,并再次找到aaaa。之后,由于剩余的输入字符串不足以匹配任何token,因此剩下的单个a 不会被识别为token。

      如果词法分析器搜索要转换为标记的整个输入的最佳匹配,而不是问题中的最长匹配,则第二个结果是可能的。

      【讨论】:

        猜你喜欢
        • 2012-07-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-29
        • 2020-09-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多