函数(过程)是程序中重要的抽象, 过程调用一般用栈实现. Lua 1.1 中尚未实现闭包(closure),
对于函数使用栈实现即已满足需求了. 在理论上, 在栈中要保存为实现调用以及返回调用处的足够
信息, 这些信息当前是返回地址(return-address,栈基址指针(base-pointer).
在虚拟机指令层次, 指令 CALLFUNC, RETCODE 用于函数调用的核心实现. 另有一些与调用参数, 局部
变量相关的指令, 稍后遇到的时候研究.
对于一个函数, 如例子 function f(a,b) ... end, 对其的调用代码为:
PUSH f -- 将函数对象 f 压入堆栈.
PUSH MARK --- 将特殊标记值 MARK 压入堆栈, 作用下面描述.
PUSH a --- 计算得到 a 的代码, 最终为将 a 的值压入堆栈.
PUSH b --- 同 a 的情况, 此时栈中数据为: [f, MARK, a, b )
CALLFUNC --- 实际产生调用.
指令 CALLFUNC 的执行如下:
case CALLFUNC: --- 位于 opcode.c 的虚拟机执行函数 lua_execute() 中.
1. 从栈顶向上找 MARK 标记, 这一标记的前面是函数 f, 后面是第一个函数参数 a.
2. 从函数对象 f 中获得该函数的代码地址 new-pc;
把当前 pc (也即返回地址)保存在函数对象 f 中. (注1)
设置 pc = new-pc, 此即实现执行地址的改变.
3. 将栈 base 指针保存在 MARK 的值中, 设置新的 base = MARK+1, 即指向栈中第一个
参数 a 的位置.
此后再执行的下一条指令就是函数的入口指令了.
上述的步骤, 所做工作就是保存 <返回地址, 基址指针>, 设置新的运行地址和基址指针. 与机器码实现
函数调用几乎是一致的. 略有不同的是由于调用者提供的参数数量可能是0个或多个(可变数量的), 所以
通过查找 MARK 方式找到, 同时 MARK 还用作保存 base 指针, 这种方式较为巧妙.
注1: 将返回地址保存到函数对象中, 这是一个"不好的"方式, 这样函数就不能重入,递归了?
指令 RETCODE 的执行与 CALLFUNC 想对应:
case RETCODE: --- 虚拟机函数 lua_execute() 中.
1. 由于当前 base 指针-1, -2 分别是保存了 base 的MARK, 和保存了返回地址的函数 f 对象,
故此从中恢复 pc, base 的值.
2. 复制/移动函数返回值 (函数的多返回值机制以后分析, 此处暂略)
此后下一条指令即返回到原调用处的下一条指令位置.
对函数产生调用的代码, 在如下产生式中生成(代码生成):
1. stat1 -> functioncall
2. expr -> functioncall
3. functioncall -> functionvalue {代码块1} '(' exprlist ')' {代码块2}
对上面的产生式3略作记录:
1. functionvalue 产生计算函数 f 的值的代码, 例如压入全局变量 f 到堆栈中. PUSHGLOBAL f
2. 代码块1: 产生代码 PUSHMARK, 更新 ntemp 值(其用于跟踪堆栈使用量)
3. exprlist 产生所有参数的计算和压栈代码, 如 PUSH a, PUSH b
4. 代码块2: 产生代码 CALLFUNC, 更新 ntemp 值.
这里生成了调用函数 f 的前述代码序列.
在前面提到过基址指针 base, 相当于 80x86 体系中的寄存器 BP, 用于寻址位于堆栈上的地址.
在 Lua 中, 函数的参数和局部变量存放在栈中, 并使用 base 指针寻址. 下面研究对局部变量访问的
指令, 及其代码生成.
在 Lua 中, 读取局部变量的指令为 PUSHLOCAL(及其同系列的 PUSHLOCAL0~~9),
写入局部变量的指令为 STORELOCAL(及其同系列的 STORELOCAL0~~9). 那些同系列的指令仅仅
是减少(优化)了指令大小, 语义是一致的, 因此只需要研究 PUSHLOCAL, STORELOCAL 即可.
case PUSHLOCAL: --- 带一个字节的指令立即数 i, 局部变量压入栈中.
*top++ = stack[base + i];
case STORELOCAL: --- 带一个字节的指令立即数 i, 栈顶值弹出存入局部变量.
stack[base + i] = *(--top);
PUSHLOCAL, STORELOCAL 都带有一个字节的指令参数, 表示所访问的局部变量的索引 i, 从 0 开始,
寻址到堆栈 stack[base + i] 位置. 由于只有一个字节, 也即限制最多只能有 256 个局部变量.
由于调用函数时 base 被自动维护, 因此每函数都有自己的局部变量. 又 base 指针从第一个参数开始,
因此参数实现上也是被当做局部变量看待的. 这隐含的几个问题, 第一个是如果函数所需参数数量,
和实际调用者传递的参数数量不一致的问题.
举例说明, 设函数声明为 function f(a,b), 而调用者调用为 f(1,2,3), 或 f(4), 即函数参数多或少的情况.
在函数的入口代码中, 会产生一条 ADJUST n 指令, 其中 n 是函数声明时的参数数量, 该指令执行如下:
case ADJUST:
在语义上:
1. 如果调用者提供的参数不足 n 个, 则不足的部分以 NIL 值填充.
2. 如果调用者提供的参数多于 n 个, 则多出的被裁剪掉.
最终设置栈顶指针 top = base + n (即多出的参数被裁剪, 如果有的话)
这一指令在产生式 function -> FUNCTION NAME '(' parlist ')' block END 中生成.
隐含的第二个问题是, 由于多出的参数被裁剪掉了, 这样表示无法提供 f(args, ...) 后面可变参数语义的实现.
预计 lua 以后的版本会使用某种方法实现.
局部变量的声明, 例如 local x, y, 相关产生式为:
stat1 -> LOCAL localdeclist decinit
localdeclist -> NAME {代码块1} | localdeclist ',' NAME
其中代码块1:
localvar[nlocalvar]=lua_findsymbol($1); --- 查找 $1(即NAME), 并加入到局部变量表中.
$$ = 1; --- 已声明的局部变量数.
在函数中声明的局部变量被放置在 localvar[] 表中, 值是 lua_findsymbol() 的返回值, 即到符号表
symtab 的索引.
在产生式 var -> NAME 中, 第一篇研究全局变量的文章中也有遇见, 其中代码块为:
Word s = lua_findsymbol(NAME) --- 查找 NAME 在符号表中的索引.
int local = lua_localname(s) --- 在 localvar[] 表中查找是否有 s, 如果有则表示这是个局部
--- 变量.
if (是局部变量) $$ = -(local+1) --- 是一个负数, 从而与全局变量的 正的索引 区分开.
在 lua_pushvar() 为访问 var 生成代码时, 前一篇文章研究全局变量时也碰到,
为全局变量生成代码为: PUSHGLOBAL idx-of-symtab
为局部变量生成代码为: PUSHLOCAL idx-of-localvar, 也即表示在 stack[base+idx]
在产生式 varlist1 -> var ... 中, 记录 var.$$ 到 varbuffer[] 表中, 在为其生成写入指令时,
根据是正数生成 全局变量的(STOREGLOBAL), 是负数生成局部变量访问指令 (STORELOCAL).
生成指令的时候, 如果 idx 在 0~9, 则产生较短的指令 PUSHLOCAL0~9, STORELOCAL0~9.
函数的返回值也是放在栈顶的, 是在 RETCODE 指令中设置好返回给调用者的, 由于 lua 支持多赋值,多返回值, 将它们单独放一个地方再研究也许更合适一些.