目录

一、虚拟机篇 - 语义分割单位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类型等。

lua 代码在线 调试_lua 代码在线 调试

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;
  }
}