如何写一个简单的解释器(Interpreter)-7

之前答应过你,今天我们主要讨论一下贯穿全系列的最核心的数据结构。系好安全带准备出发!

到现在我们已经有了自己的解释器,只要表达式里面有加减乘除这种语言结构,它就能够解析成功。这种解释器叫此法导向解释器。它通常只扫描代码一遍,适用于简单的语言。如果想做到分析更复杂的Pascal语言的结构,我们需要构建IR,也就是中间表示。我们的解析器负责生成IR,我们的解释器负责解释IR。

业界的经验告诉我们,树形结构适合作为IR的数据结构。

我们快速复习一下树这个概念吧:

  • 一棵树有很多节点,这些点是按照层级来组成。
  • 除了根节点,每一个节点,都有一个父节点。
  • 这张图里面,标星星的那个节点就是父节点的。二和七是他的两个孩子从左往右,依次排列。
  • 没有孩子的节点,叫叶子节点。
  • 有一个或者多个孩子的节点,叫中间节点,当然不包括根节点。
  • 有个孩子,也有可能是一个完整的书,我们叫他自述。下面这张图片里。标志星星的节点和加号这个节点就组成了一个完整的子树。
  • 计算机系的学生,画树的时候都是从上往下画的。

上面这个数就是表示,2×7+3。

我们用的IR有个名字,叫AST抽象语法书,或者抽象词法树。我们先讲讲怎么解析一个树型结构的数据,再讲讲为什么AST是这么适合表示IR,比解析树更合适。

什么是解析树?好,我们先看看解析树这个东西。比如 2 * 7 + 3的解析树是:

可以看到:

  • 解析树忠实的记录了识别输入元素的规则。
  • 树根是语法的起始符号。
  • 每一个中间节点表示一个非终结符,也就是语法的规则函数,想expr、term或者factor。
  • 叶子节点表示的是token。

我开发了一个小工具,genptdot.py ,可以把树用图画出来。

$ python genptdot.py "14 + 2 * 3 - 6 / 2" > \
  parsetree.dot && dot -Tpng -o parsetree.png parsetree.dot

输出的图片是:

那什么是AST呢?

还是拿2 * 7 + 3来举例,AST和解析树的区别如下图所示:

AST更简单,更抓住了问题的本质。系统总结一下就是:

  • AST把操作符作为根和中间节点,把操作树作为孩子。
  • AST的中间节点不表示语法规则。
  • AST忽略了真正词法的一些细节,这也是抽象两个字的含义,没有rule节点和括号节点。
  • AST的结构看起来更密集一些。

7 + ((2 + 3))的AST如下表示:

那怎么表示次序呢?比如“X在Y之前” 。很简单,只需要把X放到Y的下面。

比如下面给出了更多的例子:

 

可以看到,优先级越高的运算符,在树种的位置越低。

下面是代码时间。

首先我们定义一个AST的节点类,从下面这个基类中扩展出来。

class AST(object):
    pass

回想一下,AST表示的是操作符和操作数的模型。至今为止,我们已经有了四种操作符和整数这个操作数。我们可以给每一个操作符都创建一个节点,比如AddNode, SubNode, MulNode和 DivNode。但是,更好的做法是用一个BinOp类,来表示四个操作符。BinOP是双目操作符的简称,表示有两个操作数的操作符。

class BinOp(AST):
    def __init__(self, left, op, right):
        self.left = left
        self.token = self.op = op
        self.right = right

构造函数里面有left,op和right三个参数。left和right表示做操作数和右操作数。op表示操作符这个token,也就是Token(PLUS, ‘+’)或者Token(MINUS, ‘-‘)。

整数这个token是这么表示的:

class Num(AST):
    def __init__(self, token):
        self.token = token
        self.value = token.value

2 * 7 + 3的AST生成过程是:

>>> from spi import Token, MUL, PLUS, INTEGER, Num, BinOp
>>>
>>> mul_token = Token(MUL, '*')
>>> plus_token = Token(PLUS, '+')
>>> mul_node = BinOp(
...     left=Num(Token(INTEGER, 2)),
...     op=mul_token,
...     right=Num(Token(INTEGER, 7))
... )
>>> add_node = BinOp(
...     left=mul_node,
...     op=plus_token,
...     right=Num(Token(INTEGER, 3))
... )

 

解释器的代码更新过后,可以生成AST并解析出最终的计算结果:

class AST(object):
    pass


class BinOp(AST):
    def __init__(self, left, op, right):
        self.left = left
        self.token = self.op = op
        self.right = right


class Num(AST):
    def __init__(self, token):
        self.token = token
        self.value = token.value


class Parser(object):
    def __init__(self, lexer):
        self.lexer = lexer
        # set current token to the first token taken from the input
        self.current_token = self.lexer.get_next_token()

    def error(self):
        raise Exception('Invalid syntax')

    def eat(self, token_type):
        # compare the current token type with the passed token
        # type and if they match then "eat" the current token
        # and assign the next token to the self.current_token,
        # otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        """factor : INTEGER | LPAREN expr RPAREN"""
        token = self.current_token
        if token.type == INTEGER:
            self.eat(INTEGER)
            return Num(token)
        elif token.type == LPAREN:
            self.eat(LPAREN)
            node = self.expr()
            self.eat(RPAREN)
            return node

    def term(self):
        """term : factor ((MUL | DIV) factor)*"""
        node = self.factor()

        while self.current_token.type in (MUL, DIV):
            token = self.current_token
            if token.type == MUL:
                self.eat(MUL)
            elif token.type == DIV:
                self.eat(DIV)

            node = BinOp(left=node, op=token, right=self.factor())

        return node

    def expr(self):
        """
        expr   : term ((PLUS | MINUS) term)*
        term   : factor ((MUL | DIV) factor)*
        factor : INTEGER | LPAREN expr RPAREN
        """
        node = self.term()

        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
            elif token.type == MINUS:
                self.eat(MINUS)

            node = BinOp(left=node, op=token, right=self.term())

        return node

    def parse(self):
        return self.expr()

AST的节点生成过程是这样的:每一个BinOp节点接受node变量当前的数值,作为左孩子。term和factor作为右孩子。下面是个好例子。

我又写了一个小工具来画一下表达式:

$ python genastdot.py "7 + 3 * (10 / (12 / (3 + 1) - 1))" > \
  ast.dot && dot -Tpng -o ast.png ast.dot

 genastdot.py 工具的链接是这儿。更多的练习——2 * 7 + 3:

其实,使用后序遍历(一种深度遍历)就可以从根节点出来,一直递归的遍历每一个节点的孩子节点,从左向右,一直往最深的子节点那遍历。

伪代码如下:

为什么我们要用后序遍历呢?因为优先级高的运算操作被放到了树的下面,这样我们必须把下面的都计算清楚了,才能继续计算上面的算式。

 Visitor pattern的代码如下,把访问的核心代码封装成访问者模式的模板,是为了更好的解耦。

class NodeVisitor(object):
    def visit(self, node):
        method_name = 'visit_' + type(node).__name__
        visitor = getattr(self, method_name, self.generic_visit)
        return visitor(node)

    def generic_visit(self, node):
        raise Exception('No visit_{} method'.format(type(node).__name__))

And here is the source code of our Interpreter class that inherits from the NodeVisitor class and implements different methods that have the form visit_NodeType, where NodeType is replaced with the node’s class name like BinOpNum and so on:

NodeVisitor的子类恰恰是我们的interpreter类。它额外实现了visit_NodeType的不同方法。这里的NodeType被节点的类型替换了,比如BinOp,Num等等的这些类名。

class Interpreter(NodeVisitor):
    def __init__(self, parser):
        self.parser = parser

    def visit_BinOp(self, node):
        if node.op.type == PLUS:
            return self.visit(node.left) + self.visit(node.right)
        elif node.op.type == MINUS:
            return self.visit(node.left) - self.visit(node.right)
        elif node.op.type == MUL:
            return self.visit(node.left) * self.visit(node.right)
        elif node.op.type == DIV:
            return self.visit(node.left) / self.visit(node.right)

    def visit_Num(self, node):
        return node.value

 

对AST节点的访问逻辑,跟AST节点本身的逻辑彻底解耦了。你可以看到AST节点的类里面(也就是BinOP和Num类)并没有提供访问自己数据的代码。相反这些代码被放入了Interprerer类中。这个类就是访问代码的类。

还有,我们并没有用大量的if else语句,而是用了如下的语句:

def visit(node):
    node_type = type(node).__name__
    if node_type == 'BinOp':
        return self.visit_BinOp(node)
    elif node_type == 'Num':
        return self.visit_Num(node)
    elif ...
    # ...

或者这样:

def visit(node):
    if isinstance(node, BinOp):
        return self.visit_BinOp(node)
    elif isinstance(node, Num):
        return self.visit_Num(node)
    elif ...

NodeVisitor的 visit 方法很通用的,它根据节点的类型去分发不同的方法调用。比如如果节点的类型是BinOp,那visit方法就激活visit_BinOp方法。

Python 有个模块 ast 使用了相同的遍历机制。感兴趣就试试。

generic_visit 方法是备用方法,他会抛异常,来警告它遇到了一个没有对应visit_NodeType方法的节点。 

我们尝试构建 2 * 7 + 3 的AST,并看看不同的visit方法是怎么计算出结果的。

>>> from spi import Token, MUL, PLUS, INTEGER, Num, BinOp
>>>
>>> mul_token = Token(MUL, '*')
>>> plus_token = Token(PLUS, '+')
>>> mul_node = BinOp(
...     left=Num(Token(INTEGER, 2)),
...     op=mul_token,
...     right=Num(Token(INTEGER, 7))
... )
>>> add_node = BinOp(
...     left=mul_node,
...     op=plus_token,
...     right=Num(Token(INTEGER, 3))
... )
>>> from spi import Interpreter
>>> inter = Interpreter(None)
>>> inter.visit(add_node)
17

我把根节点add_node传入visit方法,然后就连锁反应遍历了整个树,得到了17这个结果。

完整的interpreter代码如下:

""" SPI - Simple Pascal Interpreter """

###############################################################################
#                                                                             #
#  LEXER                                                                      #
#                                                                             #
###############################################################################

# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, MUL, DIV, LPAREN, RPAREN, EOF = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', '(', ')', 'EOF'
)


class Token(object):
    def __init__(self, type, value):
        self.type = type
        self.value = value

    def __str__(self):
        """String representation of the class instance.

        Examples:
            Token(INTEGER, 3)
            Token(PLUS, '+')
            Token(MUL, '*')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()


class Lexer(object):
    def __init__(self, text):
        # client string input, e.g. "4 + 2 * 3 - 6 / 2"
        self.text = text
        # self.pos is an index into self.text
        self.pos = 0
        self.current_char = self.text[self.pos]

    def error(self):
        raise Exception('Invalid character')

    def advance(self):
        """Advance the `pos` pointer and set the `current_char` variable."""
        self.pos += 1
        if self.pos > len(self.text) - 1:
            self.current_char = None  # Indicates end of input
        else:
            self.current_char = self.text[self.pos]

    def skip_whitespace(self):
        while self.current_char is not None and self.current_char.isspace():
            self.advance()

    def integer(self):
        """Return a (multidigit) integer consumed from the input."""
        result = ''
        while self.current_char is not None and self.current_char.isdigit():
            result += self.current_char
            self.advance()
        return int(result)

    def get_next_token(self):
        """Lexical analyzer (also known as scanner or tokenizer)

        This method is responsible for breaking a sentence
        apart into tokens. One token at a time.
        """
        while self.current_char is not None:

            if self.current_char.isspace():
                self.skip_whitespace()
                continue

            if self.current_char.isdigit():
                return Token(INTEGER, self.integer())

            if self.current_char == '+':
                self.advance()
                return Token(PLUS, '+')

            if self.current_char == '-':
                self.advance()
                return Token(MINUS, '-')

            if self.current_char == '*':
                self.advance()
                return Token(MUL, '*')

            if self.current_char == '/':
                self.advance()
                return Token(DIV, '/')

            if self.current_char == '(':
                self.advance()
                return Token(LPAREN, '(')

            if self.current_char == ')':
                self.advance()
                return Token(RPAREN, ')')

            self.error()

        return Token(EOF, None)


###############################################################################
#                                                                             #
#  PARSER                                                                     #
#                                                                             #
###############################################################################

class AST(object):
    pass


class BinOp(AST):
    def __init__(self, left, op, right):
        self.left = left
        self.token = self.op = op
        self.right = right


class Num(AST):
    def __init__(self, token):
        self.token = token
        self.value = token.value


class Parser(object):
    def __init__(self, lexer):
        self.lexer = lexer
        # set current token to the first token taken from the input
        self.current_token = self.lexer.get_next_token()

    def error(self):
        raise Exception('Invalid syntax')

    def eat(self, token_type):
        # compare the current token type with the passed token
        # type and if they match then "eat" the current token
        # and assign the next token to the self.current_token,
        # otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        """factor : INTEGER | LPAREN expr RPAREN"""
        token = self.current_token
        if token.type == INTEGER:
            self.eat(INTEGER)
            return Num(token)
        elif token.type == LPAREN:
            self.eat(LPAREN)
            node = self.expr()
            self.eat(RPAREN)
            return node

    def term(self):
        """term : factor ((MUL | DIV) factor)*"""
        node = self.factor()

        while self.current_token.type in (MUL, DIV):
            token = self.current_token
            if token.type == MUL:
                self.eat(MUL)
            elif token.type == DIV:
                self.eat(DIV)

            node = BinOp(left=node, op=token, right=self.factor())

        return node

    def expr(self):
        """
        expr   : term ((PLUS | MINUS) term)*
        term   : factor ((MUL | DIV) factor)*
        factor : INTEGER | LPAREN expr RPAREN
        """
        node = self.term()

        while self.current_token.type in (PLUS, MINUS):
            token = self.current_token
            if token.type == PLUS:
                self.eat(PLUS)
            elif token.type == MINUS:
                self.eat(MINUS)

            node = BinOp(left=node, op=token, right=self.term())

        return node

    def parse(self):
        return self.expr()


###############################################################################
#                                                                             #
#  INTERPRETER                                                                #
#                                                                             #
###############################################################################

class NodeVisitor(object):
    def visit(self, node):
        method_name = 'visit_' + type(node).__name__
        visitor = getattr(self, method_name, self.generic_visit)
        return visitor(node)

    def generic_visit(self, node):
        raise Exception('No visit_{} method'.format(type(node).__name__))


class Interpreter(NodeVisitor):
    def __init__(self, parser):
        self.parser = parser

    def visit_BinOp(self, node):
        if node.op.type == PLUS:
            return self.visit(node.left) + self.visit(node.right)
        elif node.op.type == MINUS:
            return self.visit(node.left) - self.visit(node.right)
        elif node.op.type == MUL:
            return self.visit(node.left) * self.visit(node.right)
        elif node.op.type == DIV:
            return self.visit(node.left) / self.visit(node.right)

    def visit_Num(self, node):
        return node.value

    def interpret(self):
        tree = self.parser.parse()
        return self.visit(tree)


def main():
    while True:
        try:
            try:
                text = raw_input('spi> ')
            except NameError:  # Python3
                text = input('spi> ')
        except EOFError:
            break
        if not text:
            continue

        lexer = Lexer(text)
        parser = Parser(lexer)
        interpreter = Interpreter(parser)
        result = interpreter.interpret()
        print(result)


if __name__ == '__main__':
    main()

运行结果是:

$ python spi.py
spi> 7 + 3 * (10 / (12 / (3 + 1) - 1))
22
spi> 7 + 3 * (10 / (12 / (3 + 1) - 1)) / (2 + 3) - 5 - 3 + (8)
10
spi> 7 + (((3 + 2)))
12


今天所有这些的概念,他们之间的关系是:

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的G代码解释器的QT实现: 1. 创建一个新的QT项目,选择“应用程序”模板。 2. 在项目中添加一个新的类,命名为“GCodeInterpreter”。 3. 在GCodeInterpreter类中添加以下变量和函数: ```c++ private: QString m_gCode; // 存储G代码 int m_lineNumber; // 当前执行的行号 QMap<QString, double> m_variables; // 存储变量及其值的映射表 double getVariableValue(const QString& variableName); // 获取变量的值 void setVariableValue(const QString& variableName, double value); // 设置变量的值 void executeCurrentLine(); // 执行当前行 ``` 4. 实现getVariableValue和setVariableValue函数,用于获取和设置变量的值: ```c++ double GCodeInterpreter::getVariableValue(const QString& variableName) { if (m_variables.contains(variableName)) { return m_variables[variableName]; } else { return 0.0; } } void GCodeInterpreter::setVariableValue(const QString& variableName, double value) { m_variables[variableName] = value; } ``` 5. 实现executeCurrentLine函数,用于执行当前行的G代码: ```c++ void GCodeInterpreter::executeCurrentLine() { QString line = m_gCode.split('\n')[m_lineNumber - 1].trimmed(); // 获取当前行的G代码 // 解析G代码 QStringList parts = line.split(' '); QString command = parts[0].toUpper(); QStringList arguments = parts.mid(1); // 根据G代码命令执行相应的操作 if (command == "G00" || command == "G01") { // 移动 double x = getVariableValue("X"); double y = getVariableValue("Y"); double z = getVariableValue("Z"); for (const QString& argument : arguments) { QStringList argParts = argument.split('='); QString argName = argParts[0].toUpper(); double argValue = argParts[1].toDouble(); if (argName == "X") { x = argValue; } else if (argName == "Y") { y = argValue; } else if (argName == "Z") { z = argValue; } } // 执行移动操作 qDebug() << "Moving to (" << x << ", " << y << ", " << z << ")"; } else if (command == "G02" || command == "G03") { // 圆弧 double x = getVariableValue("X"); double y = getVariableValue("Y"); double z = getVariableValue("Z"); double i = getVariableValue("I"); double j = getVariableValue("J"); double k = getVariableValue("K"); double radius = qSqrt(qPow(i, 2) + qPow(j, 2) + qPow(k, 2)); for (const QString& argument : arguments) { QStringList argParts = argument.split('='); QString argName = argParts[0].toUpper(); double argValue = argParts[1].toDouble(); if (argName == "X") { x = argValue; } else if (argName == "Y") { y = argValue; } else if (argName == "Z") { z = argValue; } else if (argName == "I") { i = argValue; } else if (argName == "J") { j = argValue; } else if (argName == "K") { k = argValue; radius = qSqrt(qPow(i, 2) + qPow(j, 2) + qPow(k, 2)); } } // 执行圆弧操作 qDebug() << "Drawing arc with radius" << radius << "from (" << x << ", " << y << ", " << z << ")"; } else if (command == "M02" || command == "M30") { // 停止 qDebug() << "Stopping execution"; } else if (command == "M03" || command == "M04") { // 开启刀具 qDebug() << "Turning on tool"; } else if (command == "M05") { // 关闭刀具 qDebug() << "Turning off tool"; } else if (command == "M06") { // 更换刀具 qDebug() << "Changing tool"; } else if (command == "M08") { // 开启冷却液 qDebug() << "Turning on coolant"; } else if (command == "M09") { // 关闭冷却液 qDebug() << "Turning off coolant"; } m_lineNumber++; // 前往下一行 } ``` 6. 在GCodeInterpreter类中添加以下公共函数,用于设置G代码和执行G代码: ```c++ public: void setGCode(const QString& gCode); // 设置G代码 void execute(); // 执行G代码 ``` 7. 实现setGCode函数,用于设置G代码: ```c++ void GCodeInterpreter::setGCode(const QString& gCode) { m_gCode = gCode.trimmed(); m_lineNumber = 1; m_variables.clear(); setVariableValue("X", 0.0); setVariableValue("Y", 0.0); setVariableValue("Z", 0.0); } ``` 8. 实现execute函数,用于执行G代码: ```c++ void GCodeInterpreter::execute() { while (m_lineNumber <= m_gCode.split('\n').count()) { executeCurrentLine(); } } ``` 9. 在QT项目中的主窗口中添加一个文本框和一个按钮,用于输入G代码和执行G代码。 10. 在主窗口的构造函数中连接按钮的clicked信号到一个槽函数,该槽函数中创建一个GCodeInterpreter对象并调用setGCode和execute函数,将文本框中的G代码作为参数传递给GCodeInterpreter对象。 ```c++ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // ... connect(ui->executeButton, &QPushButton::clicked, this, [this]() { GCodeInterpreter interpreter; interpreter.setGCode(ui->gCodeTextEdit->toPlainText()); interpreter.execute(); }); } ``` 11. 运行QT项目,输入一些G代码并点击执行按钮,查看输出结果。 注意:这只是一个简单的G代码解释器的实现,实际中可能需要添加更多的G代码命令和参数解析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值