lex & yacc
1. 背景
网上关于lex和yacc的介绍真的又老又少,而goyacc的更加少,最近需要解析sql,接触到这块,虽然最后发现其效率没有用手动写代码解析效率高而放弃使用,但是学会了这种快速构建文法解析的工具还是有所收获吧。
2. 相关知识
增加一点内容篇幅,不想看的直接跳到3就ok
2.1词法分析
词法分析的作用就是对一个文本或称为一串字符串(包括空格,换行,特殊符号等)进行内容提取。通过分析给每一个字段打上标记(TOKEN)。那么如何分析呢,以sql的查询语句为例
select * from userinfo where name = 'zhang' order by age
这个字符串有多少信息呢,首先,select,from, where, order, by 这些单词在从左到右扫描时与userinfo,age这些并无不同,但很明显这些单词是数据库的保留单词,也称为关键字。所以一般的我们需要一个关键字表,在扫描到单词时判断其是可变的字段还是关键字。其次对于 * ,=,’zhang’,我们单独将其标记为特殊符号,操作符,字符串,就这样,我们对所有的可能情况进行识别给不同类型的字段以唯一的标识(TOKEN)一般为int。所以此法分析的作用就是将一系列的文本转换成TOKEN序列。
2.2文法分析
对于词法分析后的TOKEN序列,我们需要知道他是否符合我们的文法规范,例如
select from a=1
像这样的句子不符合sql规范,我们需要明确的指出。文法分析其实就是一个有限状态自动机,当扫描到一个TOKEN时,我们根据不同的情况跳转到另一状态,直到终结状态。
2.3语义分析
语义阶段与文法阶段往往可以同步进行,在sql文法分析的阶段我们便可以标明该语义树的各个部分,比如在扫描到select时,我们便可以确定这是一个select语句或者可能是错误语句,然后根据后续信息补全select语法树。
3. goyacc
为什么用goyacc,因为influxdb本身是go语言写的,我需要解析的语法树与influxdb使用的语法树相同,并且不需要二次构建,所以使用goyacc。其实还有一个原因就是go比c好写,alloc和malloc写起来,真的难受。
工具 goyacc
go git github.com/golang/tools/cmd/goyacc
go build
go intall
3.1 lex与yacc工作流程
go提供goyacc用于语法分析。在我们写好一个yacc规则后,使用goyacc sql.y指令生成对用的go文件。其中包含两个重要的对象
type yyLexer interface {
Lex(lval *yySymType) int
Error(s string)
} type yyParser interface {
Parse(yyLexer) int
Lookahead() int
}
yyparser是yacc自动实现的,不需要我们操作,parser是入口函数,它会不停的调用Lex函数来获取TOKEN进行文法分析。我们需要自己实现一个Lex即实现yyLexer接口。
Lex实现
type Tokenizer struct {
query Query
scanner *Scanner
}
func (tkn *Tokenizer) Lex(lval *yySymType) int{
var typ int
var val string
for {
typ, _, val = tkn.scanner.Scan()
if typ == EOF{
return 0
}
if typ !=WS{
break
}
}
lval.str = val
return typ
}
func (tkn *Tokenizer) Error(err string){
log.Fatal(err)
}
我们调用scan函数来获取一个字段的typ也就是TOKEN和该字符串val。虽然返回值只有token,但是我们将val的值根据其不同类型也传给了yacc。需要注意两点,(1)如果读取到文件末尾,需要返回0,则parser就会知道已完结。(2)文法分析中不包含空格,所以读取到空格时忽略知道读取到非空格返回TOKEN。scan的实现很简单也是一种状态自动机。但代码量不小,此处就不放出来了。
3.2 yacc写法
go语言的yacc和c的yacc格式并无不同,也都是由三部分构成,但一般的我们只写规则部分,函数代码最好单独放到go文件中。
出于方便阅读考虑,sql.y文件放在最后。我们分析一下该文件的意义。
3.2.1 第一部分
由%{…}%概括起来的部分会完全作为源代码保留。
3.2.2 第二部分
接下里我们看%% …%%部分。该部分是文法的规则,仔细看一下不难理解。该文法规则由终结符和非终结符组成。终结符就是不可再拆分的状态,非终结符就是可以拆分的状态。更加具体的,我们的文本每一个字段都是一个终结符,那么有人会问,为什么还要有非终结符呢。其实非终结符的作用有两个方面1.适用递归下降;2.模块化逻辑。
对于递归下降,比如我们的语法有(1)select a;(2)select a,b; (3)select a,b,c;
我们可以发现这三条语句基本一致,这是标记的个数不同,我们当然可以写三条不同的语法,但明显这是很蠢的,所以我们引入右递归。用
ids = id,ids | id
这样的形式表示,这时ids就叫非终结符,而id就是终结符
对于模块化,比如 语句from name,我们直达from之后总会跟随至少一个标示符,此时我们就可以把这两部分合为一部分。
FROM_CLAUSE = from name
FROM_CLAUSE 就叫做非终结符
文法的表述形式一般为
NONTEMINAL:
P1 P2 P3
{
$$ = &struct{a:$1,b:$2,c:$3}
}
|S1 S2 S3
{
}
|{
}
$$ 表示该终结符,分解的表达式从左到右依次为$1,$2,$3
3.2.3 第三部分
union 其实我也不知道是什么,反正定义一些常用的数据类型,因为我们在文法阶段构建语义树不可避免需要用到数据类型。
%token type 我们要记得token是扫描的字符串的表示符,而该字符串可能是数字,字符,以及其他,所以我们需要对不同的token标记不同的类型,其实是给该字段标记类型。对于一些不参与语义构建的关键字,我们可以省略类型。
%type type 部分非终结符代表语义树的某些值,需要明确指出该类型。
4 sql.y文件
该文件引用的相关数据结构与influxdb的ast一致,可查看inflxudb的ast.go文件做参考。scan函数用的influxdb的scan逻辑,相关代码做了调整,具体代码放在github上,等上传了再贴地址。
sql.y文件
%{
package influxqlyacc
import (
"time"
)
func setParseTree(yylex interface{},stmt Statement){
yylex.(*Tokenizer).query.Statements = append(yylex.(*Tokenizer).query.Statements,stmt)
}
%}
%union{
stmt Statement
stmts Statements
selStmt *SelectStatement
sdbStmt *ShowDatabasesStatement
cdbStmt *CreateDatabaseStatement
smmStmt *ShowMeasurementsStatement
str string
query Query
field *Field
fields Fields
sources Sources
sortfs SortFields
sortf *SortField
ment *Measurement
dimens Dimensions
dimen *Dimension
int int
int64 int64
float64 float64
expr Expr
tdur time.Duration
bool bool
}
%token <str> SELECT FROM WHERE AS GROUP BY ORDER LIMIT SHOW CREATE
%token <str> DATABASES DATABASE MEASUREMENTS
%token <str> COMMA SEMICOLON
%token <int> MUL
%token <int> EQ NEQ LT LTE GT GTE
%token <str> IDENT
%token <int64> INTEGER
%token <tdur> DURATIONVAL
%token <str> STRING
%token <bool> DESC ASC
%token <float64> NUMBER
%left <int> AND OR
%type <stmt> STATEMENT
%type <sdbStmt> SHOW_DATABASES_STATEMENT
%type <cdbStmt> CREATE_DATABASE_STATEMENT
%type <selStmt> SELECT_STATEMENT
%type <smmStmt> SHOW_MEASUREMENTS_STATEMENT
%type <fields> COLUMN_NAMES
%type <field> COLUMN_NAME
%type <stmts> ALL_QUERIES
%type <sources> FROM_CLAUSE TABLE_NAMES
%type <ment> TABLE_NAME
%type <dimens> DIMENSION_NAMES GROUP_BY_CLAUSE
%type <dimen> DIMENSION_NAME
%type <expr> WHERE_CLAUSE CONDITION CONDITION_VAR OPERATION_EQUAL
%type <int> OPER LIMIT_INT
%type <sortfs> SORTFIELDS ORDER_CLAUSES
%type <sortf> SORTFIELD
%%
ALL_QUERIES:
STATEMENT
{
setParseTree(yylex, $1)
}
| STATEMENT SEMICOLON
{
setParseTree(yylex, $1)
}
| STATEMENT SEMICOLON ALL_QUERIES
{
setParseTree(yylex, $1)
}
STATEMENT:
SELECT_STATEMENT
{
$$ = $1
}
|SHOW_DATABASES_STATEMENT
{
$$ = $1
}
|CREATE_DATABASE_STATEMENT
{
$$ = $1
}
|SHOW_MEASUREMENTS_STATEMENT
{
$$ = $1
}
SELECT_STATEMENT:
//SELECT COLUMN_NAMES
//SELECT COLUMN_NAMES FROM_CLAUSE GROUP_BY_CLAUSE WHERE_CLAUSE ORDER_CLAUSES INTO_CLAUSE
SELECT COLUMN_NAMES FROM_CLAUSE GROUP_BY_CLAUSE WHERE_CLAUSE ORDER_CLAUSES LIMIT_INT
{
sel := &SelectStatement{}
sel.Fields = $2
//sel.Target = $7
sel.Sources = $3
sel.Dimensions = $4
sel.Condition = $5
sel.SortFields = $6
sel.Limit = $7
$$ = sel
}
COLUMN_NAMES:
COLUMN_NAME
{
$$ = []*Field{$1}
}
|COLUMN_NAME COMMA COLUMN_NAMES
{
$$ = append($3,$1)
}
COLUMN_NAME:
MUL
{
$$ = &Field{Expr:&Wildcard{Type:$1}}
}
|IDENT
{
$$ = &Field{Expr:&VarRef{Val:$1}}
}
|IDENT AS IDENT
{
$$ = &Field{Expr:&VarRef{Val:$1},Alias:$3}
}
FROM_CLAUSE:
FROM TABLE_NAMES
{
$$ = $2
}
|
{
$$ = nil
}
TABLE_NAMES:
TABLE_NAME
{
$$ = []Source{$1}
}
|TABLE_NAME COMMA TABLE_NAMES
{
$$ = append($3,$1)
}
TABLE_NAME:
IDENT
{
$$ = &Measurement{Name:$1}
}
GROUP_BY_CLAUSE:
GROUP BY DIMENSION_NAMES
{
$$ = $3
}
|
{
$$ = nil
}
DIMENSION_NAMES:
DIMENSION_NAME
{
$$ = []*Dimension{$1}
}
|DIMENSION_NAME COMMA DIMENSION_NAMES
{
$$ = append($3,$1)
}
DIMENSION_NAME:
IDENT
{
$$ = &Dimension{Expr:&VarRef{Val:$1}}
}
WHERE_CLAUSE:
WHERE CONDITION
{
$$ = $2
}
|
{
$$ = nil
}
CONDITION:
OPERATION_EQUAL
{
$$ = $1
}
|CONDITION AND CONDITION
{
$$ = &BinaryExpr{Op:$2,LHS:$1,RHS:$3}
}
|CONDITION OR CONDITION
{
$$ = &BinaryExpr{Op:$2,LHS:$1,RHS:$3}
}
OPERATION_EQUAL:
CONDITION_VAR OPER CONDITION_VAR
{
$$ = &BinaryExpr{Op:$2,LHS:$1,RHS:$3}
}
OPER:
EQ
{
$$ = $1
}
|NEQ
{
$$ = $1
}
|LT
{
$$ =$1
}
|LTE
{
$$ = $1
}
|GT
{
$$ = $1
}
|GTE
{
$$ = $1
}
CONDITION_VAR:
IDENT
{
$$ = &VarRef{Val:$1}
}
|NUMBER
{
$$ = &NumberLiteral{Val:$1}
}
|INTEGER
{
$$ = &IntegerLiteral{Val:$1}
}
|DURATIONVAL
{
$$ = &DurationLiteral{Val:$1}
}
|STRING
{
$$ = &StringLiteral{Val:$1}
}
ORDER_CLAUSES:
ORDER BY SORTFIELDS
{
$$ = $3
}
|
{
$$ = nil
}
SORTFIELDS:
SORTFIELD
{
$$ = []*SortField{$1}
}
|SORTFIELD COMMA SORTFIELDS
{
$$ = append($3,$1)
}
SORTFIELD:
IDENT
{
$$ = &SortField{Name:$1}
}
|IDENT DESC
{
$$ = &SortField{Name:$1,Ascending:$2}
}
|IDENT ASC
{
$$ = &SortField{Name:$1,Ascending:$2}
}
LIMIT_INT:
LIMIT INTEGER
{
$$ = int($2)
}
|
{
$$ = 0
}
SHOW_DATABASES_STATEMENT:
SHOW DATABASES
{
$$ = &ShowDatabasesStatement{}
}
CREATE_DATABASE_STATEMENT:
CREATE DATABASE IDENT
{
$$ = &CreateDatabaseStatement{Name:$3}
}
SHOW_MEASUREMENTS_STATEMENT:
SHOW MEASUREMENTS WHERE_CLAUSE ORDER_CLAUSES LIMIT_INT
{
sms := &ShowMeasurementsStatement{}
sms.Condition = $3
sms.SortFields = $4
sms.Limit = $5
$$ = sms
}
%%