[译]Python编写虚拟解释器

使用Python编写虚拟机解释器

一、实验说明

1. 环境登录

无需密码自动登录,系统用户名shiyanlou,密码shiyanlou

2. 环境介绍

本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到程序:

1. LX终端(LXTerminal):Linux命令行终端,打开后会进入Bash环境,可以使用Linux命令
2. GVim:非常好用的编辑器,最简单的用法可以参考课程Vim编辑器

3. 环境使用

使用R语言交互式环境输入实验所需的代码及文件,使用LX终端(LXTerminal)运行所需命令进行操作。

完成实验后可以点击桌面上方的“实验截图”保存并分享实验结果到微博,向好友展示自己的学习进度。实验楼提供后台系统截图,可以真实有效证明您已经完成了实验。

实验记录页面可以在“我的主页”中查看,其中含有每次实验的截图及笔记,以及每次实验的有效学习时间(指的是在实验桌面内操作的时间,如果没有操作,系统会记录为发呆时间)。这些都是您学习的真实性证明。

二、课程介绍

众所周知,python语言作为一门超级人性化的语言越来越被受到重视。虚拟服务同样受到人们的重视,就比如实验楼的虚拟实验环境,那么本次项目的目的就是让大家学会使用python制作一个虚拟解释器,这里的虚拟解释器指的是一定意义上的堆栈机

感谢Christian Stigen Larsen的开源项目Crianza,那么我们就跟着他一起学习如何创建一个解释器吧!

三、课程内容

>解释器是能够执行用其他计算机语言编写的程序的系统软件,它是一种翻译程序。它的执行方式是一边翻译一边执行,因此其执行效率一般偏低,但是解释器的实现较为简单,而且编写源程序的高级语言可以使用更加灵活和富于表现力的语法。

1、构建堆栈机

堆栈机本身并不拥有寄存器,它的执行原理非常简单:将需要处理的值放入栈中,然后执行它们。尽管堆栈机的原理就是这么简单,但是不能不说它确实很强大,不然Python、Java等高级语言也不会将它作为它们的虚拟机。

无论如何,先来深入了解一下堆栈的原理。首先,我们需要一个指令指针栈,它能够储存返回地址。这个返回地址是当我们执行一个子例程(比如函数)的时候,需要用它跳回到开始调用该函数的地方。

那么有了这个神奇的堆栈,很多复杂难以理解的程序就变得非常简单。比如说,有这么一个数学表达式:(2+3)*4。在堆栈机中,这个数学表达式等价于2 3 + 4 * ——将'2'和'3'依次推入栈中,接下来要推入的指令是'+',将前面两个数字弹出,令他们执行加法运算后再将它们的和入栈。然后依次将'2'与'3'的和与4相乘的结果推入栈中。运算结束,so easy!

那么,让我们开始建立一个栈,由于Python这个语言拥有类似于C语言中数据结构的一个类collections.deque,因此可以在这个类的基础上定义出属于我们的栈。

 1 from collections import deque
 2 
 3 class Stack(deque):
 4 """定义一个栈类"""
 5     # 添加元素
 6     push = deque.append
 7 
 8     # 返回最后一个元素
 9     def top(self):
10         return self[-1]    

 

那么这里面既然定义了'push'、'top'方法,为什么没有定义'pop'?因为'deque'这个类本身就拥有方法'pop',除了'pop'还有'popleft'呢,有兴趣的同学可以研究一下这个类与'list'对象的区别和联系。

接下来,让我们建立一个虚拟机类——'Machine'。综上所述,我们需要两个栈和一段存储代码的内存空间。得益于Python的动态类型,因此我们可以往列表里面存储任何东西,但是我们不能区分列表里面的内置函数和字符串,正确的做法是将Python内置函数单独存放于一个列表,关于这个问题大家可以思考一下。在这个项目中用的是字典)方法,键值分别对应字符串和函数。另外,我们还需要一个指令指针,用来指向代码中下一个需要被执行的模块。

1 class Machine:
2     def __init__(self, code):
3     """预先定义一个初始化函数"""
4 
5         self.data_stack = Stack()
6         self.return_addr_stack = Stack()
7         self.code = code
8         self.instruction_pointer = 0        

再创建一些栈结构中必备的函数:

1 def pop(self):
2     return self.data_stack.pop()
3 
4 def push(self, value):
5     self.data_stack.push(value)
6 
7 def top(self):
8     return self.data_stack.top()

 

为了执行我们“操作码”(实际上,并不是真正意义上的操作码,只是一种动态类型,但是你懂得~)我们需要建立一个'dispatch'函数。但是在这之前,我们需要创建一个解释器的循环:

1 def run(self):
2 """代码运行的条件"""
3 
4     while self.instruction_pointer < len(self.code):
5     opcode = self.code[self.instruction_pointer]
6     self.instruction_pointer += 1
7     self.dispatch(opcode)

 

 

上面的代码原理很简单:获取下一个指令,指令指针自增1个然后基于操作码执行'dispatch'函数,下面是'dispatch'函数的定义(函数定义有点长,你们可以尝试改进一下):

 1 def dispatch(self, op):
 2     dispatch_map = {
 3         "%": self.mod,
 4         "*": self.mul,
 5         "+": self.plus,
 6         "-": self.minus,
 7         "/": self.div,
 8         "==": self.eq,
 9         "cast_int": self.cast_int,
10         "cast_str": self.cast_str,
11         "drop": self.drop,
12         "dup": self.dup,
13         "if": self.if_stmt,
14         "jmp": self.jmp,
15         "over": self.over,
16         "print": self.print_,
17         "println": self.println,
18         "read": self.read,
19         "stack": self.dump_stack,
20         "swap": self.swap,
21         }
22 
23     if op in dispatch_map:
24         dispatch_map[op]()
25     elif isinstance(op, int):
26     # 如果指令是整型数据,就将数据存放到数据栈中
27         self.push(op)
28     elif isinstance(op, str) and op[0]==op[-1]=='"':
29     # 如果是字符串类型的,就将字符串内容存放到数据栈中
30         self.push(op[1:-1])
31     else:
32         raise RuntimeError("Unknown opcode: '%s'" % op)        

 

上面的代码非常浅显易懂,就是当输入一段指令,该函数就会根据这段指令在'dispatch_map'字典中找到对应的方法,比如:符号'\*'对应的是'self.mul'函数。以上过程就类似于'Forth'语言的构建过程。

当输入指令'*',程序就会执行函数'self.mul',所以我们还需要定义对应的函数:

1 def mul(self):
2     self.push(self.pop() * self.pop())

其他的函数定义也是依次类推,根据它们的功能和名称定义不同的函数。

到这里,你可以定义你想要的函数了,一个虚拟机环境基本构成就是这样!

然而并没有完,环境搭建好了,最重要的'解释'还没有完成,一个语言解释器包括两部分:
1. 解析:解析部分接受一个由字符序列表示的输入指令,然后将输入字符分解成一系列的词法单元
2. 执行:程序内部的解释器根据语义规则进一步处理词法单元,进而执行原指令的实际运算。

流程如下图所示:

下面一节中,我们将会讨论如何构建解析器。

2、为我们的指令创建一个简单的解析器

让我们使用'tokenize'模块为输入的指令构建一个解析器吧~

 1 import tokenize
 2 from StringIO import StringIO
 3 
 4 def parse(text):
 5 
 6     # 以StingIO的形式将text对象读入到内存中,并以字符串形式返回到                                                    generate_tokens()函数中
 7     tokens = tokenize.generate_tokens(StringIO(text).readline)
 8 
 9     # generate_tokens生成器生成一个5元祖:标记类型、标记字符串、标记开始位置二元组、标记结束位置二元组以及标记所在的行号
10     # 下面大写的单词都属于token模块的常量
11     for toknum, tokval, _, _, _ in tokens:
12         if toknum == tokenize.NUMBER:
13             yield int(tokval)
14         elif toknum in [tokenize.OP, tokenize.STRING,         tokenize.NAME]:
15             yield tokval
16         elif toknum == tokenize.ENDMARKER:
17             break
18         else:
19             raise RuntimeError("Unknown token %s: '%s'" %
20     (tokenize.tok_name[toknum], tokval))                           

 

更多关于Python令牌器('tokenize')的常量查看请查阅官方文档

3、简单优化:常量折叠

>“常量折叠”是 就是在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表。可以算作一种编译优化。

 1 def constant_fold(code):
 2 """对简单的数学表达式诸如:2 3 + 进行计算并将结果作为常数返回原指令列表中"""
 3     while True:
 4     # 在指令中找到两个连续的数字以及一个算数运算符
 5         for i, (a, b, op) in enumerate(zip(code, code[1:], code[2:])):
 6            if isinstance(a, int) and isinstance(b, int) \
 7             and op in {"+", "-", "*", "/"}:
 8                 m = Machine((a, b, op))
 9                 m.run()
10                 code[i:i+3] = [m.top()]
11                 print("Constant-folded %s%s%s to %s" % (a,op,b,m.top()))
12                 break
13          else:
14             break
15     return code

这个方法唯一的缺点是我们必须得更新跳转的地址,尤其是在遇到读取或者跳转等操作时需要不断的跳转。但是任何问题都有它对应解决的方案,有一个简单的例子就是在跳转的时候只允许调到指令的命名标签上,这样的话,在执行常量折叠之后就可以跳转到它们真正的地址上。

4、读取-求值-输出循环

我们可以通过以下代码实现一个简单的“读取-求值-输出循环”的交互式编程环境:

 1 def repl():
 2     print('Hit CTRL+D or type "exit" to quit.')
 3 
 4     while True:
 5         try:
 6             source = raw_input("> ")
 7         code = list(parse(source))
 8         code = constant_fold(code)
 9         Machine(code).run()
10     except (RuntimeError, IndexError) as e:
11        print("IndexError: %s" % e)
12     except KeyboardInterrupt:
13         print("\nKeyboardInterrupt")

5、课后作业

1. 列表项试着在不查看完整源代码的情况下制作这个虚拟解释器(可以参考Python内置函数),并尝试生成'Fibonacci'序列,将运行过程和结果截图;
2. 如果完成了第一题,恭喜你,又'get'一个技能,你可以查看下面的完整代码对比你自己的代码,把你的代码中重要的细节和你的思考写入实验报告;
3. 那么接下来请尝试给指令中的函数添加'call'和'return'功能。提示:'call'函数是先将当前地址返回到栈中,再调用'self.jmp'函数。'return'函数显然是先将栈中的地址弹出,根据该地址设置一个指令指针从'call'函数中返回到原来开始被调用的地方。

6、完整代码

代码同样可以通过github获取:

  1 #!/usr/bin/env python
  2 # coding: utf-8
  3 
  4 """
  5 A simple VM interpreter.
  6 
  7 Code from the post at http://csl.name/post/vm/
  8 This version should work on both Python 2 and 3.
  9 """
 10 
 11 from __future__ import print_function
 12 from collections import deque
 13 from io import StringIO
 14 import sys
 15 import tokenize
 16 
 17 
 18 def get_input(*args, **kw):
 19 """Read a string from standard input."""
 20     if sys.version[0] == "2":
 21         return raw_input(*args, **kw)
 22     else:
 23         return input(*args, **kw)
 24 
 25 
 26 class Stack(deque):
 27     push = deque.append
 28 
 29     def top(self):
 30         return self[-1]
 31 
 32 
 33 class Machine:
 34     def __init__(self, code):
 35         self.data_stack = Stack()
 36         self.return_stack = Stack()
 37         self.instruction_pointer = 0
 38         self.code = code
 39 
 40     def pop(self):
 41         return self.data_stack.pop()
 42 
 43     def push(self, value):
 44         self.data_stack.push(value)
 45 
 46     def top(self):
 47         return self.data_stack.top()
 48 
 49     def run(self):
 50         while self.instruction_pointer < len(self.code):
 51             opcode = self.code[self.instruction_pointer]
 52             self.instruction_pointer += 1
 53             self.dispatch(opcode)
 54 
 55     def dispatch(self, op):
 56         dispatch_map = {
 57             "%": self.mod,
 58             "*": self.mul,
 59             "+": self.plus,
 60             "-": self.minus,
 61             "/": self.div,
 62             "==": self.eq,
 63             "cast_int": self.cast_int,
 64             "cast_str": self.cast_str,
 65             "drop": self.drop,
 66             "dup": self.dup,
 67             "exit": self.exit,
 68             "if": self.if_stmt,
 69             "jmp": self.jmp,
 70             "over": self.over,
 71             "print": self.print,
 72             "println": self.println,
 73             "read": self.read,
 74             "stack": self.dump_stack,
 75             "swap": self.swap,
 76         }
 77 
 78         if op in dispatch_map:
 79             dispatch_map[op]()
 80         elif isinstance(op, int):
 81             self.push(op) # push numbers on stack
 82         elif isinstance(op, str) and op[0]==op[-1]=='"':
 83         self.push(op[1:-1]) # push quoted strings on stack
 84         else:
 85             raise RuntimeError("Unknown opcode: '%s'" % op)
 86 
 87         # OPERATIONS FOLLOW:
 88 
 89     def plus(self):
 90         self.push(self.pop() + self.pop())
 91 
 92     def exit(self):
 93         sys.exit(0)
 94 
 95      def minus(self):
 96         last = self.pop()
 97         self.push(self.pop() - last)
 98 
 99     def mul(self):
100         self.push(self.pop() * self.pop())
101 
102     def div(self):
103         last = self.pop()
104         self.push(self.pop() / last)
105 
106     def mod(self):
107         last = self.pop()
108         self.push(self.pop() % last)
109 
110     def dup(self):
111         self.push(self.top())
112 
113     def over(self):
114         b = self.pop()
115         a = self.pop()
116         self.push(a)
117         self.push(b)
118         self.push(a)
119 
120     def drop(self):
121         self.pop()
122 
123     def swap(self):
124         b = self.pop()
125         a = self.pop()
126         self.push(b)
127         self.push(a)
128 
129     def print(self):
130         sys.stdout.write(str(self.pop()))
131         sys.stdout.flush()
132 
133     def println(self):
134         sys.stdout.write("%s\n" % self.pop())
135         sys.stdout.flush()
136 
137     def read(self):
138         self.push(get_input())
139 
140     def cast_int(self):
141         self.push(int(self.pop()))
142     
143     def cast_str(self):
144         self.push(str(self.pop()))
145 
146     def eq(self):
147         self.push(self.pop() == self.pop())
148 
149     def if_stmt(self):
150         false_clause = self.pop()
151         true_clause = self.pop()
152         test = self.pop()
153         self.push(true_clause if test else false_clause)
154 
155     def jmp(self):
156         addr = self.pop()
157         if isinstance(addr, int) and 0 <= addr < len(self.code):
158             self.instruction_pointer = addr
159         else:
160             raise RuntimeError("JMP address must be a valid integer.")
161 
162     def dump_stack(self):
163         print("Data stack (top first):")
164 
165         for v in reversed(self.data_stack):
166             print(" - type %s, value '%s'" % (type(v), v))
167 
168 
169 def parse(text):
170     # Note that the tokenizer module is intended for parsing Python source
171     # code, so if you're going to expand on the parser, you may have to use
172     # another tokenizer.
173 
174     if sys.version[0] == "2":
175         stream = StringIO(unicode(text))
176     else:
177         stream = StringIO(text)
178 
179         tokens = tokenize.generate_tokens(stream.readline)
180 
181         for toknum, tokval, _, _, _ in tokens:
182             if toknum == tokenize.NUMBER:
183                 yield int(tokval)
184             elif toknum in [tokenize.OP, tokenize.STRING, tokenize.NAME]:
185                 yield tokval
186             elif toknum == tokenize.ENDMARKER:
187                 break
188             else:
189                 raise RuntimeError("Unknown token %s: '%s'" %
190 (tokenize.tok_name[toknum], tokval))
191 
192 def constant_fold(code):
193 """Constant-folds simple mathematical expressions like 2 3 + to 5."""
194     while True:
195     # Find two consecutive numbers and an arithmetic operator
196         for i, (a, b, op) in enumerate(zip(code, code[1:], code[2:])):
197             if isinstance(a, int) and isinstance(b, int) \
198 and op in {"+", "-", "*", "/"}:
199                 m = Machine((a, b, op))
200                 m.run()
201                 code[i:i+3] = [m.top()]
202                 print("Constant-folded %s%s%s to %s" % (a,op,b,m.top()))
203                 break
204         else:
205             break
206     return code
207 
208 def repl():
209     print('Hit CTRL+D or type "exit" to quit.')
210 
211     while True:
212         try:
213             source = get_input("> ")
214             code = list(parse(source))
215             code = constant_fold(code)
216             Machine(code).run()
217         except (RuntimeError, IndexError) as e:
218             print("IndexError: %s" % e)
219         except KeyboardInterrupt:
220             print("\nKeyboardInterrupt")
221 
222 def test(code = [2, 3, "+", 5, "*", "println"]):
223         print("Code before optimization: %s" % str(code))
224     optimized = constant_fold(code)
225     print("Code after optimization: %s" % str(optimized))
226 
227     print("Stack after running original program:")
228     a = Machine(code)
229     a.run()
230     a.dump_stack()
231 
232     print("Stack after running optimized program:")
233     b = Machine(optimized)
234     b.run()
235     b.dump_stack()
236 
237     result = a.data_stack == b.data_stack
238     print("Result: %s" % ("OK" if result else "FAIL"))
239     return result
240 
241     def examples():
242     print("** Program 1: Runs the code for `print((2+3)*4)`")
243     Machine([2, 3, "+", 4, "*", "println"]).run()
244 
245     print("\n** Program 2: Ask for numbers, computes sum and product.")
246     Machine([
247         '"Enter a number: "', "print", "read", "cast_int",
248         '"Enter another number: "', "print", "read", "cast_int",
249         "over", "over",
250         '"Their sum is: "', "print", "+", "println",
251         '"Their product is: "', "print", "*", "println"
252         ]).run()
253 
254     print("\n** Program 3: Shows branching and looping (use CTRL+D to exit).")
255     Machine([
256         '"Enter a number: "', "print", "read", "cast_int",
257         '"The number "', "print", "dup", "print", '" is "', "print",
258         2, "%", 0, "==", '"even."', '"odd."', "if", "println",
259         0, "jmp" # loop forever!
260         ]).run()
261 
262 
263 if __name__ == "__main__":
264     try:
265         if len(sys.argv) > 1:
266             cmd = sys.argv[1]
267             if cmd == "repl":
268                 repl()
269             elif cmd == "test":
270                 test()
271                 examples()
272             else:
273                 print("Commands: repl, test")
274         else:
275             repl()
276     except EOFError:
277         print("")                                        

 

posted @ 2015-07-28 17:29  wing1995  阅读(541)  评论(0编辑  收藏  举报