Lua 5.1.3源代码分析之词法分析[2]

来源:互联网 发布:淘宝公积金代缴可靠吗 编辑:程序博客网 时间:2024/05/20 06:29
01static void save (LexState *ls, int c) {
02  Mbuffer *b = ls->buff;
03  if (b->n + 1 > b->buffsize) {
04    size_t newsize;
05    if (b->buffsize >= MAX_SIZET/2)
06      luaX_lexerror(ls, "lexical element too long", 0);
07    newsize = b->buffsize * 2;
08    luaZ_resizebuffer(ls->L, b, newsize);
09  }
10  b->buffer[b->n++] = cast(char, c);
11}

save的功能是给Lua Lex状态机的缓存中添加一个字符,会自动进行内存的扩充,按2倍方式扩充。Mbuffer中,buffer是字符串指针,buffsize是当前缓存的大小,n是当前待存储字符的一个索引,n要小于buffsize。

01static const char *txtToken (LexState *ls, int token) {
02  switch (token) {
03    case TK_NAME:
04    case TK_STRING:
05    case TK_NUMBER:
06      save(ls, '\0');
07      return luaZ_buffer(ls->buff);
08    default:
09      return luaX_token2str(ls, token);
10  }
11}

txtToken的功能是分析所得到的token,是不是 标识符(也就是变量名)(name),常量字符串(string),常量数字(number),如果是的话,就加入个串结束符,并返回这个缓存内容,以便做进一步的搜集判断工作。

1static void inclinenumber (LexState *ls) {
2  int old = ls->current;
3  lua_assert(currIsNewline(ls));
4  next(ls);  /* skip `\n' or `\r' */
5  if (currIsNewline(ls) && ls->current != old)
6    next(ls);  /* skip `\n\r' or `\r\n' */
7  if (++ls->linenumber >= MAX_INT)
8    luaX_syntaxerror(ls, "chunk has too many lines");
9}

用于增加词法分析器状态机的行数状态,里面对’\n’和’\r’以及’\r\n’,’\n\r’做了跳过处理。到新行的标准就是越过了一次这4种符号。currIsNewline及next的定义如下:

01#define currIsNewline(ls)       (ls->current == '\n' || ls->current == '\r')
02#define next(ls) (ls->current = zgetc(ls->z))
03#define zgetc(z)  (((z)->n--)>0 ?  char2int(*(z)->p++) : luaZ_fill(z))
04 
05int luaZ_fill (ZIO *z) {
06  size_t size;
07  lua_State *L = z->L;
08  const char *buff;
09  lua_unlock(L);
10  buff = z->reader(L, z->data, &size);
11  lua_lock(L);
12  if (buff == NULL || size == 0) return EOZ;
13  z->n = size - 1;
14  z->p = buff;
15  return char2int(*(z->p++));
16}
17 
18struct Zio {
19  size_t n;                     /* bytes still unread */
20  const char *p;                /* current position in buffer */
21  lua_Reader reader;
22  void* data;                   /* additional data */
23  lua_State *L;                 /* Lua state (for reader) */
24};

currIsNewline的意思是当前ls指向的字符上,刚好是一个换行符,或者一个回车符,就返回1。 而不是指已经到了一个新行了。Zio是一个输入流缓冲结构。 next的作用是获取当前输入流的下一个字符。Ls->z就是流的实例。zgetc返回的是一个字符,这个字符返回到LexState中后,类型会成为int型,所以在宏里面用了一个char2int强制转换。Ls->current变量用于存储当前获取到的字符。Zgetc中,读完一个字符,会将字符缓冲中的指针p向前移一位,并且余量计数n减1,一直到n降到0时,立马调用luaZ_fill重新填充字符缓冲区。

luaZ_fill中,真正起读功能的函数是z->reader,即读入器,是外部挂入的一个钩子函数,是具体平台相关的。可以看到,在读之前,要lua_unlock释放一下锁,读完后,再lua_lock锁上。读完后,会填充zio实例的n,p等成员。

1static int check_next (LexState *ls, const char *set) {
2  if (!strchr(set, ls->current))
3    return 0;
4  save_and_next(ls);
5  return 1;
6}
7#define save_and_next(ls) (save(ls, ls->current), next(ls))

Check_next这个函数检查当前字符是否在一个预定字符集set中,如果不在,就直接返回(而不做索引移动),如果在,就保存当前字符,并检查下一个字符。 save_and_next功能是保存当前字符,并读入检查下一个字符。

1static void buffreplace (LexState *ls, char from, charto) {
2  size_t n = luaZ_bufflen(ls->buff);
3  char *p = luaZ_buffer(ls->buff);
4  while (n--)
5    if (p[n] == from) p[n] = to;
6}

buffreplace功能是将字符缓存中(这里说的字符缓冲,皆指状态机中的字符缓冲),所有出现字符from的地方,替换成字符to。 没什么好讲的,luaZ_bufflen和luaZ_buffer都是简单的宏。

01/* LUA_NUMBER */
02static void read_numeral (LexState *ls, SemInfo *seminfo) {
03  lua_assert(isdigit(ls->current));
04  do {
05    save_and_next(ls);
06  while (isdigit(ls->current) || ls->current == '.');
07  if (check_next(ls, "Ee"))  /* `E'? */
08    check_next(ls, "+-");  /* optional exponent sign */
09  while (isalnum(ls->current) || ls->current == '_')
10    save_and_next(ls);
11  save(ls, '\0');
12  buffreplace(ls, '.', ls->decpoint);  /* follow locale for decimal point */
13  if (!luaO_str2d(luaZ_buffer(ls->buff), &seminfo->r)) /* format error? */
14    trydecpoint(ls, seminfo); /* try to update decimal point separator */
15}

read_numeral功能是将一个数字字串 (含十六进制情况,和科学计数情况xxxxxE-yyy)转换成数字存储。
打头的lua_assert,判断当前字符是不是数字字符,如果不是,直接报错,并退出。
标紫色的两句表示将连续的数字 (包括小数点) 字符全部读完。

中间打蓝色的那一段,功能是将数字,字符和下划线组成的一个串也一起读进去,不过这个串是跟在数字后面的一个串。
然后,记录一个”结尾。

buffreplace(ls, ‘.’, ls->decpoint)这一句的主要意图是为了本地化。因为在有些国家,小数点不是用.号表示的,比如说用,表示,为了适应,就用当地的decpoint来替换此时的.。
具体转换的功能是在luaO_str2d中完成的。

luaO_str2d功能是将一个buff中的内容(这个时候,只存了一个数字,buff中存的字符串只有一个)转换成数字(含十六进制处理),并且存入lua_Number结构体中,在上例中就是把转换后的结果存入seminfo->r中。

01int luaO_str2d (const char *s, lua_Number *result) {
02  char *endptr;
03  *result = lua_str2number(s, &endptr);
04  if (endptr == s) return 0;  /* conversion failed */
05  if (*endptr == 'x' || *endptr == 'X')  /* maybe an hexadecimal constant? */
06    *result = cast_num(strtoul(s, &endptr, 16));
07  if (*endptr == '\0'return 1;  /* most common case */
08  while (isspace(cast(unsigned char, *endptr))) endptr++;
09  if (*endptr != '\0'return 0;  /* invalid trailing characters? */
10  return 1;
11}

下面来看看SemInfo的定义:

1typedef union {
2  lua_Number r;
3  TString *ts;
4} SemInfo;  /* semantics information */

从名字上理解,这个结构记录的是语义信息,它是联合体。里面有两个成员,一个数字,一个字符串。其实语言本身的语义也就这两种。

01static int skip_sep (LexState *ls) {
02  int count = 0;
03  int s = ls->current;
04  lua_assert(s == '[' || s == ']');
05  save_and_next(ls);
06  while (ls->current == '=') {
07    save_and_next(ls);
08    count++;
09  }
10  return (ls->current == s) ? count : (-count) - 1;
11}
12#define lua_assert(x)  ((void)0)

skip_sep的功能就是跳过[====[及]====]中间的那些等号。lua_assert(s == ‘[' || s == ']‘)的意思就是只要s不是[]中的一个,就报错并返回。因为[[]]中括号的正反两边都是配对的,所以有ls->current == s。整个函数返回括号中间的等号个数,以表示此为几级注释,否则返回一个负值(干什么用?)。

lua_assert被定义为 (void)0,实在不知道是什么意思?看其出现的位置难道是括号的表达式值非零的话就往下走,为0的话就退出此函数?

1static void read_long_string (LexState *ls, SemInfo *seminfo, int sep) {
2        ......
3}
4static void read_string (LexState *ls, int del, SemInfo *seminfo) {
5        ......
6}

read_long_string的功能是读取使用[[ ]]括起来的多行字符串。其中,处理了括号中间有等号的情况,并且还处理了出现嵌套字符串的情况,其中考虑了COMPAT宏定义。多行字符串里面忽略了转义的情况。

在read_long_string的最后,使用

1seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + (2 + sep),
2                                     luaZ_bufflen(ls->buff) - 2*(2 + sep));

实现字符串的创建和记录,至于里面的数值的大小为什么要减个2, 此处还没看懂。

1luaX_newstring定义如下,
2TString *luaX_newstring (LexState *ls, const char *str,size_t l) {
3  lua_State *L = ls->L;
4  TString *ts = luaS_newlstr(L, str, l);
5  TValue *o = luaH_setstr(L, ls->fs->h, ts);  /* entry for `str' */
6  if (ttisnil(o))
7    setbvalue(o, 1);  /* make sure `str' will not be collected */
8  return ts;
9}

其中,luaS_newlstr接收外部的str创建一个lua内部的字符串(换种存储,并与内部的垃圾回收器结合起来),而luaH_setstr则将这个lua string,加入到全局表中去,并给其分配索引值。这里面的内容相当复杂,涉及到lua中最深层次的理论和概念了。这时暂时先不讲。 ttisnil函数检测值是不是nil。

read_string功能是读取使用”或’括起来的单行字符串。其中处理了转义字符的情况。在最后,使用了

1seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1,
2                                   luaZ_bufflen(ls->buff) - 2);

实现字符串的创建和记录。 这里只有数字和字符串的读取函数,原因就在于语义上,只区分数字和字符串。保留字,变量标识符,运算符,等都归到字符串里面去。

下面详细讲解主解析函数llex的实现:

1static int llex (LexState *ls, SemInfo *seminfo) {
2        ......
3}

本函数调用一次,返回一个完整的token(记号)。

进入这个函数后,是一个for(;;)无限循环,表示尽可能长地搜索一个记号单元。对于读取的每个字符,会用switch..case进行比较分析。字符是一个字符一个字符读取的。
首先,会判断是否是\n, \r,如果是的话,就用inclinenumber函数把状态机里面的行号值增1;

接着判断是否是’-'字符,因为在LUA中,-字符有可以表示减号,也有可能是注释号的一部分。于是还要往下找一个字符,进行双重判断。如果第二个字符不是’-',那么这趟判断就返回一个’-'号。如果是-号,就表示是一个注释。再接着判断下一个字符。因为同样为注释号,也有短注释与长注释之区分。如果第三个字符为[号,就进行长注释的处理(含错误情况处理)。如果不是[号,就按短注释处理。

接着,会判断是否是'['号,因为在LUA中,记号第一个字符为[的话,即可以是长字符串的开头,也可能是数组引用的开头。于是会利用skip_sep函数判断后面是否有一长串的等号,并根据其返回值来区分到底是长字符串(返回值大于等于0)还是数组下标的引用号(返回值小于0)。

接着,会判断是否是'='号,因为在LUA中,记号第一个字符如果是=的话,既有可能表示赋值号,也有可能表示等于号。于是再读入一个字符判断,并根据返回结果分别返回。如果是一个字符的赋值号,就返回'=',如果是两个字符的等于号,就返回一个编码TK_EQ,被解释为一个大于256的值;

接着,会类似判断 ~号,不再赘述。

接着,会判断" ' 单引号和双引号。并会读取它们所括起来的字符串,返回一个TK_STRING值;

接着,会判断.号,在LUA中,.有很多种意义 1。数字的开头;2。表成员连接符;3。字符串连接符的一部分;4。变参量符号的一部分。因此,会最多读取3个字符进行判断,并分别返回TD_DOTS(变参量符号),TK_CONCAT(字符串连接符),.(表成员连接符),TK_NUMBER(数字)。

接着,会判断是否是EOZ,如果是的话,就返回标志TK_EOS;

上面判断的情况,还不是可能会遇见的所有情况。或者说,最一般的情况都还没有判断的。接下来就在default分支中判断。

下面判断,是否是空白字符,并且确认不是\r及\n。是的话,就忽略,直接到下一个记号去;

接着,判断是否是数字打头的记号,是的话,就通过read_numeral读取完整的数字记号,并返回TK_NUMBER;

接着,会判断是否是由字母和下划线开头的记号。如果是,就读取完整的以字母,数字和下划线为集合的记号,并且创建一个TString,判断ts->tsv.reserved是否大于0,如果大于零的话,就表示读入的这个记号是一个保留字,直接返回代表这个保留字的一个序号值(大于256)。非常巧妙。而如果不是保留字的话,就将新建的字符串存入seminfo->ts中,返回TK_NAME,表示新获取到了一个“标识符”。

还有一种情况没有考虑到,就是单个的字符+-*/等。由于它们都属于ASCII可显示字符,在1~256以内,所以直接返回此字符。

到此,牛掰的llex就分析完了,记住,运行一次llex,就会返回一个完整记号。设计得真的很简洁,思路很清晰。

另外还有两个函数

01void luaX_next (LexState *ls) {
02   ls->lastline = ls->linenumber;
03  if (ls->lookahead.token != TK_EOS) {  /* is there a look-ahead token? */
04    ls->t = ls->lookahead;  /* use this one */
05    ls->lookahead.token = TK_EOS;  /* and discharge it */
06  }
07  else
08ls->t.token = llex(ls, &ls->t.seminfo);  /* read next token */
09}
10void luaX_lookahead (LexState *ls) {
11  lua_assert(ls->lookahead.token == TK_EOS);
12  ls->lookahead.token = llex(ls, &ls->lookahead.seminfo);
13}

TK_EOS指的是当前LexState状态机中的字符串缓冲中字符串的(也可以说是文件内容)的结束符。ls->lookahead.token是token的编号。前面说过,小于256的,就是单字符token,大于256的,由于不能用一个字符来表示,所以就定义成一个宏名,使token.token在够存的时候就直接存储,不够存的时候就指示类型。这也是为什么各处反馈的token的类型要int的原因。因为只有int才能包涵这么多不同类型。Token的定义如下:

1typedef struct Token {
2  int token;
3  SemInfo seminfo;
4} Token;

因为在前面,用的一个无限循环for(;;), 所以通过一个TK_EOZ来标识分析结束,返回的时候TK_EOZ就替换成TK_EOS。所以碰到TK_EOS就说明当前字符串解析结束了。luaX_next的一个其它函数中要调用llex的一个封装函数,如果llex中已经解析了一个token了,外界需要的话,就用这个已有的token。而如果没有,就调用llex重新读取解析一个token。LexState里面定义了一个当前的token,定义了一个ahead token,要注意,这是编译器中词法语法分析的常用方法。

luaX_lookahead的作用是为ahead token填充一个新的token(向前看一下嘛……)。
在最后,列出llex文件中对外导出的函数名集合。

1LUAI_FUNC void luaX_init (lua_State *L);
2LUAI_FUNC void luaX_setinput (lua_State *L, LexState *ls, ZIO *z,
3                              TString *source);
4LUAI_FUNC TString *luaX_newstring (LexState *ls, constchar *str, size_t l);
5LUAI_FUNC void luaX_next (LexState *ls);
6LUAI_FUNC void luaX_lookahead (LexState *ls);
7LUAI_FUNC void luaX_lexerror (LexState *ls, const char*msg, int token);
8LUAI_FUNC void luaX_syntaxerror (LexState *ls, const char*s);
9LUAI_FUNC const char *luaX_token2str (LexState *ls, inttoken);

至此,lua源代码的词法分析就差不多了,下面进行语法分析。