欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-13 12:14:16
...

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文件中定制语法分析过程