无论是 lua_dostring 或者是 lua_dofile,都调用了语法分析 lua_parse。
在 lua 里面语法分析器是用 yacc 生成的,就是y.tab.c 文件,lua.stx 就是 yacc 的输入文件。
这里说的东西基本上编译原理书里都有介绍,如需要进一步了解,请自行参阅。
在说语法分析之前,说下词法分析。
lua 的词法分析是手写的,手写的词法分析性能比较好,这个在 lua1.1 自带的文档里有说明,
文件名 lua.ps, 第 8 页 (或者 www.lua.org/semish94.html 的 Implementation 节 )
,手写的词法分析器性能提高了 2 倍。
(文件内部的标题为 :The design and implementation of a language for extending applications)。
词法分析的输入就是从 lua_setinput 过来的那个 Input 函数指针。
一些保留字做特殊处理,reserved 数组里存在着所有的保留字。
词法分析的主要函数为 yylex,它由语法分析调用,每次返回一个 token。它内部实现是一个大的 switch case。注意,当 token 的类型为 NAME 时,它要先调用方法 findReserved 检查一下其是否是保留字,如果是的话, 直接返回保留字 token。
token 分为两种,一种只有类型,一种有值和类型。
如果 token 的类型够以描述它自己的话,就不需要值,比如保留字,比较运算符,只有类型便足以识别它自己。
而像数值型就需要值,比方一个数为 789,词法分析会这样返回它:返回一个 NUMBER,它的值为 789。至于语法分析怎么用,是语法分析自己的事。
可以参考它的 Lex 输入文件(虽然该输入文件在 Lua1.1 里没有使用),就是在 src/yacc 目录里的 lua.lex。更容易看出 lex.c 里的那个 swith case 为什么要那样写。
接下来看下语法分析 lua_parse :
y.tab.c 文件
/* ** Parse LUA code and execute global statement. ** Return 0 on success or 1 on error. */ int lua_parse (void) { Byte *init = initcode = (Byte *) calloc(GAPCODE, sizeof(Byte)); maincode = 0; maxmain = GAPCODE; if (init == NULL) { lua_error("not enough memory"); return 1; } err = 0; if (yyparse () || (err==1)) return 1; initcode[maincode++] = HALT; init = initcode; #if LISTING PrintCode(init,init+maincode); #endif if (lua_execute (init)) return 1; free(init); return 0; }
可以看到在语法分析 yyparse (这个就是由 yacc 生成的语法分析器的入口)之后,调用虚拟机执行指令 lua_execute。
PrintCode 打印字节码,它由 LISTING 宏控制。如果要打印字节码,需要保证 PrintCode 被正常的调用。具体修改请参见打印字节码那篇。
因为这里是由 yacc 生成的,关于 yacc 的内容不是我关注的重点,不再详细说明,感兴趣的可以自行查找相关资料。
我比较喜欢那个从 lua3.1 版本开始的手写的递归下降语法分析,有可能的话到那时再分析下语法分析过程吧。
虽然说 lua1.1 是栈式虚拟机,但是,从他生成的指令却不能保存下来直接使用,而必须从源代码解析后直接生成。可以认为它里面没有编译这一步,一边解释一边执行。到版本 Lua2.4 才可以保存生成的指令,有了独立的编译器,数据和代码的全部信息都保存到编译生成的字节码里。
语法分析的结果是一个字节码指令数组,也就是之前我们看到的字节码展示。
到此,字节码已经生成,编译器前端的工作算是做完了。