The Definitive Antlr 4 第7章学习笔记

第7章 将文法与程序代码分离

将文法与文法处理程序混合在一起使得最终的程序不易维护,例如下面的代码。

grammar PropertyFile;
file : { ? start file ? } prop+ { ? finish file ? } ;
prop : ID ‘=‘ STRING ‘\n‘ { ? process property ? } ;
ID : [a-z]+ ;
STRING : ‘"‘ .*? ‘"‘ ;

grammar PropertyFile;
@members {
void startFile() { } // blank implementations
void finishFile() { }
void defineProperty(Token name, Token value) { }
}
file : {startFile();} prop+ {finishFile();} ;
prop : ID ‘=‘ STRING ‘\n‘ {defineProperty($ID, $STRING)} ;
ID : [a-z]+ ;
STRING : ‘"‘ .*? ‘"‘ ;

这种形式不易维护,因此将二者分离将会使程序更容易维护与扩展。在Antlr v4 中可以通过listener与visitor来完成分离的工作。

通过Parse-Tree Listeners来实现程序

为了将文法与分析程序分离,关键是通过分析器创建一颗分析树,随后遍历这颗树,并在遍历时触发相关的处理代码。这可以通过Antlr提供的树遍历机制来实现。在Antlr中可以通过内置的ParseTreeWalker来实现遍历,代码如下。

属性文件文法定义。

file : prop+ ;
prop : ID ‘=‘ STRING ‘\n‘ ;

文件内容可以是。

user="parrt"
machine="maniac"

根据文法,Antlr生成PropertyFileParser,并构建分析树,结果图1所示。

一旦得到这棵分析树,就可以通过ParseTreeWalker来访问所有结点,并在进入与退出结点时触发相应的处理方法。

接下来看一下PropertyFileParser中由Antlr根据文法文件所生成的接口。当Antlr ParseTreeWalker进入一个结点或退出一个结点时,会调用进入与退出方法。由于在属性文件文法描述中只有两个文法规则,因此最终Antlr会生成四个方法。

import org.antlr.v4.runtime.tree.*;
import org.antlr.v4.runtime.Token;
public interface PropertyFileListener extends ParseTreeListener {
void enterFile(PropertyFileParser.FileContext ctx);
void exitFile(PropertyFileParser.FileContext ctx);
void enterProp(PropertyFileParser.PropContext ctx);
void exitProp(PropertyFileParser.PropContext ctx);
}

FileContext与ProContext对象表示分析树结点,并与某个具体的规则相关联。同时这个两个对象还包含一些很有用的方法。为了方便使用,Antlr会生成PropertyFileBaseListener类,该类实现了接口中的方法,但方法体为空,具体的实现交给使用者完成。

public static class PropertyFileLoader extends PropertyFileBaseListener {
Map<String,String> props = new OrderedHashMap<String, String>();
public void exitProp(PropertyFileParser.PropContext ctx) {
String id = ctx.ID().getText(); // prop : ID ‘=‘ STRING ‘\n‘ ;
String value = ctx.STRING().getText();
props.put(id, value);
}
}

接下来看一下Antlr根据文法所生成的以及用户编写的类之间的关系。

parsetReeListener在ANTLR运行时库中,其中每个listener会对下列方法做出响应。

  • visitTerminal
  • enterEveryRule
  • exitEveryRule
  • visitErrorNode

Antlr根据文法文件生成接口文件PropertyFileListener。该文件实现了PropertyFileListener接口,并提供了默认方法的实现。

而作为使用者仅需要创建PropertyFileLoader,该类继承PropertyFileBaseListener,并提供方法的具体实现。其中方法参数能够访问规则上下文对象PropContext。该对象与规则prop相关联。这个上下文对象对每个文法规则中的元素都有一个处理方法(对于prop来说是ID和STRING)。ID,STRING都是对终结符的引用。我们可以直接通过getText访问终结符中的文本数据,或通过getSymbol()方法。

接下来遍历分析树。并通过所实现的PropertyFileLoader来完成结点监听,触发处理方法。

// 创建分析树遍历器
ParseTreeWalker walker = new ParseTreeWalker();
// 创建监听器,并提供给遍历器
PropertyFileLoader loader = new PropertyFileLoader();
walker.walk(loader, tree); // walk parse tree
System.out.println(loader.props); // print results

基于Visitor的实现方法

为了生成基于访问者模式的代码,需要指定-visitor选项。Antlr会生成PropertyFileVisitor接口和PropertyFileBaseVisitor类,该类带有下列默认实现的方法。

public class PropertyFileBaseVisitor<T> extends AbstractParseTreeVisitor<T>
implements PropertyFileVisitor<T>
{
@Override public T visitFile(PropertyFileParser.FileContext ctx) { ... }
@Override public T visitProp(PropertyFileParser.PropContext ctx) { ... }
}

下面是访问者模式中设计到的类之间的关系。

Visitors 通过调用接口ParseTreeVisitor的visit方法来遍历分析树。该方法在AbstractParseTreeVisitor类中实现。而visitor与listener的一个重要区别是,visitor不需要创建ParseTreeWalker来遍历分析树,而是使用visitor方法来遍历。

PropertyFileVisitor loader = new PropertyFileVisitor();
loader.visit(tree);
System.out.println(loader.props); // print results

为规则加标签实现精确处理

例如有文法

grammar Expr;
s : e;
e : e op=MULT e // MULT is ‘*‘
  | e op=ADD e // ADD is ‘+‘
  | INT
;

生成的listener如下。

public interface ExprListener extends ParseTreeListener {
void enterE(ExprParser.EContext ctx);
void exitE(ExprParser.EContext ctx);
...
}

由于在文法规则中有多个规则e,(e:op=MUL ,e op=ADD),为了在exitE中判断当前到底是离开哪个规则e,可以使用op标识符标签以及ctx(上下文对象)的方法,代码如下。

public void exitE(ExprParser.EContext ctx) {
    if ( ctx.getChildCount()==3 ) {
       // operations have 3   children
        int left = values.get(ctx.e(0));
        int right = values.get(ctx.e(1));
        if ( ctx.op.getType()==ExprParser.MULT ) {
            values.put(ctx, left * right);
        }
        else {
            values.put(ctx, left + right);
        }
    }
    else {
        values.put(ctx, values.get(ctx.getChild(0))); // an INT
    }
}

代码中通过op判断类型,最终进行正确的计算。exitE()方法中的MULT 是有ANTLR生成,并放在ExprParser中。

exitE()方法中的MULT 是有ANTLR生成,并放在ExprParser中。
public class ExprParser extends Parser {
public static final int MULT=1, ADD=2, INT=3, WS=4;
...
}

为了得到更精确的监听器事件,Antlr 提供#来在文法规则最左侧为文法加标签。

e : e MULT e # Mult
   | e ADD e # Add
   |     INT # Int
;

加上标签后,Antlr会为每个规则e生成一个监听方法。这样就不在需要op 标示符标签了。

public interface LExprListener extends ParseTreeListener {
void enterMult(LExprParser.MultContext ctx);
void exitMult(LExprParser.MultContext ctx);
void enterAdd(LExprParser.AddContext ctx);
void exitAdd(LExprParser.AddContext ctx);
void enterInt(LExprParser.IntContext ctx);
void exitInt(LExprParser.IntContext ctx);
...
}

在事件方法中共享信息

无论收集还是计算数据,都会传递参数或返回值。现在的问题是,Antlr会自动生成监听器方法,这些方法没有具体的返回值与参数。而生成visitor方法,该方法也没有具体的参数。因此,本节将学习利用一些机制让事件方法传递数据,而不用修改事件方法签名。接下来以计算器为例,提供三种不同的计算器的实现。第一种方法是使用visitor方法返回值,第二种是使用栈,第三种是注解分析树结点来存储感兴趣的数据。

通过Visitors来遍历分析树

构建基于visitor的计算器的最简单方法是让事件方法与返回子表达式值的规则关联。例如visitAdd会返回两个子表达式相加的结果。visitInt()将会返回一个整数值。传统的visitor并不会为其visit方法指定返回值。为了返回数据,为我们所实现的类中的方法添加返回值。

public static class EvalVisitor extends LExprBaseVisitor<Integer> {
    public Integer visitMult(LExprParser.MultContext ctx) {
        return visit(ctx.e(0)) * visit(ctx.e(1));
    }
    public Integer visitAdd(LExprParser.AddContext ctx) {
        return visit(ctx.e(0)) + visit(ctx.e(1));
    }
    public Integer visitInt(LExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }
}

EvalVisitor从Antlr的AbstractParseTreeVisitor类继承了visit()方法,visitor通过这个方法触发对子树的遍历。

通过栈存储返回值

因为Antlr生成的监听器时间方法没有返回值,所以要在不同的方法中用到返回值,可以通过栈来存储。在存储时一定要注意参数的顺序,以保证计算结果的正确,完整演示代码如下。

public class Evaluator extends LExprBaseListener{
    Stack<Integer> stack = new Stack<Integer>();

    @Override
    public void exitMult(MultContext ctx) {
        int right = stack.pop();
        int left = stack.pop();
        stack.push(right * left);
        System.out.println(stack.peek());
    }

    @Override
    public void exitAdd(AddContext ctx) {
        int right = stack.pop();
        int left = stack.pop();
        stack.push(right + left);
        System.out.println(stack.peek());
    }

    @Override
    public void exitInt(IntContext ctx) {
        stack.push(Integer.valueOf(ctx.INT().getText()));
        System.out.println(stack.peek());
    }
}
public class Main {

    public static void main(String[] args) {
        String exp = "1+2*3+4";
        ANTLRInputStream inputStream = new ANTLRInputStream(exp);
        LExprLexer lexer = new LExprLexer(inputStream);
        CommonTokenStream tk = new CommonTokenStream(lexer);
        LExprParser parser = new LExprParser(tk);
        Evaluator evaluator = new Evaluator();
        ParseTreeWalker walker = new ParseTreeWalker();
        walker.walk(evaluator, parser.e());
    }
}

程序最终打印出表达式的值11。

注释分析树

除了通过栈,返回值保存临时数据,也可以将临时结果存放在分析树的结点上。如下图是表达式1+2*3的注释分析树。图中箭头所指数字为结点所存储的临时结果。

以文法LExpr.g4 为例。

LExpr.g4
e : e MULT e # Mult
| e ADD e # Add
| INT # Int
;

最简单的为分析树结点添加数据的方法是使用Map。Antlr提供了一个名为ParseTreeProperty简单的帮助类,其中ParseTreeProperty的代码如下。

public class ParseTreeProperty<V> {
     protected Map<ParseTree, V> annotations = new IdentityHashMap<ParseTree, V>();

    public V get(ParseTree node) {
        return annotations.get(node);
    }

    public void put(ParseTree node, V value) {
         annotations.put(node, value);
    }

    public V removeFrom(ParseTree node) {
        return annotations.remove(node);
    }
}

如过要使用自定义Map,确保自定义Map是继承于IdentityHashMap,而不是HashMap,而结点相等判断则通过identity方法完成。两个结点可能相等,但在内存中可能并不是同一个物理结点。随后使用put,get方法添加临时数据。使用ParseTreeProperty实现的表达式计算代码如下。

public class Evaluator extends LExprBaseListener{
    public ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();

    @Override
    public void exitS(SContext ctx) {
        values.put(ctx, values.get(ctx.e()));
        System.out.println(values.get(ctx));
    }

    @Override
    public void exitMult(MultContext ctx) {
        int left = values.get(ctx.e(0));
        int right = values.get(ctx.e(1));
        values.put(ctx, left * right);
        System.out.println(left * right);
    }

    @Override
    public void exitAdd(AddContext ctx) {
        int left = values.get(ctx.e(0));
        int right = values.get(ctx.e(1));
        values.put(ctx, left + right);
        System.out.println(left + right);
    }

    @Override
    public void exitInt(IntContext ctx) {
        String intText = ctx.INT().getText();
        values.put(ctx, Integer.valueOf(intText));
    }
}

public class Main {

    public static void main(String[] args) {
        String exp = "1+2*3+4";
        ANTLRInputStream inputStream = new ANTLRInputStream(exp);
        LExprLexer lexer = new LExprLexer(inputStream);
        CommonTokenStream tk = new CommonTokenStream(lexer);
        LExprParser parser = new LExprParser(tk);
        Evaluator evaluator = new Evaluator();
        ParseTreeWalker walker = new ParseTreeWalker();
        SContext s = parser.s();
        walker.walk(evaluator, s);
        System.out.println(evaluator.values.get(s));
    }
}

前文共提到三种方法,Visitor下方法的返回值、使用栈存储数据及使用Map存储数据。在具体应用的过程各种可以根据实际需求来选择,或结合使用。

时间: 2024-11-13 05:57:57

The Definitive Antlr 4 第7章学习笔记的相关文章

The Definitive Antlr 4 第8章学习笔记

第8章介绍了四个例子,讲述了Antlr了实际应用.下面的阅读笔记中,最终实现与书中并非完全一致.其中调用关系仅输出关系,而未转换为Dot语言. 加载CSV数据 CSV是逗号分隔值的缩写,其形式为. Details,Month,Amount Mid Bonus,June,"$2,000" ,January,"""zippo""" Total Bonuses,"","$5,000" 接下来将

【tapestry3笔记】--tapestry 初探,《 tapestry in action 》第一章学习笔记

由于要维护一个项目,要用到tapestry3这个老框架,虽然这个框架很老,但是在我看来ta的思想还是很先进的---面向组件编程. 由于网上资料少的可怜,辛苦找了很久终于找到一本名为<tapestry in action>的工具书,以下学习笔记均以此书为参考. 正文---tapestry初探 tapestry in action 第一章学习笔记 tapestry是一款以组件为核心的开发框架,组件就向一个黑盒子,我们无需关系组件是如何实现的,只需合理使用即可.这有点像jquery的插件,我们无需关

《Linux内核设计与实现》第一、二章学习笔记

<Linux内核设计与实现>第一.二章学习笔记 姓名:王玮怡  学号:20135116 第一章 Linux内核简介 一.关于Unix ——一个支持抢占式多任务.多线程.虚拟内存.换页.动态链接和TCP/IP网络的现代化操作系统 1.主要发展过程   1969年,贝尔实验室的程序员们设计了一个文件系统原型,最终发展演化成了Unix 1971年,Unix被移植到PDP-11型机中 1973年,整个Unix系统使用C语言进行重写,为后来Unix系统的广泛移植铺平了道路 Unix第六版(V6)被贝尔实

安卓权威编程指南 - 第五章学习笔记(两个Activity)

学习安卓编程权威指南第五章的时候自己写了个简单的Demo来加深理解两个Activity互相传递数据的问题,然后将自己的学习笔记贴上来,如有错误还请指正. IntentActivityDemo学习笔记 题目:ActivityA登录界面(用户名.密码.登陆按钮),ActivityB(Edit,返回按键:SubmitButton).A界面输入用户名和密码传到B中,B验证用户输入的用户名和密码,如果错误就返回A,并用Toast 显示用户名和密码错误:如果正确,就在第二个 activity中显示一个Edi

《HeadFirst Python》第一章学习笔记

对于Python初学者来说,舍得强烈推荐从<HeadFirst Python>开始读起,这本书当真做到了深入浅出,HeadFirst系列,本身亦是品质的保证.这本书舍得已在<Python起步:写给零编程基础的童鞋>一文中提供了下载.为了方便大家的学习,舍得特意制作了Jupyter Notebook格式的笔记,文章末尾舍得提供了笔记的下载地址. 读<HeadFirst Python>的同时,最紧要的是及时做练习,你甚至可以在快速浏览过一章后,便拿起练习来做. 做练习的时候

Python核心编程第三版第二章学习笔记

第二章 网络编程 1.学习笔记 2.课后习题 答案是按照自己理解和查阅资料来的,不保证正确性.如由错误欢迎指出,谢谢 1. 套接字:A network socket is an endpoint of a connection across a computer network,Sockets are often represented internally as simple integers, which identify which connection to use. 套接字是网络通信的

《Linux内核设计与实现》第四章学习笔记

第四章 进程调度 [学习时间:1小时45分 撰写博客时间:2小时10分钟] [学习内容:Linux的进程调度实现.抢占和上下文切换.与调度相关的系统调用] 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间.进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统. 最大限度利用处理器时间的原则:只要有可以执行的进程,那么总会有程序正在执行. 一.多任务 1.概念:多任务操作系统就是能同时并发地交互执行多个进程的操作系统,在单处理器机器上这会产生多个进程在同时运行的幻觉

《HeadFirst Python》第二章学习笔记

现在,请跟着舍得的脚步,打开<HeadFirst Python>第二章. 一章的内容其实没有多少,多练习几次就能掌握一个大概了! <HeadFirst Python>的第二章设计得很有意思.它直接从制作一个模块入手,顺带讲了模块的导入,传统的书可不会这么搞. 不过书中关于编辑器的观点略显陈旧. 最好的编辑器是什么? 别用书中推荐的Python自带IDLE,在现阶段,请使用Jupyter Notebook来进行各项练习. 等学完这本书后,你可以选择PyCharm/Eric6/Wing

Machine Learning In Action 第二章学习笔记: kNN算法

本文主要记录<Machine Learning In Action>中第二章的内容.书中以两个具体实例来介绍kNN(k nearest neighbors),分别是: 约会对象预测 手写数字识别 通过“约会对象”功能,基本能够了解到kNN算法的工作原理.“手写数字识别”与“约会对象预测”使用完全一样的算法代码,仅仅是数据集有变化. 约会对象预测 1 约会对象预测功能需求 主人公“张三”喜欢结交新朋友.“系统A”上面注册了很多类似于“张三”的用户,大家都想结交心朋友.“张三”最开始通过自己筛选的