0x01 自言自语

一直就对解析文档,比较感兴趣,一直没深入研究,只停留在仅知道 Lex & yacc 和 antlr 的名词阶段,最近看了 go-zero 的 api 解析器,觉得甚好,是时候花时间学习一下了。

简单看了 zero 发现是自己实现了词法分析、语法解析,这不符合我的一贯偷懒作风,所以并未其源码开始学习。既然用 golang 那么他自带的 goyacc 就是我学习的不二之选。当然你可能会听说 Lex&yacc 已经很古老了,antlr 更先进一点。但是既然 goyacc 能成为 golang 官方工具,那么肯定还是值得你学习的。

goyacc 的文档非常的少,少到什么程度?少到你未来一定能搜到这篇。甚至连 github 上的使用例子也不多,大致就分两类:计算器、sql 解析器,其中计算器目测是国外某大学的课程。

所以研究 goyacc 我花了好几个通宵、掉了少许头发。不经让这篇文章有了一个营销文案:花了一夜时间,搞懂了外国的一堂编译原理课。

个人对技术文章的理解是,文章可以有自己的观点、啰嗦、甚至幽默,但尽量不要放在学术部分,毕竟技术是严禁的。所以下面描述,我可尽能做一个无情的打字机,尽可能的按照文档风描述。

0x02 goyacc 简易入门

安装 goyacc

golang 1.8 版本之前 yacc 直接再带与 go tool 无需自行安装。
鉴于使用的频率太少,遂在 golang 1.8 版本后 移除默认安装,即之后版本需手动安装(仍然为官方包)。

一键安装:

  1. go get -u github.com/golang/tools/tree/master/cmd/goyacc

之后就可以通过执行goyacc 命令来测试安装情况了,如看到如下信息大致就安装成功了

  1. $ goyacc -h
  2. Usage of goyacc:
  3. -l disable line directives
  4. -o string
  5. parser output (default "y.go")
  6. -p string
  7. name prefix to use in generated code (default "yy")
  8. -v string
  9. create parsing tables (default "y.output")

goyacc 工具带有一些可选参数

参数 说明
-l 显示 line 指令
-o string 指定输出解析器的文件名称 (默认 y.go)
-p string 指定解析器输出接口的前缀
-v string 生成解析过程表 (默认 y.output)

goyacc 编译 .y 文件

goyacc 工具的参数很简单,其作用主要是用来翻译 BNF(一种语法标识方法)语法描述文件(通常以. y 为后缀的文件)

让我们先看一个. y 文件的例子:

file: parser.y

  1. %{
  2. package parser
  3. import (
  4. "fmt"
  5. )
  6. %}
  7. %union {
  8. var string
  9. num int
  10. }
  11. %token <num> NUM
  12. %token <var> ADD SUB '+' '-'
  13. %type <val> expr
  14. %start expr
  15. %%
  16. expr: NUM {
  17. $$ =$1
  18. }
  19. | expr '+' NUM {
  20. $$ = $1 + $3
  21. }
  22. | expr '-' NUM {
  23. $$ = $1 + $3
  24. }
  25. ;

复制上面代码命名为 parser.y 然后用我们之前安装好的 goyacc 执行:

  1. $ goyacc parser.y

查看一下文件变化:

  1. $ ls
  2. parser.y y.go y.output

我们发现生成了两个新文件: y.go y.output

至此 你已经实践了 goyacc 的作用,即:通过文法文件(parser.y)生成语法解析器(y.go) 、解析过程表 (y.output)

请观察一下 y.go 文件的内容 (迫于篇幅,此处不贴 y.go 内容了,请按照上面进行试验即可得到)

下面我们详细说明一下 文法 (.y 文件) 的格式:

.y 构成格式

如同 c 版本的 yacc 一样, goyacc 的. y 文件大致分成几个部分:

1, 由 {% 与 %} 包括的 嵌入代码
2, 用 %% 分割的三段: 文法定义 %% 文法规则 %% 嵌入代码

  1. {%
  2. 嵌入代码
  3. %}
  4. 文法定义
  5. %%
  6. 文法规则
  7. %%
  8. 嵌入代码 golang代码,通常忽略此部分直接在写在代码头中)
名称 解释
嵌入代码 golang 代码 通常用来定义生成的文件的包名、引入包、结构定义等
文法定义 由 %union %type %token %left %right %start 等组成的定义
文法规则 由 非终结符 与终结符组成的规则

下面详细描述一下 文法定义 与 文法规则:

文法定义

通常使用 %union %type %token %left %right 等构成

描述符 说明
%union 用来定义一个类型并映射 golang 的一个数据类型(可以是一个自定义类型)
%struct 同 %union 建议使用 %union
%token 定义非终结符 是一个 union 中定义的类型空间 可无类型空间
%type 定义终结符
%start 定义从哪个终结符开始解析 默认规则段中的第一个终结符
%left 定义规则结合性质 左优先
%right 定义规则结合性质 右优先
%nonasso 定义规则结合性质 不结合
%perc term 定义优先级与 term 一致

如:
定义一个 val 类型 映射到 golang 的 string 类型,
定义一个 stu 映射到 golang 中定义的一个 Student 结构体类型

  1. %union {
  2. val string
  3. stu Student
  4. }
  • %type

定义一个 val 类型的 expr 非终结符 (nonterminal)

  1. %type <val> expr
  • %token 定义终结符

定义一个 val 类型的 Val 终结符 (terminal)

  1. %token <val> Val

(这听起来不好懂,可以想象成有个 Val 类型的变量接收器)

也可以定义个没有类型值的终结符,如

  1. %token OP
  • %start 定义一个开始终结符

即设置一个开始的终结符,即从这里开始解析语法

  1. %start expr

%left %right 主要用来设置匹配结合方式 这里先不展开了

文法规则

非终结符: 规则描述 {
动作描述
};

“:” 左边的是非终结符 右侧的规则 通常由 非终结符 终结符 构成

非终结符 使用 %type 定义的符号 可以理解为一串特定排序的 token
终结符 可以理解成 一个 token
规则描述 可以是 非终结符、终结符, 可用 | 分割多个规则, {} 内是具体动作,{} 内符合 golang 语法,并且 $$ 代表规约后的值 $1 $2 代表类型变量值
动作描述 即用 “{” 与“}”包裹的部分 符合 golang 语法且拥有特殊宏替换的语法块

一条规则最后要用 “;” 结束

例子 1:

  1. expr1: NUM '+' NUM {
  2. $$ = $1 + $3
  3. };

这个例子定义了 一个 两数求和的规则 (a+b)
expr 构成终结符这个非 终结符的条件又三个终结符(token)构成 NUM ‘+’ NUM ,方法是求两数之和 返回给 expr

例子 2:

  1. expr2: expr1 '+' NUM {
  2. $$ = $1 + $3
  3. };

这个例子了 三个数求和,并使用了一个非终结符作为规则的一部分

当需要定一个多个数求和时就需要通过递归定义了

例子 3:

  1. expr3: expr3 '+' NUM {
  2. $$ = $1 + $3
  3. } | NUM '+' NUM {
  4. $$ = $1 + $3
  5. };

这个例子通过递归定义了 支持多个数相加的规则 其中 使用 “|” 分割了两个规则描述

其实语法解析 就是根据 token 去拼出多个解析规则,然后通过递归下降等算法去解析。
yacc 就是是通过. y 描述文件 生成递归下降程序(y.go 程序)实现语法解析的一个过程。

实现 lexer 接口

goyacc 没有对应的 lex 生成工具
通常需要手工写两个方法来实现 yyLexer 接口

  1. type yyLexer interface {
  2. Lex(lval *yySymType) int
  3. Error(s string)
  4. }

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 - 参考文献

最后更新于 2020-09-14 06:50:09 并被添加「」标签,已有 1599 位童鞋阅读过。

本站使用「署名 4.0 国际」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处
https://www.1thx.com/golang/189.html