背景
领域特定语言(DSL),如 SQL、Elasticsearch Querystring 等,往往是为专门的目的设计的。在特定的任务中,DSL 通过在表达能力上做的妥协换取在某一领域内的高效。
在飞书套件日志系统的私有化研发过程中,为了符合研发同学查询日志的习惯,尝试使用 Elasticquery Querystring(下简称为 Querystring)作为过滤器的查询条件语句,由此需要可用的 Golang Querystring 解析器。由于目前开源界无法找到完善的实现,尝试使用 Goyacc 自行构建。
Yacc 是一个被普遍采用的编译器代码生成器,生成出的代码主要用于语法分析阶段,常常与作为词法分析器的 lex 匹配使用,使用 LALR 算法,将源代码构建为抽象语法树(AST)。Goyacc 是 yacc 的 Golang 版本。
本文尝试实现的 Querystring 解析器本质上是词法分析器和语法分析器的组合。语法分析器由 Goyacc 生成;为了方便实现符合 Querystring 习惯的反转义,词法分析器是自行实现的。Yacc 和它在各个语言上的实现,中文互联网上较少有具体的介绍。本文会尽量详细的描述 Goyacc 实践中可能需要注意的点。所述的 Querystring 解析器代码可在 https://github.com/bytedance/go-querystring-parser 此处查看。
框架
一套完整的应用 Goyacc 的 DSL 解析器包含以下部分:
-
语法分析器(parser)
-
词法分析器(lexer)
-
较为简单的做法,是采用
Scanner
来进行构建;当存在特殊需求时,一般自行构建。
-
-
AST 节点的定义
-
包装实体与工具函数
大致流程为,语法分析器调用词法分析器将源代码拆解为基本「Token(记号)」,根据语法规程将若干个「Token」组合成「Expr(表达式)」(Token 本身也被作为 Expr 处理)。「Expr」的结构构建在「AST」中,得到结果。
语法分析器
语法分析(syntactic analysis,或 parsing)是根据某种给定的形式文法对由记号序列构成的输入文本进行分析并确定其语法结构的一种过程。
语法分析器的作用是进行语法检查、并构建由输入的记号组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的「记号」,并将记号流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成。
在本例中,即为工具自动生成。使用巴科斯范式描述对应的语法定义,并使用 goyacc 生成 golang 代码,提供一个 LALR 语法分析器,并定义了供 lexer 返回的 token 定义。
安装 Goyacc
在安装了 golang 的环境中,执行:
1go get -u golang.org/x/tools/cmd/goyacc
如安装后无法正常运行,请检查 $GOPATH/bin
是否加入到了 $PATH
中。
Goyacc 语法定义文件
一个 yacc 语法定义文件,一般由以下若干部分构成。
头部目标语言代码
可参考「附录一:L1-L3」,使用 %{
和 }%
将需要的目标语言原生代码段落包围起来。目标语言往往涉及到语言应用中的一些头部代码。在例子里,golang 所需的包名称定义等需要通过这一部分添加进来。其他的例子如常量的定义、结构体的定义等。
Union(集合)声明
可参考「附录一:L5-L9」,以 %union{}
格式定义,只可定义一次。「Union」这一概念包含了下述类型声明中的各个类型,及这些类型对应的目标语言类型(即 Golang 中的类型)。与 c 语言中 union 的概念类似,在目标代码中(生成为