學了編譯原理能否用 Java 寫一個編譯器或解釋器?


16 個回答

能。我一開始學編譯原理的時候就是用Java寫了好多小編譯器和解釋器。其實用什么語言來實現編譯器並不是最重要的部分(雖然Java也不是實現編譯器最方便的語言),最初用啥語言都可以。

我在大學的時候,我們的軟件工程和計算機科學的編譯原理課的作業好像都是可以用Java來寫的。反正我印象中我給這兩門課寫的作業都是用的Java。

================================================

關於書

用Java寫編譯器/解釋器的話題主可以試試從ANTLR的作者Terence Parr所寫的《Language Implementation Patterns》開始讀,跟着它做實驗。如果是一開始對編譯原理還沒啥頭緒、而又已經對Java的使用比較熟悉的話,跟着這本書做實驗會能學習到不少知識面。不過要把這些知識點都學習扎實了的話還是得進一步讀別的書。

我推薦的書單里《自制編譯器》那本書的類C語言編譯器也是用Java實現的:學習編程語言與編譯優化的一個書單。以這本為第一本書也行。側重點跟上面那本不太一樣。

其它用Java作為范例實現語言的編譯原理書還有若干,例如正統系編譯原理教材:

不過比起上面那些,我還是更推薦EAC2作為學習編譯原理的“第二本書”:
Engineering a Compiler》 Second Edition

================================================

關於現有的實現

其實現實生活中大家用得多的、用Java寫的編譯器還真不少。也舉幾個例子吧。

教學用編譯器

偏重學術研究的用Java實現的編譯器 / 編譯器框架:

在實際產品中用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]
  • …同樣,還有很多,暫時列舉這么多

比較簡易/玩具性質的:

正則表達式庫:

詞法/語法分析器生成器(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插件。
跟語言沒關系,原則上你用什么語言都能搞,只是麻不麻煩,是否有必要的區別了
咦,龍書最后不就附帶了一個 java 寫的嗎?
當然可以啦,編譯原理課的實驗就是用Java實現一個PL/0語言的編譯器,感覺比C語言實現起來要簡單,畢竟Java可用的方法比較多嘛
建議你也可以試試寫一下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編譯器源碼及文檔

最后加上我自己實現的,這個是有別於網上的一般實現,采用的SLR(這個具體可以參考編譯原理類的書籍)的方法,但是由於時間關系,在代碼生成的時候並沒有采用標准的語法制導翻譯,所以還有待改進,不過整體也算是實現了一個SLR分析方法,而且我實現了讀入文法由程序自動的構造SLR分析表,所以文法可以隨時修改,相當於實現了一個Yacc的部分功能,也算有一定參考價值,也把代碼放在這里吧:

也可以參考用flex+bison做詞法分析和語法分析,用c寫語義部分和中間代碼生成。

最近用上面的方法寫了個類java語言的編譯器,有興趣可以交流。

建議開始入門編譯原理不要看龍書,略難,容易在細枝末節處徘徊太長時間。
可以,我們那時候的課程就是要我們自己設計一個編程語言,並解析成對應的c語言程序運行出來,雖然那時候寫的很差勁,但基本能實現。
可以,這個就是鄙校大三編譯原理課的大作業

實現一個編譯器並不難,因為我們在學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() 進行下一次解析。

 

答案是肯定的。我們就是學完了《編譯原理》課程,小學期做的實驗項目就是實現一個簡單的編譯器。

我天,我們那渣學校讓學編譯原理時,作業就要求手寫lex、遞歸下降、parser,側重前端。。項目大作業我偷懶用JAVA Antlr4擼了...
PS,你學了編譯原理不是應該知道高級語言都圖靈等價了嗎?什么語言都能擼的,玩的6了你就會想元編程、FP、C++了。。。。
 
from: https://www.zhihu.com/question/39835953


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM