BST

BST Community Official Blog
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

第一部分

设计笔记

在提取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
图5-8:包含赋值语句的复合语句产生的分析树。
image如果行位置是从第11行到14行,分析树的XML表示为:
 
<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>
表达式分析器生成了INTEGER_CONSTANT节点。
 

解析表达式

分析器子类ExpressionParser内容很多,但如UML图5-7指明的那样,它的方法紧密贴近语法图5-2的语法规则。
清单5-18 展示了方法parse(),它只是简单的调用了parseExpression并返回生成的表达式子树根节点。请参考源代码第71行。

parseExpression()方法

清单5-19 展示了parseExpression()方法,它用来解析表达式。对应语法图(图5-2),一个表达式包含一个简单表达式,还有可能跟着一个比较操作符(> < 之类)和另一个简单 表达式。此方法使用静态枚举集合REL_OPS包含Pascal关系操作符token,使用哈希表REL_OPS_MAP映射每个操作符token到对应的分析树节点类型。它返回生成表达式子树跟节点。
清单5-19:ExpressionParser类的parseExpression()方法。详细参见本章源代码第77行到125行,这里不再显示。
 
方法parseSimpleExpression()
方法parseSimpleExpression()解析简单表达式,按照语法图规范,此表达式包含一个term,紧接着0个或多个被加法类运算法分隔的term。第一个term可以前置一个加号+或减号-。如果有减号,此方法创建一个NEGATE节点。这个方法使用静态枚举结合ADD_OPS包含Pascal加法类运算符,使用哈希表ADD_OPS_OPS_MAP映射每个操作符token到对应的分析树节点类型。它返回生成的表达式子树根节点。见清单5-10
清单5-10:ExpressionParser中的parseSimpleExpression()方法。详细参见本章源代码第127行到186行,这里不再显示。
每趟while循环会寻找加法类运算符,它使用ADD_OPS_OPS_MAP根据找到的运算符去创建对应类型的树节点。循环在没有加法类操作符时退出。每个后续的操作符接纳当前rootNode为第一个孩子。换句话说,表达式子树构建目前是“下压”到左边成为新操作符的第一个操作数(表达式是二叉树的中序遍历形成的串,那么创建的分析子树就是这个二叉树)。图5-9 展示了简单表达式 a+b+c+d+e 的分析树创建过程。
image
当最终的分析树被执行后,简单表达式的操作数按顺序被求值(你将会在下一章见到此分析树的后序遍历)。这遵循前面描述的优先级规则那就是同一级的操作符执行时是从左到右。最终的表达式分析树的XML表现形式为:
<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>
 
方法parseTerm()
parseTerm()方法类似于parseSimpleExpression()方法。它解析term,它的语法图规定term是一个factor紧接着0个或多个被乘法类运算法隔开的factor。此方法使用静态枚举集合MULT_OPS包含Pascal乘法类运算符,使用MULT_OPS_MAP将每个运算符token映射到对应分析树节点类型。它返回生成的term子树根节点。见清单5-21
清单5-21:Expression类中的parseTerm()方法。详细参见本章源代码第188行到229行,这里不再显示。

方法parseFactor()
parseFactor方法,内容如清单5-22显示,用来解析factor。遵照factor的语法图,它可以是一个变量,一个数字,一个字符串,NOT紧接另一个factor,或圆括号括起来的表达式。
清单5-22:ExpressionParser中的parseFactor方法详细参见本章源代码第188行到229行,这里不再显示。
  • 如果当前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

添加如下语句到Pascal类的构造函数中,详细参见源代码第68行:
if (intermediate){
ParseTreePrinter tree_printer = new ParseTreePrinter(System.out);
tree_printer.print(iCode);
}
以上代码使用工具类ParseTreePrinter以XML格式打印分析树,正如你在清单5-11中看到的那样。
因为你还没有在后端做任何改动,后端的编译器/解释器除了打印摘要信息外没有任何东西。基本上你写的是一个语法检查工具,解析源程序,建立符号表,生成分析树。在下一章中,你将会开发后端解释器中的执行器(executor)来执行生成的分析树。
假定类文件在class目录(eclipse中在bin目录)且根目录包含源文件assignments.txt,用类似命令行"java -classpath classes Pascal execute -i assignments.txt"(建议在Eclipse直接执行,什么命令行,降低效率)将会解析assignments.txt文件中的"program"。"-i"命令行参数明确打印中间码,如果没语法错误,打印输出将是整个分析生成树的XML呈现。清单5-23 展示了这种输出。
清单5-23:没有语法错误时的输出列表(请在Eclipse上运行程序观看完整输出,下面为样例)
----------代码解析统计信息--------------
源文件共有 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秒
清单5-24:带语法错误时的输出列表,将参数中的assignments.txt改成assignerrors.txt。运行带语法错误的文件是,将原来注释掉的"source.addMessageListener(new SourceMessageListener());",方便指示出错位置。(Pascal文件第50行)(请在Eclipse上运行程序观看完整输出,下面为样例)
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秒