构建Lua解释器Part5,对Lua解释器进行了整体介绍,并且以一个hello world程序为例子,给读者一个初步的概念。通过那一篇,我们知道了编译器至少要包括词法分析其和语法分析器,而本篇,我将集中时间和精力,用来介绍和讲解Lua词法分析器的设计与实现,实际上,它是对Part5词法分析器部分的一个补充。本文所指的词法分析器,是参照Lua-5.3这个版本的源码,并且亲自动手实现和测试过,它也已经被整合到dummylua这个工程中,欢迎大家star。由于整个词法分析是我自己重新实现,因此不会在所有的细节上和官方lua保持一致,最后由于本人水平有限,如有写的不正确的地方,欢迎大家批评指正。此外,我已经建了一个qq群(QQ:185017593),有兴趣参与技术讨论的同学可以加进来。
词法分析器简介
首先我要对词法分析器作一个简短的介绍,所谓的词法分析器,其实就是对输入的内容(文本、字符串等)进行词法分析的模块。英文维基百科对词法分析的解释如下[1]:
In computer science, lexical analysis, lexing or tokenization is the process of converting a sequence of characters (such as in a computer program or web page) into a sequence of tokens (strings with an assigned and thus identified meaning). A program that performs lexical analysis may be termed a lexer, tokenizer,[1] or scanner, although scanner is also a term for the first stage of a lexer. A lexer is generally combined with a parser, which together analyze the syntax of programming languages, web pages, and so forth.
总而言之,词法分析就是将一串字符,转化为一串token,而词法分析器就是执行这一过程的逻辑模块。
在编译器中,什么是token?token是一门语言中,能够被识别的最小单元。现在来看一个例子,假设我么有一个文件,其内部的内容如下所示:
name "hello" 2020.
现在要对这个文本进行词法分析,首先我们要做的是,将文本字符加载到词法分析器的缓存中。在完成文本加载以后,我们需要从中,一个一个地获取有效token,而获取token的操作,是通过词法分析器里的一个函数来实现,我们可以假设它叫next函数。我们通过调用next函数若干次,得到以下结果
- 我们通过调用next函数,词法分析器返回第一个有效被识别的token,这个token就是name,它是一个标识符,能够表示变量。
- 然后我们再次调用next函数时,能够获取第二个token,这个token是一个字符串“hello”。
- 当我们第三次调用next函数时,则获取了第三个token,这个token是个数值。
- 当我们第四次调用next函数时,获取一个ASCII符号”.”
- 当我们第五次调用next函数时,获取文本文书标志EOF
从上面的例子中,我们可以看到一些有效的信息。首先是,词法分析需要和文本或者字符串打交道,如果我们的代码是存放在文本中的,那么词法分析器首先要将文本中的代码,加载到内存中。被加载到内存的文本内容,实际上就是一个字符序列。词法分析器需要对这个字符序列进行进一步的提取工作。我们一次只能获取一个有效的token,获取这个token的函数由词法分析器提供,比如例子里的next函数。其次是,我们所称的token,其实能够表示的内容非常丰富,它可以表示标志符,可以表示字符串,可以表是ASCII字符,可以是表示文本结束的EOF标识。正因为token能够表示的内容相当丰富,因此我们需要对token进行分类。实际上,我们一个token,既要表明它是什么类型的,还要表明自己包含的内容是什么,它的结构,在逻辑上如下所示:
Token +--------+--------+ | Type | Data | +--------+--------+
有些token,只是说明类型是不够的,它还需要存储token的内容,比如我们的标识符,需要将组成标识符的内容的字符串存入token的data域中,接下来,我们通过< Type, Data >的方式来表示一个token,那么我们持续调用next函数,并将其打印,则获得如下内容(Type使用lua中使用的定义):
<TK_NAME, "name"> // TK_NAME表示它是标识符类型 <TK_STRING, "hello"> <TK_INT, 2020> <., null> <TK_EOS, null>
从上面的例子中,我们可以感知,token能够表示的内容丰富,有些token通过type就能表示,有些还需要存储其内容在data域,以供词法分析器使用。
本节对词法分析器的简介,就到此结束,后面将展开lua词法分析器的设计与实现的论述。
词法分析器基本数据结构
本节开始介绍dummylua词法分析器的基本数据结构,dummylua的词法分析器,基本参照lua-5.3.5的设计与实现。首先,我来介绍一下lua的Token数据结构:
// lualexer.h typedef union Seminfo { lua_Number r; lua_Integer i; TString* s; } Seminfo; typedef struct Token { int token; // token enum value Seminfo seminfo; // token info } Token;
Token结构中,包含一个int变量token,还有一个Seminfo结构的变量seminfo。其中token字段代表Token实例的类型,而seminfo则用来保存token对应类型的实际值。
lua的token类型,一部分是直接使用ASCII值,一部分是定义在一个枚举类型中。我们现在来看一下,token一共有哪些类型,我们现在来看一下分类:
- EOF:文件结束符,表示文件结束,意为End Of File,使用单独的枚举值TK_EOS
- 算数:+,-,*,/,%,^,~,&,|,<<,>>。由于<<和>>无法使用单独的字符来表示token的类型,因此他们使用单独的枚举值,TK_SHL和TK_SHR
- 括号:(,),[,],{,}
- 赋值:=
- 比较:>,<,>=,<=,~=,==。由于>=,<=,~=,==无法单独使用,因此他们要使用单独的枚举值,TK_GREATEQ,TK_LESSEQ,TK_NOTEQUAL,TK_EQUAL
- 分隔:,和;
- 字符串:’string’和”string”,字符串类型使用TK_STRING
- 连接符:..,因为单个字符无法表示,因此它也是用单独的枚举值TK_CONCAT
- 数字:数值分为浮点数和整数,浮点数使用TK_FLOAT,而整数使用TK_INT
- 标识符:就是我们常说的identity,通常用来表示变量,这种类型在lua中,统称为TK_NAME
- 保留字:local,nil,true,false,end,then,if,elseif,not,and,or,function等,每个保留字,都是一个token类型,比如local是TK_LOCAL类型,而NIL则是TK_NIL,依次类推
此外,词法分析器,遇到空格,换行符\r\n、\n\r,制表符\t、\v等,是直接跳过,直至获取下一个不需要跳过的字符为止。在dummylua中,对Token的类型定义主要分为两个部分,第一部分是直接通过ASCII值来表示,比如’>‘, ‘<‘, ‘.‘和’,‘等,还有一部分通过枚举值来定义,我们现在来看一下枚举值的定义有哪些:
// lualexer.h // 1~256 should reserve for ASCII character token enum RESERVED { /* terminal token donated by reserved word */ TK_LOCAL = FIRST_REVERSED, TK_NIL, TK_TRUE, TK_FALSE, TK_END, TK_THEN, TK_IF, TK_ELSEIF, TK_NOT, TK_AND, TK_OR, TK_FUNCTION, /* other token */ TK_STRING, TK_NAME, TK_FLOAT, TK_INT, TK_NOTEQUAL, TK_EQUAL, TK_GREATEREQUAL, TK_LESSEQUAL, TK_SHL, TK_SHR, TK_MOD, TK_DOT, TK_VARARG, TK_CONCAT, TK_EOS, };
FIRST_REVERSED的值是257,为什么取257开始呢?由于我们有很多token类型(主要是单个字符就能表示的token)是直接通过ASCII值来表示,为了避免和ASCII的值冲突,因此这里直接从257开始。在众多的类型中,只有几种需要保存值到token实例中,他们分别是TK_NAME、TK_FLOAT、TK_INT和TK_STRING,于是我们的Seminfo结构就派上用场了。Seminfo结构是一个union类型,它包含三个域,一个是lua_Number类型,用于存放浮点型数据,一个是lua_Integer类型,用于存放整型数据,一个是TString用于存放标识符和字符串的值。
现在我们对token结构有了一个初步的认识,接下来要介绍则是词法分析器里要用到的最重要的数据结构,它就是LexState结构,其定义如下所示:
typedef struct LexState { Zio* zio; // 负责从文件中读取、缓存字符,并提供字符的模块 int current; // 从zio实例中,获取的当前需要使用的字符 struct MBuffer* buff; // 保留字本身以及TK_STRING、TK_NAME、TK_FLOAT和TK_INT的值, // 由于不止一个字符组成,因此token在被完全识别之前,读取出来 // 的字符,应当存在buff结构中,当词法分析器攒够一个完整的token // 时,则将其拷贝到Seminfo.s(TK_NAME、TK_STRING类型和保留字) // Seminfo.r(TK_FLOAT类型,string转换成浮点型数值)或Seminfo.i // (TK_INT类型,string转换成整型数值)中 Token t; // current token Token lookahead; // 提前获取的token,如果它存在(不为TK_EOS),那么词法分析器调用 // next函数时,它的值直接被获取。 int linenumber; // 代码的行号 struct Dyndata* dyd; // 语法分析过程中,存放local变量信息的结构 struct FuncState* fs; // 语法分析器数据实例 lua_State* L; // lua vm实例 TString* source; // 正在进行编译的源码文件名称 TString* env; // 一般是_ENV struct Table* h; // 常量缓存表,用于缓存lua代码中的常量,加快编译时的常量查找 } LexState;
当我们的编译模块,要对一个文本里的代码进行编译时,首先会创建一个LexState的数据实例。词法分析器的工作,首先是要将代码文件,加载到内存中,被加载到内存中的代码,是一个字符串。词法分析器要做的第二个工作,就是将被加载到内存中的字符串里的字符,一个一个获取出来,并组成合适的token,如果不能组成,则抛出异常。
正如前面所说,词法分析器第一个工作,就是要将代码加载到内存中,作为官方lua-5.3的仿制品,dummylua的词法分析器和官方lua一样,采用一个叫做Zio的结构,负责存放从磁盘中加载出来的代码,其结构如下所示:
// luazio.h typedef char* (*lua_Reader)(struct lua_State* L, void* data, size_t* size); typedef struct LoadF { FILE* f; char buff[BUFSIZE]; // read the file stream into buff int n; // how many char you have read } LoadF; typedef struct Zio { lua_Reader reader; // read buffer to p int n; // the number of unused bytes char* p; // the pointer to buffer void* data; // structure which holds FILE handler struct lua_State* L; } Zio;
与官方lua一样,dummylua的Zio结构,并没有限制使用者,用哪种方式来加载代码到内存中。而具体操作的函数,则是函数指针reader指向的函数,我们只要自定义的函数,符合这个签名,就能够被词法分析器调用,另外Zio的data指针,是作为reader函数的重要参数存在的,它同样可以由用户自己定义。不过lua提供了一个默认的LoadF结构,以及一个getF函数,用于将文件里的代码,加载到内存中,我将在接下来的内容中详细讨论。
现在我们抛开具体的代码实现,通过一个实例,将整个流程串联起来,如图1所示,我们现在要将一个文件里的字节流读出,并识别里面的token。
图1
识别token的流程,也是需要将源码文件里的字符,一个一个获取出来,第一个被获取的字符,将决定它进入哪个token类型的处理分支之中,事实上lua是通过一个叫做zget的宏,获取字符的,这个宏如下所示:
// luazio.h #define zget(z) (((z)->n--) > 0 ? (*(z)->p++) : luaZ_fill(z))
传入这个宏的,是一个Zio结构的数据实例。事实上,我们识别token的逻辑是在一个叫做llex的函数内进行的,这个函数会不断读取新的字符,并且判断应该生成哪个token,下面的伪代码展示了这一点:
// lualexer.c static int llex(LexState* ls, Seminfo* s) { ... ls->current = zget(ls->z); switch(ls->current) { ... case '\'': case '\"': { return readstring(ls, s); } break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { return readnumber(ls, s); } break; ... } }
从上面的伪代码我们可以看到,词法分析器识别一个token,需要不断从源码文件中,一个一个读取字符,然后判断它可能是哪种类型的token,最后作相应的处理,比如readstring和readnumber操作,就是把有效字符串识和数值别出来,最后存储在LexState结构的Token类型变量t中。readstring函数和readnumber函数内部也会多次调用zget函数,不断获取新的字符,最后生成token。
回到图1的例子,在词法分析器数据结构实例LexState完成初始化的伊始,我们刚完成初始化的Zio结构实例,会关联一个已经打开的文件,这个文件就是LoadF结构中的FILE*指针f指定,正如图所示,LoadF类型变量的n值为0,这意味着我们未读取buff中任何一个字符。而buff此时也未存储任何一个文件源码中的字符。现在回头来看看Zio数据实例本身,其void*指针,data指向了我们刚刚讨论过的LoadF类型实例,Zio的变量n表示,LoadF结构的buff中,还剩下多少个未读取的字符,char*指针p,应当指向LoadF结构中buff的某个位置,但是现在还是初始化状态,因此它是NULL,至于Zio中的reader函数,主要用于处理从文件中读取字符到LoadF结构的buff中的情况。
回顾一下zget这个宏,当我们第一次调用zget这个宏的时候,它会首先调用一个叫做luaZ_fill的函数来处理,我们先忽略它的具体实现,通过一个情景展示来观察它的逻辑流程。这段逻辑的本质是,因为没有未被读取的字符,因此需要重新到文件里加载。以图1为例,假设BUFFSIZ的值为8,现在我们来看看它的执行步骤:
- 从文件中读取8个字符,到LoadF结构实例中的buff中,此时LoadF中的n值仍然是。
- Zio结构中的n值被赋值为8,指针p被赋值为buff的地址,如图2所示:图2
- 返回p所指向的字符
- p指针自增1,移动到下一个地址,结果如图3所示:图3
在这个阶段之后,每次我们调用zget这个宏,就会返回Zio的变量p所指向的字符,并且自增,同时Zio的变量n自减1,LoadF的n自增1。当Zio的变量n为0时,此时如果再调用zget宏,那么说明LoadF中的buff的字符已经被取完了,因此需要重新从磁盘获取BUFFSIZ个字符,得到图4的结果。
图4
然后和前面概述的一样,返回p所指向的字符,并且p指针自增,LoadF的n值加1,Zio的n值减1。
现在我们来看一下luaZ_fill函数的实现:
// luazio.c int luaZ_fill(Zio* z) { int c = 0; size_t read_size = 0; z->p = (void*)z->reader(z->L, z->data, &read_size); if (read_size > 0) { z->n = (int)read_size; c = (int)(*z->p); z->p++; z->n--; } else { c = EOF; } return c; }
而这里的reader函数,在是在luaaux.c文件里,名称为getF函数:
// luaaux.c static char* getF(struct lua_State* L, void* data, size_t* sz) { LoadF* lf = (LoadF*)data; if (lf->n > 0) { *sz = lf->n; lf->n = 0; } else { *sz = fread(lf->buff, sizeof(char), BUFSIZE, lf->f); lf->n = 0; } return lf->buff; }
这里的逻辑非常清晰,每当Zio结构里,字符被取完的时候,就需要调用luaZ_fill函数,该函数会通过reader函数,获取新的字符缓存,以及缓存的字符数量,然后返回p指针所指向的字符,最后p指针向前移动一位,n变量减1。这里的reader函数,实际上就是getF函数。结合上面描述的流程,要读懂这两段代码并不难。
我们之所以要用到Zio这样的结构,主要的原因是词法分析器,需要从源码文件中,一个一个获取字符,如果每次都要走io,那么效率将会非常低,但是如果每次都将所有的代码加载进来,并且缓存,如果代码中,某个block有词法错误,整个词法分析的流程就会中断,如果文本较大,且位于开头的token识别起来就有问题,那么花费时间在io处理上就是极大的浪费,因此这里采用的策略就是,一部分一部分地加载到代码缓存中。
到目前为止,我就完成了lua词法分析器的基本数据结构的说明了,接下来将会对lua词法分析器的常用接口,初始化流程,和token识别流程进行说明。
词法分析器的接口
在了解了词法分析器的数据结构以后,我们现在来看看,词法分析器一共有哪些主要的接口,现在将接口展示如下所示:
// 模块初始化 void luaX_init(struct lua_State* L); // 词法分析器实例初始化 void luaX_setinput(struct lua_State* L, LexState* ls, Zio* z, struct MBuffer* buffer, struct Dyndata* dyd, TString* source, TString* env); // 提前获取下一个token的信息,并暂存 int luaX_lookahead(struct lua_State* L, LexState* ls); // 获取下一个token,如果有lookahead暂存的token,就直接获取,否则通过 // llex函数获取下一个token int luaX_next(struct lua_State* L, LexState* ls); // 抛出词法分析错误 void luaX_syntaxerror(struct lua_State* L, LexState* ls, const char* error_text);
接口非常简洁,无非就是初始化和获取下一个token的接口。
初始化流程
在完成了lua词法分析器的讨论以后,我们现在来看一下词法分析器的初始化操作。词法分析器首要要做的初始化处理,即是对保留字进行内部化处理,由于保留字全部是短字符串,因此TString实例的extra字段不为0,并且值为对应token类型的枚举值。这样做的目的是,由于我们经过内部化的短字符串会被缓存起来,因此当我们识别到保留字的token的时候,会从缓存中直接获取字符串TString实例,通过这个TString实例的extra字段,我们可以直接获得其token类别的枚举值,方便我们在语法分析时的处理。
我们现在来看一下,lua词法分析器的初始化逻辑:
// lualexer.c // the sequence must be the same as enum RESERVED const char* luaX_tokens[] = { "local", "nil", "true", "false", "end", "then", "if", "elseif", "not", "and", "or", "function" }; void luaX_init(struct lua_State* L) { TString* env = luaS_newliteral(L, LUA_ENV); luaC_fix(L, obj2gco(env)); for (int i = 0; i < NUM_RESERVED; i++) { TString* reserved = luaS_newliteral(L, luaX_tokens[i]); luaC_fix(L, obj2gco(reserved)); reserved->extra = i + FIRST_REVERSED; } }
我们可以看到,初始化阶段,会创建保留字的字符串,由于保留字的字符数均少于40字节,因此他们属于短字符串,并且能够内部化。init函数对这些字符串,进行了脱离gc管理的操作,其本质就是从allgc列表中,移到fixgc列表中,避免这些保留字字符串被gc掉。回顾一下Part3,我们可以知道,当TString为短字符串时,extra字段不为0,则表示不能被gc,并且这个值现在被赋值为保留字类型的枚举值。我们的词法分析器初始化操作,主要是在创建lua_State实例的函数lua_newstate里调用的。
我们在加载一段lua代码,并且开始编译的时候,需要创建一个LexState的结构,并且初始化它,初始化它的操作,在luaY_parser函数中,这个函数主要用来编译lua代码的:
// luaparser.c LClosure* luaY_parser(struct lua_State* L, Zio* zio, MBuffer* buffer, Dyndata* dyd, const char* name) { FuncState fs; LexState ls; luaX_setinput(L, &ls, zio, buffer, dyd, luaS_newliteral(L, name), luaS_newliteral(L, LUA_ENV)); ls.current = zget(ls.zio); LClosure* closure = luaF_newLclosure(L, 1); closure->p = fs.p = luaF_newproto(L); setlclvalue(L->top, closure); increase_top(L); ptrdiff_t save_top = savestack(L, L->top); ls.h = luaH_new(L); setgco(L->top, obj2gco(ls.h)); increase_top(L); mainfunc(L, &ls, &fs); L->top = restorestack(L, save_top); return closure; }
上面调用luaX_setinput函数,则是对词法分析器实例进行初始化操作:
// lualexer.c void luaX_setinput(struct lua_State* L, LexState* ls, Zio* z, struct MBuffer* buffer, struct Dyndata* dyd, TString* source, TString* env) { ls->L = L; ls->source = source; ls->env = env; ls->current = 0; ls->buff = buffer; ls->dyd = dyd; ls->env = env; ls->fs = NULL; ls->linenumber = 1; ls->t.token = 0; ls->t.seminfo.i = 0; ls->zio = z; }
这里的初始化操作很简单,主要是做一些赋值操作,前面介绍数据结构的时候,有对LexState结构进行说明,这里不再赘述。
Token识别流程
词法分析器,将在语法分析器内被使用,语法分析器要调用一个新的token,需要调用luaX_next来实现,我们现在来看看luaX_next函数的定义:
// lualexer.c int luaX_next(struct lua_State* L, LexState* ls) { if (ls->lookahead.token != TK_EOS) { ls->t.token = ls->lookahead.token; ls->lookahead.token = TK_EOS; return ls->t.token; } ls->t.token = llex(ls, &ls->t.seminfo); return ls->t.token; }
我们可以看到,如果lookhead里有存token的信息,那么就直接返回给LexState的token变量t,否则直接调用llex函数来识别新的token:
// lualexer.h #define next(ls) (ls->current = zget(ls->zio)) // lualexer.c static int llex(LexState* ls, Seminfo* seminfo) { for (;;) { luaZ_resetbuffer(ls); switch (ls->current) { // new line case '\n': case '\r': { inclinenumber(ls); } break; // skip spaces case ' ': case '\t': case '\v': { next(ls); } break; case '-': { next(ls); // this line is comment if (ls->current == '-') { while (!currIsNewLine(ls) && ls->current != EOF) next(ls); } else { return '-'; } } break; case EOF:{ next(ls); return TK_EOS; } case '+': { next(ls); return '+'; } case '*': { next(ls); return '*'; } case '/': { next(ls); return '/'; } case '~': { next(ls); // not equal if (ls->current == '=') { next(ls); return TK_NOTEQUAL; } else { return '~'; } } case '%': { next(ls); return TK_MOD; } case '.': { next(ls); if (isdigit(ls->current)) { return str2number(ls, true); } else if (ls->current == '.') { next(ls); // the '...' means vararg if (ls->current == '.') { next(ls); return TK_VARARG; } // the '..' means concat else { return TK_CONCAT; } } else { return '.'; } } case '"': case '\'': { // process string return read_string(ls, ls->current, &ls->t.seminfo); } case '(': { next(ls); return '('; } case ')': { next(ls); return ')'; } case '[': { next(ls); return '['; } case ']': { next(ls); return ']'; } case '{': { next(ls); return '{'; } case '}': { next(ls); return '}'; } case '>': { next(ls); if (ls->current == '=') { next(ls); return TK_GREATEREQUAL; } else if (ls->current == '>') { next(ls); return TK_SHR; } else { return '>'; } } case '<':{ next(ls); if (ls->current == '=') { next(ls); return TK_LESSEQUAL; } else if (ls->current == '<') { next(ls); return TK_SHL; } else { return '<'; } } case '=': { next(ls); if (ls->current == '=') { next(ls); return TK_EQUAL; } else { return '='; } } case ',': { next(ls); return ','; } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { if (ls->current == '0') { save_and_next(ls->L, ls, ls->current); if (ls->current == 'x' || ls->current == 'X') { save_and_next(ls->L, ls, ls->current); return str2hex(ls); } else { return str2number(ls, false); } } return str2number(ls, false); } default: { // TK_NAME or reserved name if (isalpha(ls->current) || ls->current == '_') { while (isalpha(ls->current) || ls->current == '_' || isdigit(ls->current)) { save_and_next(ls->L, ls, ls->current); } save(ls->L, ls, '\0'); TString* s = luaS_newlstr(ls->L, ls->buff->buffer, strlen(ls->buff->buffer)); if (s->extra > 0) { return s->extra; } else { ls->t.seminfo.s = s; return TK_NAME; } } else { // single char int c = ls->current; next(ls); return c; } } } } return TK_EOS; }
从上面的代码中,我们不难看出,对于单个ASCII字符就能表示的token,llex函数基本上是直接返回的,对于那些需要多个字符组成的token,llex函数,则是返回在枚举类型里定义的枚举值,比如TK_LESSEQUAL,TK_GREATEREQ等。这些都比较简单,不过需要进行特殊处理的类型,则是TK_INT,TK_FLOAT,TK_STRING和TK_NAME。
如果token首字母是0~9,那么判定分支则走入识别数值的分支之中,紧接着判断第二个字母是否是x或者是X,如果是,那么判定它是十六进制的数值,我们通过str2hex函数来进行处理:
// lualexer.c 1 #define save_and_next(L, ls, c) save(L, ls, c); ls->current = next(ls) 2 #define currIsNewLine(ls) (ls->current == '\n' || ls->current == '\r') 3 static int str2hex(LexState* ls) { 4 int num_part_count = 0; 5 while (is_hex_digit(ls->current)) { 6 save_and_next(ls->L, ls, ls->current); 7 num_part_count++; 8 } 9 save(ls->L, ls, '\0'); 10 11 if (num_part_count <= 0) { 12 LUA_ERROR(ls->L, "malformed number near '0x'"); 13 luaD_throw(ls->L, LUA_ERRLEXER); 14 } 15 16 ls->t.seminfo.i = strtoll(ls->buff->buffer, NULL, 0); 17 return TK_INT; 18 }
上述代码的save操作,则是将ls->current所存储的字符,存入到LexState结构MBuffer类型的buff缓存中。词法分析器会不断调next(内部调用了zget宏)函数,并判断它是不是16进制的字符,如果是,则存储到buff缓存中,直到第一个不是16进制字符为止,我们则需将buff缓存里存储的字符,通过strtoll函数,转化为一个整型变量,并存储在ls->t这个表示当前token的变量之中,同时它的类型被设置为TK_INT。
如果token首字母是0~9,并且紧随其后的第二个字符不是’x‘或者’X‘时,那么我们则进入到识别整型数值或者浮点型数值的逻辑之中了,此时调用str2number函数来进行操作:
// lualexer.c static int str2number(LexState* ls, bool has_dot) { if (has_dot) { save(ls->L, ls, '0'); save(ls->L, ls, '.'); } while (isdigit(ls->current) || ls->current == '.') { if (ls->current == '.') { if (has_dot) { LUA_ERROR(ls->L, "unknow number"); luaD_throw(ls->L, LUA_ERRLEXER); } has_dot = true; } save_and_next(ls->L, ls, ls->current); } save(ls->L, ls, '\0'); if (has_dot) { ls->t.seminfo.r = atof(ls->buff->buffer); return TK_FLOAT; } else { ls->t.seminfo.i = atoll(ls->buff->buffer); return TK_INT; } }
这里同样会不断将字符存储到MBuffer类型的buff变量中,直至有一个字符不是0~9的字符或者’.‘为止,而后会进入到讲buff中的字符串转为数值的操作。函数会判断buff中是否包含’.‘,如果有且只有1个,那么转化为数值的操作则是将字符串转化为浮点型数据,如果没有,则进入到将字符串转化为整型数据的操作。
识别字符串则简单得多,如果首字母是’\‘‘或者’\“‘,那么则进入到字符串识别流程之中,这些操作则由read_string函数来执行:
// lualexer.c static int read_string(LexState* ls, int delimiter, Seminfo* seminfo) { next(ls); while (ls->current != delimiter) { int c = 0; switch (ls->current) { case '\n': case '\r': case EOF: { LUA_ERROR(ls->L, "uncomplete string"); luaD_throw(ls->L, LUA_ERRLEXER); } break; case '\\': { next(ls); switch (ls->current) { case 't':{ c = '\t'; goto save_escape_sequence; } case 'v':{ c = '\v'; goto save_escape_sequence; } case 'a':{ c = '\a'; goto save_escape_sequence; } case 'b':{ c = '\b'; goto save_escape_sequence; } case 'f':{ c = '\f'; goto save_escape_sequence; } case 'n':{ c = '\n'; goto save_escape_sequence; } case 'r': { c = '\r'; save_escape_sequence: save_and_next(ls->L, ls, c); } break; default: { save(ls->L, ls, '\\'); save_and_next(ls->L, ls, ls->current); } break; } } default: { save_and_next(ls->L, ls, ls->current); } break; } } save(ls->L, ls, '\0'); next(ls); seminfo->s = luaS_newliteral(ls->L, ls->buff->buffer); return TK_STRING; }
整个逻辑也很简单,只要没有遇到delimiter字符(’\‘‘或者’\“‘),除了一些转义字符会做特殊处理(两个字符合成一个字符),其他的字符都会直接存到MBuffer类型的buff数组中,直至遇到delimiter字符,此时会根据buff中的字符串,去生成一个TString类型的字符串,并存到token的seminfo变量中,这里需要注意的是,delimiter字符本身不存入buff中。
识别标识符(identify)也是简单的多,其开头必须是alphabet字符,或者是’_‘,然后接下来的字符,只要是alphabet、下划线或者数字的其中一种,它都会被存储到MBuffer类型的buff变量中,直至条件不成立,此时会将buff传入luaS_newlstr函数中去生成一个TString类型的字符串,如果字符串是个保留字,那么返回extra字段的值,这表示它的token类型的值,由于保留字是特定的,因此我们只需要extra所代表的值(token类型的枚举值)即可。如果字符串不是保留字,那么新生成的TString字符串则会被保存到seminfo变量中,并且token类型为TK_NAME。
一个测试用例
在文章的最后,我通过一个测试用例来展现词法分析器的分析结果,对part06.lua脚本中的代码,进行解析,获得的打印,则在下方展示。测试代码在p6_test.c中。
-- part06.lua local function print_test() local str = "hello world" print("hello world") end print_test() local number = 0.123 local number2 = .456 local tbl = {} tbl["key"] = "value" .. "value2" function print_r(...) return ... end tbl.key -- This is comment tbl.sum = 100 + 200.0 - 10 * 12 / 13 % (1+2) if tbl.sum ~= 100 then tbl.sum = tbl.sum << 2 elseif tbl.sum == 200 then tbl.sum = tbl.sum >> 2 elseif tbl.sum > 1 then elseif tbl.sum < 2 then elseif tbl.sum >= 3 then elseif tbl.sum <= 4 then tbl.sum = nil end tbl.true = true tbl.false = false local a, b = 11, 22
保留字会在开头加上”RESERVED:“的开头,而单个ASCII字符就能展示的token,则是直接打印,其他的会打印枚举定义。
? REVERSED: local REVERSED: function TK_NAME print_test ( ) REVERSED: local TK_NAME str = TK_STRING hello world TK_NAME print ( TK_STRING hello world ) REVERSED: end TK_NAME print_test ( ) REVERSED: local TK_NAME number = TK_FLOAT 0.123000 REVERSED: local TK_NAME number2 = TK_FLOAT 0.456000 REVERSED: local TK_NAME tbl = { } TK_NAME tbl [ TK_STRING key ] = TK_STRING value TK_CONCAT .. TK_STRING value2 REVERSED: function TK_NAME print_r ( TK_VARARG ... ) TK_NAME return TK_VARARG ... REVERSED: end TK_NAME tbl . TK_NAME key TK_NAME tbl . TK_NAME sum = TK_INT 100 + TK_FLOAT 200.000000 - TK_INT 10 * TK_INT 12 / TK_INT 13 TK_MOD % ( TK_INT 1 + TK_INT 2 ) REVERSED: if TK_NAME tbl . TK_NAME sum TK_NOEQUAL ~= TK_INT 100 REVERSED: then TK_NAME tbl . TK_NAME sum = TK_NAME tbl . TK_NAME sum TK_SHL << TK_INT 2 REVERSED: elseif TK_NAME tbl . TK_NAME sum TK_EQUAL == TK_INT 200 REVERSED: then TK_NAME tbl . TK_NAME sum = TK_NAME tbl . TK_NAME sum TK_SHR >> TK_INT 2 REVERSED: elseif TK_NAME tbl . TK_NAME sum > TK_INT 1 REVERSED: then REVERSED: elseif TK_NAME tbl . TK_NAME sum < TK_INT 2 REVERSED: then REVERSED: elseif TK_NAME tbl . TK_NAME sum TK_GREATEREQUAL >= TK_INT 3 REVERSED: then REVERSED: elseif TK_NAME tbl . TK_NAME sum TK_LESSEQUAL <= TK_INT 4 REVERSED: then TK_NAME tbl . TK_NAME sum = REVERSED: nil REVERSED: end TK_NAME tbl . REVERSED: true = REVERSED: true TK_NAME tbl . REVERSED: false = REVERSED: false REVERSED: local TK_NAME a , TK_NAME b = TK_INT 11 , TK_INT 22 total linenumber = 35请按任意键继续. . .
结束语
本章节,我用一些篇幅讨论了词法分析器的设计与实现,实际上part5已经有对词法分析器进行一些必要的概述了,我这里是对part5进行一些补充,目的是为了能够让读者对lua的词法分析器有更深刻的认识。下一篇开始,我将开始论述lua语法分析器的设计与实现。