编译原理构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 9.)(笔记)语法分析(未完,先搁置了!)

Posted Dontla

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编译原理构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 9.)(笔记)语法分析(未完,先搁置了!)相关的知识,希望对你有一定的参考价值。

【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 9.)

文章目录


我记得当我在大学(很久以前)学习系统编程时,我相信唯一“真正”的语言是汇编和 C。而 Pascal 是——怎么说好一点——一种非常高级的语言不想知道幕后发生了什么的应用程序开发人员。

那时我几乎不知道我会用 Python 编写几乎所有东西(并且喜欢它的每一点)来支付我的账单,而且我还会因为我在第一篇文章中提到的原因为 Pascal 编写解释器和编译器该系列的。

这些天,我认为自己是一个编程语言爱好者,我对所有语言及其独特的功能都很着迷。话虽如此,我必须指出,我比其他语言更喜欢使用某些语言。我有偏见,我将是第一个承认这一点的人。😃

这是我之前:


好的,让我们进入正题。以下是您今天要学习的内容:

如何解析和解释 Pascal 程序定义。
1、如何解析和解释复合语句(compound statements)。
2、如何解析和解释赋值语句(assignment statements),包括变量(variables)。
3、关于符号表(symbol tables)以及如何存储和查找变量的一些知识。

我将使用以下示例 Pascal-like 程序来介绍新概念:

BEGIN
    BEGIN
        number := 2;
        a := number;
        b := 10 * a + 10 * number / 4;
        c := a - - b
    END;
    x := 11;
END.

您可以说,按照本系列的前几篇文章,到目前为止您编写的命令行解释器是一个很大的跳跃,但我希望这种跳跃会带来兴奋。它不再“只是”一个计算器,我们在这里变得认真了,Pascal 是认真的。😃

让我们深入了解新语言结构的语法图(syntax diagrams)及其相应的语法规则(grammar rules)。

在你的标记上:准备好了。放。出发!




1、我将从描述什么是 Pascal程序开始。Pascal程序由一个以点结尾的复合语句组成。下面是一个程序示例:

“BEGIN  END.

我必须提示,这不是一个完整的程序定义,我们将在本系列的后面对其进行扩展。

2、什么是复合语句?复合语句(compound statement)是标有块BEGIN和END 语句包括其它复合语句,它可以包含一个列表(list)(可能为空)。复合语句中的每一条语句,除了最后一条,都必须以分号(semicolon)结束。块中的最后一条语句可能有也可能没有终止分号。以下是一些有效复合语句的示例:

“BEGIN END”
“BEGIN a := 5; x := 11 END”
“BEGIN a := 5; x := 11; END”
“BEGIN BEGIN a := 5 END; x := 11 END”

3、语句列表(statement list )是一个复合语句中的零条或多个语句的列表。有关一些示例,请参见上文。

4、语句可以是一个复合语句,一个赋值语句,或者它可以是一个空的 语句。

5、赋值语句(assignment statement)是一个变量后面跟着个ASSIGN标记(两个字符,“:”和“=”),后面再跟着个表达式。

“a := 11”
“b := a + 9 - 5 * 2

6、变量是一个标识符。我们为变量使用ID标记。标记的值将是变量的名称,如“a”、“number”等。在以下代码块中,‘a’ 和 ‘b’ 是变量:

“BEGIN a := 11; b := a + 9 - 5 * 2 END”

7、一个空的声明表示,没有进一步的生成语法规则。我们使用empty_statement语法规则来指示 解析器中statement_list的结尾,并允许像“ BEGIN END ”中那样的空复合语句。

8、factor规则被用来更新处理变量。

现在让我们来看看我们完整的语法:

    program : compound_statement DOT

    compound_statement : BEGIN statement_list END

    statement_list : statement
                   | statement SEMI statement_list

    statement : compound_statement
              | assignment_statement
              | empty

    assignment_statement : variable ASSIGN expr

    empty :

    expr: term ((PLUS | MINUS) term)*

    term: factor ((MUL | DIV) factor)*

    factor : PLUS factor
           | MINUS factor
           | INTEGER
           | LPAREN expr RPAREN
           | variable

    variable: ID

您可能已经注意到,我没有在复合语句规则中使用星号“*” 来表示零次或多次重复,而是明确指定了statement_list规则。这是表示“零或多个”操作的另一种方式,当我们查看本系列后面的PLY等解析器生成器时,它会派上用场。我还将“( PLUS | MINUS ) factor ”子规则拆分为两个单独的规则。

为了支持更新的语法,我们需要对词法分析器、解析器和解释器进行一些更改。让我们一一回顾这些变化。

以下是词法分析器更改的摘要:


1、为了支持 Pascal 程序的定义、复合语句、赋值语句和变量,我们的词法分析器需要返回新的标记:

  • BEGIN(标记复合语句的开始)
  • END(标记复合语句的结束)
  • DOT(Pascal 程序定义所需的点字符“.”的标记)
  • ASSIGN(两个字符序列“:=”的标记)。在 Pascal 中,赋值运算符与 C、Python、Java、Rust 或 Go 等许多其他语言不同,在这些语言中,您可以使用单个字符“=”来表示赋值
  • SEMI(分号字符“;”的标记,用于标记复合语句内的语句结束)
  • ID(有效标识符的标记。标识符以字母字符开头,后跟任意数量的字母数字字符)

2、有时,为了能够区分以相同字符开头的不同标记,(':' vs ':=' or '==' vs '=>' )我们需要查看输入缓冲区而不实际消耗下一个字符。出于这个特殊目的,我引入了一个peek 方法,它将帮助我们标记赋值语句。该方法不是严格要求的,但我想我会在本系列的前面介绍它,它也会使get_next_token 方法更简洁一些。它所做的只是从文本缓冲区返回下一个字符,而不增加self.pos变量。这是方法本身:

def peek(self):
    peek_pos = self.pos + 1
    if peek_pos > len(self.text) - 1:
        return None
    else:
        return self.text[peek_pos]

3、因为 Pascal 变量和保留关键字都是标识符,所以我们将它们的处理合并到一个称为_id 的方法中。它的工作方式是词法分析器使用一系列字母数字字符,然后检查该字符序列是否为保留字(reserved word)。如果是,则为该保留关键字返回一个预先构造的标记。如果它不是保留关键字,则返回一个新的ID令牌,其值为字符串(词素【lexeme】)。我打赌此时你会想,“天哪,给我看看代码。” :) 这里是:

RESERVED_KEYWORDS = {
    'BEGIN': Token('BEGIN', 'BEGIN'),
    'END': Token('END', 'END'),
}

def _id(self):
    """Handle identifiers(标识符) and reserved keywords"""
    result = ''
    while self.current_char is not None and self.current_char.isalnum():
        result += self.current_char
        self.advance()

    token = RESERVED_KEYWORDS.get(result, Token(ID, result))
    return token

现在让我们看看主词法分析器方法get_next_token的变化:

def get_next_token(self):
    while self.current_char is not None:
        ...
        if self.current_char.isalpha():
            return self._id()

        if self.current_char == ':' and self.peek() == '=':
            self.advance()
            self.advance()
            return Token(ASSIGN, ':=')

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

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

是时候看看我们闪亮的新词法分析器的所有荣耀和动作了。从GitHub下载源代码并从保存spi.py 文件的同一目录启动 Python shell :(spi.py代码见文章目录)也可直接见spi_lexer代码,在pycharm中可直接运行或调试看到结果

>>> from spi import Lexer
>>> lexer = Lexer('BEGIN a := 2; END.')
>>> lexer.get_next_token()
Token(BEGIN, 'BEGIN')
>>> lexer.get_next_token()
Token(ID, 'a')
>>> lexer.get_next_token()
Token(ASSIGN, ':=')
>>> lexer.get_next_token()
Token(INTEGER, 2)
>>> lexer.get_next_token()
Token(SEMI, ';')
>>> lexer.get_next_token()
Token(END, 'END')
>>> lexer.get_next_token()
Token(DOT, '.')
>>> lexer.get_next_token()
Token(EOF, None)
>>>

继续解析器更改。

以下是我们的解析器更改的摘要:

让我们从新的AST 节点开始:

复合 AST节点表示复合语句。它在其children 变量中包含一个语句节点列表。

class Compound(AST):
    """Represents a 'BEGIN ... END' block"""
    def __init__(self):
        self.children = []

Assign AST节点代表一个赋值语句。它的左变量用于存储一个Var节点,它的右变量用于存储由 expr 解析器方法返回的节点:

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

Var AST节点(你猜对了)代表一个变量。该self.value持有变量的名称。

class Var(AST):
    """The Var node is constructed out of ID token."""
    def __init__(self, token):
        self.token = token
        self.value = token.value

NoOp节点用于表示空语句。例如,’ BEGIN END ’ 是一个没有语句的有效复合语句。

class NoOp(AST):
    pass

您还记得,语法中的每个规则在我们的递归下降解析器中都有一个对应的方法。这次我们添加了七个新方法。这些方法负责解析新的语言结构和构建新的AST节点。它们非常简单:(ps. 我就喜欢作者这么说!)

def program(self):
    """program : compound_statement DOT"""
    node = self.compound_statement()
    self.eat(DOT)
    return node

def compound_statement(self):
    """
    compound_statement: BEGIN statement_list END
    """
    self.eat(BEGIN)
    nodes = self.statement_list()
    self.eat(END)

    root = Compound()
    for node in nodes:
        root.children.append(node)

    return root

def statement_list(self):
    """
    statement_list : statement
                   | statement SEMI statement_list
    """
    node = self.statement()

    results = [node]

    while self.current_token.type == SEMI:
        self.eat(SEMI)
        results.append(self.statement())

    if self.current_token.type == ID:
        self.error()

    return results

def statement(self):
    """
    statement : compound_statement
              | assignment_statement
              | empty
    """
    if self.current_token.type == BEGIN:
        node = self.compound_statement()
    elif self.current_token.type == ID:
        node = self.assignment_statement()
    else:
        node = self.empty()
    return node

def assignment_statement(self):
    """
    assignment_statement : variable ASSIGN expr
    """
    left = self.variable()
    token = self.current_token
    self.eat(ASSIGN)
    right = self.expr()
    node = Assign(left, token, right)
    return node

def variable(self):
    """
    variable : ID
    """
    node = Var(self.current_token)
    self.eat(ID)
    return node

def empty(self):
    """An empty production"""
    return NoOp()

我们还需要更新现有的factor方法来解析变量:

def factor(self):
    """factor : PLUS  factor
              | MINUS factor
              | INTEGER
              | LPAREN expr RPAREN
              | variable
    """
    token = self.current_token
    if token.type == PLUS:
        self.eat(PLUS)
        node = UnaryOp(token, self.factor())
        return node
    ...
    else:
        node = self.variable()
        return node

解析器的parse方法更新为通过解析程序定义来启动解析过程:

def parse(self):
    node = self.program()
    if self.current_token.type != EOF:
        self.error()

    return node

这是我们的示例程序:

BEGIN
    BEGIN
        number := 2;
        a := number;
        b := 10 * a + 10 * number / 4;
        c := a - - b
    END;
    x := 11;
END.

让我们用genastdot.py对其进行可视化(为简洁起见,当显示Var节点时,它只显示节点的变量名称,当显示一个 Assign 节点时,它显示 ‘:=’ 而不是显示 ‘Assign’ 文本):

$ python genastdot.py assignments.txt > ast.dot && dot -Tpng -o ast.png ast.dot


最后,这里是所需的解释器更改:

要解释新的AST节点,我们需要向解释器添加相应的访问者方法。有四种新的访问者方法:

访问_Compound
访问_分配
访问_变量
访问_NoOp

Compound和NoOp访问者方法非常简单。该visit_Compound方法遍历它的孩子和参观各一转,和visit_NoOp方法不起作用。

def visit_Compound(self, node):
    for child in node.children:
        self.visit(child)

def visit_NoOp(self, node):
    pass

在分配和瓦尔游客方法值得仔细研究。

当我们为变量赋值时,我们需要将该值存储在某个地方以备日后需要时使用,这正是visit_Assign方法所做的:

def visit_Assign(self, node):
    var_name = node.left.value
    self.GLOBAL_SCOPE[var_name] = self.visit(node.right)

该方法在符号表GLOBAL_SCOPE 中存储键值对(变量名和与变量关联的值)。什么是符号表?甲符号表是一个抽象数据类型(ADT用于在源代码中追踪各种符号)。我们现在唯一的符号类别是变量,我们使用 Python 字典来实现符号表ADT。现在我只想说,本文中符号表的使用方式非常“hacky”:它不是一个具有特殊方法的单独类,而是一个简单的 Python 字典,它还作为内存空间执行双重任务。在以后的文章中,我将更详细地讨论符号表,我们还将一起删除所有的黑客。

让我们看一下语句“a := 3;”的AST 和visit_Assign方法之前和之后的符号表完成它的工作:

现在让我们看一下语句“b := a + 7;”的AST

如您所见,赋值语句的右侧 - “a + 7” - 引用了变量 ‘a’,因此在我们评估表达式“a + 7”之前,我们需要找出 ’ 的值a’ 是,这是visit_Var 方法的职责:

def visit_Var(self, node):
    var_name = node.value
    val = self.GLOBAL_SCOPE.get(var_name)
    if val is None:
        raise NameError(repr(var_name))
    else:
        return val

当该方法访问上图AST 中的Var节点时,它首先获取变量的名称,然后使用该名称作为GLOBAL_SCOPE字典中的键来获取变量的值。如果它可以找到该值,则返回该值,否则会引发NameError异常。以下是在评估赋值语句“b := a + 7;”之前的符号表内容:

这些都是我们今天需要做的改变,以使我们的解释器打勾。在主程序结束时,我们简单地将符号表 GLOBAL_SCOPE 的内容打印到标准输出。

让我们从 Python 交互式 shell 和命令行中使用我们更新的解释器作为驱动器。确保在测试之前下载了解释器的源代码和assignments.txt文件:

启动你的 Python shell:

$ python
>>> from spi import Lexer, Parser, Interpreter
>>> text = """\\
... BEGIN
...
...     BEGIN
...         number := 2;
...         a := number;
...         b := 10 * a + 10 * number / 4;
...         c := a - - b
...     END;
...
...     x := 11;
... END.
... """
>>> lexer = Lexer(text)
>>> parser = Parser(lexer)
>>> interpreter = Interpreter(parser)
>>> interpreter.interpret()
>>> print(interpreter.GLOBAL_SCOPE)
{'a': 2, 'x': 11, 'c': 27, 'b': 25, 'number': 2}

从命令行,使用源文件作为我们解释器的输入:

$ python spi.py assignments.txt
{'a': 2, 'x': 11, 'c': 27, 'b': 25, 'number': 2}

如果您还没有尝试过,现在就尝试一下,亲眼看看解释器是否正确地完成了它的工作。

让我们总结一下你在这篇文章中扩展 Pascal 解释器需要做的事情:

向语法添加新规则
向词法分析器添加新标记和支持方法并更新get_next_token 方法
向解析器添加新的AST节点以获得新的语言结构
将与新语法规则相对应的新方法添加到我们的递归下降解析器中,并在必要时更新任何现有方法(因子方法,我在看着你。😃
向解释器添加新的访问者方法
添加用于存储变量和查找变量的字典

在这一部分中,我不得不介绍一些“技巧”,我们将随着系列的推进而将其删除:

该程序的语法规则是不完整的。稍后我们将使用其他元素对其进行扩展。
Pascal 是一种静态类型语言,您必须在使用它之前声明一个变量及其类型。但是,正如您所看到的,本文中的情况并非如此。
到目前为止没有类型检查。在这一点上这没什么大不了的,但我只是想明确地提到它。例如,一旦我们向解释器添加更多类型,当您尝试添加字符串和整数时,我们将需要报告错误。
这部分中的符号表是一个简单的 Python 字典,它具有双重存储空间的功能。不用担心:符号表是一个非常重要的主题,我将专门针对它们撰写几篇文章。内存空间(运行时管理)本身就是一个话题。
在我们之前文章中的简单计算器中,我们使用正斜杠字符“/”来表示整数除法。但是,在 Pascal 中,您必须使用关键字div来指定整数除法(参见练习 1)。
我还特意引入了一个 hack,以便您可以在练习 2 中修复它:在 Pascal 中,所有保留关键字和标识符都不区分大小写,但本文中的解释器将它们视为区分大小写。

为了让你保持健康,这里有新的练习给你:

Pascal 变量和保留关键字不区分大小写,这与许多其他编程语言不同,因此BEGIN、begin和BeGin它们都引用相同的保留关键字。更新解释器,使变量和保留关键字不区分大小写。使用以下程序对其进行测试:

BEGIN

    BEGIN
        number := 2;
        a := NumBer;
        B := 10 * a + 10 * NUMBER / 4;
        c := a - - b
    end;

    x := 11;
END.

我之前在“hacks”部分提到我们的解释器使用正斜杠字符“/”来表示整数除法,但它应该使用 Pascal 的保留关键字div进行整数除法。更新解释器以使用div关键字进行整数除法,从而消除其中一种技巧。

更新解释器,以便变量也可以以下划线开头,如 ‘_num := 5’。

spi.py

""" SPI - Simple Pascal Interpreter. Part 9."""

###############################################################################
#                                                                             #
#  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, ID, ASSIGN,
 BEGIN, END, SEMI, DOT, EOF) = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', '(', ')', 'ID', 'ASSIGN',
    'BEGIN', 'END', 'SEMI', 'DOT', '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__()


RESERVED_KEYWORDS = {
    'BEGIN': Token('BEGIN', 'BEGIN'),
    'END': Token('END', 'END'),
}


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编译原理构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 9.)(笔记)语法分析(未完,先搁置了!)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 2.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 6.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 4.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 3.)(python/c/c++版)(笔记)

编译原理构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 7.)(笔记)解释器 interpreter 解析器 parser 抽象语法树AST(代码片