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