学了编译原理能否用 Java 写一个编译器或解释器?

时间:2021-09-18 14:49:15

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 写的吗?
当然可以啦,编译原理课的实验就是用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() 进行下一次解析。
学了编译原理能否用 Java 写一个编译器或解释器?

 

答案是肯定的。我们就是学完了《编译原理》课程,小学期做的实验项目就是实现一个简单的编译器。

我天,我们那渣学校让学编译原理时,作业就要求手写lex、递归下降、parser,侧重前端。。项目大作业我偷懒用JAVA Antlr4撸了...
PS,你学了编译原理不是应该知道高级语言都图灵等价了吗?什么语言都能撸的,玩的6了你就会想元编程、FP、C++了。。。。
 
from: https://www.zhihu.com/question/39835953