【ANTLR学习笔记】3:数组初始化列表的解析和翻译

这节跟着书上第三章学习解析例如{val,val,{val,val},val}这样的数组初始化列表,其中每个val都是一个无符号整数。它可以用来将Java中

static short[] data = {1,2,3};

转化成等价的Unicode字符串形式:

static String data = "\u0001\u0002\u0003";

因为Java中的char就是unsigned short,可以用Unicode字符(四个16进制表示的16位字符)来表达。

这样转换的目的是使数组初始化不受编译出的class文件的限制。因为class文件会将数组初始化显式存储为赋值:

data[0] = 1;
data[1] = 2;
data[2] = 3;

而JVM规范规定,包括cinit方法在内,一个方法的字节码长度不能超过65535字节。这样如果需要初始化的数组很长就会超过这个限制。

但是如果使用转换后的字符串就没有这个限制,因为字符串常量存储在class文件的常量池中,没有长度限制。

1 从语法规则文件生成程序

这里需要注意语法规则必须小写字母开头,一般直接用全小写。而词法符号规则必须大写字母开头,一般直接用全大写

这里还有一个技巧,因为生成的程序类名总是这个语法名+后缀拼接的方式,为了符合Java类命名规范,这里的语法名(和对应文件名) 也使用大写字母开头的驼峰式命名

// 和文件名相同的语法名
grammar ArrayInit;

// ----------语法规则的定义(必须小写字母开头)----------

// 顶层规则
init:
    '{' value (',' value)* '}' // 花括号内若干value用逗号隔开
    ;

// value的定义
value:
    init    // 可以是嵌套的花括号
    | INT   // 也可以是单独一个INT词法符号
    ;

// ----------词法符号的定义(必须大写字母开头)----------

// 词法符号INT的定义
INT : [0-9]+; // 一至多个数字
// 空白符号定义
WS  : [ \t\r\n]+ -> skip; // 匹配到时将其丢弃

然后用笔记1中的插件方法自动生成程序,暂时不勾选生成visitor,书上是直接用命令行(antlr4 xxx.g4)。

如果生成的程序中@Override下面有红线报错,是IDEA的项目Language level设置得太低,在模块上F4打开设置,然后将Language level设置为6就可以了。

2 生成的程序文件概览

2.1 ArrayInitParser.java

这个文件中存放语法分析器类:

public class ArrayInitParser extends Parser { ... }

用来解析ArrayInit这个语法文件中的语法规则,每条语法规则都对应其中的一个内部类(对应笔记2中学习的语法树上的非叶子结点)和一个访问方法,即:

public static class InitContext extends ParserRuleContext { ... }
public final InitContext init() throws RecognitionException { ... }

public static class ValueContext extends ParserRuleContext { ... }
public final ValueContext value() throws RecognitionException { ... }

另外还有一些辅助代码。

2.2 ArrayInitLexer.java

这个文件中存放词法分析器类:

public class ArrayInitLexer extends Lexer { ... }

它是通过分析.g4文件中的词法规则INTWS)以及语法规则中的字面值{},)生成的,用于将输入的字符序列分解成词法符号。

2.3 ArrayInit.tokens

这个文件用于存储词法符号到一个数字值的对应关系,每个词法符号都会对应一个数字值,使用它可以在多个小型语法之间同步全部的词法符号类型,以将一个大型语法切分成多个小型语法。

2.4 ArrayInitListener.java

该文件用于存储监听器接口:

public interface ArrayInitListener extends ParseTreeListener { ... }

在遍历语法分析树时,遍历到指定的结点会触发一系列事件回调,该接口即存放这些回调方法的定义。具体地,对于这个ArrayInit语法的语法树而言,这包括:

InitContext结点的进入和退出:

void enterInit(ArrayInitParser.InitContext ctx);
void exitInit(ArrayInitParser.InitContext ctx);

ValueContext结点的进入和退出:

void enterValue(ArrayInitParser.ValueContext ctx);
void exitValue(ArrayInitParser.ValueContext ctx);

进入/退出哪个结点,就会调用上面相应的方法,并且把那个结点的上下文对象作为参数传进去。

2.5 ArrayInitBaseListener.java

该文件用于存储监听器接口的默认实现类:

public class ArrayInitBaseListener implements ArrayInitListener { ... }

也就是ANTLR提供的ArrayInitListener接口的默认实现,可以看到里面的回调方法都是空的,只需要按照需求覆盖感兴趣的回调方法,就能用监听器的方式来访问语法分析树了。

3 构建从数组到字符串的翻译程序

这里通过继承前面生成的监听器类ArrayInitBaseListener,在语法规则回调时翻译对应的部分,然后直接打印出来:
【ANTLR学习笔记】3:数组初始化列表的解析和翻译_第1张图片
实现监听器:

import antlr.ArrayInitBaseListener;
import antlr.ArrayInitParser;

// 继承监听器类,覆盖回调函数
public class ShortToUnicodeString extends ArrayInitBaseListener {
    // 在init结点开始遍历之前,把'{'翻译的'"'输出
    @Override
    public void enterInit(ArrayInitParser.InitContext ctx) {
        System.out.print('"');
    }

    // 在init结点结束遍历之后,把'}'翻译的'"'输出
    @Override
    public void exitInit(ArrayInitParser.InitContext ctx) {
        System.out.print('"');
    }

    // 在value结点开始遍历之前(之后也可以),把数字翻译出的Unicode输出
    @Override
    public void enterValue(ArrayInitParser.ValueContext ctx) {
        // 假设不存在嵌套,value只走INT的分支
        // 这里ctx.INT().getText()从上下文对象中获取这个结点的文本
        int value = Integer.parseInt(ctx.INT().getText());
        // 输出Unicode(数字的4位16进制表示)
        System.out.printf("\\u%04x", value);
    }
}

在主类调用:

import antlr.ArrayInitLexer;
import antlr.ArrayInitParser;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

public class Main {
    public static void main(String[] args) {
        // 词法分析、语法分析并得到语法树(的根节点上下文对象)
        ArrayInitLexer lexer = new ArrayInitLexer(CharStreams.fromString("{1,2,3,4}"));
        ArrayInitParser parser = new ArrayInitParser(new CommonTokenStream(lexer));
        ArrayInitParser.InitContext tree = parser.init();
        System.out.println(tree.toStringTree(parser));
        // 创建一个能触发回调函数的语法分析树遍历器
        ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
        // 将监听器和语法树传入walk方法,该方法会遍历语法树触发回调
        parseTreeWalker.walk(new ShortToUnicodeString(), tree);
    }
}

运行结果:

(init { (value 1) , (value 2) , (value 3) , (value 4) })
"\u0001\u0002\u0003\u0004"

这样就完成了最初设想的,从一维数组到Unicode字符串的翻译器。最后记录一个坑点,如果在Java程序的注释里面打\u字样就会在build的时候报错"非法的Unicode转义"。

你可能感兴趣的:(#,ANTLR)