IBM----Yacc 与 Lex 快速入门
Lex?和?Yacc?是?UNIX?兩個非常重要的、功能強大的工具。事實上,如果你熟練掌握?Lex?和?Yacc?的話,它們的強大功能使創建?FORTRAN?和?C?的編譯器如同兒戲。Ashish Bansal?為您詳細的討論了編寫自己的語言和編譯器所用到的這兩種工具,包括常規表達式、聲明、匹配模式、變量、Yacc?語法和解析器代碼。最后,他解釋了怎樣把?Lex?和?Yacc?結合起來。
Lex?代表?Lexical Analyzar。Yacc?代表?Yet Another Compiler Compiler。 讓我們從?Lex?開始吧。
?
Lex
Lex?是一種生成掃描器的工具。掃描器是一種識別文本中的詞匯模式的程序。這些詞匯模式(或者常規表達式)在一種特殊的句子結構中定義,這個我們一會兒就要討論。
一種匹配的常規表達式可能會包含相關的動作。這一動作可能還包括返回一個標記。當?Lex?接收到文件或文本形式的輸入時,它試圖將文本與常規表達式進行匹配。它一次讀入一個輸入字符,直到找到一個匹配的模式。如果能夠找到一個匹配的模式,Lex?就執行相關的動作(可能包括返回一個標記)。另一方面,如果沒有可以匹配的常規表達式,將會停止進一步的處理,Lex?將顯示一個錯誤消息。
Lex?和?C?是強耦合的。一個?.lex?文件(Lex?文件具有?.lex?的擴展名)通過?lex?公用程序來傳遞,并生成?C?的輸出文件。這些文件被編譯為詞法分析器的可執行版本。
?
Lex?的常規表達式
常規表達式是一種使用元語言的模式描述。表達式由符號組成。符號一般是字符和數字,但是?Lex?中還有一些具有特殊含義的其他標記。下面兩個表格定義了?Lex?中使用的一些標記并給出了幾個典型的例子。
用?Lex?定義常規表達式
| 字符 | 含義 |
| A-Z, 0-9, a-z | 構成了部分模式的字符和數字。 |
| . | 匹配任意字符,除了?\n。 |
| - | 用來指定范圍。例如:A-Z?指從?A?到?Z?之間的所有字符。 |
| [ ] | 一個字符集合。匹配括號內的?任意?字符。如果第一個字符是?^?那么它表示否定模式。例如: [abC]?匹配?a, b,?和?C中的任何一個。 |
| * | 匹配?0個或者多個上述的模式。 |
| + | 匹配?1個或者多個上述模式。 |
| ? | 匹配?0個或1個上述模式。 |
| $ | 作為模式的最后一個字符匹配一行的結尾。 |
| { } | 指出一個模式可能出現的次數。 例如: A{1,3}?表示?A可能出現1次或3次。 |
| \ | 用來轉義元字符。同樣用來覆蓋字符在此表中定義的特殊意義,只取字符的本意。 |
| ^ | 否定。 |
| | | 表達式間的邏輯或。 |
| "<一些符號>" | 字符的字面含義。元字符具有。 |
| / | 向前匹配。如果在匹配的模版中的“/”后跟有后續表達式,只匹配模版中“/”前面的部分。如:如果輸入A01,那么在模版?A0/1?中的?A0?是匹配的。 |
| ( ) | 將一系列常規表達式分組。 |
?
常規表達式舉例
| 常規表達式 | 含義 |
| joke[rs] | 匹配?jokes?或?joker。 |
| A{1,2}shis+ | 匹配?AAshis, Ashis, AAshi, Ashi。 |
| (A[b-e])+ | 匹配在?A?出現位置后跟隨的從?b?到?e?的所有字符中的?0?個或?1個。 |
?
Lex?中的標記聲明類似?C?中的變量名。每個標記都有一個相關的表達式。(下表中給出了標記和表達式的例子。)使用這個表中的例子,我們就可以編一個字數統計的程序了。我們的第一個任務就是說明如何聲明標記。
?
標記聲明舉例
| 標記 | 相關表達式 | 含義 |
| 數字(number) | ([0-9])+ | 1個或多個數字 |
| 字符(chars) | [A-Za-z] | 任意字符 |
| 空格(blank) | " " | 一個空格 |
| 字(word) | (chars)+ | 1個或多個?chars |
| 變量(variable) | (字符)+(數字)*(字符)*(數字)* | ? |
?
?
Lex?編程
Lex?編程可以分為三步:
注意:?如果掃描器是用?Yacc?開發的解析器的一部分,只需要進行第一步和第二步。關于這一特殊問題的幫助請閱讀Yacc和?將 Lex?和 Yacc?結合起來部分。
現在讓我們來看一看?Lex?可以理解的程序格式。一個?Lex?程序分為三個段:第一段是?C?和?Lex?的全局聲明,第二段包括模式(C?代碼),第三段是補充的?C?函數。 例如,?第三段中一般都有?main()?函數。這些段以%%來分界。 那么,回到字數統計的?Lex?程序,讓我們看一下程序不同段的構成。
?
C?和?Lex?的全局聲明
這一段中我們可以增加?C?變量聲明。這里我們將為字數統計程序聲明一個整型變量,來保存程序統計出來的字數。我們還將進行?Lex?的標記聲明。
字數統計程序的聲明
| ?????? %{ ??????? int wordCount = 0; ??????? %} ??????? chars [A-za-z\_\'\.\"] ??????? numbers ([0-9])+ ??????? delim [" "\n\t] ??????? whitespace {delim}+ ??????? words {chars}+ ??????? %% |
?
兩個百分號標記指出了?Lex?程序中這一段的結束和三段中第二段的開始。
?
Lex?的模式匹配規則
讓我們看一下?Lex?描述我們所要匹配的標記的規則。(我們將使用?C?來定義標記匹配后的動作。)繼續看我們的字數統計程序,下面是標記匹配的規則。
字數統計程序中的?Lex?規則
| ?????? {words} { wordCount++; /* ??????? increase the word count by one*/ } ??????? {whitespace} { /* do ??????? nothing*/ } ??????? {numbers} { /* one may ??????? want to add some processing here*/ } ??????? %% |
?
C?代碼
Lex?編程的第三段,也就是最后一段覆蓋了?C?的函數聲明(有時是主函數)。注意這一段必須包括?yywrap()?函數。?Lex有一套可供使用的函數和變量。 其中之一就是?yywrap。一般來說,yywrap()?的定義如下例。我們將在?高級 Lex?中探討這一問題。
字數統計程序的?C?代碼段
| ??? ?? void main() ??????? { ??????? yylex(); /* start the ??????? analysis*/ ??????? printf(" No of words: ??????? %d\n", wordCount); ??????? } ??????? int yywrap() ??????? { ??????? return 1; ??????? } |
?
上一節我們討論了?Lex?編程的基本元素,它將幫助你編寫簡單的詞法分析程序。在?高級 Lex?這一節中我們將討論?Lex?提供的函數,這樣你就能編寫更加復雜的程序了。
?
將它們全部結合起來
.lex文件是?Lex?的掃描器。它在?Lex?程序中如下表示:
| ?? $ lex <file name.lex> |
?
這生成了?lex.yy.c?文件,它可以用?C?編譯器來進行編譯。它還可以用解析器來生成可執行程序,或者在鏈接步驟中通過選項�ll?包含?Lex?庫。
這里是一些?Lex?的標志:
- -c表示?C?動作,它是缺省的。
- -t寫入?lex.yy.c?程序來代替標準輸出。
- -v提供一個兩行的統計匯總。
- -n不打印?-v?的匯總。
- ?
高級?Lex
?
Lex?有幾個函數和變量提供了不同的信息,可以用來編譯實現復雜函數的程序。下表中列出了一些變量和函數,以及它們的使用。詳盡的列表請參考?Lex?或?Flex?手冊(見后文的?資源)。
?
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?的討論就到這里。下面我們來討論?Yacc...
?
Yacc
Yacc?代表?Yet Another Compiler Compiler。?Yacc?的?GNU?版叫做?Bison。它是一種工具,將任何一種編程語言的所有語法翻譯成針對此種語言的?Yacc?語 法解析器。它用巴科斯范式(BNF, Backus Naur Form)來書寫。按照慣例,Yacc?文件有?.y?后綴。編譯行如下調用?Yacc?編譯器:
| ?????? $ yacc <options> ??????? <filename ending with .y> |
?
在進一步闡述以前,讓我們復習一下什么是語法。在上一節中,我們看到?Lex?從輸入序列中識別標記。如果你在查看標記序列,你可能想在這一序列出現時執行某一動作。這種情況下有效序列的規范稱為語法。Yacc?語法文件包括這一語法規范。它還包含了序列匹配時你想要做的事。
為了更加說清這一概念,讓我們以英語為例。 這一套標記可能是:名詞,?動詞,?形容詞等等。為了使用這些標記造一個語法正確的句子,你的結構必須符合一定的規則。一個簡單的句子可能是名詞+動詞或者名詞+動詞+名詞。(如?I care. See spot run.)
所以在我們這里,標記本身來自語言(Lex),并且標記序列允許用?Yacc?來指定這些標記(標記序列也叫語法)。
|
|
|
用?Yacc?來創建一個編譯器包括四個步驟:
- 編寫一個?.y?的語法文件(同時說明?C?在這里要進行的動作)。
- 編寫一個詞法分析器來處理輸入并將標記傳遞給解析器。 這可以使用?Lex?來完成。
- 編寫一個函數,通過調用?yyparse()?來開始解析。
- 編寫錯誤處理例程(如?yyerror())。
?
?
?
用?Yacc?編寫語法
如同?Lex?一樣,?一個?Yacc?程序也用雙百分號分為三段。它們是:聲明、語法規則和?C?代碼。我們將解析一個格式為 姓名?=?年齡的文件作為例子,來說明語法規則。我們假設文件有多個姓名和年齡,它們以空格分隔。在看?Yacc?程序的每一段時,我們將為我們的例子編寫一個語法文件。
?
C?與?Yacc?的聲明
C?聲明可能會定義動作中使用的類型和變量,以及宏。還可以包含頭文件。每個?Yacc?聲明段聲明了終端符號和非終端符號(標記)的名稱,還可能描述操作符優先級和針對不同符號的數據類型。?lexer (Lex)?一般返回這些標記。所有這些標記都必須在?Yacc?聲明中進行說明。
在文件解析的例子中我們感興趣的是這些標記:name, equal sign,?和?age。Name?是一個完全由字符組成的值。?Age?是數字。于是聲明段就會像這樣:
文件解析例子的聲明
| ?????? % ??????? #typedef char* string; /* ??????? to specify token types as char* */ ??????? #define YYSTYPE string /* ??????? a Yacc variable which has the value of returned token */ ??????? %} ??????? %token NAME EQ AGE ??????? %% |
?
你可能會覺得?YYSTYPE?有點奇怪。但是類似?Lex, Yacc?也有一套變量和函數可供用戶來進行功能擴展。?YYSTYPE?定義了用來將值從?lexer?拷貝到解析器或者?Yacc?的?yylval?(另一個?Yacc?變量)的類型。默認的類型是?int。 由于字符串可以從?lexer?拷貝,類型被重定義為?char*。 關于?Yacc?變量的詳細討論,請參考?Yacc?手冊(見?資源)。
?
Yacc?語法規則
Yacc?語法規則具有以下一般格式:
| ?????? result: components { /* ??????? action to be taken in C */ } ??????? ; |
?
在這個例子中,result?是規則描述的非終端符號。Components?是根據規則放在一起的不同的終端和非終端符號。 如果匹配特定序列的話?Components?后面可以跟隨要執行的動作。 考慮如下的例子:
| ?????? param : NAME EQ NAME { ??????? printf("\tName:%s\tValue(name):%s\n", $1,$3);} ??????????? | NAME EQ VALUE{ ??????????? printf("\tName:%s\tValue(value):%s\n",$1,$3);} ??????? ; |
?
如果上例中序列?NAME EQ NAME?被匹配,將執行相應的?{ }?括號中的動作。 這里另一個有用的就是?$1?和?$3?的使用,它們引用了標記?NAME?和?NAME(或者第二行的?VALUE)的值。?lexer?通過?Yacc?的變量?yylval?返回這些值。標記?NAME的?Lex?代碼是這樣的:
| ?????? char [A-Za-z] ??????? name {char}+ ??????? %% ??????? {name} { yylval = strdup(yytext); ??????? return NAME; } |
?
文件解析例子的規則段是這樣的:
文件解析的語法
| ?????? file : record file ??????? | record ??????? ; ??????? record: NAME EQ AGE { ??????? printf("%s is now %s years old!!!", $1, $3);} ??????? ; ??????? %% |
?
附加?C?代碼
現在讓我們看一下語法文件的最后一段,附加?C?代碼。(這一段是可選的,如果有人想要略過它的話:)一個函數如main()?調用?yyparse()?函數(Yacc?中?Lex?的?yylex()?等效函數)。 一般來說,Yacc?最好提供?yyerror(char msg)?函數的代碼。 當解析器遇到錯誤時調用?yyerror(char msg)。錯誤消息作為參數來傳遞。一個簡單的?yyerror( char* )?可能是這樣的:
| ?????? int yyerror(char* msg) ??? ??? { ??????? printf("Error: %s ??????? encountered at line number:%d\n", msg, yylineno); ??????? } |
?
yylineno?提供了行數信息。
這一段還包括文件解析例子的主函數:
附加?C?代碼
| ?????? void main() ??????? { ??????????? yyparse(); ??????? } ??????? int yyerror(char* msg) ??????? { ?????? printf("Error: %s ??????? encountered \n", msg); |
?
要生成代碼,可能用到以下命令:
| ?????? $ yacc _d <filename.y> |
?
這生成了輸出文件?y.tab.h?和?y.tab.c,它們可以用?UNIX?上的任何標準?C?編譯器來編譯(如?gcc)。
?
命令行的其他常用選項
- '-d'?,'--defines'?:?編寫額外的輸出文件,它們包含這些宏定義:語法中定義的標記類型名稱,語義的取值類型YYSTYPE,?以及一些外部變量聲明。如果解析器輸出文件名叫?'name.c',?那么?'-d'?文件就叫做?'name.h'。 如果你想將?yylex?定義放到獨立的源文件中,你需要?'name.h',?因為?yylex?必須能夠引用標記類型代碼和?yylval變量。
- '-b file-prefix'?,'--file-prefix=prefix'?:?指定一個所有Yacc輸出文件名都可以使用的前綴。選擇一個名字,就如輸入文件名叫?'prefix.c'.
- '-o outfile'?,'--output-file=outfile'?:?指定解析器文件的輸出文件名。其他輸出文件根據?'-d'?選項描述的輸出文件來命名。
Yacc?庫通常在編譯步驟中自動被包括。但是它也能被顯式的包括,以便在編譯步驟中指定?�ly選項。這種情況下的編譯命令行是:
| ?????? $ cc <source file ??????? names> -ly |
?
將?Lex?與?Yacc?結合起來
到目前為止我們已經分別討論了?Lex?和?Yacc。現在讓我們來看一下他們是怎樣結合使用的。
一個程序通常在每次返回一個標記時都要調用?yylex()?函數。只有在文件結束或者出現錯誤標記時才會終止。
一個由?Yacc?生成的解析器調用?yylex()?函數來獲得標記。?yylex()?可以由?Lex?來生成或完全由自己來編寫。 對于由?Lex?生成的?lexer?來說,要和?Yacc?結合使用,每當?Lex?中匹配一個模式時都必須返回一個標記。 因此?Lex?中匹配模式時的動作一般格式為:
| ?????? {pattern} { /* do smthg*/ ??????? return TOKEN_NAME; } |
?
于是?Yacc?就會獲得返回的標記。當?Yacc?編譯一個帶有?_d?標記的?.y文件時,會生成一個頭文件,它對每個標記都有#define?的定義。 如果?Lex?和?Yacc?一起使用的話,頭文件必須在相應的?Lex?文件?.lex中的?C?聲明段中包括。
讓我們回到名字和年齡的文件解析例子中,看一看?Lex?和?Yacc?文件的代碼。
Name.y -?語法文件
| ?????? % ??????? typedef char* string; ??????? #define YYSTYPE string ??????? %} ??????? %token NAME EQ AGE ??????? %% ??????? file : record file ??????? | record ??????? ; ??????? record : NAME EQ AGE { ??????? printf("%s is %s years old!!!\n", $1, $3); } ??????? ; ??????? %% ??????? int main() ????? ? { ??????? yyparse(); ??????? return 0; ??????? } ??????? int yyerror(char *msg) ??????? { ??????? printf("Error ??????? encountered: %s \n", msg); ??????? } |
?
Name.lex - Lex?的解析器文件
| ?????? %{ ??????? #include "y.tab.h" ??????? ??????? #include <stdio.h> ??????? #include <string.h> ??????? extern char* yylval; ??????? %} ??????? char [A-Za-z] ??????? num [0-9] ??????? eq [=] ??????? name {char}+ ??????? age {num}+ ??????? %% ??????? {name} { yylval = strdup(yytext); ??????? return NAME; } ??????? {eq} { return EQ; } ??????? {age} { yylval = strdup(yytext); ??????? return AGE; } ??????? %% ??????? int yywrap() ??????? { ??????? return 1; ??????? } |
?
作為一個參考,我們列出了?y.tab.h, Yacc?生成的頭文件。
y.tab.h - Yacc?生成的頭文件
| ?????? # define NAME 257 ??????? # define EQ 258 ??????? # define AGE 259 |
?
我們對于?Lex?和?Yacc的討論到此為止。今天你想要編譯什么語言呢?
參考資料
- 您可以參閱本文在?developerWorks?全球站點上的?英文原文.
- Lex and Yacc, Levine, Mason?和?Branson, O�Reilly?及其合作公司,2nd Ed。
- Program Development in UNIX, J. T. Shen, Prentice-Hall India。
- Compilers: Principles, Techniques and Tools, Ahoo, Sethi?和?Ullman, Addison-Wesley Pub.?Co., 1985,11。
- Lex and Yacc and compiler writing指導。
- Java?版的?Lex?指導,?叫做?Jlex。
- 使用?Lex?和?Yacc?的?formalizing a grammar實例。
?
總結
以上是生活随笔為你收集整理的IBM----Yacc 与 Lex 快速入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实现一个脚本引擎
- 下一篇: 纯c#编写的脚本引擎(非CodeDom)