关于这个右递归 Antlr 规则的非 LL(*) 是什么?

问题描述

考虑以下 Antlr3 语法:

grammar stat;

s : 
  label ID '=' ID
| label 'return' ID
;

label
  : ID ':' label
|
;

ID: 'a'..'z' + ;

鉴于 Antlr3 是 LL(*),这意味着解析器在选择替代项时可以任意向前查看令牌流,为什么我会收到以下错误

 [fatal] rule s has non-LL(*) decision due to recursive rule invocations reachable from alts 1,2.  Resolve by left-factoring or using syntactic predicates or using backtrack=true option.

问题:

  1. 如何修正上述语法? (修复背后的解释将不胜感激。)

  2. 如果在一次不成功的、任意长的前瞻后不回溯,LL(*) 中的 * 与 LL(k) 的静态 k 相比是什么?

  3. 自动左因子分解不是由 Antlr3 自动完成的吗?

  4. 很好奇,如何通过使用句法谓词来解决这个问题?

我使用的是 Antlr 3.5.2。

解决方法

这个确切的例子(逐个字符)可以在 The Definitive ANTLR Reference 的 ANTLR3 版本的第 277 页找到,它可以在线获取(至少用于有限的预览以及付费下载)。它被用作对 ANTLR3 中使用的 LL(*) 算法的一个方面的解释,因此以下页面的解释可能是对您的问题的最佳详细答案。此外,还讨论了各种可能的解决方案。

所以让我只关注您的一个问题,我认为这也将回答帖子标题中的问题。

  1. 如果在一次不成功的、任意长的前瞻之后没有回溯,LL(*) 中的 * 是什么?

LL(*) 是一种启发式算法,而不是形式语言类别; Terence Parr(ANTLR 的作者)使用该术语来标记 ANTLR3 中使用的特定解析算法。 (ANTLR4 使用不同的算法,Parr 将其称为自适应 LL(*)。我不打算讨论这个。)

LL 解析,一般来说,在开始解析产生式之前决定使用哪个产生式。 LL(k) 解析基于以下 k 个标记做出决定,其中 k 是某个固定(通常很小)的整数。这基本上是 ANTLR2 中使用的算法,它并不足以处理许多语法。

为了使算法更通用,您可以添加回溯(ANTLR 会这样做);如果在检查了 k 个标记之后,解析器仍然不能决定使用哪个产生式,它会按顺序尝试它们,直到找到一个有效的产生式。如果尝试的生产失败,解析器将返回到决策点并尝试下一个生产。如果生产成功,解析器会接受它并继续,即使这可能会导致稍后的解析失败。 (所以语法中的产生顺序在回溯解析器中很重要。)

LL(*) 不涉及回溯。相反,它构建了一个基于有限自动机的前向扫描,该扫描将任意长的前瞻序列与正则表达式(标记,不是字符,但原理相同)进行匹配。如果它可以为每个候选产生式构建(同时编译语法)正则表达式,明确区分备选方案,那么它可以在决策点快速并行运行有限自动机并做出决策。 LL(*) 中的 * 表示前瞻没有任意长度限制;正则表达式只需要在未来的某个时间点发生分歧。

作为一个实际例子,在语法中你提出了替代方案

  label ID '=' ID

如果前瞻匹配(ID ':')* ID '=',则应选择,而

| label 'return' ID
如果前瞻匹配 (ID ':')* ID 'return',则应选择

。这些显然是不同的正则表达式;最多可以匹配任何一个前瞻序列。

然而,所有这些都依赖于解析器生成器能够计算出前瞻模式。在一般情况下,这是一个困难(可能不可能)的问题。 ANTLR3 尽其所能,但它无法处理前瞻序列中的递归非终结符(如 label)。

由于它可以处理重复运算符,您可以通过简单地将 label 的定义更改为非递归替代来解决该问题。 (这直接来自之前引用的书。)

    label: (ID ':')*

该定义已经是正则表达式形式,ANTLR3 只需将其插入到生产中即可生成有效的前瞻模式。

这在实践中可能没问题,因为在实践中不太可能出现长标签序列。但就我个人而言,我会选择一个简单的 LL(2) 解决方案,它根本不使用 label 非终端:

s : 
  ID '=' ID
| 'return' ID
| ID ':' s
;

此解决方案不需要无限制的超前搜索。它仍然允许您为每个选项插入任意操作。

,

除了 rici 的精彩回答之外,我还有一些评论:

  1. ANTLR3 不是 LL(*),而是LL(k),有少量k(而且你必须在生成步骤中指定k,所以它是不是动态的。 对于每个 k 级别,它都会创建一个 switch 语句来确定路径,这会很快导致巨大的(且不可编译的)嵌套构造。因此 k > 3 通常不可用(因此前瞻根本不是无限的)。

  2. ANTLR3 可以选择使用回溯来提高解析准确性。

  3. ANTLR3 不支持直接左递归 - ANTLR4 支持。因此根本没有自动重构。

  4. 我认为,从解析器生成的角度来看,您不能将递归与谓词一起使用。谓词将动态引导解析过程,它确实可能在运行时解决问题,但无论如何您都无法克服生成步骤。

更新

您的评论可能是对的。我忘记了 ANTLR3 在代码生成期间执行前瞻(而 ANTLR4 在运行时执行)。因此,即使我对结果代码(嵌套 switch 语句,每个 k 一个级别)是正确的,我也无法真正证明我对 LL(k) 的主张。 ANTLR3 documentation 表示 k 参数阻止使用非循环 LL* DFA。

因此,这只是对已接受答案的补充。