• lua-设计与实现-6


    记背约定:

    • StkId是栈元素的索引(比如代表RA(i):寄存器的索引) - typedef TValue StkId; / index to stack elements */
    • cast类型转换

    6.1 Lua词法

    • 一般,对一门语言进行解析是两遍遍历的过程
      第一遍解析源代码并生成AST ( Abstract Syntax Tree ,抽象语法树),第二遍再将AST翻译为对应的字节码

    • Lua的EBNF语法,小写字母表示非终结符,大写字母表示终结符,

    在上面的EBNF词法中,需要注意以下几点。

    • {}大括号包住的部分表示可选,比如在一个if表达式if stat 中, else部分不是必须存在的。
    • []中括号包住的部分,表示会有0 次或多次出现,比如一个返回表达式rets tat 中,return 关键字后面的表达式列表就是这样的。
    • 大写字母表示一个终结符。所谓终结符,就是不能继续用子表达式表示它的符号,如上面的STRING表示字符串, NUMBER表示数字,等等。
    --eg:
    ifstat -> IF cond THEN block {ELSEIF cond THEN block} [ELSE block] END
    

    “执行单元”既可以是一个Lua文件,也可以是一个函数体,这是body表达式的EBNF 中也出现chunk 的原因。

    6.2.1 局部变量

    例子讲解:

    1语句

    local a = 10

    2词法分析-EBNF词法

    3代码实现

    如chunk:(更多看源码)

    static void chunk (LexState *ls) {
      /* chunk -> { stat [`;'] } */
      int islast = 0;
      enterlevel(ls);
      while (!islast && !block_follow(ls->t.token)) {
        islast = statement(ls);
        testnext(ls, ';');
        lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg &&
                   ls->fs->freereg >= ls->fs->nactvar);
        // 调整freereg
        ls->fs->freereg = ls->fs->nactvar;  /* free registers */
      }
      leavelevel(ls);
    }
    
    • =号左边是一个变量,只有变量才能赋值,于是涉及如下问题:如何存储局部变革,如何查找变量,怎么确定一个变量是局部变量、全局变量还是Up Value ?
    • =号右边是一个表达式列表explist1 ,在这个最简单的例子中,这个表达式是常量数字10 。这种情况很简单,但是如果不是一个立即能得到的值,比如是一个函数调用的返回
      值,或者是对这个block之外的其他变量的引用,又怎么处理呢?
    static void simpleexp (LexState *ls, expdesc *v) {
      /* simpleexp -> NUMBER | STRING | NIL | true | false | ... |
                      constructor | FUNCTION body | primaryexp */
      switch (ls->t.token) {
        case TK_NUMBER: {
          init_exp(v, VKNUM, 0);
          v->u.nval = ls->t.seminfo.r;//
          break;
          ...
        }
      }
    }
    

    解析(做了两个事情):
    1使用类型VKNUM初始化expdesc结构体,这个类型表示数字常量。
    2将具体的数据(也就是这里的10 )赋值给expdesc 结构体中的nval (在VKNUM 这个类型下就是用来存储数字)。

    指令:move 1 0
    图示:如下

    6.2.2 全局变量

    上面的例子改为:
    a = 10
    local b = a

    • 首先生成了OP GETGLOBAL 指令,用于获取全局变量的值到当前函数的寄存器中;
    • 将类型修改为VRELOCABLE (即类型为重定向),注意在上面的代码中, OP GETGLOBAL 指令的A参数目前为0 ,因为这个参数保存的是获取这个全局变孟之后需要存放到的寄存器地址,而此时并不知道。
    // 1 表达式 - list
    static int explist1 (LexState *ls, expdesc *v) {
      /* explist1 -> expr { `,' expr } */
      while (testnext(ls, ',')) {
        ...
        luaK_exp2nextreg(ls->fs, v);
        ...
      }
      return n;
    }
    
    //2 
    // 讲表达式dump到当前栈的下一个位置中
    void luaK_exp2nextreg (FuncState *fs, expdesc *e) {
      // 首先如果是一个变量的话 现在根据变量类型(upval, GLOBAL, LOCAL)dump出来
      luaK_dischargevars(fs, e);
       ...
      // 将表达式dump到这个新分配的栈空间中
      exp2reg(fs, e, fs->freereg - 1);
    }
    
    
    //3 全局变量需要重定向 (卸载vars-根据不同类型)
    void luaK_dischargevars (FuncState *fs, expdesc *e) {
     case VGLOBAL: {
          // 全局变量需要重定向,因为需要赋值到本函数栈中
          e->u.s.info = luaK_codeABx(fs, OP_GETGLOBAL, 0, e->u.s.info);
          e->k = VRELOCABLE;
          break;
        }
    }
    
    //4 根据表达式的e->k载入寄存器(将表达式dump到这个新分配的栈空间中)
    static void discharge2reg (FuncState *fs, expdesc *e, int reg) {
    
        case VRELOCABLE: {
          // 重定位, 因为赋值的时候不知道当前可用的寄存器指针,所以这里要重新定位下
          Instruction *pc = &getcode(fs, e);
          SETARG_A(*pc, reg);
          break;
        }
        case VNONRELOC: {
          // 不需要重定位的指令,说明已经在当前栈中 如果两者不相等, 那么要使用MOVE指令
          if (reg != e->u.s.info)
            luaK_codeABC(fs, OP_MOVE, reg, e->u.s.info, 0);
           ...
    }
    

    6.3 表相关的操作指令

    解析表达式时,会谈到数据结构ex pdesc ,用于存储解析后的表达式的信息。而在解析表的信息时,会存放在ConsControl结构体中,它包括如下几个字段。
    口expdesc v : 存储表构造过程中最后一个表达式的信息。
    口expdesc -t :构造表相关的表达式信息,与上一个字段的区别在于这里使用的是指针,因为这个字段是由外部传入的。
    口int nh :初始化表时,散列部分数据的数量。
    口int na :初始化表时,数组部分数据的数量。

    6.4 函数相关的操作指令

    • 独立的函数环境,函数体内部定义的局部变量都局限在这个环境。
    • 函数的参数,也被认为是这个函数体内的局部变量。
    • 函数调用前,要保护好调用者的环境,而在函数调用完毕之后,要准确恢复调用者之前的环境,并且如果存在返回值的话,也要正确处理。
    • 函数是第一类( first class )的数据类型。

    6.4.5 UpValue与闭包

    概念:

    • 函数是第一类类型值。
    • 内嵌函数可以访问外包函数的所有局部变量(这特性称词法作用域
    • UpValue: 以上这局部变量。(或称为内嵌函数的外部局部变量)
    • 闭包:内嵌函数加上它所需要的访问的UpValue。

    源码

    typedef struct LClosure {
      ClosureHeader;
      struct Proto *p;
      UpVal *upvals[1];
    } LClosure;
    

    表查找函数 (元表的原理)

    // 在一个table中查找key对应的值, 找到存放到val中
    void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) {
      int loop;
      // 函数外层以MAXTAGLOOP做为计数,防止死循环
      for (loop = 0; loop < MAXTAGLOOP; loop++) {
        const TValue *tm;
        if (ttistable(t)) {  /* `t' is a table? */
          Table *h = hvalue(t);
          // 首先尝试在表中查找这个key
          const TValue *res = luaH_get(h, key); /* do a primitive get */      
          if (!ttisnil(res) ||  /* result is no nil? */ // 如果结果不是nil
              (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */ // 在结果为nil的时候如果metable为nil
            setobj2s(L, val, res);
            return;
          }
          /* else will try the tag method */
        }
        // 来查这个表的meta表, 如果不存在则报错
        else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX)))
          luaG_typeerror(L, t, "index");
        // 如果是一个函数则直接调用这个函数
        if (ttisfunction(tm)) {
          callTMres(L, val, tm, t, key);
          return;
        }
        // 否则继续找
        t = tm;  /* else repeat with `tm' */
      }
      luaG_runerror(L, "loop in gettable");
    }
    

    6.4.3 函数的调用与返回值的处理

    • 保存当前虚拟机的savedpc,在函数调用前后, lua_State的savedpc成员就会跟着当前函数的Callinfo的savedpc进行切换。
    • 准备该调用函数的初始枝空间,其中包括初始化函数的base 地址,它一定是在函数地址的下一个位置,紧跟着的是留出函数参数的地址,最后设置函数拢的top地址。函数调用时,内部的局部变量数量最终不能超出这个地址。 (如第5章图所示)
    • 最后将Lua虚拟机的savedpc成员切换到该函数Proto结构体的code 成员上。前面的分析提到过,一个函数分析完毕之后,会有一个对应Proto 结构体存放解析完毕之后的指令,这些指令就是存放在code 数组中,此时切换到这里,结合前面就可以知道, Lua 虚拟机将执行环境切换到这个函数了,开始进行函数的调用。
    ####调用函数完毕之后的恢复工作在函数luaD_poscall 中进行
    口将虚拟机的base 地址和saved pc 恢复到上一个调用环境Call Info保存下来的值。
    口根据函数需要返回的值将这些值写入函数战中相应的地址,返回参数不足的用nil 补全。
    口最后更新虚拟机的top 地址。
    
    ####源码略看
    // 结束完一次函数调用(无论是C还是lua函数)的处理, firstResult是函数第一个返回值的地址
    int luaD_poscall (lua_State *L, StkId firstResult) {
      StkId res;
      int wanted, i;
      CallInfo *ci;
      if (L->hookmask & LUA_MASKRET)
        firstResult = callrethooks(L, firstResult);
      // 得到当时的CallInfo指针
      ci = L->ci--;
      res = ci->func;  /* res == final position of 1st result */
      // 本来需要有多少返回值
      wanted = ci->nresults;
      // 把base和savepc指针置回调用前的位置
      L->base = (ci - 1)->base;  /* restore base */
      L->savedpc = (ci - 1)->savedpc;  /* restore savedpc */
      /* move results to correct place */
      // 返回值压入栈中
      for (i = wanted; i != 0 && firstResult < L->top; i--)
        setobjs2s(L, res++, firstResult++);
      // 剩余的返回值置nil
      while (i-- > 0)
        setnilvalue(res++);
      // 可以将top指针置回调用之前的位置了
      L->top = res;
      return (wanted - LUA_MULTRET);  /* 0 if wanted == LUA_MULTRET */
    }
    

    调用函数时,需要先做如下准备工作:

    口函数也是一种变量,因此需要先定位这个函数在哪里,然后加载到寄存器中才可以在后面调用。
    口准备好函数的参数,按照前面分析的那样,将函数参数加载到函数栈空间中。


    表6-7 OP CALL 指令的参数及其说明

    • 参数A
      被调用函数的地址
    • 参数B
      函数参数的数量,有两种情况: 1 )为0表示参数从A+l 的位置一直到函数梭的top位坐,这种情况下用于处理在函数参数中有另外的函数调用时,因为在调用时并不知道有多少参数,所以只好告诉编译器该函数的参数从A+ l 的位置一直到函数栈的top位置; 2 )大于0时,表示两数参数的数量为B-1
      参数C
      函数返回值的数量,也有两种情况: I )为0 时,表示有可变数量的值返回; 2 )大于l 时,表示返回值数量为C-1

    总结起来,调用函数的过程大体是这样的。
    (1 )调用函数之前,在函数luaD_precall 中,准备好调用函数的函数校和函数参数,将虚拟机的环境切换到被调用函数的环境,同时将待返回变量的数量存储在CallInfo结构体的nresults成员中。
    (2)调用函数之后,在函数luaD_poscall 中,恢复调用者的函数环境,同时根据被调用函数的返回参数数量和前面存下来的nresults成员的值,来决定返回值的多少,如果不够,就使用nil补全。

    UpVal和闭包

    本质上:
    闭包==函数+UpVal数组 (另一种说法:闭包是一种特殊的函数)

    typedef struct LClosure {
      ClosureHeader;
      struct Proto *p;
      UpVal *upvals[1];
    } LClosure;
    

    lua_execute 对闭包的处理如下:
    根据引用到的外部变量是同层的局部变量或者是更上层的变量,来决定使用move还是getupval指令来得到这个外部变量

    void luaV_execute (lua_State *L, int nexeccalls) {
      LClosure *cl;
      StkId base;
      TValue *k;
      const Instruction *pc;
      ...
      pc = L->savedpc;
      cl = &clvalue(L->ci->func)->l;
      base = L->base;
      k = cl->p->k;
      for (;;) {
        case OP_CLOSURE: {
            Proto *p;
            Closure *ncl;
            int nup, j;
            // Bx中存放的是在上层函数proto数组中的索引
            p = cl->p->p[GETARG_Bx(i)];
            nup = p->nups;
            ncl = luaF_newLclosure(L, nup, cl->env);
            ncl->l.p = p;
            // 紧跟着CLOSURE指令的是MOVE或者GETUPVAL指令
            // 如果是GETUPVAL, 则从上层函数的upval中寻找upval
            // 如果是MOVE, 则从
            // 特别注意这里pc指针每次循环都递增了
            for (j=0; j<nup; j++, pc++) {
              if (GET_OPCODE(*pc) == OP_GETUPVAL)
                ncl->l.upvals[j] = cl->upvals[GETARG_B(*pc)];
              else {
                lua_assert(GET_OPCODE(*pc) == OP_MOVE);
                ncl->l.upvals[j] = luaF_findupval(L, base + GETARG_B(*pc));
              }
            }
            // 将创建好的closure存放到ra中
            setclvalue(L, ra, ncl);
          ...
    }
    

    函数结束时做了哪些操作--函数luaF close

    口函数在创建的时候,会根据该函数中用到的UpValue 查找,如果在openupval这个存放所有处于open 状态的UpValue链表中没有找到将要引用的UpValue ,就新创建一个UpValue 出来井将其放在openupval链表上。注意,此时Up Value 中存放的值是指针引用。
    口函数在结束之后,会释放open upval 链表上所有在该函数的Up Value ,此时如果发现这个变革被内嵌函数作为UpValue 引用了,那么找到这个Up Value 并将值赋值过去,同时切换指针指向自己的数据域,这样即使在这个函数中关闭了所有UpValue ,处于close 状态时,被引用到的UpValue还能正确使用。

    function test1()
     local a = 1
     function test2()
       local b = 100
       function test3()
        print(a)
        print(b)
       end
     end
     return test2
    end
    local fun = testl ()
    fun()
    ``
    ![](https://img2020.cnblogs.com/blog/1565924/202007/1565924-20200729111024297-824498774.png)
    
    --------------
    #总结
    ##1 EBNF语法
    ##2
  • 相关阅读:
    (视频)Erich Gamma 与 Visual Studio Online 的一点野史
    三维重建技术概述
    三维重建基础
    用户故事驱动的敏捷开发 – 2. 创建backlog
    用户故事驱动的敏捷开发 – 1. 规划篇
    TFS 10周年生日快乐 – TFS与布莱恩大叔的故事
    【DevOps敏捷开发动手实验】开源文档 v2015.2 stable 版发布
    看见的力量 – (II) 影响地图
    看见的力量 – (I) 解题的思维
    UDAD 用户故事驱动的敏捷开发 – 演讲实录
  • 原文地址:https://www.cnblogs.com/Jaysonhome/p/13307758.html
Copyright © 2020-2023  润新知