antlr4使用总结2

接着之前的总结内容,我一直对于解释函数的定义,解释,执行感到比较迷惑,与最终生成的二进制执行不同,我没有 想过模拟栈的处理,因此,对于比如局部变量,全局变量,平台实现的接口函数,自定义函数的调用和执行感到迷惑。

这里主要以lua为例子进行测试的,我最终要实现的是执行grammars-v4中的lua的下列代码:

-- defines a factorial function
    function fact (n)
      if n == 0 then
        return 1
      else
        return n * fact(n-1)
      end
    end

    print("enter a number:")
    a = 5 -- 随便的一个数字
    print(fact(a))

为了保存变量,或者程序执行的全局变量, 我在自己的LuaListener中加了部分变量来保存全局的函数或自定义的函数或变量

export default class myLuaListener extends LuaListener {
    constructor() {
        super();
        this.global_api = {
            print: function(...args) {
                console.log("lua print:", ...args);
            }
        };
        this.state_stack = [];
        this.nodeid = 0;
        this.prog = {
            type: "chunk",
            nodeid:0,
            block:[],
        };
        this.var_map = {};
    }
//...
}

我的想法是如果底层提供了某个api,则优先底层的函数,这样这些函数就类似于保留字或者关键字了。为了处理全局变量 和局部变量,在函数的调用中,可以将局部变量和全局变量合并为新的结构,在这一点上,js的…的方式是很方便的了。

function eval(node, local_obj) {
//...
switch(node.type) {
//...
case "func_dec": {
let funcname = this.eval(node.funcname, local_obj);
local_obj[funcname] = node.functionbody;
return local_obj[funcname];
}
case "make_functioncall": {
let ret = null;
ret = this.eval(node.functioncall, local_obj);
return ret;
}
case "functioncall": {
let ret = null;
let varorexp = this.eval(node.varorexp, local_obj);
let args = this.eval(node.nameandargs, local_obj);
if(this.global_api[varorexp]) ret = this.global_api[varorexp](...args);
else {
let func = local_obj[varorexp];
let parlist = func.parlist;
let localobj = {};
if(parlist && parlist.namelist) {
let names = this.eval(parlist.namelist, local_obj);
for(let i =0, j=names.length; i<j; i++) {
localobj[names[i]] = args[i];
}
}
for(let i =0,j=func.block.length; i<j;i++) {
ret = this.eval(func.block[i], {...local_obj, ...localobj});
}
//,,,
}
//...
}

函数的调用必须要有函数名,根据lua的语法,无论是var或者exp,最后必须产生一个名称, 如果在global_api中,实现了 函数,则直接调用其实现。否则就认为是代码自己实现的,则从local_obj中提取对应的函数定义的结构,然后根据 parlist,依次将参数的名字和参数的值对应起来,如果参数的值多于参数的名字个数,会被直接忽略(我没有处理不定 参数)。然后生成新的本地变量来执行函数体, {…local_obj, ….localobj} 后面的是新组成的,在js中这样的 写法确保后面相同的key会覆盖前面的,比如{…{a:4},…{a:5}} 最后生成的结构是{a:5}, 这样来处理局部变量 和全局变量的优先级。 但对于lua的下面的这种赋值,在生成的语法树中,会使用到prefixexp的调用:

a = test(1,2) -- test为使用lua实现的函数

prefixexp下可以是var, 也可以是exp, 如果是exp, 则这个exp是需要执行的, 如果是var_, 则真实返回的应该是变量的值, 而不是变量,如果exp是函数,返回的应该是函数的执行结果,因此,就需要新生成函数的执行节点了

case "prefixexp": {
            let ret = null;
            if (node.varorexp) {
                let var_ = this.eval(node.varorexp, local_obj);
                if(node.varorexp.type === "varorexp_exp") {
                    ret = var_;
                } else {
                    ret = local_obj[var_];
                }
            }
            if(ret.type === "functionbody") {
                ret = this.eval({...node, type: "functioncall"}, local_obj);
            }
            return ret;
        }

在eval的参数中,我每次都传进去了一个local_obj的结构,这个用来保存当前用的变量列表, 如果这个结构中放入了预先设置的变量,则可以直接在执行过程中使用。

我用来解释lua执行的处理流程:

const chars = new antlr4.InputStream(lua_script);
            const lexer = new LuaLexer(chars);
            const tokens = new antlr4.CommonTokenStream(lexer);
            const parser = new LuaParser(tokens);
            parser.buildParseTrees = true;
            const tree = parser.chunk();
            const printer = new myLuaListener();
            antlr4.tree.ParseTreeWalker.DEFAULT.walk(printer, tree);
            console.log("printer.prog===", printer.prog);
            try {
                let ret = printer.eval(printer.prog, printer.var_map);
                console.log("eval ret=", ret, printer.var_map);
            } catch (e) {
                console.log("eval error===", e);
            }

使用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();
	}

unraid 的假想

我自己目前已经不再使用unraid了,这个只是吸取之前放弃的经验教训。

修改配置,使docker使用某个独立的硬盘(SSD或许会更好,不在阵列中)来加速及独立docker服务器的启动,不过,unraid的docker服务必须在阵列启动的情况下才能启动。因为docker默认使用的是阵列中的docker.img文件,这样 操作可以加速docker服务的快速启动。

通常,docker会挂载外部的目录。通过规划不同的服务数据,将不经常更改的,可以使用阵列的磁盘(可以通过cache加速),经常变更的(比如mysql的数据库的数据,邮件服务器的数据等等),使用独立的硬盘(不在阵列中), 这样来解决阵列修改文件和写入速度慢的影响,如果数据量比较大,机械硬盘也可以,因为不再阵列中,因此,写入和读取速度不受影响。

因为上述的数据不在阵列的数据保护中,因此,需要通过另外的手段进行数据保护,比如rsnapshot来进行定时的增量化备份。备份的目的地可以是阵列中的磁盘或cache。rsnapshot也支持docker化的。

对于虚拟机的硬盘,我建议也这样来做,因为实时的写入速度实在太夸张,碎片文件的情况下几乎整个系统处于崩溃边缘了。

对于下载服务的下载数据,我自己对这个数据的保护性定为可以不保护的,也可以使用独立硬盘的方式。这样也不会受到阵列的影响。共享的化,可以通过在阵列中创建链接的方式,将下载数据共享出去。

上述的方案,通过规避不往阵列中写入数据,来达到可用的目的。不过,我目前是在ubuntu 20.04的基础上,通过docker各种服务,来定制各种内容,这样对我而言,可以不用再安装一个linux的虚拟机了。以后有机会可以按照上述的思路试试。

xpra 是个好工具

我经常需要在别人的电脑上操作一些我自己电脑上的事情,虽然现在win10已经内置ssh了,但每次连上去,受限于win10自带的控制台,spacemacs经常花屏,错位,随便动 两下就不知道当前在哪一行了,可以说完全无法使用了。如果用图形界面的话,就需要xming之类的工具了,电脑里默认又没有。

昨天看到一个很不错的工具:xpra, 这个工具最强大的地方在于,可以将X11服务以h5的形式推送出去,类似与vnc的网页版本,但又不需要安装各种插件,想想下通过网页 操作linux服务器,对客户端的需求只有一个网页浏览器,这不正是我需要的么。

于是赶紧屁颠屁颠的安装配置,这样我从任何地方操作就只需要一个浏览器了。

org2blog在org 9.1.9无法创建模板

错误信息大概是一个check org template alist的错误, 我已经找不到了。表现就是模式已经是org2blog的org-mode了, 但创建的buffer里面是空的,命令行报告一个类似的错误。

原因在于在之前的版本中,如果使用模板的话,需要手动加载org-tempo这个,新版本下不需要加载了。 所以在配置中将 (require ‘org-tempo)去掉即可。

不过,我的软件包一向很少更新,获取9.1.9之前的某个版本就已经有问题了。