【ANTLR学习笔记】5:使用监听器构建翻译程序,在g4文件中定制语法分析过程

1 使用监听器构建翻译程序

这里对应书上4.3节,需求是把Java类中的方法都抽取出来生成接口文件,并且保留方法签名中的空白字符和注释。要保留空白符和注释就只能用解析源代码的方式了,不能从字节码文件获取。

1.1 监听器类

import antlr.JavaBaseListener;
import antlr.JavaParser;
import org.antlr.v4.runtime.TokenStream;

// 监听器类,实现ANTLR生成的默认监听器
public class ExtractInterfaceListener extends JavaBaseListener {
    private JavaParser parser;

    // 在构造时把解析器对象传进来,因为要用这个Parser获取Token流
    public ExtractInterfaceListener(JavaParser parser) {
        this.parser = parser;
    }

    // 遍历import结点之前
    @Override
    public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {
        // 直接用Parser中的Token流打印这个结点的内容,也就是抄写整个import语句
        TokenStream tokens = parser.getTokenStream();
        System.out.println(tokens.getText(ctx));
    }

    // 遍历class结点之前
    @Override
    public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
        // 翻译成接口,而且在类名前面多加了个'I'
        System.out.println("interface I" + ctx.Identifier() + " {");
    }

    // 遍历完class结点之后
    @Override
    public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
        // 要把类结尾的花括号打印
        System.out.println("}");
    }

    // 遍历method结点之前
    @Override
    public void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx) {
        TokenStream tokens = parser.getTokenStream();
        // 这里通过看type是不是null来检查走哪条分支
        String type = "void"; // 是null那转换完就是void
        if (ctx.type() != null) {
            // 不是null那就把类型字符串拿到
            type = tokens.getText(ctx.type());
        }
        // 获取方法参数
        String args = tokens.getText(ctx.formalParameters());
        // 转换成方法签名输出,方法体全不要了
        System.out.println("\t" + type + " " + ctx.Identifier() + args + ";");
    }
}

这里需要说明的就是对方法转换为方法签名的处理,可以看Java.g4中方法的部分:

methodDeclaration
    :   type Identifier formalParameters ('[' ']')* methodDeclarationRest
    |   'void' Identifier formalParameters methodDeclarationRest
    ;

也就是说对于void类型是不可能有数组的,所以单独拿了出来(在第二分支),走这个分支的时候取type()只能取到null,因为这里void是字面值。而对于其它分支直接取type名称就可以了,这里的type是包含数组符号的,例如直接打印type部分可以看到类似于下面的输出:

int[ ]

1.2 从主类调用

这里也没什么特别的地方,唯独和之前不同的就是这里是从文件里读取,而且监听器创建时候把parser对象传了进去。

import antlr.JavaLexer;
import antlr.JavaParser;
import org.antlr.v4.runtime.*;
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 = "class2interface/src/main/resources/Demo.java";
        InputStream is = new FileInputStream(inputFile);
        CharStream input = CharStreams.fromStream(is);

        // 解析得语法树
        JavaLexer lexer = new JavaLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        JavaParser parser = new JavaParser(tokens);
        ParseTree tree = parser.compilationUnit();

        // 触发监听器回调
        ParseTreeWalker walker = new ParseTreeWalker();
        ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
        walker.walk(extractor, tree);
    }
}

1.3 运行结果

将文件中的类:

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

翻译成了对应的接口:

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

2 在g4文件中定制语法分析过程

这里对应书上4.4节,这节是讲除了监听器/访问器这种将访问语法树和语法定义分离开的方式之外,还可以为了灵活性而把一些代码片段嵌入到语法中。

2.1 在语法中嵌入任意动作

例如想要识别

parrt	Terence Parr	101
tombu	Tom Burns		020
bke		Kevin Edgar		008

这样的用tab分隔的数据的某一列。

2.1.1 语法规则文件
grammar Rows;

// 表示在生成的RowsParser中添加成员
@parser::members {
    // 添加要解析的列号
    int col;
    // 添加构造器,传入Token流和要解析的列号
    public RowsParser(TokenStream input, int col) {
        this(input);
        this.col = col;
    }
}

// 匹配整个文件:一到多个row后跟换行符
file: (row NL)+ ;

// 对row的定义
row
locals [int i=0]
    : (   STUFF
          {
          $i++;
          if ( $i == col ) System.out.println($STUFF.text);
          }
      )+
    ;

TAB  :  '\t' -> skip ;   // 匹配到tab丢弃
NL   :  '\r'? '\n' ;     // 匹配换行符
STUFF:  ~[\t\r\n]+ ;     // 匹配除了tab和换行符外的任何字符连续若干个

这个文件相比以前写的语法文件,多了一些代码片段,最上面的给RowsParser加成员的部分很好理解。对row的定义的部分有点难懂需要说明一下。

先不看加的代码片段,row这部分就是:

row : (STUFF)+;

这个括号也是因为加了代码片段要括起来的,其实row就是匹配若干个连续的STUFF。综合整个文件知道它会被NL隔开,也就是不同行,所以row就是匹配一行的内容。

然后加的代码片段就好理解了,其实就是在匹配row里的STUFF的时候计数(列号),如果到了col这个数值,那就把这个STUFF的文本输出出来,这样就实现了在当前行提取这一列的内容。

2.1.2 生成解析器代码

这里因为内嵌代码就已经足够完成需求了,既不需要生成Listener也不需要生成Visitor了,所以这两个都可以不用勾选,只要把基本的Lexer和Parser之类的生成好就可以了。

2.1.3 从主类调用
import antlr.RowsLexer;
import antlr.RowsParser;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;

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

public class Col {
    public static void main(String[] args) throws Exception {
        // 从文件读
        String inputFile = "rows/src/main/resources/rows.txt";
        InputStream is = new FileInputStream(inputFile);
        CharStream input = CharStreams.fromStream(is);

        // 词法分析,生成Token流
        RowsLexer lexer = new RowsLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        // 在创建对象语法分析器对象时候把col也传进去
        int col = 1;
        // 这里调用的其实是嵌入的构造器代码
        RowsParser parser = new RowsParser(tokens, 1);
        // 注意,不必花时间建立语法树了
        parser.setBuildParseTree(false);

        // 这里开始语法分析,就可以完成选取列的功能
        parser.file(); // 不需要用到返回值ParseTree
    }
}

可以看到这也省去了构造语法树的时间,直接调用顶层规则.file()就可以了。

2.1.4 运行结果

因为传入的col=1,所以是打印第一列:

parrt
tombu
bke

2.2 使用语义判定改变语法分析过程

书上这部分主要是想引入语义判定这个重要概念,如对于这样的文本:

2 9 10 3 1 2 3

第一个2表示匹配后面2个为一组,接下来的3表示匹配后面三个为一组,也就是要形成这样的语法树:
【ANTLR学习笔记】5:使用监听器构建翻译程序,在g4文件中定制语法分析过程

语法规则文件如下:

grammar Data;

// 匹配文件:若干个group
file : group+ ;

// 匹配group:首先是一个数字,接下来匹配sequence,并将数字值作为参数传入
group: INT sequence[$INT.int] ;

// 匹配sequence,传入的参数值为n
sequence[int n]
locals [int i = 1;] // 设置一个本地变量i,初始为1
     : ( {$i<=$n}? INT {$i++;} )* // 语法判定为true时匹配一个INT,并将i加1
     ;
     
INT :   [0-9]+ ;             // 匹配无符号整数
WS  :   [ \t\n\r]+ -> skip ; // 匹配到空白符时丢弃

2.1中的语法文件相比,一方面是多出了“在匹配时传入参数”的概念,这里是先匹配一个数字,然后把这个数字n传给sequence使用。另一方面就是sequence在匹配时如何做到循环n次,就是这里每次匹配一个INT,然后代码里{$i++;},再进入(...)*的循环匹配,直到语义判定{$i<=$n}?为假的时候,后面的分支要被剪除,这样就退出了循环,完成了循环n次的工作。
【ANTLR学习笔记】5:使用监听器构建翻译程序,在g4文件中定制语法分析过程

猜你喜欢