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

推荐一个天翼网盘的客户端cloudpan189-go

我算是电信的老用户了,电信宽带给电信客户附送了天翼云盘的账号。前段时间硬盘损坏,造成N年的照片全丢了。 虽然最后通过其他手段几乎找回了99%的照片,但还是不得不多做备份。前段时间升级宽带,发现天翼云盘送的空间 竟然有2TB,家庭共享有4TB, 这么大的空间不好好利用实在太过分了。

而天翼网盘不支持linux系统,我也不太喜欢带界面的,自带的客户端最多只能1周不登录,鼠标点点真麻烦。结果 在网上看到了一款cloudpan189-go的客户端,纯命令行,多平台支持,基本上除了dos,无论哪个平台都能跑。 最关键的, 可以自动登录,自动签到, 做个定时脚本,完全可以定时备份,爱了爱了。

项目的地址在https://github.com/tickstep/cloudpan189-go, 作者的帮助也很详细的,把签到扔到定时脚本里面, 连签到都自动化了。

太感谢这个有爱心的作者了。我也是初次使用,不知道会不会碰到太多坑

使用win10自带的ssh服务器

我太喜欢ssh了,因为它的功能实在太强大,而且几乎不占用啥资源。(PS:类似的软件:p7zip,tightvnc)。因此,无论是哪个系统,我总想着安装一个ssh的服务器。

之前我在虚拟机中安装了个win10, 虽然安装了tightvnc, 但并不喜欢用远程管理的方式去连接,因为占用带宽不说,操作也不方便,比如虚拟机内部的拼音输入法状态经常和外面的不相同。我还是喜欢控制台程序。

win10自带了ssh的客户端,在设置->应用和功能->可选功能->添加功能 中可以安装win10的 openssh 客户端和服务器端,安装完后,在控制面板->管理工具->服务 中找到openssh服务,设置为自动或者启动都可以运行。

openssh 的服务器运行只需要1.3MB的内存,相比于其他软件,可能需要按照内存倒着排序才能看的到。

关于win10 自带的openssh服务器的配置文件,在windows系统目录的Windows\System32\OpenSSH目录下的sshd_config_default文件,和配置linux一样。我比较喜欢把GatewayPorts 设置为yes, 懂得人都懂的这个功能。

自带的openssh客户端的配置,则是在用户目录下的。连上去默认的终端管理就是windows的cmd.

我虚拟机中的win10 看来可以卸载wsl了。

关于ssh 文件及目录权限

今天在整理ssh配置的时候,发现wsl2中的ssh无法配置成免密证书登录,奇怪的是,我另外一台却可以。 首先可以肯定的是两个的配置完全相同,连匹配的证书都用的是相同的,最后无奈,只能打开sshd_config 中的调试功能。

sshd_config中对应的调试开关如下:


# Logging
SyslogFacility AUTH
LogLevel DEBUG3

打开后,在/var/log/auth.log 中可以看到日志。。。。我的看到很多尝试登陆的日志。

找到几条相关的日志:


Aug 28 17:24:22 pipo sshd[604587]: debug1: trying public key file /home/test/.ssh/authorized_keys
Aug 28 17:24:22 pipo sshd[604587]: debug1: fd 5 clearing O_NONBLOCK
Aug 28 17:24:22 pipo sshd[604587]: Authentication refused: bad ownership or modes for directory /home/test
Aug 28 17:24:22 pipo sshd[604587]: debug1: restore_uid: 0/0
Aug 28 17:24:22 pipo sshd[604587]: debug1: temporarily_use_uid: 1000/1000 (e=0/0)
Aug 28 17:24:22 pipo sshd[604587]: debug1: trying public key file /home/test/.ssh/authorized_keys2
Aug 28 17:24:22 pipo sshd[604587]: debug1: Could not open authorized keys '/home/test/.ssh/authorized_keys2': No such file or directory
Aug 28 17:24:22 pipo sshd[604587]: debug1: restore_uid: 0/0
Aug 28 17:24:22 pipo sshd[604587]: debug3: mm_answer_keyallowed: publickey authentication test: RSA key is not allowed

ssh会从两个文件中读取公钥来判断, 如果一个没有就读取另外一个,同时,ssh还做了一件事情就是关于文件,文件夹的权限判断。

.ssh的目录权限必须是 0700 或者 0755

公钥及authorized_keys*必须是 0644

密钥文件必须是 0600

其实还有一个就是用户的目录文件,比如上面日志中的/home/test 目录, 必须属于用户并且为755

因为我的home目录windows的路径下复制过来的,所以权限全错了。修改了之后就可以使用证书登录了。

另外,ssh 从7.3(2016年8月1日发布) 的版本开始,在config文件中可以使用include指令了, 这样 可以把不同的主机进行分门别类的存放到不同的文件中,方便了很多。

而且,还可以在config文件中使用下面的两行指令来暂存密码登录:

ControlMaster auto
ControlPath /tmp/%r@%h:%p

加入后,输入密码连上某台主机后,再在新窗口中,连接相同主机的话,就不需要输入密码了。对我这种 经常需要一边打开文件编辑,一边编译,一边观察运行结果的人来说,便利不少