16 個回答
能。我一開始學編譯原理的時候就是用Java寫了好多小編譯器和解釋器。其實用什么語言來實現編譯器並不是最重要的部分(雖然Java也不是實現編譯器最方便的語言),最初用啥語言都可以。
我在大學的時候,我們的軟件工程和計算機科學的編譯原理課的作業好像都是可以用Java來寫的。反正我印象中我給這兩門課寫的作業都是用的Java。
================================================
關於書
用Java寫編譯器/解釋器的話題主可以試試從ANTLR的作者Terence Parr所寫的《Language Implementation Patterns》開始讀,跟着它做實驗。如果是一開始對編譯原理還沒啥頭緒、而又已經對Java的使用比較熟悉的話,跟着這本書做實驗會能學習到不少知識面。不過要把這些知識點都學習扎實了的話還是得進一步讀別的書。
我推薦的書單里《自制編譯器》那本書的類C語言編譯器也是用Java實現的:學習編程語言與編譯優化的一個書單。以這本為第一本書也行。側重點跟上面那本不太一樣。
其它用Java作為范例實現語言的編譯原理書還有若干,例如正統系編譯原理教材:- 龍書第二版:《Compilers: Principles, Techniques, and Tools》 (2nd Edition)。這本書的附錄A有一個完整而簡單的編譯器前端的范例,用Java語言實現。這個前端包括詞法分析、語法分析和三地址線性中間代碼的生成。
- 虎書的Java版:《Modern Compiler Implementation in Java》。不過這書配套的范例Java代碼寫得很不Java…書本身還不錯,范例代碼風格我不愛。
- 《Introduction to Compiler Construction in a Java World》。這本我沒仔細讀過,不知道好不好。
- 《Compiler Construction Using Java, JavaCC, and Yacc》。同上,這本我也沒讀過,不過這兩本據說都還挺適合入門閱讀。
不過比起上面那些,我還是更推薦EAC2作為學習編譯原理的“第二本書”:《
Engineering a Compiler》 Second Edition
================================================
關於現有的實現
其實現實生活中大家用得多的、用Java寫的編譯器還真不少。也舉幾個例子吧。
教學用編譯器- 斯坦福大學編譯器課程CS143的Cool語言。請跳傳送門:斯坦福大學編譯原理課程質量怎么樣? - RednaxelaFX 的回答
偏重學術研究的用Java實現的編譯器 / 編譯器框架:
- COINS: a COmpiler INfraStructure project
- joeq [IBM/Stanford]
- Jikes RVM [IBM]
- Maxine VM / T1X / C1X / Graal [Oracle/Sun]
- Soot: A framework for analyzing and transforming Java and Android Applications
- WALA [IBM] <- 這個自身不是編譯器而是做程序分析的庫,但其內容跟編譯器有許多關聯。
- …還有很多,不列舉了
在實際產品中用Java實現的編譯器 / 解釋器:
- javac [Oracle/Sun]
- ECJ: Eclipse Compiler for Java [Eclipse]
- Groovy
- JRuby
- Jython
- Nashorn [Oracle]
- Rhino [Mozilla]
- DynJS
- Graal [Oracle]
- RoboVM
- asc: ActionScript 3 Compiler [Adobe]
- GWT [Google]
- Google Closure Compiler [Google]
- Quercus [Caucho]
- …同樣,還有很多,暫時列舉這么多
比較簡易/玩具性質的:
正則表達式庫:
- java.util.regex.*
- joni: Java port of Oniguruma regexp library
- …
詞法/語法分析器生成器(parser generator):
- ANTLR
- JavaCC
- Jay
- CUP
- JLex
- JMeta
- …
匯編器庫:
2016.2.12更新:
其實僅僅是一個玩具級的編譯器前端,並沒有太高的技術含量,本人菜鳥一枚,並非大神^_^
這兩天整理了一下源碼,已發布到Github,有興趣的同學歡迎關注^_^
源碼鏈接:GitHub - kasonyang/kalang: A toy compiler front-end
-----------------以下原回答-----------------------
大贊
的回答。
最近用java折騰了一個類java的編譯器前端,說說本人的思路:
詞法語法分析->構建抽象語法樹(AST)->類型檢查->生成目標代碼
1. 首先,第一步是詞法語法分析,這一塊我采用的是antlr。antlr項目里有很多語言的文法實現可供參考:GitHub - antlr/grammars-v4: Grammars written for ANTLR v4; expectation that the grammars are free of actions.
2. 語法分析后,接下來就是構建抽象語法樹。這一步就是將parser(由antlr生成)的輸出(ParseTree)進行進一步的處理,得到AST,以便更好的對語法進行分析,同時可以對語法樹就行標志或者轉換等操作。在構建語法樹的過程中,可以對ParseTree和AST進行映射,這樣對AST進行分析時如果有任何問題可以由AST反推回去ParseTree,以便定位錯誤的位置。
3. 得到抽象語法樹后,如果你的編譯器是靜態類型安全的,那么還需要對語法樹進行類型檢查,在做類型檢查的時候可以順便把語法糖處理了,比如說Integer i=3,這個賦值是類型不匹配的,你需要將其轉換為:Integer i=new Integer(3),這樣就類型匹配了。
4. 類型檢查完后,就可以生成目標代碼了。目標代碼可以生成本地的二進制代碼,也可以生成java的字節碼,甚至可以生成另一種高級語言的表示。剛開始時我是直接生成java的class二進制文件的,后來發現因為自己在這方面沒什么經驗,生成的代碼老是有各種問題,每次出問題調試起來非常費勁,有好幾次找bug找半天沒找出來,信心大受打擊,直接想放棄了。后來痛定思痛,把直接生成class文件的功能暫時先砍掉了,而是生成另一種高級語言java的表示,這樣有什么bug,找起來就方便多了。
編譯寫完后,總覺得還缺點什么,然后我又給自己的語言寫了個IDE插件。
建議你也可以試試寫一下PL/0語言的編譯器,原因是比較簡單容易入手而且自頂向下的PL/0編譯器的實現網上有很多資料
下面是題目要求:
Pl/0語言文法的BNF表示: <字母> → a|b|c…x|y|z <數字> → 0|1|2…7|8|9 <無符號整數> → <數字>{<數字>} <標識符> → <字母>{<字母>|<數字>} 1.A→B. 〈程序〉→〈分程序>. 2.B→CEFH|H|CH|EH|FH|CFH|CEH|EFH 〈分程序〉→ [<常量說明部分>][<變量說明部分>][<過程說明部分>]〈語句〉 3.C→CD;|c <常量說明部分> → CONST<常量定義>{ ,<常量定義>}; 4.D→b=a <常量定義> → <標識符>=<無符號整數> 5.E→Eb;|d <變量說明部分> → VAR<標識符>{ ,<標識符>}; 6.F→GB;F <過程說明部分> → <過程首部><分程序>;{<過程說明部分>} 7.G→eb; <過程首部> → procedure<標識符>; 8.H→I|R|T|S|U|V|J|ε <語句> → <賦值語句>|<條件語句>|<當型循環語句>|<過程調用語句>|<讀語句>|<寫語句>|<復合語句>|<空> 9.I→b:=L <賦值語句> → <標識符>:=<表達式> 10.J→fWg <復合語句> → begin<一個或多個語句><end> 11.K→LQL|hL <條件> → <表達式><關系運算符><表達式>|ood<表達式> 12.L→LOM|M|-M|+M <表達式> → [+|-]<項>{<加減運算符><項>} 13.M→MPN|N <項> → <因子>{<乘除運算符><因子>} 14.N→b|a|(L) <因子> → <標識符>|<無符號整數>|(<表達式>) 15.O→+|- <加減運算符> → +|- 16.P→*|/ <乘除運算符> → *|/ 17.Q→=|#|<|<=|>|>= <關系運算符> → =|#|<|<=|>|>= 18.R→pKqH <條件語句> → if<條件>then<語句> 19.S→mb <過程調用語句> → call<標識符> 20.T→nKoH <當型循環語句> → while<條件>do<語句> 21.U→i(X) <讀語句> → read(<一個或多個標識符>) 22.V→j(X) <寫語句> → write(<一個或多個標識符>) 23.W→W;H|H <一個或多個語句>→<語句>{ ;<語句>} 24.X→X,b|b <一個或多個標識符>→<標識符>{,<標識符>} 一. 為PL/0語言建立一個詞法分程序GETSYM(函數) 把關鍵字、算符、界符稱為語言固有的單詞,標識符、常量稱為用戶自定義的單詞。為此設置三個全程量:SYM,ID,NUM 。 SYM:存放每個單詞的類別,為內部編碼的表示形式。 ID:存放用戶所定義的標識符的值,即標識符字符串的機內表示。 NUM:存放用戶定義的數。 GETSYM要完成的任務: 1. 濾掉單詞間的空格。 2. 識別關鍵字,用查關鍵字表的方法識別。當單詞是關鍵字時,將對應的類別放在SYM中。如IF的類別為IFSYM,THEN的類別為THENSYM。 3. 識別標識符,標識符的類別為IDENT,IDRNT放在SYM中,標識符本身的值放在ID中。關鍵字或標識符的最大長度是10。 4. 拼數,將數的類別NUMBER放在SYM中,數本身的值放在NUM中。 5. 拼由兩個字符組成的運算符,如:>=、<=等等,識別后將類別存放在SYM中。 6. 打印源程序,邊讀入字符邊打印。 由於一個單詞是由一個或多個字符組成的,所以在詞法分析程序GETSYM中定義一個讀字符過程GETCH。 二. 為PL/0語言建立一個語法分析程序BLOCK(函數) PL/0編譯程序采用一遍掃描的方法,所以語法分析和代碼生成都有在BLOCK中完成。BLOCK的工作分為兩步: a) 說明部分的處理 說明部分的處理任務就是對每個過程(包括主程序,可以看成是一個主過程)的說明對象造名字表。填寫所在層次(主程序是0層,在主程序中定義的過程是1層,隨着嵌套的深度增加而層次數增大。PL/0最多允許3層),標識符的屬性和分配的相對地址等。標識符的屬性不同則填寫的信息不同。 所造的表放在全程量一維數組TABLE中,TX為指針,數組元素為結構體類型數據。LEV給出層次,DX給出每層的局部量的相對地址,每說明完一個變量后DX加1。 例如:一個過程的說明部分為: const a=35,b=49; var c,d,e; procedure p; var g; 對它的常量、變量和過程說明處理后,TABLE表中的信息如下: NAME: a NAME: b NAME: c NAME: d NAME: e NAME: p KIND: CONSTANT KIND: CONSTANT KIND: VARIABLE KIND: VARIABLE KIND: VAEIABLE KIND: PROCEDURE VAL: 35 VAL: 49 LEVEL: LEV LEVEL: LEV LEVEL: LEV LEVEL: LEV ADR: DX ADR: DX+1 ADR: DX+2 ADR: NAME: g 。 。 。 KIND: VARIABLE 。 。 。 LEVEL: LEV+1 。 。 。 ADR: DX 。 。 。 對於過程名的ADR域,是在過程體的目標代碼生成后返填過程體的入口地址。 TABLE表的索引TX和層次單元LEV都是以BLOCK的參數形式出現,在主程序調用BLOCK時實參的值為0。每個過程的相對起始位置在BLOCK內置初值DX=3。 2.語句處理和代碼生成 對語句逐句分析,語法正確則生目標代碼,當遇到標識符的引用則去查TABLE表,看是否有過正確的定義,若有則從表中取出相關的信息,供代碼生成用。PL/0語言的代碼生成是由過程GEN完成。GEN過程有三個參數,分別代表目標代碼的功能碼、層差、和位移量。生成的目標代碼放在數組CODE中。CODE是一維數組,數組元素是結構體類型數據。 PL/0語言的目標指令是一種假想的棧式計算機的匯編語言,其格式如下: 其中f代表功能碼,l代表層次差,a代表位移量。 目標指令有8條: ① LIT:將常數放到運棧頂,a域為常數。 ② LOD:將變量放到棧頂。a域為變量在所說明層中的相對位置,l為調用層與說明層的層差值。 ③ STO:將棧頂的內容送到某變量單元中。a,l域的含義與LOD的相同。 ④ CAL:調用過程的指令。a為被調用過程的目標程序的入中地址,l為層差。 ⑤ INT:為被調用的過程(或主程序)在運行棧中開辟數據區。a域為開辟的個數。 ⑥ JMP:無條件轉移指令,a為轉向地址。 ⑦ JPC:條件轉移指令,當棧頂的布爾值為非真時,轉向a域的地址,否則順序執行。 ⑧ OPR:關系和算術運算。具體操作由a域給出。運算對象為棧頂和次頂的內容進行運算,結果存放在次頂。a域為0時是退出數據區。 三. 建立一個解釋執行目標程序的函數 編譯結束后,記錄源程序中標識符的TABLE表已退出內存,內存中只剩下用於存放目標程序的CODE數組和運行時的數據區S。S是由解釋程序定義的一維整型數組。解釋執行時的數據空間S為棧式計算機的存儲空間。遵循后進先出的規則,對每個過程(包括主程序)當被調用時,才分配數據空間,退出過程時,則所分配的數據空間被釋放。 為解釋程序定義四個寄存器: 1. I:指令寄存器,存放當前正在解釋的一條目標指令。 2. P:程序地址寄存器,指向下一條要執行的目標指令(相當於CODE數組的下標)。 3. T:棧頂寄存器,每個過程運行時要為它分配數據區(或稱為數據 段),該數據區分為兩部分。 靜態部分:包括變量存放區和三個聯單元。 動態部分:作為臨時工作單元和累加器用。需要時臨時分配,用完立即釋放。棧頂寄存器T指出了當前棧中最新分配的單元(T也是數組S的下標)。 4. B:基地址寄存器,指出每個過程被調用時,在數據區S中給出它分配的數據段起始地址,也稱為基地址。每個過程被調用時,在棧頂分配三個聯系單元。這三個單元的內容分別是: SL:靜態鏈,它是指向定義該過程的直接外過程運行時數據段的基地址。 DL:動態鏈,它是指向調用該過程前正在運行過程的數據段的基地址。 RA:返回地址,記錄調用該過程時目標程序的斷點,即當時的程序地址寄存器P的值。 具體的過程調用和結束,對上述寄存器及三個聯系單元的填寫和恢復由下列目標指令完成。 1. INT 0 a a:為局部量個數加3 2. OPR 0 0 恢復調用該過程前正在運行過程(或主程序)的數據段的基地址寄存器的值,恢復棧頂寄存器T的值,並將返回地址送到指令寄存器P中。 3. CAL l a a為被調用過程的目標程序的入口,送入指令地址寄存器P中。 CAL指令還完成填寫靜態鏈,動態鏈,返回地址,給出被調用過程的基地址值,送入基址寄存器B中。 例:一個Pl/0源程序及生成的目標代碼: const a=10; var b,c; procedure p; begin c:=b+a end; 2 int 0 3 3 lod 1 3 4 lit 0 10 5 opr 0 2 6 sto 1 4 7 opr 0 0 begin read(b); while b#0 do begin call p; write(2*c); read(b) end end . 8 int 0 5 9 opr 0 16 10 sto 0 3 11 lod 0 3 12 lit 0 0 13 opr 0 9 14 jpc 0 24 15 cal 0 2 16 lit 0 2 17 lod 0 4 18 opr 0 4 19 opr 0 14 20 opr 0 15 21 opr 0 16 22 sto 0 3 23 jmp 0 11 24 opr 0 0
下面是一些資料
Java版的自頂向下實現:
JAVA課程設計PL0編譯器
C語言版的自頂向下實現:
pl/0編譯器源碼及文檔
https://github.com/wangzhaode/PL0-compiler-SLR
也可以參考用flex+bison做詞法分析和語法分析,用c寫語義部分和中間代碼生成。
最近用上面的方法寫了個類java語言的編譯器,有興趣可以交流。
建議開始入門編譯原理不要看龍書,略難,容易在細枝末節處徘徊太長時間。實現一個編譯器並不難,因為我們在學Java之初,就是用記事本進行編譯的。當然,如果制作復雜的編譯器需要編譯器框架,但原理是一樣的。
作者:tyler_download
鏈接:編譯原理動手實操,用java實現一個簡易編譯器1-詞法解析入門 - CSDN博客
來源:CSDN博客
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
技術的發展可謂是日新月異,層出不窮,但無論是炙手可熱的大數據,還是火熱的人工智能,所有這些高大上的尖端科技無不建立在基礎技術的根基之上。編譯原理,計算機網絡,操作系統,便是所有軟件技術的基石。在這三根支柱中,維編譯原理最為難懂,特別是大學課本那種晦澀難通,不講人話的言語,更是讓人覺得這門基礎技術就像九十多歲的老嫗,皮膚干巴,老態龍鍾,讓人提不起一點欲望。除了國內教材,就算是被廣為稱贊的一千多頁的”龍書“,也是滿篇理論,讓人望而生畏。
味道怎樣,咬一口就知道,手感如何,摸一把就曉得。編譯原理缺的不是理論概念,而是能夠動手實踐的流程,代碼,很多原理用話語怎么講都難以明了,但跑一遍代碼,基本就水落石出。本文本着動手實操(念第一聲)的原則,用java實現一個簡單的編譯器,讓讀者朋友能一感編譯原理的實質,我秉持一個原則,沒有代碼可實踐的計算機理論,都是耍流氓。
編譯器作用就是將一種計算機無法理解的文本,轉譯成計算機能執行的語句,我們要做的編譯器如下,將帶有加法和乘法的算術式子,轉譯成機器能執行的匯編語句,t0, t1 是對寄存器的模擬,上述語句基本上就類似計算機能執行的匯編語句了。本章首先專注於詞法解析的探討。
編譯原理由兩部分組成,一是詞法分析,一是語義分析。先說詞法分析,詞法分析就是將一個語句分割成若干個有意義的字符串的組合,然后給分割的字符串打標簽。例如語句:1+2*3+4; 可以分割成 1+, 2*, 3+, 4; 但這些子字符串沒有實質意義,有意義的分割是1, +, 2, * , 3, +, 4, ;. 接着就是給這些分割后的字符串打標簽,例如給1, 2, 3, 4 打上的標簽是NUM_OR_ID, + 打的標簽是PLUS, *的標簽是TIMES, ;的標簽是SEMI, 好了,看看詞法分析的代碼,代碼中2到6行是對標簽的定義,其中LP 代表左括號(, RP代表右括號), EOI 表示語句末尾, 第10行的lookAhead 變量用於表明當前分割的字符串指向的標簽值,yytext用於存儲當前正在分析的字符串,yyleng是當前分析的字符串的長度,yylineno是當前分析的字符串所在的行號。input_buffer 用於存儲要分析的語句例如: 1+2*3+4; isAlNum 用於判斷輸入的字符是否是數字或字母。lex() 函數開始了詞法分析的流程,31到40行從控制台讀入語句,語句以"end"表明結束,例如在控制台輸入:
1+2*3+4;
end
回車后,從52行開始執行詞法解析流程。以上面的輸入為例,input_buffer 存儲語句 1+2*3+4, 由於第一個字符是 1, 在for 循環中,落入switch 的default 部分,isAlNum 返回為真,yyleng 自加后值為1, yytext 存儲的字符串就是 "1", current前進一個字符變為+2*3+4, 再次執行lex(), 則解析的字符是+, 在for 循環中,落入switch的case '+' 分支,於是yytext為"+", 返回的標簽就是PLUS依次類推, advance 調用一次, lex()就執行一次詞法分析,當lex執行若干次后,語句1+2*3+4;會被分解成1, +, 2, *, 3, +, 4, ; 。字符串1, 2, 3, 4具有的標簽是NUM_OR_ID, + 具有的標簽是PLUS, *的標簽是TIMES, ;的標簽是SEMI。
runLexer() 將驅動詞法解析器,執行解析流程,如果解析到的當前字符串,其標簽不是EOI(end of input), 也就是沒有達到輸入末尾,那么就打印出當前分割的字符串和它所屬的標簽,接着調用advance() 進行下一次解析。

PS,你學了編譯原理不是應該知道高級語言都圖靈等價了嗎?什么語言都能擼的,玩的6了你就會想元編程、FP、C++了。。。。
Google: Classrooom Object Oriented Language.
https://theory.stanford.edu/~aiken/software/cool/cool-manual.pdf
code:GitHub - jackzhao-mj/CS164-PA