2. Antlr 的快速使用

本文详细介绍ANTLR工具的安装、IDEA插件使用、语法文件创建及与Java程序集成的方法。涵盖词法、语法分析器生成,监听器与访问器操作,以及语法中嵌入动作的实践案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. Antlr 的安装
  • Step1: 下载 Antlr jar包
    antlr-4.0-complete.jar
  • Step2: 将jar包添加至classPath
    export CLASSPATH=".:/usr/local/lib/antlr-4.0-complete.jar:$CLASSPATH"
  • Step3: 调用antlr工具类查看是否安装成功
    java -jar /usr/local/lib/antlr-4.0-complete.jar
    或者 java org.antlr.v4.Tool

以上步骤是不使用集成工具的时候可使用的方法,IDEA集成了Antlr4,可在pom.xml中配置antlr相关jar包的依赖,直接使用antlr工具的功能。

2. IDEA 上Antlr插件的使用
  • Step1: 安装Antlr4插件
    在这里插入图片描述
  • Step2: 创建语法文件,使用Antlr工具生成词法分析和语法分析类
    语法文件ArrayInit.g4
// 语法文件通常以grammar关键字开头,语法名字必须和文件名字相匹配
grammar ArrayInit;

// 一条名为init的规则,它匹配一堆花括号中的逗号分隔的value
init : '{' value ( ',' value)* '}' ;  // 必须匹配至少一个value

// 一个value可以是嵌套的花括号结构,也可以是一个简单的正数,即INT词法符号
value : init
      | INT
      ;

// 语法分析器的规则必须以小写字母开头,词法分析器的规则必须用大写字母开头
INT : [0-9]+ ;
WS  : [ \t\r\n]+ -> skip ;  // 定义词法规则"空白符号",丢弃

设置词法、语法分析器生成目录和包名:右击文件名,选中 Configure ANTLR…进行设置
在这里插入图片描述
在这里插入图片描述

  • Step3: 右击文件名,选中 Generate ANTLR Recognizer…在设置的位置下会生成一些列java类
    在这里插入图片描述
  • Step4: 添加依赖
<dependency>
    <groupId>org.antlr</groupId>
    <artifactId>antlr4-runtime</artifactId>
    <version>4.7.2</version>
</dependency>
<dependency>
    <groupId>org.antlr</groupId>
    <artifactId>antlr4</artifactId>
    <version>4.7.2</version>
</dependency>
  • Step5: ANTLR Preview的使用
    左侧输入要测试的语句,选中语法文件中的语法规则右击,选择 Test Rule xx, 在右边会生成相应的语法树
    在这里插入图片描述
3. 使用Antlr工具生成的文件
  • ArrayInitParser.java: 语法分析器类的定义,专门用来识别我们定义的"数组语言"的语法。每条规则都有对应的方法。
  • ArrayInitLexer.java: 词法分析器的类定义,由ANTLR通过分析词法规则INT和WS,以及语法中的字面值’{’, ‘,’ , '}'生成的。作用是将输入字符序列分解成词汇符号。
  • ArrayInit.tokens: ANTLR会给我们定义的每个词法符号指定一个数字形式的类型,然后将映射关系存储在这个文件中。
  • ArrayInitListener.java, ArrayInitBaseListener.java:在遍历语法分析树时,遍历器能够触发一系列事件(回调),并通知我们提供的监听对象。ArrayInitListener接口给出了这些回调方法的定义,我们可以实现它来完成自定义功能,ArrayInitBaseListener是该接口的默认实现类(空实现)。
4. Antlr 使用案例
4.1 使用步骤
  • Step1: 创建语法文件,语法文件必须为 .g4文件
  • Step2: 使用ANTLR工具生成java类等文件
  • Step3: 按照需求将生成的类集成到更大的应用程序中

Step1 和 Step2上面已经给出步骤,下面主要介绍 Step3 中不同场景的使用方式

4.2 将生成的语法分析器与JAVA程序集成
public class TestArrayInit {
    public static void main(String[] args) throws Exception {
        // 1. 获取字符输入流 CharStream
        ANTLRInputStream input = new ANTLRInputStream(System.in);

        // 2. 创建词法分析器
        ArrayInitLexer lexer = new ArrayInitLexer(input);

        // 3. 创建词法符号缓冲区,存储由词法分析器生成的词法符号
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        // 4. 创建语法分析器
        ArrayInitParser parser = new ArrayInitParser(tokens);

        // 5. 生成语法分析树
        ParseTree tree = parser.init();

        // 6. 打印语法分析树
        System.out.println(tree.toStringTree(parser));
    }
}

上述代码仅仅能够生成语法树,或者检查语法正确性(ANTLR能够自动报告语法错误)。我们还需要对输入的语句进行另外的处理,例如能够将short数组初始化语句转换为String对象,这时候就可以使用ANTLR内置的语法分析树遍历器进行深度优先遍历,然后在它触发的一系列回调函数中进行适当的操作。

4.3 使用监听器操纵输入的数据

在语法规则对应的语句的开始和结束位置处,监听器方法可以得到通知,所以我们只要继承监听器,然后实现我们感兴趣的方法。

例如short数组 {99, 3, 451}
翻译成 String形式 “\u0063 \u0003 \u01c3”
翻译过程就是:
将 { 翻译为 "
将 } 翻译为 "
将每个整数翻译为四位的十六进制形式然后加前缀 \u

遵循我们的翻译规则的监听器实现类为:

public class ShortToUnicodeString extends ArrayInitBaseListener{
    // 将 { 翻译为 "
    @Override
    public void enterInit(ArrayInitParser.InitContext ctx) {
        System.out.println('"');
    }
    // 将 } 翻译为 "
    @Override
    public void exitInit(ArrayInitParser.InitContext ctx) {
        System.out.println('"');
    }
    // 将每个整数翻译为4位的十六进制形式,然后加前缀 \u
    @Override
    public void enterValue(ArrayInitParser.ValueContext ctx) {
        // 假定不存在嵌套结构
        int value = Integer.valueOf(ctx.INT().getText());
        System.out.printf("\\u%04x", value);
    }
}

实现完监听器后,我们要应用于主程序
主程序中 Step1 ~ Step5 与 4.2 相同,监听器在主程序中的使用如下

// 7. 新建一个通用的、能够触发回调函数的语法分析遍历器
ParseTreeWalker walker = new ParseTreeWalker();
// 8. 遍历语法分析过程中生成语法分析树,触发回调
walker.walk(new ShortToUnicodeString(), tree);
4.4 增强版语法

案例:计算器的实现(只允许基本的算数操作符: 加减乘除、圆括号、整数以及变量)

本语言的全部特性:一个语句可以是一个表达式、一个赋值语句或者是一个空行

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

ANTLR语法:

grammar Expr;
// 起始规则,语法分析的七点
prog: stat+ ;
stat: expr NEWLINE
    | ID '=' expr NEWLINE   // 使用 | 来分隔同一个语言规则的若干备选分支
    | NEWLINE
    ;
expr: expr ('*'|'/') expr   // 使用圆括号把一些符号组合成子规则
    | expr ('+'|'-') expr
    | INT
    | ID
    | '(' expr ')'
    ;
ID : [a-zA-Z]+ ;
INT: [0-9] + ;
NEWLINE: '\r'?'\n' ;
WS : [ \t]+ -> skip;

以上案例中语法都比较简单,但是语法比较复杂,有成千上万条的情况下,我们要如何处理呢?
跟在软件开发中一样,我们可以将语法拆分成逻辑单元,然后采用语法导入的方式将它们结合起来。
例如:将上面语法拆分成语法分析器的语法和词法分析器的语法

词法规则:

lexer grammar CommonLexerRules; // 注意是 lexer grammar
ID : [a-zA-Z]+ ;
INT: [0-9]+ ;
NEWLINE:'\r'?'\n';
WS : [ \t]+ -> skip ; 

语法规则:

grammar LibExpr;
import CommonLexerRules;
prog: stat+ ;
stat: expr NEWLINE
    | ID '=' expr NEWLINE
    | NEWLINE
    ;
expr: expr ('*'|'/') expr
    | expr ('+'|'-') expr
    | INT
    | ID
    | '(' expr ')'
    ;

一般情况下,在ANTLR自带的遍历器(监听器和访问器)中,ANTLR会为每条规则生成一个方法,但是在规则下有很多备选分支的情况下,很多情况下我们会希望为每个备选分支生成不同的方法,这样对每种输入就都可以有不同的实现。这时候我们可以为每个备选分支加上标签,语法修改如下:

grammar LibExpr;
import CommonLexerRules;
prog: stat+ ;
stat: expr NEWLINE                  # printExpr
    | ID '=' expr NEWLINE           # assign
    | NEWLINE                       # blank
    ;
expr: expr op=('*'|'/') expr        # MulDiv
    | expr op=('+'|'-') expr        # AddSub
    | INT                           # int
    | ID                            # id
    | '(' expr ')'                  # parens
    ;

// 为运算符这样词法符号定义一些名字
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
4.5 访问器的使用

写一个类继承ANTLR自动生成的空访问器接口实现类,重写其中我们感兴趣的方法:

package demo.calculate;

import java.util.HashMap;
import java.util.Map;

public class EvalVisitor extends LibExprBaseVisitor<Integer> {
    // 存放变量名和变量值的对应关系
    Map<String, Integer> memory = new HashMap<String, Integer>();

    // ID '=' expr NEWLINE
    @Override
    public Integer visitAssign(LibExprParser.AssignContext ctx) {
        String id = ctx.ID().getText();
        int value = visit(ctx.expr());
        memory.put(id, value);
        return value;
    }

    // expr NEWLINE
    @Override
    public Integer visitPrintExpr(LibExprParser.PrintExprContext ctx) {
        Integer value = visit(ctx.expr());
        System.out.println(value);
        return 0;   // 返回一个假值
    }

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

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

    // expr ('*'|'/') expr
    @Override
    public Integer visitMulDiv(LibExprParser.MulDivContext ctx) {
        int left = visit(ctx.expr(0));
        int right = visit(ctx.expr(1));
        return ctx.op.getType() == LibExprParser.MUL ? left*right : left/right;
    }

    // expr op=('+'|'-') expr
    @Override
    public Integer visitAddSub(LibExprParser.AddSubContext ctx) {
        int left = visit(ctx.expr(0));
        int right = visit(ctx.expr(1));
        return ctx.op.getType() == LibExprParser.ADD ? left + right : left - right;
    }

    // '(' expr ')'
    @Override
    public Integer visitParens(LibExprParser.ParensContext ctx) {
        return visit(ctx.expr());
    }
}
4.6 在语法中嵌入动作

案例:从文本文件中提取特定的列
语法文件如下:

grammar Rows;
// 自定义构造器以便能传入希望提取的列号(从1开始计数)
@parser::members {   // 在生成的RowsParser中添加一些成员
    int col;
    public RowsParser(TokenStream input, int col) {
        this(input);
        this.col = col;
    }
}

// 匹配输入文件的语法
file : (row NL)+ ;
row
locals [int i=0]
    : (  STUFF
         {
         $i++;
         if ( $i == col ) System.out.println($STUFF.text);
         }
    )+
  ;

TAB   : '\t' -> skip ;  // 匹配但是不将其传递给语法分析器
NL    : '\r'?'\n' ;     // 匹配并将其传递给语法分析器
STUFF : ~[\t\r\n]+ ;    // 匹配除tab符合换行符之外的任何字符

java整合代码如下:

ANTLRInputStream input = new ANTLRInputStream(System.in);
RowsLexer lexer = new RowsLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
int col = Integer.valueOf(args[0]);
RowsParser parser = new RowsParser(tokens, col); // 传递序列号作为参数
parser.setBuildParseTree(false);   // 不需要浪费时间建立语法分析树
parser.file(); // 开始语法分析
5. 小结

以上主要通过案例对如何使用ANTLR工具进行了简要的说明,接下来对语法文件定义的具体规则进行具体学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值