最近由于某种原因,在考虑实现一套自定义的脚本。正好看到了antlr4,于是就试了一下。使用lua语言作为研究参照, 研究了大约一周的时间,总算弄明白一些事情。
我没有完整的实现lua的执行,目前自己写了大约1400多行代码(大约一半左右为copy的listener接口),运行环境为网页版本的 javascript, 即使用javascript执行lua脚本,实现了lua中的简单的函数定义,调用,变量的各种运算,if_else语句的实现, 使用的测试脚本为grammars-v4中lua的examples的例子代码。
在这个使用过程中,走了不少弯路。
第一个弯路是解析实现的时候使用的是listener模式还是visitor模式
现在官方默认生成的是listener, visitor模式需要手动添加参数才能实现。最初我想使用visitor模式,但发现一个困难在于 函数递归的时候,难以确定上下游的关系,以exp为例, 上层到底是explist还是prefixexp,所以最后用了listener模式。 其实到最后发现使用哪种模式也可以说是自己喜好或者目的如何,只是在最初考虑的时候,由于对antlr4的设计的不理解, 造成了很大的误区。下面的内容也主要围绕listener模式,visitor模式理论上也可以实现类似的效果,只是可能写法有所区别。
第二个弯路是解析节点的管理
无论是listener模式还是visitor模式,都要自己管理语法节点。visitor模式有点像半管理状态,我没有特别的深入研究, 因为我采用了listener模式。其实我本来很看好visitor模式的,但简单测试后发现反而不太好用,或者还是需要自己管理节点, 既然自己管理节点,listener模式反而会好用些。可以生成语法树,然后最后再整个执行语法树。
我不太清楚别人是如何管理语法树的节点的,我使用的是javascript的数组来管理的,javascript似乎没有栈的变量, 而数组正好有个push和pop函数,因此正好可以模拟进栈和出栈。类似下面的例子:
// Enter a parse tree produced by LuaParser#lua_make_functioncall. enterLua_make_functioncall(ctx) { console.log("enter make_functioncall", ctx); let last_node = this.state_stack[this.state_stack.length-1]; let newnode = {type: "make_functioncall", id: this.nodeid++, parentid: last_node.id, functioncall: null }; last_node.stats.push(newnode); this.state_stack.push(newnode); } // Exit a parse tree produced by LuaParser#lua_make_functioncall. exitLua_make_functioncall(ctx) { let function_node = this.state_stack.pop(); }
上面的代码中, nodeid主要是用来记录有多少节点的,顺便当作节点id,这样随着节点的入栈和出栈,当前节点可以知道 上一级节点的内容。如果执行完成后,栈中的剩余节点不是0, 则说明语法解析有问题了。
第三个弯路是grammars-v4中的g4语法定义,仅仅是语法定义
你需要添加不同的处理流程,grammars-v4定义了语法树的框架结构,以及在这个结构上基本的节点Context,但并不包含处理 流程,因此如果认为String最终拿到的是最终的字符串,理解就错了,以字符串为例, 拿到的其实是包含双引号的, 任何一个节点的getText()获取到的也是语言的内容。所以, 还是需要自己来解析为对应类型的。比如字符串在javascript 中可以getText().slice(1,-1)就是脚本执行过程中的字符串,不做处理的话,脚本中的”aa”..”bb” 和 “aabb” 是比较结果 是有问题的。而我最初以为大部分的处理流程都已经包含好了,所以走了不少弯路,查了不少资料后才发现原来该自己 写的代码还是要自己写,也是基于这一点,最后选择了listener模式
第四个弯路是具体规则的处理流程
varOrExp : var_ | '(' exp ')' ;
对于上述的定义,在处理的时候,需要判断这个节点到底是var_的还是exp的,而exp其实是可以调用很多个exp的, var_中也可以有很多个,如果不具体区分开,在调用实现上就很多问题。因此需要自己定义后续的动作,然后再生成。 所以,最后我生成的时候,添加了自定义的处理才进行生成:
varOrExp : var_ # make_lua_varOrExp_var | '(' exp ')' # make_lua_varOrExp_exp ;
第五个弯路是关于语法树的执行
语法树可以在listener解析的过程中执行,也可以在最终执行,对于像C/C++这种带继承的严格类型的高级语言,可以写 很多不同的类来实现统一调用,而像javascript这种动态的,完全可以扔到同一个函数中,比如我的g4文件的exp部分
exp : 'nil' # make_lua_exp_nil | 'false' # make_lua_exp_false | 'true' # make_lua_exp_true | number # make_lua_exp_number | string # make_lua_exp_string | '...' # make_lua_exp_unknow | functiondef # make_lua_exp_functiondef | prefixexp # make_lua_exp_prefixexp | tableconstructor # make_lua_exp_tableconstructor | <assoc=right> exp operatorPower exp # make_lua_exp_operatorPower | operatorUnary exp # make_lua_exp_operatorUnary | exp operatorAddSub exp # make_lua_exp_operatorAddSub | exp operatorMulDivMod exp # make_lua_exp_operatorMulDivMod | <assoc=right> exp operatorStrcat exp # make_lua_exp_operatorStrcat | exp operatorComparison exp # make_lua_exp_operatorComparison | exp operatorAnd exp # make_lua_exp_operatorAnd | exp operatorOr exp # make_lua_exp_operatorOr | exp operatorBitwise exp # make_lua_exp_operatorBitwise ;
通过不同的处理流程将exp下的不同类型的语句解析为不同的结构,不然默认的exp结构中,完全无法区分到底是哪一个操作。 而执行部分则采用类似这样的:
class mylualistener{ //... eval(node, local_obj) { switch(node.type) { //... case "exp_comparison": { let left = this.eval(node.left, local_obj); let right = this.eval(node.right, local_obj); console.log("left==", left); console.log("right=", right); switch(node.op) { case "<": return left < right; case ">": return left > right; case "<=": return left <= right; case ">=": return left >= right; case "~=": return !(left == right); case "==": return left == right; } } //... } return null; } //... }
上面的exp_comparison类型,则是在解析的listener生成的节点:
// Enter a parse tree produced by LuaParser#make_lua_exp_operatorComparison. enterMake_lua_exp_operatorComparison(ctx) { console.log("enterMake_lua_exp_comparison!!!", ctx); let last_node = this.state_stack[this.state_stack.length-1]; console.log("last node=", last_node); let newnode = {type: "exp_comparison", id: this.nodeid++, children:[], parentid: last_node.id, exp:[]}; last_node.exp.push(newnode); this.state_stack.push(newnode); } // Exit a parse tree produced by LuaParser#make_lua_exp_operatorComparison. exitMake_lua_exp_operatorComparison(ctx) { let node = this.state_stack.pop(); node.left = node.exp[0]; node.right = node.exp[1]; node.op = ctx.operatorComparison().getText(); }