续 第一部分
设计笔记 |
在提取token和解析结构之间有一种对称美。扫描器用token的第一个字符来判断接下来用何种类型去提取token。在token被提取后,当前字符是token的尾字符后的第一个字符(比如xy = bc,在提取到特殊符号token "="之后,当前字符指向'b')。同样,解析器用Pascal结构的第一个token(比如复合语句的BEGIN)来判断解析何种结构类型(复合语句还是复制语句?,还是其它类型?)。在这个结构被解析后,当前的Token是结构最后一个token的下一个token。(这种对称性解释了为什么Antlr对Lexer和Parser采用一样的LL(*)解析方式) |
解析语句
清单5-14 展示了frontend.pascal.parsers包中Pascal解析器子类StatementParser中的parse()和setLineNumber()方法。其中构造器如上所述获取解析上下文(获得父解析器的scanner,也就是解析的上下文)。
1: /**
2: * 解析一个语句,以传入的token判断该进行何种解析,子类会覆盖此方法
3: * @param token 语句的第一个token
4: * @return 分析子树的根节点
5: * @throws Exception
6: */
7: public ICodeNode parse(Token token)
8: throws Exception
9: {
10: ICodeNode statementNode = null;
11:
12: switch ((PascalTokenType) token.getType()) {
13:
14: //复合语句总是以begin开头,如"begin xxxx end."
15: case BEGIN: {
16: CompoundStatementParser compoundParser =
17: new CompoundStatementParser(this);
18: statementNode = compoundParser.parse(token);
19: break;
20: }
21:
22: //类似于 a = b 之类的赋值语句,这里a就是标识符identifier
23: case IDENTIFIER: {
24: AssignmentStatementParser assignmentParser =
25: new AssignmentStatementParser(this);
26: statementNode = assignmentParser.parse(token);
27: break;
28: }
29: default: {
30: statementNode = ICodeFactory.createICodeNode(NO_OP);
31: break;
32: }
33: }
34:
35: // 设置根节点“行位置”属性,即第一个token的行位置
36: setLineNumber(statementNode, token);
37: return statementNode;
38: }
39:
40: /**
41: * 设置节点的行属性。PS: 我认为这种写法很怪,为何不setLineNumber(ICodeNode, int)?
42: * @param node 分析树节点
43: * @param token token
44: */
45: protected void setLineNumber(ICodeNode node, Token token)
46: {
47: if (node != null) {
48: node.setAttribute(LINE, token.getLineNumber());
49: }
50: }
parse()方法检查传入的token,它必定是下一语句的第一个token。方法用这个token判断给解析何种语句。本章中只有两种Pascal语句要解析:复合语句和赋值语句。因此,如果此token是BEGIN,方法就调用compoundParser.parse();如果是一个标识符,方法调用assignmentParser.parse();如果是其它token,方法调用中间码工厂创建一个NO_OP节点。
方法compoundParser.parse()和assignmentParser.parse()分别解析相应的Pascal语句,产生分析子树,并返回子树的根节点。方法parse()调用setLineNumber()为生成的语句子树根节点设置行位置属性,接着返回根节点。你将在第7章扩展parse()方法处理其它类型Pascal语句。
清单5-15 展示了受保护的方法parseList(),它解析一个语句列表(statement list)。在每趟while()循环中,parentNode参数将递归调用产生的语句子树(见11行)作为子节点。每条语句解析过后,循环寻找语句间的分号token,如有必要标记一个MISSING_SEMICOLON错误。循环碰到结束terminator(是参数名)token终止(比如END),方法将terminator token吞噬。如果没有terminator token就会标记一个语法错误(使用错误码)。
清单5-15:类StatementParser中的parseList()方法
1: protected void parseList(Token token, ICodeNode parentNode,
2: PascalTokenType terminator,
3: PascalErrorCode errorCode)
4: throws Exception
5: {
6: //遍历每条语句直到遇见结束TOKEN类型或文件结束
7: while (!(token instanceof EofToken) &&
8: (token.getType() != terminator)) {
9:
10: // 解析一条语句
11: ICodeNode statementNode = parse(token);
12: //语句子树根节点作为子节点附加到父节点上
13: parentNode.addChild(statementNode);
14:
15: token = currentToken();
16: TokenType tokenType = token.getType();
17:
18: //每条语句之后肯定是一个分号 ; 否则报错
19: if (tokenType == SEMICOLON) {
20: token = nextToken();
21: }else if (tokenType == IDENTIFIER) { //遗漏分号
22: errorHandler.flag(token, MISSING_SEMICOLON, this);
23: }else if (tokenType != terminator) {//除非碰到结束token了
24: errorHandler.flag(token, UNEXPECTED_TOKEN, this);
25: token = nextToken();
26: }
27: }
28:
29: //判断是否已到结束token,如果是就跳过它,否则报错
30: if (token.getType() == terminator) {
31: token = nextToken(); // consume the terminator token
32: }else {
33: errorHandler.flag(token, errorCode, this);
34: }
35: }
解析复合语句
清单5-16 展示了语句解析器子类CompoundStatementParser的parse()方法
1: public ICodeNode parse(Token token)
2: throws Exception
3: {
4: token = nextToken(); //吞噬掉BEGIN
5:
6: // 创建复合语句节点节点
7: ICodeNode compoundNode = ICodeFactory.createICodeNode(COMPOUND);
8:
9: //解析BEGIN ... END中的语句列表
10: StatementParser statementParser = new StatementParser(this);
11: statementParser.parseList(token, compoundNode, END, MISSING_END);
12:
13: return compoundNode;
14: }
此方法创建一个COMPOUND节点,后面它将作为生成子树的根节点返回。如同前面图5-4展示的那样,一个COMPOUND节点可包含任意有序子节点,每个子节点都是复合语句中每条语句产生的分子子树。方法调用statementParser.parseList()解析语句列表,传一个END 作为类表结束token类型,还有一个MISSING_END错误码。
解析赋值语句
清单5-17 展示了语句解析器子类AssignmentStatementParser的parse()方法。如图5-4所示,一个ASSIGN节点有两个孩子。第一个孩子是一个VARIABLE节点表示赋值token := 左边的目标变量(即存入变量)。第二个孩子是右边的表达式分析子树。
1: public ICodeNode parse(Token token)
2: throws Exception
3: {
4: ICodeNode assignNode = ICodeFactory.createICodeNode(ASSIGN);
5: //在符号表堆栈中查找此变量的项,如果到不到就在局部表建个新项。
6: //所有表项的名都是小写
7: String targetName = token.getText().toLowerCase();
8: SymTabEntry targetId = symTabStack.lookup(targetName);
9: if (targetId == null) {
10: targetId = symTabStack.enterLocal(targetName);
11: }
12: targetId.appendLineNumber(token.getLineNumber());
13: //跳过ID
14: token = nextToken();
15: //变量ID作为赋值节点的第一个孩子
16: ICodeNode variableNode = ICodeFactory.createICodeNode(VARIABLE);
17: variableNode.setAttribute(ID, targetId);
18: assignNode.addChild(variableNode);
19: // 找不到赋值:=就报错,找到就吞噬
20: if (token.getType() == COLON_EQUALS) {
21: token = nextToken();
22: }
23: else {
24: errorHandler.flag(token, MISSING_COLON_EQUALS, this);
25: }
26: //解析赋值语句右边的表达式,将其子树作为赋值节点的第二个孩子
27: ExpressionParser expressionParser = new ExpressionParser(this);
28: assignNode.addChild(expressionParser.parse(token));
29: return assignNode;
30: }
因为在第9章之前你不会涉及到解析变量申明,本章就做了几个主要简化。每个变量就是一个表示符,没有数字小标,没有记录域(record field)。在变量首次出现在赋值语句的左边时,就认为它申明了(变量在使用前需要申明的,因为有类型,这儿简化了)并加它到符号表中。
方法parse()创建一个ASSIGN节点。它搜索局部符号表,判断传入token对应的标识符是否存在,必要时创建一个。它创建一个VARIABLE节点并设置属性ID为标识符的符号表项。ASSIGN节点接受VARIABLE节点作为第一个孩子。方法接着查找并吞噬赋值符:=,或表示一个MISSING_COLON_EQUALS错误。方法 expressionParser.parse()解析右边的表达式并返回生成的表达式子树根节点,作为ASSIGN节点的第二个孩子。最后,ASSIGN节点被作为整个赋值语句分析树根节点被返回。
图5-8 展示了如下复合语句产生的分析树
BEGIN
alpha := 10;
beta := 20
END
<COMPOUND line="11">
<ASSIGN line="12">
<VARIABLE id="alpha" level="0" />
<INTEGER_CONSTANT value="10" />
</ASSIGN>
<ASSIGN line="13">
<VARIABLE id="beta" level="0" />
<INTEGER_CONSTANT value="20" />
</ASSIGN>
</COMPOUND>
解析表达式
parseExpression()方法
<ADD>
<ADD>
<ADD>
<ADD>
<VARIABLE id="a" level="0" />
<VARIABLE id="b" level="0" />
</ADD>
<VARIABLE id="c" level="0" />
</ADD>
<VARIABLE id="d" level="0" />
</ADD>
<VARIABLE id="e" level="0" />
</ADD>
方法parseFactor()
- 如果当前token是标识符(ID):方法首先在局部符号表中寻找此变量名。如果没有(本章的意思是赋值左边的变量从来没出现过)则标记一个IDENTIFIER_UNDEFINED错误并把它放入符号表。此方法另外还创建一个VARIABLE节点并设置ID属性值为变量对应的符号表项。后面章节将会讲述更多关于解析表达式标识符的内容。
- 如果当前token是数字:方法根据数值类型创建一个INTEGER_CONSTANT节点或REAL_CONSTANT节点。此方法设置VALUE属性为数值的值,值是一个Integer或Float对象。
- 如果当前token是字符串:方法创建一个STRING_CONSTANT节点并设VALUE属性为字符串的字面文本。
- 如果当前token是NOT:方法parseFactor()创建一个NOT节点,并递归调用它本身。NOT节点接纳递归的那个factor分析子树为仅有的孩子节点。
- 如果当前token是(:方法吞噬掉括号 ( 并递归调用parseExpression()方法去解析括号里面的表达式,然后查找并吞噬掉反括号 )或标记一个MISSING_RIGHT_PAREN错误。
- 如果当前token是其它token:方法parseFactor()标记一个UNEXPECTED_TOKEN错误并返回null。
最后,方法返回factor生成子树的根节点。
设计笔记 |
现在应该明白为何这种解析方式叫做top-down(至上而下,实际上从左到右)。解析器从最高层的结构(此时它是复合语句)开始,顺着这种方式下溯到最底下的结构。我们的top-down解析器也称“递归下降”解析器。因为语句和表达式能互相嵌套,解析方法顺着下溯方式互相递归调用(即语句递归表达式,表达式递归语句,直到结构的最下层Factor)。 |
程序5:Pascal 语法检查器 I
if (intermediate){
ParseTreePrinter tree_printer = new ParseTreePrinter(System.out);
tree_printer.print(iCode);
}
----------代码解析统计信息--------------
源文件共有 27行。
有 0个语法错误.
解析共耗费 0.02秒.
======== 中间码 ========
<COMPOUND line="1">
<COMPOUND line="2">
<ASSIGN line="3">
<VARIABLE id="five" level="0" />
<!--此处省略N行 -->
<ASSIGN line="25">
<VARIABLE id="ch" level="0" />
<STRING_CONSTANT value="x" />
</ASSIGN>
<ASSIGN line="26">
<VARIABLE id="str" level="0" />
<STRING_CONSTANT value="hello, world" />
</ASSIGN>
</COMPOUND>
----------编译统计信--------------
共生成 0 条指令
代码生成共耗费 0.00秒
001 BEGIN
002 BEGIN {Temperature conversions.}
003 five := -1 + 2 - 3 + 4 - -3;
^
*** 意外的token [在 "-" 处]
004 ratio := five/9.0;
005
006 fahrenheit := 72;
007 centigrade := (((fahrenheit - 32)))*ratio;
008
009 centigrade := 25;;;
010 fahrenheit := centigrade/ratio + 32;
011
012 centigrade := 25
013 fahrenheit := 32 + centigrade/ratio;
^
*** 缺失分号 ; [在 "fahrenheit" 处]
014
015 centigrade := 25;
016 fahrenheit := celsius/ratio + 32
^
*** 未定义identifier [在 "celsius" 处]
017 END
018
019 dze fahrenheit/((ratio - ratio) := ;
^
*** 缺失分号 ; [在 "dze" 处]
^
*** 缺失赋值符 := [在 "fahrenheit" 处]
^
*** 缺失反括号 ) [在 ":=" 处]
^
*** 意外的token [在 ":=" 处]
020
021 END.
----------代码解析统计信息--------------
源文件共有 21行。
有 7个语法错误.
解析共耗费 0.05秒.
======== 中间码 ========
<COMPOUND line="1">
<COMPOUND line="2">
<ASSIGN line="3">
<VARIABLE id="five" level="0" />
<SUBTRACT>
<!-- 此处省略N字-->
</ASSIGN>
<NO_OP line="19" />
</COMPOUND>
----------编译统计信--------------
共生成 0 条指令
代码生成共耗费 0.00秒