将优先级表转换为适合递归下降的语法?

时间:2012-12-20 11:13:44

标签: algorithm parsing

如果我们的语言只包含原子元素和一元和二元运算符:

atomic elements: a b c
unary operators: ! ~ + -
binary operators: + - / *

然后我们可以定义一个语法:

ATOM := a | b | c
UNOP := ! | ~ | + | -
BINOP := + | - | / | *
EXPR := ATOM | UNOP EXPR | EXPR BINOP EXPR

然而,这个语法会导致一个模糊的解析树(由于左递归而在递归下降解析器中出现无限循环)。

所以我们添加一个优先表:

Precendence 1: unary+ unary- ~ ! (Right to Left)
Precendence 2: * / (Left to Right)
Precendence 3: binary+ binary- (Left to Right)

我的问题是我们可以采用什么算法或过程来获取优先级表,并为递归下降解析器(而不是左递归)生成适当的语法。

优先级表是操作员组和相关方向的有序列表(L-> R或R< -L)。答案是将此作为输入并将语法作为输出。

2 个答案:

答案 0 :(得分:2)

将运算符优先级语法转换为LR(1)语法[1]很容易,但结果语法将使用左递归来解析左关联运算符。消除左递归很容易 - 例如,使所有运算符都正确关联 - 但是当结果语法识别相同的语言时,解析树就不同了。

事实证明,稍微修改递归下降解析器以便能够处理优先级关系并不困难。该技术由Vaughan Pratt发明,基本上使用调用堆栈替换经典shunting-yard algorithm中的显式堆栈。

Pratt解析似乎正在经历某种复兴,你可以找到很多关于它的博客文章;一个相当不错的是Eli Bendersky。普拉特在20世纪70年代早期设计了这个程序,大约在同一时间Frank deRemer证明LR(1)解析是实用的。普拉特对正式解析的实用性和不灵活性持怀疑态度。从那以后,我认为这场辩论一直在酝酿着。 Pratt解析器确实简单而灵活,但另一方面,很难证明它们是正确的(或者它们解析特定的正式描述的语法)。另一方面,虽然bison最近获得了对GLR解析的支持,但是使用它可能要少得多,尽管bison - 生成的解析器实际上解析了他们声称要解析的语法还有很多人会同意普拉特的声明(从1973年开始),即正式的解析方法“不易使用且使用起来不太愉快”。


[1]在实践中,所有yacc衍生物和许多其他LR解析器生成器都将接受优先关系以消除歧义;生成的语法表较小,涉及的单位减少量较少,因此如果您要使用解析器生成器,没有特别好的理由不使用此技术。

答案 1 :(得分:2)

描述任意优先级的一般语法可以使用基于表的LALR解析器进行解析,并且可以使用yacc生成。但是,这并不意味着当您希望使用递归下降解析器时,所有内容都会丢失。

递归下降解析器只能验证语法是否正确。构建语法树是一个不同的问题,优先级应该在树构建级别上处理。

因此,请考虑以下语法,不带左递归,可以解析中缀表达式。没有什么特别没有优先权的迹象:

Expr := Term (InfixOp Term)*
InfixOp := '+' | '-' | '*' | '/'
Term := '(' Expr ')'
Term := identifier

(右侧使用的符号是正则表达式,使用大型驼峰案例编写替换的规则,使用小型驼峰案例引用或编写令牌)。

构建语法树时,您有一个当前节点,您可以在其中添加新节点。

解析规则时,通常会在当前节点上创建一个新的子节点,并使该子节点成为当前节点。完成解析后,您将升级到父节点。

现在,在解析InfixOp规则时,应该采用不同的方法。您应该为相关节点分配优先级。 Expr节点具有最弱的优先级,而所有其他运算符具有更强的优先级。

处理中缀优先级

解析InfixOp规则时,请执行以下操作:

  1. 虽然当前节点的优先级高于传入节点的优先级,但仍然保持上升一级(使父节点成为当前节点)。

  2. 然后插入传入节点的节点作为当前节点的最后一个子节点的父节点并使其成为当前节点。

  3. 由于Expr节点被宣布为具有最弱的优先级,因此最终将停止攀爬。

    我们来看一个例子:A+B*C

    在使用当前令牌后,当前节点始终标有!

    Parsed: none
    
    Expr!
    
    Parsed: A
    
    Expr!
    |
    A
    
    Parsed: A+
    
    Expr
    |
    +!
    |
    A
    
    Parsed: A+B
    
      Expr
      |
      +!
     / \
    A   B
    
    Parsed: A+B*
    
      Expr
      |
      +
     / \
    A   *!
       /
      B
    
    Parsed: A+B*C
    
      Expr
      |
      +
     / \
    A   *!
       / \
      B   C
    

    如果您以后序方式遍历此项,您将获得可用于评估它的表达式的反向抛光表示法。

    或者反过来一个例子:A*B+C

    Parsed: none
    
    Expr!
    
    Parsed: A
    
    Expr!
    |
    A
    
    Parsed: A*
    
    Expr
    |
    *!
    |
    A
    
    Parsed: A*B
    
      Expr
      |
      *!
     / \
    A   B
    
    Parsed: A*B+
    
      Expr
      |
      +!
      |
      *
     / \
    A   B
    
    Parsed: A*B+C
    
        Expr
        |
        +!
       / \
      *   C
     / \
    A   B
    

    处理关联性

    有些运算符是左关联的,而其他运算符是右关联的。例如,在C语言系列中,+是关联的,而=是右关联的。

    实际上,整个关联性事物归结为在相同优先级上处理运算符。对于左关联运算符,当您在​​相同的优先级别遇到运算符时,攀爬会继续上升。对于右关联运算符,遇到相同的运算符时停止。

    (展示所有技术需要太多空间,我建议在一张纸上试一试。)

    处理前缀和后缀运算符

    在这种情况下,您需要稍微修改一下语法:

    Expr := PrefixOp* Term PostfixOp* (InfixOp PrefixOp* Term PostfixOp*)*
    InfixOp := '+' | '-' | '*' | '/'
    Term := '(' Expr ')'
    Term := identifier
    

    当您遇到前缀运算符时,只需将其作为新子项添加到当前节点并将新子项作为当前节点,无论优先级如何,即使它是强运算符还是弱运算符,它都是正确的,优先级中缀运算符的爬升规则确保了正确性。

    对于后缀运算符,您可以使用我在中缀运算符中描述的相同优先级攀爬,唯一的区别是我们没有右侧的后缀运算符,因此它只有1个子级。

    处理三元运算符

    C语言系列具有?:三元运算符。关于语法树构建,您可以将?:作为单独的中缀运算符来处理。但有一个技巧。您为?创建的节点应该是一个不完整的三元节点,这意味着您执行通常的优先级攀登并放置它,但是这个不完整的节点将具有最低优先级,这可以防止甚至更弱的运算符(如逗号运算符)爬过它。当你到达:时,你必须爬到第一个不完整的三元节点(如果你没找到,然后报告语法错误),然后将它改为一个具有正常优先级的完整节点,然后进行当前。如果当前分支上存在不完整的三元节点时意外到达表达式的末尾,则再次报告语法错误。

    因此a , b ? c : d被解释为a , (b ? c : d)

    但是a ? c , d : e将被解释为a ? (c , d) : e,因为我们阻止了逗号爬过?。

    处理数组索引和函数调用

    尽管有后缀外观,但它们是中缀操作符,右侧是语法强制括号术语,对于数组索引和函数调用也是如此。