(基于Java)编写编译器和解释器-第8章:解释Pascal控制语句(连载)

时间:2021-09-18 14:49:39

在这章中,你将会继续完善第6章的工作,为第7章解析的Pascal控制语句开发对应的后端执行器

==>> 本章中文版源代码下载:svn co http://wci.googlecode.com/svn/branches/ch8/ 源代码使用了UTF-8编码,下载到本地请修改!

目标和方法

本章有一个主要目标:

  • 开发后端的语言无关的执行器(executors),可用来解释中间码,执行控制语句逻辑。

这章使用的方法是开发更多的Executor子类。为验证你的代码,你将继续沿用第6章做好的简单解释程序。

程序 8:简单解释器II

第6,7章编写的主程序Pascal类本章还是不需要做任何改动,当它是个解释器再次运行这个类。程序解释能力的完整性要求我们得为控制语句增加更多新的执行器子类。你将针对不同Pascal控制语句把这个程序运行好几次,以便验证这些新子类是否能正确执行这些控制语句。

解释控制语句

下面的UML类图8-1是图6-1的一个补充。比较图7-2,看看新增执行器的关系。

(基于Java)编写编译器和解释器-第8章:解释Pascal控制语句(连载)

执行器子类StatementExecutor依赖它的子类,而它的每个子类同样也依赖StatementExecutor(因为控制语句是语句,但是每个控制语句除了条件之外还有要控制的语句)。子类LoopExecutor,IfExecutor以及SelectExector还依赖ExpressionExecutor。

清单8-1 展示了StatementExecutor中更新过的execute()方法,它现在能够处理LOOP,IF以及 SELECT节点。(留意15到27行)

   1: public Object execute(ICodeNode node)
   2:  {
   3:      ICodeNodeTypeImpl nodeType = (ICodeNodeTypeImpl) node.getType();
   4:      sendSourceLineMessage(node);
   5:      switch (nodeType) {
   6:          case COMPOUND: {
   7:              CompoundExecutor compoundExecutor = new CompoundExecutor(this);
   8:              return compoundExecutor.execute(node);
   9:          }
  10:          case ASSIGN: {
  11:              AssignmentExecutor assignmentExecutor =
  12:                  new AssignmentExecutor(this);
  13:              return assignmentExecutor.execute(node);
  14:          }
  15:          case LOOP: {
  16:              LoopExecutor loopExecutor = new LoopExecutor(this);
  17:              return loopExecutor.execute(node);
  18:          }
  19:  
  20:          case IF: {
  21:              IfExecutor ifExecutor = new IfExecutor(this);
  22:              return ifExecutor.execute(node);
  23:          }
  24:  
  25:          case SELECT: {
  26:              SelectExecutor selectExecutor = new SelectExecutor(this);
  27:              return selectExecutor.execute(node);
  28:          }
  29:          case NO_OP: return null;
  30:          default: {
  31:              errorHandler.flag(node, UNIMPLEMENTED_FEATURE, this);
  32:              return null;
  33:          }
  34:      }
  35:  }

  执行一个循环语句

清单8-2 展示了语句解析器子类LoopExecutor的execute()方法,它执行LOOP分析树。

 

   1: public Object execute(ICodeNode node)
   2: {
   3:     boolean exitLoop = false;
   4:     ICodeNode exprNode = null;
   5:     List<ICodeNode> loopChildren = node.getChildren();
   6:     //循环判别式
   7:     ExpressionExecutor expressionExecutor = new ExpressionExecutor(this);
   8:     //循环主体
   9:     StatementExecutor statementExecutor = new StatementExecutor(this);
  10:     //循环直到判别式估值为真即推出
  11:     while (!exitLoop) {
  12:         ++executionCount; //循环语句也算计数
  13:         for (ICodeNode child : loopChildren) {
  14:             ICodeNodeTypeImpl childType =
  15:                                   (ICodeNodeTypeImpl) child.getType();
  16:             //是否判别式节点?是就更新一下退出值
  17:             if (childType == TEST) {
  18:                 if (exprNode == null) {
  19:                     exprNode = child.getChildren().get(0);
  20:                 }
  21:                 exitLoop = (Boolean) expressionExecutor.execute(exprNode);
  22:             }
  23:             else {
  24:                 statementExecutor.execute(child);
  25:             }
  26:             if (exitLoop) {
  27:                 break;
  28:             }
  29:         }
  30:     }
  31:     return null;
  32: }

此execute()方法的while循环不断的解释LOOP节点的子节点。里层的for循环执行每一个是语句子树的子节点。子节点中有一个可能是TEST节点,它的孩子节点是一个关系表达式子树。这个方法计算TEST的表达式,如果表达式值为真,整个循环就退出。

清单8-3 展示了在Eclipse中运行Pascal,使用参数"execute repeat.txt"的输出结果。你可以在Eclipse控制台看到完整输出,这里省略输出结果

清单8-4 展示了在Eclipse中运行Pascal,使用参数"execute while.txt"的输出结果。你可以在Eclipse控制台看到完整输出,这里省略输出结果

清单8-5 展示了在Eclipse中运行Pascal,使用参数"execute for.txt"的输出结果。你可以在Eclipse控制台看到完整输出,这里省略输出结果

设计笔记

在上一章设计笔记中提到过,中间码以一种语言无关的方式用LOOP节点适应各种不同风格的循环结构(FOR/REPEAT/WHILE)。这使得执行器子类LoopExecutor能够以单一执行器执行Pascal REPEAT、WHILE和FOR循环。

  执行IF语句

清单8-6 展示了语句执行器子类IfExecutor的execute()方法,它执行IF分析树。

   1: public Object execute(ICodeNode node)
   2: {
   3:     //记住IF有两个或者三个节点
   4:     List<ICodeNode> children = node.getChildren();
   5:     ICodeNode exprNode = children.get(0);
   6:     ICodeNode thenStmtNode = children.get(1);
   7:     ICodeNode elseStmtNode = children.size() > 2 ? children.get(2) : null;
   8:     //判别式和语句主体
   9:     ExpressionExecutor expressionExecutor = new ExpressionExecutor(this);
  10:     StatementExecutor statementExecutor = new StatementExecutor(this);
  11:     //算一下IF表达式
  12:     boolean b = (Boolean) expressionExecutor.execute(exprNode);
  13:     if (b) {
  14:         statementExecutor.execute(thenStmtNode);
  15:     }
  16:     else if (elseStmtNode != null) {
  17:         statementExecutor.execute(elseStmtNode);
  18:     }
  19:     ++executionCount;//if虽然可能后好几句,但是只可能执行一句。 
  20:     return null;
  21: }

此execute()方法执行IF节点的第一个子节点即计算关系表达式子树的值,如果值为真,方法执行第二个子节点即THEN后的语句子树;如果为假,在还有第三个子节点的情况下,就执行第三个子节点即ELSE后的语句子树。

清单8-7 展示了Pascal IF语句执行输出。注意它能正确的处理级联IF THEN ELSE语句和"dangling else"(摇摆else,参见第7章第二部分解释)。为了减少文章大小,这儿的输出省略,请将Eclipse中Pascal执行的参数改成"execute if.txt"。

  执行SELECT语句(即CASE语句)

清单8-8 展示了执行器子类SelectExecutor的execute()方法,它执行SELECT分析树。

   1: public Object execute(ICodeNode node) {
   2:     //参见图7-7,CASE的节点分布
   3:     List<ICodeNode> selectChildren = node.getChildren();
   4:     ICodeNode exprNode = selectChildren.get(0);
   5:     // 计算CASE表达式
   6:     ExpressionExecutor expressionExecutor = new ExpressionExecutor(this);
   7:     Object selectValue = expressionExecutor.execute(exprNode);
   8:     //根据表达式值找到一个可匹配的分支,找到就执行此分支后的语句
   9:     ICodeNode selectedBranchNode = searchBranches(selectValue,
  10:             selectChildren);
  11:     if (selectedBranchNode != null) {
  12:         ICodeNode stmtNode = selectedBranchNode.getChildren().get(1);
  13:         StatementExecutor statementExecutor = new StatementExecutor(this);
  14:         statementExecutor.execute(stmtNode);
  15:     }
  16:     ++executionCount; //CASE虽然有一大片,但最后只会选择一条分支
  17:     return null;
  18: }

此execute()方法执行SELECT节点的第一个子节点即计算CASE表达式的值,然后searchBranches()根据这个值搜索SELECT_BRANCH子节点,如果找到一个分支(一个SELECT_BRANCH节点),接着execute()执行这个分支的语句。清单8-9展示了searchBranches()方法。它尝试找到一个常量值与表达式值相等的分支。它遍历SELECT节点的SELECT_BRANCH子节点。对每一个分支,它调用searchConstant()在分支常量中找到匹配的值。如果匹配到,它马上返回一个SELECT_BRANCH节点;如果找不到返回null值。

   1: //查找与此selectValue相匹配的分支
   2:     private ICodeNode searchBranches(Object selectValue,
   3:             List<ICodeNode> selectChildren) {
   4:         for (int i = 1; i < selectChildren.size(); ++i) {
   5:             ICodeNode branchNode = selectChildren.get(i);
   6:             if (searchConstants(selectValue, branchNode)) {
   7:                 return branchNode;
   8:             }
   9:         }
  10:         return null;
  11:     }

清单8-10 展示了searchConstant()方法。这个代码比较简单,就不展示了,可以参考源代码wci.backend.interpreter.executors.RudimentSelectExecutor中的第50到75行

清单8-11 展示了解析和执行一个Pascal CASE语句的输出,为了减少文章大小,这儿的输出省略,请将Eclipse中Pascal执行的参数改成"execute case.txt"。这个输出是就下面优化版本输出的,可以将StatementExecutor的54行的SelectExecutor改成RudimentSelectExecutor,看看这个“慢”版的时间到底少多少。

这个版本(RudimentExecutor)不是很有效率。searchBranches()执行一个线性查找,搜索SELECT_BRANCH(效率O(n)),还在每个分支上执行一个线性查找,搜索匹配常量(也算O(n))。如果源代码语句有很多分支和常量,性能就变的很低了。找一个出现在CASE语句靠后的分支比靠前的分支要花更多时间。

  一个优化版的SELECT执行器

你能通过"jump tables"(查找表)让SELECT节点达到更好的运行性能。清单8-12 展示了SelectExecutor类中这样一个优化本的execute()方法。

   1: // 查找表缓存: Map键为SELECT节点,值是此节点的查找表,这个是全局的!!!执行完要清空!
   2:    static HashMap<ICodeNode, HashMap<Object, ICodeNode>> jumpCache =
   3:        new HashMap<ICodeNode, HashMap<Object, ICodeNode>>();
   4:   
   5:    public Object execute(ICodeNode node)
   6:    {
   7:        //缓存是否有?没有创建一个缓存项
   8:        HashMap<Object, ICodeNode> jumpTable = jumpCache.get(node);
   9:        if (jumpTable == null) {
  10:            jumpTable = createJumpTable(node);
  11:            jumpCache.put(node, jumpTable);
  12:        }
  13:        List<ICodeNode> selectChildren = node.getChildren();
  14:        //计算case表达式
  15:        ICodeNode exprNode = selectChildren.get(0);
  16:        ExpressionExecutor expressionExecutor = new ExpressionExecutor(this);
  17:        Object selectValue = expressionExecutor.execute(exprNode);
  18:        // 在查找表中找Branch
  19:        ICodeNode statementNode = jumpTable.get(selectValue);
  20:        if (statementNode != null) {
  21:            StatementExecutor statementExecutor = new StatementExecutor(this);
  22:            statementExecutor.execute(statementNode);
  23:        }
  24:        ++executionCount; 
  25:        return null;
  26:    }

execute()方法在jumpCache中搜索当前SELECT节点的查找表,假如没找到就创建一个表。因而它仅根据需要在运行时中为SELECT节点创建一个静态表。在计算SELECT表达式的值后,execute()是用这个值作为键在查找表中找到对应的分支语句去执行。

方法createJumpTable()为每一个SELECT节点创建一个查找表即hashmap,它将每个SELECT常量值映射到对应的SELECT分支语句上。见清单8-13

清单8-13:优化版SelectExecutor中的createJumpTable()方法。

   1: /**
   2:      * 为某一个SELECT节点根据CASE情况创建静态查找表
   3:      * @param node SELECT节点
   4:      * @return 查找表
   5:      */
   6:     private HashMap<Object, ICodeNode> createJumpTable(ICodeNode node)
   7:     {
   8:         HashMap<Object, ICodeNode> jumpTable = new HashMap<Object, ICodeNode>();
   9:  
  10:         // 遍历分支,将常量和语句变成查找表的某一项
  11:         List<ICodeNode> selectChildren = node.getChildren();
  12:         for (int i = 1; i < selectChildren.size(); ++i) {
  13:             ICodeNode branchNode = selectChildren.get(i);
  14:             ICodeNode constantsNode = branchNode.getChildren().get(0);
  15:             ICodeNode statementNode = branchNode.getChildren().get(1);
  16:             //将如1,2,3: xx的三个常量变成三个查找项
  17:             List<ICodeNode> constantsList = constantsNode.getChildren();
  18:             for (ICodeNode constantNode : constantsList) {
  19:                 Object value = constantNode.getAttribute(VALUE);
  20:                 jumpTable.put(value, statementNode);
  21:             }
  22:         }
  23:         return jumpTable;
  24:     }

为SELECT分析树包含这样一个针对中间码的优化步骤,三阶段(three-pass,parse-解析,optimize-优化,execute-执行)解释器你也算入门了。对于其它语句分析树也有优化可能。本书最后一章将讲述更多关于解释器和编译器优化的东西。

在接下来两章,你将解析Pascal声明(declaration)并给语句解析器增加类型检查(type checking)。