目录
一、虚拟机篇 - 语义分割单位Token结构
二、虚拟机篇 - 语义分割主流程
三、虚拟机篇 - 保留字类型的实现
四、虚拟机篇 - 复杂语义信息存储
上一篇,我们讲到了Lua脚本文件加载和读取的方式。其中luaX_next函数就是用来将Lua脚本字符串逐个切割出Token。
一、虚拟机篇 - 语义分割单位Token结构
Token定义:Lua会对脚本语言逐个切分出最小单位Token。例如lua保留字“if”的Token是TK_IF,字符串Token为TK_STRING。
- Lua通过luaX_next逐个读取字符流字符,直到切割出一个完整的Token。(每次切1个)
- Token包含计算机语言的基础保留符号(;{}等)、Lua保留字(nil、if等)和其它标记Token关键值
- Token包含各种保留字和基础符号等,同时针对字符串/数字等类型,Token结构提供了SemInfo来保存语法信息
看一下Token的数据结构:
- Token中的token代表类型
- SemInfo用于存储不同token类型下的语义辅助信息(例如:TK_STRING,seminfo->ts上用于存储字符串)
//语义辅助信息
typedef union {
lua_Number r;
lua_Integer i;
TString *ts;
} SemInfo; /* 语义信息 semantics information */
//语义分割最小单位Token
typedef struct Token {
int token; //Token类型
SemInfo seminfo; //语义信息,例如token为字符串/数字等都需要存储具体的值
} Token;
然后看一下Token的类型
- Token的类型是用int来存储的。枚举RESERVED中包含了两部分类型:Lua系统关键字和其它标记Token关键值
- FIRST_RESERVED是从257开始的,相当于将系统基础符号的类型给留空了。
- 基础保留符号类型,则直接返回单个字符(每个符号对应是一个int类型编号)
enum RESERVED {
/* 系统默认关键字 terminal symbols denoted by reserved words */
TK_AND = FIRST_RESERVED, TK_BREAK,
TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION,
TK_GOTO, TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT,
TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE,
/* 其它关键字 other terminal symbols */
TK_IDIV, TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE,
TK_SHL, TK_SHR,
TK_DBCOLON, TK_EOS,
TK_FLT, TK_INT, TK_NAME, TK_STRING
};
static const char *const luaX_tokens [] = {
"and", "break", "do", "else", "elseif",
"end", "false", "for", "function", "goto", "if",
"in", "local", "nil", "not", "or", "repeat",
"return", "then", "true", "until", "while",
"//", "..", "...", "==", ">=", "<=", "~=",
"<<", ">>", "::", "<eof>",
"<number>", "<integer>", "<name>", "<string>"
};
二、虚拟机篇 - 语义分割主流程
我们先看一个非常简单的Lua语言代码例子:
age=5;
name='zhuli';
这个例子中,通过luaX_next分割后,会分割成N个部分。age分割成TK_NAME类型,=分割成=符号Token,5分割成TK_INT类型等。
luaX_next方法底层主要调用的是llex函数。llex是一个for循环状态机,通过循环读取文件流中的字符,进行Token的切割操作,当切割到一个Token后,就会返回Token的类型和语义信息。针对换行,空格等符号,则会跳过。
next方法调用的是zgetc方法,逐个读取文件流中的数据,直到切割出一个完整的Token来。
/**
* Token解析函数,逐个读取字符流
* 其中next函数:从ZIO文件流上读取下一个字符
* 完成一个Token的切割,则返回Token结果
*/
static int llex (LexState *ls, SemInfo *seminfo) {
luaZ_resetbuffer(ls->buff);
for (;;) {
switch (ls->current) {
/* 换行符号 ,跳过 */
case '\n': case '\r': { /* line breaks */
inclinenumber(ls);
break;
}
/* 长字符串处理 */
case '[': { /* long string or simply '[' */
int sep = skip_sep(ls);
if (sep >= 0) {
read_long_string(ls, seminfo, sep);
return TK_STRING;
}
else if (sep != -1) /* '[=...' missing second bracket */
lexerror(ls, "invalid long string delimiter", TK_STRING);
return '[';
}
/* == 处理 */
case '=': {
next(ls);
if (check_next1(ls, '=')) return TK_EQ;
else return '=';
}
//.........................
/* 变量名称等处理/关键字 */
default: {
if (lislalpha(ls->current)) { /* identifier or reserved word? */
TString *ts;
do {
save_and_next(ls);
} while (lislalnum(ls->current));
ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
seminfo->ts = ts;
if (isreserved(ts)) /* 保留关键字? reserved word? */
return ts->extra - 1 + FIRST_RESERVED;
else {
return TK_NAME;
}
}
else { /* single-char tokens (+ - / ...) */
int c = ls->current;
next(ls);
return c;
}
}
}
}
}
三、虚拟机篇 - 保留字类型的实现
上面的枚举RESERVED中,我们看到了Lua的Token切割器会将语法的保留字切割出来。
- 保留字是一个luaX_tokens类型的数组,对应了RESERVED里面的Token类型
- 保留字模块初始化的时候,会将luaX_tokens上的字符串缓存到字符串池上
- 被缓存的保留字,通过ts->extra保存对应的token类型,通过函数isreserved进行判断是否是保留字
- 保留字模块的初始化,在f_luaopen中调用。luaX_init主要将保留字数组循环设置到字符串缓存池上。
void luaX_init (lua_State *L) {
int i;
TString *e = luaS_newliteral(L, LUA_ENV); /* 创建环境变量名称 create env name */
luaC_fix(L, obj2gco(e)); /* never collect this name */
for (i=0; i<NUM_RESERVED; i++) { //循环值从保留字循环值开始
TString *ts = luaS_new(L, luaX_tokens[i]);
luaC_fix(L, obj2gco(ts)); /* reserved words are never collected */
ts->extra = cast_byte(i+1); /* reserved word */
}
}
//llex函数
TString *ts;
do {
save_and_next(ls);
} while (lislalnum(ls->current));
ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
seminfo->ts = ts;
if (isreserved(ts)) /* 保留关键字? reserved word? */
return ts->extra - 1 + FIRST_RESERVED;
else {
return TK_NAME;
}
四、虚拟机篇 - 复杂语义信息存储
上面我们知道,Token主要用来存储类型值(int),而针对字符串/数字等复杂的类型,需要通过SemInfo语义辅助结构来存储辅助的语义信息(字符串/数字等)。
我们可以看一个数字的例子:
/*
** this function is quite liberal in what it accepts, as 'luaO_str2num'
** will reject ill-formed numerals.
** 读取数字类型,具体的数字放置在seminfo->i/seminfo->r上
*/
static int read_numeral (LexState *ls, SemInfo *seminfo) {
..........
if (luaO_str2num(luaZ_buffer(ls->buff), &obj) == 0) /* format error? */
lexerror(ls, "malformed number", TK_FLT);
if (ttisinteger(&obj)) {
seminfo->i = ivalue(&obj); //存储数字
return TK_INT;
}
else {
lua_assert(ttisfloat(&obj));
seminfo->r = fltvalue(&obj);
return TK_FLT;
}
}