描述任意优先级的通用语法可以使用基于表的 LALR 解析器进行解析,并且可以使用 yacc 生成。但这并不意味着当您希望使用递归下降解析器时一切都会丢失。
递归下降解析器只能验证语法是否正确。构建语法树是另一回事,应该在树构建级别处理优先级。
因此,请考虑以下可以解析中缀表达式的没有左递归的语法。没有什么特别的没有优先的迹象:
Expr := Term (InfixOp Term)*
InfixOp := '+' | '-' | '*' | '/'
Term := '(' Expr ')'
Term := identifier
(右侧使用的符号类似于正则表达式,使用大驼峰写的替换规则,使用小驼峰引用或写标记的规则)。
在构建语法树时,您有一个当前节点,您可以向其中添加新节点。
大多数情况下,当您解析规则时,您会在当前节点上创建一个新的子节点并使该子节点成为当前节点。完成解析后,您将进入父节点。
现在,当您解析InfixOp
规则时,应该采取不同的做法。您应该为相关节点分配优先级强度。Expr
节点具有最弱的优先级,而所有其他运算符具有更强的优先级。
处理中缀优先级
解析InfixOp
规则时,请执行以下操作:
当当前节点的优先级强于传入节点的优先级时,继续上一级(使父节点成为当前节点)。
然后插入传入节点的节点作为当前节点的最后一个子节点的父节点并使其成为当前节点。
由于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
,因为我们阻止了逗号越过 ?。
处理数组索引和函数调用
尽管出现后缀,但它们是中缀运算符,右侧有句法强制括号术语,数组索引和函数调用也是如此。