Let’s Build A Simple Interpreter 1



为什么要你学解释器和编译器?这里你三条理由。

  1. 要写一个解释器或编译器,你必须同时用到很多技术。编写一个解释器或编译器会帮助 你提高这些技能并且成为一个更好的软件开发者。而且,你将学到的这些技能在开发任 何软件时都有可能用到,而不仅仅是解释器或编译器。
  2. 你确实想要知道计算机如何工作。一般解释器和编译器看上去都像魔法一样。但你不应 该对这些魔法感到舒服。你想要揭开解释器和编译器的神秘面纱,理解它们如何工作并 控制所有一切。
  3. 你想要创造自己的编程语言或者领域特定语言。如果是这样,你就需要为这个语言创建 一个解释器或编译器。最近,创建新语言再度兴起。你几乎每天都可以看到一门新语言 的诞生:Elixir, Go, Rust 等。

原文链接:https://ruslanspivak.com/lsbasi-part1/

好了,但什么是解释器编译器呢?

解释器与编译器

解释器与编译器都是“高级语言与机器之间的翻译官”。都是将代码翻译成机器可以执行的二进制机器码,只不过在运行原理和翻译过程不同。

那它们的区别在于:

用一个通俗的例子来讲:我们去饭馆吃饭,点了八菜一汤。编译器的方式就是厨师把所有的菜给你全做好了,一起给你端上来,至于你在哪吃,怎么吃,随便。解释器的方式就是厨师做好一个菜给你上一个菜,你就吃这个菜,而且必须在饭店里吃。

解释器与编译器的区别

编译器与解释器的工作流程的差别:

编译器与解释器的工作流程的差别

编译器与解释器的各自的特点:

各自特点

构造解释器V1.0

该系列文章的作者使用 Python 编写Pascal语言的解释器。

第一版V1.0,构造的计算器有诸多限制。如:

这些约束使得构建一个计算器很简单,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# Token types:
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'


class Token(object):
    def __init__(self, type, value):
	# token type: INTEGER, PLUS, or EOF
	self.type  = type
	# token value: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '+', or None
	self.value = value

    def __str__(self):
	"""String representation of the class instance.

	Examples:
	    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):
	# client string input, e.g. "3+5"
	self.text = text
	# self.pos is an index into self.text
	self.pos  = 0
	# current token instance
	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)

	This method is responsible for breaking a sentence
	apart into tokens. One token at a time.
	"""
	text = self.text

	# is self.pos index past the end of the self.text ?
	# if so, then return EOF token because there 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):
	# 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.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 = 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):

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

代码分析

假设我们在命令行输入一个表达式“3+5”。你的解释器得到一个字符串 “3+5”。为了使解释器真正理解如何处理这个字符串,需要先把输入的 “3+5” 拆分成被叫做 token 的部件。

词法分析:(lexical analysis,简称lexer,亦称scannertokenizer

​ 词法分析也称为 分词 ,此阶段编译器从左向右扫描源文件,将其字符流分割成一个个的 token记号 ,后文中将称为 token )。

Token

​ 所谓 token ,就是源文件中不可再进一步分割的一串字符,类似于英语中单词,或汉语中的词。

这里的 token 就是一个有类型的值的对象(即,token还存着值的类型)。例如对于字符串“3”来说,token 类型为 INTEGER , 相应的值是整数 3 。

解释器Interpreter要做的第一步就是读取输入的字符串并把他转化成 token 。解释器中做这个工作的部分被称为 词法分析器(lexical analyzer),简称 lexer 。也可以称它为: scannertokenizer 。他们的含义是一样的:表示解释器或编译器中将输入的字符串转化为 token 流的部分。

那是如何转化为token流呢?

img

现在 pos 指向了 text 中的字符‘+’,下次你调用这个方法时,它会先测试 pos 位 置的字符是否是数字,然后再测试它是否是加号,此时它是加号。这样该方法就递增 pos 并返回一个类型为 PLUS 值为‘+’的 token:

img

现在 pos 指向了字符‘5’。当你再次调用 get_next_token 时,它会检查 pos 位置 是否是一个数字,此时是的,因此它递增 pos 并返回一个类型为 INTEGER 值为‘5’的 token:

img

现在索引 pos 越过了字符串“3+5”的末尾,接下来每次调用 get_next_token 方法都会 返回 EOF token:

img

自己动手试试看看你的计算器的 lexer 组件怎么工作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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)
>>>

此时你的解释器已经可以从输入的字符流中获得 token 流了,解释器需要对它做点什么: 它需要从使用 lexer get_next_token 得到的字符流中找到结构。你的解释器期望从 流中找到如下的结构: INTEGER -> PLUS -> INTEGER. 即,它试着找到这样一个 token 序 列:整数后跟一个加号再跟一个整数。

负责查找和解释这个结构的方法是 expr. 这个方法验证一个 token 序列是否遵从期望的 token 序列,即 INTEGER -> PLUS -> INTEGER. 当确定遵从这个结构后,它就把 PLUS 左 边和右边 token 的值相加来生成结果,从而成功地解释了你传给解释器的算术表达式。

expr 方法使用了辅助方法 eat 来验证传给 eat 的 token 类型与当前的 token 类 型相匹配。在匹配到传入的 token 类型后, eat 方法会取得下一个 token 并把它赋值 给变量 current_token, 这样实际上是“吃掉”了当前匹配的 token 并把想象中的 token 流中的指针向前移动了。如果 token 流中的结构不遵从期望的 INTEGER PLUS INTEGER 序 列, eat 方法就会抛出一个异常。

小结

回顾一下你的解释器为了对一个算术表达式求值都做了什么:

  1. 解释器接Interpreter收一个输入字符串,假设为“3+5”
  2. 解释器调用了 expr 方法来从词法解析器 get_next_token 返回的 token 流中寻找一个结构。这个结构就是一个 INTEGER PLUS INTEGER 的形式。当确认了这个结构以后,它就使用把两个 INTEGER token 相加的方式来解释这个输入,因为此时解释器已经清楚 地知道它要做的就是把 3 和 5 两个整数相加。

祝贺你。你刚刚学会了怎么构造你的第一个解释器!

现在是时候做此练习了。

img

你不会觉得你刚刚读了这篇文章就足够了,是吧?好了,自己动手做下面的练习:

  1. 修改代码使得允许输入多位整数,例如“12+3”
  2. 增加一个跳过空白符的方法,使你的计算器可以处理包含空白符的输入如 “ 12 + 3”
  3. 修改代码使得它可以处理‘-’而非‘+’的情况

检查你的理解。

  1. 什么是解释器?
  2. 什么是编译器?
  3. 解释器和编译器的区别是什么?
  4. 什么是 token?
  5. 将输入拆分成 token 的过程叫什么?
  6. 解释器中做词法分析的部分叫什么?
  7. 解释器或编译器的这个部分还有什么其他常见的名字?

相关文章链接:

编译器与解释器:https://www.liujiangblog.com/course/python/9

Let’s Build A Simple Interpreter. Part 1:https://feng-qi.github.io/2018/01/23/lets-build-a-simple-interpreter-part-01/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值