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

相关推荐
UWA4 天前
Unreal开发痛点破解!GOT Online新功能:Lua全监控 + LLM内存可视化!
开发语言·lua·unreal
1nullptr4 天前
Lua迭代器与泛型for
lua
半夏知半秋4 天前
skynet debug_console控制台中debug指令使用
服务器·开发语言·学习·lua
h7997104 天前
redis lua脚本(go)调用教程以及debug调试
redis·golang·lua
玩转C语言和数据结构7 天前
Lua下载和安装教程(附安装包)
lua·lua下载·lua安装教程·lua下载和安装教程·lua安装包
Arva .7 天前
HTTP Client
网络协议·http·lua
爱吃小胖橘8 天前
Lua语法(2)
开发语言·unity·lua
ellis19709 天前
LuaC API知识点汇总
unity·lua
爱吃小胖橘12 天前
Lua语法
开发语言·unity·lua
东方芷兰12 天前
JavaWeb 课堂笔记 —— 20 SpringBootWeb案例 配置文件
java·开发语言·笔记·算法·log4j·intellij-idea·lua