LuaJIT源码分析(五)词法分析

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还是.类型了。

相关推荐
unix2linux17 小时前
YOLO v5 Series - Image & Video Storage ( Openresty + Lua)
yolo·lua·openresty
最后一个bug18 小时前
如何理解Lua 使用虚拟堆栈
linux·c语言·开发语言·嵌入式硬件·lua
半夏知半秋2 天前
lua-lru缓存算法解析
笔记·学习·算法·缓存·lua
最后一个bug3 天前
C语言执行Lua进行错误处理
linux·服务器·c语言·开发语言·嵌入式硬件·lua
袁庭新3 天前
Lua如何连接MySQL数据库?
redis·mysql·lua·lua5.4·袁庭新
红黑色的圣西罗3 天前
Lua 实现继承的一种方式
开发语言·lua
weixin_428498494 天前
Lua资料
开发语言·lua
Clown954 天前
go-zero(四) 错误处理(统一响应信息)
开发语言·golang·lua
半夏知半秋5 天前
lua实现雪花算法
开发语言·笔记·算法·lua
最后一个bug5 天前
lua调用C语言函数,在函数中进行类型检查
linux·c语言·开发语言·lua