antlr4读书笔记

antlr4读书笔记

在第一周的学习中主要阅读了《Pragmatic.The Definitive ANTLR 4 Reference.2013》的一到九章,《Language Implementation Patterns》的六到八章,查询了各种网络教程并最终实现了一个简单的计算器,计算器包括简单的赋值操作,print操作,局部变量,错误处理,自定义错误信息等,下面主要记录一些在实现计算器的过程中学习到的关键点。

1-antlr4的安装与配置

(1)在使用intellij+antlr4时,只需要在settings中的plugin里搜索antlr v4并安装即可。

antlr4读书笔记_第1张图片

(2)在intellij中可以很方便的进行语法树的查看和自定义语法的检查,创建.g4文件并输入antlr4语法,可以通过点击鼠标右键选择test rule对特定的语法规则进行测试

antlr4读书笔记_第2张图片

(3)但是仅仅安装intellij的antlr4是不够的,所以需要对电脑环境进行配置,将antlr4加入到环境变量中,并添加命令行命令,方便后面的调试。下面是添加的步骤。

  • 下载antlr4的jar包,将它保存到一个系统下的文件夹中,我保存在C:\javalib\antlr-4.5.3-complete.jar
  • 在环境变量中CLASSPATH后面加C:\javalib\antlr-4.5.3-complete.jar;
  • 在环境变量中PATH后面加C:\javalib;
  • 在C:\javalib中添加两个文件antlr4.txt和grun.txt
  • antlr4.txt中写入java org.antlr.v4.Tool %*
  • grun中写入java org.antlr.v4.runtime.misc.TestRig %*
  • 然后将两个文件后缀改为.bat,打开命令行,输入antlr4看是否配置成功

2-命令行中antlr4常用命令

(1)antlr4 -visitor -no-listener Test.g4(生成visitor而不生成listener),-o选项指定输出位置,-lib选项指定 .tokens文件输出位置。

(2)$ grun规则如下:
java org.antlr.v4.runtime.misc.TestRig GrammarName startRuleName
[-tokens] [-tree] [-gui] [-ps file.ps] [-encoding encodingname]
[-trace] [-diagnostics] [-SLL]
[input-filename(s)]

3-g4语法

(1)采用与java类似的注释方式
(2)TOKEN均采用开头字母大写,语法规则均采用开头字母小写
(3)在单引号中写字符串,使用\表示转义,大致的词法规则与正则表达式类似,很容易上手
(4)antlr的关键字有:import, fragment, lexer, parser, grammar, returns, locals, throws, catch, finally, mode, options, tokens
(5)使用#为可选规则标记名称

grammar AltLabels;
stat: 'return' e ';' # Return
    | 'break' ';'    # Break
    ;
e : e '*' e # Mult
  | e '+' e # Add
  | INT     # Int
  ;

生成的listener如下:

public interface AltLabelsListener extends ParseTreeListener {
    void enterMult(AltLabelsParser.MultContext ctx);
    void exitMult(AltLabelsParser.MultContext ctx);
    void enterBreak(AltLabelsParser.BreakContext ctx);
    void exitBreak(AltLabelsParser.BreakContext ctx);
    void enterReturn(AltLabelsParser.ReturnContext ctx);
    void exitReturn(AltLabelsParser.ReturnContext ctx);
    void enterAdd(AltLabelsParser.AddContext ctx);
    void exitAdd(AltLabelsParser.AddContext ctx);
    void enterInt(AltLabelsParser.IntContext ctx);
    void exitInt(AltLabelsParser.IntContext ctx);
}

(6)antlr4中可以处理直接左递归的情况,但无法处理具有间接左递归的情况,因此在制定语法时应当注意。
(7)antlr在处理具有二义性的文法时,优先采用写在前面的文法,如

expr:   expr '*'|'/' expr      # MulDiv
    |   expr '+'|'-' expr      # AddSub

对于1+2*3,优先匹配2*3,再匹配1+(2*3)。

一个额外的例子,可以在g4中写java语言,添加局部变量等,但我还不是很会用:

grammar Count;
@header {
    package foo;
}
@members {
    int count = 0; //local variable
}
list
@after {System.out.println(count+" ints");}
: INT {count++;} (',' INT {count++;} )*
;
INT : [0-9]+ ;
WS : [ \r\t\n]+ -> skip ;

4-visitor与listener

在通过#标记语法名称后,使用命令行默认生成带listener的java文件。要使得程序中能够正常的传递值和各种信息,需要继承Listener或Visitor并实现其中的部分方法。《Pragmatic.The Definitive ANTLR 4 Reference.2013》中第七章提到了三种方法,以实现语法树的遍历和值传递。

a.使用visitor,将值以函数返回值的形式层层传递

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());
    }
}

b.使用listener,用栈代替返回值

public static class Evaluator extends LExprBaseListener {
    Stack stack = new Stack();
    public void exitMult(LExprParser.MultContext ctx) {
        int right = stack.pop();
        int left = stack.pop();
        stack.push( left * right );
    }
    public void exitAdd(LExprParser.AddContext ctx) {
        int right = stack.pop();
        int left = stack.pop();
        stack.push(left + right);
    }
    public void exitInt(LExprParser.IntContext ctx) {
        stack.push( Integer.valueOf(ctx.INT().getText()) );
    }
}

c.使用listener和antlr中的ParseTreeProperty,将值存储在语法树的节点上

public static class EvaluatorWithProps extends LExprBaseListener {
    /** maps nodes to integers with Map */
    ParseTreeProperty values = new ParseTreeProperty();

    /** Need to pass e's value out of rule s : e ; */
    public void exitS(LExprParser.SContext ctx) {
        setValue(ctx, getValue(ctx.e())); // like: int s() { return e(); }
    }

    public void exitMult(LExprParser.MultContext ctx) {
        int left = getValue(ctx.e(0));  // e '*' e   # Mult
        int right = getValue(ctx.e(1));
        setValue(ctx, left * right);
    }

    public void exitAdd(LExprParser.AddContext ctx) {
        int left = getValue(ctx.e(0)); // e '+' e   # Add
        int right = getValue(ctx.e(1));
        setValue(ctx, left + right);
    }

    public void exitInt(LExprParser.IntContext ctx) {
        String intText = ctx.INT().getText(); // INT   # Int
        setValue(ctx, Integer.valueOf(intText));
    }

    public void setValue(ParseTree node, int value) { values.put(node, value); }
    public int getValue(ParseTree node) { return values.get(node); }
}

在实现计算器的过程中,我采用的是第三种方案,因为需要添加局部变量,所以使用listener更为方便,其次利用树节点存储值会更容易思考和实现。

5-局部变量

在《Pragmatic.The Definitive ANTLR 4 Reference.2013》的第八章和《Language Implementation Patterns》的六到八章中都分别提到了如何使用Symbol和Scope实现局部变量,总体来说,每个block(也就是大括号包起来的)就是一个Scope,每个Scope,除了GlobalScope外,都可以获取上一层的Scope,也就是enclosingScope,在每个Scope中定义各种变量函数,称作Symbol,并将这些Symbol以HashMap的形式存储起来。在enterBlock的时候,就新建一个Scope,并将原来的Scope赋值给enclosingScope,在exitBlock的时候就将Scope还原。

为了实现计算器和局部变量的结合,同时采用了ParseTreeProperty存储节点值,我在每个Scope中都加入了一个memory和一个ParseTreeProperty,memory用于存储变量和变量值,ParseTreeProperty则用于存储这个块中语法树结构以及节点值。

6-错误处理

《Pragmatic.The Definitive ANTLR 4 Reference.2013》的第九章主要讲了如何更改报错信息以及错误恢复的一些问题,修改错误信息需要继承BaseErrorListener类,并重写syntaxError方法,然后在parser中移除掉默认的ErrorListener并添加自定义的Listener

public static class VerboseListener extends BaseErrorListener {
    @Override
    public void syntaxError(Recognizer recognizer,
                            Object offendingSymbol,
                            int line, int charPositionInLine,
                            String msg,
                            RecognitionException e)
    {
    List stack = ((Parser)recognizer).getRuleInvocationStack();
    Collections.reverse(stack);
    System.err.println("rule stack: "+stack);
    System.err.println("line "+line+":"+charPositionInLine+" at "+
                        offendingSymbol+": "+msg);
    }
}
    parser.removeErrorListeners(); // remove ConsoleErrorListener
    parser.addErrorListener(new UnderlineListener()); // add ours

为了更接近于平时用的编译器,我没有将报错信息直接输出,而是将所有的报错信息字符串存储到ErrorCollector中,而将所有打印的信息保存在另一个Collector中,保证错误信息和打印信息不会同时出现。

在设置ErrorListener之后,要想自定义报错,就不能采用以往的直接throw Exception的方式了,因为这样会直接使程序中断,而且打印的错误信息也很难看。在第九章最后提到了一些如何使用报错并且自定义错误恢复的方式,主要是通过继承DefaultErrorStrategy类,重写report和recover方法,自定义错误处理的策略,查看antlr的类DefaultErrorStrategy:

public void reportError(Parser recognizer, RecognitionException e) {
        if(!this.inErrorRecoveryMode(recognizer)) {
            this.beginErrorCondition(recognizer);
            if(e instanceof NoViableAltException) {
                this.reportNoViableAlternative(recognizer, (NoViableAltException)e);
            } else if(e instanceof InputMismatchException) {
                this.reportInputMismatch(recognizer, (InputMismatchException)e);
            } else if(e instanceof FailedPredicateException) {
                this.reportFailedPredicate(recognizer, (FailedPredicateException)e);
            } else {
                System.err.println("unknown recognition error type: " + e.getClass().getName());
                recognizer.notifyErrorListeners(e.getOffendingToken(), e.getMessage(), e);
            }
        }
    }

public void recover(Parser recognizer, RecognitionException e) {
    if(this.lastErrorIndex == recognizer.getInputStream().index() && this.lastErrorStates != null && this.lastErrorStates.contains(recognizer.getState())) {
        recognizer.consume();
    }

    this.lastErrorIndex = recognizer.getInputStream().index();
    if(this.lastErrorStates == null) {
        this.lastErrorStates = new IntervalSet(new int[0]);
    }

    this.lastErrorStates.add(recognizer.getState());
    IntervalSet followSet = this.getErrorRecoverySet(recognizer);
    this.consumeUntil(recognizer, followSet);
}

从中可以观察到,错误信息是通过notifyErrorListeners广播给ErrorListener的,因此也可以直接调用这一函数,发出报错信息,函数的几个参数分别为Token,String,RecognitionException,所以对于被除数为0的情况,我们可以直接调用函数,并执行一定的错误恢复,就可以使最后的报错信息按照我们希望的方式展示出来了。

7- 计算器实现结果

(1)测试一

antlr4读书笔记_第3张图片

antlr4读书笔记_第4张图片

(2)测试二
antlr4读书笔记_第5张图片

result

你可能感兴趣的:(编译原理)