Go 語言 SQL 解析之 goyacc 實戰
【導讀】本文介紹了語法解析基本概念,結合 goyacc 實戰做了詳細介紹。
一、Lex & Yacc 簡介
Lex & Yacc 是用來生成詞法分析器和語法分析器的工具,與 GNU 用戶所熟知 Flex&Bison 所對應(Flex 是由 Vern Paxon 實現的一個 Lex,Bison 則是 GNU 版本的 YACC)。除了編譯器領域,Lex & Yacc 對於 DSL 或者 SQL 解析領域也大有用處。
Lex (A Lexical Analyzer Generator)用於生成詞法分析器,用於把輸入分割成一個個有意義的詞塊(稱爲 token)。
Yacc(Yet Another Compiler-Compiler)用於生成語法解析器,用於確定上述分隔好的 token 之間的關聯。
下圖描述了整個處理的流程:
-
Lex 根據輸入的 patterns 生成詞法分析器。
-
詞法分析器根據 patterns 將輸入的源代碼轉換爲 tokens 輸出。
-
Yacc 根據定義的語法規則,生成語法分析器。
-
語法分析器根據語法規則,並以 tokens 爲輸入,創建出語法樹。
二、詞法分析器
詞法分析器用於獲取一個 token 流。通過調用 yylex() 讀取輸入,然後返回相應的 token,之後循環調用,持續返回解析好的 token。每個 token 有兩部分組成:
-
token 類型,一個整數值。該值爲 yylex() 的返回值。
-
token 取值。結果保存到 yylval 中。
如下是計算器詞法分析器的規則定義。該定義由三部分組成,由 %% 分隔。
-
第一部分爲聲明部分。%{、%} 之間的代碼,會被原樣拷貝到 C 文件中。其中定義了 token 類型,以及 token 值的變量
-
第二部分模式匹配部分。每行開頭是匹配的模式,接着是要執行的代碼。
-
例如,[0-9]+ 行表示:當匹配爲數字時,則轉換成整型複製給 yylval,並返回 token 類型爲 NUMBER。
-
第三部分爲主程序代碼,負責調用詞法分析函數 yylex(),並輸出結果。
// 計算器詞法分析器
%{
enum yytokentype {
NUMBER = 258,
ADD = 259,
SUB = 260,
MUL = 261,
DIV = 262,
ABS = 263,
EOL = 264
};
int yylval;
%}
%%
"+" {return ADD;}
"-" {return SUB;}
"*" {return MUL;}
"/" {return DIV;}
"|" {return ABS;}
[0-9]+ {yylval = atoi(yytext); return NUMBER;}
\n {return EOL;}
[ \t] {/*忽略空白字符*/}
%%
main(int argc, char **argv) {
int tok;
while(tok = yylex()) {
printf("%d", tok);
if (tok == NUMBER) {
printf(" = %d\n", yylval);
} else {
print("\n");
}
}
}
// 運行
/// 輸入:
34 + 45
/// 輸出:
258 = 34
259
258 = 45
三、語法分析器
語法分析器用於找出輸入 token 之間的關係,使用巴科斯範式(BNF, Backus Naur Form)來書寫。同詞法分析器規則一樣,也是三部分組成。
-
聲明部分:
-
%{、%} 聲明部分原樣拷貝到 C 文件。
-
%token 聲明,用於告訴 yacc 在語法分析程序中 token 的類型。任何沒有聲明爲 token 的語法符號,必須出現在至少一條規則的左邊。
-
規則部分:通過 BNF 定義規則。每一條規則中語法符號都有一個語義值。
-
目標符號(冒號左邊的語法符號)用 $$ 代替。
-
右邊語法符號的語義值以此用 12 代替。
-
C 代碼部分
樣例:
%{
#include <stdio.h>
%}
/*declare tokens*/
%token NUMBER
%token ADD SUB MUL DIV ABS
%token EOL
%%
calclist: /*空規則*/
| calclist exp EOL { printf("= %d\n", $2); }
;
exp: factor defalut $$ = $1
| exp ADD factor { $$ = $1 + $3; }
| exp SUB factor { $$ = $1 - $3; }
;
factor: term default $$ = $1
| factor MUL term { $$ = $1 * $3; }
| factor DIV term { $$ = $1 / $3; }
;
term: NUMBER default $$ = $1
| ABS term { $$ = $2 >=0 ? $2 : - $2; }
;
%%
main(int argc, char **argv) {
yyparse();
}
yyerror(char *s) {
fprintf(stderr, "error: %s\n", s);
}
四、yacc 語法規範
整體結構
由三部分組成,其中前兩部分必須。
...定義部分...
%%
...規則部分...
%%
...用戶子程序...
符號
由詞法分析器產生的符號稱爲終結符或者 token。
在規則左側定義的的符號稱爲非終結符。
定義部分
-
%{%} 之間定義的文字塊,通常被原樣拷貝到 C 代碼中。
-
聲明部分
-
%start
-
起始規則,即語法分析器首先分析的規則。默認是第一條規則,如果希望其他規則作爲起始規則,需要增加聲明:
%start somename
-
%token
-
也可以叫終結符,是詞法分析器傳遞給語法分析器的符號。
-
語法分析器調用 yylex() 獲取下一個 token。
-
%left、%right、%nonassoc:
-
定義了運算符的結合性,同一行的運算符優先級相同,不同行的運算符,後定義的行具有更高的優先級。
-
%type
-
使用 %type 來聲明非終結符。
-
%type 的名字必須以 %union 定義過。
-
%union
-
%union 聲明標示了符號值所有的變量類型。
-
%union 聲明將原樣拷貝到以 YYSTYPE 爲類型的 C 的 union 聲明中。
-
如果不存在 %union 聲明,yacc 將把 YYSTYPE 定義爲 int,這樣所有的符號值都是 int。
-
%type 把 %union 中聲明的類型與特定的符號關聯起來。
規則部分
包括語法規則和語義動作代碼。動作即 yacc 匹配語法中的一條規則時之行的代碼。例如:
date: month '/' day '/' year { $$ = sprintf("date %d-%d-%d", $1, $3, $5); }
用戶子程序
原樣被拷貝到 C 文件的代碼片段。
移進 / 歸約
移進(shift):語法分析器讀取 token 時,當讀取的 token 無法匹配一條規則時,則將其壓入一個內部堆棧 (union 爲壓入堆棧的結構體),然後切換到一個新的狀態,這個新的狀態能夠反映出剛剛讀取的 token。
歸約(reduction):當發現壓入的 token 已經能夠匹配規則時,則把匹配的符號從堆棧中取出,然後將左側符號壓入堆棧。
例如,fred = 12 + 13
的處理過程:
// 規則
statement: NAME '=' expression
expression: NUMBER + NUMBER
// 處理過程
fred
fred =
fred = 12
fred = 12 +
fred = 12 + 13 //匹配expression: NUMBER + NUMBER,可以歸約
fred = expression //匹配statement: NAME '=' expression
statement
但是如果引入乘法後,可能會產生衝突,造成如下不同的語法結構。
對於這種情況需要顯示指定優先級和結合性來解決衝突:
-
優先級:決定了那個操作符先執行。例如,乘除法的優先級高於加減法。
-
結合性:表示具有相同優先級的操作符的結合順序。
-
自左向右:a-b-c 等價於 (a-b)-c。
-
自右向左:a=b=c 等價於 a=(b=c)。
// 意義:+ - * /都是左結合,* /的優先級高於+ -。
%left '+' '-'
%left '*' '/'
五、goyacc
github.com/cznic/goyacc 是 golang 版的 Yacc
。和 Yacc
的功能一樣,goyacc
根據輸入的語法規則文件,生成該語法規則的 go 語言版解析器。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 中(與 yacc 的 yylval 對應)。Error 相當於原 yacc 中的 yyerror。
六、goyacc 樣例
電話號碼解析
源代碼:github.com/sougou/parser_tutorial/tree/master/v3
-
功能實現
-
支持電話號碼 4085551212、408-555-1212 解析。
-
語法解析器定義 parser.y(github.com/sougou/parser_tutorial/blob/master/v3/parser.y)
# 定義部分
%{
package jsonparser
func setResult(l yyLexer, v Result) {
l.(*lex).result = v
}
%}
%union{
result Result
part string
ch byte
}
%token <ch> D
%type <result> phone
%type <part> area part1 part2
%start main
%%
# 規則部分
main: phone
{
setResult(yylex, $1)
}
phone:
area part1 part2
{
$$ = Result{area: $1, part1: $2, part2: $3}
}
| area '-' part1 '-' part2
{
$$ = Result{area: $1, part1: $3, part2: $5}
}
area: D D D
{
$$ = cat($1, $2, $3)
}
part1: D D D
{
$$ = cat($1, $2, $3)
}
part2: D D D D
{
$$ = cat($1, $2, $3, $4)
}
- 生成 parser.go (goyacc -l -o parser.go parser.y 自動生成)。比較關鍵的定義:
// 對應.y中union的結構體
type yySymType struct {
yys int
result Result
part string
ch byte
}
//token類型
const (
yyDefault = 57347
yyEofCode = 57344
D = 57346
yyErrCode = 57345
yyMaxDepth = 200
yyTabOfs = -7
)
//詞法分析器接口
type yyLexer interface {
Lex(lval *yySymType) int
Error(s string)
}
type yyLexerEx interface {
yyLexer
Reduced(rule, state int, lval *yySymType) bool
}
//語法解析入口
func yyParse(yylex yyLexer) int {
}
- 詞法解析定義 lex.go,這裏沒有采用 lex 定義。
// Result is the type of the parser result
type Result struct {
area string
part1 string
part2 string
}
// 解析的總入口。輸入電話號碼,輸出爲Result的解析結果。
func Parse(input []byte) (Result, error) {
l := newLex(input)
_ = yyParse(l)
return l.result, l.err
}
// 手動定義詞法解析器
type lex struct {
input *bytes.Buffer
result Result
err error
}
func newLex(input []byte) *lex {
return &lex{
input: bytes.NewBuffer(input),
}
}
// 詞法分析器滿足Lex接口。能夠逐個讀取token。
// 如果是有效的電話號碼數字,返回D類型,並賦值lval.ch。
func (l *lex) Lex(lval *yySymType) int {
b := l.nextb()
if unicode.IsDigit(rune(b)) {
lval.ch = b
return D
}
return int(b)
}
func (l *lex) nextb() byte {
b, err := l.input.ReadByte()
if err != nil {
return 0
}
return b
}
// Error satisfies yyLexer.
func (l *lex) Error(s string) {
l.err = errors.New(s)
}
func cat(bytes ...byte) string {
var out string
for _, b := range bytes {
out += string(b)
}
return out
}
轉自: zhuanlan.zhihu.com/p/264367718
Go 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qSbftFcRfigqEl_2-bfw3A