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

Posted Dontla

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 4.)(python/c/c++版)(笔记)相关的知识,希望对你有一定的参考价值。

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


在上一篇文章中,您学习了如何解析(识别)和解释包含任意数量的加号或减号运算符的算术表达式,例如“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_token
        if 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 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> ')
            text = input('calc> ')
        except EOFError:
            break
        if not text:
            continue
        lexer = 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 5

struct 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;
	else
		return 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++版)(笔记)的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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