一起来写个简单的解释器(8)
本文是Ruslan Spivak的《一起来写个简单的解释器》系列的第8篇(第7篇)。
如果你对kotlin感兴趣,可以参考我的spi-kotlin。
今天我们要学习两种一元操作符:一元加(+)和一元减(-)。
今天的很多内容都是以上一篇文章为基础的。如果上一章的有些内容你已经记不清了,可以先回去复习一下。记住:重复是学习之母。
闲话少说,今天你会学到:
- 扩展语法,增加对一元加和一元减的支持
- 增加新的 UnaryOp AST节点类
- 扩展分析器,支持 UnaryOp 类型的AST节点
- 扩展翻译器,增加新的visit_UnaryOp方法来处理一元操作符
开始吧?
到目前为止我们处理的都是二元操作符(+, -, *, /),也就是操作两个操作数的操作符。那么一元操作符是什么呢?一元操作符 是只操作一个操作数的操作符。一元加和一元减操作符的运算规则如下:
- 一元减操作符的结果是它的操作数的相反数
- 一元加操作符的结果是它的操作数自身
- 一元操作符的优先级高于二元操作符 (加减乘除)
在表达式“+ - 3”中,第一个操作符“+”表示一元加操作,第二个操作符“-”表示一元减操作。表达式“+ - 3”等价于“+(-(3))”,也就是-3。你也可以把表达式里的-3看做一个负数,不过在这篇文章里我们把它当做一个一元减操作符和一个正数3:
再来看另一个表达式,“5 - - 2”:
在表达式“5 - - 2”里,第一个“-”表示二元减操作符,第二个“-”表示一元减操作符。
下面是更多的例子:
现在,我们要升级语法规则,来支持一元加和一元减操作符。首先先修改 factor 规则,在其中添加一元操作符。把一元操作符加在 factor 规则里是因为它的优先级要高于二元操作符(加减乘除)。
现在 factor 的规则从这个样子:
变成了这个样子:
可以看到,factor 规则引用了自己,这样 factor 就能扩展出像“- - - + - 3”这样的表达式了。
下面是现在的完整语法规则:
下一步是增加表示一元操作符的AST节点类型:
class UnaryOp(AST):
def __init__(self, op, expr):
self.token = self.op = op
self.expr = expr
构造器接收两个参数:op 表示一元操作符的token(加或减),expr 表示一个AST节点。
升级后的语法规则中, factor 规则发生了变化,所以我们的分析器中的相应方法 factor 也需要进行修改,增加代码来处理“(PLUS | MINUS) factor”这条子规则。
def factor(self):
"""factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN"""
token = self.current_token
if token.type == PLUS:
self.eat(PLUS)
node = UnaryOp(token, self.factor())
return node
elif token.type == MINUS:
self.eat(MINUS)
node = UnaryOp(token, self.factor())
return node
elif token.type == INTEGER:
self.eat(INTEGER)
return Num(token)
elif token.type == LPAREN:
self.eat(LPAREN)
node = self.expr()
self.eat(RPAREN)
return node
接下来扩展 Interpreter 类,增加 visit_UnaryOp 方法来翻译一元节点:
def visit_UnaryOp(self, node):
op = node.op.type
if op == PLUS:
return +self.visit(node.expr)
elif op == MINUS:
return -self.visit(node.expr)
继续前进!
我们先手工构建表达式“5 - - - 2”的AST,把它传给解释器,来验证一下我们的 visit_UnaryOp 方法。你可以在Python shell里这样测试:
>>> from spi import BinOp, UnaryOp, Num, MINUS, INTEGER, Token
>>> five_tok = Token(INTEGER, 5)
>>> two_tok = Token(INTEGER, 2)
>>> minus_tok = Token(MINUS, '-')
>>> expr_node = BinOp(
... Num(five_tok),
... minus_tok,
... UnaryOp(minus_token, UnaryOp(minus_token, Num(two_tok)))
... )
>>> from spi import Interpreter
>>> inter = Interpreter(None)
>>> inter.visit(expr_node)
3
上面生成的AST树长这样:
直接从GitHub下载完整的解释器代码,试试更新后的解释器能不能正确求解包含一元运算符的数学表达式。
下面是一些测试用例:
$ python spi.py
spi> - 3
-3
spi> + 3
3
spi> 5 - - - + - 3
8
spi> 5 - - - + - (3 + 4) - +2
10
我也更新了genastdot.py工具,现在它能处理一元操作符了。下面是一些生成含有一元操作符的表达式的AST图的例子:
$ python genastdot.py "- 3" > ast.dot && dot -Tpng -o ast.png ast.dot
$ python genastdot.py "+ 3" > ast.dot && dot -Tpng -o ast.png ast.dot
$ python genastdot.py "5 - - - + - 3" > ast.dot && dot -Tpng -o ast.png ast.dot
$ python genastdot.py "5 - - - + - (3 + 4) - +2" \
> ast.dot && dot -Tpng -o ast.png ast.dot
给你留个小练习:
- 安装Free Pascal,编译并运行testunary.pas,检查结果是否与spi生成的结果一致。
下面是我推荐的一些书籍列表,它们对你学习解释器和编译器有帮助:
- Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
- Writing Compilers and Interpreters: A Software Engineering Approach
- Modern Compiler Implementation in Java
- Modern Compiler Design
- Compilers: Principles, Techniques, and Tools (2nd Edition)
顺便,我正在写一本叫做“Let’s Build A Web Server: First Steps”的书,主要内容是关于怎么从零开始编写一个简单的web服务器。想要先睹为快的话可以点击这里,这里,和这里。想知道这本书的最近更新和出版日期的话,可以到邮件列表里询问。 ⤧ Next post 给静态博客添加页面切换效果 ⤧ Previous post 一起来写个简单的解释器(7)