快速上手ANTLR
回顾前文:
下面通过两个实例来快速上手ANTLR。
使用Listener转换数组
完整源码见:https://github.com/bytesfly/antlr-demo/tree/main/array-init/src/main/java/com/github/bytesfly/arr
效果如下图所示:

该程序能识别输入的整数数组并将其转化为JSON格式的字符串数组。
语法规则为ArrayInit.g4,如下:
/** Grammars always start with a grammar header. This grammar is called
 *  ArrayInit and must match the filename: ArrayInit.g4
 */
grammar ArrayInit;
@header {package com.github.bytesfly.arr.antlr;}
/** A rule called init that matches comma-separated values between {...}. */
init  : '{' value (',' value)* '}' ;  // must match at least one value
/** A value can be either a nested array/struct or a simple integer (INT) */
value : init
      | INT
      ;
// parser rules start with lowercase letters, lexer rules with uppercase
INT :   [0-9]+ ;             // Define token INT as one or more digits
WS  :   [ \t\r\n]+ -> skip ; // Define whitespace rule, toss it out
我们自定义IntToStringListener.java,如下:
public class IntToStringListener extends ArrayInitBaseListener {
    private final StringBuilder builder = new StringBuilder();
    /**
     * Translate { to [
     */
    @Override
    public void enterInit(ArrayInitParser.InitContext ctx) {
        builder.append('[');
    }
    /**
     * Translate } to ]
     */
    @Override
    public void exitInit(ArrayInitParser.InitContext ctx) {
        // 去除value节点后的逗号
        builder.setLength(builder.length() - 1);
        builder.append(']');
        if (ctx.parent != null) {
            // 嵌套结构需要追加逗号,与enterValue的处理保持一致
            builder.append(',');
        }
    }
    /**
     * Translate integers to strings with ""
     */
    @Override
    public void enterValue(ArrayInitParser.ValueContext ctx) {
        TerminalNode node = ctx.INT();
        if (node != null) {
            builder.append("\"").append(node.getText()).append("\",");
        }
    }
    public String getResult() {
        return builder.toString();
    }
}
最终的转换程序为Translate.java,如下:
public class Translate {
    public static void main(String[] args) throws Exception {
        // 从键盘输入
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            String s = sc.nextLine();
            // 从字符串读取输入数据
            CharStream input = CharStreams.fromString(s);
            // 新建一个词法分析器
            ArrayInitLexer lexer = new ArrayInitLexer(input);
            // 新建一个词法符号的缓冲区,用于存储词法分析器将生成的词法符号
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            // 新建一个语法分析器,处理词法符号缓冲区中的内容
            ArrayInitParser parser = new ArrayInitParser(tokens);
            // 针对init规则,开始语法分析
            ParseTree tree = parser.init();
            // 新建一个通用的、能够触发回调函数的语法分析树遍历器
            ParseTreeWalker walker = new ParseTreeWalker();
            // 创建我们自定义的监听器
            IntToStringListener listener = new IntToStringListener();
            // 遍历语法分析过程中生成的语法分析树,触发回调
            walker.walk(listener, tree);
            // 打印转换结果
            System.out.println(listener.getResult());
        }
    }
}
监听器机制的优雅之处在于,不需要自己编写任何遍历语法分析树的代码。事实上,我们甚至都不知道ANTLR运行库是怎么遍历语法分析树、怎么调用我们的方法的。我们只知道,在语法规则对应的语句的开始和结束位置处,我们的监听器方法可以得到通知。
当然如果想知道ANTLR运行库是怎么遍历语法分析树并不困难,见org.antlr.v4.runtime.tree.ParseTreeWalker#walk():
public void walk(ParseTreeListener listener, ParseTree t) {
    if ( t instanceof ErrorNode) {
        listener.visitErrorNode((ErrorNode)t);
        return;
    }
    else if ( t instanceof TerminalNode) {
        listener.visitTerminal((TerminalNode)t);
        return;
    }
    RuleNode r = (RuleNode)t;
    enterRule(listener, r);
    int n = r.getChildCount();
    for (int i = 0; i<n; i++) {
        walk(listener, r.getChild(i));
    }
    exitRule(listener, r);
}
这段代码也非常容易懂,其实就是常规树的遍历,遍历过程中反调自定义的IntToStringListener中的实现方法。
使用Visitor构建计算器
该程序能识别赋值语句并做加减乘除四则运算,效果大致如下。
输入:
100
a = 1
b = 2
a+b*2
((2+3)*(6+1)-5) / 3
输出:
100
5
10
语法规则为Expr.g4,如下:
grammar Expr;
@header {package com.github.bytesfly.calculator.antlr;}
/** The start rule; begin parsing here. */
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 :   '*' ; // assigns token name to '*' used above in grammar
DIV :   '/' ;
ADD :   '+' ;
SUB :   '-' ;
ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace
编写一个用于处理计算逻辑的访问器EvalVisitor.java,如下:
public class EvalVisitor extends ExprBaseVisitor<Integer> {
    /**
     * 存放变量名和变量值的对应关系
     */
    private final Map<String, Integer> memory = new HashMap<>();
    /**
     * ID '=' expr NEWLINE  # assign
     */
    @Override
    public Integer visitAssign(ExprParser.AssignContext ctx) {
        // 获取变量名
        String id = ctx.ID().getText();
        // 计算表达式的值
        Integer value = visit(ctx.expr());
        // 暂存到map中
        memory.put(id, value);
        return value;
    }
    /**
     * expr NEWLINE  # printExpr
     */
    @Override
    public Integer visitPrintExpr(ExprParser.PrintExprContext ctx) {
        // 计算表达式的值
        Integer value = visit(ctx.expr());
        // 打印
        System.out.println(value);
        return 0;
    }
    /**
     * INT  # int
     */
    @Override
    public Integer visitInt(ExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }
    /**
     * ID  # id
     */
    @Override
    public Integer visitId(ExprParser.IdContext ctx) {
        String id = ctx.ID().getText();
        return memory.getOrDefault(id, 0);
    }
    /**
     * expr op=('*'|'/') expr  # MulDiv
     */
    @Override
    public Integer visitMulDiv(ExprParser.MulDivContext ctx) {
        // 计算左侧子表达式的值
        Integer left = visit(ctx.expr(0));
        // 计算右侧子表达式的值
        Integer right = visit(ctx.expr(1));
        // 根据不同的操作符做相应的运算
        if (ctx.op.getType() == ExprParser.MUL) {
            return left * right;
        } else {
            return left / right;
        }
    }
    /**
     * expr op=('+'|'-') expr  # AddSub
     */
    @Override
    public Integer visitAddSub(ExprParser.AddSubContext ctx) {
        // 计算左侧子表达式的值
        Integer left = visit(ctx.expr(0));
        // 计算右侧子表达式的值
        Integer right = visit(ctx.expr(1));
        // 根据不同的操作符做相应的运算
        if (ctx.op.getType() == ExprParser.ADD) {
            return left + right;
        } else {
            return left - right;
        }
    }
    /**
     * '(' expr ')'  # parens
     */
    @Override
    public Integer visitParens(ExprParser.ParensContext ctx) {
        // 返回子表达式的值
        return visit(ctx.expr());
    }
}
最终能识别输入表达式并做加减乘除四则运算的Calc.java,如下:
public class Calc {
    public static void main(String[] args) throws Exception {
        // 读取resources目录下example.expr表达式文件
        String s = FileUtil.readUtf8String("example.expr");
        // 从字符串读取输入数据
        CharStream input = CharStreams.fromString(s);
        // 新建一个词法分析器
        ExprLexer lexer = new ExprLexer(input);
        // 新建一个词法符号的缓冲区,用于存储词法分析器将生成的词法符号
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        // 新建一个语法分析器,处理词法符号缓冲区中的内容
        ExprParser parser = new ExprParser(tokens);
        // 针对prog规则,开始语法分析
        ParseTree tree = parser.prog();
        // 创建访问器对象
        EvalVisitor eval = new EvalVisitor();
        // 访问语法树
        eval.visit(tree);
    }
}
对整个调用过程疑惑的朋友,建议自行下载源码,打断点观察执行逻辑。
从上面的例子可以看出:使用ANTLR4语法文件独立于程序,具有编程语言中立性。
访问器机制也使得语言识别之外的工作在我们所熟悉的Java领域进行。在生成的所需的语法分析器之后,就不再需要同ANTLR语法标记打交道了。
快速上手ANTLR的更多相关文章
- 【Python五篇慢慢弹】快速上手学python
		快速上手学python 作者:白宁超 2016年10月4日19:59:39 摘要:python语言俨然不算新技术,七八年前甚至更早已有很多人研习,只是没有现在流行罢了.之所以当下如此盛行,我想肯定是多 ... 
- 快速上手Unity原生Json库
		现在新版的Unity(印象中是从5.3开始)已经提供了原生的Json库,以前一直使用LitJson,研究了一下Unity用的JsonUtility工具类的使用,发现使用还挺方便的,所以打算把项目中的J ... 
- [译]:Xamarin.Android开发入门——Hello,Android Multiscreen快速上手
		原文链接:Hello, Android Multiscreen Quickstart. 译文链接:Hello,Android Multiscreen快速上手 本部分介绍利用Xamarin.Androi ... 
- [译]:Xamarin.Android开发入门——Hello,Android快速上手
		返回索引目录 原文链接:Hello, Android_Quickstart. 译文链接:Xamarin.Android开发入门--Hello,Android快速上手 本部分介绍利用Xamarin开发A ... 
- 快速上手seajs——简单易用Seajs
		快速上手seajs——简单易用Seajs 原文 http://www.cnblogs.com/xjchenhao/p/4021775.html 主题 SeaJS 简易手册 http://yslo ... 
- Git版本控制Windows版快速上手
		说到版本控制,之前用过VSS,SVN,Git接触不久,感觉用着还行.写篇博文给大家分享一下使用Git的小经验,让大家对Git快速上手. 说白了Git就是一个控制版本的工具,其实没想象中的那么复杂,咱在 ... 
- Objective-C快速上手
		最近在开发iOS程序,这篇博文的内容是刚学习Objective-C时做的笔记,力图达到用最短的时间了解OC并使用OC.Objective-C是OS X 和 iOS平台上面的主要编程语言,它是C语言的超 ... 
- Netron开发快速上手(二):Netron序列化
		Netron是一个C#开源图形库,可以帮助开发人员开发出类似Visio的作图软件.本文继前文”Netron开发快速上手(一)“讨论如何利用Netron里的序列化功能快速保存自己开发的图形对象. 一个用 ... 
- Netron开发快速上手(一):GraphControl,Shape,Connector和Connection
		版权所有,引用请注明出处:<<http://www.cnblogs.com/dragon/p/5203663.html >> 本文所用示例下载FlowChart.zip 一个用 ... 
随机推荐
- OPPO 图数据库平台建设及业务落地
			本文首发于 OPPO 数智技术公众号,WeChat ID: OPPO_tech 1.什么是图数据库 图数据库(Graph database)是以图这种数据结构存储和查询的数据库.与其他数据库不同,关系 ... 
- React-Router基础(<HashRouter>)
			<Router>使用URL(即window.location.hash)的哈希部分来保持UI与URL同步的A. 重要说明:哈希历史记录不支持location.key或location.st ... 
- 【备忘】下载Oracle 8u202及之前的商用免付费版本JDK
			访问地址: https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html 虽然是商用免付费版本,下载仍需 ... 
- [atARC114F]Permutation Division
			由于是排列,即任意两个数字都各不相同,因此字典序最大的$q_{i}$就是将每一段的第一个数从大到小排序 接下来,考虑第一个元素,也就是每一段开头的最大值,分类讨论: 1.当$p_{1}\le k$时, ... 
- 钓鱼小技巧-XLM
			前言 XLM钓鱼不是一项新的技术,自从公开以后,网上有很多对其的分析文章,这里仅仅做一个分享和摸索记录.文章中有问题的地方还请指出. 一个简单的例子 新建一个excel表格,右键选择表,选择插入 插入 ... 
- 获取客户端Mac地址
			近期有个需求,需要获取客户端Mac地址作为白名单验证的依据.使用.net,B/S架构.先百度找了一些获取mac地址的方法, using System; using System.Collections ... 
- 快来使用Portainer让测试环境搭建飞起来吧
			Portainer是Docker的图形化管理工具,提供状态显示面板.应用模板快速部署.容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作).事件日志显示.容器控制台操作.Swarm集群和服 ... 
- UOJ 422 - 【集训队作业2018】小Z的礼物(Min-Max 容斥+轮廓线 dp)
			题面传送门 本来说要找道轮廓线 \(dp\) 的题目刷刷来着的?然后就找到了这道题. 然鹅这个题给我最大的启发反而不在轮廓线 \(dp\),而在于让我新学会了一个玩意儿叫做 Min-Max 容斥. M ... 
- python判断字符串是否为空和null
			1.使用字符串长度判断 len(s==0)则字符串为空 test1 = '' if len(test1) == 0: print('test1为空串') else: print('test非空串,te ... 
- 【机器学习与R语言】10- 关联规则
			目录 1.理解关联规则 1)基本认识 2)Apriori算法 2.关联规则应用示例 1)收集数据 2)探索和准备数据 3)训练模型 4)评估性能 5)提高模型性能 1.理解关联规则 1)基本认识 购物 ... 
