让我们做一个简单的解释器(四)

原文:Let’s Build A Simple Interpreter. Part 4.

你是在被动地(passively)学习文章中的内容,还是积极地(actively)跟着练习呢?我真的希望你是后者~

还记得孔夫子说的话吗?

不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。
译:(在学习中)听说比不听好,见到比听说好,知晓比见到好,实践比知晓好,学习的最终就是实践,实践了,就明白了。

在这里插入图片描述

“I hear and I forget.”

在这里插入图片描述

“I see and I remember.”

在这里插入图片描述

“I do and I understand.”

在之前的文章中你学会了如何解析识别(parse / recognize)和解释(interpret)有多个数字相加减的算术表达式,比如 “7 - 3 + 2 - 1”。你也学习了语法图(syntax diagrams)是如何被用于指明编程语言的语法。

今天你将会学习如何解析/解释有多个整数求积和求商的算术表达式,比如“7 * 4 / 2 * 3”。这里的除法运算符暂时把它作为一个整除运算符,因此如果表达式是“9 / 4”,结果将会是2。

今天我还会着重讲解另一个被广泛用于编程语言语法的符号——上下文无关文法context-free grammars,简称grammer)或者也叫做巴科斯范式 (Backus-Naur Form)。为了达到本文的目的,我不会使用纯粹的BNF符号,而使用一个被修改后的扩展巴科斯范式(EBNF)。

这里有几个使用文法(grammar)的理由:

  1. 文法以一个简单的方式指明了编程语言的语法(syntax)。与语法图相比,文法更加紧凑。在以后的文章中,你会看到我越来越多地使用文法。
  2. 文法可以作为优秀的文档。
  3. 即使你是从头开始手动编写解析器,文法也是一个很好的开始。你可以按照一组简单的规则将文法转换为代码。
  4. 有一种称为解析器生成器(parser generators)的工具,它们接受文法作为输入,并根据该文法自动为你生成解析器。我将在本系列文章的后面部分讨论这些工具。

现在,我们开始讲解文法的机制。

下图文法被用来描述算术表达式,类似于表达式“7 * 4 / 2 * 3”(这只是该文法可以生成的表达式之一):
在这里插入图片描述

一个文法包含一系列的规则(rules),这些规则也称作产生式集合(productions),上图的文法中有两条规则:

在这里插入图片描述

一条规则包含一个非终结符(non-terminal),也叫做产生式集合(productions)的head或者左半部分(left-hand side),一个冒号,还有一系列的终结符和(或)非终结符terminals and/or non-terminals),也被称为产生式集合的body或者右半部分(right-hand side

在这里插入图片描述

上述文法中类似于MUL、DIV和INTEGER的Token被叫做终结符;类似于exprfactor被叫做非终结符。非终结符通常由一系列的终结符和(或)非终结符组成:

在这里插入图片描述

第一条规则的左半部分的非终结符被称为初始符号start symbol,在下图中,初始符号为expr
在这里插入图片描述

你可以像这样理解这条expr规则:expr可以是一个factor,后面跟着一个乘法或除法运算符,然后再跟着另一个factor,这个factor又可以跟着一个乘法或除法运算符,然后再跟着另一个factor,以此类推。

那啥是factor呢?就本文而言,factor是一个整数。

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

  • | ——“一条竖线表示“或”,因此(MUL|DIV)表示即可以是MUL也可以是DIV
  • (...) —— 一对括号表示一组终结符和(或)非终结符,例如(MUL|DIV)
  • (...)*—— 匹配组内的内容0次或多次。

如果你之前使用过正则表达式,那么你可能会非常熟悉上述的符号。

一个文法通过解释句子的组成定义一个语言。使用文法来获得一个算术表达式的过程如下:首先从开始符号expr开始,然后用非终结符的规则主体重复替换非终结符,直到生成一个仅由终结符组成的句子。这些句子构成了一种由文法定义的语言。

如果文法不能派生某个算术表达式,那么就表示它不支持该表达式,并且解析器在尝试解析该表达式时会抛出异常。

这里有一些例子展示文法如何派生出表达式的:

3

在这里插入图片描述

3 * 7

在这里插入图片描述

3 * 7 / 2

在这里插入图片描述

哇哦,理论知识有点多了!

当我第一次接触关于文法、相关术语等等,我就像图中的小人一样迷茫:

在这里插入图片描述

我保证我并不是像这样自信满满:
在这里插入图片描述

虽然我花了很长时间消化这些符号、工作原理、解析器和词法分析器的关系,但我必须告诉您,从长远来看,学习它受益匪浅,因为它在实践中和编译器文献中广泛存在,你一定会在某个时候和他碰面。所以,为什么不早一点接触它呢?

现在让我们将文法翻译成代码!

下面是我们用于把文法转换为源代码的指导原则。通过这些原则,你可以将文法翻译成解析器:

  1. 定义在文法里的每一条规则R,成为一个具有相同名称的函数,对该规则的引用可以调用此函数:R()。The body of the method follows the flow of the body of the rule using the very same guidelines.(难以翻译,往下面看就明白了)
  2. (a1 | a2 | aN)使用if-elif-else语句。
  3. (…)*使用while循环0次或者N次。
  4. 每一个Token T 都会调用eat(T)方法。eat方法的工作方式是,如果Token T匹配当前需要的Token,那么它将消耗此Token。然后从词法分析器中获得一个新的Token并赋值给内部变量current_token

从视觉上看,指导原则是这样的:
在这里插入图片描述

现在让我们根据上述的指导原则转换文法为代码。

我们的文法中有两条规则:一条expr规则和一条factor规则。然我们从factor规则开始。根据指导规则,你需要创建一个名为factor的方法(指导原则1),函数体中会调用eat()方法消费一个INTEGER token(指导规则4)

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

这并不难,对吧!
继续!

规则expr也需要一个expr方法(指导原则1),规则的body以对factor的引用开始,该引用成为factor()方法调用。可选分组(…)*成为一个whlie循环。选项(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中下载该文件。它有一个交互式提示符,你可以输入表达式并查看它们是否生效。

这里是在我的电脑上运行的一些例子:

$ 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规则的文法图:

在这里插入图片描述
是时候深入研究我们新的算术表达式解释器的源代码了。下面是一个计算器的代码,它可以处理包含整数和任意数量的乘法和除法(整数除法)运算符的有效算术表达式。你还可以看到,我将词法分析器重构为一个单独的类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):
        # 用户输入文本,  如. "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):
        """向前移动 `pos` 坐标并设置 `current_char`变量."""
        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):
        """词法分析器 (also known as scanner or tokenizer)
        
        将句子拆分成Token
         """
        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):
        """
        如果当前的token类型和需要的类型相匹配,那么就会将下一个token赋值
        给current_token。若不匹配则会抛出异常
        """
        if self.current_token.type == token_type:
            self.current_token = self.lexer.get_next_token()
        else:
            self.error()

    def factor(self):
        """返回INTEGER类型的值

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

    def expr(self):
        """算数表达式的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 / 214
calc> 7 * 4 / 2 * 342
calc> 10 * 4  * 2 * 3 / 830

我知道你已经等不及了!这里是今天的新练习:

  • 编写一个文法来描述包含任意数量的+、-、* 或 / 运算符的算术表达式。使用文法,你应该能够推导出像“2 + 7 * 4”、“7 - 8 / 4”、“14 + 2 * 3 - 6 / 2”这样的表达式。
  • 使用文法,编写一个解释器,可以对包含任意数量的+、-、* 或 / 操作符的算术表达式求值。您的解释器应该能够处理“2 + 7 * 4”、“7 - 8 / 4”、“14 + 2 * 3 - 6 / 2”等表达式。
  • 如果你已经完成了以上的练习,那就很快乐。

检验你的理解:

记住今天文章讲解的的文法,回答以下问题,根据需要参考下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FmxuyZZp-1595238208946)(en-resource://database/821:1)]

  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)

如果你想看到更多关于作者最新的文章,请等我慢慢翻译,或者直接查看原文

翻译:@echoechoin
校译:@echoechoin

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值