Let’s Build A Simple Interpreter 4

原文链接:https://ruslanspivak.com/lsbasi-part4/

在前面的文章中你学会了怎样识别和解释包含任意数量的加减操作的算术表达式,例如“7 - 3 + 2 - 1”。还学会了句法图以及它们如何被用来表示一门编程语言的语法。

今天你将会学习解析(parse)和解释(interpret)包含任意乘除操作的算术表达式,例如“7 * 4 / 2 * 3”。在这篇文章中使用的是整数除法,所以对于表达式“9 / 4”来说,结果是一个 整数:2。

我今天会讲很多另一个表示编程语言句法的广泛使用的表示法,叫 上下文无关语法 (context-free grammars, 简记为 grammars)或 BNF (Backus-Naur Form)。为了这篇文章的目的,我不会使用纯 BNF 记法,而更像是一个修改过的 EBNF 记法。

文法(语法):描述语言的语法结构的形式规则。

上下文无关语法就是说这个文法中所有的产生式左边只有一个非终结符,比如:

S -> aSb

S -> ab
这个文法有两个产生式,每个产生式左边只有一个非终结符S,这就是上下文无关文法,因为你只要找到符合产生式右边的串,就可以把它归约为对应的非终结符。

比如:

aSb -> aaSbb

S -> ab
这就是上下文相关文法,因为它的第一个产生式左边有不止一个符号,所以你在匹配这个产生式中的S的时候必需确保这个S有正确的“上下文”,也就是左边的a和右边的b,所以叫上下文相关文法。

作者:徐辰
链接:https://www.zhihu.com/question/21833944/answer/40689967
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以下是一些使用语法的原因:

  1. 语法使用了一种简明的方式来描述一门编程语言的句法。不像语法图,语法非常紧凑。 在以后的文章中,你会看到我越来越多地使用语法。
  2. 语法可以做为文档保存。
  3. 即使对从头开始写解析器(parser)来说,语法也是一个好的入手点。很多时候通过遵循 一套简单的规则你就可以把语法转化成代码。
  4. 有一套工具,叫解析器生成器(parser generator),可以把语法做为输入并自动根据它为你生成一个解析器。我会以后在这个系列中谈到这些工具。

语法机制

现在,我们来聊聊语法的机制方面:

下面的语法描述了算术表达式,像“7 * 4 / 2 * 3”这样的(这只是该语法可以生成的许多表达式之一):

BNF-1

语法是由一系列规则组成的,也被称为产生式(production)。我们的语法中有两条规则(产生式):

第一条规则左边的非终结符被叫做 开始符号(start symbol). 在我们的语法中,开始符号是 expr:

你可以这么理解 expr 这条规则:“expr 是一个 factor 后面可选地跟一个乘或除运算符再跟 另一个 factor,后面也相应可选地跟一个乘或除运算符再跟另一个 factor,如此重复”。

factor 是什么?对于本文来说 factor 就是一个整数。

语法中的符号

让我们快速地过一遍语法中的符号及它们的意义。

语法通过解释可以组成什么样的句子来定义一门语言。通过语法派生出算术表达式的方式为:首先从开始符号 expr 开始,然后反复地使用所包含的非终结符的规则替换该非终结符, 直到生成一个只包含终结符的句子。语法能组成的句子构成了一门语言。

下面举例说明:

将语法变成代码

下面是一些我们在把语法转化成源代码时会用到的==四个准则==。按照这些准则,你真的就可以把语法翻译成一个可工作的 parser:

  1. 对于语法中定义的每个规则 R,将它做成一个有相同名字的方法,对该规则的引用就变成了一个方法调用:R()。该方法的方法体遵循该规则的步骤,过程中使用相同的准则。
  2. 多选一 (a1|a2|aN) 变成 if-elif-else 语句
  3. 可选组 (...)* 变成一个可以执行 0 或多次的 while 循环(can loop over zero or more times)
  4. 每个 Token 记为 T 变成一个 eat 方法调用: eat(T). eat 方法的工作是 当它匹配到当前的向前看 (lookahead) token 就消耗掉它,然后从 lexer 中得到一个新 token 并将它赋值给内部变量 current_token.

这些准则看上去像这样:

让我们继续并遵循上述准则把我们的语法转化为代码。

我们的语法中有两条规则: exprfactor. 我们从 factor 规则(生成式)开始。 根据准则,需要新建一个名为 factor 的方法(准则1),它调用了一次 eat 方法来消耗INTEGER token (准则4):

1
2
def factor(self):   #factor就是指的integer型数值
    self.eat(INTEGER)

expr 规则变成了 expr 方法(还是准则1)。规则体(body)开始的 factor 引用 变成了对 factor() 方法的调用。可行组 (...)* 变成了一个 while 循环,多选一 (MUL|DIV) 变成了一个 if-elif-else 语句。把这些片段合并在一起就得到了下面的expr 方法:

1
2
3
4
5
6
7
8
9
10
11
def expr(self):
    self.factor() #语法准则中的第一个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 中,它包含了 lexer 和 parser 但没有interpreter。你可以直接从 GitHub下载并尝试一下。它包含有一个 interpreter 提示符,你可以输入表达式来查看它是否合法,即查看根据语法建立的 parser 是否可以识别出表达式。

下面是在我笔记本上的一次尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ 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 规则(也叫,产生式(production))对应的句法图:

expr

下面是原作者的关于本文的源码。下面是可以处理包含任意数量整数 乘除(整数除法)操作的合法的算术表达式的计算器代码。这里把词法分析器重构 到了一个单独的类 Lexer 中,并让 Interpreter 类使用 Lexer 实例做为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 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)
	
    #不如叫做:get_token_and_next
    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 上下载。和以往一样,自己尝 试一下,确认它能工作。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值