【Interpreter】构建简单的解释器(第4部分)

【Interpreter】构建简单的解释器(第4部分)

简单翻译了下,方便查看,水平有限,喜欢的朋友去看 原文

你被动地学习这些文章,还是你积极地参与练习? 我希望你一直在积极地练习。 我真的会很高兴:)

还记得孔子说过的话吗?(译注:这好像出自《荀子·儒效》 ?)

I hear and I forget.

I see and I remember.

I do and I understand.

不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。

LSBAWS_confucius_hear
LSBAWS_confucius_see
LSBAWS_confucius_do

在上一篇文章中,你学习了如何解析(识别)和解释任意数量的加号或减号运算符组成的算术表达式,如 “7 - 3 + 2 - 1”。 你还了解了语法图以及如何使用它们来分辨编程语言的语法。

今天你将继续学习如何解析(识别)和解释任意数量的乘号或除号运算符组成的算术表达式,如 “7 * 4 / 2 * 3”。本文中的除法指的是整数除法,如果表达式为 “9 / 4”,那么答案会是整数:2。

今天我还会聊聊另一个广泛用来指定编程语言语法的符号。 它被称为上下文无关语法(简称语法)或 BNF(巴科斯范式)。 出于本文的目的,我不会使用纯 BNF 表示法,而是修改后的 EBNF 表示法。

以下是使用语法的几个原因:

  1. 一种语法以简洁的方式规定了编程语言的语法。 与语法图不同,语法非常简洁凝练。 你将在以后的文章中看到我越来越多地使用语法。
  2. 一种语法可以提供丰富的文档。
  3. 即使你从头开始手动编写解析器,语法也是一个很好的起点。 通常你可以通过遵循一组简单的规则将语法转换为代码。
  4. 有一组工具称为解析器生成器,它接受语法作为输入,并根据该语法自动生成解析器。 我将在本系列的后面讨论这些工具。

现在,让我们聊聊语法机制方面的问题吧?

这是一个描述像 “7 * 4 / 2 * 3” (它只是语法可以生成的众多表达式之一)一样的算术表达式的语法:

lsbasi_part4_bnf1
语法由一系列规则组成,也称为产生式。 我们的语法有两个规则:

lsbasi_part4_bnf2
规则 包括 非终结符(称为产生式的 headleft side),冒号,以及一系列 终结符 和 / 或 非终结符(称为产生式的 bodyright side):

lsbasi_part4_bnf3
在我上面展示的语法中,像 MULDIVINTEGER 这样的 token 被称为终结符,而像 exprfactor 这样的 变量 被称为非终结符。 非终结符 通常由一系列 终结符 和 / 或 非终结符 组成:

lsbasi_part4_bnf4
第一个规则左侧的非终结符号称为起始符号(start symbol)。 在我们的语法中,起始符号是 expr

lsbasi_part4_bnf5
你可以把规则 expr 作如下定义: “expr 可以是一个因子(factor)后面 跟零个或任意多个乘法或除法运算符的另一个因子,然后还可以 跟零个或任意多个乘法或除法运算符的另一个因子,依此循环。”

什么是因子(factor)? 在本文中,因子只代表一个整数。

让我们快速浏览一下语法中使用的符号及其含义。

  • | : 二选一。 表示 “或”。 因此(MUL | DIV)表示可以是 MUL,也可以是 DIV。
  • ( … ) : 一个开放和关闭的圆括号表示在 (MUL | DIV) 中的一组 终结符 和 / 或者 非终结符。
  • ( … )* : 将组内的内容匹配零次或多次。

一种语言的语法定义了它可以支持的语句。 这就是为什么你可以使用语法派生算术表达式:首先从起始符号 expr开始,然后循环将非终结符号使用一个规则的主体进行替换,直到生成一个只包含终结符的表达式为止。 这些语句构成了语法定义的语言。

如果语法无法派生某个算术表达式,则它不支持该表达式,并且解析器在尝试识别表达式时将产生语法错误。

让我们举几个具体例子。

下图展示了语法派生 表达式 3:

lsbasi_part4_derive1
下图展示了语法派生 表达式 3 * 7:

lsbasi_part4_derive2
下图展示了语法 派生 表达式 3 * 7 / 2:

lsbasi_part4_derive3
哇,这也太理论了吧!

我想起我第一次接触语法,相关术语和所有其它符号时,我的感觉是下面这样的:

lsbasi_part4_bnf_hmm
我可以向你保证,我绝对不是这样的:

lsbasi_part4_bnf_yes
我花了一些时间来熟悉符号,弄明白工作原理,以及它与解析器和词法分析器的关系,但我必须告诉你,从长远来看它是值得的,因为它们广泛地应用在编译器实践中(类似的分析工具也可以应用在编译器外的其他地方)。 那么,为什么不早点呢??

下面,让我们一起把语法图翻译成对应的代码。

以下是我们将用来把 语法 转换成 源代码 的步骤。 按照步骤,可以直接把 语法 翻译成 可以工作的 解析器:

  1. 将语法中定义的每一条规则 R,翻译成一个方法,而对这个规则的引用则翻译成一个方法调用: R()。使用相同的办法,将规则的定义翻译成方法的实现。
  2. (a1 | a2 | aN) 转换成 if-elif-else 语句。
  3. ( … )* 转换成可以执行零次或者多次 while 循环语句。
  4. 将每个 token 的引用 T 转换成 eat 方法的调用:eat(T)eat 方法的工作方式是:如果当前 token 与 传入的 token 匹配,它就会使用 token T,然后它从词法分析器中获取一个新 token分配给 current_token 变量。

把上面步骤转换成图示,看起来是这样的:

lsbasi_part4_rules

让我们按照上述步骤将 语法 转换成 代码。

我们的语法有两条规则:一个是 expr 规则,一个是 factor 规则。我们先看 factor 规则,根据上述步骤需要先创建一个 factor(步骤1) 方法,这个方法只有一个对 eat 方法(eat 方法用来识别 INTEGER token)的调用(步骤4)。

def factor(self):
    self.eat(INTEGER)

很简单,是吧!

继续!

规则 expr 翻译成 expr 方法(根据规则1)。规则主体开始的 factor 引用 翻译成 factor 方法。可选组 ( … )* 翻译成 while 循环语句,( MUL | DIV ) 翻译成 if-elif-else 条件语句。将这些部分组合在一起,我们得到以下 expr 方法:

def expr(self):
    self.factor()

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

请花一些时间研究如何将 语法 翻译成 源代码。 确保你理解这部分,它稍后会派上用场。

方便起见,我将上面的代码放入 parser.py 文件中,该文件包含词法分析器和不带解释器的解析器。 您可以直接从GitHub下载并运行它。 它有一个交互式提示符("calc> "),你可以在后面输入表达式来查看它们是否合法:也就是验证 语法 构建的解析器是否可以识别表达式。

这是我在计算机上运行的示例:

$ python parser.py
calc> 3
calc> 3 * 7
calc> 3 * 7 / 2
calc> 3 *
Traceback (most recent call last):
  File "parser.py", line 155, in <module>
    main()
  File "parser.py", line 151, in main
    parser.parse()
  File "parser.py", line 136, in parse
    self.expr()
  File "parser.py", line 130, in expr
    self.factor()
  File "parser.py", line 114, in factor
    self.eat(INTEGER)
  File "parser.py", line 107, in eat
    self.error()
  File "parser.py", line 97, in error
    raise Exception('Invalid syntax')
Exception: Invalid syntax

试着运行一下!

我忍不住再次强调一下语法图。 这是同一个 expr 规则的语法图:

lsbasi_part4_sd
现在是时候完成我们算术表达式解释器的源代码了。下面是可以处理包含整数和任意数量的乘法和除法(整数除法)运算符的有效算术表达式的计算器的源代码。你还可以看到我将词法分析器重构为单独的类 Lexer 并更新了 Interpreter 类 将 Lexer 实例作为参数:

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


class Token(object):
    def __init__(self, type, value):
        # token type: INTEGER, MUL, DIV, or EOF
        self.type = type
        # token value: non-negative integer value, '*', '/', or None
        self.value = value

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

        Examples:
            Token(INTEGER, 3)
            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. "3 * 5", "12 / 3 * 4", etc
        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(MUL, '*')

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

            self.error()

        return Token(EOF, None)


class Interpreter(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):
        """Return an INTEGER token value.

        factor : INTEGER
        """
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def expr(self):
        """Arithmetic expression parser / interpreter.

        expr   : factor ((MUL | DIV) factor)*
        factor : INTEGER
        """
        result = self.factor()

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

        return result


def main():
    while True:
        try:
            # To run under Python3 replace 'raw_input' call
            # with 'input'
            text = raw_input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        lexer = Lexer(text)
        interpreter = Interpreter(lexer)
        result = interpreter.expr()
        print(result)


if __name__ == '__main__':
    main()

将以上代码保存到calc4.py文件中或直接从GitHub下载。 像往常一样,尝试一下,亲眼看看它是否有效。

这是我在笔记本电脑上运行的示例会话:

$ python calc4.py
calc> 7 * 4 / 2
14
calc> 7 * 4 / 2 * 3
42
calc> 10 * 4  * 2 * 3 / 8
30

我知道你已经迫不及待了 ?

下面是今天的练习题:

lsbasi_part4_exercises

  • 写一个描述包含任意数量 +,- ,*,或 / 操作的语法。语法来源于表达式,如 “2 +7 * 4”,“7 - 8 / 4”,“14 + 2 * 3 - 6 / 2” 等等。
  • 使用上面写的语法,写一个可以计算包含任意数量 +,- ,*,或 / 操作的解释器。你的解释器应该能够处理像 “2 +7 * 4”,“7 - 8 / 4”,“14 +2 * 3 - 6 / 2”等表达式。
  • 如果你已经完成了上述练习,开始放松和享受吧 ?

理解测试

根据今天文章学习的内容,参考下图,回答下面问题:

lsbasi_part4_bnf1

  1. 什么叫做上下文无关文法?
  2. 本文中的文法包含多少规则,这些规则可以产生多少语句?
  3. 什么叫终结符? (找出上图中所有的终结符)
  4. 什么叫非终结符? (找出上图中所有的非终结符)
  5. 什么叫规则头? (找出上图所有的规则头)
  6. 什么叫规则体? (找出上图所有的规则体)
  7. 哪个符号是上图文法中的起始符号?

嘿,你完成了整篇文章的学习。这篇文章包含了大量的理论知识,我很高兴你能够完成学习。

我会再发布一些关于解释器的新的文章 — 保持求知欲,并且尝试完成最后的练习题,能够让你更好地文章中的知识。

以下是我推荐的书籍清单,可以帮助您学习解释器和编译器:

  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 Simple Interpreter. Part 4.

作者博客:Ruslan’s Blog


——2019-01-13——

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值