一起来写个简单的解释器(5):加减乘除表达式

1,502 阅读13分钟
原文链接: blog.jobbole.com

本系列:

如何创建一个解释器或编译器这么复杂的问题,你会如何处理呢?开始的时候它很像是一团乱糟糟的毛线,你得重新梳理展开,然后缠成一个完美的毛线球 。

达到上述目的的方法只需一次解开一根线、一个结。虽然有时候你可能会觉得你无法马上理解某些事情,但是你必须坚持下去。我保证如果你足够坚持,最后你会“豁然开朗”(哎呀呀,如果每次我不能马上弄懂某些事情的时候,我都存 25美分,那么我早就变成富豪了 :))。

关于理解如何创建一个解释器和编译器,也许我能给你的最好建议之一就说阅读本系列文章的解释、代码,然后自己去编写代码,甚至在一段时间内多次编写同样的代码,使得这些材料和代码对于你来说是很自然的。直到那时才继续学习新的主题。不要着急,请慢下来,花时间去深刻地理解基础概念。虽然这种方法看起来有点慢,但是你会受益匪浅。相信我。

你在最后终究会得到完美的毛线球。你知道吗?即使它不够完美,但是总比什么都不做和不学习这些课题,或者走马观花然后几天之后就忘记了要好。

记住——只需要坚持不懈地解开缠绕:一次一根线、一个结,并且通过编写大量代码来实践你所学过的:

查看图片

今天你将会用到在本系列前面几篇文章中学到的所有知识,并且学习如何解析和解释带有任意数量的加法、减法、乘法和除法运算符的算术表达式。你将会编写一个可以计算像“14 + 2 * 3 – 6 / 2”这样的表达式的解释器。

在深入研究和编写代码之前,让我们讨论一下运算符的结合律优先级

按照约定,7 + 3 + 1 等同 (7 + 3) + 1,7 – 3 – 1 等同 (7 – 3) – 1。这里没有什么可惊讶的。我们在某个时候学过那些约定,并且从那以后把那些约定当作是理所当然的。如果我们把 7 – 3 – 1 当作是 7 – (3 – 1),那么结果会是 5 而不是预期的 3。

在普通的算术运算和大部分编程语言中,加法、减法、乘法和除法都是左结合

7 + 3 + 1 is equivalent to (7 + 3) + 1
7 - 3 - 1 is equivalent to (7 - 3) - 1
8 * 4 * 2 is equivalent to (8 * 4) * 2
8 / 4 / 2 is equivalent to (8 / 4) / 2

一个运算符是左结合是什么意思?

当一个操作数像表达式 7 + 3 + 1 中的 3 一样两边都有加号时,我们需要一个约定来决定哪个运算符适用于 3。是操作数 3 左边的运算符还是右边的?因为两边都有加号的操作数属于它左边的运算符,所以 + 运算符结合左边的操作数。因此我们说运算符 + 是左结合。那就是为什么根据结合律,7 + 3 + 1 等同于 (7 + 3) + 1。

好,那么像 7 + 5 * 2 这样的表达式中的操作数 5,两边有着不同种类的运算符会是怎样呢?该表达式等同于 7 + (5 * 2) 还是 (7 + 5) * 2?我们如何解决这个歧义呢?

在这个例子中,结合律对我们已经没有帮助了。因为它只适用于同一种运算符,要么是加法类(+、-),要么是乘法类(*、/)。当在同一条表达式中有不同种类的运算符时,我们需要另一个约定来解决歧义。我们需要定义了运算符相对优先级的约定。

这就是了:如果运算符 * 比 + 先接受操作数,那么我们说 * 有更高的优先级。在我们知道和使用的算术运算中,乘法和除法比加法和减法优先级更高。所以表达式 7 + 5 * 2 等同于 7 + (5 * 2),表达式 7 – 8 / 4 等同于 7 – (8 / 4)。

在一个例子中,表达式的运算符有同样的优先级,我们只需运用结合律并且从左到右执行运算符:

7 + 3 - 1 is equivalent to (7 + 3) - 1
8 / 4 * 2 is equivalent to (8 / 4) * 2

讨论这么多关于运算符的结合律和优先级,我希望你不要认为我想让你无聊死。那些约定的好处是我们可以从一个展示算术运算符的结合律和优先级的表格中构建算术表达式的文法。然后我们可以遵循我在《一起来写个简单的解释器(4)》文章中概括的准则,将文法翻译成代码,我们的解释器也有能力处理除了结合律之外的运算符优先级。

好了,下面是我们的优先级表格:

查看图片

在表格中,运算符 + 和 – 有着相同的优先级,它们都是左结合的。你也可以看到运算符 * 和 / 也是左结合,有着相同的优先级,但是它们的优先级比加法和减法运算符的优先级要高。

下面是如何根据优先级表格构建文法的规则:

  1. 为每个优先级定义一个非终结符。非终结符所在产生式的主体应该包含同等级的算术运算符和优先级高一级的非终结符。
  2. 为表达式的基数创建一个额外的非终结符系数,在我们的例子中该系数是整数。通用的规则是如果你有 N 层优先级,那么你总共需要 N + 1 个非终结符:每层优先级需要一个非终结符,加上一个用于表达式基数的非终结符。

继续前进!

下面我们根据这些规则来构造文法。

根据规则 1,我们要定义两个非终结符:用于等级 2 的非终结符称为 expr,用于等级 1 的非终结符称为 term。而根据规则 2,我们要为算术表达式的基数定义一个整数系数

新文法的开始符号exprexpr 产生式包含一个主体,该主体使用来自等级 2 的运算符(在我们的例子中是 + 和 – 运算符)。同时 expr 产生式也包含更高优先级(等级 1)的 term 非终结符:

查看图片

term 表达式包含一个主体,该主题使用来自等级 1 的运算符(在我们的例子中是 * 和 /)。同时 term 产生式也包含用于表达式基数的整数系数

查看图片

而非终结符 factor 的产生式是:

查看图片

我们已经在前几篇文章中看过上述产生式的文法和语法图形式,下面我们将会把它们结合到一个文法中,该文法会关注到运算符的结合律和优先级:

查看图片

下面是与上述文法相对应的语法图:

查看图片

框图中的每个矩形框是对另一个框图的“方法调用”。对于表达式 7 + 5 * 2,如果你从最上面的框图 expr 开始一直往下看到最下面的框图 factor,那么你应该可以看到较下面的框图中更高级的运算符 * 和 / 会比较上面的框图中运算符 + 和 – 先执行。

为了彻底地说明运算符的优先级,下面我们看一下根据上述的文法和语法图对同一个算术表达式 7 + 5 * 2 的分解。这只是从另一方面展示更高优先级的运算符会比低优先级运算符先执行:

查看图片

好的,下面根据《一起来写个简单的解释器(4)》中的准则将文法转换成代码,再看下我们的新解释器是怎么工作的,好吗?

再次列出文法:

查看图片

下面是一个计算器的全部代码,该计算器可以处理包含整数和任意数量的加法、减法、乘法和除法(整除)运算符的有效算术表达式。

下面是对比《一起来写个简单的解释器(4)》中的代码的主要修改:

  • Lexer 类现在可以标记 +、-、* 和 /(这里没有新的东西,我们只是把以前的代码整合到一个类中,从而支持所有的标记)
  • 回想一下,文法中定义的每条规则(产生式),R,会变成一个同名的方法,而对那条规则的引用变成一个方法调用:R()。因此 Interpreter 类现在有三个方法,分别对应文法中的非终结符:exprtermfactor

源代码:

# 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, EOF = (
    'INTEGER', 'PLUS', 'MINUS', 'MUL', 'DIV', 'EOF'
)

class Token(object):
    def __init__(self, type, value):
        # token type: INTEGER, PLUS, MINUS, 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(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. "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(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, '/')

            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):
        """factor : INTEGER"""
        token = self.current_token
        self.eat(INTEGER)
        return token.value

    def term(self):
        """term : factor ((MUL | DIV) factor)*"""
        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 expr(self):
        """Arithmetic expression parser / interpreter.

        calc>  14 + 2 * 3 - 6 / 2
        17

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

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

        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()

将上述代码保存到 calc5.py 文件中,或者直接从 GitHub 上下载。像往常一样,自己尝试一下,看下解释器是否可以正确地计算出带有不同优先级运算符的算术表达式。

下面是运行在我的电脑上一段简单的会话:

$ python calc5.py
calc> 3
3
calc> 2 + 7 * 4
30
calc> 7 - 8 / 4
5
calc> 14 + 2 * 3 - 6 / 2
17

下面是今天的练习:

查看图片

  • 不要窥视文章中的代码,不假思索地编写一个类似这篇文章中描述的解释器。为你的解释器写一些测试,确保都通过这些测试。
  • 扩展解释器来处理包含括号的算术表达式,使得你的解释器可以计算像 7 + 3 * (10 / (12 / (3 + 1) – 1)) 这样深层嵌套的算术表达式。

检测你的理解。

  1. 运算符是左结合是什么意思?
  2. 运算符 + 和 – 是左结合还是右结合?运算符 * 和 / 呢?
  3. 运算符 + 的优先级比 * 高么?

嘿,你读到最后了!真是太棒了。下次我会带来一篇新的文章——敬请期待,还有不要忘记做练习哦。

下面是我推荐的一些书籍列表,它们对你学习解释器和编译器有帮助:


你是在被动地学习这几篇文章中的材料,还是主动地去做练习?我希望你主动地去练习。我真的希望哦 :)

记得孔子说过的话么?

“听而易忘”

查看图片

“见而易记”

查看图片

“做而易懂”

查看图片

上一篇文章中,你已经学会了如何解析(识别)和解释带有任意数量加法或者减法运算符的算术表达式,例如“7 – 3 + 2 – 1”。你也学习了语法图以及如何使用语法图来指定一种程序设计语言的语法。

本文你将会学习如何解析(识别)和解释带有任意数量乘法或者除法运算符的算术表达式,例如“7 * 4 / 2 * 3”。这篇文章讨论的除法是整数除法,所以如果表达式是“9 / 4”,那么答案将会是一个整数:2。

今天我也会讨论不少有关另一种被广泛使用的、用于指定一种程序设计语言语法的表示法。它叫做上下文无关文法(简称文法)或者 BNF(Backus-Naur Form 巴科斯-诺尔范式)。在这篇文章中,我不会使用纯净的 BNF 表示法,而是使用类似的修改过的 EBNF 表示法。

下面是使用文法的几个原因:

  1. 文法以简明的方式说明一种程序设计语言的语法。不像语法图,文法十分简洁。你将会看到我在未来的文章中越来越多地使用文法。
  2. 文法可以用作很好的文档。
  3. 即使你从零开始编写你的解析器,文法也是一个很好的起点。通常,你可以通过遵循一系列简单的规则将文法转换成代码。
  4. 有一组工具叫做解析器生成器,它接收一段文法作为输入,并且根据那段文法自动地生成一个解析器。我会在系列后面的文章中讨论那些工具。

现在,让我们谈一下文法的原理,好吗?

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

查看图片

一段文法由一系列规则(rule)组成,也称为 产生式(productions)。在我们的文法中有两条规则:

查看图片

一条规则由一个非终结符(称为产生式的或者左边)、一个冒号和一系列终结符(和 | 或者)非终结符(称为产生式的主体或者右边)组成:

查看图片

上面介绍的文法中,像 MUL、DIV 和 INTEGER 这样的标记称为终结符(terminals),像 exprfactor 这样的变量称为非终结符(non-terminals)。非终结符通常由一系列终结符(和 | 或者)非终结符组成:

查看图片

第一条规则左边的非终结符符号称为开始符号(start symbol)。在我们的文法例子中,开始符号是 expr

查看图片

你可以这样读 expr 规则:“expr 可以是一个 factor 可选地接着一个乘法或者除法运算符,再接着另一个 factor,依次可选地接着一个乘法或者除法运算符,再接着另一个 factor……”

factor 是什么?就本文而言,factor 就是一个整数。

让我们快速地回顾一下文法中用到的符号和它们的含义。

  • | – 多选一。表示“或”。所以 (MUL | DIV) 表示要么是 MUL,要么是 DIV。
  • ( … ) – 一对圆括号表示把终结符(和 | 或者)非终结符组合起来,就像 (MUL | DIV) 一样。
  • ( … )* – 匹配组合里面的内容 0 次或者多次。

如果你以前了解过正则表达式,那么 |()(…)* 这些符号对于你来说应该会比较熟悉。

一段文法通过说明它可以组成什么句子来定义一种语言(language)。这是如何使用文法推导出算术表达式:首先以开始符号 expr 开始,然后反复地用一个非终结符所在规则的主体替代该非终结符,直到产生一个只包含终结符的句子。那些句子组成了由文法定义的语言

如果文法不能得到一条确定的算术表达式,那么它不支持该表达式,并且当解析器尝试识别该表达式时,解析器会生成一个语法错误。

我依次想了几个例子。下面是文法如何得到表达式 3 的例子:

查看图片

这是文法如何得到表达式 3 * 7 的例子:

查看图片

这是文法如何得到表达式 3 * 7 / 2 的例子:

查看图片

哇,这里有相当多的理论!

我想当我第一次阅读关于文法相关的术语和诸如此类的东西,我有这样一种感觉:

查看图片

我可以向你保证我肯定不会像这样:

查看图片

我花费了一些时间来适应这种表示法,它是如何工作的和它与解析器、语法分析器的关系。但是我必须告诉你,从长远来看,学习它是值得的。因为它在实际中被广泛应用,你也一定会遇到一些编译器文献在某些时候会用到它。所以,为何不早点学呢?:)

现在,让我们将文法映射成代码,可以么?

这里是用于将文法转换成源代码的准则。遵循这些准则,你可以逐字逐句地把文法翻译给正在工作的分析器:

  1. 文法中定义的每条规则,R,会变成一个同名的方法,而对那条规则的引用变成一个方法调用:R()。方法的主体跟着同一套准则的规则的主体流。
  2. 多个可选项 (a1 | a2 | aN) 变成 if-elif-else 语句。
  3. 可选的 (…)* 集合变成 while 语句,该语句可以循环 0 次或者多次。
  4. 每个符号引用 T 变成对 eat 方法的调用:eat(T)eat 方法的工作方式是如果该方法匹配当前的 lookahead 符号,那么 eat 方法会传入符号 T,然后它会从词法分析器中得到一个新的符号,并且把该符号分配给内部变量 current_token

这些准则直观上看起来像这样:

查看图片

让我们继续前进,遵循上述准则将我们的文法转换成代码。

在我们的文法中有两条规则:expr 规则和 factor 规则。我们以 factor 规则(产生式)开始。根据准则,你需要创建一个名为 factor 的方法(准则 1),该方法有一个对以 INTEGER 符号为参数的 eat 方法的调用(准则 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 下载文件并使用它。该程序有交互式提示,你可以在提示中输入表达式并查看表达式是否可用:也就是说,依据文法构建的解析器是否可以识别该表达式。

下面是运行在我的电脑上一段简单的会话:

$ 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 
    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 类,并且把 Lexer 实例作为参数更新 Interpreter 类:

# 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

我知道你迫不及待这一部分了 :) 这是今天新的练习:

查看图片

  • 写一段文法来描述包含任意数量的 +、-、* 或者 / 运算符的算术表达式。你应该有能力从文法中得到像“2 + 7 * 4”、“7 – 8 / 4”、“14 + 2 * 3 – 6 / 2” 等这样的表达式。
  • 使用文法,编写一个解释器,该解释器可以计算包含任意数量的 +、-、* 或者 / 运算符的算术表达式。你的解释器应该可以处理像“2 + 7 * 4”、“7 – 8 / 4”、“14 + 2 * 3 – 6 / 2” 等这样的表达式。
  • 如果你完成了上述的联系,那么放松一下把 :)

检测你的理解。

牢记今天这篇文章介绍的文法,回答下面的几个问题,需要时可以参考下面的图片:

查看图片

  1. 什么是上下文无关文法?
  2. 文法有多少条规则或者产生式?
  3. 什么是终结符?(在图片中找到所有的终结符)
  4. 什么是非终结符?(在图片中找到所有的非终结符)
  5. 什么是一条规则的头部?(在图片中找到所有的头部或者左边)
  6. 什么是一条规则的主体?(在图片中找到所有的主体或者右边)
  7. 文法的开始符号是什么?

嘿,你读到最后了!这篇文章包含了相当多理论,所以我真的为你读完这篇文章而感到骄傲。