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
}
%%