构建一个简单的编译器系列教程.Part 1

Posted Python程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了构建一个简单的编译器系列教程.Part 1相关的知识,希望对你有一定的参考价值。


Python部落(www.freelycode.com)组织翻译,禁止转载,欢迎转发


如果你不了解编译器的工作原理,那么你就不能很好的了解计算机的工作原理。你必须确保自己百分百的了解编译器的工作原理,才能跟朋友说,你是懂得计算机如何工作的。
— Steve Yegge

       

仔细听好,不论你是有经验的软件开发者还是刚刚入门的菜鸟,如果你不知道编译器和解释器如何工作,你意味着,你根本不了解计算机是如何工作的。就是这么简单的道理。


现在,请问下自己,是否百分百懂得编译器和解释器是如何工作的?如果你不懂的话....
构建一个简单的编译器系列教程.Part 1
如果你确实不知道,而且对此感到激动,请继续往下看。
构建一个简单的编译器系列教程.Part 1
不要担心。如果你能坚持看完这个系列的教程,跟我一起一步一步手工构建一个编译器和解释器以后,你就不会再为此而烦恼啦。至少,我是这么期望的。
构建一个简单的编译器系列教程.Part 1
在此,我会给你

三个学习解释器(interpreters)和编译器(compilers)的理由


A. 在写一个解释器(interpreters)或是编译器(compilers)的过程,同时是对许多专业技术和技巧有机结合的综合运用的过程。 这个过程会帮你学习和提高相关的技能和技巧,从而提高你的软件开发能力。这些技能和技巧不仅适用于解释器(interpreters)和编译器(compilers)的开发,对其他方面的开发场景同样适用。


B. 假如你此刻对解释器(interpreters)和编译器(compilers)的工作原理充满了求知欲,迫切的想要揭开它神秘的面纱,清楚它的工作原理,并掌控它,为己所用。


C. 当你想要创建一门自己的变成语言,或所在领域的特定语言。那么你需要给这门语言开发配套的解释器或是编译器。不仅如此,圈子里,大家创造新语言的热情也重新高涨了起来,几乎每天都能看到一门新语言横空出世,比如:Elixir, Go, Rust等。

下面,我们来谈一下,

什么是解释器和编译器?
       

解释器和编译器的目标,是将源码翻译为另一种更高级的语言,以新的形式存在。是不是感觉,还是挺模糊的,暂且先记住上面的那句话,后面,在这个系列的教程中,我会让你彻底的明白,源码程序是如何被翻译的。
       

现在,你一定还想知道,解释器和编译器到底有什么区别?如果这个转换器能够将程序源码(Source Code)翻译成机器语言,那么我们称之为编译器。如果这个转换器不需要将程序源码翻译成机器语言(Machine Language),就可以直接处理并执行程序,那么我们称之为解释器。通过下面的图片,我们可以直观的看出不同来:
构建一个简单的编译器系列教程.Part 1
相信现在,你已经有了学习解释器和编译器的动力,下面告诉你,通过本系列的文章,你能了解到关于解释器的哪些内容:


你和我一起去创建一个Pascall语言子集的解释器。在教程系列结束的时候,你会做出一个正常工作的Pascal解释器,和一个类似Python pdb的源码级的Pascal调试器(debugger)。


你可能会问,为什么选择Pascal?第一,这不是我为了这个系类的教程,随便编造的语言,他是一门真实的正在被广泛使用的,拥有很多重要的语言设计。很多老旧的,但十分有用的计算机科学书籍,都用Pascal编程语言来编写他们的例子(这并不是选择这门语言的根本原因,但是,咱们偶尔了解一下非主流的语言,也是很有趣的,不是吗?)。


下面是一个Pascal语言中的阶乘(factorial)函数的代码,咱们要用自己的编译器实现下面代码的解释,并用自己编写的交互式源码级调试器进行相应的调试:


program factorial;
 
function factorial(n: integer): longint;
begin
    if n = 0 then
        factorial := 1
    else
        factorial := n * factorial(n - 1);
end;
 
var
    n: integer;
 
begin
    for n := 0 to 16 do
        writeln(n, '! = ', factorial(n));
end.


我们选择Python语言作为马上要做的Pascal解释器的实现语言,但是你也可以根据自己的喜好选择其他实现语言,因为思路是夸语言的。好了,废话不多说,开始动手吧!


首先,我们来写一个算数表达式,也被称为计算器。今天的目标很简单:让你的计算器能够处理两个数字的整数的加法运算,比如3+5。下面就是你要写的计算器(解释器)的源码:


# Token 类型
#
# EOF (文件结尾) 这个token 类型表示没有更多输入用来做语义分析了

INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'
 
class Token(object):
    def __init__(self, type, value):
        # token 类型: INTEGER, PLUS, or EOF
        self.type = type
        # token 值: 0, 1, 2. 3, 4, 5, 6, 7, 8, 9, '+', or None
        self.value = value
 
    def __str__(self):
        """类的字符串实例表示
 
        形式如:
            Token(INTEGER, 3)
            Token(PLUS '+')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )
 
    def __repr__(self):
        return self.__str__()
 
 
class Interpreter(object):
    def __init__(self, text):
        # 客户端输入字符串,如: "3+5"
        self.text = text
        # self.pos 是self.text里面每个字符的索引
        self.pos = 0
        # 当前token 实例
        self.current_token = None
 
    def error(self):
        raise Exception('Error parsing input')
 
    def get_next_token(self):
        """Lexical analyzer (also known as scanner or tokenizer)
 
        该方法负责把输入的句子分割为一个一个的token,一次一个。
        """
        text = self.text
 
        # is self.pos index past the end of the self.text ?
        # if so, then return EOF token because there is no more
        # input left to convert into tokens
        if self.pos > len(text) - 1:
            return Token(EOF, None)
 
        # get a character at the position self.pos and decide
        # what token to create based on the single character
        current_char = text[self.pos]
 
        # if the character is a digit then convert it to
        # integer, create an INTEGER token, increment self.pos
        # index to point to the next character after the digit,
        # and return the INTEGER token
        if current_char.isdigit():
            token = Token(INTEGER, int(current_char))
            self.pos += 1
            return token
 
        if current_char == '+':
            token = Token(PLUS, current_char)
            self.pos += 1
            return token
 
        self.error()
 
    def eat(self, token_type):
        # 比较传入的token_type和current_type是否一致
        # 如果一致,则“eat”掉current_type
        # 把next_token赋值给self.current_token,
        # 否则抛异常.
        if self.current_token.type == token_type:
            self.current_token = self.get_next_token()
        else:
            self.error()
 
    def expr(self):
        """expr -> INTEGER PLUS INTEGER"""
        # set current token to the first token taken from the input
        self.current_token = self.get_next_token()
 
        # we expect the current token to be a single-digit integer
        left = self.current_token
        self.eat(INTEGER)
 
        # we expect the current token to be a '+' token
        op = self.current_token
        self.eat(PLUS)
 
        # we expect the current token to be a single-digit integer
        right = self.current_token
        self.eat(INTEGER)
        # after the above call the self.current_token is set to
        # EOF token
 
        # at this point INTEGER PLUS INTEGER sequence of tokens
        # has been successfully found and the method can just
        # return the result of adding two integers, thus
        # effectively interpreting client input
        result = left.value + right.value
        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
        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)
 
 
if __name__ == '__main__':
    main()


保存上面的代码到文件 calc1.py 或者直接从 GitHub 下载。在你仔细研究这段代码之前,先在命令行下运行这段代码,看下运行效果。 下面是在我笔记本上运行的效果(如果你想在 Python3 环境下运行,请先替换代码中的 raw_input 方法为 input):


$ python calc1.py
calc> 3+4
7
calc> 3+5
8
calc> 3+9
12
calc>


为了正常运行你的简单计算器,保证不抛异常,你需要准守以下规则:

  • 只许输入各位十进制数字

  • 目前支持的算法只有加法

  • 输入中不许出现空格


现在这个计算器还太简单,不够健壮,需要上面三条约束,不用担心,很快我们就会进行相应的完善。


好的,下面我们来深入研究下,你的解释器的工作原理以及它是如何求解算数运算表达式的。

       

当你在命令行输入3+5时,你的解释器得到的是一个字符串“3+5”。为了使解释器看懂这串字符串,解释器先将字符串“3+5”进行分解,分解出来的组件,我们称之为tokens。一个token就是一个拥有类型和值的对象。比如,字符串“3”,这个token的类型为整数(INTEGER),对应的值(value)为整数3。


把字符串分解为tokens的过程,称之为词法分析(lexical analysis)。因此,你的解释器的第一步就是进行词法分析,把输入的字符串,分解为一个一个的短的token。


编译器(Inerpreter)类的 get_next_token 方法就是你的词法分析器(lexical analyzer)。每次调用该方法,你就得到传入字符串的下一个方法。让我们仔细看一下这个方法,看看它具体是如何将字符串转化为tokens的。输入的字符串存储在变量 text 中,并且在变量中,包含字符的索引 (可以把他看作是一个字符串数组)。 pos 的初始值被置为 0 并指向字符 ‘3’,如下图。这个方法首先检查这个字符是不是数字,如果是,那么 pos 值加1,并返回一个类型是整数,值为3的token实例:
构建一个简单的编译器系列教程.Part 1
现在 pos 值为1,指向字符串的第二个字符,“+”号。第二次调用那个方法,会校验当前位置的字符是不是数字或者是加法运算符。当确认为加法运算符后, pos 自加1,并返回一个类型为 PLUS 值为 “+” 的token实例:
构建一个简单的编译器系列教程.Part 1
现在 pos 指向字符 ‘5’。 再次调用 get_next_token 方法时,再一次校验字符是否为数值,如果是的话, pos  的值加1,并返回一个新的整数型token,值为 5:


因为 pos 索引的值已超过字符串 “3+5” 的长度,此时调用 get_next_token 方法,会返回 EOF token:

亲自尝试一下吧,看看你的解释器的词法分析组件是如何工作的:


>>> from calc1 import Interpreter
>>>
>>> interpreter = Interpreter('3+5')
>>> interpreter.get_next_token()
Token(INTEGER, 3)
>>>
>>> interpreter.get_next_token()
Token(PLUS, '+')
>>>
>>> interpreter.get_next_token()
Token(INTEGER, 5)
>>>
>>> interpreter.get_next_token()
Token(EOF, None)
>>>


现在你的解释器有访问tokens的权限了,下面以这些token为对象,做些具体的操作吧。解释器需要识别出这些从方法get_next_token tokens拿到的字符串的结构 。你的解释器需要找出以下结构: INTEGER -> PLUS -> INTEGER。换句话说,解释器尝试着去找出tokens之间的顺序: 整数 + 加法运算符 + 整数 这么一个顺序。


负责找出并解释结构的是 expr 方法。这个方法确保tokens的顺序符合期望的顺序,比如: INTEGER -> PLUS -> INTEGER。当成功确认顺序以后,它会生成加法运算结果,把加法运算符左边的数字和右边的数字加起来,这样就成功解释了你的运算表达式。


expr 方法本身依赖帮助方法 eat 来确保传入的token类型和当前token(current_token)类型相匹配。检查匹配后,eat 方法拿到下一个token,并赋予变量 current_token。然后指针自加1,去匹配下一个token。如果tokens中的结构不符合预期的 “INTEGER PLUS INTEGER” 顺序,那么,eat方法会抛出异常。


让我们总结一下你的解释器运算出算数表达式的过程吧:

  • 解释器接收输入字符串: “3+5”

  • 解释器调用 expr 方法去确认由词法分析器 get_next_token 返回的tokens的结构。确认结构为INTEGERPLUS INTEGER后,然后解释器会把连个INTEGER tokens 对应的值想加,即 3 + 5。


恭喜你,你刚刚学会了如何构建第一个非常简单的解释器!
下面,让我们做一些练习吧。

不会认为仅仅读完这边文章就够了吧?哈哈,跟着下面的提示做些练习题吧:
1. 修改代码,使程序支持多位整数的输入:  “12+3”
2. 添加一个方法,可以过滤掉空格,如此一来就可以处理输入中有空格的表达式了,比如:” 12 + 3”
3. 修改代码,把加法运算功能呢,改为减法运算,如:“7-5”


检验一下对本文的理解:
1. 什么是解释器(interpreter)?
2. 什么是编译器(compiler)?
3. 编译器和解释器之间的区别在哪?
4. 什么是token?
5. 把输入字符拆解成tokens的处理过程是什么?
6. 做词法分析的部分叫啥?
7. 一个完成解释器或编译器的组件,还有什么其他叫法?


在本文结束前, 我需要提醒你,如果你只是把本文浏览了个大概,请从新好好看一便。 如果你读的很仔细但是没有做练习,请立刻去做。


好的,这就是今天的全部内容,下一篇文章,我们一起来扩展你的算法,让他实现更多算法。


英文原文: <https://ruslanspivak.com/lsbasi-part1/>
译者:cg2580

以上是关于构建一个简单的编译器系列教程.Part 1的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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