LuaJIT源码分析(五)词法分析
lua虽然是脚本语言,但在执行时,还是先将脚本编译成字节码,然后再由虚拟机解释执行。在编译脚本时,首先需要对源代码进行词法分析,把源代码分解为token流。lua的token可以分为若干不同的类型,比如关键字,标识符,字面量,运算符,分隔符等等。
标识符
可以是由字母、数字和下划线组成的任意字符串,但不能以数字开头。
关键字
具有特殊含义的保留字,不可以用作标识符,共有22个。
and break do else elseif
end false for function if
in local nil not or
repeat return then true until while
字符串字面常量
lua的字面字符串定义相当灵活,以下几种写法都是合法的,而且表示同一个字符串:
lua
a = 'alo\n123"'
a = "alo\n123\""
a = '\97lo\10\04923"'
a = [[alo
123"]]
a = [==[
alo
123"]==]
数字字面常量
一个数值常量可以用可选的小数部分和可选的小数指数来表示。lua还接受十六进制整数常量,通过在前面加上0x来表示。以下几种表示形式都是合法的:
3 3.0 3.1416 314.16e-2 0.31416E1 0xff 0x56
运算符和分隔符
主要有以下若干种。
+ - * / % ^ #
== ~= <= >= < > =
( ) { } [ ]
; : , . .. ...
LuaJIT的词法分析代码集中在lex_scan
这个函数上。在深入之前,我们先了解一下LuaJIT用于词法分析的数据结构。
c
// lj_lex.h
/* Lua lexer state. */
typedef struct LexState {
struct FuncState *fs; /* Current FuncState. Defined in lj_parse.c. */
struct lua_State *L; /* Lua state. */
TValue tokval; /* Current token value. */
TValue lookaheadval; /* Lookahead token value. */
const char *p; /* Current position in input buffer. */
const char *pe; /* End of input buffer. */
LexChar c; /* Current character. */
LexToken tok; /* Current token. */
LexToken lookahead; /* Lookahead token. */
SBuf sb; /* String buffer for tokens. */
lua_Reader rfunc; /* Reader callback. */
void *rdata; /* Reader callback data. */
BCLine linenumber; /* Input line counter. */
BCLine lastline; /* Line of last token. */
GCstr *chunkname; /* Current chunk name (interned string). */
const char *chunkarg; /* Chunk name argument. */
const char *mode; /* Allow loading bytecode (b) and/or source text (t). */
VarInfo *vstack; /* Stack for names and extents of local variables. */
MSize sizevstack; /* Size of variable stack. */
MSize vtop; /* Top of variable stack. */
BCInsLine *bcstack; /* Stack for bytecode instructions/line numbers. */
MSize sizebcstack; /* Size of bytecode stack. */
uint32_t level; /* Syntactical nesting level. */
int endmark; /* Trust bytecode end marker, even if not at EOF. */
int fr2; /* Generate bytecode for LJ_FR2 mode. */
} LexState;
数据结构看上去很复杂,不过好在每个成员变量都有相应的注释,而且在目前讨论的词法分析阶段中,只有少数几个成员变量需要考虑:
c
// lj_lex.h
/* Lua lexer state. */
typedef struct LexState {
TValue tokval; /* Current token value. */
TValue lookaheadval; /* Lookahead token value. */
const char *p; /* Current position in input buffer. */
const char *pe; /* End of input buffer. */
LexChar c; /* Current character. */
LexToken tok; /* Current token. */
LexToken lookahead; /* Lookahead token. */
SBuf sb; /* String buffer for tokens. */
} LexState;
tokval和lookaheadval分别表示当前扫描到的token和下一个即将被扫描的token;p和pe表示扫描的源代码buffer当前位置和重点位置;c表示当前扫描到的字符;tok和lookahead分别表示当前和下一个扫描的token类型;最后sb表示处理当前token所缓存的buffer。
LuaJIT的词法分析实现基本上也是个有限状态机,根据当前读到的字符,切换到不同的读取状态:
c
/* Get next lexical token. */
static LexToken lex_scan(LexState *ls, TValue *tv)
{
lj_buf_reset(&ls->sb);
for (;;) {
if (lj_char_isident(ls->c)) {
GCstr *s;
if (lj_char_isdigit(ls->c)) { /* Numeric literal. */
lex_number(ls, tv);
return TK_number;
}
/* Identifier or reserved word. */
do {
lex_savenext(ls);
} while (lj_char_isident(ls->c));
s = lj_parse_keepstr(ls, ls->sb.b, sbuflen(&ls->sb));
setstrV(ls->L, tv, s);
if (s->reserved > 0) /* Reserved word? */
return TK_OFS + s->reserved;
return TK_name;
}
switch (ls->c) {
case '\n':
case '\r':
lex_newline(ls);
continue;
case ' ':
case '\t':
case '\v':
case '\f':
lex_next(ls);
continue;
case '-':
lex_next(ls);
if (ls->c != '-') return '-';
lex_next(ls);
if (ls->c == '[') { /* Long comment "--[=*[...]=*]". */
int sep = lex_skipeq(ls);
lj_buf_reset(&ls->sb); /* `lex_skipeq' may dirty the buffer */
if (sep >= 0) {
lex_longstring(ls, NULL, sep);
lj_buf_reset(&ls->sb);
continue;
}
}
/* Short comment "--.*\n". */
while (!lex_iseol(ls) && ls->c != LEX_EOF)
lex_next(ls);
continue;
case '[': {
int sep = lex_skipeq(ls);
if (sep >= 0) {
lex_longstring(ls, tv, sep);
return TK_string;
} else if (sep == -1) {
return '[';
} else {
lj_lex_error(ls, TK_string, LJ_ERR_XLDELIM);
continue;
}
}
case '=':
lex_next(ls);
if (ls->c != '=') return '='; else { lex_next(ls); return TK_eq; }
case '<':
lex_next(ls);
if (ls->c != '=') return '<'; else { lex_next(ls); return TK_le; }
case '>':
lex_next(ls);
if (ls->c != '=') return '>'; else { lex_next(ls); return TK_ge; }
case '~':
lex_next(ls);
if (ls->c != '=') return '~'; else { lex_next(ls); return TK_ne; }
case ':':
lex_next(ls);
if (ls->c != ':') return ':'; else { lex_next(ls); return TK_label; }
case '"':
case '\'':
lex_string(ls, tv);
return TK_string;
case '.':
if (lex_savenext(ls) == '.') {
lex_next(ls);
if (ls->c == '.') {
lex_next(ls);
return TK_dots; /* ... */
}
return TK_concat; /* .. */
} else if (!lj_char_isdigit(ls->c)) {
return '.';
} else {
lex_number(ls, tv);
return TK_number;
}
case LEX_EOF:
return TK_eof;
default: {
LexChar c = ls->c;
lex_next(ls);
return c; /* Single-char tokens (+ - / ...). */
}
}
}
}
lex_scan
会返回当前扫描的token类型,LuaJIT只对那些不能用单字符表示的token,进行了定义,如果token本身就是单字符的,比如( + - / )之类,就直接用该字符作为它的token类型。由于char的取值范围为0-255,那么特殊定义的token类型,需要从256开始了。
c
// lj_lex.h
/* Lua lexer tokens. */
#define TKDEF(_, __) \
_(and) _(break) _(do) _(else) _(elseif) _(end) _(false) \
_(for) _(function) _(goto) _(if) _(in) _(local) _(nil) _(not) _(or) \
_(repeat) _(return) _(then) _(true) _(until) _(while) \
__(concat, ..) __(dots, ...) __(eq, ==) __(ge, >=) __(le, <=) __(ne, ~=) \
__(label, ::) __(number, <number>) __(name, <name>) __(string, <string>) \
__(eof, <eof>)
enum {
TK_OFS = 256,
#define TKENUM1(name) TK_##name,
#define TKENUM2(name, sym) TK_##name,
TKDEF(TKENUM1, TKENUM2)
#undef TKENUM1
#undef TKENUM2
TK_RESERVED = TK_while - TK_OFS
};
可能会有疑问的一点是,为什么这里要引入TKENUM1和TKENUM2两种不同的宏,明明作用完全相同。答案是作者为了简洁,少写一些代码,把LuaJIT的关键字定义,也套用到了TKDEF这个宏上:
c
// lj_lex.c
/* Lua lexer token names. */
static const char *const tokennames[] = {
#define TKSTR1(name) #name,
#define TKSTR2(name, sym) #sym,
TKDEF(TKSTR1, TKSTR2)
#undef TKSTR1
#undef TKSTR2
NULL
};
接下来我们回到lex_scan
函数上,首先函数会调用lj_buf_reset
清理缓存的token buffer,这个buffer只在单次scan中有效。然后,LuaJIT开始判断当前字符是一个什么样的字符。这里LuaJIT使用了查表的方式,预先将ASCII表中的所有字符进行属性标记。
c
// lj_char.h
#define LJ_CHAR_CNTRL 0x01
#define LJ_CHAR_SPACE 0x02
#define LJ_CHAR_PUNCT 0x04
#define LJ_CHAR_DIGIT 0x08
#define LJ_CHAR_XDIGIT 0x10
#define LJ_CHAR_UPPER 0x20
#define LJ_CHAR_LOWER 0x40
#define LJ_CHAR_IDENT 0x80
#define LJ_CHAR_ALPHA (LJ_CHAR_LOWER|LJ_CHAR_UPPER)
#define LJ_CHAR_ALNUM (LJ_CHAR_ALPHA|LJ_CHAR_DIGIT)
#define LJ_CHAR_GRAPH (LJ_CHAR_ALNUM|LJ_CHAR_PUNCT)
LJ_DATA const uint8_t lj_char_bits[257];
剩下的逻辑其实就比较简单了,如果当前字符是数字,那么就走假设token是数字字面常量的逻辑;如果是字母下划线,那就走关键字或是标识符的逻辑;否则就走其他处理逻辑。这些处理逻辑都比较简单,如果只通过当前字符无法判断token类型,就会去读取下一个字符甚至更多字符来进行判断。例如遇到字符.
时,会尝试再读取一个字符,如果依旧是.
,那么还需要再读一个字符来确定当前token是TK_dots ...
还是TK_concat ..
;如果不是,那么根据字符是否为数字,就能得出token是TK_number还是.
类型了。