如何使这个LL(1)解析器适应LL(k)解析器?

时间:2014-05-14 07:19:25

标签: parsing compiler-construction ll

在Dragon-book的附录中,给出了一个LL(1)前端作为例子。我认为这非常有帮助。但是,我发现对于下面的上下文无关语法,至少需要一个LL(2)解析器。

statement : variable ':=' expression
          | functionCall

functionCall : ID'(' (expression ( ',' expression )*)? ')'
             ;
variable : ID
         | ID'.'variable
         | ID '[' expression ']'
         ;

我如何调整LL(1)解析器的词法分析器以支持k向前看令牌? 有一些优雅的方式吗?

我知道我可以为令牌添加一些缓冲区。我想讨论编程的一些细节。


这是Parser:

class Parser
{
    private Lexer lex;
    private Token look;
    public Parser(Lexer l)
    {
        lex = l;
        move();
    }

    private void move()
    {
        look = lex.scan();
    }
}

并且Lexer.scan()返回流中的下一个标记。

1 个答案:

答案 0 :(得分:1)

实际上,您需要缓冲k前瞻标记才能进行LL(k)解析。如果k为2,那么您只需要扩展当前方法,该方法使用另一个私有成员look或其他类似的方式在look2中缓冲一个标记。对于较大的k,您可以使用环形缓冲区。

在实践中,你并不需要一直充分的前瞻。大多数时候,单一令牌前瞻就足够了。您应该将代码构造为决策树,只有在必要时才能查询未来的令牌以解决歧义。 (提供特殊的令牌类型通常很有用," unknown&#34 ;,可以将其分配给缓冲的令牌列表,以指示前瞻尚未到达该点。或者,您可以始终保持k前瞻标记;对于手工构建的解析器,可以更简单。)

或者,您可以使用回退结构,只需尝试一种替代方案,如果不起作用,而不是报告语法错误,请将解析器和词法分析器的状态恢复到下一个替代方案。在此模型中,词法分析器将当前输入缓冲区位置作为显式参数,并且输入缓冲区需要可重新循环。但是,您可以使用前瞻缓冲区来有效地记忆词法分析器功能,这可以避免重绕和重新扫描。 (扫描通常足够快,偶尔重新扫描不重要,因此您可能希望推迟添加代码复杂性,直到您的分析表明它有用。)

两个注释:

1)我对这条规则持怀疑态度:

functionCall : ID'(' (expression ( ',' expression )*)* ')'
             ;

这将允许,例如:

function(a[3], b[2] c[x] d[y], e.foo)

对我来说并不合适。通常,您可以将()的内容标记为可选而不是可重复,例如。使用可选标记而不是第二个Kleene星 *

functionCall : ID'(' (expression ( ',' expression )*)? ')'
             ;

2)在我看来,你真的应该考虑对表达式语言使用自下而上的解析,无论是生成的LR(1)解析器还是手工构建的Pratt解析器。 LL(1)很少足够。当然,如果你正在使用解析器生成器,你可以使用像ANTLR这样有效实现LL(∞)的工具;这将照顾你的先行。