一起来写个简单的解释器(8)

英文出处:Ruslan’s Blog

今天我们将讨论一元运算符,即一元加(+)和一元减(-)运算符。

现在很多材料都是基于上一篇文章,所以如果你需要复习一下,那么回到第七部分,再重新翻一遍。记住:重复是所有学习之母。

话虽如此,这就是今天你要做的事情:

  • 扩展语法来处理一元加号和一元减号运算符
  • 添加一个新的 UnaryOp AST节点类
  • 扩展解析器以生成带有UnaryOp节点的AST
  • 扩展解释器并添加一个新的visit_UnaryOp方法来解释一元运算符

我们开始吧!

到目前为止,我们只使用二元运算符(+, -, *, /),也就是操作两个操作数的运算符。

什么是一元运算符呢?一元运算符是仅在一个操作数上操作的运算符。

这里是一元加号和一元减号运算符的规则:

  • 一元减号 (-) 运算符对其数字操作数的求反
  • 一元加 (+) 运算符产生的数字操作数没有改变
  • 一元运算符的优先级高于二元运算符 +, - ,*和/

在表达式“+ - 3”中,第一个“+”运算符表示一元加运算,第二个“ - ”运算符表示一元减运算。表达式“+ - 3”相当于 “+ (- (3))”即等于-3。也可以说表达式中的-3是一个负整数,但在我们的例子中,我们把它作为一个一元减运算符,其中3作为其正整数操作数:

图1

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

图1

在表达式“5 - - 2”中,第一个 “-”表示二元减运算符,第二个“-”表示一元减运算符,求反。看下更多的例子:

图1

图1

现在让我们更新我们的语法来包含一元加和一元减运算符。 我们将修改*factor*规则并在那里添加一元运算符,因为一元运算符比二元运算符 +, -,*和/具有更高的优先级。

这是我们当前的*factor*规则:

图1

这是我们为处理一元加和减运算符而更新后的*factor*规则:

图1

正如你所看到的,我扩展了factor规则来引用它自己,这使得我们可以推导出像“ - - - + - 3”这样有多个一元运算符的合法表达式。

这里是完整的语法,现在它可以推导出包含一元加法和一元减法运算符的表达式:

图1

下一步是添加一个AST节点类来表示一元运算符。

这个会做:

class UnaryOp(AST):
    def __init__(self, op, expr):
        self.token = self.op = op
        self.expr = expr

构造函数有两个参数:op表示一元运算符标记(加号或减号),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终端里的操作:

>>> 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长这样:

图1

直接从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

图1

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

图1

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

图1

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

图1

这里有新的练习给你:

图1

安装Free Pascal,编译并运行testunary.pas,验证与使用spi解释器生成的结果是否相同。

这就是今天的全部内容。 在下一篇文章中,我们将处理赋值语句。 敬请期待,再见!

以下是我推荐的书籍列表,可以帮助你学习解释器和编译器:

  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服务器。你可以在这里这里这里查看这本书。