上次在公司內部講《詞法分析——使用正則文法》是一次失敗的嘗試——上午有十幾個人在場,下午就只來了四個聽眾。
本來我還在構思如何來講“語法分析”的知識呢,但現在看來已不太可能。
這個課程沒有預想中的受歡迎,其原因可能是:
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 }
整個計算器只寫了一個文法和三個類,所有代碼都貼在上面了,相對於完全自己手寫的計算器來說,的確是簡單很多了。