本章描述了本書的目標和用到的方法並鳥瞰編譯器和解釋器的全貌。
目標和方法
本書講授編譯器和解釋器的基本寫法,目標是呈現給你怎樣設計和開發它們:
- 用Java寫的編譯器,編譯Pascal(一個高級的面向過程的編程語言)的一個主要子集。(即包含主要的語言特征,但去掉一些為寫編譯器方便而去掉的無關大雅的特性)。
- 用Java寫的解釋器且包含一個交互式的符號調試器(符號調試器即基於符號表,而不是基於機器的指令集、硬件的調試功能),解釋同樣的Pascal語言子集。
- 帶圖形用戶界面的集成開發環境(IDE)。這個IDE是你看到的功能全面的開源的Eclipse或者Borland的JBuilder等IDE的一個簡化版。不過,它也包含一個源程序編輯器和一個交互界面用來設置斷點,單步調試,查看和修改變量值等等其它。
達成這些個極具野心的目標是個大挑戰。好的技能將會幫你如何如把程序編譯成為機器語言或解釋執行程序。現代軟件工程法則和優秀的面向對象設計思想將會給你呈現怎么通過代碼實現一個編譯器或解釋器而最終所有組件能良好協作。編譯器和解釋器程序大且復雜。開發個小程序僅需要某種技能即可,然NB的程序如編譯器或解釋器還需要軟件工程法則和面向對象設計。因此本書強調必備技能,軟件工程法則和面向對象思想。
什么是編譯器和解釋器
編譯器和解釋器的主要目的是“翻譯”由高階(High-Level)源語言寫的源程序。把源程序翻譯成什么樣是接下幾個段落的主題。
本書中源語言為Pascal的一個大子集,換句話說,你能夠編譯或解釋正規的Pascal程序。因為編譯器和解釋器是用Java寫的,實現語言是Java。
Pascal編譯器將Pascal源程序翻譯成為低階(Low-Level)的某具體機器的機器語言(更准確的講是CPU的機器語言)。通常源程序是文本格式。如果編譯器工作正常,對應的機器語言和最初的Pascal源程序殊路同歸(一樣的行為,只不過呈現方式不一樣。比如你用鑰匙而偷車的直接電線打火發動汽車一樣)。機器語言是目標語言,編譯器生成用機器語言組成目標代碼。代碼生成之后,編譯器任務就算完成。目標代碼一般寫到文件里(一般是二進制文件)。
一個程序可包含數個源文件,而編譯器為每個文件生成一個目標文件。一個名叫“鏈接器”(linker)的輔助程序將這些目標文件的內容連同運行時庫程序合成到一個計算機能夠加載和執行的目標程序(如windows的PE程序)中。庫程序一般來自於預先編譯好的目標文件。
因為機器語言不好記,編譯器可生成匯編語言作為目標語言,匯編語言離機器語言只有一步之遙。通常每個匯編指令都有機器語言的指令與之對應。如果你掌握了短助記名(比如ADD和MOV等)匯編語言好記多了。匯編器(另一個編譯器)將匯編語言翻譯成為機器語言。
圖1-1 概括了將一個或多個源程序編譯成為目標程序的過程。
圖左邊展示了將一個包含三個源文件sort1.pas、sort2.pas、sort3.pas的Pascal程序翻譯成為三個相應機器語言目標文件sort1.obj、sort2.obj、sort3.obj。鏈接器將三個目標文件(連帶相關運行時庫) 合成為一個可執行的目標程序sort.exe。圖右邊展示了編譯器將Pascal源文件翻譯為匯編語言目標文件sort1.asm、sort2.asm、sort3.asm,接着匯編器將其轉化為機器語言目標文件。最后鏈接器產生目標程序sort.exe。
圖1-1
那么編譯器和解釋器到底有和不同?
解釋器不生成任何目標程序,相反它讀進源程序就會執行。這好比你被一個Pascal程序把住手,按照它說的某種語句讀進順序去做。你可以在一張草稿紙上記下程序的變量值直到程序結束才輸出每條語句的輸出結果。本質上你做的正是Pascal解釋器干的事情。Pascal解釋器讀進程序,執行程序。沒有任何目標程序需要生成和加載,相反,解釋器將程序翻譯成為一系列用來執行程序的動作(Action)。
比較編譯器和解釋器
該如何決策何時用編譯器和何時用解釋器?
當你把一個源程序交給解釋器,解釋器接管檢查和執行。編譯器也檢查但生成目標代碼。運行完編譯器之后還有運行鏈接器產生目標程序,且還需加載目標程序到內存中去執行它。如果編譯器生成匯編語言代碼,你還得運行匯編器。所以很顯然解釋器需要更少步驟。
解釋器比編譯器更常見。你可用Java寫個Pascal解釋器運行在基於微軟Windows的PC上,蘋果的MAC(麥金塔)或某個Linux主機上,解釋器能夠在前面提到的平台上執行Pascal程序。而編譯器必須為某個具體的機器生成代碼(無論直接生成或間接通過匯編器生成)。所以即使你要把原來為PC寫的Pascal編譯器放到MAC上運行,它生成的代碼仍舊是PC的,如果想讓它為MAC生成代碼,你可能得重寫編譯器的某些部分。
(接下來討論的編譯器將問題的重心放在為Java虛擬機生成代碼上,因為虛擬機能夠運行在很多平台上。所以為具體機器生成代碼先放一邊,有興趣可以將虛擬機替換成為真實PC機上生成x86指令看看)
如果源程序中包含邏輯錯誤,比如除值為0的變量,直到運行時才發現,那么會發生什么情況?
因為解釋器在執行程序過程中控制一切,它能停下來告訴你出問題的行數和變量名稱。它甚至能提示你在繼續執行程序之前可以做哪些正確操作比如修改變量值為非零。解釋器可包含一個交互式的源級(source-level)調試器,俗稱符號調試器(symbolic debugger)。符號調試器意味着你可用程序中的符號,比如變量名。
另一方面,由編譯器和鏈接器產生的目標程序通常自我運行(由機器執行,無需第三方)。源程序有關行號和變量名等信息在目標程序中不可見。當運行時拋錯,程序簡單中斷,還可能打印一條包含出問題指令地址的消息。於是找出源程序中相關語句變量除零的問題就交給你了,。
所以通常就調試來說,解釋器才是正道。有些編譯器在目標代碼中添加一些額外的信息,這樣當錯我發生時,目標程序能打印出相應的問題行數和變量名等。於是你改正錯誤,重新編譯,然后重新運行。生成額外的信息會導致程序執行的比正常要慢(這也是Visual C++為什么有Run/Debug編譯模式)。這提示你在認為程序到達最終“產品”版本后,應關掉調試特征重新編譯。
假設你已經成功調試好程序,那重點將是怎樣使運行更快。因為機器能夠以最快速度執行原生機器語言程序,編譯程序能夠比解釋器快好幾個量級。顯然就速來來說編譯器是勝者,當優化版編譯器知道怎么生成具體場景的優化代碼的情況下尤其確定。所以是否使用編譯器或解釋器取決於程序的開發和執行誰更重要。理想情況是一個帶符號源級調試器的解釋器用在開發過程中,一個生成機器代碼的編譯器在程序調試OK之后以求更快的執行速度。這些就是本書的目標,因為它編譯器,解釋器都教。
情景變得有點模糊
編譯器和解釋器的差異很容易說明清楚,但是隨着虛擬機的快速流行,情景變得有點模糊。
虛擬機是一個用來模擬機器(計算機)的程序。此程序能夠運行在不同的真實計算機平台上。舉個例子,Java 虛擬機(JVM)能夠運行在基於微軟Windows的PC上,蘋果的MAC(麥金塔),Linux系統和其它很多平台上。(比如Sparc,IBM小型機等)。
虛擬機有自己的虛擬機器語言,而虛擬語言指令被真實宿主機所解釋。那么如果你寫了一個翻譯器將Pascal源程序翻譯成為被宿主機解釋的虛擬機語言,這個翻譯器算編譯器還是解釋器?
不斤斤計較了,我們本書約定如果一個翻譯器將源程序轉化成為機器語言,不管是真實的機器語言還是虛擬機器語言,那么這個翻譯器就是編譯器。翻譯器沒有優先生成機器語言去執行程序的就算解釋器。
為什么學習編譯器編寫技術?
我們都想當然的認為對編譯器和解釋器學習了個大概,因為你在開發中需要聚焦在編寫和調試程序上,你甚至不需要思考編譯器的工作機制。你或許僅僅在搞錯語法編譯器拋出錯誤信息后才留意到編譯器的存在。如果沒有語法錯誤,那么編譯將會生成正確的代碼無疑。如果你的程序運行失常,你有可能怪罪編譯器,但大多時候,你會發現錯誤在你的程序中。
以上情形通常會出現在你在使用某個流行編程語言(比如Java或C++)它的編譯器、解釋器和IDE都給你准備好了的時候。這先聊到這。
不過最近我們看到很多新編程語言在被開發。驅動力包括www(比如HTML5)和與基於web的應用相適應的新語言(典型比如PHP,純web)。對程序員生產力的更高要求催生與具體應用領域緊密結合的新語言(這個可以舉很多例子,比如為系統管理員的各種Shell語言,為數據庫開發的各種SQL/NO SQL語言,為電路板/DSP開發的類VHDL語言等,為工作流開發的各種BPM語言等)。你可能非常期待自己有天能發明個新腳本語言表達算法或控制與你領域相關的流程。如果你要發明新語言,對應的編譯器和解釋器必不可少。
編譯器和解釋器本身很好玩,但你前面注意到了,任何一個都不是個小程序,要開發成功相關的技能,現代軟件工程法則和良好的OO設計思想必不可少。除了學習編譯器解釋器工作機制帶來的滿足感外,你也要笑着面對編寫它們帶來的挑戰。
概念設計
為接下幾章做准備,讓我們重溫編譯器和解釋器的概念設計
設計筆記 |
程序的概念設計是它的軟件架構的一個高級視圖。概念設計包含程序的主要組件,它們怎么組織,相互之間的交互細節等。它不需要說明組件怎么實現,更確切的說,它可讓你先確認和理解組件而無需擔心最終怎么去開發它們。 |
你可將編譯器和解釋器歸為程序語言翻譯器。如前面解釋的那樣,編譯器將源程序翻譯成機器語言而解釋器將之翻譯成系列動作(Action)。站在最高角度看翻譯器,它包含一個前端(front end)和一個后端(back end)。遵從軟件重用法則,你將看到Pascal編譯器和Pascal解釋器共享前端,但有不同的后端。
翻譯器的前端讀入源程序然后執行最初的翻譯過程。它的主要組件有parser, scanner(更學院派的說法是Lexer即詞法分析器),token(最小語言單位,最大詞法單元)和source(表示源代碼)。
paser控制前端的翻譯過程。它不斷的從scanner讀入token,根據token串(就是token模式)判定當前正翻譯的高階語言元素,比如算術表達式,賦值語句,過程申明等。parser檢驗源程序的語法是否正確。paser干的事情稱之為解析(parsing),parser分析源程序然后將之轉換。(轉換成啥?后面會有,一般為抽象語法樹之類的中間層)
scanner一個接一個字符讀入源程序的內容,然后構造tokens即源語言的低階元素。例如Pascal tokens包含關鍵字如BEGIN、END、IF、THEN和ELSE,標識符即變量、過程、函數名稱(identifier,又稱ID)以及特殊符號如= := + - *和/ 。scanner干的事情稱為掃描(scanning)。scanner掃描源程序,將之分成一個個token。
圖1-2 展示了編譯器和解釋器前端的概念設計
圖1-2
此圖中,箭頭表示一個組件給另外一個發送命令。parser告訴scanner要下一個token。scanner從source中獲取字符然后構造新的token。token 也從source中讀入字符。(13章會講到為何scanner和token組件都需要從source中讀取字符)
編譯器最終將源程序翻譯成機器語言目標代碼,所以后端的一個重要組件是代碼生成器(目標代碼生成器 code generator)。解釋器執行程序,所以其后端的首要組件是執行器(executor)。
如果你想讓編譯器和解釋器共享前端,那么它們不同的后端需要有個通用接口用來與前端打交道(也就是只需要將前端傳入這個接口即可)。記住前端處理最初的翻譯過程。前端生成作為公共接口中間層的中間代碼(intermediate code,分析樹/語法樹,抽象語法樹等)和符號表(symbol table)。
中間碼(intermediate code)是源程序的預摘要格式(pre-digested,可以理解為在源程序格式和機器語言格式中間的一個摘要格式,一般為分析樹parse tree或語法樹syntax tree)為方便后端的更有效處理(假設翻譯器將塑料翻譯成為瓶子,那么源程序為塑料,中間碼為瓶蓋,瓶身,包裝紙,這樣后端就能更快的裝瓶子)。本書中的中間碼是一個駐內存表示源程序語句的樹狀數據結構(也就是語法樹,廢話一堆啊)。符號表包含源程序的符號信息(比如標識符)。編譯器的后端處理中間碼和符號表,生成源程序對應的機器語言。解釋器碰到中間碼和符號表就直接執行了(通常是樹遍歷過程)。
為軟件重用,你可將中間碼和符號表設計成語言無關的結構。換句話說,你可用同樣的結構應用於不同的源語言。因此,后端同樣可以語言無關,當它處理這些結構(中間碼和符號表)是根本不需要知道具體源語言。
圖1-3 展示了一個更為復雜的編譯器和解釋器的概念設計。如果你萬事安好,僅需前端知道源語言定義且僅需后端知道區分編譯器和解釋器。
第2章開始通過設計一個編譯器解釋器框架來充實概念設計。第3章講的是掃描(scanning)。第4章構建第一個符號表,第五章生成最初的中間碼。第6章開始編寫執行器(executor)且增量式開發直到14章,其中包含符號調試器和IDE。代碼生成直到在15章學了了JVM架構之后的16章才涉及。
語法和語義(syntax and semantics)
編程語言的語法是一系列規則用來斷定用此語言寫的語句或表達式是否正確。語言的語義傳達語句和表達式的具體意思(賦值誰賦給誰,循環終止條件是什么)。舉個例子,Pascal的語法告訴我們 i := j+k 是一個有效的賦值語句。它的語義是說將變量j 和k的當前值加起來,然后將和賦給 i。
parser基於源語言的語法和語義執行有關動作。掃描源程序抽取tokens是語法動作。查找賦值語句 := 之后的目標變量是語法動作。將標識符(identifiers) i、j、k當作變量存入符號表或日后在符號表中查找是語義動作,因為parser必須明白當前表達式和賦值的意思才知道得用到符號表。生成代表此賦值語句的中間碼屬於語義動作。
語法動作盡在前端發生,語義動作在前后端都有。在后端執行程序或者生成目標代碼需要知道語句的具體意思,所以是語義動作一部分。中間碼和符號表存儲語義信息。
詞法,語法和語義分析
詞法分析是掃描(scanning)的正式說法,所以scanner也稱詞法分析器(lexical analyzer)。語法分析是parsing(解析,parser的主要任務)的正式稱謂,語法分析器就是parser。語義分析主要是檢查語義規則是否完整。類型檢查(type checking)就是一例,它確保操作符(operator)的操作數(operand)類型保持一致。其它的語義分析操作有構造符號表和生成中間碼。