运行时环境
AST或字节码的解释过程只是在代码过程层面,不足以成为一个完整的运行,因为程序计算是需要数据和存储空间的,光有代码跑不起来,需要运行时环境,至少要有数据,实际情况中还需要一些其他信息。为讨论方便,在解释器中将运行时环境抽象为前述的env对象,通过一些接口来实现存取,这里先只讨论单执行序列,不考虑并发
env在前面的分析中总共就涉及了三个接口,get,set和set_exception(当然还应该有get_exception),也就是说env里面只保存了数据和异常信息,如果考虑到异常机制对一个语言并非必要(比如C),则env只是保存数据即可
首先是常量数据在env中,一般来说,常量在程序启动时加载到env,然后不变,直到程序结束。确切说并非所有代码中出现的常量都在env,有两种可能的情况:
一,某些类型的常量以立即数的形式存在于代码中,具体说一般就是一些简单的基础类型,比如整数:
a = a + 1
编译为:
[plain] view plaincopy
load a
load_immed 1
add
store a
这里我们认为整数是一个立即数,但某些语言如python,整数是作为一个对象存在的,还是设计为在const表里面:
load a
load_const IDX_OF_INT_1
add
store a
这里IDX_OF_INT_1是整数1在const表的索引,运行时要进行一次查表操作
二,语义分析阶段做了常量静态计算,比如:
a = (1, 2)
并不会被编译成:
[plain] view plaincopy
load 1
load 2
build_tuple 2 //参数指定两个元素
store a
而是将(1,2)作为一个常量:
load_const IDX_OF_TUPLE_xxx //(1,2)编译后的const索引
store a
对于第二点也有个需要说明的地方,一般来说大多数可静态计算的常量都在编译期可以算好,输出成目标代码或者执行,但某些情况下这种做法会造成空间浪费,比如python代码:
a = "a" * 1000000
赋值右边的表达式的行为是确定的,就是一个包含一百万个字母a的字符串,但是如果在编译期就算好,那目标代码pyc文件就差不多有1M,这样加载速度是很慢的,需要做一个权衡,这种情况可以在编译期不展开,在程序启动的时候预先算好,相对于执行期也是静态行为
常量之外,变量一般也分两种,全局和局部,其中全局变量没什么好说的,一个大表即可,局部变量可以通过栈来实现,每个栈元素是一个变量表,对应当前执行函数的局部空间,每次有函数调用的时候进行压栈,函数返回后弹出。不过实际实现中,没必要将所有函数调用栈帧在env中管理,根据前述call_func指令的行为,是利用宿主语言的函数调用来实现源语言的函数调用,因此每次调用时在函数中建立自己的local_env即可
除全局和局部外,可能有人还会提到类属性(静态属性和实例属性),其实类静态属性跟全局变量没啥区别,就是套了个类名的名字空间罢了,而实例属性并不是变量,是通过实例本身来访问,java和C++在方法代码中直接访问实例属性,其实是省略了this,而this本身可以看做一个T *const类型的局部变量,即不可改变的指针,python做得更直接些,强制要求写self
env中数据的set接口,一般来说是行为确定的,因为set的时候需要指明是全局还是局部的名字空间,而get在某些动态语言中可能就不指定,比如执行load a的时候,先在局部作用域找,找不到去全局作用域,显然这是比较慢的,所以像之前说过的,python在这里就牺牲了这点动态性,局部和全局变量区分开,但也有例外的情况,比如:
def f(s):
exec s
print a
假如没有那个exec语句,则a必定是一个全局变量,因为f中没有给它赋值,这个编译期就能确定。但如果引入exec,由于s的内容可能是"a=xxx"之类,因此对于f来说,a是全局还是局部就无法确定了,这时候python就会将对a的访问编译成LOAD_NAME字节码,表示根据名字进行上述的由内而外的查找,会降低速度,如果没有exec这种不确定的动态性质语句,则可以直接编译成LOAD_FAST(局部变量)或LOAD_GLOBAL(全局变量),运行速度就快了
如果做了异常机制,则也可以保存在env中,前面讲的虚拟机中实现异常机制还是比较简单的,就一个set_exception,要做的好点的需要一个traceback机制,不但指出哪个地方异常,还需要给出调用树,最好给出每次调用的代码的位置(文件and行数),这样env就需要维护一个异常栈,方便维护,具体实现很简单就不说了