antlr4 验证程序中符号的使用

758 阅读10分钟

1、介绍

在为类似Cymbol的编程语言编写解释器、编译器或者翻译器之前,我们需要确保

Cymbol程序中使用的符号(标识符)用法正确。在本节中,我们计划编写一个能做

出以下校验的Cymbol验证器:

引用的变量必须有可见的(在作用域中)定义

引用的函数必须有定义(函数可以以任何顺序出现,即函数定义提升)

变量不可用作函数

函数不可用作变量

让我们首先来看一些包含不同标识符引用的样例代码,其中一些标识符是无效的

vars.cymbol 如下:

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示例代码包含了不同作用域(以黑圈数字标记)下的符号定

义。

image.png

全局作用域①包含了变量x和y,以及函数a()和b()。函数定义在全局作用域

中,但是建立了新的作用域,该作用域包含函数的参数(如果有的话),参见②和

⑤。函数内部作用域(③和⑥)也可以嵌套产生一个新的作用域。局部变量声明于

嵌套在对应函数作用域中的局部作用域(③、④和⑥)中。

由于符号x被定义了两次,我们无法避免在同一个集合中处理所有标识符时的冲突问

题。这就是作用域存在的意义。我们维护一组作用域,在同一个作用域中一个标识

符只允许被定义一次。我们还为每个作用域维护一个指向父作用域的指针,这样,

我们就能在外层作用域中寻找符号定义。全部的作用域构成一棵树

image.png

圆圈中的数字代表源代码中的作用域。任何节点到根节点(全局作用域)的路径构

成了一个作用域栈。当寻找一个符号定义时,我们从引用所在的作用域开始,沿着

作用域树向上查找,直至找到其定义为止。

2、涉及到的类

  • Scope 接口
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 类
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
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
package com.g4.model;

/**
 * @author Administrator
 */
public class GlobalScope extends BaseScope {

    public GlobalScope(Scope enclosingScope) {
        super(enclosingScope);
    }

    @Override
    public String getScopeName() {
        return "globals";
    }
}
  • LocalScope
package com.g4.model;

/**
 * @author Administrator
 */
public class LocalScope extends BaseScope {

    public LocalScope(Scope parent) {
        super(parent);
    }

    @Override
    public String getScopeName() {
        return "locals";
    }
}
  • FunctionSymbol
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
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)。

为了支持这种情况,我们需要对语法分析树进行两趟遍历,第一趟遍历——或者说

第一个阶段——对包括函数在内的符号进行定义,第二趟遍历中就可以看到文件中

全部的函数了。下列代码触发了对语法分析树的两趟遍历

image.png

在定义阶段,我们将会创建很多个作用域。我们必须保持对这些定义域的引用,否

则垃圾回收器会将它们清除掉。为保证符号表在从定义阶段到解析阶段的转换过程

中始终存在,我们需要追踪这些作用域。最合乎逻辑的存储位置是语法分析树本身

(或者使用一个将节点和值映射起来的标注Map)。这样,在沿语法分析树下降的过

程中,查找一个引用对应的作用域就变得十分容易,因为函数或者局部代码块对应

的树节点可以获得指向自身作用域的指针。

4、 定义和解析符号

确定了全局的策略,我们就可以开始编写验证器了,不妨从DefPhase开始。它需要

三个字段:一个全局作用域的引用、一个用于追踪我们创建的作用域的语法分析树

标注器,以及一个指向当前作用域的指针。监听器方法enterFile()启动了整个

验证过程,并创建了一个全局作用域。最后的exitFile()方法负责打印结果。

image.png

当语法分析器发现一个函数定义时,我们的程序就需要创建一个FunctionSymbol对

象。FunctionSymbol对象有两项职责:作为一个符号,以及作为一个包含参数的作

用域。为构造一个嵌套在全局作用域中的函数作用域,我们将一个函数作用域“入

栈”。“入栈”是通过将当前作用域设置为该函数作用域的父作用域,并将它本身

设置为当前作用域来完成的。

image.png

方法saveScope()使用新建的函数作用域标注了该functionDecl规则节点,这样

之后进行的下一个阶段就能轻易地获取相应的作用域。在函数结束时,我们将函数

作用域“出栈”,这样当前作用域就恢复为全局作用域。

image.png

局部作用域的实现与之类似。我们在监听器方法enterBlock()中将一个作用域入

栈,然后在exitBlock()中将其出栈。

现在,我们已经能够很好地处理作用域和函数定义了,接下来让我们完成对参数和

变量的定义。

image.png

这样,我们就完成了定义阶段代码的编写。

5、解析阶段

image.png

之后,当树遍历器触发Cymbol函数和代码块的进入和退出方法时,我们根据定义阶

段在树中存储的值,将currentScope设为对应的作用域。

image.png

在遍历器正确设置作用域之后,我们就可以在变量引用和函数调用的监听器方法中

解析符号了。当遍历器遇到一个变量引用时,它调用exitVar(),该方法使用

resolve()方法在当前作用域的符号表中查找该变量名。如果resolve方法在当前

作用域中没有找到相应的符号,它会沿着外围作用域链查找。必要情况下,

resolve将会一直向上查找,直至全局作用域为止。如果它没有找到合适的定义,

则返回null。此外,若resolve()方法找到的符号是函数而非变量,我们就需要

生成一个错误消息。

image.png

处理函数调用的方法与之基本相同。如果找不到定义,或者找到的定义是变量,那

么我们就输出一个错误。

6、完整的定义和解析代码

1、定义部分DefPhase

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

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

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);
    }
}