1. 程式人生 > >編譯原理-如何使用flex和yacc工具構造一個高階計算器

編譯原理-如何使用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 可以理解的格式指定模式相關的動作。
在這一檔案上執行 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();
}
#!/bin/bash
#
bison -d test.y
flex test.l
gcc lex.yy.c test.tab.c -lm -o test
./test