打破國外壟斷,開發中國人本身的編程語言(2):使用監聽器實現計算器

上一篇:實現能夠解析表達式的計算器javascript


本文已經同步到公衆號「極客起源」,輸入379404開始學習!html

 

本文是 《打破國外壟斷,開發中國人本身的編程語言》系列文章的第2篇。本系列文章的主要目的是教你們學會如何從零開始設計一種編程語言(marvel語言),並使用marvel語言開發一些真實的項目,如移動App、Web應用等。marvel語言能夠經過下面3種方式運行:
1. 解釋執行
2. 編譯成Java Bytecode,利用JVM執行
3. 編譯成二進制文件,本地執行(基於LLVM)
 

本文詳細講解如何用Listener方式實現一個能夠計算表達式的程序,該程序不只能夠計算表達式,也能夠識別表達式的錯誤,若是某一個表達式出錯,那麼該表達式不會輸出任何結果。java


1. Visitor與Listener
 
在上一篇文章中使用Antlr和Visitor實現了一個能夠計算表達式的程序MarvelCalc。這個程序很是簡單,至關於Antlr的HelloWorld。不過Antlr除了Visitor方式外,還支持Listener方式,也就是監聽器方式。不論是哪一種方式,其目的都是遍歷AST(抽象語法樹),只是Visitor方式須要顯式訪問子節點(經過visit方法訪問),例如,下面的代碼訪問了MulDiv的兩個子節點,也就是MulDiv的左右操做數(ctx.expr(0)和ctx.expr(1))。
// expr op=('*'|'/') expr      # MulDiv
public Integer visitMulDiv(CalcParser.MulDivContext ctx) {

    int left = visit(ctx.expr(0));          // 訪問MulDiv的左操做數
    int right = visit(ctx.expr(1));         // 訪問MulDiv的右操做數

    if ( ctx.op.getType() == CalcParser.MUL ) return left * right;
        return left / right; 
    }
}
 
而Listener方式是由系統自動訪問當前節點的子節點的,並不須要顯式訪問子節點。並且Listener能夠攔截當前節點的開始處理和結束處理動做。開始處理動做的事件方法以enter開頭,結束處理動做的事件方法以exit開頭。例如,處理MulDiv動做時,會生成兩個事件方法:enterMulDiv和exitMulDiv,分別表示開始處理MulDiv和結束處理MulDiv,這兩個方法的代碼以下:
@Override 
public void enterMulDiv(CalcParser.MulDivContext ctx) {
}
@Override 
public void exitMulDiv(CalcParser.MulDivContext ctx) {
}

 

 那麼開始處理動做和結束處理動做有什麼區別呢?若是是原子表達式(內部不包含其餘表達式的表達式),如id、數值等,這兩個事件方法沒什麼不一樣的(用哪個處理表達式均可以)。但若是是非原子表達式,就要考慮下使用enter仍是exit了。例如,下面的表達式:
 
3 * (20 / x * 43)
 
這個表達式明顯是非原子的。編譯器會從左向右掃描整個表達式,當掃描到第一個乘號(*)時,會將右側的全部內容(20 / x * 43)當作一個總體處理,這就會第一次調用enterMulDiv方法和exitMulDiv方法。只不過在調用enterMulDiv方法後,還會作不少其餘的工做,最後纔會調用exitMulDiv方法。那麼中間要作什麼工做呢?固然是處理表達式(20 / x * 43)了。因爲這個表達式中有一個變量x,因此在掃描到x時,須要搜索該變量是否存在,若是存在,須要提取該變量的值。也就是說,在第一次調用enterMulDiv方法時尚未處理這個變量x,若是在enterMulDiv方法中要計算整個表達式的值顯然是不可能的(由於x的值尚未肯定),因此正確的作法應該是在exitMulDiv方法中計算整個表達式的值,由於在該方法被調用時,整個表達式的每個子表達式的值都已經計算完了。
 
enterXxx和exitXxx方法也常常被用於處理做用域,例如,在掃描到下面的函數時, 在該函數對應的enterXxx方法中會將當前做用域切換到myfun函數(一般用Stack處理),而在exitXxx方法中,會恢復myfun函數的parent做用域。類、條件語句、循環語句也一樣涉及到做用域的問題。關於做用域的問題,在後面的文章中會詳細介紹做用域的實現方法。
void myfun() {
}

 

從前面的介紹可知,Listener比Visitor更靈活,Listener也是我推薦的遍歷AST的方式,後面的文章也基本上使用Listener的方式實現編譯器。
 
2. Listener對應的接口和基類
 
如今回到本文的主題上來,本文的目的是使用Listener的方式取代Visitor的方式實現計算器。在編譯Calc.g4時,除了生成CalcVisitor.java和CalcBaseVisitor.java,還生成了另外兩個文件:CalcListener.java和CalcBaseListener.java。其中CalcListener.java文件是Listener的接口文件,接口中的方法會根據Calc.g4文件中的產生式生成,該文件的代碼以下:
 
import org.antlr.v4.runtime.tree.ParseTreeListener;
public interface CalcListener extends ParseTreeListener {
    void enterProg(CalcParser.ProgContext ctx);
    void exitProg(CalcParser.ProgContext ctx);
    void enterPrintExpr(CalcParser.PrintExprContext ctx);
    void exitPrintExpr(CalcParser.PrintExprContext ctx);
    void enterAssign(CalcParser.AssignContext ctx);
    void exitAssign(CalcParser.AssignContext ctx);
    void enterBlank(CalcParser.BlankContext ctx);
    void exitBlank(CalcParser.BlankContext ctx);
    void enterParens(CalcParser.ParensContext ctx);
    void exitParens(CalcParser.ParensContext ctx);
    void enterMulDiv(CalcParser.MulDivContext ctx);
    void exitMulDiv(CalcParser.MulDivContext ctx);
    void enterAddSub(CalcParser.AddSubContext ctx);
    void exitAddSub(CalcParser.AddSubContext ctx);
    void enterId(CalcParser.IdContext ctx);
    void exitId(CalcParser.IdContext ctx);
    void enterInt(CalcParser.IntContext ctx);
    void exitInt(CalcParser.IntContext ctx);
}

 

一般來說,並不須要實現CalcListener接口中的全部方法,因此antlr還爲咱們生成了一個默認實現類CalcBaseListener,該類位於CalcBaseListener.java文件中。CalcListener接口的每個方法都在CalcBaseListener類中提供了一個空實現,因此使用Listener方式遍歷AST,只須要從CalcBaseListener類繼承,而且覆蓋必要的方法便可。
 
3. 用Listener方式實現可計算器
 
如今建立一個MyCalcParser.java文件,並在該文件中編寫一個名爲MyCalcParser的空類,代碼以下:
public class MyCalcParser  extends  CalcBaseListener{
    ... ...
}

 

如今的問題是,在MyCalcParser類中到底要覆蓋CalcBaseListener中的哪個方法,並且如何實現這些方法呢?
 
要回答這個問題,就要先分析一下上一篇文章中編寫的EvalVisitor類的代碼了。其實在EvalVisitor中覆蓋了哪個動做對應的方法,在MyCalcParser類中也一樣須要覆蓋該動做對應的方法,區別只是使用enterXxx,仍是使用exitXxx,或是都使用。
 
如今將EvalVisitor類的關鍵點提出來:
(1) 在EvalVisitor類中有一個名爲memory的Map對象,用來保存變量的值,這在Listener中一樣須要;
(2)在EvalVisitor類中有一個error變量,用來標識分析的過程當中是否有錯誤,在Listener中一樣須要;
(3)每個visitXxx方法都有返回值,其實這個返回值是向上一層節點傳遞的值。而Listener中的方法並無返回值,但仍然須要將值向上一層節點傳遞,因此須要想其餘的方式實現向上傳值;
 
 
那麼爲何要向上傳值呢?先來舉一個例子,看下面的表達式:
 
4 * 5
 
這是一個乘法表達式,編譯器對這個表達式掃描時,會先識別兩個整數(4和5),這兩個整數是兩個原子表達式。若是使用Listener的方式,須要在這兩個整數對應的enterInt方法(exitInt方法也能夠)中將'4'和'5'轉換爲整數,這是由於無論值是什麼類型,編譯器讀上來的都是字符串,因此須要進行類型轉換。
 
包含4和5的表達式是MulDiv,對應的動做方法是exitMulDiv(不能用enterMulDiv,由於這時4和5尚未掃描到)。在exitMulDiv方法中要獲取乘號(*)左右兩個操做數的值(ctx.expr(0)和ctx.expr(1))。而這兩個操做數的值在enterInt方法中已經獲取了,咱們要作的只是將獲取的值傳遞給上一層表達式,也就是MulDiv表達式。向上一層傳值的方法不少,這裏採用一個我很是推薦的方式,經過用一個Map對象保存全部須要傳遞的值,key就是上一層節點的ParseTree對象(每個enterXxx和exitXxx方法的ctx參數的類型都實現了ParseTree接口),而value則是待傳遞的值,可使用下面的方式定義這個Map對象。
 
 private Map<ParseTree,Integer> values = new HashMap<>();
 
同時還須要兩個方法來設置和獲取值,分別是setValue和getValue,代碼以下:
public void setValue(ParseTree node, int value) {
    values.put(node,value);
}
public int getValue(ParseTree node) {
    try {
        return values.get(node);
    } catch (Exception e) {
        return 0;
    }
}
 
下面給出MyCalcParser類的完整代碼:
import org.antlr.v4.runtime.tree.ParseTree;

import java.util.HashMap;
import java.util.Map;

public class MyCalcParser  extends  CalcBaseListener{
    private Map<ParseTree,Integer> values = new HashMap<>();        // 用於保存向上一層節點傳遞的值
    Map<String, Integer> memory = new HashMap<String, Integer>();    // 用於保存變量的值
    boolean error = false;                                           // 用於標識分析的過程是否出錯
    // 設置值
    public void setValue(ParseTree node, int value) {
        values.put(node,value);
    }
    // 獲取值
    public int getValue(ParseTree node) {
        try {
            return values.get(node);
        } catch (Exception e) {
            return 0;
        }

    }
    @Override public void enterPrintExpr(CalcParser.PrintExprContext ctx) {
        // 當開始處理表達式時,默認沒有錯誤
        error = false;
    }
    @Override public void exitPrintExpr(CalcParser.PrintExprContext ctx) {
        if(!error) {
            // 只有在沒有錯誤的狀況下,纔會輸出表達式的值 
            System.out.println(getValue(ctx.expr()));
        }
    }

    // 必需要放在exitAssign裏
    @Override public void exitAssign(CalcParser.AssignContext ctx) {
        String id = ctx.ID().getText();     // 獲取變量名
        int value = getValue(ctx.expr());   // 獲取右側表達式的值
        memory.put(id, value);              // 保存變量
    }    
    // 必須在exitParens中完成
    @Override public void exitParens(CalcParser.ParensContext ctx) {
        setValue(ctx,getValue(ctx.expr()));
    }

    // 計算乘法和除法(必須在exitMulDiv中完成)
    @Override public void exitMulDiv(CalcParser.MulDivContext ctx) {

        int left =  getValue(ctx.expr(0));  // 獲取左操做數的值
        int right = getValue(ctx.expr(1));  // 獲取右操做數的值
        if ( ctx.op.getType() == CalcParser.MUL )
            setValue(ctx,left * right);       // 向上傳遞值
        else
            setValue(ctx,left / right);       // 向上傳遞值
    }

    // 計算加法和減法(必須在exitAddSub中完成)
    @Override public void exitAddSub(CalcParser.AddSubContext ctx) {
        int left =  getValue(ctx.expr(0));  // 獲取左操做數的值
        int right = getValue(ctx.expr(1));  // 獲取右操做數的值
        if ( ctx.op.getType() == CalcParser.ADD )
            setValue(ctx,left + right);
        else
            setValue(ctx,left - right);
    }

    // 在enterId方法中也能夠
    @Override public void exitId(CalcParser.IdContext ctx) {
        String id = ctx.ID().getText();
        if ( memory.containsKey(id) ) {
            setValue(ctx,memory.get(id));      // 將變量的值向上傳遞
        } else {
            // 變量不存在,輸出錯誤信息(包括行和列), 
            System.err.println(String.format("行:%d, 列:%d, 變量<%s> 不存在!",ctx.getStart().getLine(),ctx.getStart().getCharPositionInLine() + 1,  id));
            error = true;
        }
    }
    // 處理int類型的值
    @Override public void enterInt(CalcParser.IntContext ctx) {

        int value = Integer.valueOf(ctx.getText());
        setValue(ctx, value);      // 將整數值向上傳遞
    }
}

 

如今編寫用於遍歷AST和計算結果的MarvelListenerCalc類,代碼以下:
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 org.antlr.v4.runtime.tree.ParseTreeWalker;

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

public class MarvelListenerCalc {
    public static void main(String[] args) throws Exception  {
        String inputFile = null;
        if ( args.length>0 ) {
            inputFile = args[0];
        } else {
            System.out.println("語法格式:MarvelCalc inputfile");
            return;
        }
        InputStream is = System.in;
        if ( inputFile!=null ) is = new FileInputStream(inputFile);


        CharStream input = CharStreams.fromStream(is);
        // 建立詞法分析器
        CalcLexer lexer = new CalcLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        // 
        CalcParser parser = new CalcParser(tokens);
        ParseTree tree =  parser.prog();
        MyCalcParser calc = new MyCalcParser();
        ParseTreeWalker walker = new ParseTreeWalker();
        // 開始遍歷AST
        walker.walk(calc, tree);

    }
}
 
 
咱們仍然使用上一篇文章使用的測試用例:
 
1+3 * 4 - 12 /6;
x = 40;
y = 13;
x * y + 20 - 42/6;
z = 12;
4;
x + 41 * z - y;
 
運行MarvelListenerCalc的執行結果以下圖所示:
 
 
本文實現的程序還支持錯誤捕捉,例如,將最後一個表達式的變量x改爲xx,再執行程序,就會拋出異常,出錯的表達式沒有輸出任何值,異常會指示出錯的位置(行和列),以下圖所示:
 

 

 

請關注微信公衆號「 極客起源」,更多精彩內容期待您的光臨!
 
相關文章
相關標籤/搜索