Lex和Yacc應用方法(八).使用堆棧編譯文法
草木瓜 20070604
一、序
前面一些系列文章著重介紹了遞迴文法樹在編譯理論方面的應用。本文則會介紹另一種
實現方式----堆棧。
堆棧在底層系統有十分廣泛的應用,同樣也十分擅長處理文法結構,這裡通過實際樣本
探討如何構造堆棧完成文法分析。
重要補充:下面是本系列文章全範例程式碼統一的調試測試環境,另對於lex,yacc檔案需
要儲存為Unix格式,這一點和Linux,Unix下shell很類似,DOS格式的Shell是不能夠被執行
的,同樣bison,lex編譯DOS格式檔案會出錯誤提示:
Red Hat Linux release 9 (Shrike)
Linux 2.4.20-8
gcc version 3.2.2 20030222
bison (GNU Bison) 1.35
lex version 2.5.4
flex version 2.5.4
註:本站文章難免有錯誤疏漏之處。Lex,Yacc系列文章 http://blog.csdn.net/liwei_cmg/category/207528.aspx
二、具體樣本
本樣本主要完成功能:
1 支援整型,浮點型和字串型
2 支援變數儲存,變數名可為多個字元
3 支援整型,浮點型的+-*/()=運演算法則
4 支援字串型賦值
5 支援print列印整型,浮點型和字串型
6 支援列印變數值
7 支援while if else switch四種控制結構,並支援控制結構的嵌套
8 支援> >= < <= != == 六種比較運算,同時也支援字串的比較
9 支援 && || 複合比較運算
10 支援對空格和TAB的忽略處理
11 支援#的單行注釋
12 支援{}多重組合
13 支援編譯錯誤的具體顯示
14 支援外部變數值傳入(整型,浮點型和字元型)
15 支援外部變數擷取(整型,浮點型和字元型)
16 完整的公司專屬應用程式模式
三、樣本全代碼(略)
A.stack.l
----------------------------------------------
B.stack.y
----------------------------------------------
C.stack.h
----------------------------------------------
D.stackparser.c
----------------------------------------------
E.public.h
----------------------------------------------
F.main.c
----------------------------------------------
G.mk 編譯shell檔案
----------------------------------------------
bison -d stack.y
lex stack.l
gcc -g -c lex.yy.c stack.tab.c stackparser.c
ar -rl stack.a *.o
gcc -g -o lw main.c stack.a
H.mkclean shell檔案
----------------------------------------------
rm stack.tab.c
rm stack.tab.h
rm lex.yy.c
rm *.o
rm *.a
rm lw
四、思路說明
上面列出的代碼是目前最長的。
可見寫一個堆棧編譯器並不是一簇而就的事情,即使對於目前的執行個體,也需要有很多完
善的地方。設計一個堆棧編譯器,我們往往需要從最簡單最容易的語句開始。
A.簡單的堆棧分析思想
我們先舉一個簡單的例子,a=1+2。要完成這個公式的計算,我們首先需要將1和2,壓
入堆棧,然後分析到+運算,此時又需要將1和2出棧,執行+法後將3壓入。繼續分析,需要
壓入a,在最後的=運算時,將3和a出棧進行賦值運算後,將a入棧。運作序列如下:
id: 0 act: pushvalue
id: 1 act: pushvalue
id: 2 act: add
id: 3 act: pushvar
id: 4 act: assign
使用堆棧進行編譯的痛點在於,將無比複雜的文法結構抽象到簡單的入棧出棧操作。單
從這個角度講,是很難一步倒位的。一般地,我們需要先將指令字串根據設定的文法歸併
規則編譯成有序的指令序列。然後對指令序列制定堆棧動作執行函數,依次執行指令並調用
相應函數。
值得欣慰的是,早在N年前,外國人就形成了一套十分強大的編譯理論體系(lex,yacc)
去完成歸併文法的工作。我們只需實現外部的規則動作。
B.lex和yacc的歸併文法設計
與前面例子相似的是,使用G_Var儲存編譯時間的變數資訊,G_sBuff儲存編譯語句,這裡
又增加了G_String統一儲存編譯語句中的所有字串。至於lex和yacc設計方法也是相近的,
只是冗餘了一些語句標誌,如ifx,elsex,switchx等。這些標誌是為了產生順序正確的指令
序列。
lex和yacc會把編譯後的所有結果指令存於G_Command中。見AddCommand。
/* 記憶體指令集結構 */
typedef struct {
int iTypeAction;
int iTypeVal;
float fVal;
int iVar;
int iString;
int iControl;
} TCommand;
這個指令集的設計是關鍵所在,iTypeAction說明這個指令的類型,iTypeVal表示指令
的實值型別。fVal儲存整型浮點型數值,iVar儲存變數索引,iString如果不是-1,則表示字
符串的索引,iControl表示指令返回的控制資訊。
C.堆棧編譯
lex和yacc編譯後會把指令產生到G_Command中,隨後對G_Command進行遍曆處理,並調
用相關動作函數進行出入棧操作。(見Act系列函數) 這裡出入棧操作的是G_Command索引,處
理的結果皆存於G_Command中,這是外人比較難以理解的一點。
TCommand結構體元素是相對獨立的,fVal,iString互斥,iVar標誌變數索引,iControl
只用於控制堆棧的值。
在剛開始時需要對各種文法結構做統籌分析。比如拿分支和迴圈這類語句來說,if分支
就需要維護一個控制狀態,由於嵌套語句的存在,這個控制狀態需要具有堆棧的特點。每次壓
入新if語句要進行現有堆棧的判斷,如果前一if為false,這個if即使為true也還是false,另
外else也要做相似處理。之後對於endif做出棧操作,標誌這對if/else已處理。switch比if要
稍微複雜一些,還要記錄原始值,每次case要做比較。while不僅需要條件還要進行跳轉,這是
Act動作函數有傳回值的重要原因。
以上這個分析要進行比較體系化的考慮,這些也便於以後的功能擴充,如goto等。
這裡我引入了StackValue和StackControl兩個堆棧。Value用於普通的順序計算,Control
用於if else switch while等控制結構。關於控制堆棧,可以參見Act_If,Act_Else等控制
動作函數,在編譯指令序列時,會讀取控制資訊來判斷是否執行該指令。值的注意的是,Act
動作函數還返回了下一指令的索引,這主要用於對迴圈,跳轉等方面的處理,預設是順序執行。
總得來說,TCommand的元素含義要獨立,並嚴格保證處理指示時G_Command的指令資料的
合法性。
D.變數傳值和擷取
還是回呼函數的思想,只是由於字串的存在製造了一些小麻煩,所以使用了二級指標,
並通過傳回值類型,來進行外部判斷處理。不過Linux Unix下的gcc不支援引用傳值,這裡使
用的都是指標傳遞。
五、一些注意事項
A.stack.l,stack.y 檔案要求為Unix格式,這一點和Linux,Unix下shell很類似,DOS
格式的Shell是不能夠被執行的,同樣bison,lex編譯DOS格式檔案會出錯誤提示。
B.SegmentFault多半產生於記憶體越界(前面已經著重說明過),除此以為還經常出現這類
情況,產生這類錯誤的位置的代碼並無錯誤,但是記憶體值已出現亂碼,這一般是指標使用不當。
C.避免一切的warning項,比如在stack.y中,將函數的預說明去除,會提示warning,但
是在執行中,函數傳值就會發生根本性的錯誤。
D.還在在編譯C/C++出現的堆疊溢位錯誤,當然可以用ulimit查看參數,但多半也由記憶體
越界有關,遇到莫名其妙的錯誤第一點就要想到記憶體問題。
E.stack.l,stack.y 注意 規則應用順序,shift-reduce的順序
F.耐心才是最重要的,盡量多列印一些調試資訊,對於複雜的文法結構調試起來並不是很
輕鬆的事。
六、總結
lex和yacc應用目前是在Unix/Linux平台下,產生的是C代碼,固然C++使用這些介面沒有
問題,但不能滿足Windows平台的使用需求。從下文起會開始介紹Windows下這類工具的使用,
以及C/C++/Java代碼的產生。