本章將會從前一章的概念設計帶你到初級的實現過程。你將先為編譯器和解釋器構造一個靈活的框架,接着將初級版的編譯器解釋器組件集成到框架中。最后編寫端對端的測試用例檢驗這些框架和組件。
==>> 本章中文版源代碼下載:svn co http://wci.googlecode.com/svn/branches/ch2/ 源代碼使用了UTF-8編碼,下載到本地請修改!
目標和方法
此章的設計方法首先會讓你覺得過於繁瑣啰嗦,的確,本章結束后將會有一大堆超過你預期數量的代碼。但請記你在用早被證明的軟件工程法則和優秀面向對象設計構建編譯器和解釋器。
如在概念設計中描述的那樣,編譯器和解釋器將盡可能復用組件,因只有后端有所不同。在這章中,你將構建一個靈活的框架並首先放置那些已被深度簡化的編譯器和解釋器組件。不過它們足夠驗證你設計的框架是否恰當即組件能很好的耦合並能協同工作。這個成功前提將會使得從公用前端到編譯器解釋器后端的端對端執行代碼編寫,還有后續的增量式組件開發變得簡單。
本章的目標是:
- 一個語言無關的框架,可支持編譯器和解釋器。
- 集成進框架前端(front end)的初級版Pascal語言相關組件。
- 集成進框架后端(back end)的初級版編譯器和解釋器組件。
- 通過從公共前端生成源程序清單以及從編譯器或解釋器后端生成消息,簡單的運行端對端測試,測試相關組件。
設計筆記 |
不管任何時候開發負責程序如編譯器或解釋器,成功的首要步驟是:
早期的組件集成是關鍵,甚至你已經簡化了初級組件(沒有完善的組件稱之為初級組件)也一樣。盡可能早的測試礦建和組件以讓它們更好的協作。框架和初級組件組成你后續開發的基礎。開發將是增量式的進行,代碼在每次增量后都能繼續工作(附加更多功能)。你該永遠基於可運行的代碼去構建。 |
語言無關的框架組件
基於概念設計,框架包含三個包:frontend、 intermediate、 backend。
框架組件是用來定義框架且語言無關的接口和類。有些是抽象類。一旦框架組件就緒,你能開發抽象類的Pascal實現(組件語言無關,實現語言相關)。圖2-1 展示了使用UML 包和類圖的框架組件。
圖2-1:在frontend,intermediate,backend包中的語言無關組件一起定義了一個能支持編譯器和解釋器后續開發的框架。
設計筆記 |
統一建模語言是一個工業級的展示面向對象軟件架構和過程的圖形化語言。各種圖表(序列圖,類圖等)能表示程序的結構組件之間的靜態關系,也能表示組件運行期的動態行為。 |
前端
在前端包中,語言無關類Paser,Scanner,Token,Source代表框架組件。框架類強制你在忽略具體源語言的情況下,能盡力思考每個前端組件的職責,還有它們之間的交互。圖2-2中的UML 類圖展示了它們的關系。
Parser和Scanner是抽象類;語言相關的子類將實現它們的抽象方法。parser和scanner聯系緊密,Parser有一個受保護域(protected field)scanner指向Scanner。Parser從Scanner請求token,所以它依賴Token。Scanner有一個私有域currentToken,它通過受保護域source引用Source,還將source引用傳給每個自己構造的token。每個Token也能通過受保護域source擁有Source引用,在它的構造過程中,通過source讀取字符。
圖2-3的類圖更進一步展示了四個前端框架類。它展示了域,方法和其他的前端類和接口。例如每個Token有一個用TokenType類表示的token類型,EofToken是Token的子類。
按照概念設計,parser控制翻譯過程,它翻譯源程序,所以Parser類有一個抽象方法parser();語言相關的方法實現將不斷的找scanner索取下一個token。Parser的currentToken()和nextToken()僅僅是scanner的代理方法而已(參考代理模式,不過這兒是為了少寫點代碼)。語言相關的getErrorCount()方法實現返回語法錯誤數量。
設計筆記 |
在UML類圖中,一個未填充箭頭的箭號表示一個類引用或依賴另一個類。虛線箭號(比如從Parser到Token的肩頭)表示一個僅僅在方法調用期間(比如Parser的nextToken()方法返回一個Token對象)存在的引用。實線箭號且在出發端有一個空菱形意味着一個類通過在對象生命周期持續的引用,擁有(owns)或聚合(aggregates)另一個類。(假設類A通過引用域ref聚合類B,那么類A的對象a1聚合類B的對象b1的這種關系在a1的生命周期一直存在,聚合相當於包含,a1負責b1的生命周期)。 域名稱保存標識箭頭的引用(例如,Parser類用它的scanner域維護對Scanner類的引用)。 實心箭號帶空箭頭(如EofToken類到Token類)表示一個子類到它的父類。 類名稱下,一個類圖可選擇的包含域(field)描述區域和方法描述區域。標識箭號名稱的域名不在域描述區域出現(Parser有個域scanner引用類Scanner,它不在Parser的域描述區域出現,在生成代碼后就會有)。在域名或者方法名前面的字符表明訪問控制。
跟在域名或方法名冒號后面的分別是域類型或者返回值類型。為省地方,類圖通常不顯示構造函數和域名的getter和setter方法。 抽象類名以斜體出現。抽象方法名還是斜體。 |
scanner從源程序中抽取token。Scanner類抽象方法extractToken()的語言相關實現將會根據具體語言從Source中讀取字符,以便構造Token。Scanner的快捷方法currentChar()和nextChar()會調用Source類中的對應方法(還是代理模式)
Token的域保存有關Token的有用信息,包括類型,文本串(即字面上的字符串),值和它在源程序中的位置(行號和位置【相對於行】)。Token同樣有Source類的快捷方法currentChar()和nextChar()。Token類型與具體語言有關。當前的Token類型是一個占位符(因為一個具體類型都沒有)。
后面你將會根據具體語言創建語言相關的Token子類。但目前只有語言無關EofToken子類,它表示源文件終止。使用Token子類使得scanner代碼更加模塊化,因為不同類型Token需要不同計算方式。(原文是算法,我認為談不上算法)。
Parser
清單2-1 展示了框架抽象類Parser的關鍵方法。語言相關的Parser子類要實現parse()方法和getErrorCount(),分別用來表示源程序分析過程和返回語法錯誤。如上文提到的,Parser的currentToken()和nextToken()方法是scanner對應方法的代理。
1: /**
2: * <p>語言無關的Parser,有子類完成具體語言解析</p>
3: */
4: public abstract class Parser implements MessageProducer
5: {
6: protected static SymTab symTab = null; // 生成的符號表
7:
8: protected final Scanner scanner; // 掃描器SCANNER,Parser找它要token
9: protected ICode iCode; // 語法樹根節點。
10:
11: protected Parser(Scanner scanner)
12: {
13: this.scanner = scanner;
14: this.iCode = null;
15: }
16: /**
17: * 交由子類完成具體語言相關的解析過程,這個方法調用之后將會產生符號表和中間碼iCode。
18: * @throws Exception
19: */
20: public abstract void parse()
21: throws Exception;
22: /**
23: * @return 解析過程中的錯誤數
24: */
25: public abstract int getErrorCount();
26:
27: public Token currentToken()
28: {
29: return scanner.currentToken();
30: }
31:
32: public Token nextToken()
33: throws Exception
34: {
35: return scanner.nextToken();
36: }
37: //.....
38: }
因為前端只會產生一個符號表SymTab,所以符號表在Parser中以symTab域出現。
Source類
清單2-2 展示了框架類Source的關鍵方法。
1: /**
2: * <p>此框架類的每個對象代表一個源文件</p>
3: */
4: public class Source implements MessageProducer
5: {
6: // 行結束符,注意在Windows平台上,默認行結束符是\r\n,
7: //如果用記事本之類的寫的pascal源程序,可以使用Ultraedit之類的給轉成Unix格式的。
8: public static final char EOL = '\n';
9: //文件結束標識
10: public static final char EOF = (char) 0;
11: //源程序reader
12: private final BufferedReader reader;
13: private String line;
14: private int lineNum;
15: private int currentPos; // 當前行相對位置,不是整個文件的offset!!
16: public Source(BufferedReader reader)
17: throws IOException
18: {
19: this.lineNum = 0;
20: this.currentPos = -2; // 設置為-2表示文件一行都沒有讀,后面的判斷可以根據是否等於-2讀文件第一行。
21: this.reader = reader;
22: this.messageHandler = new MessageHandler();
23: }
24:
25: /**
26: * @return 要去讀的字符
27: * @throws Exception(read過程中的異常)
28: */
29: public char currentChar()
30: throws Exception
31: {
32: // 第一次讀?
33: if (currentPos == -2) {
34: readLine();
35: return nextChar();
36: }
37:
38: // 文件結束?
39: else if (line == null) {
40: return EOF;
41: }
42:
43: // 行結束?
44: else if ((currentPos == -1) || (currentPos == line.length())) {
45: return EOL;
46: }
47:
48: // 超過一行,換一行再讀
49: else if (currentPos > line.length()) {
50: readLine();
51: return nextChar();
52: }
53:
54: // 正常讀取當前行的某一列的字符
55: else {
56: return line.charAt(currentPos);
57: }
58: }
59:
60: /**
61: *位置游標前進一步並返回對應的字符,記住source的位置游標<b>從來不后退,只有向前操作。</b>
62: * @return 下一個要讀取的字符
63: * @throws Exception
64: */
65: public char nextChar()
66: throws Exception
67: {
68: ++currentPos;
69: return currentChar();
70: }
71:
72: /**
73: * 探測下一字符,位置游標不增加,跟Stack(棧)的Peek方法一樣效果。
74: * @return 當前位置的字符
75: * @throws Exception
76: */
77: public char peekChar()
78: throws Exception
79: {
80: currentChar();
81: if (line == null) {
82: return EOF;
83: }
84:
85: int nextPos = currentPos + 1;
86: return nextPos < line.length() ? line.charAt(nextPos) : EOL;
87: }
88: /**
89: * 讀入一行
90: * @throws IOException
91: */
92: private void readLine()
93: throws IOException
94: {
95: line = reader.readLine();
96: currentPos = -1;
97: //如果讀成功,行數+1
98: if (line != null) {
99: ++lineNum;
100: }
101: //每成功讀入一行,將當前行數和當前行文本內容以消息方式廣播,方便監聽器處理。
102: if (line != null) {
103: sendMessage(new Message(SOURCE_LINE,
104: new Object[] {lineNum, line}));
105: }
106: }
107: public void close()
108: throws Exception
109: {
110: if (reader != null) {
111: try {
112: reader.close();
113: }
114: catch (IOException ex) {
115: ex.printStackTrace();
116: throw ex;
117: }
118: }
119: }
120: //more ignored
121: }
構造函數的參數是一個給Source使用的BufferdReader(I/O類用來按字符讀取源程序文件)。你將會看到通過源文件創建BufferedReader是件很Easy的事情。你也可通過其它對象如路徑串創建BufferedReader。BufferedReader是一個抽象類。你肯定不想Source類操心到底源程序文本內容怎么來的(找bufferedReader即可)。
方法currentChar()干了大部分事情,先前的調用會讓它會調用readLine()方法讀取第一行,則currentChar()返回這行的第一個字符。在后續的調用中,如果當前位置在行尾,它返回特別的EOL字符;如果已經超過行尾,currentChar再次readLine()返回下一行的第一個字符。如果讀到文件末尾,line會是null值,那currentChar()返回一個特殊的EOF字符。其它情況下,此方法currentChar()簡單的返回當在當前行currentPos位置的字符。
方法nextChar()將當前行的currentPos位置前進一步,接着調用chrrentChar()去返回下一個字符。(注意位置指針前移了)
假設源文件當前行包含ABCDE五個字符且currentPos是0。那么按如下順序調用currentChar()和nextChar(),每次調用返回字符如下標:
1: currentChar() ⇒ 'A'
2: nextChar() ⇒ 'B'
3: nextChar() ⇒ 'C'
4: nextChar() ⇒ 'D'
5: currentChar() ⇒ 'D'
6: currentChar() ⇒ 'D'
7: nextChar() ⇒ 'E'
8: nextChar() ⇒ EOL
nextChar() “吞噬”當前字符(將currentPos增1使其指向下一個字符),但currentChar()不是。有時候你需要調用nextChar()吞噬當前字符,但不用它返回的字符。你將會在后面和下章看到怎么使用這兩個方法。
方法peekChar() “向前探測”(后續將簡稱前探)挨着當前字符的下一個字符,此操作不吞噬當前字符。下章中此方法將會區分單個Pascal Token “3.14”和三個Token “3..14.”。注意peekChar在當前位置處於行尾或者超過行尾是,不會讀下一行,它總會返回EOL字符,這不會有啥問題。
在更新域line同時,方法readLine()會把lineNum加1且設置currentPos為0。
Scanner類
清單2-3 展示了框架抽象類Scanner。語言相關的子類將會實現extractMethod方法。Parser調用其nextToken() 方法,而nextToken()方法調用extractToken()去設置和返回私有域currentToken的值。快捷方法currentChar()和nextChar()來自於Source類對應的方法。
清單2-3 抽象類Scanner
1: /**
2: * <p>語言無關的scanner,產生Token</p>
3: */
4: public abstract class Scanner
5: {
6: protected Source source;
7: private Token currentToken; //當前Token
8: public Scanner(Source source)
9: {
10: this.source = source;
11: }
12: public Token currentToken()
13: {
14: return currentToken;
15: }
16:
17: /**
18: * 以source中的char序列模式抽取token
19: * @return 下一個token
20: * @throws Exception
21: */
22: public Token nextToken()
23: throws Exception
24: {
25: currentToken = extractToken();
26: return currentToken;
27: }
28:
29: /**
30: * 因為每個源語言的Token構成方式不一樣,所以這個具體語言的子類去實現。
31: * @return 語言相關的Token
32: * @throws Exception
33: */
34: protected abstract Token extractToken()
35: throws Exception;
36:
37: /**
38: * source的一個快捷方法,可讓子類比不依賴source
39: * @return 要讀取的字符
40: * @throws Exception
41: */
42: public char currentChar()
43: throws Exception
44: {
45: return source.currentChar();
46: }
47: /**
48: * source的一個快捷方法,可讓子類比不依賴source
49: * @return 下一個要讀取的字符
50: * @throws Exception
51: */
52:
53: public char nextChar()
54: throws Exception
55: {
56: return source.nextChar();
57: }
58: }
Token
下面的代碼清單2-4 展示了Token類的關鍵方法
1: /**
2: * <p>Scanner掃描返回的最小語法單元,也是個比不可少的框架類</p>
3: */
4: public class Token
5: {
6: protected TokenType type; // 語言相關的Token類型
7: protected String text; // 字面文本
8: protected Object value; // 值,如果是一些常量,直接可以算出值來的
9: protected final Source source; // source
10: protected int lineNum; // 所在行
11: protected int position; // Token第一個字符所在的位置,即行中列位置
12: public Token(Source source)
13: throws Exception
14: {
15: this.source = source;
16: this.lineNum = source.getLineNum();
17: this.position = source.getPosition();
18: extract();
19: }
20:
21: /**
22: * 當前為演示框架組件,每次都返回一個字符的Token,實際不是這樣的,后面章節會改。<br>
23: * 但是吞噬原理是一樣的,每當Token構成完之后,都把位置游標前移一步。
24: * @throws Exception
25: */
26: protected void extract()
27: throws Exception
28: {
29: text = Character.toString(currentChar());
30: value = null;
31: //吞噬&前進
32: nextChar();
33: }
34:
35: protected char currentChar()
36: throws Exception
37: {
38: return source.currentChar();
39: }
40:
41: protected char nextChar()
42: throws Exception
43: {
44: return source.nextChar();
45: }
46:
47: protected char peekChar()
48: throws Exception
49: {
50: return source.peekChar();
51: }
52: }
根據概念設計,scanner構造出token然后把它們交給parser。因為TokenType是一個接口,你能設置Token的類型(域type)為一個語言相關的值。下章將會演示scanner如何根據當前字符(源文件中),也就是token的首字符,判定要構造的下一個Token的類型。例如,如果首字符為數字,則下一個token為number類型;如果為字母,則下一個token可以是標識符(ID)或關鍵字,因為你用不同Token子類來表示不同Token類型,scanner將會根據首字符調用Token子類相應的構造函數。
構造函數調用extract()方法去實際構造一個Token。方法(extract)名意味着此方法將會從source中讀取字符來抽取token。Token子類根據語言相關的token類型邏輯實現extract方法。Token類提供了一個默認的單字符Token實現。除少數情況,extract()實現會吞噬token字符,而把行的當前位置到Token尾字符的下一個位置(假設when i < k 抽取到Token when,那么首字符為w,尾字符為n,則當前行位置會定位到 i,因為一般空格會被忽略)。
調用extract方法之前,構造函數設置token文本所在行和首字符行中位置。比如關鍵字BEGIN的文本串可為"begin"(Pascal大小寫不敏感),如果文本begin在第11到15位置,那么首字符位置為11,在extract方法返回后,當前位置為16。(15的下一個)
有些Token有值(一般為常量Token)。比如一個數字token的文本是"3.14159"則值與Pi近似。
跟Scanner類一樣,Token類同樣是調用source對象響應方法,實現了currentChar()和nextChar()和peekChar()。
清單2-5 展示了TokenType標記接口。語言相關的Token類型將會用到實現接口。
(標記接口不定義任何方法,它主要用來標識實現了此接口的類。比如所有實現了TokeType接口被認為是Token類型,這在JDK中就有,比如有名的java.io.Serializable類)
清單2-5:TokenType接口
1: package wci.frontend;
2:
3: /**
4: * <p>Token類型,此章沒有任何具體類型實現,僅僅是個為演示框架的占位類</p>
5: */
6: public interface TokenType
7: {
8: }
清單2-6 暫時了語言無關的EofToken子類。因為它僅僅表示文件結束,所以覆蓋extract方法后啥事都沒干。
1: package wci.frontend;
2:
3: /**
4: * <p>表示文件結束的特殊Token</p>
5: */
6: public class EofToken extends Token
7: {
8: public EofToken(Source source)
9: throws Exception
10: {
11: super(source);
12: }
13: protected void extract(Source source)
14: throws Exception
15: {
16: }
17: }
>>> 繼續第二章