用antlr4來實現《按編譯原理的思路設計的一個計算器》中的計算器


上次在公司內部講《詞法分析——使用正則文法》是一次失敗的嘗試——上午有十幾個人在場,下午就只來了四個聽眾。

本來我還在構思如何來講“語法分析”的知識呢,但現在看來已不太可能。

 

這個課程沒有預想中的受歡迎,其原因可能是:

1.課程內容相對復雜,聽眾知識背景與基礎差異比較大。

2.授課技巧不夠,不能把復雜的知識簡單化的呈現給基礎稍差一點的人。

 

針對這兩個可能的原因,我要嘗試做出以下調整:

1.使用antlr來實現詞法和語法的部分。

2.暫時把“編譯”過程改為“解釋”來實現。

 

使用antlr的原因是:

1.采用文法生成器可直接略過詞法和語法的部分直接進入語義分析,這樣利於速成,同時避免學員被詞法分析和語法分析的復雜性嚇到,而失去了繼續學習的勇氣。

2.antlr的文法是LL(k)型,非常易於編寫——雖然k型方法的性能肯定不如1型文法,但與初學者談性能問題並不是一個好主意,不如直接避開性能不談,能運行即可。

3.antlr默認生成的是java代碼,這與公司內大多數員工的現有知識是相吻合的。

 

 下面進入正文。

 

一、什么是antlr?如何安裝?

這不是一篇湊字數的文章,所以請直接參考官方網站(http://www.antlr.org/)。

我使用的是目前的最新版本(V4.2.2).

我上傳了參考資料(包括jar包、電子書和官方示例)到百度雲上,可從這個地址下載(http://pan.baidu.com/s/1hq65XWC)。

 

二、本計算器的文法示例及文法的解釋。

整個計算器的詞法的語法就由以下幾行的antlr4代碼來實現,先貼在下面:

grammar Calc;                            // 文法的名字為Calc

// 以下以小寫字母開頭的文法表示為語法元素
// 由大寫字母開頭的文法表示為詞法元素
// 詞法元素的表示類似於正則表示式
// 語法元素的表示類似於BNF

exprs : setExpr                            // set表達式
    | calcExpr                            // 或calc表達式
    ;

setExpr : 'set' agmts ;                    // 以set命令開頭,后面是多個賦值語句
agmts   : agmt (';' agmts)? ';'? ;        // 多個賦值語句是由一個賦值語句后根着多個賦值語句,中間由分號分隔,結尾有一個可選的分號
agmt    : id=ID '=' num=NUMBER ;        // 一個賦值語句是由一個ID,后跟着一個等號,再后面跟送一個數字組成
calcExpr: 'calc' expr ;                    // 以calc命令開頭,后面是一個計算表達式

// expr可能由多個產生式
// 在前面的產生式優先於在后面的產生式
// 這樣來解決優先級的問題

expr: expr op=(MUL | DIV) expr            // 乘法或除法
    | expr op=(ADD | SUB) expr            // 加法或減法
    | factor                            // 一個計算因子——可做為+-*/的操作數據的東西
    ;

factor: (sign=(ADD | SUB))? num=NUMBER    // 計算因子可以是一個正數或負數
    | '(' expr ')'                        // 計算因子可以是括號括起來的表示式
    | id=ID                                // 計算因子可以是一個變量
    | funCall                            // 計算因子可以是一個函數調用
    ;

funCall: name=ID '(' params ')' ;        // 函數名后面加參數列表
params : expr (',' params)? ;            // 參數列表是由一個表達式后面跟關一個可選的參數列表組成

WS : [ \t\n\r]+ -> skip ;                // 空白, 后面的->skip表示antlr4在分析語言的文本時,符合這個規則的詞法將被無視
ID : [a-z]+ ;                            // 標識符,由0到多個小寫字母組成
NUMBER : [0-9]+('.'([0-9]+)?)? ;        // 數字
ADD : '+' ;
SUB : '-' ;
MUL : '*' ;
DIV : '/' ;

 

 

我們把這段文法保存到一個文件Calc.g4中,並運行命令“antlr4 -visitor Calc.g4”即生成6個java文件和兩個tokens文件。

這幾個文件包括了這個計算器的“詞法分析程序”、“語法分析程序”和一個visitor(CalcBaseVisitor.java),不過此時這個visitor內部實現都是空的,我們需要自己實現它。

在實現這個visitor之前,我們先實現一個上下文,上下文的做用有兩個:

1.保存變量——用於在計算表達式中引用變量。

2.保存堆棧——用於函數的參數傳遞。

這個上下文的內容很少,代碼也很短,直接貼在下面:

 1 public class Context {
 2     private static Context ourInstance = new Context();
 3 
 4     public static Context getInstance() {
 5         return ourInstance;
 6     }
 7 
 8     private Context() {
 9     }
10 
11     private Map<String, Double> map = new HashMap<>();
12     private Deque<Double> stack = new ArrayDeque<>();
13 
14     public Double getValue(String key) {
15         Double d = map.get(key);
16         return d == null ? Double.NaN : d;
17     }
18 
19     public void setContext(String key, Double value) {
20         map.put(key, value);
21     }
22 
23     public void setContext(String key, String value) {
24         setContext(key, Double.valueOf(value));
25     }
26 
27     public void pushStack(Double d) {
28         stack.push(d);
29     }
30 
31     public Double popStack() {
32         return stack.pop();
33     }
34 }

 

下面我們開始實現這個計算器的visitor,

 1 public class MyCalcVisitor extends CalcBaseVisitor<Double> {
 2 
 3     @Override
 4     public Double visitExprs(CalcParser.ExprsContext ctx) {
 5         return visit(ctx.getChild(0));
 6     }
 7 
 8     @Override
 9     public Double visitAgmt(CalcParser.AgmtContext ctx) {
10         Context.getInstance().setContext(ctx.id.getText(), ctx.num.getText());
11         return null;
12     }
13 
14     @Override
15     public Double visitAgmts(CalcParser.AgmtsContext ctx) {
16         visit(ctx.agmt());
17         if (ctx.agmts() != null)
18             visit(ctx.agmts());
19         return null;
20     }
21 
22     @Override
23     public Double visitCalcExpr(CalcParser.CalcExprContext ctx) {
24         return visit(ctx.expr());
25     }
26 
27     @Override
28     public Double visitExpr(CalcParser.ExprContext ctx) {
29         int cc = ctx.getChildCount();
30         if (cc == 3) {
31             switch (ctx.op.getType()) {
32             case CalcParser.ADD:
33                 return visit(ctx.expr(0)) + visit(ctx.expr(1));
34             case CalcParser.SUB:
35                 return visit(ctx.expr(0)) - visit(ctx.expr(1));
36             case CalcParser.MUL:
37                 return visit(ctx.expr(0)) * visit(ctx.expr(1));
38             case CalcParser.DIV:
39                 return visit(ctx.expr(0)) / visit(ctx.expr(1));
40             }
41         } else if (cc == 1) {
42             return visit(ctx.getChild(0));
43         }
44         throw new RuntimeException();
45     }
46 
47     @Override
48     public Double visitFactor(CalcParser.FactorContext ctx) {
49         int cc = ctx.getChildCount();
50         if (cc == 3) {
51             return visit(ctx.getChild(1));
52         } else if (cc == 2) {
53             if (ctx.sign.getType() == CalcParser.ADD)
54                 return Double.valueOf(ctx.getChild(1).getText());
55             if (ctx.sign.getType() == CalcParser.SUB)
56                 return -1 * Double.valueOf(ctx.getChild(1).getText());
57         } else if (cc == 1) {
58             if (ctx.num != null)
59                 return Double.valueOf(ctx.getChild(0).getText());
60             if (ctx.id != null)
61                 return Context.getInstance().getValue(ctx.id.getText());
62             return visit(ctx.funCall());
63         }
64         throw new RuntimeException();
65     }
66 
67     @Override
68     public Double visitParams(CalcParser.ParamsContext ctx) {
69         if (ctx.params() != null)
70             visit(ctx.params());
71         Context.getInstance().pushStack(visit(ctx.expr()));
72         return null;
73     }
74 
75     @Override
76     public Double visitFunCall(CalcParser.FunCallContext ctx) {
77         visit(ctx.params());
78         String funName = ctx.name.getText();
79         switch (funName) {
80         case "pow":
81             return Math.pow(Context.getInstance().popStack(), Context.getInstance().popStack());
82         case "sqrt":
83             return Math.sqrt(Context.getInstance().popStack());
84         }
85         throw new RuntimeException();
86     }
87 
88     @Override
89     public Double visitSetExpr(CalcParser.SetExprContext ctx) {
90         return visit(ctx.agmts());
91     }
92 
93 }

 最后再實現一個入口,調用這個Visitor即完成了我們的計算器。

入口代碼如下:

 1 import java.util.Scanner;
 2 
 3 import org.antlr.v4.runtime.ANTLRInputStream;
 4 import org.antlr.v4.runtime.CommonTokenStream;
 5 import org.antlr.v4.runtime.tree.ParseTree;
 6 
 7 public class Portal {
 8 
 9     private static final String lineStart = "CALC> ";
10 
11     public static void main(String[] args) {
12         try (Scanner scanner = new Scanner(System.in)) {
13             System.out.print(lineStart);
14             while (scanner.hasNext()) {
15                 String line = scanner.nextLine();
16                 if (line != null) {
17                     line = line.trim();
18                     if (line.length() != 0) {
19                         if ("exit".equals(line) || "bye".equals(line))
20                             break;
21                         ANTLRInputStream input = new ANTLRInputStream(line);
22                         CalcLexer lexer = new CalcLexer(input);
23                         CommonTokenStream tokens = new CommonTokenStream(lexer);
24                         CalcParser parser = new CalcParser(tokens);
25                         ParseTree tree = parser.exprs();
26                         MyCalcVisitor mv = new MyCalcVisitor();
27                         Double res = mv.visit(tree);
28                         if (res != null)
29                             System.out.println(res);
30                     }
31                 }
32 
33                 System.out.print(lineStart);
34             }
35         }
36     }
37 
38 }

 

整個計算器只寫了一個文法和三個類,所有代碼都貼在上面了,相對於完全自己手寫的計算器來說,的確是簡單很多了。


免責聲明!

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



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