編譯原理-如何使用flex和yacc工具構造一個高級計算器
Flex工具的使用方法
Lex 是一種生成掃描器的工具。 Lex是Unix環境下非常著名的工具,主要功能是生成一個掃描器(Scanner)的C源碼。
掃描器是一種識別文本中的詞匯模式的程序。 這些詞匯模式(或者常規表達式)在一種特殊的句子結構中定義。一種匹配的常規表達式可能會包含相關的動作。這一動作可能還包括返回一個標記。 當 Lex 接收到文件或文本形式的輸入時,它試圖將文本與常規表達式進行匹配。 它一次讀入一個輸入字符,直到找到一個匹配的模式。 如果能夠找到一個匹配的模式,Lex 就執行相關的動作(可能包括返回一個標記)。 另一方面,如果沒有可以匹配的常規表達式,將會停止進一步的處理,Lex 將顯示一個錯誤消息。
Lex 和 C 是強耦合的。一個 .lex 文件(Lex 文件具有 .lex 的擴展名)通過 lex 公用程序來傳遞,並生成 C 的輸出文件。這些文件被編譯為詞法分析器的可執行版本。
Lex程序
一個典型的Lex程序的大致結構:
declarations
%%
translation rules
%%
auxiliary procedures
分別是聲明,轉換規則和其它函數。%
用作在單個部分之間做分隔。
字符及其含義列表:
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 是匹配的。 ( ) 將一系列常規表達式分組。
標記聲明:
數字(number) ([0-9])+ 1個或多個數字
字符(chars) [A-Za-z] 任意字符
空格(blank) " " 一個空格
字(word) (chars)+ 1個或多個 chars
變量(variable) (字符)+(數字)*(字符)*(數字)*
值得註意的是,lex 依次嘗試每一個規則,盡可能地匹配最長的輸入流
Lex 編程可以分為三步:
- 以 Lex 可以理解的格式指定模式相關的動作。
- 在這一文件上運行 Lex,生成掃描器的 C 代碼。
- 編譯和鏈接 C 代碼,生成可執行的掃描器。
例如,對於一下的Lex代碼:
%{
#include <stdio.h>
int k = 0;
%}
%%
[0-9]+ {
k = atoi(yytext);
if(k % 6 == 0 && k % 8 == 0) {
printf("%d\n", k);
}
}
執行:
lex prog.lex
gcc lex.yy.c -o prog -ll
然後將會得到一個可執行文件,這個可執行文件的功能是:如果輸入的字符串不是數字,原樣輸出,如果是數字,判斷是否為6和8的公倍數,若是,則輸出。
其中,-ll
表示鏈接lex的相關庫文件,要想編譯時不帶-ll
選項,就必須實現main
函數和yywrap
函數(return 1即可)。
Lex中,一般聲明為如下形式:
%{
int wordCount = 0;
%}
chars [A-Za-z\_\‘\.\"]
numbers ([0-9])+
delim [" "\n\t]
whitespace {delim}+
words {chars}+
模式匹配規則如下例:
{words} { wordCount++; /* increase the word count by one*/ }
{whitespace} { /* do nothing*/ }
{numbers} { /* one may want to add some processing here*/ }
含義為針對不同的模式采取不同的策略(狀態機)。
Lex程序的最後一段一般為C代碼,為如下形式:
void main()
{
yylex(); /* start the analysis*/
// ... do some work.
}
int yywrap()
{
return 1;
}
最後一段覆蓋了 C 的函數聲明(有時是主函數)。註意這一段必須包括 yywrap()
函數。
在上文中的判斷公倍數的例子中,省略了程序的第三段,Lex生成了默認的C風格的main()
函數。
在使用Lex做文法解析時,某些特殊結構的表達式會使由表格轉化的確定的自動機成指數增長,並因此造成指數級的空間和時間復雜度消耗。
Lex變量和函數
一些常用的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內部預定義宏:
ECHO #define ECHO fwrite(yytext, yyleng, 1, yyout) 也是未匹配字符的默認動作。
一個簡單的Lex的例子:
%{
#include <stdio.h>
%}
%%
[\n] { printf("new line\n"); }
[0-9]+ { printf("int: %d\n", atoi(yytext)); }
[0-9]*\.[0-9]+ { printf("float: %f\n", atof(yytext)); }
[a-zA-Z][a-zA-Z0-9]* { printf("var: %s\n", yytext); }
[\+\-\*\/\%] { printf("op: %s\n", yytext); }
. { printf("unknown: %c\n", yytext[0]); }
%%
Yacc
Yacc 代表 Yet Another Compiler Compiler。 Yacc 的 GNU 版叫做 Bison。它是一種工具,將任何一種編程語言的所有語法翻譯成針對此種語言的 Yacc 語 法解析器。它用巴科斯範式(BNF, Backus Naur Form)來書寫。按照慣例,Yacc 文件有 .y 後綴。
用 Yacc 來創建一個編譯器包括四個步驟:
- 通過在語法文件上運行 Yacc 生成一個解析器。
-
說明語法:
- 編寫一個 .y 的語法文件(同時說明 C 在這裏要進行的動作)。
- 編寫一個詞法分析器來處理輸入並將標記傳遞給解析器。 這可以使用 Lex 來完成。
- 編寫一個函數,通過調用 yyparse() 來開始解析。
- 編寫錯誤處理例程(如 yyerror())。
- 編譯 Yacc 生成的代碼以及其他相關的源文件。
- 將目標文件鏈接到適當的可執行解析器庫。
Yacc程序
如同 Lex 一樣, 一個 Yacc 程序也用雙百分號分為三段。 它們是:聲明、語法規則和 C 代碼。 每兩段內容之間用%%
。
一個Yacc程序示例:
%{
typedef char* string;
#define YYSTYPE string
%}
%token NAME EQ AGE
%%
file: record file
| record
;
record: NAME EQ AGE {
printf("name: %s, eq: %d, age: %d\n, $1, $2, $3);
}
;
%%
int main()
{
yyparse();
return 0;
}
int yyerror(char *msg)
{
printf("ERORR MESSAGE: %s\n", msg);
}
Lex和YACC內部工作原理
在YACC文件中,main
函數調用了yyparse()
,此函數由YACC替你生成的,在y.tab.c文件中。函數yyparse
從yylex
中讀取符號/值組成的流。你可以自己編碼實現這點,或者讓Lex幫你完成。在我們的示例中,我們選擇將此任務交給Lex。
Lex中的yylex
函數從一個稱作yyin
的文件指針所指的文件中讀取字符。如果你沒有設置yyin
,默認是標準輸入(stdin
)。輸出為yyout
,默認為標準輸出(stdout
)。
你可以在yywrap
函數中修改yyin
,此函數在每一個輸入文件被解析完畢時被調用,它允許你打開其它的文件繼續解析,如果是這樣,yywarp
的返回值為0
。如果想結束解析文件,返回1
。
每次調用yylex
函數用一個整數作為返回值,表示一種符號類型,告訴YACC當前讀取到的符號類型,此符號是否有值是可選的,yylval
即存放了其值。
默認yylval
的類型是整型(int
),但是可以通過重定義YYSTYPE
以對其進行重寫。分詞器需要取得yylval
,為此必須將其定義為一個外部變量。原始YACC不會幫你做這些,因此你得將下面的內容添加到你的分詞器中,就在#include<y.tab.h>
下即可:
extern YYSTYPE yylval;
Bison會自動做這些工作(使用-d
選項生成y.tab.h文件)。
Lex與Yacc配合
使用Lex和Yacc實現一個高級計算器
Lex代碼的內容:
%{ #include <stdlib.h> #include "test.tab.h" extern int yyerror(const char *); %} %% [" "; \t] { } (0(\.[0-9]+)?)|([1-9][0-9]*(\.[0-9]+)?) { yylval.dv = strtod(yytext,0);return NUMBER;} [a-zA-Z] { yylval.cv = *yytext; return CHARA;} [-+*/()^%~!=\n] {return *yytext;} "&" {return AND;} "|" {return OR;} "||" {return or;} "&&" {return and;} "log" {return LOG;} "cos" {return COS;} "sin" {return SIN;} "tan" {return TAN;} "++" {return PP;} "--" {return SS;} "<<" {return LOL;} ">>" {return LOR;} "cot" {return COT;} "ans" {return ANS;} "drop" {return DROP;} "list" {return LIST;} "erase" {return ERASE;} "clear" {return CLEAR;} "help" {return HELP;} %% int yywrap() { return 1; }
Yacc代碼的內容:
%{ #define Pi 3.14159265358979 #include <stdlib.h> #include <stdio.h> #include <math.h> int yylex(); int yyerror(char *); void convert(int num ,int mode); double vars[26]={0}; double last=0; long var; int i; int flag=1; %} %token ANS %token <dv> NUMBER %token <cv> CHARA %type <dv> expr %type <cv> cmdline %union { double dv; char cv; } %token DROP HELP CLEAR LIST ERASE %token ‘+‘ ‘-‘ ‘*‘ ‘/‘ ‘^‘ ‘%‘ ‘`‘ ‘~‘ ‘!‘ ‘=‘ %token COS SIN TAN OR AND PP SS LOR LOL COT or and %token LOG %left ‘=‘ %left ‘+‘ ‘-‘ %left ‘*‘ ‘/‘ ‘%‘ %left AND OR and or %left COS SIN TAN LOG PP SS LOR LOL COT %left ‘^‘ %left ‘~‘ ‘!‘ %right ‘(‘ ‘)‘ %% program: program expr ‘\n‘ { if(flag) { printf( "你的結果是:\t=%g\n" , $2 ); last = $2; } else {printf("");} flag=1; } | program cmdline ‘\n‘ | program stat ‘\n‘ | ; stat : CHARA ‘=‘ expr { if(islower($1)) i = $1 - ‘a‘; else i = $1 - ‘A‘; vars[i] = $3; flag =1; } expr : NUMBER { $$ = $1; } | ANS { $$ = last; } | CHARA { if(islower($1)) i = $1 - ‘a‘; else i = $1 - ‘A‘; $$ = vars[i]; } | expr ‘+‘ expr { $$ = $1 + $3; } | expr ‘-‘ expr { $$ = $1 - $3; } | expr ‘*‘ expr { $$ = $1 * $3; } | expr ‘/‘ expr { $$ = $1 / $3; } | expr ‘^‘ expr { $$ = pow($1, $3);} | ‘~‘ expr { $$=~(int)$2; } | ‘!‘ expr { if(!(int)$2) printf("true\n"); else printf("false\n"); flag=0; } | expr ‘%‘ expr { $$ = (int)$1 % (int)$3; } | ‘-‘ expr { $$ = -$2; } | ‘(‘ expr ‘)‘ { $$ = $2; } | COS expr { $$ = cos($2 * Pi /180); } | SIN expr { $$ = sin($2 * Pi /180); } | TAN expr { $$ = tan($2 * Pi /180); } | COT expr { $$ =1/sin($2 * Pi /180);} | expr LOG expr { $$ = log($1)/log($3); } | expr AND expr { printf("與前的二進制($1):\n"); convert($1,2); printf("\n"); printf("與前的二進制($3):\n"); convert($3,2); printf("\n"); $$=(int)$1&(int)$3; printf("結果的二進制($$):\n"); convert($$,2); printf("\n"); } | expr OR expr { printf("或前的二進制($1):\n"); convert($1,2); printf("\n"); printf("或前的二進制($3):\n"); convert($3,2); printf("\n"); $$ =(int)$1|(int)$3; printf("結果的二進制($$):\n"); convert($$,2); printf("\n"); } | expr and expr { if( (int)$1 && (int)$3) printf("true\n"); else printf("false\n"); flag=0; } | expr or expr { if( (int)$1 || (int)$3) printf("true\n"); else printf("false\n"); flag=0; } | expr PP { $$ =$1+1;} | expr SS { $$ =$1-1;} | expr LOL expr { printf("移位前的二進制:"); convert($1,2); printf("\n"); $$ =(int)$1<<(int)$3; printf("移位後的二進制:"); convert($$,2); printf("\n"); } | expr LOR expr { printf("移位前的二進制:"); convert($1,2); printf("\n"); $$ =(int)$1>>(int)$3; printf("移位後的二進制:"); convert($$,2); printf("\n"); } ; cmdline : DROP { exit(0);} | CLEAR { system("clear"); } | LIST { for(i=0;i<26;i++) printf("\t%c=%g\n",‘a‘+i,vars[i]); } | ERASE { for(i=0;i<26;i++) vars[i]=0; printf("已經清空所有的寄存器的值!\n");} | HELP { printf("命令:\n"); printf(">>help :獲取幫助.\n"); printf(">>ans :列出上次計算的結果.\n"); printf(">>list :列出寄存器中所有的值 ‘a‘/‘z‘.\n"); printf(">>erase:重置寄存器.\n"); printf(">>clear:清屏.\n"); printf(">>drop :退出程序.\n"); } ; %% int yyerror(char *s) { printf("%s\n", s); return 1; } void convert(int num ,int mode) { if(num/mode==0) { printf("\t%d",num);return;} else { convert(num/mode,mode); printf("%d",num%mode); } } int main(int argc,char **argv) { printf("\t _______________________________________________________________________ \n"); printf("\t | HeFei Noraml University |\n"); printf("\t | 1410441036 計算科學與技術(嵌入式) 編譯原理課程設計 童慧林 |\n"); printf("\t | _______________ |\n"); printf("\t | |_______ ______| + - * / ^ || && 操作數 操作符 操作數 |\n"); printf("\t | | | ++ -- |\n"); printf("\t | | | _____ ______ ______ |\n"); printf("\t | | | | | | | | | a=1 |\n"); printf("\t | | | | | | | | | b=2 |\n"); printf("\t | | | |_____| | |_ |______| a+b |\n"); printf("\t | | | | sin 30 |\n"); printf("\t | | | 1 + 1 | =0.5 |\n"); printf("\t | | | sin cos tan cot log |______| 5 log 5 |\n"); printf("\t | |__| << >> 1<<2 3>>1 =1 |\n"); printf("\t |_______________________________________________________________________|\n"); yyparse(); }
執行腳本1.sh
#!/bin/bash # bison -d test.y flex test.l gcc lex.yy.c test.tab.c -lm -o test ./test
運行結果:
編譯原理-如何使用flex和yacc工具構造一個高級計算器