使用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
这个方法唯一的缺点是我们必须得更新跳转的地址,尤其是在遇到读取或者跳转等操作时需要不断的跳转。但是任何问题都有它对应解决的方案,有一个简单的例子就是在跳转的时候只允许调到指令的命名标签上,这样的话,在执行常量折叠之后就可以跳转到它们真正的地址上。
我们可以通过以下代码实现一个简单的“读取-求值-输出循环”的交互式编程环境:
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("")