【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表示匹配后面三个为一组,也就是要形成这样的语法树:
语法规则文件如下:
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
次的工作。