Implementing a Simple Calculator Using Antlr

Implementing a Simple Calculator Using Antlr

In this experiment, we are going to set up antlr in idea.

See the Antlr4 documentation here

1.The first step is to import antlr jar package to the jdk library. We can complete the process in idea.
这里写图片描述

2.In order to use antlr to generate the Lexer, we should create a file which ends with .g4, in this project, I name it Calculator.g4. Then we can put the grammar of our calculator in the .g4 file.

grammar Calculator;

program: (assign | print)*;
assign: VARIABLE '=' expression ';';
print: 'print' '(' expression ')' ';';
//parents: '(' expression ')';
//expression: expression (ADD|MINUS|MUL|DIV|MOD) expression | VARIABLE | OPRAND | parents;
expression:'(' expression ')'               # Parents
          | expression MUL expression       # MUL
          | expression DIV expression       # DIV
          | expression MOD expression       # MOD
          | expression ADD expression       # ADD
          | expression MINUS expression     # MINUS
          | VARIABLE                        # Variable
          | OPRAND                          # Oprand
          ;

VARIABLE: [a-zA-Z][a-zA-Z0-9]*;
OPRAND: INT | DOUBLE;
INT: [0-9]+('_'[0-9]+)* | '0'('x'|'X')[0-9a-fA-F]+;
DOUBLE: [0-9]+('_'[0-9]+)* '.' [0-9]+('_'[0-9]+)*;
ADD: '+';
MINUS: '-';
MUL: '*';
DIV: '/';
MOD: '%';

NEWLINE: [\t\r\n] -> skip;
WS: ' ' -> skip;

Notice that for the grammar expression, we should put the operation of parent in the first line, followed by mul, div, mod, lastly add and minus, for the priority is in descending order.
The name after the # mark will be generated to separated method with the same name.

3.We can set up the encoding and language of output, then we choose visitors as output:
这里写图片描述

4.Then we get files:
- Calculator.tokens
- CalculatorLexer.java
- CalculatorParser.java
- CalculatorVisitor.java
- CalculatorBaseVisitor.java

CalculatorVisitor is Interface and CalculatorBaseVisitor implements it. What we are going to do is to create a EvalVisitor inherit from CalculatorBaseVisitor to overwrite the methods to achieve the goal of calculating.

5.Firstly we finish the operation for add, minus and multiply:

    @Override
    public Float visitADD(CalculatorParser.ADDContext ctx) {
        Float left = visit(ctx.expression(0));
        Float right = visit(ctx.expression(1));
        return left + right;
    }

    @Override
    public Float visitMINUS(CalculatorParser.MINUSContext ctx) {
        Float left = visit(ctx.expression(0));
        Float right = visit(ctx.expression(1));
        return left - right;
    }

    @Override
    public Float visitMUL(CalculatorParser.MULContext ctx) {
        Float left = visit(ctx.expression(0));
        Float right = visit(ctx.expression(1));
        return left * right;
    }

Then for divide and mod operation, the right one should not be zero(with will cause a divide by zero exception).So we add a judgment. We let the program exit right after it have reported the error by adding System.exit(0)

    @Override
    public Float visitDIV(CalculatorParser.DIVContext ctx) {
        Float left = visit(ctx.expression(0));
        Float right = visit(ctx.expression(1));
        if (right == 0) {
            System.err.println("divide by zero at: Line " + ctx.getStart().getLine() + ", Position " + ctx.getStart().getCharPositionInLine());
            System.exit(0);
        }
        return left / right;
    }

    @Override
    public Float visitMOD(CalculatorParser.MODContext ctx) {
        Float left = visit(ctx.expression(0));
        Float right = visit(ctx.expression(1));
        if (right == 0) {
            System.err.println("mod by zero at line: " + ctx.getStart().getLine());
            System.exit(0);
        }
        return left % right;
    }

For operend and parent:

    @Override
    public Float visitOprand(CalculatorParser.OprandContext ctx) {
        return Float.valueOf(ctx.OPRAND().getText());
    }

    @Override
    public Float visitParents(CalculatorParser.ParentsContext ctx) {
        return visit(ctx.expression());
    }

For variable and assignment, we use hash map in Java.util to store the variables.

    @Override
    public Float visitAssign(CalculatorParser.AssignContext ctx) {
        String var = ctx.VARIABLE().getText();
        Float value = visit(ctx.expression());
        memory.put(var, value);
        return value;
    }

    @Override
    public Float visitVariable(CalculatorParser.VariableContext ctx) {
        String id = ctx.VARIABLE().getText();
        if (!memory.containsKey(id)) {
            System.err.println("Using a variable without assignment at: Line " + ctx.getStart().getLine() + ", Position " + ctx.getStart().getCharPositionInLine());
            System.exit(0);
        }
        return memory.get(id);
    }

Finally, for print:

    @Override
    public Float visitPrint(CalculatorParser.PrintContext ctx) {
        Float value = visit(ctx.expression());

        Integer intValue = value.intValue();

        if ((value - intValue) == 0) {
            System.out.println(intValue);
        } else {
            System.out.println(value);
        }

        return value;
    }

Notice that we add a judgement in print. The function of it is to judge whether the output can be treated as int. If can, we print it out as an integer, otherwise float. Maybe it is not very rigorous but it does cost less time and space for the program to achieve the requirement of automatically judge whether a variable is int or float. A better but more time-space-consuming solution is to change the template in class EvalVisitor to Object, and check whether it is int or float every time when the variable is involved.

6.Then we can test it in our main function:

package calculator;

import java.io.FileInputStream;
import java.io.InputStream;

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


public class Main {

    public static void main(String[] args) throws Exception {
        String inputFile = "Test Cases/test.in";
//        String inputFile = "Test Cases/test2.in";
//        String inputFile = "Test Cases/error1.in";
//        String inputFile = "Test Cases/error2.in";
//        String inputFile = "Test Cases/error3.in";
        InputStream is = System.in;

//        if (inputFile != null)
        is = new FileInputStream(inputFile);
        ANTLRInputStream input = new ANTLRInputStream(is);

        CalculatorLexer lexer = new CalculatorLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CalculatorParser parser = new CalculatorParser(tokens);
        ParseTree tree = parser.program(); // parse
        EvalVisitor eval = new EvalVisitor();
        eval.visit(tree);
    }
}

Notice that firstly we create a lexer with reads the input, and change it to tokens. The we create a parse tree and calculate its value.

7.We may notice that even if a syntax error happens, the program still calculate a value, which is obviously wrong. This is because antlr has some error handle method, perhaps it will skip some nodes when an error occurs. So we should overwrite its error detection method.
BaseErrorListener is what we are going to inherit. We can create a class named CalculatorErrorListener, and overwrite method syntaxError:

package calculator;

import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;

public class CalculatorErrorListener extends BaseErrorListener{
    @Override
    public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
        super.syntaxError(recognizer, offendingSymbol, line, charPositionInLine, msg, e);
        System.exit(0);
    }
}

Notice that the details in Base Class don’t need to be changed, we just guarantee the program reports and exits right after the error occurs.
With this definition, our application can easily add an error listener to the
parser before invoking the start rule.

8.Then we add the new ErrorListener to the parser(in main):

    CalculatorErrorListener errorLisener = new CalculatorErrorListener();
    parser.addErrorListener(errorLisener);

Finally when we encounter syntax error, we just report it without calculate a wrong number.


Conclusion:

  • In this project, we have finished all the requirement of the calculator project last semester.
  • We fix the error hanlder in antlr to let it quit directly after a syntax error has been reported.
  • Due to the deadline limit, there is still a little problem: for example, if i calculate 1.2 * 3, it will get 3.59999999. This is due to the conversion between binary digits to decimal numbers. A possible solution is to change all the Float data type to BigDecimal.Float
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值