Antlr4.7学习笔记——小型计算器实现

如何安装

由于是在MAC OS 下面,所以跟着官网的教程,直接copy5行代码搞定

$ cd /usr/local/lib
$ sudo curl -O http://www.antlr.org/download/antlr-4.7-complete.jar
$ export CLASSPATH=".:/usr/local/lib/antlr-4.7-complete.jar:$CLASSPATH"
$ alias antlr4='java -jar /usr/local/lib/antlr-4.7-complete.jar'
$ alias grun='java org.antlr.v4.gui.TestRig'

但是经历多了,就会发现

vi ~/.bash_profile

把与环境相关的内容都copy进来,这样的话重启计算机后仍能生效

[esc]
:wq

保存退出
source ~/.bash_profile
更新环境变量。

好了,现在就可以进行初步的操作了。

grun

这个命令的基本格式为
grun xxx.g4 __garmmar_begin [参数] [资源文件]+

其中xxx.g4为语法文件

__garmmar_begin 为语法开始内容

首先列出一些比较重要的参数:

  • -tokens 打印出记号流
  • -tree 以LISP风格的文本形式打印出语法分析树
  • -gui 在对话框中可视化地显示出语法分析树。
  • -ps file.ps 在PostScript中生成一个可视化的语法分析树表示,并把它存储在file.ps文件中
  • -encoding _name 指定输入文件编码
  • -trace 在进入/退出规则前打印规则名称和当前记号
  • -diagnostics分析时打开诊断信息。此生成消息仅用于异常情况,如二义性
  • -SLL使用更快但稍弱的分析策略

-tokens

-tree

-gui

-ps

这个功能就我发现双击这个ps文件后会出一个pdf文件。

其他参数由于暂时涉及不到,就暂时没有尝试。

二义性

在处理二义性方面的问题时。ANTLR通过选择涉及决定的第一个选项来解决二义性。

并且在面对下面的问题时,会“智能地选择合理结果”

BEGIN : 'begin';
ID : [a-zA-Z]+;

在遇到begin时,会用第一个规则进行匹配,如果遇到了类似beging、abegin这样的都会用第二个规则进行匹配。

Visitor和Listener

ANTLR在它的运行库中为两种树遍历机制提供支持。默认下ANTLR生成语法分析树和Listener接口,并在其中定义了回调方法,用于响应被内建的树遍历器的触发。

在Listener和Visitor机制之间最大的不同是:Listener方法被ANTLR提供的遍历器对象调用; 而Visitor方法必须显式的调用visit方法遍历它们的子节点,在一个节点的子节点上如果忘记调 用visit方法就意味着那些子树没有得到访问。

在这次学习中,是用Visitor实现了一个计算器。首先上计算器的语法:

grammar Calc;

prog : stat+;

stat : expr             # printExpr
     | ID '=' expr      # assign
     | 'print(' ID ')'  # print
     ;

expr : <assoc=right> expr '^' expr # power
     | expr op=(MUL|DIV) expr   # MulDiv
     | expr op=(ADD|SUB) expr   # AddSub
     | sign=(ADD|SUB)?NUMBER       # number
     | ID                       # id
     | '(' expr ')'             # parens
     ;


ID   : [a-zA-Z]+;
NUMBER  : [0-9]+('.'([0-9]+)?)?
        | [0-9]+;
COMMENT : '/*' .*? '*/' -> skip;
LINE_COMMENT : '//' .*? '\r'? '\n' -> skip;
WS   : [ \t\r\n]+ -> skip;
MUL  : '*';
DIV  : '/';
ADD  : '+';
SUB  : '-';

我觉得在这个文法中有一些细节值得强调,一个是运算符的优先级,第二个是 #号后面的东西有什么用,最后就是<assoc=right>这个东西。

运算符优先级类如加减乘除这些基本法则在Antlr中已经自动帮你处理,就参考我上面写的expr,有时候并不意味着你这样写

....
|	expr op=(MUL|DIV) expr

|	expr op=(ADD|SUB) expr
....

就可以让乘除先于加减,它与怎样排列无关。

但是,Antlr会把我们的目标脚本,解析生一棵抽象语法树,越是靠近叶子节点的地方,结合优先级越高,越是靠近根节点的地方,结合优先级越低。

在就上面的expr来谈,

...
| ID
| '(' expr ')'
...

他们的优先级要高于加减乘除运算

#号有什么用呢?

在Visitor模式中,它会给你生成一些visit方法,方便你的编程。

<assoc=right>有什么用呢?

在默认情况下,Antlr是默认从左向右结合运算符,然而像指数群这样的运算符则是要从右向左,因此我们必须使用assoc手动指定运算符,这样就能把234解释成2(34)。

下面上一下自己写的EvalVisitor类。


import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;

public class EvalVisitor extends CalcBaseVisitor<Double> {
    Map<String, Double> memory = new HashMap<String, Double>();

    //id = expr
    @Override
    public Double visitAssign(CalcParser.AssignContext ctx){
        String id = ctx.ID().getText();
        Double value = visit(ctx.expr());
        memory.put(id, value);
        return value;
    }

    // expr
    @Override
    public Double visitPrintExpr(CalcParser.PrintExprContext ctx) {
        Double value = visit(ctx.expr());
        //保留两位有数字的方法
        DecimalFormat df = new DecimalFormat("#.##");
        String s_value = df.format(value);
        System.out.println(s_value);
        return 0.0;
    }

    //print
    @Override
    public Double visitPrint(CalcParser.PrintContext ctx){
        String id = ctx.ID().getText();
        Double value=0.0;
        if(memory.containsKey(id)) value = memory.get(id);
        DecimalFormat df = new DecimalFormat("#.##");
        String s_value = df.format(value);
        System.out.println(s_value);
        return value;

    }

    //Number
    @Override
    public Double  visitNumber(CalcParser.NumberContext ctx){
        int size = ctx.getChildCount();
        if(size == 2){
            if(ctx.sign.getType() == CalcParser.SUB){
                return -1 *  Double.valueOf(ctx.getChild(1).getText());
            }else{
                return Double.valueOf(ctx.getChild(1).getText());
            }
        }else{
            return Double.valueOf(ctx.getChild(0).getText());
        }
    }

    //ID
    @Override
    public Double visitId(CalcParser.IdContext ctx){
        String id = ctx.ID().getText();
        if(memory.containsKey(id)) return memory.get(id);
        return 0.0;
    }

    //expr op=('*'|'/') expr
    @Override
    public Double visitMulDiv(CalcParser.MulDivContext ctx)  {
        Double left = visit(ctx.expr(0));
        Double right = visit(ctx.expr(1));

        if(ctx.op.getType() == CalcParser.MUL){
            return left * right;
        }else{
            if(right == 0 || right == 0.0){
                System.out.println("Divisor can not be zero");
                return 0.0;
            }else{
                return left / right;
            }
        }
    }

    //expr op=('+'|'-') expr
    @Override
    public Double visitAddSub(CalcParser.AddSubContext ctx){
        Double left = visit(ctx.expr(0));
        Double right = visit(ctx.expr(1));
        if(ctx.op.getType() == CalcParser.ADD)
            return left + right;
        return left - right;
    }

    // '(' expr ')'
    @Override
    public Double visitParens(CalcParser.ParensContext ctx){
        return visit(ctx.expr());
    }

    // '^'
    @Override
    public Double visitPower(CalcParser.PowerContext ctx){
        Double base = visit(ctx.expr(0));
        Double exponet = visit(ctx.expr(1));
        return Math.pow(base, exponet);
    }
}

还有Calc.java类,为开始类


import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

import java.io.InputStream;


public class Calc {
    public static void main(String[] args) throws Exception {
        CharStream input;
        if(args.length == 1) {
            String fileName = String.valueOf(args[0]);
            input = CharStreams.fromFileName(fileName);
        }else if(args.length > 1 || args.length < 0){
            throw  new Exception("the number of arguments is false, Please only give the source file or nothing and then you input your text");
        }else {
            InputStream is = System.in;
            input = CharStreams.fromStream(is);
        }
        CalcLexer lexer = new CalcLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CalcParser parser = new CalcParser(tokens);
        ParseTree tree = parser.prog();
        EvalVisitor eval = new EvalVisitor();
        eval.visit(tree);
        System.out.println(tree.toStringTree(parser));
    }
}

最后通过下面的命令便可以运行

antlr4 -no-listener -visitor Calc.g4
javac *.java
java Calc 或 java Calc calc.txt
grun Calc prog -gui calc.txt #可看生成树

具体代码及相关学习书籍在这摸我

地址: https://github.com/wojiaxiaoyueyue/Antlr4.7-Calculator

posted @ 2017-09-19 10:05  一棵球和一枝猪  阅读(5148)  评论(1编辑  收藏  举报