使用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. 执行:程序内部的解释器根据语义规则进一步处理词法单元,进而执行原指令的实际运算。

流程如下图所示:

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

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("")

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

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

    [译]如何使用 Python 创建一个虚拟机解释器? 原文地址:Making a simple VM interpreter in Python 更新:根据大家的评论我对代码做了轻微的改动.感谢 ro ...

  2. RobotFramework自动化测试框架-使用Python编写自定义的RobotFramework Lib

    使用Python构建Lib工程 可以用来开发Python Lib的IDE工具有很多,常见的有Pycharm,Eclipse with PyDev插件等,而且在RobotFramework官网中也已经提 ...

  3. 【转】python编写规范——中标软件有限公司测试中心

    [转]python编写规范 一.说明 二.内容 1. 代码布局 1.1 缩进 1.2 表达式和语句中的空格 1.3 行的最大长度 1.4 空行... 1.5 编码... 2. 语句... 2.1 标准 ...

  4. python编写shell脚本详细讲解

    python编写shell脚本详细讲解 那,python可以做shell脚本吗? 首先介绍一个函数: os.system(command) 这个函数可以调用shell运行命令行command并且返回它 ...

  5. Python 编译器与解释器

    Python 编译器与解释器 Python的环境我们已经搭建好了,可以开始学习基础知识了.但是,在此之前,还要先说说编译器与解释器相关的内容. 如果这部分内容,让你觉得难以理解或不能完全明白,可以暂时 ...

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

    原作:Archie Mistry 翻译:豌豆花下猫@Python猫 原文:https://morioh.com/p/765b19f066a4 Python 是一门对初学者友好的编程语言,是一种多用途的 ...

  7. python设计模式之解释器模式

    python设计模式之解释器模式 对每个应用来说,至少有以下两种不同的用户分类. [ ] 基本用户:这类用户只希望能够凭直觉使用应用.他们不喜欢花太多时间配置或学习应用的内部.对他们来说,基本的用法就 ...

  8. 基于python编写的天气抓取程序

    以前一直使用中国天气网的天气预报组件都挺好,可是自从他们升级组件后数据加载变得非常不稳定,因为JS的阻塞常常导致网站打开速度很慢.为了解决这个问题决定现学现用python编写一个抓取程序,每天定时抓取 ...

  9. 用Python编写博客导出工具

    用Python编写博客导出工具 罗朝辉 (http://kesalin.github.io/) CC 许可,转载请注明出处   写在前面的话 我在 github 上用 octopress 搭建了个人博 ...

随机推荐

  1. debian和ubuntu的sh dash bash

    Ubuntu和debian 的 shell 默认安装的是 dash,而不是 bash.运行以下命令查看 sh 的详细信息,确认 shell 对应的程序是哪个:$ls -al /bin/sh dash ...

  2. Effective Java 65 Don't ignore exceptions

    Principle An empty catch block defeats the purpose of exceptions, which is to force you to handle ex ...

  3. JavaScript Patterns 4.7 Init-Time Branching

    When you know that a certain condition will not change throughout the life of the program, it makes ...

  4. netty-socketio使用namespace

    一.简介 netty-socketio中的namespace可以用于区别在相同连接地址下的不同用户,当两个不同的用户打开同一个页面的时候,可以使用namespace用来标记不同用户.例如我们可以在用户 ...

  5. Oracle 11g 中恢复管理器RMAN介绍

    这是我平时摘录的笔记,从管理艺术那本书上摘录出来的,放到这里 RMAN 可在数据库服务器的帮助下从数据库内备份数据文件,可构造数据文件映像副本.控制文件和控制文件映像.对当日志 SPFILE 和RMA ...

  6. js获取页面传过来的参数

    //接收页面传过来的值 //RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i" ...

  7. 给Apache增加SSI支持(shtml的奥秘)

    什么是SSI? SSI是英文Server Side Includes的缩写,翻译成中文就是服务器端包含的意思.从技术角度上说,SSI就是在HTML文件中,可以通过注释行调用的命令或指针.SSI具有强大 ...

  8. mysql日志类型

    在MySQL中共有4中日志:错误日志.二进制日志.查询日志和慢查询日志 一.错误日志 错误日志名 host_name.err,并默认在参数DATADIR指定的目录中写入日志文件.可使用 --log-e ...

  9. matlab中subplot函数的功能

    转载自http://wenku.baidu.com/link?url=UkbSbQd3cxpT7sFrDw7_BO8zJDCUvPKrmsrbITk-7n7fP8g0Vhvq3QTC0DrwwrXfa ...

  10. 矩阵乘法快速幂 codevs 1574 广义斐波那契数列

    codevs 1574 广义斐波那契数列  时间限制: 1 s  空间限制: 256000 KB  题目等级 : 钻石 Diamond   题目描述 Description 广义的斐波那契数列是指形如 ...