首页 > 代码库 > 编译器开发系列--Ocelot语言2.变量引用的消解
编译器开发系列--Ocelot语言2.变量引用的消解
“变量引用的消解”是指确定具体指向哪个变量。例如变量“i”可能是全局变量i,也可能是静态变量i,还可能是局部变量i。通过这个过程来消除这样的不确定性,确定所引用的到底是哪个变量。
为了消除这样的不确定性,我们需要将所有的变量和它们的定义关联起来,这样的处理称为“变量引用的消解”。具体来说,就是为抽象语法树中所有表示引用变量的VariableNode 对象添加该变量的定义(Variable 对象)的信息。
LocalResolver就是用来处理变量引用消解的类,继承自Visitor(“变量引用的消解”和“静态类型检查”等一连串的处理)。入口方法为:
/*入口 * 生成了以ToplevelScope 为根节点的 Scope 对象的树,并且将所有VariableNode 和其定义关联起来了。 */ // #@@range/resolve{ public void resolve(AST ast) throws SemanticException { /* * 第1 部分先生成ToplevelScope 对象, 然后将生成的ToplevelScope 对象用 scopeStack.add(toplevel) 添加到scopeStack。这样栈里面就有了1 个Scope 对象 */ ToplevelScope toplevel = new ToplevelScope(); scopeStack.add(toplevel); /*变量定义的添加. * 两个foreach 语句都是将全局变量、函数以及类型添加到ToplevelScope 中。 * 两者都是调用ToplevelScope#declareEntity 往ToplevelScope 对象中添加定义或声明。 * 第1个foreach 语句添加导入文件(*.hb)中声明的外部变量和函数 * 第2 个foreach 语句用于导入所编译文件中定义的变量和函数 */ // #@@range/declareToplevel{ for (Entity decl : ast.declarations()) { toplevel.declareEntity(decl); } for (Entity ent : ast.definitions()) { toplevel.defineEntity(ent); } // #@@} // #@@range/resolveRefs{ resolveGvarInitializers(ast.definedVariables());//遍历全局变量 resolveConstantValues(ast.constants());//遍历常量的初始化表达式 resolveFunctions(ast.definedFunctions());//最重要的 // #@@} toplevel.checkReferences(errorHandler); if (errorHandler.errorOccured()) { throw new SemanticException("compile failed."); } /* * 在最后部分中,将在此类中生成的ToplevelScope 对象和ConstantTable 对象保存到 AST 对象中。这两个对象在生成代码时会用到,为了将信息传给下一阶段,所以保存到AST 对 象中。 */ ast.setScope(toplevel); ast.setConstantTable(constantTable); /* * 至此为止变量引用的消解处理就结束了,上述处理生成了以ToplevelScope 为根节点的 Scope 对象的树,并且将所有VariableNode 和其定义关联起来了。 */ }
先往栈中添加ToplevelScope,ToplevelScope表示程序顶层的作用域,保存有函数和全局变量。
然后往这个顶层的作用域添加各种全局变量和函数。
这里着重说下resolveFunctions(ast.definedFunctions());这个方法,对抽象语法树中的所有函数的消解:
/* * 函数定义的处理. */ // #@@range/resolveFunctions{ private void resolveFunctions(List<DefinedFunction> funcs) { for (DefinedFunction func : funcs) { //调用pushScope 方法,生成包含函数形参的作用域,并将作用域压到栈(scopeStack)中 pushScope(func.parameters()); //用resolve(func.body()) 方法来遍历函数自身的语法树 resolve(func.body()); //调用popScope 方法弹出刚才压入栈的Scope 对象,将该Scope 对象用func.setScope //添加到函数中 func.setScope(popScope()); } }
遍历所有的函数节点,首先将单个函数节点里的所有形参或者已定义的局部变量压入一个临时变量的作用域。然后再将这个临时变量的作用域LocalScope压入scopeStack:
//pushScope 方法是将新的LocalScope 对象压入作用域栈的方法 // #@@range/pushScope{ private void pushScope(List<? extends DefinedVariable> vars) { //生成以currentScope() 为父作用域的LocalScope 对象 //currentScope 是返回当前栈顶的Scope 对象的方法 LocalScope scope = new LocalScope(currentScope()); /* * 接着用foreach 语句将变量vars 添加到LocalScope 对象中。也就是说,向LocalScope 对象添加在这个作用域上所定义的变量。特别是在函数最上层的LocalScope 中,要添加形参 的定义。 */ for (DefinedVariable var : vars) { //先用scope.isDefinedLocally 方法检查是否已经定义了同名的变量 if (scope.isDefinedLocally(var.name())) { error(var.location(), "duplicated variable in scope: " + var.name()); } //然后再进行添加。向LocalScope 对象添加变量时使用defineVariable 方法 else { scope.defineVariable(var); } } /* * 最后通过调用scopeStack.addLast(scope) 将生成的LocalScope 对象压到作用域 的栈顶。这样就能表示作用域的嵌套了。 */ scopeStack.addLast(scope); }
然后resolve(func.body());是消解当前函数的函数体,也就是BlockNode节点,根据java的多态特性,最终是调用以下方法:
/* * 添加临时作用域. * C 语言(C?)中的程序块({...}block)也会引入新的变量作用域。 */ // #@@range/BlockNode{ public Void visit(BlockNode node) { /* * 首先调用pushScope 方法,生成存储着这个作用域上定义的变量的Scope 对象,然后压 入作用域栈。 */ pushScope(node.variables()); /* * 接着执行super.visit(node);,执行在基类Visitor 中定义的处理,即对程序块的 代码进行遍历。 visit(VariableNode node) */ super.visit(node); /* * 最后用popScope 方法弹出栈顶的Scope 对象,调用BlockNode 对象的setScope 方 法来保存节点所对应的Scope 对象。 */ node.setScope(popScope()); return null; }
又是一个pushScope(node.variables());将函数体中的临时变量声明的列表保存在一个LocalScope中,然后将这个LocalScope压入scopeStack栈中。然后最关键的来了,super.visit(node);调用Visitor类中的visit方法来对函数体中的局部变量进行消解,
public Void visit(BlockNode node) { for (DefinedVariable var : node.variables()) { if (var.hasInitializer()) { visitExpr(var.initializer()); } } visitStmts(node.stmts()); return null; }
visitStmts是关键,因为变量的引用是保存在VariableNode节点中,最终会调用:
/* * 建立VariableNode 和变量定义的关联. * 使用之前的代码已经顺利生成了Scope 对象的树,下面只要实现树的查找以及引用消解的 代码就可以了。 */ // #@@range/VariableNode{ public Void visit(VariableNode node) { try { //先用currentScope().get 在当前的作用域中查找变量的定义 Entity ent = currentScope().get(node.name()); /* * 取得定义后,通过调用ent.refered() 来记录定义的引用信息,这样当变量没有被用到 时就能够给出警告。 */ ent.refered(); /* * 还要用node.setEntity(ent) 将定义保存到变量节点中,以便随时能够从VariableNode 取得变量的定义。 建立VariableNode 和变量定义的关联 */ node.setEntity(ent); } /* * 如果找不到变量的定义,currentScope().get 会抛出SemanticException 异常, 将其捕捉后输出到错误消息中。 */ catch (SemanticException ex) { error(node, ex.getMessage()); } return null; }
首先查找离栈最近的一层currentScope(),如果找到了就调用setEntity建立VariableNode 和变量定义的关联。这里比较关键的是这个get方法,它首先调用LocalScope的get:
/*从作用域树取得变量定义. * LocalScope#get 是从作用域树获取变量定义的方法。 * 首先调用variables.get 在符号表中查找名为name 的变量,如果找到的话就返回 该变量,找不到的话则调用父作用域(parent)的get 方法继续查找。如果父作用域是 LocalScope 对象,则调用相同的方法进行递归查找。 如果父作用域是ToplevelScope 的话,则调用ToplevelScope#get */ // #@@range/get{ public Entity get(String name) throws SemanticException { DefinedVariable var = variables.get(name); if (var != null) { return var; } else { return parent.get(name); } }
如果找不到就一直往父作用域上找,一直找到ToplevelScope的get:
/* * 如果在ToplevelScope 通过查找entities 找不到变量的定义, 就会抛出 SemanticException 异常,因为已经没有更上层的作用域了。 */ /** Searches and gets entity searching scopes upto ToplevelScope. */ // #@@range/get{ public Entity get(String name) throws SemanticException { Entity ent = entities.get(name); if (ent == null) { throw new SemanticException("unresolved reference: " + name); } return ent; }
如果还找不到就抛出异常。所以总体来说,变量的引用是发现最近作用域的定义。
编译器开发系列--Ocelot语言2.变量引用的消解