Go实现自定义语言的基础 - goyacc简易入门

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 版本后 移除默认安装,即之后版本需手动安装(仍然为官方包)。

一键安装:

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

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

$ goyacc -h
Usage of goyacc:
  -l    disable line directives
  -o string
        parser output (default "y.go")
  -p string
        name prefix to use in generated code (default "yy")
  -v string
        create parsing tables (default "y.output")

goyacc 工具带有一些可选参数

Go实现自定义语言的基础 - goyacc简易入门
goyacc 编译 .y 文件
goyacc 工具的参数很简单,其作用主要是用来翻译BNF(一种语法标识方法)语法描述文件(通常以.y 为后缀的文件)

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


file: parser.y

%{
package parser

import (
    "fmt"
)
%}

%union {
   var string
   num int
}

%token <num> NUM
%token <var> ADD SUB '+' '-'

%type <val> expr

%start expr

%%

expr: NUM {
  $$ =$1
}
| expr '+' NUM {
  $$ = $1 + $3
}
| expr '-' NUM {
  $$ = $1 + $3
}
;

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


$ goyacc parser.y

查看一下文件变化:

$ ls
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,用%%分割的三段: 文法定义%%文法规则%%嵌入代码


{%
嵌入代码
%}
文法定义
%%
文法规则
%%

Go实现自定义语言的基础 - goyacc简易入门
下面详细描述一下 文法定义 与 文法规则:

文法定义
通常使用 %union %type %token %left %right 等构成

Go实现自定义语言的基础 - goyacc简易入门
如:
定义一个val类型 映射到 golang 的string类型,
定义一个stu 映射到 golang 中定义的一个 Student 结构体类型


%union {
    val string
    stu Student
}

%type

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

%type <val> expr

%token 定义终结符

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

%token <val>  Val

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

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

%token OP

%start 定义一个开始终结符

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


%start expr

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

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

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

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

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

例子1:


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

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

例子2:

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

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

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

例子3:

expr3: expr3 '+' NUM {
   $$ = $1 + $3
} | NUM '+' NUM {
  $$ = $1 + $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-名词解释

Go实现自定义语言的基础 - goyacc简易入门
0x04 附录B-常见错误

  • default action causes potential type *: 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-参考文献

上一篇:MySQL基本手册


下一篇:decltype判断变量或表达式类型