使用antlr4总结

最近由于某种原因,在考虑实现一套自定义的脚本。正好看到了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();
	}

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据