作為語義分析的一部分,解釋器/編譯器的解析器在整個翻譯過程中創建和維護符號表。符號表用來存儲源文件中的token數據信息,基本上跟標識符有關。如你在圖1-3和2-1中所看到的,符號表是橫在前端和后端之間即中間層的一個核心組件。
==>> 本章中文版源代碼下載:svn co http://wci.googlecode.com/svn/branches/ch4/ 源代碼使用了UTF-8編碼,下載到局部請修改!
目標與方法
對於編譯器開發者來說,維護一個組織良好的符號表(Symbol Table)是一個重要技能。當編譯器/解釋器翻譯源程序時,它必須能夠快速有效地建立新數據,訪問和更新現存數據。否則翻譯過程會變慢或變糟,生成不正確的結果。
本章目標是:
- 一個靈活的,語言無關的符號表。
- 一個簡單的實用程序用來解析Pascal 源程序,並生成一個標識符(ID)交叉引用(cross-reference)列表。
方法先建立符號表的概念設計,接着開發表現此設計的Java接口,最后編寫Java類實現接口。交叉引用的實用程序將幫助驗證你代碼的正確性,它將通過建立(entering),查找(finding),和更新(updating)數據等操作來使用符號表。
符號表概念設計
在翻譯過程中,編譯器/解釋器創建和更新符號表的表項(entry),表項用來存儲源程序中某些token的信息。每個表項有個名字即token的文本串。例如,存儲標識符 token的表項用了標識符的名字,它還包括標識符的其它信息。在源程序翻譯過程中,編譯器/解釋器查找和更新這些信息。
符號表要放什么樣的數據? 有用的數據!有關標識符的符號表項一般會包括它的類型,結構,以及是怎么定義的。(符號表概念設計)目標之一保持符號表的靈活性,使其不僅限於Pascal。不管符號表存儲什么樣的信息,它必須要支持的基本操作有:
- 建立新數據(enter new information)
- 查找現存數據(look up existing information)
- 更新現存數據(update existing information)
符號表棧
為解析塊狀結構化的語言比如Pascal,你實際上需要多個符號表(當然類型只有一個,這兒是說多個實例)-- 全局符號表一個,每個主程序一個,每個過程(procedure)和函數(function)一個,每個記錄(Record)類型一個。因為Pascal函數和記錄可以嵌套(routine,procedure和function都是routine),符號表必須在一個棧上維護。棧頂的符號表維護當前解析器正處理的程序、函數、結構、記錄的相關信息。當解析器按照某種方式解析一個Pascal程序時,碰到進入和離開嵌套函數和記錄類型定義的情況,分別壓入符號表(入棧)和彈出符號表(出棧)。棧頂的符號表就是俗稱的局部表(local table)。(如果全局表是棧中唯一元素,那么它也是局部表)
圖4-1 展示了符號表棧、符號表、符號表項的概念設計。在概念設計中,符號表棧包含一個或多個符號表,而每個表中又包含多個表項。每個符號表項包含一個一般為標識符 token的信息,還包含表項名稱和屬性形式的token信息。符號表以表項名為搜索關鍵字搜索相關表項。
此時你不需知道也不需關心該用何種數據結構構建符號表或該如何存儲表項。根據概念設計,你僅需要明白符號表有哪些重要組件,它們各扮演什么角色,以及相互之間的關系。
設計筆記 |
理想情況下,編譯器/解釋器的其它組件不必知道太多符號表概念層面以外的東西(比如實現),這維護了主要組件之間的松耦合關系。 直到第9章,你僅會在棧中看到一個符號表。現在定義符號表將會使得后續涉及到多個符號表的翻譯過程更輕松。 |
(圖中的Symbol table為符號表,Entry為符號表中的一項。符號表項有名稱name和各式其它屬性attributes)。
點擊圖片放大看
符號表接口
根據圖4-1所示的符號表組件,你可得到一個接口的UML設計圖,這些接口放在包intermediate中。
圖4-2:符號表接口
盡管目前棧中只有一個(符號)表,你也能引入符號表的一些關鍵操作:
* 在局部符號表中建立一個新的表項,當前的表在棧頂。
* 通過搜索局部符號表查找某個表項。
* 在棧中的所有表中查找某個表項。
一旦某個符號表項被找到,你就能更新它的內容。注意你只能在局部表中建立新表項。然后你能在局部表或棧中的所有表中查找某表項。
接口SymTabStack支持上面的這些操作。方法enterLocal在局部表中建立一個新項。方法lookupLocal搜索局部表,還有方法lookup搜索棧中所有表。接口SymTabStack定義了另兩個方法:getCurrentNestingLevel()返回當前的嵌套層級及getLocalSymTab()返回棧頂的局部符號表。直到第9章才涉及到嵌套(多個符號表,目前只有一個)。
接口SymTabEntry 表示一個符號表項。方法getName()獲取項的名稱,方法setAttributes和getAttributes分別設置和返回表項的屬性信息。為支持交叉引用,方法appendLineNumber() 在每次表項名稱出現時,存儲對應的源代碼行位置。方法getLineNumbers()返回表項所有的行位置信息。每個表項保留一個指向包含它的符號表的引用,方法getSymTab返回這個引用。
根據圖4-2中的UML類圖很自然的創建這些Java接口(為毛不用代碼生成,還要手寫??)。清單4-1 展示了SymTabStack接口。
點擊圖片放大看
清單4-1:SymTabStack接口 詳細參見本章源代碼,這里不再顯示。
第二章有SymTab接口的早期版本。清單4-2 展示了一個內容更豐富的版本。詳細參見本章源代碼,這里不再顯示。
清單4-3 展示了SymTabEntry接口。詳細參見本章源代碼,這里不再顯示。
最后,清單4-4 展示SymTabKey的占位接口,它用來表示表項的屬性鍵。 詳細參見本章源代碼,這里不再顯示。
符號表工廠
圖4-3 展示了符號表實現類的UML類圖,這些類都在包intermediate.symtabimpl中。
設計筆記 |
擁有權(ownership)箭號(arrow)的箭頭端的星號'*'表示重數(multiplicity,多個的意思)。左邊的圖展示了一個SymtabStackImpl對象可擁有0或多個SymTab對象,一個SymTab對象能擁有0或多個SymTabEntry對象。 |
設計筆記 |
使用接口定義全部符號表組件使得其它組件(語法分析器等)在代碼調用符號表的時候只需要關注接口,而不需要知道任何符號表的具體實現。這樣的松耦合為最大程度上支持了靈活性。你可將符號表實現成你任意喜歡的那種,不管實現怎么改,只要接口不變,其它組件的調用也就不需要改。換句話說,所有調用者僅需要明白概念級別上的符號表即可。(而不需要關心實現上的符號表) |
因為你希望符號表的調用代碼僅關注它的接口,你得搞個符號表工廠將調用者與實現細節隔離開來。清單 4-5 展示了類SymTabFactory。每個方法可構造某個此實現類的實例,返回實現的接口。詳細參見本章源代碼,這里不再顯示。
因為一個編譯器/解釋器僅有單個符號表棧,你能通過一個靜態域指向它。清單展示了框架類Parser中的靜態域symTabStack。
清單4-6 Parser類的靜態域(符號表棧)
protected ICode iCode; // 語法樹根節點。
//符號表棧
protected static SymTabStack symTabStack = SymTabFactory.createSymTabStack();
符號表實現
現在將准備開發符號表接口的實現了。圖4-3 展示了實現接口的類以及對更下級(被包含的元素是更下級元素)接口的擁有關系(ownership relationship)。例如,類SymTabImpl實現了接口SymTab,每個SymTabImpl對象擁有0個或多個SymTabEntry對象。再比如,一個SymTabEntryImpl對象引用包含它的SymTab對象。
設計筆記 |
因為實現類自己只對接口(這里肯定是除實現接口以外的其它接口了)編碼,所以沒有實現類依賴其他實現類(比如SymTabImpl只對SymTabEntry接口編碼,所以它不依賴SymTabEntryImpl)。 |
類SymTabStackImpl實現了SymTabStack接口並擴展了java.util.ArrayList<SymTab>。換句話說,將符號表堆棧用數組表(array list)實現。清單4-7 展現了這個類的關鍵方法。留意在構造函數中創建和添加了一個符號表在堆棧中(也就是默認的全局表)。
public class SymTabStackImpl
extends ArrayList<SymTab>
implements SymTabStack
{
private int currentNestingLevel; // 當前嵌套,默認為全局的0
public SymTabStackImpl()
{
this.currentNestingLevel = 0;
add(SymTabFactory.createSymTab(currentNestingLevel));
}
public SymTabEntry enterLocal(String name)
{
return get(currentNestingLevel).enter(name);
}
public SymTabEntry lookupLocal(String name)
{
return get(currentNestingLevel).lookup(name);
}
public SymTabEntry lookup(String name)
{
//目前只有一個符號表在棧中,所以全局即局部
return lookupLocal(name);
}
}
方法enterLocal和lookupLocal僅牽涉到棧頂的局部符號表。目前域currentNestingLevel總是返回0(因為只有一個全局表,沒有實際嵌套發生)。還有,因為只有棧中只有一個符號表,方法lookup()和lookupLocal()功能上是一樣的。方法get()被基類的ArrayList定義。
清單4.8 展示了類SymTabImpl的關鍵方法,它實現SymTab接口並擴展了java.util.TreeMap即以鍵的升序來存儲每項的哈希表。
public class SymTabImpl
extends TreeMap<String, SymTabEntry>
implements SymTab
{
private final int nestingLevel; //所在嵌套層次
public SymTabImpl(int nestingLevel)
{
this.nestingLevel = nestingLevel;
}
public SymTabEntry enter(String name)
{
SymTabEntry entry = SymTabFactory.createSymTabEntry(name, this);
put(name, entry);
return entry;
}
public SymTabEntry lookup(String name)
{
return get(name);
}
@Override
public Collection<SymTabEntry> sortedEntries() {
return Collections.unmodifiableCollection(values());
}
}
1: public class SymTabEntryImpl
2: extends HashMap<SymTabKey, Object>
3: implements SymTabEntry
4: {
5: private String name; // 名稱
6: private SymTab symTab; // 所在表
7: private ArrayList<Integer> lineNumbers; // 所有出現的行位置
8:
9: public SymTabEntryImpl(String name, SymTab symTab)
10: {
11: this.name = name;
12: this.symTab = symTab;
13: this.lineNumbers = new ArrayList<Integer>();
14: }
15: public void appendLineNumber(int lineNumber)
16: {
17: lineNumbers.add(lineNumber);
18: }
19: public void setAttribute(SymTabKey key, Object value)
20: {
21: put(key, value);
22: }
23: public Object getAttribute(SymTabKey key)
24: {
25: return get(key);
26: }
27: }
方法setAttribute()和getAttribute分別調用了父哈希表的put()和get()方法。
設計筆記 |
就每項你能存儲什么這點來說,使用hashmap實現符號表項的設計提供了最大的靈活性。 |
清單4-10 展示了枚舉類型SymTabKeyImpl,它實現了接口SymTabKey。后續章節將會用到這些鍵。詳細參見本章源代碼,這里不再顯示。
到此完成了本章的第一個目標,即建立一個靈活的,語言無關的符號表。符號表的靈活性體現在調用者只需關注它的接口,因而容許后續實現的改進。符號表項能存儲任意信息,並沒有Pascal特定限制。
程序4:Pascal交叉引用 I
你即將驗證新創建的符號表。你可通過生成Pascal源程序中的標識符(ID)交叉引用列表來完成這個目標(即本章的第二個目標)。清單4-11 展示了用類似下面的命令行產生的輸出樣例。
java -classpath classes Pascal compile -x newton.pas
-x選項用來生成交叉引用列表。
清單4-11:一個生成的交叉引用清單。
----------代碼解析統計信--------------
源文件共有 36行。
有 0個語法錯誤.
解析共耗費 0.01秒.
============ 交叉引用列表 ========
Identifier 所在行位置
---------- ------------
abs 031 033
epsilon 004 033
input 001
integer 007
newton 001
number 007 014 016 017 019 023 024 029 033 035
output 001
read 014
real 008
root 008 027 029 029 029 030 031 033
sqr 033
sqroot 008 023 024 031 031
sqrt 023
write 013
writeln 012 017 020 024 025 030
----------編譯統計信--------------
共生成 0 條指令
代碼生成共耗費 0.00秒
1: public void parse() throws Exception {
2: Token token;
3: long startTime = System.currentTimeMillis();
4:
5: try {
6: while (!((token = nextToken()) instanceof EofToken)) {
7: TokenType tokenType = token.getType();
8: if (tokenType == PascalTokenType.IDENTIFIER){
9: String entry_name = token.getText().toLowerCase();
10: //是否出現過
11: SymTabEntry entry_existed = symTabStack.lookup(entry_name);
12: if (null == entry_existed){
13: //第一次出現?
14: entry_existed = symTabStack.enterLocal(entry_name);
15: }
16: entry_existed.appendLineNumber(token.getLineNumber());
17: }else if (tokenType==PascalTokenType.ERROR){
18: // 留意當token有問題是,它的值表示其錯誤編碼
19: errorHandler.flag(token,
20: (PascalErrorCode) token.getValue(), this);
21: }else{
22: //其它暫且忽略
23: }
24: }
25: // 發送編譯摘要信息
26: float elapsedTime = (System.currentTimeMillis() - startTime) / 1000f;
27: sendMessage(new Message(PARSER_SUMMARY, new Number[] {
28: token.getLineNumber(), getErrorCount(), elapsedTime }));
29: } catch (IOException e) {
30: errorHandler.abortTranslation(PascalErrorCode.IO_ERROR, this);
31: }
32: }
清單4-13 展示了util包中的新輔助類CrossReferencer。詳細參見本章源代碼,這里不再顯示。
方法printSymTab()遍歷符號表的有序表項。對每個項,它先打印出標識符的名字,接着是所有行位置的明細。
在第9章,在學過怎么解析Pascal聲明(declarations)后,你將會寫一個更NB的CrosssReferencer版本。
為支持打印交叉引用表,須在Pascal主程序的構造函數中做些小改動。
首先將源程序的行輸出注釋掉,因為沒啥意義。詳見源程序中的第49行。
//source.addMessageListener(new SourceMessageListener());
private SymTabStack stack;
// 生成中間碼和符號表
iCode = parser.getICode();
//symTab = parser.getSymTab();
stack = parser.getSymTabStack();
if (xref){
CrossReferencer cr = new CrossReferencer();
cr.print(stack);
}
// 交由后端處理
backend.process(iCode, stack);