antlr4 验证程序中符号的使用

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);
    }
}
相关推荐
落落落sss4 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己4 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
2401_8532757325 分钟前
ArrayList 源码分析
java·开发语言
爪哇学长29 分钟前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
MoFe143 分钟前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
深情废杨杨1 小时前
后端-实现excel的导出功能(超详细讲解)
java·spring boot·excel
智汇探长1 小时前
EasyExcel自定义设置Excel表格宽高
java·excel·easyexcel
酸奶代码1 小时前
Spring AOP技术
java·后端·spring
代码小鑫1 小时前
A034-基于Spring Boot的供应商管理系统的设计与实现
java·开发语言·spring boot·后端·spring·毕业设计