热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

实现一个简单的解释器(4)

译自:https:ruslanspivak.comlsbasi-part4(已获得作者授权,个别语句翻译的不到位,我会将原句

译自:https://ruslanspivak.com/lsbasi-part4/
(已获得作者授权,个别语句翻译的不到位,我会将原句跟在后边作为参考)

你是在被动地学习这些文章中的材料还是在积极地实践它?希望你一直在积极练习。

孔子曾经说过:


“闻之我也野。”



“视之我也饶。”



“行之我也明。”


在上一篇文章中,我们学习了如何解析(识别)和解释具有任意数量的加或减运算的算术表达式,例如"7 - 3 + 2 - 1",还了解了语法图以及如何用它来表示(specify)编程语言的语法。

今天,你将学习如何解析和解释具有任意数量的乘法和除法运算的算术表达式,例如"7 * 4 / 2 * 3"。本文中的除法将是整数除法,因此,如果表达式为"9 / 4",则答案将是整数:2。

今天,我还将谈论广泛用于表示某一编程语言语法的标记方法,称为上下文无关文法或BNF(简称文法)(Backus-Naur形式)。在本文中,我将不使用纯BNF表示法,而使用修改后的EBNF表示法。

我们为什么要使用文法,这是几个原因:

1、文法以简洁的方式指定编程语言的语法,与语法图不同,文法非常紧凑,在以后的文章中会越来越多地使用文法。

2、文法可以作为出色的文档。

3、即使你是从头开始手动编写解析器,文法也是一个不错的起点。通常,你可以按照一组简单的规则将文法转换为代码。

4、有一套称为解析器生成器的工具,可以接受文法作为输入,并根据该文法自动生成解析器,我将在本系列的后面部分讨论这些工具。

这是一个描述算术表达式的文法,例如"7 * 4 / 2 * 3"(这只是该文法可以生成的众多表达式之一):

文法由一系列规则(rules)组成,也称为productions,我们的文法有两个规则:

一个规则由称为headleft-hand side的非终结符(non-terminal)开始,然后跟上一个冒号(colon),最后由称为bodyright-hand side的终结符(terminal)和/或非终结符(non-terminal)组成:(A rule consists of a non-terminal, called the head or left-hand side of the production, a colon, and a sequence of terminals and/or non-terminals, called the body or right-hand side of the production)
(这里我翻译的很绕,直接看图会更清楚一点)

在上面显示的文法中,诸如MUL,DIV和INTEGER之类的标记称为终结符(terminals),而诸如expr和factor之类的变量称为非终结符(non-terminals),非终结符通常由一系列终结符和/或非终结符组成:(In the grammar I showed above, tokens like MUL, DIV, and INTEGER are called terminals and variables like expr and factor are called non-terminals. Non-terminals usually consist of a sequence of terminals and/or non-terminals)

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

你可以将规则expr解释为: “expr可以只是一个因数(factor),也可以可选地跟上乘法或除法运算符,然后再乘以另一个因数,之后又可选地跟上乘法或除法运算符,然后再乘以另一个因数,当然之后也可以继续循环下去”(An expr can be a factor optionally followed by a multiplication or division operator followed by another factor, which in turn is optionally followed by a multiplication or division operator followed by another factor and so on and so forth)

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

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

1、"|",表示或者,因此(MUL | DIV)表示MUL或DIV。

2、"(…)",表示(MUL | DIV)中的终结符和/或非终结符的一个组(grouping)。

3、"(…)*",表示该组可以出现零次或多次。

如果你过去使用过正则表达式,那么这些符号你应该非常熟悉。

文法通过解释如何形成句子来定义语言(A grammar defines a language by explaining what sentences it can form),那么我们如何使用文法来推导出算术表达式呢?有这几个步骤:首先从起始符expr开始,然后用该非终止符的规则主体重复替换非终止符,直到生成仅包含终止符的句子为止,这样我们就通过文法来形成了语言。(first you begin with the start symbol expr and then repeatedly replace a non-terminal by the body of a rule for that non-terminal until you have generated a sentence consisting solely of terminals. Those sentences form a language defined by the grammar)

如果文法不能导出某个算术表达式,则表示它不支持该表达式,那么解析器在尝试识别该表达式时将生成错误。

这里有几个例子,你可以看看来加深理解:
1、这是推导一个整数"3"的步骤:

2、这是推导"3 * 7"的步骤:

3、这是推导"3 * 7 / 2"的步骤:

确实这部分包含很多理论!

当我第一次阅读文法和相关术语时会感觉像这样:

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

我花了一些时间来熟悉符号(notation),理解它如何工作方式以及它与解析器和词法分析器之间的关系,我必须告诉你,由于它在编译器相关文章中得到了广泛的使用,从长远来看,你一定会碰到它,所以我推荐你早点认识它:)

现在,让我们将该文法映射到代码。

这是我们将文法转换为源代码的一些指导方法(guideline),遵循它们可以将文法从字面上转换为有效的解析器:

1、语法中定义的每个规则R可以成为具有相同名称的函数(method),并且对该规则的引用将成为方法调用:R(),函数的主体中的语句流也依照与同样的指导方法。(Each rule, R, defined in the grammar, becomes a method with the same name, and references to that rule become a method call: 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语句。(Alternatives (a1 | a2 | aN) become an if-elif-else statement.)

3、(…)转换while语句,可以循环零次或多次。(An optional grouping (…) becomes a while statement that can loop over zero or more times.)

4、对Token的引用T转换为对eat函数的调用:eat(T),也就是如果T的类型与当前的Token类型一致的话,eat函数将消耗掉T,然后从词法分析器获取一个新的Token并将其赋值给current_token这个变量。(Each token reference T becomes a call to the method eat: eat(T). The way the eat method works is that it consumes the token T if it matches the current lookahead token, then it gets a new token from the lexer and assigns that token to the current_token internal variable.)

准则总结如下所示:

让我们按照上述指导方法将文法转换为代码。

我们的语法有两个规则:一个expr规则和一个因数规则。让我们从因数规则(生产)开始。根据准则,需要创建一个称为factor的函数(准则1),该函数需调用一次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_tokenif 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 mainparser.parse()File "parser.py", line 136, in parseself.expr()File "parser.py", line 130, in exprself.factor()File "parser.py", line 114, in factorself.eat(INTEGER)File "parser.py", line 107, in eatself.error()File "parser.py", line 97, in errorraise 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 EOFself.type = type# token value: non-negative integer value, '*', '/', or Noneself.value = valuedef __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", etcself.text = text# self.pos is an index into self.textself.pos = 0self.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 += 1if self.pos > len(self.text) - 1:self.current_char = None # Indicates end of inputelse: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_charself.advance()return int(result)def get_next_token(self):"""Lexical analyzer (also known as scanner or tokenizer)This method is responsible for breaking a sentenceapart into tokens. One token at a time."""while self.current_char is not None:if self.current_char.isspace():self.skip_whitespace()continueif 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 inputself.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_tokenself.eat(INTEGER)return token.valuedef 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_tokenif token.type == MUL:self.eat(MUL)result = result * self.factor()elif token.type == DIV:self.eat(DIV)result = result / self.factor()return resultdef main():while True:try:# To run under Python3 replace 'raw_input' call# with 'input'text = raw_input('calc> ')except EOFError:breakif not text:cOntinuelexer= 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

这是今天的练习:

1、编写描述包含任意数量的+,-,* 或/运算符的算术表达式的文法。使用此文法,应该能够推导出诸如"2 + 7 * 4","7 - 8 / 4","14 + 2 * 3 - 6 / 2"之类的表达式,依此类推。

2、使用上一问编写的文法,实现一个解释器,该解释器可以计算包含任意数量的+,-,* 或/运算符的算术表达式。 解释器应该能够处理"2 + 7 * 4","7 - 8 / 4","14 + 2 * 3 - 6 / 2"等表达式。

最后再来复习回忆一下,记住今天文章的文法,回答以下问题,并根据需要参考以下图片:

1、什么是无上下文文法?

2、本文中文法有多少条规则(rule)/产生式(production)?

3、什么是终结符(terminal)? (识别图中的所有终结符)

4、什么是非终结符? (标识图片中的所有非终结符)

5、什么是规则的头部(head)? (识别图片中的所有头部/左侧(left-hand sides))

6、什么是规则的主体(body)? (标识图中的所有主体/右侧(right-hand sides))

7、什么是文法的开始符(start symbol)?

嘿,你一直阅读到最后! 这篇文章包含了相当多的理论知识,所以我为你的完成感到自豪。

下次我会再写一篇新文章,敬请期待,不要忘记练习,它们对你有好处。


推荐阅读
  • 在尝试加载支持推送通知的iOS应用程序的Ad Hoc构建时,遇到了‘no valid aps-environment entitlement found for application’的错误提示。本文将探讨此错误的原因及多种可能的解决方案。 ... [详细]
  • 本文详细介绍了 `org.apache.tinkerpop.gremlin.structure.VertexProperty` 类中的 `key()` 方法,并提供了多个实际应用的代码示例。通过这些示例,读者可以更好地理解该方法在图数据库操作中的具体用途。 ... [详细]
  • Java 中的十进制样式 getZeroDigit()方法,示例 ... [详细]
  • 问题场景用Java进行web开发过程当中,当遇到很多很多个字段的实体时,最苦恼的莫过于编辑字段的查看和修改界面,发现2个页面存在很多重复信息,能不能写一遍?有没有轮子用都不如自己造。解决方式笔者根据自 ... [详细]
  • 深入理解Java SE 8新特性:Lambda表达式与函数式编程
    本文作为‘Java SE 8新特性概览’系列的一部分,将详细探讨Lambda表达式。通过多种示例,我们将展示Lambda表达式的不同应用场景,并解释编译器如何处理这些表达式。 ... [详细]
  • 在1995年,Simon Plouffe 发现了一种特殊的求和方法来表示某些常数。两年后,Bailey 和 Borwein 在他们的论文中发表了这一发现,这种方法被命名为 Bailey-Borwein-Plouffe (BBP) 公式。该问题要求计算圆周率 π 的第 n 个十六进制数字。 ... [详细]
  • 长期从事ABAP开发工作的专业人士,在面对行业新趋势时,往往需要重新审视自己的发展方向。本文探讨了几位资深专家对ABAP未来走向的看法,以及开发者应如何调整技能以适应新的技术环境。 ... [详细]
  • 本文通过C++语言实现了一个递归算法,用于解析并计算数学表达式的值。该算法能够处理加法、减法、乘法和除法操作。 ... [详细]
  • 洛谷 P4009 汽车加油行驶问题 解析
    探讨了经典算法题目——汽车加油行驶问题,通过网络流和费用流的视角,深入解析了该问题的解决方案。本文将详细阐述如何利用最短路径算法解决这一问题,并提供详细的代码实现。 ... [详细]
  • Requests库的基本使用方法
    本文介绍了Python中Requests库的基础用法,包括如何安装、GET和POST请求的实现、如何处理Cookies和Headers,以及如何解析JSON响应。相比urllib库,Requests库提供了更为简洁高效的接口来处理HTTP请求。 ... [详细]
  • spring boot使用jetty无法启动 ... [详细]
  • 高级缩放示例.就像谷歌地图一样.它仅缩放图块,但不缩放整个图像.因此,缩放的瓷砖占据了恒定的记忆,并且不会为大型缩放图像调整大小的图像.对于简化的缩放示例lookhere.在Win ... [详细]
  • Python正则表达式(Python RegEx)
    Python正则表达式快速参考常用函数:re.match():从字符串的起始位置匹配一个正则表达式。re.search():扫描整个字符串并返回第一个成功的匹配。re.s ... [详细]
  • Java中字符串截取方法详解
    本文详细介绍了Java中常用的字符串截取方法及其应用场景,帮助开发者更好地理解和使用这些方法。 ... [详细]
  • 本文介绍了 PHP 的基本概念、服务器与客户端的工作原理,以及 PHP 如何与数据库交互。同时,还涵盖了常见的数据库操作和安全性问题。 ... [详细]
author-avatar
Leeeeeee7777777
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有