在上一节的最小uGo程序中,对于语句只需要处理表达式
1 | func (p *Parser) parseStmt_block() *ast.BlockStmt { |
优先级处理
表达式节点生成的实现也是依赖于递归下降的思想
各个子表达式节点会根据优先级进行融合,最后得到一个用于描述该表达式的子树:
1 | + |
- 该子树所代表的表达式为:
(1+(2*(3+4))) => 1+2*(3+4)
- 数字节点“3”与数字节点“4”进行融合,然后融合后的表达式与数字节点“2”进行融合,以此类推
这里先给出一个简单的解决方式,假设需要处理的符号只有:+-*/()
1 | func (p *parser) build_expr() *ExprNode { |
- 这样写可以保证高优先级的表达式一定先融合,一定程度上遵守了表达式合成的规则
上述解决方案有一个很明显的问题,那就是不够通用,一个语言可能有很多的运算符号,对每个符号都进行优先级处理就会显得代码的臃肿
这里有一个更通用的表达式处理方案,其思路来自于 ACM 中的表达式求值:
1 | 1+2*(3+4) |
- 需要的数据结构为两个栈:一个存储符号,一个存储数字
- 遇到数字直接 push 到数字栈中,遇到符号则先判断该符号和符号栈顶符号的优先级关系:
- 该符号的优先级大于栈顶:从数字栈中 pop 两个数字,从符号栈中 pop 栈顶符号,计算该算式后将结果 push 到数字栈中
- 该符号的优先级小于或等于栈顶:将该符号 push 到符号栈中
- 循环执行上一步,最终符号栈为空,数字栈中只有一个数字(输出结果)
在编译原理中不需要栈进行辅助,但判断子节点是否进行融合的思路是一致的
获取优先级的函数如下:
1 | func (op TokenType) Precedence() int { |
基于优先级的处理方案如下:
1 | func (p *Parser) parseExpr() ast.Expr { |
- 如果后一个符号 token 的优先级比当前符号 token 的优先小,则直接返回,并在回溯中融合子节点生成子树
- 相反则是再次递归调用
parseExpr_binary
,等待parseExpr_binary
融合完成后,与融合后的子树进行融合
打印JSON
最简单的打印 AST 方式是输出 JSON 格式,为 ast.File
增加 JSONString
方法如下:
1 | func (p *File) JSONString() string { |
使用案例还是之前的最小uGo程序:
1 | package main |
修改 main 函数,使其方便我们进行测试:
1 | func main() { |
最后的效果如下:
1 | 00: 5 : "package" // ../hello.ugo:1:1 |