antrl4学习(三)——用idea实现简单计算器

来源:互联网 发布:淘宝网店开店流程 编辑:程序博客网 时间:2024/06/01 08:27

antrl4学习(三)——用idea实现简单计算器

    为简单起见,我们只允许基本的算术操作符(加减乘除)、圆括号、整数以及变量出现。我们的算术表达式限制浮点数的使用,只允许整数出现。

首先,新建一个Model,命名为Rules,在其中创建新文件,命名为LabeledExpr.g4

其次,设计词法规则和语法规则

词法规则:

/* 词法规则 */
ID: [a-zA-Z]+;         //匹配标识符
INT: [0-9]+;           //匹配整数
NEWLINE:'\r'?'\n';   //告诉语法分析器一个新行的开始
WS: [ \t]+ ->skip;     //丢弃空白字符

 

为了后续的计算的方便性,我们再定义一下加减乘除。

MUL:'*';
DIV:'/';
ADD:'+';
SUB:'-'

 

语法规则:

首先我们要定义表达式expr.

表达式可以是数字(INT),也可以是标识符(ID),也可以是加减乘除表达式,也可以是括号表达式。所以我们的expr的定义如下:

expr:expr op=('*'|'/')expr
    |expr op=('+'|'-')expr
    |INT
    |ID
    |'('expr')'
    ;

接着我们要定义语句stat

语句可以是表达式+换行符,可以是声明变量的语句,也可以是空行

定义如下:

stat:exprNEWLINE
    |ID'='exprNEWLINE
    |NEWLINE
    ;

最后定义我们的program.即多个语句

prog:stat+;

好了至此我们的语法和语义规则就定义完成了。

接下来为了以后的运算的方便性,我们还需要给每一步规则打上标签,标签以#”开头,出现在每一条规则的右边。打上标签后,antlr会为每一个规则都生成一个事件。(这里看不懂的暂时先记下,看到后面的代码就明白了这步的作用了)。于是我们的整个g4文件定义如下。

grammarLabeledExpr;

        /* 起始规则,语法分析的起点   */
        
prog:stat+;

        stat:exprNEWLINE             #printExpr
            |ID'='exprNEWLINE      #assign
            |NEWLINE                  #blank
            ;

        expr:expr op=('*'|'/')expr   #MulDiv
            |expr op=('+'|'-')expr   #AddSub
            |INT                      #int
            |ID                       #id
            |'('expr')'             #parens
            ;

        /* 词法规则 */
        
ID: [a-zA-Z]+;         //匹配标识符
        
INT: [0-9]+;           //匹配整数
        
NEWLINE:'\r'?'\n';   //告诉语法分析器一个新行的开始
        
WS: [ \t]+ ->skip;     //丢弃空白字符
        
MUL:'*';
        DIV:'/';
        ADD:'+';
        SUB:'-'

 

好了之后我们来验证一下语法是否正确

右击prog,选择Test Rule prog

 

在左下角的框中输入表达式,在右下角的框中会显示语法树

 

 

然后右击LabeledExpr.g4,点击configure ANTLR

 

我做了如图设置,记得选取最后面的第二框,这样会自动生成visitor的文件,之后想要手动遍历语法树的时候会用到

 

然后在右击g4文件,点击generate ANTRL recognizer,

 

之后,antlr会自动生成文件。这时,我们会发现它多给我们生成了两个java文件。LabeledExprBaseVisitor.javaLabeledExprVisitor.java。而原来生成的两个listener文件在这个项目中暂时用不到,可以删掉。至于visitorlistener有什么区别,由于是入门,我们在这里先不做详细的介绍,不过从其名字上我们可以大致看出他们的区别。

Listener---监听。当有事件发生时即可自动进行相关操作,强调自动

Visitor----访问。需要我们手动进行遍历,强调手动。

 

 

好了至此我们已经全部完成了antlr项目的构建,接下来我们要新建一个java project,然后把生成好的4java文件和两个token文件拷贝过去。

有得同学可能会想,能不能直接在antlr项目下新建一个java文件来书写我们的代码,答案是否定的,antlr项目并不允许我们去直接新建class。所以我们首先需要新建一个java project,这里我们取名为Calculator,然后,然后将之前生成的文件都放到项目中,在这里,我新建app文件夹,将生成的文件(src文件夹中的所有文件)都放入其中。

这时候我们发现所有的java文件的图标上有红色图标,而且引用这些文件都会显示无法找到。

  这是因为这些文件都不是源文件,在src源文件夹里的java文件都“看不见”那些文件。

因此,右击app文件夹,如图,选择source file

 

其次,随意打开一个java文件,发现全是报错,因为没有给项目添加antrl依赖,现在我们添加依赖,右击file,点击Progect Struture

 

再点击Modules,在点击dependencies

 

点击右侧的加号(注意是右侧的)

 

选择JARS or directors,然后找到之前下载的antrl4jar

 

点击ok,再看文件,我们发现报错都消失了。

 

现在继续项目,我们先看一下刚才生成的两个Visitor.

 

 

import org.antlr.v4.runtime.tree.ParseTreeVisitor;

/**
 * This interface defines a complete generic visitor for a parse tree produced
 * by {
@linkLabeledExprParser}.
 *
 *
@param<T>The return type of the visit operation. Use {@linkVoid} for
 * operations with no return type.
 */
public interface LabeledExprVisitor<T>extendsParseTreeVisitor<T>{
   /**
    * Visit a parse tree produced by {
@linkLabeledExprParser#prog}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitProg(LabeledExprParser.ProgContextctx);
   /**
    * Visit a parse tree produced by the {
@codeprintExpr}
    * labeled alternative in {
@linkLabeledExprParser#stat}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitPrintExpr(LabeledExprParser.PrintExprContextctx);
   /**
    * Visit a parse tree produced by the {
@codeassign}
    * labeled alternative in {
@linkLabeledExprParser#stat}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitAssign(LabeledExprParser.AssignContextctx);
   /**
    * Visit a parse tree produced by the {
@codeblank}
    * labeled alternative in {
@linkLabeledExprParser#stat}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitBlank(LabeledExprParser.BlankContextctx);
   /**
    * Visit a parse tree produced by the {
@codeparens}
    * labeled alternative in {
@linkLabeledExprParser#expr}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitParens(LabeledExprParser.ParensContextctx);
   /**
    * Visit a parse tree produced by the {
@codeMulDiv}
    * labeled alternative in {
@linkLabeledExprParser#expr}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitMulDiv(LabeledExprParser.MulDivContextctx);
   /**
    * Visit a parse tree produced by the {
@codeAddSub}
    * labeled alternative in {
@linkLabeledExprParser#expr}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitAddSub(LabeledExprParser.AddSubContextctx);
   /**
    * Visit a parse tree produced by the {
@codeid}
    * labeled alternative in {
@linkLabeledExprParser#expr}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitId(LabeledExprParser.IdContextctx);
   /**
    * Visit a parse tree produced by the {
@codeint}
    * labeled alternative in {
@linkLabeledExprParser#expr}.
    *
@paramctxthe parse tree
    *
@returnthe visitor result
    */
   
TvisitInt(LabeledExprParser.IntContextctx);
}

 

 

看看我们这几个方法的名字,PrintExpr, Assign,Blank......有没有感觉很熟悉。没错这就是我们刚才在g4文件的时候为每一条规则的后面打上了tag.

 

而另一个visitorLabeledExprBaseVisitor.java则是对本接口的一个简单的实现,我们后续的操作需要继承LabeledExprBaseVisitor.java来实现。

Visitor的作用顾名思义就是对整个语法树进行遍历,然后进行相关操作。

现在我们进行对语法树的操作,需要重写每一个方法。

src文件夹中新建class EvalVisitor,开始编辑该类

我们先声明一个map.用于存放变量与其对应的值。

Map<String,Integer>memory=newHashMap<String,Integer>();

 

@Override
public IntegervisitAssign(LabeledExprParser.AssignContextctx) {
    //TODO Auto-generatedmethod stub
    
Stringid=ctx.ID().getText();// id is left-hand side of '='
    intvalue=visit(ctx.expr());// compute valueof expression on right
    memory.put(id,value);// store it in our memory
    returnvalue;
}

这个实现很简单,获取id,获取value(通过expr()计算表达式的值),然后放入map

@Override
public IntegervisitPrintExpr(LabeledExprParser.PrintExprContextctx) {
    //TODO Auto-generatedmethod stub
    
Integervalue=visit(ctx.expr());// evaluate the exprchild
    System.out.println(value);// print theresult
    return0;// return dummyvalue
}

这个实现就是计算通过expr()计算表达式的值,然后打印出来。

 

@Override
public IntegervisitInt(LabeledExprParser.IntContextctx) {
    //TODO Auto-generatedmethod stub
    
returnInteger.valueOf(ctx.INT().getText());
}

这个实现就是如果碰见了INT,就直接返回它的数值。

 

@Override
public IntegervisitParens(LabeledExprParser.ParensContextctx) {
    //TODO Auto-generatedmethod stub
    
returnvisit(ctx.expr());// return childexpr's value
}

碰到了这样的带有括号的表达式,返回括号内的值。

 

@Override
public IntegervisitId(LabeledExprParser.IdContextctx) {
    //TODO Auto-generatedmethod stub
    
Stringid=ctx.ID().getText();
    if (memory.containsKey(id) )return memory.get(id);
    return 0;
}

ID,如果map中含有这个ID,就返回其对应的value值。

 

@Override
public IntegervisitMulDiv(LabeledExprParser.MulDivContextctx) {
    //TODO Auto-generatedmethod stub
    
intleft=visit(ctx.expr(0));// get value ofleft subexpression
    intright=visit(ctx.expr(1));// get value ofright subexpression
    if( ctx.op.getType()==LabeledExprParser.MUL)returnleft*right;
    return left/right;// must be DIV
}

@Override
public IntegervisitAddSub(LabeledExprParser.AddSubContextctx) {
    //TODO Auto-generatedmethod stub
    
intleft=visit(ctx.expr(0));// get value ofleft subexpression
    intright=visit(ctx.expr(1));// get value ofright subexpression
    if( ctx.op.getType()==LabeledExprParser.ADD)returnleft+right;
    return left-right;// must be SUB
}

上面两个函数即是我们的加减乘除的核心运算了。我们再来看一下我们的语法规则。以乘除为例.expr op=(*|/) expr.

在上述式子中我们有两个expr。第一个代表我们运算的第一个值,第二个代表我们运算的第二个值,中间是运算符 ‘*’或者‘/’。所以我们要先获取两个值,然后判断计算符‘op’到底是加号还是减号,由于我们刚才在g4文件中已经定义了,ADD,MULTIPLY等四个值,现在直接可以使用LabeledExprParser.MULTIPLY来获取其运算符。

好了至此我们所有的函数都已重写完毕。整体的代码如下。

 

/**
 * Created by think on 2017/9/21.
 */
import java.util.HashMap;
import java.util.Map;


public class EvalVisitorextendsLabeledExprBaseVisitor<Integer>{
    Map<String,Integer>memory=newHashMap<String,Integer>();

    @Override
    publicIntegervisitPrintExpr(LabeledExprParser.PrintExprContextctx) {
        //TODO Auto-generatedmethod stub
        
Integervalue=visit(ctx.expr());// evaluate the exprchild
        System.out.println(value);// print theresult
        return0;// return dummyvalue
    }

    @Override
    publicIntegervisitAssign(LabeledExprParser.AssignContextctx) {
        //TODO Auto-generatedmethod stub
        
Stringid=ctx.ID().getText();// id is left-hand side of '='
        intvalue=visit(ctx.expr());// compute valueof expression on right
        memory.put(id,value);// store it in our memory
        returnvalue;
    }

    @Override
    publicIntegervisitBlank(LabeledExprParser.BlankContextctx) {
        //TODO Auto-generatedmethod stub
        
return super.visitBlank(ctx);
    }

    @Override
    publicIntegervisitParens(LabeledExprParser.ParensContextctx) {
        //TODO Auto-generatedmethod stub
        
returnvisit(ctx.expr());// return childexpr's value
    }

    @Override
    publicIntegervisitMulDiv(LabeledExprParser.MulDivContextctx) {
        //TODO Auto-generatedmethod stub
        
intleft=visit(ctx.expr(0));// get value ofleft subexpression
        intright=visit(ctx.expr(1));// get value ofright subexpression
        if( ctx.op.getType()==LabeledExprParser.MUL)returnleft*right;
        return left/right;// must be DIV
    }

    @Override
    publicIntegervisitAddSub(LabeledExprParser.AddSubContextctx) {
        //TODO Auto-generatedmethod stub
        
intleft=visit(ctx.expr(0));// get value ofleft subexpression
        intright=visit(ctx.expr(1));// get value ofright subexpression
        if( ctx.op.getType()==LabeledExprParser.ADD)returnleft+right;
        return left-right;// must be SUB
    }

    @Override
    publicIntegervisitId(LabeledExprParser.IdContextctx) {
        //TODO Auto-generatedmethod stub
        
Stringid=ctx.ID().getText();
        if (memory.containsKey(id) )return memory.get(id);
        return 0;
    }

    @Override
    publicIntegervisitInt(LabeledExprParser.IntContextctx) {
        //TODO Auto-generatedmethod stub
        
returnInteger.valueOf(ctx.INT().getText());
    }
}

最后我们来书写一个主程序来启动代码。

/**
 * Created by think on 2017/9/21.
 */
import java.io.FileInputStream;
import java.io.InputStream;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;

public class Main{
    public static voidmain(String[] args)throws Exception{
        // create a CharStream thatreads from standard input
        StringinputFile="data.txt";
        InputStreamis=System.in;

        if ( inputFile!=null) is=newFileInputStream(inputFile);
        ANTLRInputStreaminput=newANTLRInputStream(is);

        LabeledExprLexerlexer=newLabeledExprLexer(input);
        CommonTokenStreamtokens=newCommonTokenStream(lexer);
        LabeledExprParserparser=newLabeledExprParser(tokens);
        ParseTree tree=parser.prog();// parse
        EvalVisitoreval=newEvalVisitor();
        eval.visit(tree);
    }
}

 

单击运行,会发现有一个文件报方法重载错误,注释该方法后再运行,会报错ANTLR Tool version 4.7 used for code generation does not match the current runtime 4.5.3.

这是因为IDEA的antlr4插件只支持4.7,因此我们需要重新下载antlr-4.7-complete.jar,重新配置环境变量,然后给项目添加插件和依赖,这时会报Fail to load plugin descriptor from file antlr-4.7-complete.jar,这时,添加插件的方法改为Browse respositories,搜索到插件再安装,最后重新添加项目依赖,改为4.7版本的jar包




我们的data文件的内容及运行结果如图

 

这样,简单的计算器就完成了


参考链接:http://blog.csdn.net/yuexiang321/article/details/52770283

原创粉丝点击