第五章开发了前端的解析器,用来解析Pascal赋值语句、复合语句和表达式并生成中间码。第六章开发了解释器后端的执行器,用来执行语句和表达式。这章中将开发Pascal控制语句的解析器。这些控制语句的执行器将会在下一章实现。
==>> 本章中文版源代码下载:svn co http://wci.googlecode.com/svn/branches/ch7/ 源代码使用了UTF-8编码,下载到本地请修改!
目标和方法
本章的目标是:
- Pascal控制语句的前端解析器,如WHILE、REPEAT、FOR、IF以及CASE。
- 解析器生成灵活的,语言无关的中间码,用以表现这些控制语句结构。
- 可靠的错误恢复机制确保即使源程序中有语法错误也能继续运行。
方法与前面一样。从语法图开始,它将引导你对这些控制语句解析器子类的开发。为了生成分析树,你将完善第五章开发的中间码接口。你还将扩展第五章的语法检查使用程序,达到解析控制语句和验证新编代码的目的。
语法图
图7-1 展示了Pascal控制语句的语法图(本书不会涉及到Pascal的goto语句,goto是个有争议的话题)
跟以前一样,这类图形将引导我们的解析器子类开发。
错误恢复
第三章提及过前端的语法错误处理是一个三步骤过程:检测(detaction),标记(flagging)和恢复(recovery)。在此之前,解析器子类使用的是非常初级的恢复机制。每当解析器检测到一个错误,它标记这个出错的token并尝试往前探。如果错误是“遗漏token”(missing token),解析器就假定token存在。如果是意外的token,解析器会吞噬掉这个token并向前探测以希望下一个是正常的。
对于错误恢复(后面的“它”指的就是错误恢复)那些是解析器可作出的选择?
- 遇到语法错误,它可以简单粗暴地终止运行。最坏情况下,它可能因为卡在一个永不被吞噬的token而挂住(hang),甚至崩溃。换句话说,没有任何错误恢复。这个选择对于编译器作者来说很容易做,但是对于使用这个编译器的程序员来说那就是极度的闹心了。
- 它可能变得彻底晕乎但还尝试去解析其余源程序,同时不断分离出一系列不着边际的错误消息。这次仍旧没有错误恢复,要选一个的话编译器作者也不会想到采用这个选择。
- 它从出错token往前跳(比如T1 T2 T3 T4,T1出错,它就会从T1后跳跃直到发现T4可以成为某个结构的起始token,也就是能够识别。为什么跳跃?因为T1 T2 T3可能属于同一个结构,只是在T1处出错了而已),直到它能找到一个可识别的token,这样又能正常对其余源程序做语法检查了。
很明显,前两个选项不是我们所期望的。为实现第三个选项,解析器必须时常“同步”自己到它期待的token上。遇到语法错误时,解析器必须在源程序中查找下一个能让语法检查继续可靠进行下去的token。理想情况下,它能尽可能快的在错误发生后找到这样一个token。
清单7-1 展示了在frontend.pascal包中,你要加到Pascal解析器类PascalParserTD中的新方法synchronize()。这个方法被解析器子类用来维护适合自己的同步。
1: /**
2: * 同步解析器的当前token到集合所允许的Token
3: * @param syncSet 下一步有效的token集合
4: * @return 找到的某个有效的token
5: * @throws Exception
6: */
7: public Token synchronize(EnumSet syncSet) throws Exception {
8: Token token = currentToken();
9: //如果当前token不在同步集合里,那么就需要跳跃到结合范围内的token。
10: if (!syncSet.contains(token.getType())) {
11: //报告一个解析错误然后继续往前解析
12: errorHandler.flag(token, PascalErrorCode.UNEXPECTED_TOKEN, this);
13: do {
14: token = nextToken();
15: } while (!(token instanceof EofToken)
16: && !syncSet.contains(token.getType()));
17: }
18: return token;
19: }
此方法的调用者传入一个Pascal Token类型同步集合。方法检查当前token是否在这个集合中,如果是,则没有语法错误,方法马上返回这个token;如果不是,方法先标记一个UNEXPECTED_TOKEN错误并通过跳过后续的token直到一个类型在此集合中的token的方式进行恢复。不管哪一种情况,方法调用后解析器在返回的token这个点上是同步的。(请注意这个方法总是在解析某个结构前先调用一把,这好比打麻将,每次胡牌的过程相当于创建一种结构,比如清一色,一条龙等。同步的过程好比洗牌码牌,码好之后你又很多种允许的打法,这些打法好比集合。我们在打出麻将前首先得检测一下牌是不是少了多了,牌少了搞不好有人的牌多了,你会把它的牌拿过来,这相当于一个同步的过程,这个例子举的不太好,等我想到更好的再修改)
设计笔记 |
top-down解析器的错误恢复机制可以艺术比科学更多一些,它通常需编译器作者尝试好几次才能搞定每种语言结构(或构成,construct)。本书中的解析器使用一个简单多于精致的方法(简单是美?)。sychronize()方法的核心是同步集合参数的元素,个中元素判定在遇到一个语法错误后,解析器得跳过多少token后才能开始再次解析,有可能直接到达源程序结束而不可得。它被认为是一种“应急模式”(panic mode)恢复策略。(出错的先跳过,继续后面正常的) |
程序 7:语法检查器II
这章对第6章的入口主类Pascal没有任何改动。不过作为一个整体的程序语法检查功能,如要兼容控制语句,毫无疑问要添加新解析器子类。
你将会针对各种源文件把这个程序运行好几次,包括带语法错误的,以便验证新解析器子类。由于你还没有写任何解释器后端对应的执行器子类(也就是只有控制语句的中间码,而没有对应的执行器,对应的解释器下一章会讲到),你将用编译器方式(即命令行使用compile pascalfile.txt)运行这个程序以避免UNIMPLEMENTED_FEATURE运行时错误。
控制语句解析器
类图7-2 是类图 5-7的一个补充,它描述了Pascal控制语句的解析器子类关系。
每一个控制语句解析器都是StatementParser的子类,同时StatementParser也依赖它们。此外,因为每个控制语句都包含嵌套语句,所以每个控制语句解析器依赖StatementParser。因此依赖连线两边都有箭头(互相依赖)。每一个控制语句包含一个表达式,所以每个控制语句解析器同样包含依赖ExpressionParser。FOR语句包含一个嵌套的赋值语句,因而ForStatementParser也依赖AssignmentStatementParser(没在这个图中显示,在图5-7)。
首先更新Pascal解析器子类StatementParser和它的子类AssignmentStatementParser,这两个都是你在第5章中开发的。清单7-2 展示了StatementParser中的新版本parse()方法。
1: //5种控制语句
2: case REPEAT: {
3: RepeatStatementParser repeatParser =
4: new RepeatStatementParser(this);
5: statementNode = repeatParser.parse(token);
6: break;
7: }
8: case WHILE: {
9: WhileStatementParser whileParser =
10: new WhileStatementParser(this);
11: statementNode = whileParser.parse(token);
12: break;
13: }
14: case FOR: {
15: ForStatementParser forParser = new ForStatementParser(this);
16: statementNode = forParser.parse(token);
17: break;
18: }
19: case IF: {
20: IfStatementParser ifParser = new IfStatementParser(this);
21: statementNode = ifParser.parse(token);
22: break;
23: }
24: case CASE: {//类似Java/C的switch
25: CaseStatementParser caseParser = new CaseStatementParser(this);
26: statementNode = caseParser.parse(token);
27: break;
28: }
同步集合STMT_START_SET包含能够开始一条新语句的所有token类型,同步集合STMT_FOLLOW_SET包含一条语句之后容许的token类型。STMT_START_SET包含分号用来处理空语句(即";" 也是一个语句,但什么都不干)。parse()方法现在能够应付Pascal控制语句了。
清单7-3 展示了StatementParser中更新过的parseList()方法。它通过克隆STMT_START_SET创建同步集合terminatorSet并增加一个terminator token类型。while循环最末尾调用synchronize(termniatorSet)以便同步在终止token出或新语句开始处。
清单7-3 StatementParser的parseList()方法,黑体为改动处。
1: protected void parseList(Token token, ICodeNode parentNode,
2: PascalTokenType terminator,
3: PascalErrorCode errorCode)
4: throws Exception
5: {
6: EnumSet<PascalTokenType> terminatorSet = STMT_START_SET.clone();
7: terminatorSet.add(terminator);
8: //遍历每条语句直到遇见结束TOKEN类型或文件结束
9: while (!(token instanceof EofToken) &&
10: (token.getType() != terminator)) {
11:
12: // 解析一条语句
13: ICodeNode statementNode = parse(token);
14: //语句子树根节点作为子节点附加到父节点上
15: parentNode.addChild(statementNode);
16:
17: token = currentToken();
18: TokenType tokenType = token.getType();
19:
20: //每条语句之后肯定是一个分号 ; 否则报错
21: if (tokenType == SEMICOLON) {
22: token = nextToken();
23: } else if (STMT_START_SET.contains(tokenType)) {
24: errorHandler.flag(token, MISSING_SEMICOLON, this);
25: }
26: token = synchronize(terminatorSet);
27: }
28: //判断是否已到结束token,如果是就跳过它,否则报错
29: if (token.getType() == terminator) {
30: token = nextToken(); // consume the terminator token
31: }else {
32: errorHandler.flag(token, errorCode, this);
33: }
34: }
清单7-4 展示了语句解析器子类AssignmentStatementParser中更新过后的parse()方法。这儿改动基本与清单7-3类似,就是加入了一致的同步集合方式的错误恢复功能,详细请参见源代码,这里不再展示。
在解析左边的目标变量后,parse()方法调用synchronize(COLON_EQUALS_SET)去在:= token处同步自己。这个同步集合是EXPR_START_SET的克隆加上额外的COLON_EQUALS token类型。为防止错的应急模式恢复策略,这个集合和其它类似集合都包含StatementParser.STMT_FOLLOW_SET集合中的所有token类型。如果解析器找不到:= token,它将同步在表达式开始处,或更坏情形下同步在一个赋值语句后第一个token处。(这个可能不好理解,但记住一条,如果找不到集合中的元素类型,就跳啊跳,跳到合适的位置为止,因为COLON_EQUALS_SET集合包含STMT_FOLLOW_SET,所以当然可以跳到赋值语句后的第一个token处)
ExpressionParser定义了同步集合EXPR_START_SET:
static final EnumSet<PascalTokenType> EXPR_START_SET =
EnumSet.of(PLUS, MINUS, IDENTIFIER, INTEGER, REAL, STRING,
PascalTokenType.NOT, LEFT_PAREN);
解析Pascal控制语句
解析REPEAT 语句
这个语句解析器子类RepeatStatementParser解析一个Pascal REPEAT 语句并产生它的分析树。比如语句:
1: REPEAT
2: j := i;
3: k := i
4: UNTIL i <= j
parse()方法将生成如图7-3 展示的语法树
LOOP节点可以有任意语句子树孩子节点。其中至少有一个是TEST节点,它只有一个关系表达式子树子节点。在运行时,如果表达式计算值为true循环就退出。TEST节点可以使LOOP孩子节点中的任意一个,因而退出测试能发生循环开始出,结尾处或中间位置。对于Pascal REPEAT节点来说,TEST节点是LOOP节点的最后一个子节点,因此退出测试在循环的末尾。
清单7-5:RepeatStatementParser中的parse()方法
1: public ICodeNode parse(Token token)
2: throws Exception
3: {
4: //吞噬掉语句开始的REPEAT
5: token = nextToken();
6:
7: ICodeNode loopNode = ICodeFactory.createICodeNode(LOOP);
8: ICodeNode testNode = ICodeFactory.createICodeNode(TEST);
9:
10: //解析包含在REPEAT和UNTIL中间的语句列表
11: StatementParser statementParser = new StatementParser(this);
12: statementParser.parseList(token, loopNode, UNTIL, MISSING_UNTIL);
13: token = currentToken();
14:
15: //最后的测试表达式
16: ExpressionParser expressionParser = new ExpressionParser(this);
17: testNode.addChild(expressionParser.parse(token));
18: loopNode.addChild(testNode);//最后一个子节点
19:
20: return loopNode;
21: }
此parse()方法创建LOOP和TEST节点,构建如图7-3 展示的分析树。这个方法还调用statementParser.parseList()去解析包含在BEGIN和UTIL之间的语句。LOOP节点成为语句子树的父节点。
在Eclipse中运行Pascal,使用参数"compile -i repeat.txt",查看正确的输出结果。使用参数"compile -i repeaterrors.txt",查看带错误的输出结果。这里省略输出结果。
解析WHILE语句
这个语句解析器子类WhileStatementParser解析一个Pascal WHILE语句并产生它的分析树。比如语句:
对于一个Pascal WHILE语句来说,LOOP节点的第一个孩子是TEST节点,第二个孩子是嵌套的语句子树。因此在运行时退出测试发生在循环的开始处。因为WHILE循环在测试表示值为false时退出(与REPEAT相反),关系表达式子树的父节点是一个生成的NOT节点(而不是TEST节点)。
清单7-8 展示了WhileStatementParser的parse()方法
1: //DO同步集合
2: private static final EnumSet<PascalTokenType> DO_SET =
3: StatementParser.STMT_START_SET.clone();
4: static {
5: DO_SET.add(DO);
6: DO_SET.addAll(StatementParser.STMT_FOLLOW_SET);
7: }
8: public ICodeNode parse(Token token)
9: throws Exception
10: {
11: //干掉语句开始的WHILE
12: token = nextToken();
13: ICodeNode loopNode = ICodeFactory.createICodeNode(LOOP);
14: ICodeNode breakNode = ICodeFactory.createICodeNode(TEST);
15: //这个父节点肯定是breakNode,参见图7-4
16: ICodeNode notNode = ICodeFactory.createICodeNode(ICodeNodeTypeImpl.NOT);
17: loopNode.addChild(breakNode);
18: breakNode.addChild(notNode);
19: // 退出测试表达式
20: ExpressionParser expressionParser = new ExpressionParser(this);
21: notNode.addChild(expressionParser.parse(token));
22: // 表达式之后的DO或其它,比如 while a>b;
23: token = synchronize(DO_SET);
24: if (token.getType() == DO) {
25: token = nextToken();
26: }
27: else {
28: errorHandler.flag(token, MISSING_DO, this);
29: }
30: //循环语句主体,只有一条语句
31: StatementParser statementParser = new StatementParser(this);
32: loopNode.addChild(statementParser.parse(token));
33: return loopNode;
34: }
parse()方法创建LOOP,TEST和NOT节点并构建图7-4的分析树。它用同步集合DO_SET同步自己到DO token处。
在Eclipse中运行Pascal,使用参数"compile -i while.txt",查看正确的输出结果。使用参数"compile -i whileerrors.txt",查看带错误的输出结果。这里省略输出结果。
>>> 继续第七章