在上一章,你已为复合语句、赋值语句和表达式开发出分析树格式的中间码。在这章,你将为执行这些语句和表达式在后端编写代码去解释生成的分析树。
==>> 本章中文版源代码下载:svn co http://wci.googlecode.com/svn/branches/ch6/ 源代码使用了UTF-8编码,下载到本地请修改!
方法和目标
本章从上一章结束的地方开始,它有一个主要目的:
- 后端的语言无关的执行器(Executor)将会解释中间码以便执行表达式、复合语句以及赋值语句。
上一章中,你开发了前端的Pascal解析器子类StatementParser和孙子类CompoundStatementParser、AssignmentStatementParser以及ExpressionParser,用来解析Pascal语句和表达式。本章的解释器后端开发执行器子类将会采用同样模式。然而这些执行器之类将会是语言无关的(只与分析树有关)。本章末,为验证你的开发工作,有个简单的解释器实用程序将读取和执行Pascal赋值语句和表达式。
运行时错误处理
frontend.pascal包中的Pascal相关的解析程序实用了错误处理类PascalErrorHandler和枚举类型PascalErrorCode。类似地,backend.interpreter包中的语言无关解释器使用运行时错误处理类RuntimeErrorHandler和枚举类型RuntimeErrorCode。
清单6-1 展示了包backend.interpreter中RuntimeErrorHandler类的flag方法。详细参见本章源代码,这里不再显示。任何时候解释器检测到一个运行时错误就把它发给后端的所有监听器。我们约定此错误消息格式为:
- errorCode.toString():运行时错误消息
- node.getAttribute(LINE):源代码行位置
方法flag()创建这些消息(错误消息)。它顺着传递的当前分析子树节点往上搜寻最近的带"LINE"(行)属性节点。这个行将会是造成此次运行时错误的源语句。
清单6-2 展示了枚举类型RuntimeErrorCode。枚举值表示解释器能够检测到的运行时错误。详细参见本章源代码,这里不再显示。
执行赋值语句和表达式
在第五章已解析过Pascal表达式、赋值语句和复合语句并生成语言无关的分析树格式的中间码。为执行这些语句和表达式,马上你将会编写后端语言无关类实现用于解释这些分析树。
语句执行器子类
图6-1 展示了backend.interpreter.executors包中执行器的UML类图。你在第二章就见过的类Executor,现在是类StatementExecutor的基类,而StatementExecutor又是CompoundExecutor和ExpressionExecutor的基类。比较一下这幅图和图5-7。
设计笔记 |
设计良好的软件时常显示出结构对称。第二章的图2-3展示了解析器和后端框架类对于中间码和符号表等组件的对称性。图5-7和图6-1 展示了PascalParserTD及其它的子类和类Executor及其子类的对称性。 |
类StatementExecutor和每个子类都有一个execute()方法专门用来以解释某种具体语言结构分析树的方式执行此语言相关结构。除了类ExpressionExecutor的返回值为表达式的计算值,它总是返回null值。因为中间码和符号表都与具体语言无关,自然StatementExecutor及其子类也是语言无关的。(因为在本书中你将只解释Pascal程序,你的Executor子类具有Pascal倾向性。然而后端要达到的其余目标是能解释不只一门源语言)
清单6-3 展示了Executor类的新构造函数和新版本的process()方法。
public class Executor extends Backend
{
//执行的语句数
protected static int executionCount = 0;
//错误处理类
protected static RuntimeErrorHandler errorHandler = new RuntimeErrorHandler();
//本章的parent继承主要是共享此Executor的两个static域,真无聊的对称!
public Executor(Executor parent){
super();
}
public void process(ICode iCode, SymTabStack stack)
throws Exception
{
this.iCode = iCode;
this.symTabStack = stack;
long startTime = System.currentTimeMillis();
ICodeNode root_node = this.iCode.getRoot();
StatementExecutor root_executor = new StatementExecutor(this);
root_executor.execute(root_node);
float elapsedTime = (System.currentTimeMillis() - startTime)/1000f;
int runtimeErrors = errorHandler.getErrorCount();
// 发送解释摘要消息省略...
}
}
所有Executor的子类实例通过新构造函数。process()方法取得前端解析器生成的中间码的根节点并调用statementExecutor.execute()执行(相当于从根节点往下遍历,跟第5A章的Antlr一样)。
执行语句
清单6-4 展示了执行器子类StatementExecutor的构造函数和execute()方法。一同展示的还有它的私有sendSourceLineMessage()方法。详细参见本章源代码,这里不再显示。
execute()方法首先调用sendSourceLineMessage()发送一条SOURCE_LINE消息给语句执行器的监听器。我们约定SOURCE_LINE消息的格式如下:
- node.getAtrribute(LINE):源代码行位置
此消息指明将被执行语句的源代码行位置,此位置对于调试器实现跟踪可能非常有用。目前前端将忽略这消息。
消息发送后,execute()方法视传入的节点是否COMPOUND,ASSIGN或NO_OP类型,分别调用compoundExecutor.execute(),assignmentExecutor.execute()或什么也不干,如果节点是其它类型,方法标记一个UNIMPLEMENTED_FAILURE运行时错误。后面章节在你要执行更多其它类型语句的时候,此方法将会被修改。
执行复合语句
清单6-5 展示了子类CompoundExecutor的execute()方法。此方法遍历COMPOUND节点的每一个子节点并调用相应的statementExecutor.execute()。
1: public Object execute(ICodeNode node)
2: {
3: // 遍历每个子节点并执行之
4: StatementExecutor statementExecutor = new StatementExecutor(this);
5: List<ICodeNode> children = node.getChildren();
6: for (ICodeNode child : children) {
7: statementExecutor.execute(child);
8: }
9: return null;
10: }
执行赋值语句
清单6-6 展示了执行器子类AssignmentExecutor的execute()和sendMessage()方法。详细参见本章源代码,这里不再显示。execute()方法取得ASSIGN节点的两个子节点,第一个是赋值语句的目标变量VARIABLE节点,第二个是表达式子树的根节点,它调用方法expressionExecutor.execute()取得表达式的计算值,并把这个值设置为目标变量符号表项的DATA_VALUE属性值。execute()方法调用sendMessage()发送赋值消息给执行器的监听器。
我们约定的赋值消息格式如下:
- node.getAttribute(LINE):源行位置
- variableName:目标变量名
- value:表达式的值
此消息对于调试和跟踪很有用。
执行表达式
清单6-7 展示了执行器子类ExepressionExecutor的execute()方法。
1: //计算表达式的值
2: public Object execute(ICodeNode node)
3: {
4: ICodeNodeTypeImpl nodeType = (ICodeNodeTypeImpl) node.getType();
5: switch (nodeType) {
6: case VARIABLE: {
7: SymTabEntry entry = (SymTabEntry) node.getAttribute(ID);
8: return entry.getAttribute(DATA_VALUE);
9: }
10: //三中常量节点,直接返回值
11: case INTEGER_CONSTANT: {
12: return (Integer) node.getAttribute(VALUE);
13: }
14: case REAL_CONSTANT: {
15: return (Float) node.getAttribute(VALUE);
16: }
17: case STRING_CONSTANT: {
18: return (String) node.getAttribute(VALUE);
19: }
20: case NEGATE: {//“负”节点只有一个子节点且其值为数值
21: List<ICodeNode> children = node.getChildren();
22: ICodeNode expressionNode = children.get(0);
23: Object value = execute(expressionNode);
24: if (value instanceof Integer) {
25: return -((Integer) value);
26: }
27: else {
28: return -((Float) value);
29: }
30: }
31: case NOT: {//“非"节点也只有一个节点且其值为布尔值
32: List<ICodeNode> children = node.getChildren();
33: ICodeNode expressionNode = children.get(0);
34: boolean value = (Boolean) execute(expressionNode);
35: return !value;
36: }
37: //二元操作符比如加+减-乘*除/之类
38: default: return executeBinaryOperator(node, nodeType);
39: }
40: }
操作符优先级
语法图5-2包含了Pascal的操作符优先级规则。前端类ExpressionParser构建表达式子树反映这些语法图示(我们看语法图,上面的规则对应的是分析树的深度较小的节点,下面的规则对应的深度较大的节点),因此如果表达式执行器以合适的顺序计算树节点的值,那么程序执行过程中的优先级规则就水到渠成了。(比如按照中序/后序遍历,总是会先遍历深度较大的节点,也就是下面一点的规则,即优先级高的规则,那么遍历下来,自然是从优先级高到优先级低)
一个表达式子树是一个二叉树,表达式执行器必须后序遍历(你还记得前序/中序/后序遍历二叉树么?不记得找度娘)这些节点去计算它们。如果子树的根是一个操作符节点(比如+/-之类),执行器首先计算左节点的值作为第一个操作数;如果此节点有一个右孩子,执行器计算右子节点的值作为第二个操作数。最后执行器分析操作符节点并针对一个或两个操作数做具体的运算。如果任一子节点是某一子树的根节点,执行器递归的对这个子树做一个后续遍历。
图6-2 展示了如下表达式的分析树。节点上的数字是遍历的顺序。
alpha + 3/(beta - gamma) + 5
如果节点是一个二元操作符节点,execute()方法调用executeBinaryOperator()方法并返回计算后的值。参见清单6-8。executeBinaryOperator()方法使用静态的枚举集合ARITH_OPS,此集合包含表示算术运算符的所有节点类型。这个方法首先递归的调用execute(),计算此节点的两个孩子节点并将值作为第一个和第二个操作数。它设置局部变量integerMode为true当且仅当两个操作数都是整数。
整数算术运算
浮点算术运算
与和或运算
设计笔记 |
类ExpressionExecutor不做语言相关的类型检测。它依赖前端语言相关的解析器做类型检查并构建一个有效的分析树。这个类同样不必关心语言相关的操作符优先级规则。还有,它依赖前端构建合适的分析树,通过后序遍历树的方式执行。 |
程序6:简单解释器 I
1: case ASSIGN: {
2: if (firstOutputMessage) {
3: System.out.println("\n===== 运行时输出 =====\n");
4: firstOutputMessage = false;
5: }
6:
7: Object body[] = (Object[]) message.getBody();
8: int lineNumber = (Integer) body[0];
9: String variableName = (String) body[1];
10: Object value = body[2];
11:
12: System.out.printf(ASSIGN_FORMAT,
13: lineNumber, variableName, value);
14: break;
15: }
16:
17: case RUNTIME_ERROR: {
18: Object body[] = (Object []) message.getBody();
19: String errorMessage = (String) body[0];
20: Integer lineNumber = (Integer) body[1];
21:
22: System.out.print("*** 运行时错误");
23: if (lineNumber != null) {
24: System.out.printf(" 在第 %03d行", lineNumber);
25: }
26: System.out.println(": " + errorMessage);
27: break;
28: }
29: se INTERPRETER_SUMMARY: {
30: Number body[] = (Number[]) message.getBody();
31: //省略...
private static final String ASSIGN_FORMAT = ">>> 行 %03d处: %s = %s\n";
----------代码解析统计信息--------------
源文件共有 27行。
有 0个语法错误.
解析共耗费 0.02秒.
===== 运行时输出 =====
>>> 行 003处: five = 5
>>> 行 004处: ratio = 0.5555556
>>> 行 006处: fahrenheit = 72
>>> 行 007处: centigrade = 22.222223
>>> 行 009处: centigrade = 25
>>> 行 010处: fahrenheit = 77.0
>>> 行 012处: centigrade = 25
>>> 行 013处: fahrenheit = 77.0
*** 运行时错误 在第 017行: 除零操作
>>> 行 017处: dze = 0.0
>>> 行 020处: number = 2
>>> 行 021处: root = 2
>>> 行 022处: root = 1.5
>>> 行 025处: ch = x
>>> 行 026处: str = hello, world
----------解释统计信息------------
共执行 14 条语句。
运行中发生了 1 个错误。
执行共耗费 0.01 秒。
设计笔记 第6章的小伎俩(Hacks) |
这章依赖几个临时的“伎俩”(hacks),使得你的初级解释器运行的起来:
随着开发的深入,你将逐渐用实现取代这些小伎俩。 |