手动解析左递归语法的最简单方法是什么?

时间:2013-07-29 14:23:04

标签: parsing language-agnostic grammar compiler-theory

我正在为宠物项目编写解析器,并且出于教育目的,希望手动执行而不是使用解析器生成器。不幸的是,许多在线资源(以及我在大学学习的编译器课程)都假定了某种LL(k)语法。因为左递归非终端,例如

,我不想留下因素语法
E ::= E * E
    | E / E
    | E + E
    | E - E
    | ...

在类似野牛的解析器生成器中可能会大大简化语法。

手工解析这种语法最简单的方法是什么?到目前为止,我的研究告诉我,递归下降不是原因explained here的选项,并且使用递归上升进行LR解析可能是一个不错的选择,尽管有几个站点暂停提及它是非直观的。

2 个答案:

答案 0 :(得分:3)

如果你想为玩具语言构建一个主要是递归的下降解析器,其唯一的左递归是或多或少的标准表达式语法,那么你应该考虑一个Pratt解析器(Java)({{3 }})。或者(等效地,事实证明),您可以使用Javascript;这本算法在龙书中有很好的描述,并且有很多可用的例子使用operator precedence parser(但要注意;许多热心发现这种算法的人并立即为他们的博客撰写这些算法远非可靠的权威。)< / p>


请注意松散解析:

如果要解析表达式语法,并且不关心运算符优先级(例如,如果只需要对代码进行语法着色),则可以轻松地重构表达式语法以避免左递归。 / p>

起点是这样的,使用*表示Kleene星,?表示可选,( )表示分组:

expression: term (INFIX term)*
term: PREFIX* operand postfix*
operand: ID
       | CONSTANT 
       | '(' expression ')'
       ;
postfix: POSTFIX
       | '[' expression ']'
       | '(' ( expression (',' expression)* )? ')' 
       ;

递归下降解析器可以轻松处理上面的*?运算符,并且没有左递归,所以这应该足够了。

请注意,C具有强制转换表达式的尴尬,除非您知道第一个带括号的表达式包含类型而不是表达式,否则它们在语法上与函数调用无法区分。但是,对于松散的解析目的,使用上面的语法解析它们就好像它们是函数调用一样。

C ++使用尖括号作为运算符和模板参数更难处理。我见过的许多语法颜色只是完全忽略了模板参数的情况;让他们正确是一个挑战,但可能是值得的。


编辑评论

如果你愿意,请忽略这一点,但我个人认为学习使用自下而上的LR解析器,特别是GLR解析器,比将语言强制性地转换为限制解析算法更为丰富,尤其是其中语法的细节被模糊为代码路径。但话说回来,做一个野牛和手工生成的解析器可能很有价值,只要看看野牛解析器可以有多优雅和简单。

答案 1 :(得分:0)

递归下降解析器很容易,一旦你理解它们只是BNF的反转(或者是 converse ?)。

我写的最近一段:

/**
 * Parse an addition or subtraction expression.
 *
 * <p/><code><pre>
 * AdditiveExpression
 *     MultiplicativeExpression
 *     AdditiveExpression "+" MultiplicativeExpression
 *     AdditiveExpression "-" MultiplicativeExpression
 * </pre></code>
 *
 * @return an expression
 */
Expression parseAdditiveExpression()
    {
    Expression expr = parseMultiplicativeExpression();
    while (peek().getId() == Id.ADD || peek().getId() == Id.SUB)
        {
        expr = new RelOpExpression(expr, current(), parseMultiplicativeExpression());
        }
    return expr;
    }

/**
 * Parse a multiplication / division / modulo expression.
 *
 * <p/><code><pre>
 * MultiplicativeExpression
 *     PrefixExpression
 *     MultiplicativeExpression "*" PrefixExpression
 *     MultiplicativeExpression "/" PrefixExpression
 *     MultiplicativeExpression "%" PrefixExpression
 *     MultiplicativeExpression "/%" PrefixExpression
 * </pre></code>
 *
 * @return an expression
 */
Expression parseMultiplicativeExpression()
    {
    Expression expr = parsePrefixExpression();
    while (true)
        {
        switch (peek().getId())
            {
            case MUL:
            case DIV:
            case MOD:
            case DIVMOD:
                expr = new RelOpExpression(expr, current(), parsePrefixExpression());
                break;

            default:
                return expr;
            }
        }
    }

我知道很多人发誓可以自动生成解析器的工具,但我个人很喜欢理解语言如何被修复和解析的细节,所以我从不让这些工具为我做这个有趣的工作。