joe 发表于 2022-6-20 15:57:21

Lua源码分析 - 虚拟机篇 - 语义解析之Token分割器(15)

目录

一、虚拟机篇 - 语义分割单位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类型等。
https://img-blog.csdnimg.cn/20200225202446320.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2luaXRwaHA=,size_16,color_FFFFFF,t_70


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

————————————————
版权声明:本文为CSDN博主「老码农zhuli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/initphp/article/details/104503745

页: [1]
查看完整版本: Lua源码分析 - 虚拟机篇 - 语义解析之Token分割器(15)