学习 lua 源代码(一)
参考云风的博客中关于 lua 的部分:
http://blog.codingnow.com/eo/luaoeeeaeau/
前一段时间重点学习了语法分析(见前面 bison 学习的文章), 这次选择
一个简单的脚本语言 lua 来实践《编译原理》如典型的龙书, 其中重点将放在
代码生成, 虚拟机部分.
语言解析部分暂时先不多描述了, 用测试用例测试代码生成并说明一些概念:
function f()
if x then -- 这是一个简单的 lua 语句块. if elseif else 构成多个分支
a = 1
elseif y then
a = 2
else
a = 3
end
end
以下是生成的 lua 虚拟机的指令代码:
0: get_global x -- x 的值放在栈顶
1: jmp_false ?1 -- 栈顶 x 为假则跳转, 跳转地址待回填.
2: push_int 1
3: set_global a -- a = 1, 1 由上条指令放于栈顶.
4: jmp ?4 -- 无条件跳转到整个 if 块末尾, 地址待回填.
5: get_global y -- y 的值放在栈顶. ?1 跳转到这里(回填)
6: jmp_false ?6 -- 栈顶 y 为假则跳转, 跳转地址待回填.
7: push_int 2
8: set_global a -- 赋值 a = 2
9: jmp ?9 -- 同指令 4 的
10: push_int 3 -- ?6 跳转到这里(回填)
11: set_global a -- 赋值 a = 3
12: op_end -- 此函数结束, ?4, ?9 跳转到这里
解释说明:
lua 在内部解析代码, 生成 lua 的虚拟机指令如上所示. 这里的代码见
lparser.c 中的 ifstat() 函数.
这个测试用例我主要用于研究 if 块的代码生成, 主要是相应的跳转指令
的生成和地址回填技术.
语句 if x then 的实现为, 首先计算/获得 x 的值, 放在栈顶(lua 使用堆栈式
虚拟机), 指令 get_global x 表示获取全局变量 x 的值并入栈(即在栈顶).
下一条指令 jmp_false ?? 用于分支. jmp_false 的作用是, 判断栈顶的
元素值的真假, 如果为真则继续执行下一条语句, 如果为假则跳转到 pc+??
的位置, 以及将栈顶元素弹出(这个例子中是将栈顶的 x 的值弹出栈).
这条 jmp_false 指令的跳转位置应该是 elseif y then 这句 lua 代码
的地址, 因为 if x then 判断不成立时应判断下一个 elseif.
但是此时还不知道 elseif 语句块的位置, 因此 ?1 的值不能确定, 需要回填.
回填是在解析 elseif y then 语句时进行的. 稍后马上描述.
指令 2,3 实现赋值语句 a=1, 具体方法是将右值 1 放在栈顶, 然后用
set_global 指令将栈顶值 1 赋予全局变量 a. 因我重点是研究 if 块
代码生成, 因此这里选择了最简单的赋值语句当做 if 块内的代码. 理解其
大致指令含义即可.
然后是解析 elseif y then 的语句, 此时需要做的事情有:
a.1. 产生一个无条件跳转 jmp 指令, 跳转到整个 if 块的结尾. 因为前面的
if 或 elseif 部分的 代码体执行之后, 要跳过后面的 elseif 或 else
直到 if 块整个结尾. 在 ifstat() 函数中, 这个地址记录在列表
escapelist 中, 最终在 if 整个分析完成之后也会回填.
a.2. 回填指令1 jmp_false 中的回填地址 ?1 了. 要回填的指令列表记录在
v.f (falselist) 中. 使用 lcode.c 中的函数 luaK_patchlist() 函数进行.
这个函数 luaK_patchlist() 就相当于编译原理 P264 页所给出的
backpatch(p, i) 函数: 将 i 作为目标标号插入到 p 所指列表中的各指令中.
a.3. 产生 y 的判断和代码体的指令.
在此例子中, 步骤2中具体为指令1:jmp_false ?1 回填的值为 3. 这个值是这样
计算的: 当前要生成的下一条指令的地址为 5, 当执行指令1的时候,
pc(当前程序指针计数器)值=2, 这是因为取指令1的时候, pc自动+1到了2,
此时与指令5的相对距离 = 5-2 = 3. 因此回填的值为 3.
要注意的是, lua 在回填地址的方法中使用了回填列表 fs.jlt, fs.lasttarget
使得回填有时候到后面某个地方才进行, 实际我还没有彻底弄清楚这两个量
的作用和原因, 但要指出会回填, 但回填的时候可能不一定是这里. 理论上
是在 elseif 的时候回填, 程序实现可能因为某种实现需要, 不太一样.
指令 5,6,7,8 原理是类似的, 不详细说明了. 其中指令 6 中的跳转地址在后面
else 语句分析时回填.
然后是 else 语句, 类似于 elseif 语句, 也是:
b.1. 使前面结束以无条件跳转 jmp 到 if 块结尾. 这里例子中生成指令 9,
其与指令4 共同构成此整个 if 语句的 escapelist. 在最后回填.
b.2. 回填前面 if 或 elseif 的 falselist. 此例子中, 回填指令 6 中的
地址, 计算方法同上面所述.
b.3. 产生 else 代码体的指令.
最后, 整个 if 块解析完毕, 指令 12 op_end (其实是函数结束生成的, 我们
为说明方便, 以其作为 if 块结束的指令)处为前面 escapelist 的跳转地址,
因此回填 escapelist 为这里的地址.
整个 ifstat() 函数伪代码可描述如下:
escapelist = []
读入 if 关键字
生成 cond 和 body 的代码. cond.falselist[] 给出当值为 false 时待回填的指令编号.
如在这个例子中, 这里的 cond.falselist = [1]
while (下一个关键字是 elseif)
i = 生成无条件 jmp 指令的编号, 将 i 加入到 escapelist[] 中.
如这个例子中 escapelist = [4,9]
回填 cond.falselist[] 的指令编号为当前.
生成 elseif 的 cond 和 body 的代码. cond.falselist[] 意义同 if 的.
end while
if (下一个关键字是 else)
i = 生成无条件 jmp 指令的编号, 将 i 加入到 escapelist[] 中.
回填 cond.falselist[] 的指令编号为当前.
生成 else 的 body 代码. (else 没有 cond 了)
else
合并 escapelist 和 cond.falselist[], 因这两者都跳转到 if 块结束.
end
回填 escapelist, 这里是整个 if 块结束位置.
回顾:
仔细学习 lua 的源代码, 并且和书本上的理论联系起来, 可以使更理解
这些理论, 同时也明白为什么代码这么写的原因.
另外, 书上描述的主要是自底向上方式语法解析, 和 lua 中所写的自上
而下的语法分析略有不同, 因此不是简单的一一对应, 还要自己仔细思考.
我终究还是有些不太明白的地方, 例如回填中具体lua 使用的方法到底
为什么还是不清楚. 虽然能总体上理解.
希望以后有时间学习更深入些.