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手動指定運算符,這樣就能把2^3^4解釋成2^(3^4)。

下面上一下自己寫的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM