我了解 LL 递归下降解析器如何处理这种形式的规则:
A = B*;
有一个简单的循环,它根据前瞻令牌是否与第一组 B 中的终端匹配来检查是否继续循环。但是,我对基于表的 LL 解析器很好奇:这种形式的规则如何在那里工作?据我所知,处理这样的重复的唯一方法是通过右递归,但在不需要右关联解析树的情况下,这会破坏关联性。
我想知道,因为我目前正在尝试编写一个基于 LL(1) 表的解析器生成器,但我不确定如何在不更改预期解析树形状的情况下处理这样的情况。
我了解 LL 递归下降解析器如何处理这种形式的规则:
A = B*;
有一个简单的循环,它根据前瞻令牌是否与第一组 B 中的终端匹配来检查是否继续循环。但是,我对基于表的 LL 解析器很好奇:这种形式的规则如何在那里工作?据我所知,处理这样的重复的唯一方法是通过右递归,但在不需要右关联解析树的情况下,这会破坏关联性。
我想知道,因为我目前正在尝试编写一个基于 LL(1) 表的解析器生成器,但我不确定如何在不更改预期解析树形状的情况下处理这样的情况。
让我们将您的 EBNF 语法扩展为简单的 BNF 并假设它b
是一个终端并且<e>
是一个空字符串:
A -> X
X -> BX
X -> <e>
B -> b
该文法产生b
任意长度的终端字符串。
要构造表,我们需要生成第一个和后续集(构造一个 LL(1) 解析表)。
First(α)
是从任何语法符号字符串派生的字符串开始的终端集合α
。
First(A) : b, <e>
First(X) : b, <e>
First(B) : b
Follow(A)
是可以立即出现在非终结符右侧的终结符集合 a A
。
Follow(A) : $
Follow(X) : $
Follow(B) : b$
我们现在可以基于集合构造表,$
是输入标记的结束。
+---+---------+----------+
| | b | $ |
+---+---------+----------+
| A | A -> X | A -> X |
| X | X -> BX | X -> <e> |
| B | B -> b | |
+---+---------+----------+
解析器操作始终取决于解析堆栈的顶部和下一个输入符号。
$
在解析堆栈的顶部:
$
是输入符号:接受输入$
不是输入符号:解析错误让我们分析一下输入bb
。初始解析堆栈包含开始符号和结束标记A $
。
+-------+-------+-----------+
| Stack | Input | Action |
+-------+-------+-----------+
| A $ | bb$ | A -> X |
| X $ | bb$ | X -> BX |
| B X $ | bb$ | B -> b |
| b X $ | bb$ | consume b |
| X $ | b$ | X -> BX |
| B X $ | b$ | B -> b |
| b X $ | b$ | consume b |
| X $ | $ | X -> <e> |
| $ | $ | accept |
+-------+-------+-----------+
如您所见,A = B*
可以毫无问题地解析表单的规则。生成的输入具体解析树bb
将是:
是的,这绝对是可能的。重写 BNF 和构建解析表的标准方法对于弄清楚解析器应该如何工作很有用——但据我所知,你要问的是如何避免递归部分,这意味着你会得到 AST 的倾斜二叉树/链表形式。
如果您正在手动编码解析器,您可以简单地使用循环,使用解析表中指示递归调用的前瞻来决定再次绕过循环。(即,您可以只使用while
这些前瞻作为条件。)然后对于每次迭代,您只需将构造的子树附加为当前父级的子级。那么,在您的情况下,A
将获得几个直系B
子女。
现在,据我了解,您正在构建一个解析器生成器,并且通过计划 BNF 遵循标准过程可能是最简单的。但是,这不是真正的问题。毕竟,迭代和递归之间没有实质性区别。您只需要有一类“辅助规则”,它不会引入新的 AST 节点,而是将它们的结果附加到触发它们的非终结符的节点上。因此,当将重复变为X -> BX
,而不是构造X
节点时,您的规则将orX
的子列表(无论哪个触发它)扩展为它自己的孩子。你最终还是会有几个孩子,而且看不到节点。A
X
A
B
X