【问题标题】:Pretty Printing AST with Minimal Parentheses用最少的括号漂亮地打印 AST
【发布时间】:2012-11-22 10:17:57
【问题描述】:

我正在为 JavaScript AST 实现一个漂亮的打印机,我想问一下是否有人知道一种“正确的”算法,可以根据运算符优先级和 associativity 自动为表达式加上最小括号。我在 google 上没有找到任何有用的资料。

显而易见的是,应该将其父级具有更高优先级的运算符括起来,例如:

(x + y) * z // x + y has lower precedence

但是,也有一些不是关联的运算符,在这种情况下仍然需要括号,例如:

x - (y - z) // both operators have the same precedence

我想知道后一种情况的最佳规则是什么。是否足以说明除法和减法,如果 rhs 子表达式的优先级小于或等于,则应将其括起来。

【问题讨论】:

    标签: javascript compiler-construction abstract-syntax-tree pretty-print parentheses


    【解决方案1】:

    我自己在寻找答案时偶然发现了您的问题。虽然我还没有找到一个规范的算法,但我发现,就像你说的那样,单独的运算符优先级不足以最小化括号表达式。我尝试在 Haskell 中编写一个漂亮的 JavaScript 打印机,但我发现编写一个健壮的解析器很乏味,所以我更改了具体语法:https://gist.github.com/kputnam/5625856

    除了优先级之外,您还必须考虑运算符的关联性。像/- 这样的二元运算被解析为左结合。但是,赋值=、求幂^ 和相等== 是右结合的。这意味着表达式Div (Div a b) c 可以写成不带括号的a / b / c,但Exp (Exp a b) c 必须用括号括起来为(a ^ b) ^ c

    您的直觉是正确的:对于左结合运算符,如果左操作数的表达式绑定 不如其父项紧密,则应将其括起来。如果右操作数的表达式绑定与它的父项一样紧密不那么紧密,它应该用括号括起来。所以Div (Div a b) (Div c d) 不需要在左子表达式周围加上括号,但右子表达式需要:a / b / (c / d)

    接下来,一元运算符,特别是可以是二元或一元的运算符,例如否定和减法-、强制和加法+ 等,可能需要根据具体情况进行处理。例如Sub a (Neg b) 应该打印为a - (-b),即使一元否定比减法绑定更紧密。我想这取决于您的解析器,a - -b 可能不会模棱两可,只是丑陋。

    我不确定既可以是前缀又可以是后缀的一元运算符应该如何工作。在像++ (a ++)(++ a) ++ 这样的表达式中,其中一个运算符必须比另一个更紧密地绑定,否则++ a ++ 将是不明确的。但我怀疑即使其中一个不需要括号,为了便于阅读,您可能还是想添加括号。

    【讨论】:

      【解决方案2】:

      这取决于特定语法的规则。我认为您对具有不同优先级的运算符以及减法和除法正确。

      然而,取幂通常被区别对待,因为它的右手操作数首先被评估。所以你需要

       (a ** b) ** c
      

      当 c 是根的右孩子时。

      括号的使用方式取决于语法规则的定义。如果你的语法是

      exp = sub1exp ;
      exp = sub1exp op exp ;
      sub1exp = sub1exp ;  
      sub1exp = sub1exp op1 sub2exp ;
      sub2exp = sub3exp ;
      sub2exp = sub3exp op2 sub2exp ;
      sub3exp = ....
      subNexp = '(' exp ')' ;
      

      由于 op1 和 op2 是非关联的,那么如果子树的根也是 op1,那么您想用括号括住 op1 的右子树,如果左子树有根 op2,那么您想用括号括住 op2 的左子树。

      【讨论】:

        【解决方案3】:

        有一种通用方法可以用最少的括号来漂亮地打印表达式。首先为您的表达语言定义一个明确的语法,该语法编码优先级和关联性规则。例如,假设我的语言具有三个二元运算符(*、+、@)和一个一元运算符(~),那么我的语法可能看起来像

        E -> E0
        
        E0 -> E1 '+' E0       (+ right associative, lowest precedence)
        E0 -> E1
        
        E1 -> E1 '*' E2       (* left associative; @ non-associative; same precedence)
        E1 -> E2 '@' E2
        E1 -> E2
        
        E2 -> '~' E2          (~ binds the tightest)
        E2 -> E3
        
        E3 -> Num             (atomic expressions are numbers and parenthesized expressions)
        E3 -> '(' E0 ')'
        

        语法的解析树包含所有必要的(和不必要的)括号,并且不可能构造一个扁平化导致歧义表达式的解析树。例如,字符串没有解析树

        1 @ 2 @ 3
        

        因为 '@' 是非关联的并且总是需要括号。另一方面,字符串

        1 @ (2 @ 3)
        

        有解析树

        E(E0(E1( E2(E3(Num(1)))
                 '@'
                 E2(E3( '('
                        E0(E1(E2(E3(Num(2)))
                              '@'
                              E2(E3(Num(3)))))
                        ')')))
        

        因此问题被简化为将抽象语法树强制转换为解析树的问题。通过尽可能避免将 AST 节点强制为原子表达式来获得最小数量的括号。这很容易以系统的方式完成:

        维护一个由指向 AST 中当前节点的指针和正在扩展的当前产品组成的对。用根 AST 节点和“E”产生式初始化该对。在每种情况下,对于 AST 节点的可能形式,尽可能多地扩展语法以对 AST 节点进行编码。这将为每个 AST 子树留下一个未扩展的语法生成。在每个(子树、生产)对上递归地应用该方法。

        例如,如果 AST 是(* (+ 1 2) 3),那么进行如下操作:

        expand[ (* (+ 1 2) 3); E ]  -->  E( E0( E1( expand[(+ 1 2) ; E1]
                                                    '*'
                                                    expand[3 ; E2] ) ) )
        
        expand[ (+ 1 2) ; E1 ] --> E1(E2(E3( '('
                                             E0( expand[ 1 ; E1 ]
                                                 '+'
                                                 expand[ 2 ; E0 ] )
                                             ')' )))
        
        ...
        

        该算法当然可以以一种不太明确的方式实现,但该方法可用于指导实现而不至于发疯:)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-09-10
          • 2015-07-03
          • 2015-08-26
          • 2023-04-04
          相关资源
          最近更新 更多