前言
大家好,今天我们来了解一下goyacc
。随着Parse
的深入,这里需要理解一下这方面的知识。这点知识掌握后,以后可以自建SQL语法。
goyacc
在了解goyacc之前,我们需要先了解一下yacc
。
yacc(Yet Another Compiler Compiler),是一个经典的生成语法分析器的工具。yacc生成的编译器主要是用C语言写成的语法解析器(Parser),需要与词法解析器Lex一起使用,再把两部分产生出来的C程序一并编译。
这是百度百科对yacc
的解释,那么goyacc
就是golang语言的yacc
版本。简单来说goyacc
实现了go语言版本的词法分析、语法解析。
我们来初步使用一下,直接安装。
go install golang.org/x/tools/cmd/goyacc@latest
安装完成后,就可以到go环境的bin目录下看到goyacc可执行文件。
ls -lrt
-rwxr-xr-x 1 buddy staff 2952834 Feb 26 16:30 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")
这里几个参数含义如下。
| 参数 | 含义 |
|---|---|
| -l | 禁用line指令 |
| -o | 解析器输出,默认为"y.go" |
| -p | 在生成的代码中使用的名称前缀(默认"yy") |
| -v | 创建解析表(默认"y.output") |
现在还很难理解它的流程,我们继续看github上官方提供的例子。

goyacc/testdata/expr 目录里面有一个非常简单的 yacc 程序。把这个文件下载下来。
./goyacc -p "expr" expr.y
ls -lrt
-rwxr-xr-x 1 buddy staff 2952834 Feb 26 16:30 goyacc
-rw-r--r-- 1 buddy staff 2902 Feb 26 22:44 expr.y
-rw-r--r-- 1 buddy staff 12145 Feb 26 22:44 y.go
-rw-r--r-- 1 buddy staff 2634 Feb 26 22:44 y.output
执行-p参数,生成了2个文件,一个是解析器输出语法解析器y.go文件,另外一个是解析表y.output文件。
接下来执行go run y.go,一个简易的计算器程序就实现了。
go run y.go
> 1+2
3
> 3+3
6
什么是.y文件
如上面例子所示,可以说.y文件是非常重要的,只有编写好了.y文件,我们才可以通过goyacc程序把它转化成语法解析器。而这个.y文件就是我们前面科普的语法规则文件,它主要由三部分组成。

第一个称为定义部分,可以在其中进行各种定义。
第二个部分是语法的规则,是比较关键的部分。
第三个部分是用户定义部分,一般是空白,不用太关注。
我们来看看示例的expr.y文件。
第一部分如下
%union {
num *big.Rat
}
%type <num> expr expr1 expr2 expr3
%token '+' '-' '*' '/' '(' ')'
%token <num> NUM
这个%union,它其实是定义了一个go的struct,xxxSymType
。

从我们生成的y.go中,你会看到一个exprSymType
的结构体,它来自于expr.y的第18行,就是我们上面union定义的部分。

这个struct主要有两个作用,第一,定义表达式和TOKEN的类型。表达式其实就是单个或者多个TOKEN 的组合。第二就是存放变量。
%type,定义非终结符。这里定义为num类型的,给出了4个规则,expr expr1 expr2 expr3。
top:
expr
{
if $1.IsInt() {
fmt.Println($1.Num().String())
} else {
fmt.Println($1.String())
}
}
expr:
expr1
| '+' expr
{
$$ = $2
}
| '-' expr
{
$$ = $2.Neg($2)
}
expr1:
expr2
| expr1 '+' expr2
{
$$ = $1.Add($1, $3)
}
| expr1 '-' expr2
{
$$ = $1.Sub($1, $3)
}
expr2:
expr3
| expr2 '*' expr3
{
$$ = $1.Mul($1, $3)
}
| expr2 '/' expr3
{
$$ = $1.Quo($1, $3)
}
expr3:
NUM
| '(' expr ')'
{
$$ = $2
}
这里的规则是一种递归下降的方式,递归下降是一种自顶向下从起始的非终结符开始,不断地对非终结符进行分解,直到匹配到输入的终结符。
%token,定义终结符,表示不能再拆的字符。
其实还有像%left %right %nonassoc等定义,它们也是终结符,定义形式与%token类似,但是它们不同的地方在于,它们具有某种优先级和结合性,%left表示左结合,%right表示右结合,%nonassoc表示不可结合。而优先级关系则是以他们定义出现的顺序决定的,先定义的优先级低,最后定义的优先级最高,同时定义的优先级相同。
将lexer 和 goyacc 联合起来
介绍完goyacc
之后,我们需要和lexer
进行联合起来使用,如下图所示:

goyacc
生成的解析器 yyParse
要求词法分析器提供下面的接口。
type yyLexer interface {
Lex(lval *yySymType) int
Error(e string)
}
或者是
type yyLexerEx interface {
yyLexer
// Hook for recording a reduction.
Reduced(rule, state int, lval *yySymType) (stop bool) // Client should copy *lval.
}
Lex 是词法分析函数,解析过程会多次回调此方法,每次调用都会返一个token值,并将token值放到放在lval中。Error 等价于 yacc 中的 yyerror。
我们回到上篇文章中研究 TiDB 中的 Parse模块 (一)写的解析函数上进行调试。
stmtNodes, _, err := p.Parse(sql, "", "")
在执行Parse
函数的时候会调用ParseSQL
函数,ParseSQL
是真正实现将字符串转换成ast.StmtNode
的函数。
// ParseSQL parses a query string to raw ast.StmtNode.
func (parser *Parser) ParseSQL(sql string, params ...ParseParam) (stmt []ast.StmtNode, warns []error, err error) {
resetParams(parser)
for _, p := range params {
if err := p.ApplyOn(parser); err != nil {
return nil, nil, err
}
}
sql = parser.lexer.tryDecodeToUTF8String(sql)
parser.src = sql
parser.result = parser.result[:0]
var l yyLexer
parser.lexer.reset(sql)
l = &parser.lexer
yyParse(l, parser)
warns, errs := l.Errors()
if len(warns) > 0 {
warns = append([]error(nil), warns...)
} else {
warns = nil
}
if len(errs) != 0 {
return nil, warns, errors.Trace(errs[0])
}
for _, stmt := range parser.result {
ast.SetFlag(stmt)
}
return parser.result, warns, nil
}
在这个ParseSQL
函数的中间的部分,它定义了一个yyLexer。然后执行yyParse(l, parse)
。在这里就会进入我们的goyacc执行程序parse.go
。
后记
这一篇科普了一下goyacc
的一点用法,它需要和lexer
结合起来使用,这些知识学起来不容易,坚持一下。
reference
https://wizardforcel.gitbooks.io/diy-c-compiler/content/5.html
https://1thx.com/golang/189.html
https://cloud.tencent.com/developer/article/1744609
https://pkg.go.dev/github.com/cznic/goyacc
https://blog.csdn.net/tencent_teg/article/details/112914108




