lua词法语法分析

来源:互联网 发布:2016原创歌词网络投稿 编辑:程序博客网 时间:2024/06/17 14:35
lua是我工作中的第一语言,因而工作的大部分时间,都在敲着lua代码。虽然它语法是否简单
好学,但它来做工程的人,都不免要抱怨作者的一些设定理念。 据说作者是一个学院派的人(从他的
sample代码中就能看出这点),很少也不会去考虑做工程的人的需求。 因而,留给我们这些使用lua
的人不少痛苦。
    大伙不爽的事情如下:
    1. 默认的变量声明是全局的, 局部需要 local 关键字。
    原因: 正常的一段代码, local的变量怎么也比全局的多, 如果反过来, 想python一样,
    默认是local , global 来定义全局能省下不少要敲的代码量。并且避免不小心敲错一个
    单词导致多一个global变量,而又无法察觉。

    2. 没有continue....
        - -,说不好其实也没啥,毕竟绕一下还是能实现,但作为一门成熟的编程语言,应该
    不至于为了省点语法分析代码而不实现continue吧
    
    3. hash 和 array不区分
        这应该说是lua的特点, 只要把table用得正确也不会有太大问题。 但对于初学者
    每次都必须花时间去了解table.getn之类的语义。 而且在我们项目里,因为很多table
    不能区分到底是当array还是hash用,每次求table的大小时都必须一项一项去数。而且这
    似乎还成为了编程的惯例。因而,我的设想是如果table里面能多一项来跟着table元素的
    个数。将省不少麻烦。

说是这样说,但是如果真正去改虚拟机,也是个不少的工作量。而最麻烦的是,如果项目真的使用
自己改虚拟机,那么将会对以后使用第三方库带来很多麻烦。所以这一直只是个想法而没人去尝试。
    
不过既然都想到这点,就不能不了解一下lua虚拟机的词法和语法分析过程。 前阵子花的点时间去
阅读这部分源码。在此大家分享一些笔记。

    lua源文件中与编译相关的主要有四个lopcodes(字节码),llex(词法分析),lparser(语法分析),lcode(目标代码生成).
lopcodes里定义了指令的格式,寄存器和常数的表示等.这里记录下一些用语的细节,方便下文.相信大家都比较熟悉.  
--[[
一个指令由32位的值,对于有三种模式。
iABC,iABx 和 iAsBx, i是6位的操作码,A为8位, BC为9位, Bx 和 sBx为18位。
A参数通常用作为需要赋值的寄存器, sBx一般用于做跳转量。

然后是常量与寄存器的索引。lua通常使用255个寄存器,第n个寄存器一般表示为R(n),
而常量从256开始编号。 常量与寄存器一起成为RK值,RK(x)如果x小于256则为R(x)
否则,为K(x-256),即第x-256+1个常量。RK值经常作为指令的参数。

pc指下一个指令的位置
--]]

----------------------------------------------------------------------------------------------------------------
词法分析

    词法分析过程比较简单,主要的处理函数是int llex(LexState *ls, SemInfo *seminfo)
它接受一个LexState结构,通过其中的buff读取字节,返回一个token和将语义信息填充到SemInfo中(字符串和数值)。
要注意的地方有两个。

    一是保留字的处理。lua在初始化时调用luaX_init,建立起所有保留字的字符串对象(同时让它永远不回收,调用luaS_fix)。
并且设定其reserved值为该保留字的枚举值。因为lua中相同的字符串只有一份,所以在llex中遇到reserved字段非空的
字符串即为保留字,否则为TK_NAME。

    二是多行注释和字符串。即当读取到'--[' 或 '['的时候,要处理长字符串的情况。通过skip_sep 和 read_long_string处理两段‘=’
数量的匹配。
----------------------------------------------------------------------------------------------------------------

语法分析
    
    语法分析是编译过程的重点,其外部接口是Proto *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, const char *name)
它从字节流z读取符合, 建议LexState做词法分析, 同时按照语法生成规则展开, 最后调用到lcode里的接口生成各条指令记录到函数体Proto.

    最上层的语法规则都比较容易理解, 如 chunk , statment, block等. 执行根据生成规则一层层打开即可.
需要注意的是以下的内容:

----------------------------------------------------------------------------------------------------------------
1.table的构造过程

表构造的处理函数是void constructor (LexState *ls, expdesc *t)
它先生存一条OP_NEWTABLE指令, 然后根据语法, 按','(或';')切分每项, 根据格式调用listfield 或者 recfield
recfield 生成的是 记录项, 也就是 x = y 或者 [x] = y 的形式, 它用分别用expr求出key 和 value 表单式值,
然后生成一条OP_SETTABLE指令.

这里想说的是数组项的处理方式, 它并没有对每一项产生一个OP_SETTABLE指令, 而是缓存起来再一次性处理的.
其实listfield的只是对之listfield里面仅仅是对value 进行expr调用. 然后记录待处理的项数量+1 (cc->tostore++)
在每次遇到项处理前, 都调用closelistfield, closelistfield 发现当待处理的项达到FPF时(默认50个)
生成一个OP_SETLIST指令, 批量对他们赋值.
OP_SETLIST指令的格式为 OP_SETLIST
意外把 寄存器A中的table批量赋值, 数量为B个, 目标值所指寄存器范围是 [C-1]*FPF+1 ~ [C-1]*FPF+B

因而, 以下语句
local a = { 1; 2; 3; [5] = 'a', 4, 5, [7] = 'b', 6, 7, 8}

的编译结果将是
           [01] newtable       ; array=8, hash=2
           [02] loadk             ; 1
           [03] loadk             ; 2
           [04] loadk             ; 3
           [05] settable    259 260  ; 5 "a"
           [06] loadk             ; 4
           [07] loadk             ; 5
           [08] settable    262 263  ; 7 "b"
           [09] loadk             ; 6
           [10] loadk             ; 7
           [11] loadk             ; 8
           [12] setlist        ; index 1 to 8
           [13] return            


----------------------------------------------------------------------------------


2. 函数创建upvalue的过程.

lua通过upvalue的结构来存取函数外层的局部变量.
为函数创建upvalue是在变量的查找过程中进行, 即编译期间确定某个变量名字指称全局,局部还是闭包的upvalue的过程。
因而每个函数有多少个upvalue, 分别指称那个外层局部变量,都是在编译期确定的. 与之相反的是, 全局变量时再运行时
通过名字来检索的.

完成这一功能的函数是singlevar(LexState *ls, expdesc *var)
该函数在LexState中读取一个名字,然后在ls的 FuncState 环境中查找该名字,把结果
赋值给表达式描述 var。
其大致调用流程如下

singlevar -> singlevaraux(fs, ...) -> searchvar
                             -> markupval    
                 -> singlevaraux(fs->prev, ...)
                 -> indexupvalue

singlevaraux 递归地沿着FuncState的层次查找, 如果到达最底层都没找到,则返回VGBLOBAL, var视为全局.
在每一层查找中,先调用serchvar对fs->f的局部变量查找, 如果成功了当层返回VLOCAL, 否则调用singlevaraux(fs->prev, ...)
进行递归查找. 在递归返回时, 递归的结果为VGBLOBAL则,返回VGBOBAL, 否则都返回VUPVAL.
所以函数外层的局部变量都以upvalue的方式索引.

upvalue结构体存放在FuncState的upvalues[]中, 其保存的是对栈中局部变量的索引.同时FuncState的f(Proto)的upvalues中保存其
名字.
当一个外层局部变量被内层存取是,singlevaraux调用markupval标记该局部变量被用作upvalue了,以保证其过了作用域后仍不被回收.
实际生成upvalue的是在函数indexupvalue中, 它先遍历fs->upvalues,如果已经有该名字的upvalue则它的位置,
否则把局部变量的信息添加到新的upvalue结构中. 并且 f->nups ++

-----------------------------------------------------------------------------------------------

3. 表达式的语法分析过程

处理表达式的入口函数是, void expr (LexState *ls, expdesc *v), 但它是递归函数subexpr的包装.
subexpr按照优先级把表达式按语法一层一层地展开, 其需要传递一个当层优先度最小限制(limit).
处理表达式的原子操作是 simpleexp. 对应的代码生成最终回调用lcode里面的luaK_prefix, luaK_infex 和 luaK_posfix操作.
由于需要比较左右表达式的优先级, lua在 priority[]结构中保存了每个Op的左右优先级.
然后在 在subexpr中, 依靠以下代码展开表达式
  ...
  simpleexp(ls, v)
  op = getbinopr(ls->t.token);
  while (op != OPR_NOBINOPR && priority[op].left > limit) {
    expdesc v2;
    BinOpr nextop;
    ...
    nextop = subexpr(ls, &v2, priority[op].right);
    luaK_posfix(ls->fs, op, v, &v2);
    op = nextop;
  }

例如对于 1 + 2 * 4 这表达式
首先 最外层的expr调用subexpr, limit = 0
    在simpleexp 取出 1, v的值为1
    op 是 + , 左优先度是6 > limit, 进入while循环
    然后调用下一层 subexpr , limit = 6 (+ 的右优先度)
        在simpleexp 取出 2, v2 值为2
        op 是 * , 优先度是 7 > limit, 进入while循环
        然后调用下一层 subexpr , limit = 7 (* 的右优先度)
            在simpleexp 是 4 , v2 值为 4
            op 是 OPR_NOINOPR, 直接返回
        luaK_posfix(*,2,4) //  2 * 4
        op = nextop = OPR_NOINOPR, 退出while
    v2 传回来是 8    , nextop = OPR_NOINOPR
    luaK_posfix(+,1,8) //  1 + 8
    op = nextop = OPR_NOINOPR, 退出while
    返回 OPR_NOINOPR
    
这里有一点值得注意的优化, 在lcode中产生表达式时, 如果是数学运算并且都是常数, 则在编译期间把结果计算下来了.
具体操作发生在lcode 的 contfolding里面.

因而以下代码的编译结果是
local a = 1
local b = 1 + 2 * 4 - a / 5

           [1] loadk             ; 1  
           [2] div          257  ; 5  
           [3] sub         258 1    ; 9  
           [4] return             

1 + 2 * 4的结果在编译器被计算出来,保存在RK(258)里面了.
-----------------------------------------------------------------------------------------------

最后是条件跳转

在expdesc结构中 保存了
当该表达式退出时, 返回true or false 分别需要补全的条件列表
typedef struct expdesc {
  ...
  int t; 
  int f; 
} expdesc;

例如 if 条件的语法分析如下(for循环的跳转实现与之类似):

static void ifstat (LexState *ls, int line) {
  ...
  int flist;
  int escapelist = NO_JUMP;
  flist = test_then_block(ls); 
  while (ls->t.token == TK_ELSEIF) {
    luaK_concat(fs, &escapelist, luaK_jump(fs));
    luaK_patchtohere(fs, flist);
    flist = test_then_block(ls); 
  }
  if (ls->t.token == TK_ELSE) {
    luaK_concat(fs, &escapelist, luaK_jump(fs));
    luaK_patchtohere(fs, flist);
    luaX_next(ls); 
    block(ls); 
  }
  else
    luaK_concat(fs, &escapelist, flist);
  luaK_patchtohere(fs, escapelist);
  ...
}

这里必须注意的是两个变量 flist, escapelist 和两个操作, luaK_concat, luaK_patchtohere.
flist 是上一次TEST结果为false后的JMP指令位置, escapelist是TEST结果为ture,执行完操作后跳出条件流程的JMP指令位置.
由于产生该JUP时, 他们的偏移量都还没确定下来,因为把他们称为未决跳转(pending jump).
后续要进行的操作就是把他们所需的偏移量参数(sBx)补全回来. 完成这一操作的是lcode里的dischargejpc.

这里注意到跳转到条件语句的JMP其实是有多个的, 而把新的JMP放入JMP列表里的操作便是上述的luaK_concat 和 luaK_patchtohere.
luaK_concat 定义是 luaK_concat(FuncState *fs, int *l1, int l2)
其作用是把JMP l2,放到列表l1当中, 并且传回l1. 但注意到其实l1,也只是一个int值. 留意luaK_concat的实现
其实这个JMP列表,一直都是能保持到第一个JMP的索引, 而当加入新的JMP时, 其调用fixjump(fs, list, l2)把列表
中最后一个JMP的偏移临时设为新的JMP,于是只要根据每个JMP的偏移,就能把整个JMP列表找回来.

luaK_patchtohere仅仅是把当前pc记录到fs->lasttarget后再调用luaK_concat.

补全未决跳转参数的调用是dischargetjpc, 实际调用patchlistaux后, 把fs->jpc(未决的跳转表)设为NO_JUMP.
而patchlistaux的操作便是沿着刚才提到的临时串起来的JMP列表, 把最终的偏离量设定回跳转参数里面.

但patchlistaux的实现较为复杂,需要判断跳转的形式(无条件跳转和条件跳转).
其原因在于分支指令(OP_TEST)实现的方式有关。因为lua的指令是32位的,并且首6位是操作码,因而剩下的指令参数
只能在26位里完成。而一般操作OP_TEST操作需要3个参数,两个对比的变量,和一个调整的偏移量。即使用9位的参数(B或C参数)
做偏移量,也只能实现前后256指令范围内的跳转。这样对于一些较长的条件语句将不能实现。

因而lua把分支跳转拆成两条指令实现,首先是OP_TEST A C,(如果 R(A) ~= R(B),则pc ++ ),后面跟一条无条件跳转(OP_JMP)。
由于一个逻辑的指令拆成两个,代码逻辑上带来了复杂度。

这点是由于lua的虚拟机使用基于寄存器的方式实现有关,其指令较为紧凑,参数一般就在指令中。

最后附一段条件语句的编译结果
注意到TEST, EQ, LE指令后都跟上一个JMP

local a
local b
if a  then
    b = "first"
elseif a == 2 then
    b = "second"
elseif a >= 3 then
    b = "third"
else
    b = "four"
end

         [01] test              ; to [3] if true
         [02] jmp                  ; to [5]        
         [03] loadk             ; "first"       
         [04] jmp                  ; to [14]       
         [05] eq           257  ; 2, to [7] if true
         [06] jmp                  ; to [9]        
         [07] loadk             ; "second"      
         [08] jmp                  ; to [14]       
         [09] le          259 0    ; 3, to [11] if true
         [10] jmp                  ; to [13]       
         [11] loadk             ; "third"       
         [12] jmp                  ; to [14]       
         [13] loadk             ; "four"        
         [14] return                       

原创粉丝点击