編譯原理之lex,yacc學習


寫在前面的幾句廢話

最近在項目的過程中接觸了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能幫我們做什么?

一句話:解釋執行自定義語言。有幾點要注意:

  1. 自定義語言的要做的事情必須可以能通過C語言來實現。其實任何計算機能做的事情都可以用C語言來實現,lex和yacc存在的意義在於簡化語言,讓使用者能夠以一種用比較簡單的語言來實現復雜的操作。比如:對於數據庫的查詢肯定有現成的庫可以來完成,但是使用起來比較麻煩,要自己寫成語調用API,編譯才行。如果我們想實自定義一個簡單的語言(比如SQL)來實現操作,這個時候就可以用lex和yacc。
  2. lex和yacc 做的事情只是:用C語言來實現另外一種語言。所以,他沒辦法實現C語言自己,但是可以實現java、python等。當然你可以通過Antlr來實現C語言的解析和執行,如果你這么做的話,C語言程序首先是通過java來執行,然后java又變成了本地語言(C語言)來執行,誰叫我們的操作系統都是C語言實現的呢。

使用lex和yacc我們要做那幾件事情?

  1. 定義各種token類型。他們在.y中定義,這些token既會被lex使用到,也會被.y文件中的BNF使用到。
  2. 寫詞匯分析代碼。這部分代碼在.l文件(就是lex的輸入文件)中。這塊的定義方式是:正則表達式-->對應操作。如果和yacc一起來使用的話,對應的操作通常是返回一個token類型,這個token的類型要在yacc中提前定義好。
  3. 寫BNF。這些東西定義了語言的規約方式。

關於BNF

是一種context-free grammars,請參考:http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form 摘錄:

<symbol> ::= __expression__

  1. <symbol> is a nonterminal
  2. __expression__ consists of one or more sequences of symbols
  3. more sequences are separated by the vertical bar, '|'
  4. Symbols that never appear on a left side are terminals. On the other hand
  5. 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

  1. expr2 expr3都是BNF中定義的non-terminal
  2. PLUS和MINUS都是.y中定義的token類
  3. plus和minus 是事先定義好的C語言函數

關於yacc中BNF的推導過程引用后面的《lex和yacc簡明教程》做一下說明:

  1. yacc 在內部維護着兩個堆棧;一個分析棧和一個內容棧。分析棧中保存着終結符和非終結符,並且代表當前剖析狀態。內容棧是一個YYSTYPE 元素的數組,對於分析棧中的每一個元素都保存着一個對應的值。例如,當yylex 返回一個INTEGER標記時,y acc 把這個標記移入分析棧。同時,相應的yylval 值將會被移入內容棧中。分析棧和內容棧的內容總是同步的,因此從棧中找到對應於一個標記的值是很容易實現的。
  2. 對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中一些預定義的東西

Lex 變量

yyin
FILE* 類型。 它指向 lexer 正在解析的當前文件。

yyout
FILE* 類型。 它指向記錄 lexer 輸出的位置。 缺省情況下,yyin 和 yyout 都指向標准輸入和輸出。

yytext
匹配模式的文本存儲在這一變量中(char*)。

yyleng
給出匹配模式的長度。

yylineno
提供當前的行數信息。 (lexer不一定支持。)

Lex 函數

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM