上 构建Lua解释器Part7:构建完整的语法分析器( 四 )


// luaobject.htypedef struct Proto {CommonHeader;// gc部分int is_vararg;// 是否可变参数的函数int nparam;// 参数个数Instruction* code;// 指令列表 , Instruction本质是int型int sizecode;// code列表的大小TValue* k;// 常量表int sizek;// 常量表的大小LocVar* locvars;// local变量列表int sizelocvar;// local变量列表的大小Upvaldesc* upvalues; // upvalue列表int sizeupvalues;// upvalue列表的大小struct Proto** p;// Proto结构列表int sizep;// Proto结构列表的大小TString* source;// 源代码文件struct GCObject* gclist;// gc列表int maxstacksize;// 经过编译后 , 需要栈的最大大小} Proto;
早在Part5的时候 , 我就有讲解过 , 一个lua脚本内部的代码本质上就是一个chunk , 在chunk在编译后 , 本质上也是一个对象 , 一般一个对象对应一个Proto实例 , 在编译过程中 , 一个则对应一个结构 。结构实例 , 只在编译期存在 。我将在本章后续的内容里 , 阐述编译流程 , 让读者能够真正理解 。
本章所涉及的虚拟机指令
本节 , 将对本章涉及到的虚拟机指令进行说明 , 在开始阅读指令说明之前 , 如果你忘记了lua虚拟机指令的相关概念 , 那么我强烈建议读者复习一下Part5的【Lua的指令集编码方式】和【Lua5.3中的指令集】两个小节 , 本章涉及的指令如下所示:
上面展示了本章涉及的虚拟机指令 , 有些指令光看注释就能明白它的作用 , 因此不作更多的文字赘述 。
编译逻辑与EBNF的关联
现在我将介绍如何通过EBNF进行派生演变() , 然后说明的问题 , 接着为了解决这些问题会引入Parse Tree , 后面还会简要介绍一下Tree 。再往后 , 就会根据EBNF范式来实现的逻辑 , 并结合一些实例进行讲解 。
我们先以foo函数的调用为例子 , 看看派生演变的操作是怎样的 , 首先 , 很显然 , 在对foo函数这段代码进行编译时 , 我们很自然地进入到了的分支之中 , 那么它用EBNF范式的完整的演变流程如下所示:
1exprstat2func3suffixedexp4primaryexp '(' ')'5TK_NAME '(' ')'6foo'('')'
非常简单的编译流程 , 这种方式 , 我们称之为 , 但是有个问题 , 就是演变的顺序是随意的 , 比如下面一段代码 , 我们可以有两种演变方式:
a = 1
演变方式1:
1exprstat2suffixedexp=explist3primaryexp=explist4TK_NAME=explist5a=explist6a=expr7a=simpleexp8a=TK_INT9a=1
演变方式2:
1exprstat2suffixedexp=explist3suffixedexp=expr4suffixedexp=simpleexp5suffixedexp=TK_INT6suffixedexp=17primaryexp=18TK_NAME=19a=1
我们已经展示了两种演变方式 , 但实际上 , 演变的方式还有很多种 , 这里不一一展示 , 这里也说明了 , EBNF范式可以描述一门语言的语法 , 但是无法精确描述编译解析的先后顺序 , 为了解决这个问题 , 我们将引入Parse Tree的概念 。什么是Parse Tree呢?其实 , 就是将我们的lua代码 , 代入到图6所示的EBNF范式中 , 然后生成一颗树 。有了Parse Tree , 我们只要指定它的遍历顺序 , 那么一切就都清晰了 , lua编译器使用的是递归下降的方式来编译lua脚本 , 因此这里使用先序遍历更加合适(如果你忘记了树的几种遍历方式 , 可以查看Tree 这篇文章[5]),我们现在来看一下上面所示的例子的Parse Tree是怎样的 , 图7展示了这一点 , 红色数值表示遍历的顺序 。