[译]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,因此可以在这个类的基础上定义出属于我们的栈。

 from collections import deque

 class Stack(deque):
"""定义一个栈类"""
# 添加元素
push = deque.append # 返回最后一个元素
def top(self):
return self[-1]

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

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

 class Machine:
def __init__(self, code):
"""预先定义一个初始化函数""" self.data_stack = Stack()
self.return_addr_stack = Stack()
self.code = code
self.instruction_pointer = 0

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

 def pop(self):
return self.data_stack.pop() def push(self, value):
self.data_stack.push(value) def top(self):
return self.data_stack.top()

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

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

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

 def dispatch(self, op):
dispatch_map = {
"%": self.mod,
"*": self.mul,
"+": self.plus,
"-": self.minus,
"/": self.div,
"==": self.eq,
"cast_int": self.cast_int,
"cast_str": self.cast_str,
"drop": self.drop,
"dup": self.dup,
"if": self.if_stmt,
"jmp": self.jmp,
"over": self.over,
"print": self.print_,
"println": self.println,
"read": self.read,
"stack": self.dump_stack,
"swap": self.swap,
} if op in dispatch_map:
dispatch_map[op]()
elif isinstance(op, int):
# 如果指令是整型数据,就将数据存放到数据栈中
self.push(op)
elif isinstance(op, str) and op[0]==op[-1]=='"':
# 如果是字符串类型的,就将字符串内容存放到数据栈中
self.push(op[1:-1])
else:
raise RuntimeError("Unknown opcode: '%s'" % op)

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

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

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

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

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

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

流程如下图所示:

[译]Python编写虚拟解释器

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

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

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

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

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

3、简单优化:常量折叠

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

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

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

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

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

 def repl():
print('Hit CTRL+D or type "exit" to quit.') while True:
try:
source = raw_input("> ")
code = list(parse(source))
code = constant_fold(code)
Machine(code).run()
except (RuntimeError, IndexError) as e:
print("IndexError: %s" % e)
except KeyboardInterrupt:
print("\nKeyboardInterrupt")

5、课后作业

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

6、完整代码

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

 #!/usr/bin/env python
# coding: utf-8 """
A simple VM interpreter. Code from the post at http://csl.name/post/vm/
This version should work on both Python 2 and 3.
""" from __future__ import print_function
from collections import deque
from io import StringIO
import sys
import tokenize def get_input(*args, **kw):
"""Read a string from standard input."""
if sys.version[0] == "":
return raw_input(*args, **kw)
else:
return input(*args, **kw) class Stack(deque):
push = deque.append def top(self):
return self[-1] class Machine:
def __init__(self, code):
self.data_stack = Stack()
self.return_stack = Stack()
self.instruction_pointer = 0
self.code = code def pop(self):
return self.data_stack.pop() def push(self, value):
self.data_stack.push(value) def top(self):
return self.data_stack.top() def run(self):
while self.instruction_pointer < len(self.code):
opcode = self.code[self.instruction_pointer]
self.instruction_pointer += 1
self.dispatch(opcode) def dispatch(self, op):
dispatch_map = {
"%": self.mod,
"*": self.mul,
"+": self.plus,
"-": self.minus,
"/": self.div,
"==": self.eq,
"cast_int": self.cast_int,
"cast_str": self.cast_str,
"drop": self.drop,
"dup": self.dup,
"exit": self.exit,
"if": self.if_stmt,
"jmp": self.jmp,
"over": self.over,
"print": self.print,
"println": self.println,
"read": self.read,
"stack": self.dump_stack,
"swap": self.swap,
} if op in dispatch_map:
dispatch_map[op]()
elif isinstance(op, int):
self.push(op) # push numbers on stack
elif isinstance(op, str) and op[0]==op[-1]=='"':
self.push(op[1:-1]) # push quoted strings on stack
else:
raise RuntimeError("Unknown opcode: '%s'" % op) # OPERATIONS FOLLOW: def plus(self):
self.push(self.pop() + self.pop()) def exit(self):
sys.exit(0) def minus(self):
last = self.pop()
self.push(self.pop() - last) def mul(self):
self.push(self.pop() * self.pop()) def div(self):
last = self.pop()
self.push(self.pop() / last) def mod(self):
last = self.pop()
self.push(self.pop() % last) def dup(self):
self.push(self.top()) def over(self):
b = self.pop()
a = self.pop()
self.push(a)
self.push(b)
self.push(a) def drop(self):
self.pop() def swap(self):
b = self.pop()
a = self.pop()
self.push(b)
self.push(a) def print(self):
sys.stdout.write(str(self.pop()))
sys.stdout.flush() def println(self):
sys.stdout.write("%s\n" % self.pop())
sys.stdout.flush() def read(self):
self.push(get_input()) def cast_int(self):
self.push(int(self.pop())) def cast_str(self):
self.push(str(self.pop())) def eq(self):
self.push(self.pop() == self.pop()) def if_stmt(self):
false_clause = self.pop()
true_clause = self.pop()
test = self.pop()
self.push(true_clause if test else false_clause) def jmp(self):
addr = self.pop()
if isinstance(addr, int) and 0 <= addr < len(self.code):
self.instruction_pointer = addr
else:
raise RuntimeError("JMP address must be a valid integer.") def dump_stack(self):
print("Data stack (top first):") for v in reversed(self.data_stack):
print(" - type %s, value '%s'" % (type(v), v)) def parse(text):
# Note that the tokenizer module is intended for parsing Python source
# code, so if you're going to expand on the parser, you may have to use
# another tokenizer. if sys.version[0] == "":
stream = StringIO(unicode(text))
else:
stream = StringIO(text) tokens = tokenize.generate_tokens(stream.readline) for toknum, tokval, _, _, _ in tokens:
if toknum == tokenize.NUMBER:
yield int(tokval)
elif toknum in [tokenize.OP, tokenize.STRING, tokenize.NAME]:
yield tokval
elif toknum == tokenize.ENDMARKER:
break
else:
raise RuntimeError("Unknown token %s: '%s'" %
(tokenize.tok_name[toknum], tokval)) def constant_fold(code):
"""Constant-folds simple mathematical expressions like 2 3 + to 5."""
while True:
# Find two consecutive numbers and an arithmetic operator
for i, (a, b, op) in enumerate(zip(code, code[1:], code[2:])):
if isinstance(a, int) and isinstance(b, int) \
and op in {"+", "-", "*", "/"}:
m = Machine((a, b, op))
m.run()
code[i:i+3] = [m.top()]
print("Constant-folded %s%s%s to %s" % (a,op,b,m.top()))
break
else:
break
return code def repl():
print('Hit CTRL+D or type "exit" to quit.') while True:
try:
source = get_input("> ")
code = list(parse(source))
code = constant_fold(code)
Machine(code).run()
except (RuntimeError, IndexError) as e:
print("IndexError: %s" % e)
except KeyboardInterrupt:
print("\nKeyboardInterrupt") def test(code = [2, 3, "+", 5, "*", "println"]):
print("Code before optimization: %s" % str(code))
optimized = constant_fold(code)
print("Code after optimization: %s" % str(optimized)) print("Stack after running original program:")
a = Machine(code)
a.run()
a.dump_stack() print("Stack after running optimized program:")
b = Machine(optimized)
b.run()
b.dump_stack() result = a.data_stack == b.data_stack
print("Result: %s" % ("OK" if result else "FAIL"))
return result def examples():
print("** Program 1: Runs the code for `print((2+3)*4)`")
Machine([2, 3, "+", 4, "*", "println"]).run() print("\n** Program 2: Ask for numbers, computes sum and product.")
Machine([
'"Enter a number: "', "print", "read", "cast_int",
'"Enter another number: "', "print", "read", "cast_int",
"over", "over",
'"Their sum is: "', "print", "+", "println",
'"Their product is: "', "print", "*", "println"
]).run() print("\n** Program 3: Shows branching and looping (use CTRL+D to exit).")
Machine([
'"Enter a number: "', "print", "read", "cast_int",
'"The number "', "print", "dup", "print", '" is "', "print",
2, "%", 0, "==", '"even."', '"odd."', "if", "println",
0, "jmp" # loop forever!
]).run() if __name__ == "__main__":
try:
if len(sys.argv) > 1:
cmd = sys.argv[1]
if cmd == "repl":
repl()
elif cmd == "test":
test()
examples()
else:
print("Commands: repl, test")
else:
repl()
except EOFError:
print("")
上一篇:安装 Docker Machine - 每天5分钟玩转 Docker 容器技术(45)


下一篇:小白的docker极简入门(二)、5分钟教你玩转docker安装