您的位置:首页 > 编程语言 > Lua

lua词法语法分析

2013-12-04 09:04 113 查看
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 0 8 2 ; array=8, hash=2

[02] loadk 1 0 ; 1

[03] loadk 2 1 ; 2

[04] loadk 3 2 ; 3

[05] settable 0 259 260 ; 5 "a"

[06] loadk 4 5 ; 4

[07] loadk 5 3 ; 5

[08] settable 0 262 263 ; 7 "b"

[09] loadk 6 8 ; 6

[10] loadk 7 6 ; 7

[11] loadk 8 9 ; 8

[12] setlist 0 8 1 ; index 1 to 8

[13] return 0 1

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

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 0 0 ; 1

[2] div 1 0 257 ; 5

[3] sub 1 258 1 ; 9

[4] return 0 1

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 0 0 ; to [3] if true

[02] jmp 2 ; to [5]

[03] loadk 1 0 ; "first"

[04] jmp 9 ; to [14]

[05] eq 0 0 257 ; 2, to [7] if true

[06] jmp 2 ; to [9]

[07] loadk 1 2 ; "second"

[08] jmp 5 ; to [14]

[09] le 0 259 0 ; 3, to [11] if true

[10] jmp 2 ; to [13]

[11] loadk 1 4 ; "third"

[12] jmp 1 ; to [14]

[13] loadk 1 5 ; "four"

[14] return 0 1
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: