本文是Ruslan Spivak的《一起来写个简单的解释器》系列的第8篇(第7篇)。
如果你对kotlin感兴趣,可以参考我的spi-kotlin

今天我们要学习两种一元操作符:一元加(+)和一元减(-)。

今天的很多内容都是以上一篇文章为基础的。如果上一章的有些内容你已经记不清了,可以先回去复习一下。记住:重复是学习之母。

闲话少说,今天你会学到:

  • 扩展语法,增加对一元加和一元减的支持
  • 增加新的 UnaryOp AST节点类
  • 扩展分析器,支持 UnaryOp 类型的AST节点
  • 扩展翻译器,增加新的visit_UnaryOp方法来处理一元操作符

开始吧?

到目前为止我们处理的都是二元操作符(+, -, *, /),也就是操作两个操作数的操作符。那么一元操作符是什么呢?一元操作符 是只操作一个操作数的操作符。一元加和一元减操作符的运算规则如下:

  • 一元减操作符的结果是它的操作数的相反数
  • 一元加操作符的结果是它的操作数自身
  • 一元操作符的优先级高于二元操作符 (加减乘除)

在表达式“+ - 3”中,第一个操作符“+”表示一元加操作,第二个操作符“-”表示一元减操作。表达式“+ - 3”等价于“+(-(3))”,也就是-3。你也可以把表达式里的-3看做一个负数,不过在这篇文章里我们把它当做一个一元减操作符和一个正数3:

exp1

再来看另一个表达式,“5 - - 2”:

exp2

在表达式“5 - - 2”里,第一个“-”表示二元减操作符,第二个“-”表示一元减操作符。

下面是更多的例子:

exp3

exp4

现在,我们要升级语法规则,来支持一元加和一元减操作符。首先先修改 factor 规则,在其中添加一元操作符。把一元操作符加在 factor 规则里是因为它的优先级要高于二元操作符(加减乘除)。

现在 factor 的规则从这个样子:

factor_before

变成了这个样子:

factor_before

可以看到,factor 规则引用了自己,这样 factor 就能扩展出像“- - - + - 3”这样的表达式了。

下面是现在的完整语法规则:

factor_before

下一步是增加表示一元操作符的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树长这样: lsbasi_part8_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

lsbasi_part8_genastdot_01

$ python genastdot.py "+ 3" > ast.dot && dot -Tpng -o ast.png ast.dot

lsbasi_part8_genastdot_02

$ python genastdot.py "5 - - - + - 3" > ast.dot && dot -Tpng -o ast.png ast.dot

lsbasi_part8_genastdot_03

$ python genastdot.py "5 - - - + - (3 + 4) - +2" \
  > ast.dot && dot -Tpng -o ast.png ast.dot

lsbasi_part8_genastdot_04

给你留个小练习: lsbasi_part8_exercises



下面是我推荐的一些书籍列表,它们对你学习解释器和编译器有帮助:

  1. Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
  2. Writing Compilers and Interpreters: A Software Engineering Approach
  3. Modern Compiler Implementation in Java
  4. Modern Compiler Design
  5. Compilers: Principles, Techniques, and Tools (2nd Edition)

顺便,我正在写一本叫做“Let’s Build A Web Server: First Steps”的书,主要内容是关于怎么从零开始编写一个简单的web服务器。想要先睹为快的话可以点击这里这里,和这里。想知道这本书的最近更新和出版日期的话,可以到邮件列表里询问。 ⤧  Next post 给静态博客添加页面切换效果 ⤧  Previous post 一起来写个简单的解释器(7)