1、介绍
在为类似Cymbol的编程语言编写解释器、编译器或者翻译器之前,我们需要确保
Cymbol程序中使用的符号(标识符)用法正确。在本节中,我们计划编写一个能做
出以下校验的Cymbol验证器:
引用的变量必须有可见的(在作用域中)定义
引用的函数必须有定义(函数可以以任何顺序出现,即函数定义提升)
变量不可用作函数
函数不可用作变量
让我们首先来看一些包含不同标识符引用的样例代码,其中一些标识符是无效的
vars.cymbol 如下:
scss
int f(int x, float y) {
g(); // forward reference is ok
i = 3; // no declaration for i (error)
g = 4; // g is not variable (error)
return x + y; // x, y are defined, so no problem
}
void g() {
int x = 0;
float y;
y = 9; // y is defined
f(); // backward reference is ok
z(); // no such function (error)
y(); // y is not function (error)
x = f; // f is not a variable (error)
}
2、解决办法
1、符号表速成
语言的实现者通常把存储符号的数据结构称为符号表。实现这样的语言意味着建立
复杂的符号表结构。如果一门语言允许相同的标识符在不同的上下文中具备不同含
义,那么对应的符号表实现就需要将符号按照作用域分组。一个作用域仅仅是一组
符号的集合,例如一组函数的参数列表或者全局作用域中定义的变量和函数。
符号表本身仅仅是符号定义的仓库------它不进行任何验证工作。我们需要按照之前
确定的规则,检查表达式中引用的变量和函数,以完成代码的验证。符号验证的过
程中有两种基本的操作:定义符号和解析符号。定义一个符号意味着将它添加到作
用域中。解析一个符号意味着确定该符号引用了哪个定义。在某种意义上,解析一
个符号意味着寻找"最接近"的符号定义。最接近的定义域就是最内层的代码块。
例如,下面的Cymbol示例代码包含了不同作用域(以黑圈数字标记)下的符号定
义。
全局作用域①包含了变量x和y,以及函数a()和b()。函数定义在全局作用域
中,但是建立了新的作用域,该作用域包含函数的参数(如果有的话),参见②和
⑤。函数内部作用域(③和⑥)也可以嵌套产生一个新的作用域。局部变量声明于
嵌套在对应函数作用域中的局部作用域(③、④和⑥)中。
由于符号x被定义了两次,我们无法避免在同一个集合中处理所有标识符时的冲突问
题。这就是作用域存在的意义。我们维护一组作用域,在同一个作用域中一个标识
符只允许被定义一次。我们还为每个作用域维护一个指向父作用域的指针,这样,
我们就能在外层作用域中寻找符号定义。全部的作用域构成一棵树
圆圈中的数字代表源代码中的作用域。任何节点到根节点(全局作用域)的路径构
成了一个作用域栈。当寻找一个符号定义时,我们从引用所在的作用域开始,沿着
作用域树向上查找,直至找到其定义为止。
2、涉及到的类
- Scope 接口
csharp
package com.g4.model;
/**
* @author Administrator
*/
public interface Scope {
/**
* 获取作用域名称
* @return
*/
String getScopeName();
/** Where to look next for symbols
*/
Scope getEnclosingScope();
/** Define a symbol in the current scope
* @param sym 符号表
* */
void define(Symbol sym);
/**
* Look up name in this scope or in enclosing scope if not here
* @param name 名称
* @return
*/
Symbol resolve(String name);
}
- Symbol 类
typescript
package com.g4.model;
/**
* @author Administrator
*/
public class Symbol {
public static enum Type {tINVALID, tVOID, tINT, tFLOAT}
// All symbols at least have a name
String name;
Type type;
// All symbols know what scope contains them.
Scope scope;
public Symbol(String name) {
this.name = name;
}
public Symbol(String name, Type type) {
this(name);
this.type = type;
}
public String getName() {
return name;
}
@Override
public String toString() {
if (type != Type.tINVALID) {
return '<' + getName() + ":" + type + '>';
}
return getName();
}
}
- BaseScope
typescript
package com.g4.model;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Administrator
*/
public abstract class BaseScope implements Scope {
/***
* 内部作用域
*/
Scope enclosingScope;
/***
* 符号表
*/
Map<String, Symbol> symbols = new LinkedHashMap<String, Symbol>();
public BaseScope(Scope enclosingScope) {
this.enclosingScope = enclosingScope;
}
@Override
public Symbol resolve(String name) {
// 从符号表中获取
Symbol s = symbols.get(name);
if (s != null) {
return s;
}
// 从内部定义中获取解析
if (enclosingScope != null) {
return enclosingScope.resolve(name);
}
// not found
return null;
}
@Override
public void define(Symbol sym) {
symbols.put(sym.name, sym);
// track the scope in each symbol
sym.scope = this;
}
@Override
public Scope getEnclosingScope() {
return enclosingScope;
}
@Override
public String toString() {
return getScopeName() + ":" + symbols.keySet().toString();
}
}
- GlobalScope
scala
package com.g4.model;
/**
* @author Administrator
*/
public class GlobalScope extends BaseScope {
public GlobalScope(Scope enclosingScope) {
super(enclosingScope);
}
@Override
public String getScopeName() {
return "globals";
}
}
- LocalScope
scala
package com.g4.model;
/**
* @author Administrator
*/
public class LocalScope extends BaseScope {
public LocalScope(Scope parent) {
super(parent);
}
@Override
public String getScopeName() {
return "locals";
}
}
- FunctionSymbol
typescript
package com.g4.model;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Administrator
*/
public class FunctionSymbol extends Symbol implements Scope {
Map<String, Symbol> arguments = new LinkedHashMap<String, Symbol>();
Scope enclosingScope;
public FunctionSymbol(String name, Type retType, Scope enclosingScope) {
super(name, retType);
this.enclosingScope = enclosingScope;
}
@Override
public Symbol resolve(String name) {
Symbol s = arguments.get(name);
if (s != null) {
return s;
}
// if not here, check any enclosing scope
if (getEnclosingScope() != null) {
return getEnclosingScope().resolve(name);
}
// not found
return null;
}
@Override
public void define(Symbol sym) {
arguments.put(sym.name, sym);
// track the scope in each symbol
sym.scope = this;
}
@Override
public Scope getEnclosingScope() {
return enclosingScope;
}
@Override
public String getScopeName() {
return name;
}
@Override
public String toString() {
return "function" + super.toString() + ":" + arguments.values();
}
}
- VariableSymbol
scala
package com.g4.model;
/***
* Excerpted from "The Definitive ANTLR 4 Reference",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
/** Represents a variable definition (name,type) in symbol table
* @author Administrator*/
public class VariableSymbol extends Symbol {
public VariableSymbol(String name, Type type) {
super(name, type);
}
}
3、验证器的架构
为完成该验证器,让我们从全局的角度进行一下规划。我们可以将这个问题分解为
两个关键的操作:定义和解析。对于定义,我们需要监听变量和函数定义的事件,
生成Symbol对象并将其加入该定义所在的作用域中。在函数定义开始时,我们需要
将一个新的作用域"入栈",然后在它结束时将该作用域"出栈"。
对于解析和校验符号引用,我们需要监听表达式中的变量和函数引用的事件。对于
每个引用,我们要验证是否存在一个匹配的符号定义,以及该引用是否正确使用了
该符号。虽然这种策略看上去相当直白,但是实际上存在一个难题:一个Cymbol程
序可以在函数声明之前就调用它。我们称之为前向引用(forward reference)。
为了支持这种情况,我们需要对语法分析树进行两趟遍历,第一趟遍历------或者说
第一个阶段------对包括函数在内的符号进行定义,第二趟遍历中就可以看到文件中
全部的函数了。下列代码触发了对语法分析树的两趟遍历
在定义阶段,我们将会创建很多个作用域。我们必须保持对这些定义域的引用,否
则垃圾回收器会将它们清除掉。为保证符号表在从定义阶段到解析阶段的转换过程
中始终存在,我们需要追踪这些作用域。最合乎逻辑的存储位置是语法分析树本身
(或者使用一个将节点和值映射起来的标注Map)。这样,在沿语法分析树下降的过
程中,查找一个引用对应的作用域就变得十分容易,因为函数或者局部代码块对应
的树节点可以获得指向自身作用域的指针。
4、 定义和解析符号
确定了全局的策略,我们就可以开始编写验证器了,不妨从DefPhase开始。它需要
三个字段:一个全局作用域的引用、一个用于追踪我们创建的作用域的语法分析树
标注器,以及一个指向当前作用域的指针。监听器方法enterFile()启动了整个
验证过程,并创建了一个全局作用域。最后的exitFile()方法负责打印结果。
当语法分析器发现一个函数定义时,我们的程序就需要创建一个FunctionSymbol对
象。FunctionSymbol对象有两项职责:作为一个符号,以及作为一个包含参数的作
用域。为构造一个嵌套在全局作用域中的函数作用域,我们将一个函数作用域"入
栈"。"入栈"是通过将当前作用域设置为该函数作用域的父作用域,并将它本身
设置为当前作用域来完成的。
方法saveScope()使用新建的函数作用域标注了该functionDecl规则节点,这样
之后进行的下一个阶段就能轻易地获取相应的作用域。在函数结束时,我们将函数
作用域"出栈",这样当前作用域就恢复为全局作用域。
局部作用域的实现与之类似。我们在监听器方法enterBlock()中将一个作用域入
栈,然后在exitBlock()中将其出栈。
现在,我们已经能够很好地处理作用域和函数定义了,接下来让我们完成对参数和
变量的定义。
这样,我们就完成了定义阶段代码的编写。
5、解析阶段
之后,当树遍历器触发Cymbol函数和代码块的进入和退出方法时,我们根据定义阶
段在树中存储的值,将currentScope设为对应的作用域。
在遍历器正确设置作用域之后,我们就可以在变量引用和函数调用的监听器方法中
解析符号了。当遍历器遇到一个变量引用时,它调用exitVar(),该方法使用
resolve()方法在当前作用域的符号表中查找该变量名。如果resolve方法在当前
作用域中没有找到相应的符号,它会沿着外围作用域链查找。必要情况下,
resolve将会一直向上查找,直至全局作用域为止。如果它没有找到合适的定义,
则返回null。此外,若resolve()方法找到的符号是函数而非变量,我们就需要
生成一个错误消息。
处理函数调用的方法与之基本相同。如果找不到定义,或者找到的定义是变量,那
么我们就输出一个错误。
6、完整的定义和解析代码
1、定义部分DefPhase
typescript
package com.g4;
import com.g4.auto.CymbolBaseListener;
import com.g4.auto.CymbolParser;
import com.g4.model.*;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTreeProperty;
/**
* @author Administrator
*/
public class DefPhase extends CymbolBaseListener {
ParseTreeProperty<Scope> scopes = new ParseTreeProperty<Scope>();
GlobalScope globals;
// define symbols in this scope
Scope currentScope;
@Override
public void enterFile(CymbolParser.FileContext ctx) {
globals = new GlobalScope(null);
currentScope = globals;
}
@Override
public void exitFile(CymbolParser.FileContext ctx) {
System.out.println(globals);
}
@Override
public void enterFunctionDecl(CymbolParser.FunctionDeclContext ctx) {
String name = ctx.ID().getText();
int typeTokenType = ctx.type().start.getType();
Symbol.Type type = CheckSymbols.getType(typeTokenType);
// push new scope by making new one that points to enclosing scope
FunctionSymbol function = new FunctionSymbol(name, type, currentScope);
// Define function in current scope
currentScope.define(function);
// Push: set function's parent to current
saveScope(ctx, function);
// Current scope is now function scope
currentScope = function;
}
void saveScope(ParserRuleContext ctx, Scope s) {
scopes.put(ctx, s);
}
@Override
public void exitFunctionDecl(CymbolParser.FunctionDeclContext ctx) {
System.out.println(currentScope);
// pop scope
currentScope = currentScope.getEnclosingScope();
}
@Override
public void enterBlock(CymbolParser.BlockContext ctx) {
// push new local scope
currentScope = new LocalScope(currentScope);
saveScope(ctx, currentScope);
}
@Override
public void exitBlock(CymbolParser.BlockContext ctx) {
System.out.println(currentScope);
// pop scope
currentScope = currentScope.getEnclosingScope();
}
@Override
public void exitFormalParameter(CymbolParser.FormalParameterContext ctx) {
defineVar(ctx.type(), ctx.ID().getSymbol());
}
@Override
public void exitVarDecl(CymbolParser.VarDeclContext ctx) {
defineVar(ctx.type(), ctx.ID().getSymbol());
}
void defineVar(CymbolParser.TypeContext typeCtx, Token nameToken) {
int typeTokenType = typeCtx.start.getType();
Symbol.Type type = CheckSymbols.getType(typeTokenType);
VariableSymbol var = new VariableSymbol(nameToken.getText(), type);
// Define symbol in current scope
currentScope.define(var);
}
}
2、引用部分 RefPhase
typescript
package com.g4;
import com.g4.auto.CymbolBaseListener;
import com.g4.auto.CymbolParser;
import com.g4.model.*;
import org.antlr.v4.runtime.tree.ParseTreeProperty;
/**
* @author Administrator
*/
public class RefPhase extends CymbolBaseListener {
ParseTreeProperty<Scope> scopes;
GlobalScope globals;
// resolve symbols starting in this scope
Scope currentScope;
public RefPhase(GlobalScope globals, ParseTreeProperty<Scope> scopes) {
this.scopes = scopes;
this.globals = globals;
}
@Override
public void enterFile(CymbolParser.FileContext ctx) {
currentScope = globals;
}
@Override
public void enterFunctionDecl(CymbolParser.FunctionDeclContext ctx) {
currentScope = scopes.get(ctx);
}
@Override
public void exitFunctionDecl(CymbolParser.FunctionDeclContext ctx) {
currentScope = currentScope.getEnclosingScope();
}
@Override
public void enterBlock(CymbolParser.BlockContext ctx) {
currentScope = scopes.get(ctx);
}
@Override
public void exitBlock(CymbolParser.BlockContext ctx) {
currentScope = currentScope.getEnclosingScope();
}
@Override
public void exitVar(CymbolParser.VarContext ctx) {
String name = ctx.ID().getSymbol().getText();
Symbol var = currentScope.resolve(name);
if ( var==null ) {
CheckSymbols.error(ctx.ID().getSymbol(), "no such variable: "+name);
}
if ( var instanceof FunctionSymbol) {
CheckSymbols.error(ctx.ID().getSymbol(), name+" is not a variable");
}
}
@Override
public void exitCall(CymbolParser.CallContext ctx) {
// can only handle f(...) not expr(...)
String funcName = ctx.ID().getText();
Symbol meth = currentScope.resolve(funcName);
if ( meth==null ) {
CheckSymbols.error(ctx.ID().getSymbol(), "no such function: "+funcName);
}
if ( meth instanceof VariableSymbol) {
CheckSymbols.error(ctx.ID().getSymbol(), funcName+" is not a function");
}
}
}
3、验证代码 CheckSymbols
java
package com.g4;
/***
* Excerpted from "The Definitive ANTLR 4 Reference",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import com.g4.auto.CymbolLexer;
import com.g4.auto.CymbolParser;
import com.g4.model.Symbol;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;
import java.io.FileInputStream;
import java.io.InputStream;
public class CheckSymbols {
public static Symbol.Type getType(int tokenType) {
switch (tokenType) {
case CymbolParser.K_VOID:
return Symbol.Type.tVOID;
case CymbolParser.K_INT:
return Symbol.Type.tINT;
case CymbolParser.K_FLOAT:
return Symbol.Type.tFLOAT;
}
return Symbol.Type.tINVALID;
}
public static void error(Token t, String msg) {
System.err.printf("line %d:%d %s\n", t.getLine(), t.getCharPositionInLine(),
msg);
}
public void process(String[] args) throws Exception {
String inputFile = null;
if (args.length > 0) {
inputFile = args[0];
}
InputStream is = System.in;
if (inputFile != null) {
is = new FileInputStream(inputFile);
}
ANTLRInputStream input = new ANTLRInputStream(is);
CymbolLexer lexer = new CymbolLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CymbolParser parser = new CymbolParser(tokens);
parser.setBuildParseTree(true);
ParseTree tree = parser.file();
// show tree in text form
// System.out.println(tree.toStringTree(parser));
ParseTreeWalker walker = new ParseTreeWalker();
DefPhase def = new DefPhase();
walker.walk(def, tree);
// create next phase and feed symbol table info from def to ref phase
RefPhase ref = new RefPhase(def.globals, def.scopes);
walker.walk(ref, tree);
}
public static void main(String[] args) throws Exception {
new CheckSymbols().process(args);
}
}