《Language Implementation Patterns》之 解释器

前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或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 }
posted on 2014-10-07 22:09  longhuihu  阅读(526)  评论(0编辑  收藏  举报