題目: 編程識別由下列文法所定義的表達式的遞歸下降語法分析器。
EàE+T | E-T | T
TàT*F | T/F |F
Fà(E) | i
輸入:每行含一個表達式的文本文件。
輸出:分析成功或不成功信息。
解答:
(1)分析
a) ∵E=>E+T=>E+T*F=>E+T*(E)即有E=>E+T*(E)存在左遞歸。用直接改寫法消除左遞歸,得到如下:
E à TE’
E’ à +TE’ | −TE’|ε
T à FT’
T’ à *FT’ | /FT’|ε
F à (E) | i
b) 對於以上改進的方法。可得:
對於E’: FIRST( E’ )=FIRST(+TE’)∪FIRST(-TE’)∪{ε}={+,−,ε}
對於T’: FIRST( T’ )=FIRST(*FT’)∪FIRST(/FT’)∪{ε}={*,∕,ε}
而且: FIRST( E ) = FIRST( T ) = FIRST( F )=FIRST((E))∪FIRST(i)={(,i }
由此我們容易得出各非終結符的FOLLOW集合如下:
FOLLOW( E )= { ),#}
FOLLOW(E’)= FOLLOW(E)={ ),#}
FOLLOW( T )= FIRST(E’)\ε∪FOLLOW(E’)={+,−,),#}
FOLLOW( T’ ) = FOLLOW( T ) ={+,−,),#}
FOLLOW( F )=FIRST(T’)\ε∪FOLLOW(T’)={*,∕,+,−,),#}
由以上FOLLOW集可以我們可以得出SELECT集如下:
對E SELECT(EàTE’)=FIRST(TE’)=FIRST(T)={ (,i }
對E’ SELECT(E’ à+TE’)={ + }
SELECT(E’ à −TE’)={ − }
SELECT(E’ àε)={ε,),#}
對T SELECT(TàFT’)={(,i}
對T’ SELECT(T’ à*FT’)={ * }
SELECT(T’ à ∕FT’)={ ∕ }
SELECT(T’ àε)={ε,+,−,),#}
對F SELECT(Fà(E) )={ ( }
SELECT(Fài)={ i }
∴ SELECT(E’ à+TE’)∩SELECT(E’ à −TE’)∩SELECT(E’ àε)=F
SELECT(T’ à*FT’)∩SELECT(T’ à ∕FT’)∩SELECT(T’ àε)=F
SELECT(Fà(E) )∩SELECT(Fài)= F
由上可知,有相同左部產生式的SELECT集合的交集為空,所以文法是LL(1)文法。因此,轉化后的文法可以用遞歸下降分析法作語法分析。
(2)設計
這里采用遞歸下降分析法形象描述遞歸子程序。程序中將要用到的幾個重要數據如下:
一個全局變量ch,存放由文件輸入得到的字符。
一個函數宏READ(ch),實現讀取文件中的字符。
五個子函數:P(E)、P(E’)、P(T)、P(T’)、P(F)。
程序主要的子函數模塊流程圖如下:
(3)程序代碼如下
/************************************************************************
* 文件名:ana.c
* 文件描述:遞歸下降語法分析器。分析如下方法:
* E->E+T | E-T | T
* T->T*F | T/F |F
* F->(E) | i
* 輸入:每行含一個表達式的文本文件。
* 輸出:分析成功或不成功信息。
* 創建人:余洪周 <nickhome@163.com>2006-12-8
* 版本號:1.0
***********************************************************************/
#include
#include
#define READ(ch) ch=getc(fp) /*宏:READ(ch)*/
char ch; /*聲明為全局變量*/
int right=0;
FILE *fp;
struct struCH{
char ch;
struct struCH *next;
}struCH,*temp,*head,*shift;
/*head指向字符線性鏈表的頭結點*/
/*shift指向動態建成的結點(游標)*/
void main(int argc,char *argv[]){
void E (); /* P(E) */
void E1(); /* P(E')*/
void T (); /* P(T) */
void T1(); /* P(T')*/
void F (); /* P(F) */
int errnum=0,k=0,m=0,countchar=0,rownum;
int charerr=0; /*開關控制量*/
/************************以只讀方式打開文件*********************/
if((fp=fopen(argv[1],"r"))==NULL)
{
printf("\n\tCan not open file %s,or not exist it!\n",argv[1]);
exit(0); /*文件不存在or打不開時,正常退出程序*/
}
else printf("\n\tSuccess open file: %s\n",argv[1]); /*成功打開文件*/
/******************遍歷整個文件檢測是否有非法字符********************/
/*如果用while(!feof(fp))語言,將會多出一個字符
*所以這里采用以下方法遍歷整個文件檢測其否有非法字符
*/
/*[1]計算文件中字符數量*/
while(!feof(fp)){
READ(ch); /*這里讀取字符只是讓文件指針往前移*/
countchar++; /*統計文件中的字符數(包括換行符及文件結束符)*/
}
rewind(fp); /*將fp文件指針重新指向文件頭處,以備后面對文件的操作*/
if(countchar==0){ /*空文件*/
printf("\t%s is a blank file!\n",argv[1]);
exit(0); /*正常退出本程序*/
}
/*[2]開始遍歷文件*/
while(k<(countchar-1)){ /*加換行符后countchar仍多了一個,不知為何*/
ch=getc(fp);
if(!(ch=='('||ch==')'||ch=='i'||ch=='+'||ch=='-'||ch=='*'||ch=='/'||ch=='#'||ch=='\n')){
charerr=1;errnum++; /*charerror出錯標記,errnum統計出錯個數*/
}
k++;
}
rewind(fp); /*將fp文件指針重新指向文件頭處,以備后面的建鏈表操作*/
if(charerr==1){ /*文件中有非法字符*/
printf("\n\t%d Unindentify characters in file %s \n",errnum,argv[1]);
exit(0); /*正常退出本程序*/
}
/*******************非空且無非法字符,則進行識別操作*****************/
for(rownum=1;m<(countchar-1);rownum++){ /*識別所有行,rownum記錄行號*/
/*初始變量及堆棧和*/
right=1;
/*初始存放待識別的表達式的線性鏈表頭*/
shift=malloc(sizeof(struCH));/**/
shift->next=NULL;
head=shift;
/*讀取一行形成線性鏈表*/
READ(ch);putchar(ch);m++;
while(ch!='\n'&&m<(countchar)){ /*行末or到文件尾。最后會讀取文件結束符*/
/*讀取ch,讀取存入結點,這樣每行便形成一個線性鏈表*/
temp=malloc(sizeof(struCH));
temp->ch=ch;
temp->next=NULL;
shift->next=temp;
shift=shift->next;
READ(ch);
if(m!=(countchar-1)) putchar(ch); /*不輸出最后一次讀取的文件結束符*/
m++;
}
head=head->next; /*消去第一個空頭結點,並使head指向非空線性鏈表頭*/
shift=head; /*shift指向頭結點,以便后面識別操作*/
putchar('\n');
E(); /*開始識別一行*/
if(shift->ch=='#'&&right) /*正確提示:[文件名] Line [行號]:right expression!*/
printf("%s Line %d:\t right expression!\n",argv[1],rownum);
else /*錯誤提示:[文件名] Line [行號]:error expression!*/
printf("%s Line %d:\t error expression!\n",argv[1],rownum);
putchar('\n');
}/*end for*/
printf("Completed!\n");
fclose(fp); /*關閉文件*/
exit(0); /*正常結束程序*/
}
/*以下函數分別對應於子模塊程序*/
void E(){
T();
E1();
}
void E1(){
if(shift->ch=='+'||shift->ch=='-'){
shift=shift->next;
T();
E1();
}
else{
if(shift->ch=='#'||shift->ch==')')
return;
else
right=0;
}
}
void T(void){
F();
T1();
}
void T1(void){
if(shift->ch=='*'||shift->ch=='/'){
shift=shift->next;
F();
T1();
}
else{
if(shift->ch!='#'&&shift->ch!=')'&&shift->ch!='+'&&shift->ch!='-')
right=0; /*如果不是'#'or')'or'+'or'+'or'-'則出錯*/
}
}
void F(void){
if(shift->ch=='i')
shift=shift->next;
else{
if(shift->ch=='('){
shift=shift->next;
E();
if(shift->ch==')')
shift=shift->next;
else
right=0;
}
else
right=0;
}
}
(4)調試
1.編譯:在Windows平台下,用Turbo C 2編譯連接生成后ana.exe;
2.輸入表達式:在ana.exe程序同一目錄下新建一文本文件(如:test.txt)。往文本文件中輸入要識別的表達式,表達式以“#”結束,可輸入多行。同一行“#”以后的內容在識別過種中將自動丟棄。如將以下內容存入test.txt文件中:
i-i#
#
i#
++i#
(i+i)*i#++
3.運行:打開Dos控制台,進入程序所在的目錄(如C:\>)。
輸入:[程序名] [存放表達式的txt文件名],如:ana test.txt
以上test.txt文本實例運行的結果如圖:
test.txt實例運行結果圖
異常結果注釋:
a) 文件不存在or打不開時,提示“Can not open file [filename],or not exist it!”正常退出。
b) 打開文件成功,提示:Success open file:[filename]
c) 若為空文件,提示:[filename] is a blank file! 並正常退出程序。
d) 先檢測文件中是否含有非法字符,若有,則統計並輸出其個數:[errnum] Unindentify characters in file [filename]
e) 程序將分析每行的表達式,輸出每行執行的結果,如上圖可知。
結束語
文中代碼部分也可以用其他語言實現,而思想基本上都是一樣的,請讀者自行實現。水平有限,時間倉促,有什么不足之處,敬請讀者批評指正。
參考資料
關於題目的來源
* 編譯原理實驗二 遞歸下降語法分析 華南熱帶農業大學 計算機科學與技術系
關於LL(1)分析方法
* 何炎祥,編譯原理,高等教育出版社,2004-8
關於指針線性鏈表代碼實現思想
* 黃記瑤,編譯實驗(二)遞歸下降語法分析,2005-11-9