使用最小括号的漂亮打印AST

时间:2012-12-04 17:46:26

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

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

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

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

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

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

我想知道后一种情况最好的规则是什么。对于除法和减法是否足够,如果rhs子表达式具有小于或等于的优先级,则应该用括号括起来。

3 个答案:

答案 0 :(得分:9)

我自己偶然发现了你的问题。虽然我还没有找到规范算法,但我发现,就像你说的那样,单独的运算符优先级不足以最小化括号表达式。我在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 ++将是不明确的。但是我怀疑即使其中一个不需要括号,为了便于阅读,你也可能想要添加括号。

答案 1 :(得分:3)

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

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

 (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的右子树括起来,如果左子树有root op2,你想要将op2的左子树括起来。

答案 2 :(得分:0)

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

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 ] )
                                     ')' )))

...

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