前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或AST这样的中间结构,低级解释器执行执行字节码(更接近机器指令的形式)。
高级解释器比较适合DSL,而不适合通用编程语言;一般来说,DSL更需要简单、廉价的实现,不是很在乎执行效率;这个笔记只学习高级解释器,下面的文字如果提到解释器就是指“高级”解释器。为了简单起见,下面的讨论假定目标DSL是动态类型的。
解释器有两种模式:
- Pattern 24,Syntax-Directed Interpreter,通过Parser来触发解释器执行;
- Pattern 25,Tree-Based Interpreter,解释器通过遍历AST树来执行操作。
解释器模拟了一个计算机,“计算机”应该包含CPU,代码存储,数据存储,栈。CPU从代码存储里面读取指令,并解释执行;指令可能读写数据和栈;函数调用需要记住返回地址以便继续执行。实现一个解释器需要考虑3个核心问题:
- 如何存储数据;
- 如何追踪符号;
- 如何执行指令
设计解释器存储系统
解释器通过变量名字来存储对应的值,而不是内存地址;因此我们通过一个name:value的字典来代表内存区域;有三种内存空间需要我们考虑,全局内存,函数栈,数据聚合实例(struct或对象);一个内存空间实际对应着一个作用域。
解释器有一个全局空间,多个函数内存空间;每个函数调用创建一个新的内存空间来存储局部变量和参数变量;解释器通过一个栈来管理函数内存空间。
看下面的C++代码:
int x = 1;
void g(int x) { int z = 2; }
void f(int x) { int y = 1; g(2*x); }
int main() { f(3); }
在执行z=2
时,内存空间类似下图:
同样,解释器也可能存在多个数据聚合内存空间,看下面的C++代码:
struct A { int x; };
int main() {
A *a = new A(); a->x = 1;
A *b = new A(); b->x = 2;
}
对应的内存空间类似下图:
main函数的内存空间里面有两个变量a、b,分别指向一个struct内存空间,a->x=1这样的操作类似a.put('x',1)。对class的实例来说,最简单的方式是将所有的字段打包到一个内存空间里面,“继承”在这里是“include”的含义。
追踪符号
对于语言里面的一个变量x引用,解释器必须要判断它属于哪个内存空间,才能执行读写操作。解释器通过解析符号x,询问它的作用域;作用域告诉解释器变量属于何种内存空间:全局、函数、数据聚合,这样解释器就能挑选合适的内存空间字典。如果变量是一个全局变量,那么解释器从全局空间加载它;如果变量是一个函数局部变量,那么解释器从栈顶的函数内存空间;如果变量是一个字段,那么解释器先加载this变量,再从this指向的内存空间加载字段值。
符号表管理也发生在运行时,因此解析变量和加载变量很容易被混淆;前者主要依据程序的静态结构,判定符号所属的作用域;后者是纯粹的运行时行为,绑定符号和值,同一个函数局部变量,在运行时可能指向不同的内存值。在运行时解析一个变量是很昂贵的操作,因此有些语言强制符号自身要指出自己的作用域,比如Ruby,$x指示x是一个全局变量,@x指示x是一个对象字段。
动态类型的语言在使用变量之前不会提前声明,因此定义一个严格的符号表没有什么意义;但是符号表管理仍然是需要的,因为至少我们需要知道语言是否在访问一个未定义的变量。在下面的python函数中:
def fun(x):
x = 5;
y = 6;
如果没有符号表,那么在运行时解释器无法分别x=5是在操作参数变量,y=6创建了一个新的局部变量。
而C++&java这些静态类型的语言则需要完整的scope tree。
Pattern 24, Syntax- Directed Interpreter
该模式直接执行源码,不会将源码转换成某种中间形式或其他语言;实现上实际就是对Parser的功能进行增强,直接插入指定执行代码;适合简单的语言,代码是有声明和指令语句序列。
这种解释器模拟了人手动理解并计算代码结果的过程,我们逐句读代码,解析&验证&执行指令;只包含两个关键部分:
- Source Code Parser,解析语句并直接触发操作;
- Interpretor,保持状态,并为语句指令提供内部实现方法;可能包含一个code内存空间,和一个global内存空间。
Parser在解析到某个语句模式的时候,直接触发相应的操作,因此语言的语法描述类似"match this, call that"这样的模式;当Parser遇到一个赋值表达式,就会期望intepretor提供assgin()和store()方法来完成操作。
assignment : ID '=' expr {interp.assign($ID, $expr.value);} ;
expr returns [Object value] : ... ; // compute and return value
这里以一个SQL子集为例来构建解释器,SQL是一个用来执行数据库操的DSL,这个子集允许这样的语句:
create table users (primary key name, passwd);
insert into users set name='parrt', passwd='foobar';
insert into users set name='tombu', passwd='spork';
p = select passwd, name from users; // reverse column order print p;
对应的语法定义如下:
table
: 'create' 'table' tbl=ID
'(' 'primary' 'key' key=ID (',' columns+=ID)+ ')' ';'
{interp.createTable($tbl.text, $key.text, $columns);}; //create table sql 调用Interpreter的createTable方法
assign : ID '=' expr ';' {interp.store($ID.text, $expr.value);} ; //assign语句调用Interpreter的store方法来存储变量
// Match a simple value or do a query
expr returns [Object value] //表达式语法不仅匹配对应的表达式,而且对表达式进行求值
: ID {$value = interp.load($ID.text);}
| INT {$value = $INT.int;}
| STRING {$value = $STRING.text;}
| query {$value = $query.value;}
;
解释器构建了两个存储空间,一个用来存储内存全局变量,一个用来存储数据表:
class Interpreter {
Map<String, Object> globals = new HashMap<String, Object>();
Map<String, Table> tables = new HashMap<String, Table>();
}
Pattern 25, Tree- Based Interpreter
该模式基于AST来执行指令,通过AST访问器来驱动解释器,支持前向应用等复杂特性;由于预先构建了AST和scope Tree,因此可以进行一些预处理,比如AST改写,将某些x引用改写为this.x的形式;执行速度上也要快一些。
该模式解释器类比编译程序的话,相当于保留编译器前端,将后端替换成一个执行器;这一节通过构建一种类Python的动态类型语言Pie来讲述解释器。
Pie语言概述
Pie程序包含一系列的函数定义、struct定义和语句;赋值语句左侧的变量如果尚未定义,那么则定义一个,但是其他方式访问一个未定义的变量会产生一个错误;Pie有return,if,while这些控制结构;支持字符、字符串、整形这些子面量;支持操作符==, <, +, -, *, new,还有.(成员访问);struct可以定义在globle作用域也可以定义函数作用域。
x= 1 # define global variable x
def f(y): # define f in the global space
x=2 # set global variable x
y=3 # set parameter y
z=4 # create local variable z
. # end of statement list
f(5) # call f with parameter y = 5
struct Point {x, y} # define a struct symbol in global scope
p = new Point # create a new Point instance; store in global var
p.x = 1 # set the fields of p
p.y = 2
该模式预先定义好了函数、struct,通过传统的scope tree来解析符号。将符号表构建与程序执行分开极大地简化了解释器,解释器只负责内存空间;当然,在执行的过程中仍然需要scope信息来解析符号。
为了能够从parse阶段传递scope信息给解释执行阶段,我们可以标记AST节点;函数调用f()创建了一个(CALL f)子树,此时Parser将CALL节点的scope字段设置为当前Scope。在执行的时候,call指令基于那个scope来解析名字f。
解释器相当于Pattern13介绍的External Tree Visitor,当遇到=、if、CALL根节点的时候,dispatcher方法触发assign( ), ifstat( )和call( )这样的执行方法。”执行方法“有一个AST节点作为参数,需要访问该节点的子节点。
解释器实现
首先定义好语法规则Pie.g,构建好scope tree和AST;然后为解释器编写内部方法来实现Pie的各种指令和操作,解释器提供一个接口exec()给AST Visitor。
Pie的AST节点类型都继承自PieAST,PieAST有scope字段,是从一般的AST节点类型CommonTree继承的。
Pie的scope tree符合Pattern 18(Symbol Table for Data Aggregates),包含了若干symbol类型:VariableSymbol、FunctionSymbol、StructSymbol。
我们看一下解释器的核心成员变量:
public class Interpreter {
GlobalScope globalScope; // global作用域,scope tree的root node
MemorySpace globals = new MemorySpace("globals"); // global内存空间
MemorySpace currentSpace = globals; //当前内存空间
Stack<FunctionSpace> stack = new Stack<FunctionSpace>();// 函数内存空间组成调用栈
PieAST root; // AST根节点,代表了code内存空间
TokenRewriteStream tokens;
PieLexer lex; // 词法分析器
PieParser parser; //语法解析器
//这是visitor的dispatcher方法,要来执行AST各种subtree对应的操作
public Object exec(PieAST t) {
switch ( t.getType() ) {
case PieParser.BLOCK : block(t); break;
case PieParser.ASSIGN : assign(t); break;
case PieParser.RETURN : ret(t); break;
case PieParser.PRINT : print(t); break;
case PieParser.IF : ifstat(t); break;
case PieParser.CALL : return call(t);
case PieParser.NEW : return instance(t);
case PieParser.ADD : return add(t);
case PieParser.INT : return Integer.parseInt(t.getText()); case PieParser.DOT : return load(t);
case PieParser.ID : return load(t);
...
default : «error» // catch unhandled node types
}
}
//唯一的参数是AST subtree root节点
//执行一个AST subtree,意味着执行对应的指令,并访问子树的节点;
public void assign(PieAST t) {
PieAST lhs = (PieAST)t.getChild(0); //左侧操作数
PieAST expr = (PieAST)t.getChild(1); //右侧表达式
Object value = exec(expr); //递归调用exec来对表达式求值
if ( lhs.getType()==PieParser.DOT ) { //左侧额操作数是ID.memeber的形式,struct字段
fieldassign(lhs, value); // field ^('=' ^('.' a x) expr)
return;
}
// var assign ^('=' a expr)
MemorySpace space = getSpaceWithSymbol(lhs.getText());
if ( space==null ) space = currentSpace; //如果变量不存在,则在当前内存空间创建它
space.put(lhs.getText(), value); // store
}
//获取符号对应的内存空间
public MemorySpace getSpaceWithSymbol(String id) {
if (stack.size()>0 && stack.peek().get(id)!=null) { //先在栈空间里找
return stack.peek();
}
if ( globals.get(id)!=null ) return globals; //再在全局空间里面找
return null; // nowhere
}
//给struct的字段赋值,与对global空间或函数空间的变量赋值并没有本质的不同
//区别在于,会在scope tree里面去寻找这个字段名,如果发现没有,会产生一个错误
public void fieldassign(PieAST lhs, Object value) {
PieAST o = (PieAST) lhs.getChild(0);
PieAST f = (PieAST) lhs.getChild(1);
String fieldname = f.getText();
Object a = load(o);
StructInstance struct = (StructInstance)a;
if ( struct.def.resolveMember(fieldname) == null ) {
listener.error("can't assign; "+struct.name+" has no "+fieldname+
" field", f.token);
return;
}
struct.put(fieldname, value);
}
//函数分几个步骤,创建内存空间,构建参数变量,执行函数体,返回
public Object call(PieAST t) {
String fname = t.getChild(0).getText();
FunctionSymbol fs = (FunctionSymbol)t.scope.resolve(fname);
if ( fs==null ) {
listener.error("no such function "+fname, t.token);
return null;
}
FunctionSpace fspace = new FunctionSpace(fs); //创建一个新的函数内存空间
MemorySpace saveSpace = currentSpace;
currentSpace = fspace;
int i = 0; //在函数内存空间做好参数名字和参数值的映射
for (Symbol argS : fs.formalArgs.values()) {
VariableSymbol arg = (VariableSymbol)argS;
PieAST ithArg = (PieAST)t.getChild(i+1);
Object argValue = exec(ithArg);
fspace.put(arg.name, argValue);
i++;
}
Object result = null;
stack.push(fspace);
try {
exec(fs.blockAST); // 执行函数体,return语句通过一个异常来提前返回
}
catch (ReturnValue rv) {
result = rv.value;
} // trap return value
stack.pop();
currentSpace = saveSpace;
return result;
}
//return指令,通过抛出异常来结束函数体执行
public void ret(PieAST t) {
sharedReturnValue.value = exec((PieAST)t.getChild(0));
throw sharedReturnValue;
}
由于scope tree和AST是提前构建好的,所以Pie支持前向引用,下面的代码可以正常执行:
print f(4) # references definition on next line
def f(x) return 2*x
print new User # references definition on next line
struct User { name, password }