【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 4.)

文章目录

  • python代码
  • C语言代码
  • 总结

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

今天,您将学习如何解析和解释包含任意数量的乘法和除法运算符的算术表达式,例如“7 * 4 / 2 * 3”。本文中的除法将是一个整数除法,所以如果表达式是“9 / 4”,那么答案将是一个整数:2。

今天我还将讨论另一种广泛使用的用于指定编程语言语法的符号。它被称为上下文无关文法context-free grammars)(简称为文法grammars))或BNF巴科斯-诺尔形式Backus-Naur Form))。出于本文的目的,我不会使用纯BNF表示法,而是更像是一种修改后的EBNF 表示法。

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

文法以简洁的方式指定了编程语言的语法。与语法图不同,语法非常紧凑。在以后的文章中,您会看到我越来越多地使用文法。
文法可以作为很好的文档。
即使您从头开始手动编写解析器,文法也是一个很好的起点。通常,您只需遵循一组简单的规则即可将文法转换为代码。
有一组工具,称为解析器生成器,它们接受文法作为输入并根据该语法自动为您生成解析器。我将在本系列的后面部分讨论这些工具。
现在,让我们谈谈文法的机械方面,好吗?

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

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


规则由一个非终结符(称为产生式的头部或左侧)、一个冒号和一系列终结符和/或非终结符(称为产生式的主体或右侧)组成:


在我上面展示的文法中,像MUL、DIV和INTEGER这样的标记被称为终端,而像expr和factor这样的变量被称为非终端。非终结符通常由一系列终结符和/或非终结符组成:

第一条规则左侧的非终结符称为起始符号。在我们的文法中,起始符号是expr:

您可以将规则expr解读为“一个expr可以是一个因子,可选地后跟乘法或除法运算符,然后是另一个因子,后者又可选地后跟乘法或除法运算符,然后是另一个因子,依此类推。 ”

什么是因子(factor)?就本文而言,因子只是一个整数。

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

  • | - 备择方案。条形表示“或”。所以 ( MUL | DIV ) 表示MUL或DIV。
  • ( … ) - 左括号和右括号表示对终端和/或非终端进行分组,如 ( MUL | DIV )。
  • ( … ) * - 匹配组内的内容零次或多次。

如果您过去使用过正则表达式,那么符号| , ()和(…) * 对您来说应该很熟悉。

文法通过解释语言可以形成的句子来定义语言。这是使用语法推导出算术表达式的方法:首先以起始符号expr开始,然后用该非终结符的规则体重复替换非终结符,直到生成仅由终结符组成的句子. 这些句子形成语言的语法定义。

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

我认为有几个例子是有序的。这是文法推导表达式3 的方式:


这就是文法推导表达式3 * 7 的方式:


现在,让我们将该文法映射到代码,好吗?

以下是我们将用于将文法转换为源代码的指南。通过遵循它们,您可以从字面上将文法转换为工作解析器:

1、文法中定义的每条规则R成为同名的方法,对该规则的引用成为方法调用:R()。该方法的主体遵循使用完全相同的准则的规则主体的流程。
2、替代方案(a1 | a2 | aN)成为if - elif - else 语句
3、一个可选的分组**(…) *** 变成一个可以循环零次或多次的while语句
4、每个标记引用T成为对方法eat的调用:eat(T)。eat方法的工作方式是,如果token T与当前的lookahead token匹配,那么它将使用token T,然后从lexer获取一个新的token,并将该token分配给当前的 token内部变量。

从视觉上看,指南如下所示:

让我们开始行动,按照上述指南将我们的语法转换为代码。

我们的文法中有两条规则:一条expr规则和一条factor规则。让我们从因子规则(生产)开始。根据指南,您需要创建一个名为factor(指南 1)的方法,该方法只需调用Eat方法即可使用INTEGER标记(指南 4):

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

很容易,不是吗?

继续!

规则expr变成了expr方法(再次根据准则 1)。规则的主体开始与一参考因子变为一个因子()方法的调用。可选的分组(…)*成为一个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下载该文件并使用它。它有一个交互式提示,您可以在其中输入表达式并查看它们是否有效:也就是说,根据语法构建的解析器是否可以识别表达式。

我忍不住再次提到语法图。这是相同expr规则的语法图的外观:

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

python代码

# -*- coding: utf-8 -*-
"""
@File    : calc4.py
@Time    : 2021/7/18 20:37
@Author  : Dontla
@Email   : sxana@qq.com
@Software: PyCharm
"""
# 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> ')text = input('calc> ')except EOFError:breakif not text:continuelexer = Lexer(text)interpreter = Interpreter(lexer)result = interpreter.expr()print(result)if __name__ == '__main__':main()

运行结果:

D:\python_virtualenv\my_flask\Scripts\python.exe C:/Users/Administrator/Desktop/编译原理/python/calc4.py
calc> 3 *  4   /2
6.0
calc> 4   /  5   * 4
3.2
calc>

代码给人看来有点懵逼,为什么要多加一个lexer?不用加不也能实现吗,为下张加括号做准备?

C语言代码

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>
#include<math.h>#define flag_digital 0
#define flag_plus 1
#define flag_minus 2
#define flag_multiply 3
#define flag_divide 4#define flag_EOF 5struct Token
{int type;int value;
};struct Lexer
{char* text;int pos;
};struct Interpreter
{struct Lexer* lexer;struct Token current_token;
};void error() {printf("输入非法!\n");exit(-1);
}void skip_whitespace(struct Lexer* le) {while (le->text[le->pos] == ' ') {le->pos++;}
}//判断Interpreter中当前pos是不是数字
int is_integer(char c) {if (c >= '0' && c <= '9')return 1;elsereturn 0;
}void advance(struct Lexer* le) {le->pos++;
}char current_char(struct Lexer* le) {return(le->text[le->pos]);
}//获取数字token的数值(把数字字符数组转换为数字)
int integer(struct Lexer* le) {char temp[20];int i = 0;while (is_integer(le->text[le->pos])) {temp[i] = le->text[le->pos];i++;advance(le);}int result = 0;int j = 0;int len = i;while (j < len) {result += (temp[j] - '0') * pow(10, len - j - 1);j++;}return result;
}void get_next_token(struct Interpreter* pipt) {//先跳空格,再判断有没有结束符if (current_char(pipt->lexer) == ' ')skip_whitespace(pipt->lexer);if (pipt->lexer->pos > (strlen(pipt->lexer->text) - 1)) {pipt->current_token = { flag_EOF, NULL };return;}char current = current_char(pipt->lexer);if (is_integer(current)) {pipt->current_token = { flag_digital, integer(pipt->lexer)};return;}if (current == '*') {pipt->current_token = { flag_multiply, NULL };pipt->lexer->pos++;return;}if (current == '/') {pipt->current_token = { flag_divide, NULL };pipt->lexer->pos++;return;}error();//如果都不是以上的字符,则报错并退出程序
}int eat(struct Interpreter* pipt, int type) {int current_token_value = pipt->current_token.value;if (pipt->current_token.type == type) {get_next_token(pipt);return current_token_value;}else {error();}
}int factor(struct Interpreter* pipt) {return eat(pipt, flag_digital);
}int expr(struct Interpreter* pipt) {get_next_token(pipt);int result;result = factor(pipt);while (true) {int token_type = pipt->current_token.type;if (token_type == flag_multiply) {eat(pipt, flag_multiply);result = result * factor(pipt);}else if (token_type == flag_divide) {eat(pipt, flag_divide);result = result / factor(pipt);}else {return result;}}
}int main() {char text[50];while (1){printf("请输入算式:\n");//scanf_s("%s", text, sizeof(text));//sanf没法输入空格?int i = 0;while ((text[i] = getchar()) != '\n') {//putchar(text[i]);i++;}text[i] = '\0';struct Lexer le = {text, 0};struct Interpreter ipt = { &le };int result = expr(&ipt);printf("= %d\n\n", result);}return 0;
}

运行结果:

请输入算式:
3*5
= 15请输入算式:
3*5/2
= 7请输入算式:
3  *  4   /44
= 0请输入算式:6 *  3   /9
= 2请输入算式:

都已经实现了,为什么还是很懵逼呢???

总结

新练习:

  • 编写一个语法来描述包含任意数量的 +、-、* 或 / 运算符的算术表达式。使用语法,您应该能够推导出诸如“2 + 7 * 4”、“7 - 8 / 4”、“14 + 2 * 3 - 6 / 2”等表达式。
  • 使用语法编写一个解释器,该解释器可以计算包含任意数量的 +、-、* 或 / 运算符的算术表达式。您的解释器应该能够处理诸如“2 + 7 * 4”、“7 - 8 / 4”、“14 + 2 * 3 - 6 / 2”等表达式。
  • 如果您完成了上述练习,请放松并享受:)

记住今天文章中的文法,回答以下问题,根据需要参考下图:

  • 什么是上下文无关文法(grammar)?
  • 文法有多少规则/产生式?
  • 什么是终端?(识别图中所有终端)
  • 什么是非终端?(识别图中所有非终端)
  • 什么是规则头?(识别图片中的所有头部/左侧)
  • 什么是规则体?(识别图片中的所有身体/右侧)
    -文法的起始符号是什么?

【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 4.)(python/c/c++版)(笔记)相关推荐

  1. 【编译原理】构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 9.)(笔记)语法分析(未完,先搁置了!)

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 9.) 文章目录 spi.py spi_lexer 我记得当我在大学(很久以前) ...

  2. 【编译原理】构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 8.)(笔记)一元运算符正负(+,-)

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 8.) 文章目录 C语言代码(作者没提供完整的python代码,关键的改动提供了 ...

  3. 【编译原理】构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 7.)(笔记)解释器 interpreter 解析器 parser 抽象语法树AST

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 7.) 文章目录 python代码 插--后序遍历 C语言代码(有错误) C语言 ...

  4. 【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 6.)(python/c/c++版)(笔记)

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 6.) 文章目录 python代码 C语言代码 总结 今天是这一天:) &quo ...

  5. 【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 5.)(python/c/c++版)(笔记)Lexer词法分析程序

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 5.) 文章目录 python代码 C语言代码 总结 你如何处理像理解如何创建解 ...

  6. 【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 3.)(python/c/c++版)(笔记)

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 3.) 文章目录 python代码calc3.py C语言代码(calc3.cp ...

  7. 【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 2.)(python/c/c++版)(笔记)

    [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpreter. Part 2.) 文章目录 python代码 c代码 总结 让我们再次深入研究解释器和编译 ...

  8. 【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 1.)(python/c/c++版)(笔记)

    原文:Let's Build A Simple Interpreter. Part 1. 文章目录 [编译原理]让我们来构建一个简单的解释器(Let's Build A Simple Interpre ...

  9. 学了编译原理能否用 Java 写一个编译器或解释器?

    16 个回答 默认排序​ RednaxelaFX JavaScript.编译原理.编程 等 7 个话题的优秀回答者 282 人赞同了该回答 能.我一开始学编译原理的时候就是用Java写了好多小编译器和 ...

最新文章

  1. demo flink写入kafka_Flink结合Kafka实时写入Iceberg实践笔记
  2. MySQL联合查询语法内联、左联、右联、全联
  3. 记本阶段建站心得,是走无限做垃圾站之路还是真正的开发之路
  4. BZOJ1036 (其实这只是一份板子)
  5. 《算法:C语言实现》—— 第二部分 —— 第3章 —— 基本数据结构
  6. (12) ejb学习: JPA的传播属性
  7. php ipg 透明,产品中心
  8. openEuler Summit 带你解锁开源与操作系统的不解之缘
  9. windows qt 使用openssl API
  10. 图像语义分割python_图像语义分割出的json文件和原图,用plt绘制图像mask
  11. ae合成设置快捷键_AE脚本使用快捷键控制关键帧操作 Keyboard v1.2.1 + 使用教程【资源分享1081】...
  12. 报表格式.fp3打开查看方式
  13. 【目标检测】Receptive Field Block Net for Accurate and Fast Object Detection论文理解
  14. java计算机毕业设计网络作业提交与批改系统源代码+数据库+系统+lw文档
  15. numpy中的log和ln函数
  16. OS_PV操作_4.过独木桥问题
  17. python BeautifulSoup的使用
  18. Cortex-M4操作模式
  19. u-boot-1.3.4 移植到S3C2440 (带有某些解析)
  20. 晨曦记账本,记账一目了然

热门文章

  1. [转]Tomcat启动错误的几件事
  2. id_Tech5_challenges--siggraph09
  3. 微软Silverlight 3正式版已经出炉
  4. 【温故知新】CSS学习笔记(选择器)
  5. 出去旅行带上这些常用日语就够啦!
  6. 37、Power Query-不使用IF嵌套进行匹配
  7. 抢红包的红包生成算法
  8. [abap] 通过动态参数获取字段数据
  9. 内部错误:无法加载 ABAP 报表 LVBRKF0I
  10. 素质教育,是救命稻草,还是压垮教培机构的最后一根稻草