寫在前面的幾句廢話
最近在項目的過程中接觸了lex 和 yacc,他們可以幫助我們來實現自己的領域語言。最典型的應用就是可以幫助我們來實現自定義測試腳本的執行器。但是,這里也有一個限制,就是測試腳本要做的基本事情必須有現成的C語言庫來實現,否則就做不到了;如果基本的操作是用java來做的,那么還可以用Antlr,這里不對Antlr做詳細介紹。
lex是什么?
教科書上把lex的作用的作用叫做“詞法分析 lexical analysis ”,這個中文叫法非常讓人看不明白(叫做“符號提取”更合適),其實從它的英文單詞lexical上來看他的意思其實是非常清楚的。
lexical,在webster上的解釋是:of or relating to words or the vocabulary of a language as distinguished from its grammar and construction。
指的是: 一種語言中關於詞匯、單詞的,與之相對的是這種語言的語法和組織
這么來看的話 lexical analysis 的作用就應該是語言中的詞匯和單詞分析。事實上他的作用就是從語言中提取單詞。放到編程語言中來說,他要做的事情其實就是提取編程語言占用的各種保留字、操作符等等語言的元素。
所以他的另外一個名字scanner其實更形象一些,就是掃描一個文本中的單詞。
lex把每個掃面出來的單詞叫統統叫做token,token可以有很多類。對比自然語言的話,英語中的每個單詞都是token,token有很多類,比如non(名詞)就是一個類token,apple就是屬於這個類型的一個具體token。對於某個編程語言來說,token的個數是很有限的,不像英語這種自然語言中有幾十萬個單詞。
lex工具會幫我們生成一個yylex函數,yacc通過調用這個函數來得知拿到的token是什么類型的,但是token的類型是在yacc中定義的。
lex的輸入文件一般會被命名成 .l文件,通過lex XX.l 我們得到輸出的文件是lex.yy.c
yacc是什么呢?
剛才說完lex了,那么yacc呢,教科書上把yacc做的工作叫做syntactic analysis。這次我們翻譯沒有直譯做句法分析,而是叫語法分析,這個翻譯能好一點,意思也基本上比較清楚。
其實我們最開始學習英語的時候老師都會告訴我們英語其實就是“單詞+語法”,這個觀點放到編程語言中很合適,lex提取了單詞,那么是剩下的部分就是如何表達語法。那么yacc做的事情就是這一部分(實際應該說是BNF來做的)。
yacc會幫我們生成一個yyparse函數,這個函數會不斷調用上面的yylex函數來得到token的類型。
yacc的輸入文件一般會被命名成 .y文件,通過yacc -d XX.y我們得到的輸出文件是y.tab.h y.tab.c,前者包含了lex需要的token類型定義,需要被include進 .l文件中
lex和yacc的輸入文件格式
Definition section
%%
Rules section
%%
C code section
.l和.y的文件格式都是分成三段,用%%來分割,三個section的含義是:
- Definition Section
這塊可以放C語言的各種各種include,define等聲明語句,但是要用%{ %}括起來。
如果是.l文件,可以放預定義的正則表達式:minus "-" 還要放token的定義,方法是:代號 正則表達式。然后到了,Rules Section就可以通過{符號} 來引用正則表達式
如果是.y文件,可以放token的定義,如:%token INTEGER PLUS ,這里的定一個的每個token都可以在y.tab.h中看到
- Rules section
.l文件在這里放置的rules就是每個正則表達式要對應的動作,一般是返回一個token
.y文件在這里放置的rules就是滿足一個語法描述時要執行的動作
不論是.l文件還是.y文件這里的動作都是用{}擴起來的,用C語言來描述,這些代碼可以做你任何想要做的事情
- C code Section
main函數,yyerror函數等的定義
lex和yacc能幫我們做什么?
一句話:解釋執行自定義語言。有幾點要注意:
- 自定義語言的要做的事情必須可以能通過C語言來實現。其實任何計算機能做的事情都可以用C語言來實現,lex和yacc存在的意義在於簡化語言,讓使用者能夠以一種用比較簡單的語言來實現復雜的操作。比如:對於數據庫的查詢肯定有現成的庫可以來完成,但是使用起來比較麻煩,要自己寫成語調用API,編譯才行。如果我們想實自定義一個簡單的語言(比如SQL)來實現操作,這個時候就可以用lex和yacc。
- lex和yacc 做的事情只是:用C語言來實現另外一種語言。所以,他沒辦法實現C語言自己,但是可以實現java、python等。當然你可以通過Antlr來實現C語言的解析和執行,如果你這么做的話,C語言程序首先是通過java來執行,然后java又變成了本地語言(C語言)來執行,誰叫我們的操作系統都是C語言實現的呢。
使用lex和yacc我們要做那幾件事情?
- 定義各種token類型。他們在.y中定義,這些token既會被lex使用到,也會被.y文件中的BNF使用到。
- 寫詞匯分析代碼。這部分代碼在.l文件(就是lex的輸入文件)中。這塊的定義方式是:正則表達式-->對應操作。如果和yacc一起來使用的話,對應的操作通常是返回一個token類型,這個token的類型要在yacc中提前定義好。
- 寫BNF。這些東西定義了語言的規約方式。
關於BNF
是一種context-free grammars,請參考:http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form 摘錄:
<symbol> ::= __expression__
- <symbol> is a nonterminal
- __expression__ consists of one or more sequences of symbols
- more sequences are separated by the vertical bar, '|'
- Symbols that never appear on a left side are terminals. On the other hand
- symbols that appear on a left side are non-terminals and are always enclosed between the pair <>.
在yacc中定義的方式其實是:
<symbol> : __expression__ {operation}
| __expression__ {operation}
operation 是 滿足語法時要執行的C語言代碼,這里的C語言代碼可以使用一些變量,他們是:$$ $1 $2等等。$$代表規約的結果,就是表達式__expression__的值,$1代表的是前面 __expression__ 中出現的各個word。舉個例子:
expr2:
expr3 { $$ == $1; }
| expr2 PLUS expr3 { $$ = plus($1, $3); }
| expr2 MINUS expr3 { $$ = minus($1, $3); }
;
來自:http://memphis.compilertools.net/interpreter.html
- expr2 expr3都是BNF中定義的non-terminal
- PLUS和MINUS都是.y中定義的token類
- plus和minus 是事先定義好的C語言函數
關於yacc中BNF的推導過程引用后面的《lex和yacc簡明教程》做一下說明:
- yacc 在內部維護着兩個堆棧;一個分析棧和一個內容棧。分析棧中保存着終結符和非終結符,並且代表當前剖析狀態。內容棧是一個YYSTYPE 元素的數組,對於分析棧中的每一個元素都保存着一個對應的值。例如,當yylex 返回一個INTEGER標記時,y acc 把這個標記移入分析棧。同時,相應的yylval 值將會被移入內容棧中。分析棧和內容棧的內容總是同步的,因此從棧中找到對應於一個標記的值是很容易實現的。
- 對expr: expr '+' expr { $$ = $1 + $3; }來說,在分析棧中我們其實用左式替代了右式。在本例中,我們彈出“expr '+' expr” 然后壓入“expr”。 我們通過彈出三個成員,壓入一個成員縮小的堆棧。在我們的C 代碼中可以用通過相對地址訪問內容棧中的值,“ $1”代表右式中的第一個成員,“ $2”代表第二個,后面的以此類推。“ $$ ”表示縮小后的堆棧的頂部。在上面的動作中,把對應兩個表達式的值相加,彈出內容棧中的三個成員,然后把造得到的和壓入堆棧中。這樣,分析棧和內容棧中的內容依然是同步的。
來看一個用lex和yacc實現計算器的例子
參考了下面鏈接的lex和yacc文件:http://blog.csdn.net/crond123/article/details/3932014
cal.y
%{ #include <stdio.h> #include "lex.yy.c" #define YYSTYPE int int yyparse(void); %} %token INTEGER PLUS MINUS TIMES DIVIDE LP RP %% command : exp {printf("%d/n",$1);} exp: exp PLUS term {$$ = $1 + $3;} |exp MINUS term {$$ = $1 - $3;} |term {$$ = $1;} ; term : term TIMES factor {$$ = $1 * $3;} |term DIVIDE factor {$$ = $1/$3;} |factor {$$ = $1;} ; factor : INTEGER {$$ = $1;} | LP exp RP {$$ = $2;} ; %% int main() { return yyparse(); } void yyerror(char* s) { fprintf(stderr,"%s",s); } int yywrap() { return 1; }
cal.l
%{ #include<string.h> #include "y.tab.h" extern int yylval; %} numbers ([0-9])+ plus "+" minus "-" times "*" divide "/" lp "(" rp ")" delim [ /n/t] ws {delim}* %% {numbers} {sscanf(yytext, "%d", &yylval); return INTEGER;} {plus} {return PLUS;} {minus} {return MINUS;} {times} {return TIMES;} {divide} {return DIVIDE;} {lp} {return LP;} {rp} {return RP;} {ws} ; . {printf("Error");exit(1);} %%
使用方式:
yacc -d cal.y
lex cal.l
g++ -o cal y.tab.c
運行./cal 然后輸入3+4 ctrl+D就可以看到結果了
關於lex和yacc中一些預定義的東西
yyin
FILE* 類型。 它指向 lexer 正在解析的當前文件。
yyout
FILE* 類型。 它指向記錄 lexer 輸出的位置。 缺省情況下,yyin 和 yyout 都指向標准輸入和輸出。
yytext
匹配模式的文本存儲在這一變量中(char*)。
yyleng
給出匹配模式的長度。
yylineno
提供當前的行數信息。 (lexer不一定支持。)
yylex()
這一函數開始分析。 它由 Lex 自動生成。
yywrap()
這一函數在文件(或輸入)的末尾調用。 如果函數的返回值是1,就停止解析。 因此它可以用來解析多個文件。 代碼可以寫在第三段,這就能夠解析多個文件。 方法是使用 yyin 文件指針(見上表)指向不同的文件,直到所有的文件都被解析。 最后,yywrap() 可以返回 1 來表示解析的結束。
yyless(int n)
這一函數可以用來送回除了前�n? 個字符外的所有讀出標記。
yymore()
這一函數告訴 Lexer 將下一個標記附加到當前標記后。
參考資料:
首先推薦《lex and yacc tutorial》 http://epaperpress.com/lexandyacc/download/LexAndYaccTutorial.pdf
上面pdf的中文版《lex和yacc簡明教程》在在:http://ishare.iask.sina.com.cn/f/22266803.html
http://memphis.compilertools.net/interpreter.html
http://www.ibm.com/developerworks/cn/linux/sdk/lex/
http://hi.baidu.com/kuangxiangjie/blog/item/b4a11c46e333e60e6b63e5fa.html
一個老外寫的上手教程
http://www.ibm.com/developerworks/library/l-lexyac/index.html
http://www.ibm.com/developerworks/linux/library/l-lexyac2/index.html
這兩個用 lex 和 yacc實現了 c語言解釋器
http://www.lysator.liu.se/c/ANSI-C-grammar-y.html
http://www.lysator.liu.se/c/ANSI-C-grammar-l.html
http://www.ibm.com/developerworks/cn/linux/game/sdl/pirates-4/index.html