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插件。![](https://i-blog.csdnimg.cn/blog_migrate/65886ff5b8293c39d31903a8c1fe5501.png)
建议你也可以试试写一下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() 进行下一次解析。
![](https://i-blog.csdnimg.cn/blog_migrate/ec807c32cb042f95070cb7e38cb905fc.jpeg)
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