0x01 自言自语
一直就对解析文档,比较感兴趣,一直没深入研究,只停留在仅知道 Lex & yacc 和 antlr 的名词阶段,最近看了go-zero的api解析器,觉得甚好,是时候花时间学习一下了。
简单看了go-zero发现是自己实现了词法分析、语法解析,这不符合我的一贯偷懒作风,所以并未其源码开始学习。既然用golang那么他自带的goyacc就是我学习的不二之选。当然你可能会听说Lex&yacc 已经很古老了,antlr更先进一点。但是既然goyacc能成为golang官方工具,那么肯定还是值得你学习的。
goyacc的文档非常的少,少到什么程度?少到你未来一定能搜到这篇。甚至连github上的使用例子也不多,大致就分两类:计算器、sql解析器,其中计算器目测是国外某大学的课程。
所以研究goyacc我花了好几个通宵、掉了少许头发。不经让这篇文章有了一个营销文案:花了一夜时间,搞懂了外国的一堂编译原理课。
个人对技术文章的理解是,文章可以有自己的观点、啰嗦、甚至幽默,但尽量不要放在学术部分,毕竟技术是严禁的。所以下面描述,我可尽能做一个无情的打字机,尽可能的按照文档风描述。
0x02 goyacc简易入门
安装 goyacc
golang 1.8 版本之前 yacc 直接再带与go tool 无需自行安装。
鉴于使用的频率太少,遂在 golang 1.8 版本后 移除默认安装,即之后版本需手动安装(仍然为官方包)。
一键安装:
之后就可以通过执行goyacc
命令来测试安装情况了,如看到如下信息大致就安装成功了
goyacc 工具带有一些可选参数
参数 | 说明 |
---|---|
-l | 显示line指令 |
-o string | 指定输出解析器的文件名称 (默认 y.go) |
-p string | 指定解析器输出接口的前缀 |
-v string | 生成解析过程表 (默认 y.output) |
goyacc 编译 .y 文件
goyacc 工具的参数很简单,其作用主要是用来翻译BNF(一种语法标识方法)语法描述文件(通常以.y 为后缀的文件)
让我们先看一个.y 文件的例子:
file: parser.y
复制上面代码命名为 parser.y 然后用我们之前安装好的goyacc执行:
查看一下文件变化:
我们发现生成了两个新文件:y.go y.output
至此 你已经实践了goyacc 的作用,即:通过文法文件(parser.y)生成语法解析器(y.go) 、解析过程表 (y.output)
请观察一下 y.go 文件的内容 (迫于篇幅,此处不贴y.go内容了,请按照上面进行试验即可得到)
下面我们详细说明一下 文法(.y文件)的格式:
.y 构成格式
如同 c版本的yacc一样, goyacc 的.y文件大致分成几个部分:
1,由{%与%} 包括的 嵌入代码
2,用%%分割的三段: 文法定义%%文法规则%%嵌入代码
名称 | 解释 |
---|---|
嵌入代码 | golang代码 通常用来定义生成的文件的包名、引入包、结构定义等 |
文法定义 | 由 %union %type %token %left %right %start 等组成的定义 |
文法规则 | 由 非终结符 与终结符组成的规则 |
下面详细描述一下 文法定义 与 文法规则:
文法定义
通常使用 %union %type %token %left %right 等构成
描述符 | 说明 |
---|---|
%union | 用来定义一个类型并映射golang的一个数据类型(可以是一个自定义类型) |
%struct | 同%union 建议使用%union |
%token | 定义非终结符
|
%type | 定义终结符 |
%start | 定义从哪个终结符开始解析 默认规则段中的第一个终结符 |
%left | 定义规则结合性质 左优先 |
%right | 定义规则结合性质 右优先 |
%nonasso | 定义规则结合性质 不结合 |
%perc term | 定义优先级与 term 一致 |
如:
定义一个val类型 映射到 golang 的string类型,
定义一个stu 映射到 golang 中定义的一个 Student 结构体类型
%type
定义一个 val 类型的 expr 非终结符 (nonterminal)
%token 定义终结符
定义一个 val 类型的 Val 终结符 (terminal)
(这听起来不好懂,可以想象成有个Val类型的变量接收器)
也可以定义个没有类型值的终结符,如
%start 定义一个开始终结符
即设置一个开始的终结符,即从这里开始解析语法
%left %right 主要用来设置匹配结合方式 这里先不展开了
文法规则
非终结符: 规则描述 {
动作描述
};
“:” 左边的是非终结符 右侧的规则 通常由 非终结符 终结符 构成
非终结符 使用%type定义的符号 可以理解为一串特定排序的 token
终结符 可以理解成 一个token
规则描述 可以是 非终结符、终结符, 可用 | 分割多个规则, {}内是具体动作,{}内符合golang语法,并且 $$ 代表规约后的值 $1 $2 代表类型变量值
动作描述 即用“{”与“}”包裹的部分 符合golang语法且拥有特殊宏替换的语法块
一条规则最后要用“;”结束
例子1:
这个例子定义了 一个 两数求和的规则 (a+b)
expr 构成终结符这个非 终结符的条件又三个终结符(token)构成 NUM '+' NUM ,方法是求两数之和 返回给expr
例子2:
这个例子了 三个数求和,并使用了一个非终结符作为规则的一部分
当需要定一个多个数求和时就需要通过递归定义了
例子3:
这个例子通过递归定义了 支持多个数相加的规则 其中 使用“|”分割了两个规则描述
其实语法解析 就是根据token去拼出多个解析规则,然后通过递归下降等算法去解析。
yacc就是是通过.y 描述文件 生成递归下降程序(y.go程序)实现语法解析的一个过程。
实现lexer接口
goyacc 没有对应的 lex 生成工具
通常需要手工写两个方法来实现 yyLexer接口
type yyLexer interface {
Lex(lval *yySymType) int
Error(s string)
}
Lex() 词法分析函数 解析过程会多次回调此方法,每次调用应返一个token值, lval 是当前栈状态值
Error() 错误回调方法 在语法解析错误时被回调
一般可以通过 text/scanner 进行基础解析,再换算成自己的token,省去部分烦劳工作
0x03 附录A-名词解释
英文 | 中文 | 说明 |
---|---|---|
goyacc | 语法解析器golang版 | golang自带工具 |
lex | 词法分析 | |
yacc | 语法解析 | |
LHS | 左侧 | 通常为 非终结符 |
RHS | 右侧 | 通常为 非终结符 或 终结符 |
terminal | 终结符 | |
nonterminal | 非终结符 |
0x04 附录B-常见错误
default action causes potential type clash: parser.go.y:40
token illegal on LHS of grammar rule
rules never reduced
rule $NAME: $NAME never reduced
conflicts: 1 shift/reduce
0x04 附录B-参考文献
https://blog.csdn.net/wp1603710463/article/details/50365640
https://blog.csdn.net/zzhongcy/article/details/93753017
2020 GopherChinaCon 隆重开启!
时间:2020年11月20-22日
地点:上海由由喜来登酒店
报名方式:点击“阅读原文”,早鸟票火热开启中!
本文分享自微信公众号 - GoCN(golangchina)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/4628563/blog/4653013