学习笔记

Posted FserSuN

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记相关的知识,希望对你有一定的参考价值。

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

以上是关于学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

DOM探索之基础详解——学习笔记

学习笔记 链接

ReactJs学习笔记01

OpenCV 学习笔记(颜色直方图计算 calcHist)

JSP 学习笔记

vue视频学习笔记05