学习 lua 源代码(一)

学习 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 使用的方法到底
为什么还是不清楚. 虽然能总体上理解.

  希望以后有时间学习更深入些.
 

你可能感兴趣的:(lua,代码生成,编译原理)