lua-源码带读-02-词法分析与语法解析
Lua 5.4 源码带读 第2篇:词法分析与语法解析
本篇目标
深入理解 Lua 源代码如何被转换为字节码的第一步:从原始文本到 Token 流,再到语法结构。Lua 的解析器有一个独特设计——没有 AST(抽象语法树),解析器在分析语法的同时直接调用代码生成器产生字节码。
前置知识
- 编译原理基础:词法分析、语法分析、BNF 范式
- 了解递归下降解析器的概念
- 阅读过第 1 篇
1. 整体流程
源代码文本
→ [lzio.c] 缓冲输入流(ZIO)
→ [llex.c] 词法分析(lexer)
→ Token 流
→ [lparser.c] 递归下降语法解析
→ [lcode.c] 代码生成器(在解析中直接调用)
→ 字节码(Proto结构体)
注意:lparser.c 直接调用 lcode.c,中间没有 AST 中间表示。这意味着语法分析和代码生成是交织在一起的。
2. 缓冲输入流:lzio.c
2.1 ZIO 结构体
// lzio.h
typedef struct ZIO {
lua_Reader reader; // 读取器函数(从文件/内存/缓冲区读取)
void *data; // 读取器的私有数据
const char *n; // 剩余未处理的字符数
const char *p; // 当前位置指针
lua_State *L; // 关联的lua_State
} ZIO;
2.2 初始化
// lzio.c
int luaZ_init(lua_State *L, ZIO *z, lua_Reader reader, void *data) {
z->reader = reader;
z->data = data;
z->L = L;
z->n = NULL;
z->p = NULL;
return 0;
}
2.3 读取一个字符
// lzio.c
static int fillbuffer(ZIO *z) {
size_t size;
const char *buf;
buf = z->reader(z->L, z->data, &size);
if (buf == NULL) return EOZ; // EOF
z->n = buf + size; // 新缓冲区末尾
z->p = buf;
return cast_int(*(z->p++)); // 返回第一个字符
}
int luaZ_fill(ZIO *z) {
// 当前缓冲区为空时调用
return fillbuffer(z);
}
3. 词法分析器:llex.c
3.1 LexState 结构体
// llex.h
typedef struct LexState {
int current; // 当前字符
int linenumber; // 当前行号
int lastline; // 上一个token的行号
Token t; // 当前token
Token lookahead; // 预读token(用于处理1个token的向前看)
struct FuncState *fs; // 当前函数状态
lua_State *L;
ZIO *z; // 缓冲输入流
Mbuffer *buff; // token文本缓冲区
TString *source; // 源文件名
// ...
} LexState;
3.2 Token 类型
// llex.h
#define FIRST_KEYWORD TK_AND // 第一个关键字的值
// 关键字列表:
// and break do else elseif end false for function goto
// if in local nil not or repeat return then true until while
Token 类型分为:
- 单字符:
+-*/{}等(直接用ASCII值) - 多字符:
==~=<=>=<<>>..// - 关键字:
ifthenendfunction等 - 标识符:用户定义的变量名
- 字面量:数字、字符串
3.3 llex() 函数——核心扫描
// llex.c - 简化的主扫描函数
int llex(LexState *ls, SemInfo *seminfo) {
const char *buf;
size_t capacit;
int res;
luaZ_resetbuffer(ls->buff);
for (;;) { // 跳过空白字符和注释
switch (ls->current) {
case '\n': { // 换行:增加行号
incLINE(ls);
break;
}
case '-': { // 可能是注释
next(ls);
if (ls->current != '-') return '-';
// 是注释,跳过
next(ls);
if (ls->current == '[') { // 可能是嵌套注释
int s = skipSep(ls);
if (s >= 0) {
read_long_string(ls, NULL, s); // 嵌套注释
} else { /* ... */ }
} else {
while (!is_newline(ls->current) && ls->current != EOZ)
next(ls); // 单行注释
}
break;
}
case '[': { // 可能是长字符串 [[...]]
int s = skipSep(ls);
if (s >= 0) {
seminfo->ts = read_long_string(ls, NULL, s);
return TK_LSTRING;
}
break;
}
case '=': { // == 或 =
next(ls);
if (ls->current != '=') return '=';
next(ls);
return TK_EQ;
}
case '~': { // ~= 或 ~
next(ls);
if (ls->current != '=') return '~';
next(ls);
return TK_NE;
}
case '<': { // <=, <, <<, <>
next(ls);
if (ls->current == '=') { next(ls); return TK_LE; }
else if (ls->current == '<') { next(ls); return TK_SHL; }
else return '<';
}
// ... 更多操作符
case '"': case '\'': { // 字符串字面量
seminfo->ts = read_string(ls, ls->current);
return TK_FLT; // 或 TK_SHR/LNGSTR
}
default: {
if (lisalpha(ls->current)) {
TString *ts;
// 识别标识符或关键字
ts = read_name(ls);
seminfo->ts = ts;
// 检查是否为关键字
res = get_keyword(ts);
if (res >= 0) return res; // 返回关键字token
return TK_NAME;
}
else if (isdigit(ls->current)) {
// 识别数字
read_numeral(ls, seminfo);
return (ls->t.token == TK_FLT) ? TK_FLT : TK_INT;
}
// ... 其他字符
}
}
}
}
3.4 字符串字面量处理
// llex.c
static TString *read_string(LexState *ls, int sep) {
// sep = '"' 或 '\'' 或长字符串分隔符
StringBuilder *sb = &ls->lsb;
int c;
// 扩展缓冲区
luaZ_buffgrow(ls->buff, 32);
// 逐字符读取直到匹配的闭合符
while ((c = llex(ls)) != sep) {
if (c == EOZ) lexerror(ls, "unfinished string", 0);
// 处理转义序列 \n, \t, \xHH, \uXXXX 等
}
// 创建TString对象
return luaS_newlstr(ls->L, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
}
3.5 数字解析
// llex.c
static void read_numeral(LexState *ls, SemInfo *seminfo) {
TValue obj;
const char *expo = "Ee";
int first = ls->current;
luaZ_buffremove(ls->buff, 0);
// 支持前缀0x(十六进制)
if (first == '0' && check_next2(ls, "xX")) {
expo = "Pp"; // 十六进制用P/p
}
for (;;) {
// 读取数字、小数点、指数部分
if (check_next2(ls, expo)) {
check_next2(ls, "-+"); // 指数符号
}
}
// 转换为数值
if (luaO_str2num(luaZ_buffer(ls->buff), &obj)) {
if (ttisinteger(&obj)) {
seminfo->i = ivalue(&obj);
} else {
// 浮点数
seminfo->r = fltvalue(&obj);
}
}
}
3.6 预读机制
// llex.h
void luaX_lookahead(LexState *ls) {
lua_assert(ls->lookahead.token == TK_EOS);
ls->lookahead.token = llex(ls, &ls->lookahead.seminfo);
}
// llex.c
void luaX_next(LexState *ls) {
ls->lastline = ls->linenumber;
if (ls->lookahead.token != TK_EOS) {
// 使用预读的token
ls->t = ls->lookahead;
ls->lookahead.token = TK_EOS;
} else {
ls->t.token = llex(ls, &ls->t.seminfo);
}
}
预读解决的歧义:f(a, b) vs f(a, b) = c。解析器需要看 ( 之后的 token 来判断是函数调用还是赋值。
4. 语法解析器:lparser.c
4.1 设计特点
Lua 的语法解析器是一个递归下降解析器(Recursive Descent Parser)。与大多数语言解析器不同,它不生成 AST,而是在解析过程中直接调用 lcode.c 生成字节码。
这带来了:
- 优点:更简单、内存使用更少、编译速度极快
- 缺点:无法独立进行语法优化、难以实现复杂的代码变换
4.2 核心入口
// lparser.c
void luaY_parser(lua_State *L, LexState *ls, Proto *zb, const char *chunkname) {
FuncState fs;
BlockCnt bl;
// 初始化FunctionState
fs.f = addprototype(L, zb, chunkname); // 创建Proto
fs.f->is_vararg = 1; // chunk是可变参数函数
fs.nci = 0;
// 初始化词法分析器
ls->fs = &fs;
fs.prev = NULL;
fs.ls = ls;
fs.bl = NULL;
fs.pc = 0;
fs.lasttarget = 0;
fs.jpc = NO_JUMP;
fs.nk = 0;
fs.np = 0;
fs.nlocvars = 0;
fs.nactvar = 0;
fs.nups = 0;
fs.nworkvar = 0;
// 开始解析
enterblock(&bl);
luaX_next(ls);
statlist(ls); // 解析语句列表
leaveblock(&fs);
// 生成返回指令
luaK_ret(&fs, 0, 0);
// 最终检查
luaK_finish(&fs, ls->t.token != TK_EOS);
}
4.3 FuncState——解析过程中的状态
// lparser.h
typedef struct FuncState {
Proto *f; // 正在构建的函数原型
struct FuncState *prev; // 外层函数(嵌套解析)
LexState *ls; // 词法分析器
struct BlockCnt *bl; // 当前块状态
int pc; // 当前已生成的指令数
int lasttarget; // 上一个跳转目标
int jpc; // 待回填的跳转指令链
int nk; // 常量表中已有的常量数
int np; // 子函数原型数
int nlocvars; // 局部变量数
int nactvar; // 活跃局部变量数
int nups; // 上值数
int nworkvar; // 工作变量数
} FuncState;
prev 字段形成了一个链表——当解析器遇到嵌套函数定义时,创建新的 FuncState 并压入链表;内层函数解析完毕后恢复外层。
4.4 BlockCnt——块状态
// lparser.h
typedef struct BlockCnt {
struct BlockCnt *prev; // 外层块
int firstlabel; // 第一个标签
int firstgoto; // 第一个跳转
lu_byte nactvar; // 进入块时的活跃变量数
lu_byte inscope; // 是否在作用域内
lu_byte upval; // 是否有上值引用
lu_byte verbose; // 调试信息
} BlockCnt;
4.5 语法规则到函数的映射
Lua 的每条语法规则对应一个解析函数:
| 语法规则 | 解析函数 | 作用 |
|---|---|---|
block |
block() |
解析语句块 |
statement |
statement() |
解析单条语句 |
expr |
expr() |
解析表达式(优先级爬坡) |
subexpr |
subexpr(ls, limit) |
表达式递归解析 |
primaryexp |
primaryexp() |
基本表达式(变量、函数调用、括号) |
funcbody |
funcbody() |
解析函数体 |
assignment |
assignment() |
解析赋值 |
ifstat |
ifstat() |
解析 if-elseif-else-end |
whilestat |
whilestat() |
解析 while 循环 |
forstat |
forstat() |
解析 for 循环 |
fornum |
fornum() |
数值 for |
forlist |
forlist() |
泛型 for |
retstat |
retstat() |
解析 return |
funcall |
functioncall() |
解析函数调用 |
listfields |
listfields() |
解析表的列表部分 |
recfields |
recfields() |
解析表的记录部分 |
field |
field() |
解析单个表字段 |
4.6 表达式优先级
// lparser.c - 表达式解析
// 表达式优先级(从低到高):
// 1. or (8)
// 2. and (7)
// 3. < > <= >= ~= == (6)
// 4. | (位或) (5)
// 5. ~ (位异或)(4)
// 6. & (位与) (3)
// 7. << >> (2)
// 8. .. (连接)(1)
// 9. + - (0)
// 10. * / % // (前缀)
// 11. not # - ~ (一元)
// 12. ^ (幂) (前缀)
// 13. 数字/字符串/nil/true/false/function/table/...) (原子)
static const int priority[] = { /* order must match binary method names */
/* OPR_AND */ {6, 6},
/* OPR_OR */ {5, 5},
/* OPR_CONCAT */ {3, 4},
/* OPR_ADD */ {8, 8},
/* OPR_SUB */ {8, 8},
/* OPR_MUL */ {9, 9},
/* OPR_DIV */ {9, 9},
/* OPR_MOD */ {9, 9},
/* OPR_POW */ {14, 13},
/* OPR_IDIV */ {9, 9},
/* OPR_BAND */ {6, 7},
/* OPR_BXOR */ {8, 8},
/* OPR_BOR */ {4, 5},
/* OPR_SHL */ {7, 7},
/* OPR_SHR */ {7, 7},
/* OPR_UNM */ {12, 12},
/* OPR_BNOT */ {12, 12},
/* OPR_LEN */ {12, 12},
};
4.7 subexpr() 函数
// lparser.c - 简化的表达式解析
static int subexpr(LexState *ls, int limit) {
// 尝试前缀表达式(一元运算符)
int op = get_unop(ls->t.token);
if (op >= 0) {
next(ls);
int v = subexpr(ls, unary_priority);
// 生成一元操作指令
}
// 尝试原子表达式
primaryexp(ls, &v);
// 处理二元运算符
int leftop = get_binop(ls->t.token);
while (leftop >= 0 && priority[leftop][0] > limit) {
next(ls);
int right = subexpr(ls, priority[leftop][1]);
// 生成二元操作指令
leftop = get_binop(ls->t.token);
}
return leftop; // 返回未处理的运算符(-1表示完成)
}
5. 局部变量处理
// lparser.c
static void new_localvar(LexState *ls, TString *name) {
FuncState *fs = ls->fs;
Proto *f = fs->f;
int idx = fs->nactvar++; // 分配栈槽位
luaC_objbarrier(ls->L, f, name);
// 存储变量信息(用于调试)
luaX_newstring(ls, luaStrData(name), luaStrLen(name));
// 设置变量名
// 进入作用域时:nactvar 增加
// 退出作用域时:nactvar 恢复
}
5.1 变量查找
// lparser.c
static int searchvar(FuncState *fs, TString *n) {
// 从当前函数开始,逐层向外查找
// 1. 局部变量:检查 nactvar 范围内的变量
// 2. 如果找不到,标记为上值引用
for (int i = fs->nactvar - 1; i >= 0; i--) {
if (eqstr(n, getstr(f->locvars[i].varname)))
return i; // 找到,返回栈槽位
}
return -1; // 未找到
}
6. 本篇小结
| 概念 | 要点 |
|---|---|
| 缓冲输入 | ZIO 提供统一的输入抽象 |
| 词法分析 | llex.c 将源代码文本转为 Token 流 |
| 语法解析 | lparser.c 递归下降解析,不生成 AST |
| 代码生成 | lcode.c 在解析过程中直接产生字节码 |
| 关键设计 | 语法分析和代码生成交织在一起 |
| 局部变量 | 栈上分配,作用域通过 nactvar 控制 |
| 嵌套函数 | FunctionState 链表管理 |
| 预读 | lookahead 机制处理1个token的向前看 |
思考题
- 为什么 Lua 选择不在解析阶段生成 AST?这带来了什么好处和代价?
- Lua 的递归下降解析器如何处理左递归语法(如
expr → expr + expr)? - 词法分析器的
lookahead(预读)机制是为了解决什么语法歧义? - 如果要在 Lua 中新增一种语法(如三元运算符
a ? b : c),需要修改哪些源文件? read_long_string如何处理[[...]]和[=[...]=]两种格式?BlockCnt中的upval字段标记了什么?它在什么时候被设置?
思考题解答
1. 为什么 Lua 不生成 AST?
好处:
- 编译速度极快:没有构建和遍历AST的开销,代码在解析的同时就生成字节码
- 内存使用极少:AST节点需要额外的内存分配和指针链接,而直接生成字节码只需要Proto结构体
- 实现简单:整个lparser.c + lcode.c 只有约1500行代码,对比Python的Parser (~5000行) + AST (~8000行) 小得多
- 代码紧凑:适合嵌入式场景,编译器本身占用资源少
代价:
- 无法做复杂的编译期优化:没有AST就无法进行模式匹配、常量传播、死代码消除等优化
- 代码变换困难:无法实现宏展开、语法糖转换等需要在AST层面操作的功能
- 调试困难:没有AST结构,调试器难以显示完整的语法树
- 扩展语法困难:新增语法需要修改解析器代码,而不是单独写一个AST变换pass
Lua的选择:对于嵌入式脚本语言,编译速度和内存使用比编译期优化更重要。Lua通过极简的编译器实现了"够用"的性能。
2. 如何处理左递归?
递归下降解析器天然不能处理直接左递归(expr → expr + term),因为会导致无限递归。Lua的解决方案是优先级爬坡(Precedence Climbing):
// 递归下降处理左递归的经典方法
// subexpr(ls, limit) 解析优先级大于limit的表达式
static int subexpr(LexState *ls, int limit) {
// 1. 先解析一元运算符(前缀)
int op = get_unop(ls->t.token);
if (op >= 0) {
next(ls);
subexpr(ls, UNARY_PRIORITY); // 一元运算符优先级最高
}
// 2. 解析原子表达式(数字、字符串、括号等)
primaryexp(ls, &v);
// 3. 处理二元运算符(后缀)
int leftop = get_binop(ls->t.token);
while (leftop >= 0 && priority[leftop][0] > limit) {
next(ls);
// 递归解析右操作数,优先级 >= 左操作符的右结合优先级
subexpr(ls, priority[leftop][1]);
// 生成二元操作指令
leftop = get_binop(ls->t.token);
}
return leftop;
}
工作原理:
priority[op][0]是左结合优先级priority[op][1]是右结合优先级- 右操作数递归调用时传入右结合优先级,实现了左结合性
- 例如
1 + 2 + 3:解析1后遇到+,右操作数递归调用subexpr(ls, 8),遇到第二个+时因为8 <= 8而停止,让第二个+回到外层处理
幂运算 ^ 是右结合:priority[OPR_POW] = {14, 13},左优先级14,右优先级13,这使得 a^b^c 解析为 a^(b^c)。
3. 预读机制解决什么歧义?
预读解决的核心歧义是表达式和赋值的区分:
-- 歧义场景1:函数调用 vs 赋值
f(a, b) -- 函数调用
f(a, b) = c -- 赋值给f(a,b)的返回值
-- 解析器在看到 f(a,b) 后需要看下一个token
-- 如果是 '=' → 赋值
-- 如果是 '(' 或其他 → 函数调用
具体歧义:
f(a, b)vsf(a, b) = 1:解析器在解析完f(a, b)后需要判断是函数调用还是赋值目标a or bvsa and b:需要预读来确定是or还是andfunction(a) ... endvs(function(a) ... end):需要预读来区分匿名函数定义和函数调用
Lua只使用1个token的预读(lookahead),这比需要任意多个token的语法(如C++的某些歧义)简单得多。这也是Lua语法设计的一个原则——保持LL(1)或接近LL(1)的解析能力。
4. 新增三元运算符需要修改哪些文件?
假设新增语法 a ? b : c,需要修改:
| 文件 | 修改内容 |
|---|---|
llex.h |
定义新token TK_QMARK(?)和 TK_COLON(: 已有) |
llex.c |
在词法扫描中识别 ? 字符,返回对应token |
lparser.h |
可能需要新的表达式类型枚举 |
lparser.c |
在 subexpr() 中处理 ? 的优先级,在 expr() 或 subexpr() 中添加三元表达式的解析逻辑 |
lcode.c |
添加代码生成逻辑:条件表达式 + 跳转到两个分支 + phi合并 |
lopcodes.h |
如果需要新指令,定义操作码(通常不需要,可用已有指令组合) |
lopcodes.c |
如果有新指令,在opmodes表中添加条目 |
lua.h |
可能需要添加元方法支持(如果 ? 需要元方法) |
最小修改集:llex.c + lparser.c + lcode.c 三个文件,不需要修改VM。
三元运算符可以用已有指令实现:
-- a ? b : c 的字节码
eval a
OP_TEST R[a] 0 -- if not a then goto else
JMP +N -- 跳到else分支
eval b -- then分支
JMP +M -- 跳过else分支
eval c -- else分支
-- 结果在栈顶
5. read_long_string 如何处理两种格式?
[[...]] 和 [=[...]=] 的处理逻辑:
// llex.c
static void read_long_string(LexState *ls, SemInfo *seminfo, int sep) {
// sep 是等号数量:[[ ]] 的 sep=0,[=[ ]=] 的 sep=1
int cont = 0;
while (1) {
int c = luaZ_get(ls->z); // 读取下一个字符
switch (c) {
case '[': {
// 检查是否是嵌套的长字符串开头
if (skip_sep(ls) == sep) {
// 是嵌套的,继续读取(只增加嵌套计数)
cont++;
luaZ_buffremove(ls->buff, 0);
} else {
// 不是嵌套的,把 [ 写入缓冲区
}
break;
}
case ']': {
// 检查是否是闭合
if (skip_sep(ls) == sep) {
if (cont == 0) {
// 匹配成功,结束
return;
}
// 嵌套的闭合,减少嵌套计数
cont--;
luaZ_buffremove(ls->buff, 0);
}
break;
}
case EOZ:
// 文件结束,未找到闭合
lexerror(ls, "unfinished long string", 0);
break;
default: {
// 普通字符,直接写入缓冲区
}
}
}
}
处理逻辑:
- 记录分隔符的等号数量
sep([[的 sep=0,[=[的 sep=1) - 逐字符读取,遇到
[时检查是否是嵌套的长字符串(等号数量匹配则嵌套+1) - 達到
]时检查是否是闭合(等号数量匹配则嵌套-1,嵌套为0时结束) - 如果遇到
EOZ(文件结束)则报错 - 嵌套支持:
[[ ... [[ ... ]] ... ]]是合法的,内层的[[不会被当作闭合
为什么需要等号匹配:避免 [[ 字符串中出现 ] 时被误判为闭合。例如 [[if (a[1]) then]] 中的 ] 不应该结束字符串。
6. BlockCnt 中的 upval 字段
upval 字段标记当前块中是否引用了外部局部变量(需要创建上值)。
什么时候被设置:
// lparser.c
static void markupval(FuncState *fs) {
BlockCnt *bl = fs->bl;
while (bl != NULL) {
bl->upval = 1; // 标记当前块及所有外层块
bl = bl->prev;
}
}
当解析器遇到一个变量引用时(如 a),如果变量不是当前块的局部变量,就会调用 markupval()。
具体场景:
local x = 1
function foo()
print(x) -- 引用了外部变量x,需要上值
end
当解析器在 foo() 函数体中遇到 x 时:
- 在
foo的局部变量中找不到x - 标记
foo所在块的upval = 1 - 继续向外传播,直到找到
x的定义块
用途:
- 决定是否需要为当前函数创建上值
- 影响
leaveblock()时的行为:如果upval=1,需要生成OP_CLOSE指令 - 帮助调试器确定变量的来源
没有 upval 优化:如果块中没有引用外部变量,就不需要为该块创建上值,节省内存和运行时开销。

浙公网安备 33010602011771号