lua-源码带读-03-代码生成与虚拟机
Lua 5.4 源码带读 第3篇:代码生成与虚拟机
本篇目标
这是 Lua 源码阅读的核心章节。理解字节码如何生成、指令格式是什么、83条操作码如何分类、虚拟机如何执行字节码。本篇将深入到每一条指令的执行逻辑。
前置知识
- 了解汇编语言的基本概念
- 了解寄存器和栈的区别
- 阅读过第1、2篇
1. 代码生成器:lcode.c
1.1 代码生成在解析过程中进行
Lua 的独特设计:lparser.c 在解析语法树的每个节点时,直接调用 lcode.c 生成对应的字节码。没有中间的 AST 表示。
1.2 关键函数
| 函数 | 作用 |
|---|---|
luaK_code(fs, i) |
发射一条指令,返回指令索引 |
luaK_codek(fs, reg, k) |
发射加载常量指令(自动处理大常量) |
luaK_exp2val(e) |
将表达式求值结果放到栈顶 |
luaK_dischargevars(e) |
将变量转为栈上位置 |
luaK_exp2RK(e) |
将表达式放入寄存器或常量槽 |
luaK_exp2reg(e, reg) |
将表达式结果放入指定寄存器 |
luaK_poswalk(e) |
处理条件表达式的跳转链 |
luaK_patchlist() |
跳转指令的回填 |
luaK_patchtohere() |
回填到当前位置 |
1.3 发射指令
// lcode.c
static int luaK_code(FuncState *fs, Instruction i) {
Proto *f = fs->f;
// 检查指令数是否超限
if (fs->pc == MAX_INT)
luaX_syntaxerror(fs->ls, "code too long");
// 确保有足够空间
if (fs->pc == f->sizecode)
luaM_growvector(fs->ls->L, f->code, fs->pc, &f->sizecode, MAX_INT);
// 写入指令
f->code[fs->pc] = i;
// 保存行号信息
luaK_line(fs);
fs->pc++;
return fs->pc - 1; // 返回指令索引
}
1.4 常量折叠
// lcode.c
static int luaK_constfolding(FuncState *fs, int op, expdesc *e1, expdesc *e2) {
// 如果两个操作数都是常量,编译期直接计算
TValue v1, v2;
if (!tovalue(e1, &v1) || !tovalue(e2, &v2))
return 0; // 不是常量,不能折叠
// 根据操作码计算结果
switch (op) {
case OP_ADD: res = luaO_intarith(L, LUA_OPADD, i1, i2); break;
case OP_SUB: res = luaO_intarith(L, LUA_OPSUB, i1, i2); break;
case OP_MUL: res = luaO_intarith(L, LUA_OPMUL, i1, i2); break;
// ... 其他操作
}
// 设置e1的结果
e1->u.ival = res;
return 1; // 折叠成功
}
1.5 跳转回填
// lcode.c
static void luaK_patchlist(FuncState *fs, int list, int target) {
while (list != NO_JUMP) {
int next = getjumpcontrol(fs, list);
fixjump(fs, list, target);
list = next;
}
}
static void fixjump(FuncState *fs, int pc, int dest) {
Instruction *jmp = &fs->f->code[pc];
int offset = dest - (pc + 1); // 相对偏移
// 写入偏移到JMP指令的sJ字段
SETARG_sJ(*jmp, offset);
}
2. 指令格式(lopcodes.h)
2.1 32位指令布局
每条指令固定32位,5种格式:
3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
iABC C(8) | B(8) |k| A(8) | Op(7) |
iABx Bx(17) | A(8) | Op(7) |
iAsBx sBx (signed)(17) | A(8) | Op(7) |
iAx Ax(25) | Op(7) |
isJ sJ (signed)(25) | Op(7) |
2.2 各字段含义
| 字段 | 位数 | 含义 |
|---|---|---|
| Op | 7 | 操作码(0-127,最多128条指令) |
| A | 8 | 目标寄存器索引(0-255) |
| B | 8 | 源寄存器/常量索引(0-255) |
| C | 8 | 源寄存器/常量索引(0-255) |
| k | 1 | B标志位(0=寄存器,1=常量) |
| Bx | 17 | 无符号大立即数(0-131071) |
| sBx | 17 | 有符号大立即数(-65536 到 65535) |
| Ax | 25 | 超大立即数(0-33554431) |
| sJ | 25 | 有符号跳转偏移 |
2.3 k标志位
// 当 k=0 时:R[B] 和 R[C] 是寄存器索引
// 当 k=1 时:K[B] 和 K[C] 是常量表索引
// 这使得一条指令可以混合寄存器和常量操作数
// 例如:a = b + 1
// OP_ADDI R[a] R[b] sC
// 如果是 a = b + c(常量):
// OP_ADDK R[a] R[b] K[c]
2.4 有符号数编码
sBx 使用 excess-K 编码:表示的值 = 写入的无符号值 - K/2。这样可以用无符号字段表示有符号数。
3. 操作码完整分类(83条)
3.1 加载/存储(17条)
| 操作码 | 格式 | 作用 | 使用场景 |
|---|---|---|---|
| OP_MOVE | iABC | R[A] := R[B] | 局部变量复制、函数参数传递 |
| OP_LOADI | iAsBx | R[A] := sBx | 加载整数常量 |
| OP_LOADF | iAsBx | R[A] := (lua_Number)sBx | 加载浮点常量 |
| OP_LOADK | iABx | R[A] := K[Bx] | 加载常量表中的值 |
| OP_LOADKX | iABx | R[A] := K[extra arg] | 加载大常量(后续EXTRAARG) |
| OP_LOADFALSE | iABC | R[A] := false | 加载false |
| OP_LFALSESKIP | iABC | R[A] := false; pc++ | 条件转布尔 |
| OP_LOADTRUE | iABC | R[A] := true | 加载true |
| OP_LOADNIL | iABC | R[A], R[A+1], ..., R[A+B] := nil | 加载多个nil |
| OP_GETUPVAL | iABC | R[A] := UpValue[B] | 获取上值 |
| OP_SETUPVAL | iABC | UpValue[B] := R[A] | 设置上值 |
| OP_GETTABUP | iABC | R[A] := UpValue[B][K[C]:shortstring] | 从全局表获取字段 |
| OP_GETTABLE | iABC | R[A] := R[B][R[C]] | 从表中获取元素 |
| OP_GETI | iABC | R[A] := R[B][C] | 从表中获取整数键 |
| OP_GETFIELD | iABC | R[A] := R[B][K[C]:shortstring] | 从表中获取字符串键 |
| OP_SETTABUP | iABC | UpValue[A][K[B]:shortstring] := RK(C) | 向全局表设置 |
| OP_SETTABLE | iABC | R[A][R[B]] := RK(C) | 向表中设置 |
| OP_SETI | iABC | R[A][B] := RK(C) | 向表中设置整数键 |
| OP_SETFIELD | iABC | R[A][K[B]:shortstring] := RK(C) | 向表中设置字符串键 |
3.2 表操作(3条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_NEWTABLE | iABC | R[A] := {}(创建空表) |
| OP_SELF | iABC | R[A+1] := R[B]; R[A] := R[B][RK(C):string](方法调用) |
| OP_SETLIST | iABC | R[A][C+i] := R[A+i], 1 <= i <= B(批量设置表字段) |
3.3 算术运算(16条)
| 操作码 | 格式 | 作用 | 操作数来源 |
|---|---|---|---|
| OP_ADDI | iABC | R[A] := R[B] + sC | 寄存器+立即数 |
| OP_ADDK | iABC | R[A] := R[B] + K[C]:number | 寄存器+常量 |
| OP_SUBK | iABC | R[A] := R[B] - K[C]:number | 寄存器-常量 |
| OP_MULK | iABC | R[A] := R[B] * K[C]:number | 寄存器*常量 |
| OP_MODK | iABC | R[A] := R[B] % K[C]:number | 寄存器%常量 |
| OP_POWK | iABC | R[A] := R[B] ^ K[C]:number | 寄存器^常量 |
| OP_DIVK | iABC | R[A] := R[B] / K[C]:number | 寄存器/常量 |
| OP_IDIVK | iABC | R[A] := R[B] // K[C]:number | 寄存器//常量 |
| OP_BANDK | iABC | R[A] := R[B] & K[C]:integer | 寄存器&常量 |
| OP_BORK | iABC | R[A] := R[B] | K[C]:integer | 寄存器|常量 |
| OP_BXORK | iABC | R[A] := R[B] ~ K[C]:integer | 寄存器~常量 |
| OP_SHRI | iABC | R[A] := R[B] >> sC | 移位 |
| OP_SHLI | iABC | R[A] := sC << R[B] | 移位 |
| OP_ADD | iABC | R[A] := R[B] + R[C] | 寄存器+寄存器 |
| OP_SUB | iABC | R[A] := R[B] - R[C] | 寄存器-寄存器 |
| OP_MUL | iABC | R[A] := R[B] * R[C] | 寄存器*寄存器 |
| OP_MOD | iABC | R[A] := R[B] % R[C] | 寄存器%寄存器 |
| OP_POW | iABC | R[A] := R[B] ^ R[C] | 寄存器^寄存器 |
| OP_DIV | iABC | R[A] := R[B] / R[C] | 寄存器/寄存器 |
| OP_IDIV | iABC | R[A] := R[B] // R[C] | 寄存器//寄存器 |
| OP_BAND | iABC | R[A] := R[B] & R[C] | 寄存器&寄存器 |
| OP_BOR | iABC | R[A] := R[B] | R[C] | 寄存器|寄存器 |
| OP_BXOR | iABC | R[A] := R[B] ~ R[C] | 寄存器~寄存器 |
| OP_SHL | iABC | R[A] := R[B] << R[C] | 移位 |
| OP_SHR | iABC | R[A] := R[B] >> R[C] | 移位 |
5.4新增的优化指令:OP_ADDI、OP_ADDK 等。将常见的常量操作从"加载常量到寄存器 → 运算"优化为"运算时直接使用常量",减少了一条指令。
3.4 元方法调用(4条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_MMBIN | iABC | 调用R[A]和R[B]的二元元方法(运算失败后调用) |
| OP_MMBINI | iABC | 调用R[A]和sB的二元元方法 |
| OP_MMBINK | iABC | 调用R[A]和K[B]的二元元方法 |
| OP_UNM | iABC | R[A] := -R[B] |
| OP_BNOT | iABC | R[A] := ~R[B] |
| OP_NOT | iABC | R[A] := not R[B] |
| OP_LEN | iABC | R[A] := #R[B](长度运算符) |
3.5 连接操作(1条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_CONCAT | iABC | R[A] := R[A].. ... ..R[A + B - 1] |
3.6 函数调用/返回(5条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_CALL | iABC | R[A], ..., R[A+C-2] := R[A](R[A+1], ..., R[A+B-1]) |
| OP_TAILCALL | iABC | return R[A](R[A+1], ..., R[A+B-1]) |
| OP_RETURN | iABC | return R[A], ..., R[A+B-2] |
| OP_RETURN0 | iABC | return |
| OP_RETURN1 | iABC | return R[A] |
OP_CALL 语义:
- B=0:参数从R[A+1]到栈顶(动态参数数量)
- C=0:返回值不确定,设置top为最后结果+1
3.7 测试/跳转(11条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_EQ | iABC | if ((R[A] == R[B]) ~= k) then pc++ |
| OP_LT | iABC | if ((R[A] < R[B]) ~= k) then pc++ |
| OP_LE | iABC | if ((R[A] <= R[B]) ~= k) then pc++ |
| OP_EQK | iABC | if ((R[A] == K[B]) ~= k) then pc++ |
| OP_EQI | iABC | if ((R[A] == sB) ~= k) then pc++ |
| OP_LTI | iABC | if ((R[A] < sB) ~= k) then pc++ |
| OP_LEI | iABC | if ((R[A] <= sB) ~= k) then pc++ |
| OP_GTI | iABC | if ((R[A] > sB) ~= k) then pc++ |
| OP_GEI | iABC | if ((R[A] >= sB) ~= k) then pc++ |
| OP_TEST | iABC | if (not R[A] == k) then pc++ |
| OP_TESTSET | iABC | if (not R[B] == k) then pc++ else R[A] := R[B] |
| OP_JMP | isJ | pc += sJ |
比较指令的 k 标志:k 指定了比较条件的真值。例如 a == b 生成 OP_EQ R[a] R[b] 1,而 a ~= b 生成 OP_EQ R[a] R[b] 0。
3.8 循环指令(5条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_FORLOOP | iABx | 更新计数器,如果循环继续则 pc-=Bx |
| OP_FORPREP | iABx | 检查值并准备计数器,如果不需要运行则 pc+=Bx+1 |
| OP_TFORPREP | iABx | 为泛型for创建上值 |
| OP_TFORCALL | iABC | R[A+4], ..., R[A+3+C] := R[A](R[A+1], R[A+2]) |
| OP_TFORLOOP | iABx | 如果 R[A+2] ~= nil 则 |
3.9 闭包/变量参数(3条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_CLOSURE | iABx | R[A] := closure(KPROTO[Bx])(创建闭包) |
| OP_VARARG | iABC | R[A], R[A+1], ..., R[A+C-2] = vararg |
| OP_VARARGPREP | iABC | 调整可变参数 |
3.10 其他(3条)
| 操作码 | 格式 | 作用 |
|---|---|---|
| OP_CLOSE | iABC | 关闭所有 >= R[A] 的上值 |
| OP_TBC | iABC | 标记变量A为"to be closed"(5.4新增:可选的to-be-closed变量) |
| OP_EXTRAARG | iAx | 前一条指令的额外参数 |
4. 虚拟机:lvm.c
4.1 寄存器式虚拟机
Lua 5.4 使用寄存器式VM:寄存器 = 值栈上的槽位。每个函数帧的寄存器从 ci->u.l.base + 1 开始。指令通过索引引用寄存器。
// lvm.c
void luaV_execute(lua_State *L) {
CallInfo *ci = L->ci;
StkId base = ci->u.l.base; // 当前函数的基址
Instruction *pc = ci->u.l.savedpc; // 程序计数器
for (;;) {
const Instruction i = *pc++;
OpCode op = GET_OPCODE(i);
const TValue *rb, *rc;
// 获取操作数(寄存器位置)
StkId ra = RKB(i); // 目标寄存器
// 根据操作码执行
switch (op) {
case OP_MOVE: {
setobjs2s(L, ra, RKB(i));
vmbreak;
}
case OP_LOADI: {
setivalue(ra, GETARG_sBx(i));
vmbreak;
}
case OP_LOADK: {
TValue *kb = k + GETARG_Bx(i);
setobjs2s(L, ra, kb);
vmbreak;
}
case OP_ADDI: {
TValue *rb = RB(i);
lua_Integer b;
if (tointeger(rb, &b)) {
setivalue(ra, b + GETARG_sC(i));
// 如果是整数,快速路径
} else {
// 如果是浮点,转为浮点计算
lua_Number nb;
if (tonumber(rb, &nb)) {
setfltvalue(ra, cast_num(b) + cast_num(GETARG_sC(i)));
} else {
// 元方法调用
pc--;
luaT_callTM(L, rb, ...);
}
}
vmbreak;
}
// ... 80+ 条指令
}
}
}
4.2 Dispatch 机制
// ljumptab.h - 跳转表
#define vmbreak goto *disptabs[opcode]
// 或者使用 computed goto(GCC扩展):
#define vmbreak vmcase(op) \
goto *dispatchtable[op];
跳转表实现高效分发:避免每次循环重新计算 switch 目标地址。在现代CPU上,间接跳转可以被分支预测器优化。
4.3 Lua函数调用
// ldo.c
static StkId adjust_varargs(lua_State *L, Proto *p, int actual) {
int nfixargs = p->numparams;
// 将多余参数复制到栈顶
StkId base = L->top.p - actual;
StkId func = L->top.p - (actual + 1);
for (int i = actual; i < nfixargs; i++) {
if (!ttisnil(base + i))
setnilvalue(base + i);
}
// 设置新的基址
return base;
}
int luaD_precall(lua_State *L, StkId func, int nresults) {
StkId base;
Proto *p = clLp(func); // 获取Lua闭包的Proto
// 检查栈空间
if (!luaD_checkstack(L, p->maxstacksize))
return 0;
// 调整参数
if (p->is_vararg)
base = adjust_varargs(L, p, narg);
else
base = func + 1; // 固定参数
// 创建新的CallInfo
CallInfo *ci = next_ci(L);
ci->func = func;
ci->u.l.base = base;
ci->u.l.savedpc = p->code; // 从函数开头开始
ci->nresults = nresults;
ci->callstatus = CIST_LUA;
// 切换到新函数
L->ci = ci;
L->top = base + p->maxstacksize;
return 0;
}
4.4 C函数调用
int luaD_precall(lua_State *L, StkId func, int nresults) {
lua_CFunction func = clCvalue(func)->f;
int n = (*func)(L); // 调用C函数
// 将返回值放到正确位置
luaD_poscall(L, func, n, nresults);
return 1;
}
4.5 返回处理
int luaD_poscall(lua_State *L, StkId func, int nresults, int n) {
StkId base = func + 1;
StkId ret = (nresults == LUA_MULTRET) ? L->top.p : base + nresults - 1;
// 将返回值移动到调用者期望的位置
for (int i = 0; i < n && i < nresults; i++) {
setobjs2s(L, base + i, ret - i);
}
// 恢复调用栈
CallInfo *ci = L->ci;
L->ci = ci->previous; // 弹出当前CallInfo
L->top = ci->func + nresults; // 设置栈顶
return (nresults != LUA_MULTRET && nresults < 0);
}
5. 寄存器式 vs 栈式 VM
| 特性 | 栈式(JVM/CPython) | 寄存器式(Lua) |
|---|---|---|
| 操作数 | 隐式从栈弹出 | 显式指定寄存器索引 |
| 指令数 | 多(需要push/pop) | 少(直接引用) |
| 指令宽度 | 1-2字节 | 4字节(固定) |
| 执行效率 | 较低 | 较高 |
| 编译器复杂度 | 较低 | 较高(需分配寄存器) |
| 代码密度 | 高 | 中等 |
Lua 的优势:寄存器式VM减少了大量临时变量的push/pop操作,更适合嵌入式场景。
6. 特殊指令详解
6.1 OP_MMBIN 的连锁效应
-- Lua 5.4 中 a + b 的字节码
-- 先尝试快速路径:
-- OP_ADD R[a] R[b] R[c]
-- OP_MMBIN 0 0 0 ← 如果快速路径失败,跳到这里调用元方法
-- 实际执行:如果OP_ADD失败(没有快速路径),直接执行OP_MMBIN
-- OP_MMBIN成功后跳过下一条指令(pc++)
6.2 OP_LFALSESKIP
-- 用于将条件表达式转为布尔值:cond and true or false
-- OP_LFALSESKIP R[a] → R[a] = false; pc++ (跳过下一条true)
-- OP_LOADTRUE R[a] → 如果执行到这里,R[a] = true
6.3 OP_TAILCALL
-- 尾调用优化:如果一个函数的最后操作是调用另一个函数,且不需要处理返回值,
-- 可以重用当前栈帧,避免栈增长。
-- OP_TAILCALL R[a] R[b] R[c]
-- → 调用 R[a](R[a+1], ..., R[a+B-1]),结果直接覆盖当前函数的返回位置
7. 本篇小结
| 概念 | 要点 |
|---|---|
| 代码生成 | lcode.c 在解析中直接生成字节码 |
| 指令格式 | 32位固定宽度,5种格式 |
| k标志 | 区分寄存器和常量操作数 |
| 操作码 | 83条:加载/运算/调用/跳转/闭包/循环/元方法 |
| VM模型 | 寄存器式,寄存器=值栈槽位 |
| Dispatch | 跳转表高效分发 |
| 调用链 | precall → 函数帧切换 → poscall |
| 5.4新增 | OP_ADDI等立即数优化、OP_TBC(to-be-closed变量)、分代GC支持 |
| 元方法 | OP_MMBIN 与运算指令配对 |
思考题
- 为什么Lua选择寄存器式VM而非栈式?这在嵌入式场景下有什么优势?
- 常量折叠在什么情况下不适用?(提示:元方法)
- 尾调用(OP_TAILCALL)是如何优化递归的?
- 如果要给Lua添加新的运算符(如幂赋值
a **= b),需要修改哪些源文件? - OP_EQI 和 OP_EQK 有什么区别?什么情况下使用哪条?
- OP_SETLIST 如何处理大规模表构造器
{1,2,3,...,50000}? - OP_TBC 在5.4中引入了什么新特性?它与OP_CLOSE有什么区别?
思考题解答
1. 为什么Lua选择寄存器式VM而非栈式?
寄存器式VM的优势(尤其在嵌入式场景):
| 特性 | 栈式VM | 寄存器式VM |
|---|---|---|
| 指令数 | 多(需要push/pop) | 少30-50% |
| 执行效率 | 较低 | 较高 |
| 指令宽度 | 1-2字节(可变) | 4字节(固定) |
具体例子:a = b + c
栈式VM(多条指令):
LOAD b -- push b
LOAD c -- push c
ADD -- pop b,c; push b+c
STORE a -- pop result; store to a
寄存器式VM(1条指令):
ADD R[a] R[b] R[c]
嵌入式场景优势:更少的指令意味着更少的解码开销、更少的内存访问、更紧凑的字节码占用ROM/RAM。代价是编译器需要更复杂的寄存器分配算法。
2. 常量折叠在什么情况下不适用?
常量折叠的条件:两个操作数都是编译时已知的常量值。
不适用的情况:
- 元方法存在时:如果操作数有元表,
1 + "hello"应该调用__add元方法而非在编译期报错。常量折叠无法检测元表。 - 字符串连接:
"a" .. "b"需要分配新字符串,编译期做可能影响GC。 - 除零:
1 / 0在编译期折叠会导致运行时行为不一致(浮点除零产生inf而非错误)。 - 溢出检查:
math.maxinteger + 1的结果取决于运行时数值类型。 - 自定义操作符:通过元方法重载的运算符,编译器无法在编译期计算。
实际策略:只处理两个整数或两个浮点的纯算术运算。
3. 尾调用如何优化递归?
尾调用:函数的最后一步是调用另一个函数,且不需要处理其返回值。
普通调用:
栈帧1: f(x) -> 调用g -> 栈帧2: g(x) -> 返回 -> 栈帧2销毁
尾调用优化:
栈帧1: f(x) -> 调用g -> 栈帧2: g(x)
栈帧2直接复用栈帧1的空间,不需要额外栈空间
递归函数如果使用尾递归,栈空间不会增长,可以处理无限递归而不溢出。
4. 新增运算符需要修改哪些文件?
假设新增幂赋值 a **= b:
| 文件 | 修改内容 |
|---|---|
llex.h |
定义新token TK_POW_ASSIGN |
llex.c |
识别 **= 字符序列 |
lparser.h |
可能需要新的赋值类型枚举 |
lparser.c |
在赋值解析中处理 **= |
lcode.c |
生成 OP_POW + 赋值的字节码序列 |
lopcodes.h |
如果需要OP_POWA指令则添加(通常不需要) |
5. OP_EQI 和 OP_EQK 的区别
| 操作码 | B操作数来源 | C操作数来源 | 使用场景 |
|---|---|---|---|
| OP_EQI | 立即数sB(有符号) | 标志位(是否浮点) | a == 1、a == 0等小整数 |
| OP_EQK | 常量表K[B] | 标志位 | a == "hello"、a == 3.14等大值 |
选择依据:立即数用OP_EQI(8位有符号,范围-128~127),超出范围或非整数用OP_EQK。
6. OP_SETLIST 如何处理大规模表构造器?
{1, 2, 3, ..., 50000}
机制:
- 编译器每累积
LFIELDS_PER_FLUSH(50)个元素后生成一条OP_SETLIST指令 - 最后一次调用可能处理剩余元素
- 如果元素超过C字段的8位容量(255),使用
EXTRAARG指令扩展
OP_SETLIST R[a] 50 1 -- 设置 R[a][1..50] = R[a+1..R[a+50]]
OP_SETLIST R[a] 50 2 -- 设置 R[a][51..100] = R[a+51..R[a+100]]
...
OP_SETLIST R[a] 50 1000 -- 设置 R[a][4951..5000] = ...
优化:避免了为每个元素生成单独的SETTABLE指令,大幅减少指令数。
7. OP_TBC 与 OP_CLOSE 的区别
| 指令 | 含义 | 用途 |
|---|---|---|
| OP_TBC | 标记变量为"to-be-closed" | 在变量作用域结束时自动关闭上值 |
| OP_CLOSE | 关闭所有>=R[A]的上值 | 显式关闭(如块结束、错误处理) |
OP_TBC 是5.4新增,实现了 __close 元方法(类似C++的RAII/defer):
local fd <close> = io.open("file.txt")
-- 当fd离开作用域时,自动调用fd:close()
-- 即使发生错误也会关闭
OP_CLOSE 是传统的上值关闭机制,在块结束或错误传播时调用。OP_TBC 允许用户自定义关闭行为(通过 __close 元方法),而 OP_CLOSE 只是简单地复制值并关闭上值。

浙公网安备 33010602011771号