[译]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("")                                        
时间: 2024-11-08 22:24:52

[译]Python编写虚拟解释器的相关文章

【译】使用 Python 编写虚拟机解释器

原文地址:[Making a simple VM interpreter in Python](https://csl.name/post/vm/) **更新:根据大家的评论我对代码做了轻微的改动.感谢 robin-gvx. bs4h 和 Dagur,具体代码见[这里](https://github.com/cslarsen/python-simple-vm)** Stack Machine 本身并没有任何的寄存器,它将所需要处理的值全部放入堆栈中而后进行处理.Stack Machine 虽然简

【JavaScript】【译】编写高性能JavaScript

英文链接:Writing Fast, Memory-Efficient JavaScript 很多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是专门为需要快速执行的大型JavaScript应用所设计的.如果你是一个开发者,并且关心内存使用情况与页面性能,你应该了解用户浏览器中的JavaScript引擎是如何运作的.无论是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其他引擎,这样做可以帮助你更好地优

Gvim入门(3)——简易配置python编写环境

用Gvim配置python编写环境最重要的也是最基本的一项就是要在Gvim的配置启动文件中,也就是在安装目录下的_vimrc中添加配置python解释器的代码.然而前提是要有配置好环境变量. 总结一下就是: (1)配置环境变量: 将python的路径添加到系统的环境变量的path里面,注意加分号.不会的就去百度里面google一下. (2)测试python配置环境变量是否成功: 在win+r,键入cmd,在dos(命令提示符)中键入python,如果结果是下图的情况,表示python环境变量配置

在python的交互式解释器中实现命令自动补全

Python的交互式解释器没有自带像Linux Shell那样的命令自动补全功能,可以编写一个模块来实现这一功能,模块源代码来自老男孩Linux培训机构的Python讲师Alex: # python startup file import sys import readline # tab completion readline.parse_and_bind('tab: complete')

python编写shell脚本详细讲解

今天需要编写一个shell脚本实现App自动生成的功能,需要处理HTTP REST请求,解析JSON,处理文件,执行命令等,本来想用shell搞定,但感觉比较麻烦,还是用python吧,虽然以前用Python都是在树莓派上玩的,多尝试一种方法总是好的. 虽然我受linux的影响甚深,但是对于*nix 里随处可见的sh脚本却是讨厌之极.为什么讨厌呢?首先是因为sh脚本那莫名其妙的语法,感觉就像随写随扔的程序,完全没有任何美感可言.其次是sh脚本的处理能力还是比较弱的,在文本处理.XML处理还有网络

11 个最佳的 Python 编译器和解释器

Python 是一门对初学者友好的编程语言,是一种多用途的.解释性的和面向对象的高级语言. 它拥有非常小的程序集,非常易于学习.阅读和维护.其解释器可在Windows.Linux 和 Mac OS 等多种操作系统上使用.它的可移植性和可伸缩性等特性使得它更加容易被运用. Python 库可用于以下用途: Web 开发 数据科学 机器学习 多媒体 软件开发 像 Django 这样的 Web 框架 GUI 应用 大多数极客认为 Python 是解释性语言,但它也存在编译过程. 编译部分在代码执行时完

使用C语言为python编写动态模块(1)--从底层深度解析python中的对象以及变量

楔子 我们知道可以通过使用C语言编写动态链接库的方式来给python加速,但是方式是通过ctypes来加载,通过类CDLL将动态链接库加载进来得到一个对象之后,通过这个对象来调用动态链接库里面的函数.那么问题来了,我们可不可以使用C语言为python编写模块呢?然后在使用的时候不使用ctypes加载动态库的方式,而是通过python的关键字import进行加载. 答案是可以的,我们知道可以通过编写py文件的方式来得到一个模块,那么也可以使用C语言来编写C源文件,然后再通过python解释器进行编

使用C语言为python编写动态模块(2)--解析python中的对象如何在C语言中传递并返回

楔子 编写扩展模块,需要有python源码层面的知识,我们之前介绍了python中的对象.但是对于编写扩展模块来讲还远远不够,因为里面还需要有python中模块的知识,比如:如何创建一个模块.如何初始化python环境等等.因此我们还需要了解一些前奏的知识,如果你的python基础比较好的话,那么我相信你一定能看懂,当然我们一开始只是介绍一个大概,至于细节方面我们会在真正编写扩展模块的时候会说. 关于使用C为python编写扩展模块,我前面还有一篇博客,强烈建议先去看那篇博客,对你了解Pytho

python编写登录接口(上)

中途经过了好几天都没有动手了,得坚持下去啊刚看了Alex老师的视频,其中有个题目如下:编写登录接口-输入用户密码-认证成功后显示欢迎信息-输错三次后锁定 # -*- coding: cp936 -*-#用户名保存在一个文件名为user.txt文件中import os,stringtmp=file('C:\Users\hityxg\Desktop\user.txt')username=file('C:\Users\hityxg\Desktop\username.txt','w')b=tmp.rea