ANTLR4(二) Vistor Listener

434 阅读7分钟

Visitor Calculator

我们将以访问者模式做一个计算器。

预期效果

输入:

193
a=5
b=6
a+b*2
(1+2)*3

输出

193
17
9

PS:每次操作都需要换行,输入’!'可以重置标识符对应的数值。

20200828163908577.png

语法文件

Visitor模式:通过在语法规则的每条分支后加上 # Identifier (注意不能和规则名冲突)这样类似标签的形式。
使得对于每种输入我们都有不同的处理方法,后续会介绍如何定义这些处理方法。

此外分享一下在设计clear也就是清零语法时的一些心得:

  1. 文中采用的是,输入时,自动释放标识符与数值的所有对应,并且输出clear并且换行。但一开始考虑的是输入字符串clear,清零后自动换行。放弃这种方法的理由是:clear字符串本身会先被expr规则中的ID识别出来。如果要在ID的visit处理函数中识别clear,又会因为返回值必须是Integar类型而矛盾,最后放弃了输入clear这个方法。
  2. 要注意CLEAR后必须要跟NEWLINE,因为每一行都需要以回车或者换行结束操作。
//LabeledExpr.g4
grammar LabeledExpr; 

prog:   stat+ ;

stat:   expr NEWLINE                # printExpr
    |   ID '=' expr NEWLINE         # assign
    |   CLEAR NEWLINE               # clearMemory
    |   NEWLINE                     # blank
    ;

expr:   expr op=('*'|'/') expr      # MulDiv
    |   expr op=('+'|'-') expr      # AddSub
    |   INT                         # int
    |   ID                          # id
    |   '(' expr ')'                # parens
    ;

CLEAR :   '!' ;
MUL :   '*' ; 
DIV :   '/' ;
ADD :   '+' ;
SUB :   '-' ;
ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace

生成代码

使用IDEA插件生成,可以参照上一节生成。

重写Visitor

观察以下代码我们可以发现,Visitor生成的处理函数命名为 visitor+标签名

理论上,我们需要重写每个分支处理函数来应对不同的输入情况。

有一句格外的显眼:visit(ctx.expr())。

我们不管括号内的expr(),实际上是左循环的token,为了获取子分支的情况,我们需要显式地调用visit()。

visitPrintExpr为例,这句语法的目的是打印出表达式的结果。而表达式本身有多种情况,可以是INT、标识符、嵌套表达式,但是我们

不需要在这个分支中去操心这些,上文说过我们需要重写每个子分支的处理函数,因此在这里只需要通过visit得到expr的值就可以了。

而在下面的visitInt、visitId等分支处理函数中,最后返回了处理过后的值

因此我们以这样一种由上而下的方式,满足了各种输入的情况。

/** "memory" for our calculator; variable/value pairs go here */
    Map<String, Integer> memory = new HashMap<String, Integer>();

    @Override
    public Integer visitProg(LabeledExprParser.ProgContext ctx) {
        return super.visitProg(ctx);
    }


    /** expr NEWLINE */
    @Override
    public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
        // evaluate the expr child
        Integer value = visit(ctx.expr());
        // print the result
        System.out.println(value);
        // return dummy value
        return 0;
    }


    /** ID '=' expr NEWLINE */
    @Override
    public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
        // id is left-hand side of '='
        String id = ctx.ID().getText();
        // compute value of expression on right
        int value = visit(ctx.expr());
        // store it in our memory
        memory.put(id, value);
        return value;
    }

    /** CLEAR MEMORY */
    @Override
    public Integer visitClearMemory(LabeledExprParser.ClearMemoryContext ctx) {
        memory.clear();
        System.out.println("clear");
        return 0;
    }

    @Override
    public Integer visitBlank(LabeledExprParser.BlankContext ctx) {
        return super.visitBlank(ctx);
    }

    @Override
    public Integer visitParens(LabeledExprParser.ParensContext ctx) {
        return super.visitParens(ctx);
    }

    /** expr op=('*'|'/') expr */
    @Override
    public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
        // get value of left subexpression
        int left = visit(ctx.expr(0));
        // get value of right subexpression
        int right = visit(ctx.expr(1));
        if ( ctx.op.getType() == LabeledExprParser.MUL) {
            return left * right;
        }
        // must be DIV
        return left / right;
    }

    /** expr op=('+'|'-') expr */
    @Override
    public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
        // get value of left subexpression
        int left = visit(ctx.expr(0));
        // get value of right subexpression
        int right = visit(ctx.expr(1));
        if ( ctx.op.getType() == LabeledExprParser.ADD ) {
            return left + right;
        }
        // must be SUB
        return left - right;
    }

    /** ID */
    @Override
    public Integer visitId(LabeledExprParser.IdContext ctx) {
        String id = ctx.ID().getText();
        if ( memory.containsKey(id) ) {
            return memory.get(id);
        }
        return 0;
    }

    /** INT */
    @Override
    public Integer visitInt(LabeledExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }

运行结果

编写一个主函数去整合语法分析器以及自定义Vistor:

public class LabelMain {

    public static void main(String[] args) throws IOException {


        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);
        LabeledExprLexer lexer = new LabeledExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LabeledExprParser parser = new LabeledExprParser(tokens);
        ParseTree tree = parser.prog(); // parse

        EvalVisitor eval = new EvalVisitor();
        eval.visit(tree);
    }
}

编写一个 t.exp

aa = 5
bb = 6
aa+bb*2
!
aa
bb

运行的时候配置

image.png

运行结果如下:

image.png

Listener Java

我们将以监听者模式抽取Java类中的方法,并整合成一个接口。

预期效果

输入:

import java.util.List;
import java.util.Map;
public class Demo {
	void f(int x, String y) { }
	int[ ] g(/*no args*/) { return null; }
	List<Map<String, Integer>>[] h() { return null; }
}

输出

2020082817255353.png

语法文件

整个Java.g4文件比较大,这里我们只列举出关于class、method、import的语法识别部分。

Listener模式:与Visitor模式不同的是,Listener无需手动地在各个分支后打上标签,当我们以监听者模式生成语法分析器时,语法分析器每遍历一个规则都会有两次响应事件,分别是enter以及exit

例如:classDeclaration,会生成enterclassDeclaration和exitclassDeclaration,我们可以在这两个时刻被监听到时响应它们。

//Java.g4
classDeclaration
    :   'class' Identifier typeParameters? ('extends' type)?
        ('implements' typeList)?
        classBody
    ;
importDeclaration
    :   'import' 'static'? qualifiedName ('.' '*')? ';'
    ;
methodDeclaration
    :   type Identifier formalParameters ('[' ']')* methodDeclarationRest
    |   'void' Identifier formalParameters methodDeclarationRest
    ;

访问器模式生成语法分析器

生成代码

除了常见的几个文件以外,我们会生成以下两个文件:

JavaListener.java JavaBaseListener.java

前者是遍历整个语法分析树的全部响应事件定义,后者是它的实现

我们可以重写 JavaBaseListener,选择性地编写那些响应事件。

重写Listener

实际上我们只需要重写ImportDeclaration的enter、ClassDeclaration的enter和exit、MethodDeclaration的enter即可。

因为我们预想的类定义需要开始和结尾的{和},因此需要在exit时响应。而import和method对结尾并无要求。

以下代码中需要注意的是,有时需要用到parser.getTokenStream去获取token,有时候直接可以通过ctx.就可以获取。

比如:qualifiedName需要通过parser.getTokenStream获取,而Identifier通过ctx.就可以获取。

观察可以发现前者是语法规则的命名,而后者是词法规则的命名,可见前者这样的语法规则定义的词法符号往往需要通过语法分析器遍历获取。

//ExtractInterfaceListener.java
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.Interval;

public class ExtractInterfaceListener extends JavaBaseListener {
    JavaParser parser;
    public ExtractInterfaceListener(JavaParser parser) {this.parser = parser;}

    @Override 
    public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {
        TokenStream tokens = parser.getTokenStream();
        String temp=tokens.getText(ctx.qualifiedName());
        System.out.println("import "+temp+';');
    }


    /** Listen to matches of classDeclaration */
    @Override
    public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx){
        System.out.println("interface I"+ctx.Identifier()+" {");
    }
    @Override
    public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
        System.out.println("}");
    }

    /** Listen to matches of methodDeclaration */
    @Override
    public void enterMethodDeclaration(
        JavaParser.MethodDeclarationContext ctx
    )
    {
        // need parser to get tokens
        TokenStream tokens = parser.getTokenStream();
        String type = "void";
        if ( ctx.type()!=null ) {
            type = tokens.getText(ctx.type());
        }
        String args = tokens.getText(ctx.formalParameters());
        System.out.println("\t"+type+" "+ctx.Identifier()+args+";");
    }
}

运行结果

编写一个主函数去整合语法分析器以及自定义Listener:

//ExtractInterfaceTool.java
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;

import java.io.FileInputStream;
import java.io.InputStream;

public class ExtractInterfaceTool {
    public static void main(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);

        JavaLexer lexer = new JavaLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        JavaParser parser = new JavaParser(tokens);
        // parse
        ParseTree tree = parser.compilationUnit(); 
        // create standard walker
        ParseTreeWalker walker = new ParseTreeWalker(); 
        ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
        // initiate walk of tree with listener
        walker.walk(extractor, tree); 
    }
}

待用的输入:

//Demo.java
import java.util.List;
import java.util.Map;
public class Demo {
	void f(int x, String y) { }
	int[ ] g(/*no args*/) { return null; }
	List<Map<String, Integer>>[] h() { return null; }
}

编译后,输入java ExtractInterfaceTool Demo.java:

20200828200721219.png

Visitor 与 Listener

我们可以从主函数的角度去对比:

//Visitor
EvalVisitor eval = new EvalVisitor();
eval.visit(tree);
//Listener
// create standard walker
ParseTreeWalker walker = new ParseTreeWalker(); 
ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
// initiate walk of tree with listener
walker.walk(extractor, tree); 

在产生Visitor的语法分析器之前,我们需要对子分支打上标签。产生语法分析器后,需要重写Visitor(每个子分支的visit函数)。最后在主函数中,我们用重写的Visitor实例来访问语法分析树。

在产生Listener的语法分析器之前,我们并不需要额外的操作。产生语法分析器后,需要重写Listener(可选的每个子分支的enter及exit函数)。最后在主函数中,我们新建一个语法树唤醒器,再新建一个重写的Listener实例,把语法分析树和Listener实例放到唤醒器中。

对比以下可以发现:

visitor对于子分支的处理是主动的(打标签),而listener是自动的。 visitor会将输入与子分支进行比对再触发响应,而listener会在enter、exit两个节点触发响应。 在对某个分支的处理时,如果需要调用其子分支,visitor需要主动地visit。而listener在遍历时会自动处理,这让我们无需关注子分支的细节