我正在为一个JavaScript AST实现一个漂亮的打印机,我想问一下是否有人知道一个“正确”的算法,根据运算符优先级和associativity自动括起括号最小的表达式。我没有在谷歌上找到任何有用的资料。
显而易见的是,父级具有更高优先级的运算符应该用括号括起来,例如:
(x + y) * z // x + y has lower precedence
但是,也有一些运算符不是关联的,在这种情况下仍需要括号,例如:
x - (y - z) // both operators have the same precedence
我想知道后一种情况最好的规则是什么。对于除法和减法是否足够,如果rhs子表达式具有小于或等于的优先级,则应该用括号括起来。
答案 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 ] )
')' )))
...
该算法当然可以用一种不太明确的方式实现,但该方法可用于指导实现而不会疯狂:)。