zoukankan      html  css  js  c++  java
  • 《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 }
    
  • 相关阅读:
    win10 访问远程文件夹 此共享需要过时的SMB1协议 你不能访问此共享文件夹
    Navicat 1142 SELECT command denied to user 'sx'@'xxx' for table 'user'
    MySQL 密码参数配置与修改 validate_password
    MySQL 命令行下更好的显示查询结果
    MySQL 数据库的存储结构
    MySQL实验 内连接优化order by+limit 以及添加索引再次改进
    MySQL实验 子查询优化双参数limit
    MySQL 索引结构 hash 有序数组
    MySQL 树形索引结构 B树 B+树
    hbase2.1.9 centos7 完全分布式 搭建随记
  • 原文地址:https://www.cnblogs.com/longhuihu/p/4008553.html
Copyright © 2011-2022 走看看