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所示。
图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根据文法所生成的以及用户编写的类之间的关系。
图2

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) { ... }
}

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

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存储数据。在具体应用的过程各种可以根据实际需求来选择,或结合使用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
Programmers run into parsing problems all the time. Whether it's a data format like JSON, a network protocol like SMTP, a server configuration file for Apache, a PostScript/PDF file, or a simple spreadsheet macro language--ANTLR v4 and this book will demystify the process. ANTLR v4 has been rewritten from scratch to make it easier than ever to build parsers and the language applications built on top. This completely rewritten new edition of the bestselling Definitive ANTLR Reference shows you how to take advantage of these new features. Build your own languages with ANTLR v4, using ANTLR's new advanced parsing technology. In this book, you'll learn how ANTLR automatically builds a data structure representing the input (parse tree) and generates code that can walk the tree (visitor). You can use that combination to implement data readers, language interpreters, and translators. You'll start by learning how to identify grammar patterns in language reference manuals and then slowly start building increasingly complex grammars. Next, you'll build applications based upon those grammars by walking the automatically generated parse trees. Then you'll tackle some nasty language problems by parsing files containing more than one language (such as XML, Java, and Javadoc). You'll also see how to take absolute control over parsing by embedding Java actions into the grammar. You'll learn directly from well-known parsing expert Terence Parr, the ANTLR creator and project lead. You'll master ANTLR grammar construction and learn how to build language tools using the built-in parse tree visitor mechanism. The book teaches using real-world examples and shows you how to use ANTLR to build such things as a data file reader, a JSON to XML translator, an R parser, and a Java class->interface extractor. This book is your ticket to becoming a parsing guru! What You Need: ANTLR 4.0 and above. Java development tools. Ant build system optional (needed for building ANTLR from source)
### 回答1: 《汽车以太网——权威指南》是一本全面介绍汽车以太网技术的书籍,共有42章。本书对汽车以太网的原理、架构、标准、协议、安全性、性能等方面进行了详细阐述。 在第一章中,本书介绍了汽车以太网的背景和发展动力,说明了为何汽车行业需要以太网技术以应对现代汽车日益增长的数据传输需求。 接着,第二章探讨了以太网技术的基础知识,包括以太网的历史、工作原理、帧结构等。读者可以通过这一章节对以太网有一个全面的了解。 第三章到第六章介绍了汽车以太网的多种拓扑结构,如星形、总线、环形和混合结构,并比较了它们之间的优缺点。第七章到第九章则详细分析了以太网的物理层和数据链路层。 从第十章到第十六章,本书深入讨论了汽车以太网的各种标准和协议,如IEEE 802.3、TCP/IP、UDP、IPsec等。此外,本书还提供了关于时钟同步、流量控制、故障诊断等方面的技术细节。 第十七章到第二十章全面介绍了汽车以太网的安全性,包括认证、授权、加密等方面的内容,帮助读者了解如何保护汽车以太网系统免受潜在的威胁。 最后,本书还讨论了汽车以太网的性能优化和故障排除,以及未来的发展趋势和应用场景。 总而言之,这本《汽车以太网——权威指南》是一本介绍汽车以太网技术全景的专业书籍,旨在帮助读者全面了解汽车以太网的原理、应用和安全性,同时提供详尽的技术细节和实用指导。 ### 回答2: 汽车以太网——终极指南的第42章是关于汽车以太网的部署和实施。在这一章中,我们将深入探讨汽车以太网的核心概念和原理,并讨论其在现代汽车中的应用。 首先,我们将介绍汽车以太网的基本原理。汽车以太网是一种用于在汽车内部不同电子控制单元(ECU)之间进行高速数据通信的网络技术。与传统的汽车电气线束相比,汽车以太网具有更高的数据传输速率和更低的延迟。这使得汽车以太网能够支持更复杂和高性能的汽车功能,例如高级驾驶辅助系统(ADAS)和自动驾驶技术。 接下来,我们将讨论汽车以太网的部署和实施。在汽车中实施以太网网络需要考虑到诸多因素,例如网络拓扑结构、带宽要求、安全性和可靠性。我们将介绍不同的以太网拓扑结构,例如星形、总线和组网结构,并讨论它们的优缺点以及适用的应用场景。 然后,我们将探讨汽车以太网的安全性和可靠性。汽车以太网的安全性至关重要,因为它涉及到车辆对外部攻击的防护和内部数据的保护。我们将介绍汽车以太网的安全标准和协议,例如汽车以太网联盟(OPEN)的安全架构和以太网安全协议(Ethernet Security Protocols)。 最后,我们将讨论汽车以太网的未来发展和趋势。汽车以太网作为一种新兴的技术,将继续不断发展和演进。我们将探讨在自动驾驶技术和智能交通系统方面的创新应用,并展望汽车以太网在未来的发展前景。 总之,第42章是关于汽车以太网的部署和实施,涵盖了汽车以太网的基本原理、部署架构、安全性和可靠性以及未来发展趋势。这将为读者提供一个全面的汽车以太网指南,帮助他们更好地理解和应用这一技术。 ### 回答3: 《汽车以太网——权威指南》是一本涵盖42个章节的关于汽车以太网的指南。这本指南详细介绍了汽车行业中使用以太网的相关知识和技术。首先,它解释了为什么汽车制造商选择以太网作为车载通信的关键技术。与传统的汽车通信系统相比,以太网提供了更高的数据传输速率和更强的可扩展性,可以支持更多的车载应用和功能。此外,以太网还具备高可靠性和稳定性,可以在长时间的使用和恶劣的环境条件下保持良好的性能。 接下来,指南深入介绍了汽车以太网的基本原理和网络架构。它涵盖了以太网协议的不同层次和功能,以及它们在汽车系统中的应用。这包括物理层、数据链路层、网络层和传输层。在每个层次中,指南详细讨论了相应的标准和协议,以及它们的功能和特点。 此外,指南还提供了关于汽车以太网的性能评估和测试方法的重要信息。这些方法可以用于确保以太网在车载环境中的可靠性和稳定性。它包括各种测试工具和技术,如链路质量测试、时延分析、故障排除等,以帮助汽车制造商和供应商在开发和部署汽车以太网系统时进行有效的测试和验证。 最后,指南还提供了一些关于未来发展趋势和应用领域的展望。随着智能汽车和自动驾驶技术的不断发展,汽车以太网将在未来扮演更为重要的角色。它将被广泛应用于车载通信、车辆诊断、远程控制和车载娱乐等领域。因此,指南鼓励汽车制造商和供应商积极采用以太网技术,并为其在未来的发展做好准备。 总之,《汽车以太网——权威指南》是一本全面介绍汽车以太网的权威指南,提供了关于汽车以太网的基本原理、网络架构、测试和评估方法以及未来发展趋势的全面信息,对汽车行业中的专业人士和学习者来说都是一本不可或缺的参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值