本章将会从前一章的概念设计带你到初级的实现过程。你将先为编译器和解释器构造一个灵活的框架,接着将初级版的编译器解释器组件集成到框架中。最后编写端对端的测试用例检验这些框架和组件。
==>> 本章中文版源代码下载:svn co http://wci.googlecode.com/svn/branches/ch2/ 源代码使用了UTF-8编码,下载到本地请修改!
目标和方法
此章的设计方法首先会让你觉得过于繁琐啰嗦,的确,本章结束后将会有一大堆超过你预期数量的代码。但请记你在用早被证明的软件工程法则和优秀面向对象设计构建编译器和解释器。
如在概念设计中描述的那样,编译器和解释器将尽可能复用组件,因只有后端有所不同。在这章中,你将构建一个灵活的框架并首先放置那些已被深度简化的编译器和解释器组件。不过它们足够验证你设计的框架是否恰当即组件能很好的耦合并能协同工作。这个成功前提将会使得从公用前端到编译器解释器后端的端对端执行代码编写,还有后续的增量式组件开发变得简单。
本章的目标是:
- 一个语言无关的框架,可支持编译器和解释器。
- 集成进框架前端(front end)的初级版Pascal语言相关组件。
- 集成进框架后端(back end)的初级版编译器和解释器组件。
- 通过从公共前端生成源程序清单以及从编译器或解释器后端生成消息,简单的运行端对端测试,测试相关组件。
设计笔记 |
不管任何时候开发负责程序如编译器或解释器,成功的首要步骤是:
早期的组件集成是关键,甚至你已经简化了初级组件(没有完善的组件称之为初级组件)也一样。尽可能早的测试矿建和组件以让它们更好的协作。框架和初级组件组成你后续开发的基础。开发将是增量式的进行,代码在每次增量后都能继续工作(附加更多功能)。你该永远基于可运行的代码去构建。 |
语言无关的框架组件
基于概念设计,框架包含三个包:frontend、 intermediate、 backend。
框架组件是用来定义框架且语言无关的接口和类。有些是抽象类。一旦框架组件就绪,你能开发抽象类的Pascal实现(组件语言无关,实现语言相关)。图2-1 展示了使用UML 包和类图的框架组件。
图2-1:在frontend,intermediate,backend包中的语言无关组件一起定义了一个能支持编译器和解释器后续开发的框架。
设计笔记 |
统一建模语言是一个工业级的展示面向对象软件架构和过程的图形化语言。各种图表(序列图,类图等)能表示程序的结构组件之间的静态关系,也能表示组件运行期的动态行为。 |
前端
在前端包中,语言无关类Paser,Scanner,Token,Source代表框架组件。框架类强制你在忽略具体源语言的情况下,能尽力思考每个前端组件的职责,还有它们之间的交互。图2-2中的UML 类图展示了它们的关系。
Parser和Scanner是抽象类;语言相关的子类将实现它们的抽象方法。parser和scanner联系紧密,Parser有一个受保护域(protected field)scanner指向Scanner。Parser从Scanner请求token,所以它依赖Token。Scanner有一个私有域currentToken,它通过受保护域source引用Source,还将source引用传给每个自己构造的token。每个Token也能通过受保护域source拥有Source引用,在它的构造过程中,通过source读取字符。
图2-3的类图更进一步展示了四个前端框架类。它展示了域,方法和其他的前端类和接口。例如每个Token有一个用TokenType类表示的token类型,EofToken是Token的子类。
按照概念设计,parser控制翻译过程,它翻译源程序,所以Parser类有一个抽象方法parser();语言相关的方法实现将不断的找scanner索取下一个token。Parser的currentToken()和nextToken()仅仅是scanner的代理方法而已(参考代理模式,不过这儿是为了少写点代码)。语言相关的getErrorCount()方法实现返回语法错误数量。
设计笔记 |
在UML类图中,一个未填充箭头的箭号表示一个类引用或依赖另一个类。虚线箭号(比如从Parser到Token的肩头)表示一个仅仅在方法调用期间(比如Parser的nextToken()方法返回一个Token对象)存在的引用。实线箭号且在出发端有一个空菱形意味着一个类通过在对象生命周期持续的引用,拥有(owns)或聚合(aggregates)另一个类。(假设类A通过引用域ref聚合类B,那么类A的对象a1聚合类B的对象b1的这种关系在a1的生命周期一直存在,聚合相当于包含,a1负责b1的生命周期)。 域名称保存标识箭头的引用(例如,Parser类用它的scanner域维护对Scanner类的引用)。 实心箭号带空箭头(如EofToken类到Token类)表示一个子类到它的父类。 类名称下,一个类图可选择的包含域(field)描述区域和方法描述区域。标识箭号名称的域名不在域描述区域出现(Parser有个域scanner引用类Scanner,它不在Parser的域描述区域出现,在生成代码后就会有)。在域名或者方法名前面的字符表明访问控制。
跟在域名或方法名冒号后面的分别是域类型或者返回值类型。为省地方,类图通常不显示构造函数和域名的getter和setter方法。 抽象类名以斜体出现。抽象方法名还是斜体。 |
scanner从源程序中抽取token。Scanner类抽象方法extractToken()的语言相关实现将会根据具体语言从Source中读取字符,以便构造Token。Scanner的快捷方法currentChar()和nextChar()会调用Source类中的对应方法(还是代理模式)
Token的域保存有关Token的有用信息,包括类型,文本串(即字面上的字符串),值和它在源程序中的位置(行号和位置【相对于行】)。Token同样有Source类的快捷方法currentChar()和nextChar()。Token类型与具体语言有关。当前的Token类型是一个占位符(因为一个具体类型都没有)。
后面你将会根据具体语言创建语言相关的Token子类。但目前只有语言无关EofToken子类,它表示源文件终止。使用Token子类使得scanner代码更加模块化,因为不同类型Token需要不同计算方式。(原文是算法,我认为谈不上算法)。
Parser
清单2-1 展示了框架抽象类Parser的关键方法。语言相关的Parser子类要实现parse()方法和getErrorCount(),分别用来表示源程序分析过程和返回语法错误。如上文提到的,Parser的currentToken()和nextToken()方法是scanner对应方法的代理。
1: /**
2: * <p>语言无关的Parser,有子类完成具体语言解析</p>
3: */
4: public abstract class Parser implements MessageProducer
5: {
6: protected static SymTab symTab = null; // 生成的符号表
7:
8: protected final Scanner scanner; // 扫描器SCANNER,Parser找它要token
9: protected ICode iCode; // 语法树根节点。
10:
11: protected Parser(Scanner scanner)
12: {
13: this.scanner = scanner;
14: this.iCode = null;
15: }
16: /**
17: * 交由子类完成具体语言相关的解析过程,这个方法调用之后将会产生符号表和中间码iCode。
18: * @throws Exception
19: */
20: public abstract void parse()
21: throws Exception;
22: /**
23: * @return 解析过程中的错误数
24: */
25: public abstract int getErrorCount();
26:
27: public Token currentToken()
28: {
29: return scanner.currentToken();
30: }
31:
32: public Token nextToken()
33: throws Exception
34: {
35: return scanner.nextToken();
36: }
37: //.....
38: }
因为前端只会产生一个符号表SymTab,所以符号表在Parser中以symTab域出现。
Source类
清单2-2 展示了框架类Source的关键方法。
1: /**
2: * <p>此框架类的每个对象代表一个源文件</p>
3: */
4: public class Source implements MessageProducer
5: {
6: // 行结束符,注意在Windows平台上,默认行结束符是\r\n,
7: //如果用记事本之类的写的pascal源程序,可以使用Ultraedit之类的给转成Unix格式的。
8: public static final char EOL = '\n';
9: //文件结束标识
10: public static final char EOF = (char) 0;
11: //源程序reader
12: private final BufferedReader reader;
13: private String line;
14: private int lineNum;
15: private int currentPos; // 当前行相对位置,不是整个文件的offset!!
16: public Source(BufferedReader reader)
17: throws IOException
18: {
19: this.lineNum = 0;
20: this.currentPos = -2; // 设置为-2表示文件一行都没有读,后面的判断可以根据是否等于-2读文件第一行。
21: this.reader = reader;
22: this.messageHandler = new MessageHandler();
23: }
24:
25: /**
26: * @return 要去读的字符
27: * @throws Exception(read过程中的异常)
28: */
29: public char currentChar()
30: throws Exception
31: {
32: // 第一次读?
33: if (currentPos == -2) {
34: readLine();
35: return nextChar();
36: }
37:
38: // 文件结束?
39: else if (line == null) {
40: return EOF;
41: }
42:
43: // 行结束?
44: else if ((currentPos == -1) || (currentPos == line.length())) {
45: return EOL;
46: }
47:
48: // 超过一行,换一行再读
49: else if (currentPos > line.length()) {
50: readLine();
51: return nextChar();
52: }
53:
54: // 正常读取当前行的某一列的字符
55: else {
56: return line.charAt(currentPos);
57: }
58: }
59:
60: /**
61: *位置游标前进一步并返回对应的字符,记住source的位置游标<b>从来不后退,只有向前操作。</b>
62: * @return 下一个要读取的字符
63: * @throws Exception
64: */