goyacc

lex & yacc

項目github地址

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 就叫做非終結符

文法的表述形式一般爲

NONTEMINALP1 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
    }
%%
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章