ANTLR使用访问器遍历语法树
計算器
語法文件
實現一個簡單的計算器,可以對如下表達式進行識別
193
a = 5
b = 6
a+b*2
(1+2)*3
如下為匹配規則的語法文件Expr.g4
grammar Expr;/** 起始規則,語法分析的起點 */ prog: stat+ ; stat: expr NEWLINE //匹配expr表達式 + 換行| ID '=' expr NEWLINE //匹配 變量 = 表達式 換行 | NEWLINE //匹配換行 ;expr: expr ('*'|'/') expr //匹配表達式*/| expr ('+'|'-') expr //匹配表達式+-| INT //整數 | ID //變量| '(' expr ')' //括號;//詞法分析器 ID : [a-zA-Z]+ ; // 由字母組成的變量名 INT : [0-9]+ ; // 數字 NEWLINE:'\r'? '\n' ; // 換行 WS : [ \t]+ -> skip ; // 匹配空白,按->skip命令跳過左遞歸規則:例如在語法規則expr: expr ('*'|'/') expr中,expr在備選分支的起始位置對自身進行了遞歸調用
使用antlr運行編譯,然后利用TestBig工具進行測試
D:\Code\antlr\demo\chapter4>antlr4 Expr.g4 # 生成語法、詞法分析器D:\Code\antlr\demo\chapter4>javac *.java # 編譯相關文件# 對語法Expr進行測試,初始規則prog,輸入文件t.expr,并以可視化的方式輸出結果 D:\Code\antlr\demo\chapter4>grun Expr prog -gui t.expr生成的語法分析樹如下所示
文件引入
在一個龐大的項目中,通常將語法文件拆分為語法規則文件和詞法規則文件,這樣有一些重復的詞法規則就可以放在一個文件中以實現重用,當需要使用的時候再將文件引入。
如下所示,將所有詞法規則放到文件CommonLexerRules.g4中
lexer grammar CommonLexerRules; // 詞法文件以關鍵字"lexer grammar"開頭ID : [a-zA-Z]+ ; INT : [0-9]+ ; NEWLINE:'\r'? '\n' ; WS : [ \t]+ -> skip ;然后在語法規則文件LibExpr.g4中引入所需的文件,之后我們只需要對LibExpr運行antlr構建工具即可,不需要再手動操作導入的文件
grammar LibExpr; import CommonLexerRules; // 引入詞法文件prog: stat+ ; stat: expr NEWLINE | ID '=' expr NEWLINE | NEWLINE ;expr: expr ('*'|'/') expr | expr ('+'|'-') expr | INT | ID | '(' expr ')' ;錯誤處理
ANTLR語法分析器能夠自動識別語法報告中的錯誤并從錯誤中恢復。
如下所示,在輸入中少了一個括號,語法分析樹會輸出提示信息,并且會繼續向后匹配
D:\Code\antlr\demo\chapter4>grun LibExpr prog -tree (1+2 3+4 ^Z line 1:4 mismatched input '\r\n' expecting {'*', '/', '+', '-', ')'} # 提示缺失 (prog (stat (expr ( (expr (expr 1) + (expr 2)) <missing ')'>) \r\n) (stat (expr (expr 3) + (expr 4)) \r\n))\ # 不影響繼續匹配如果使用gui的方式,會在確實的節點顯示為紅色
使用訪問器遍歷樹
在構建了語法分析樹后,使用訪問器對節點進行遍歷從而得出計算結果。訪問器機制和監聽器機制最大的區別在于,監聽器方法會被遍歷器自動調用,而訪問器必須手動調用visit()方法實現對子節點的訪問。
在使用訪問器之前,需要對語法文件的每個分支添加標簽,ANTLR會為每個標簽生成相應的方法,否則只會默認為每個規則生成一個方法。如下所示為語法規則文件LabeledExpr.g4,標簽以#開頭,放在分支右側
grammar LabeledExpr;prog: stat+ ;stat: expr NEWLINE # printExpr| ID '=' expr NEWLINE # assign| NEWLINE # blank;expr: expr op=('*'|'/') expr # MulDiv| expr op=('+'|'-') expr # AddSub| INT # int| ID # id| '(' expr ')' # parens;//在語法文件中,為詞法符號命名,這樣在Java中就可以當作常量來訪問了 MUL : '*' ; // 將 '*' 命名為MUL DIV : '/' ; ADD : '+' ; SUB : '-' ;//詞法規則 ID : [a-zA-Z]+ ; // match identifiers INT : [0-9]+ ; // match integers NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal) WS : [ \t]+ -> skip ;對上述語法文件運行ANTLR構建工具,通過命令參數-visitor指定生成包含訪問器的代碼
antlr4 -no-listener -visitor LabeledExpr.g4
自動生成訪問器接口類LabeledExprVisitor,之前在語法文件中定義的標簽都會生成對應的方法,傳入相應的上下文作為參數,并且以泛型的方式定義接口類,我們可以根據需要自定義返回值類型。同時生成了接口的默認實現類LabeledExprBaseVisitor
public interface LabeledExprVisitor<T> extends ParseTreeVisitor<T> {T visitProg(LabeledExprParser.ProgContext ctx); //訪問Prog標簽T visitPrintExpr(LabeledExprParser.PrintExprContext ctx); //訪問PrintExpr標簽T visitAssign(LabeledExprParser.AssignContext ctx); //訪問Assign標簽...... }public class LabeledExprBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements LabeledExprVisitor<T> {@Override public T visitProg(LabeledExprParser.ProgContext ctx) { return visitChildren(ctx); }@Override public T visitPrintExpr(LabeledExprParser.PrintExprContext ctx) { return visitChildren(ctx); }@Override public T visitAssign(LabeledExprParser.AssignContext ctx) { return visitChildren(ctx); }...... }通過繼承LabeledExprBaseVisitor,實現自定義訪問器類EvalVisitor ,在其中實現具體訪問節點的代碼,完成計算器的運算操作。注意在每個visitXxx()方法中都通過visit()方法手動對子節點進行訪問
import java.util.HashMap; import java.util.Map;public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {/** 計算器的“內存”,存放<變量名, 變量值> */Map<String, Integer> memory = new HashMap<String, Integer>();/** ID '=' expr NEWLINE */@Overridepublic Integer visitAssign(LabeledExprParser.AssignContext ctx) {String id = ctx.ID().getText(); // 獲取=左邊的變量int value = visit(ctx.expr()); // 計算右側表達式的值memory.put(id, value); // 將計算結果儲存到“內存”中return value;}/** expr NEWLINE */@Overridepublic Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {Integer value = visit(ctx.expr()); // 計算子節點的值System.out.println(value); // 打印結果return 0; // 返回虛值}/** INT */@Overridepublic Integer visitInt(LabeledExprParser.IntContext ctx) {return Integer.valueOf(ctx.INT().getText());}/** ID */@Overridepublic Integer visitId(LabeledExprParser.IdContext ctx) {String id = ctx.ID().getText();if ( memory.containsKey(id) ) return memory.get(id);return 0;}/** expr op=('*'|'/') expr */@Overridepublic Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {int left = visit(ctx.expr(0)); // 遞歸計算左側表達式的值int right = visit(ctx.expr(1)); // 計算右側表達式的值if ( ctx.op.getType() == LabeledExprParser.MUL ) return left * right; //兩值相乘return left / right; // 或者相除}/** expr op=('+'|'-') expr */@Overridepublic Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {int left = visit(ctx.expr(0)); // get value of left subexpressionint right = visit(ctx.expr(1)); // get value of right subexpressionif ( ctx.op.getType() == LabeledExprParser.ADD ) return left + right;return left - right; // must be SUB}/** '(' expr ')' */@Overridepublic Integer visitParens(LabeledExprParser.ParensContext ctx) {return visit(ctx.expr()); // 返回子表達式的值} }最后實現一個主程序Calc.java對語法分析樹進行遍歷,計算結果
import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.tree.ParseTree;import java.io.FileInputStream; import java.io.InputStream;public class Calc {public static void main(String[] args) throws Exception {String inputFile = null;if ( args.length>0 ) inputFile = args[0];InputStream is = System.in; //從標準輸入獲取字符if ( inputFile!=null ) is = new FileInputStream(inputFile); //從文件獲取輸入字符ANTLRInputStream input = new ANTLRInputStream(is);LabeledExprLexer lexer = new LabeledExprLexer(input); //詞法分析器CommonTokenStream tokens = new CommonTokenStream(lexer); //將詞法分析器產生的詞法符號放到緩沖區ArrayInitParser parser = new ArrayInitParser(tokens); //將詞法符號送入語法分析器ParseTree tree = parser.prog(); //開始分析EvalVisitor eval = new EvalVisitor(); //創建訪問器eval.visit(tree); //開始遍歷分析樹} }對上述文件進行編譯、運行Calc,可以看到輸出計算結果
D:\Code\antlr\demo\chapter4>javac *.javaD:\Code\antlr\demo\chapter4>java Calc (1+2) 3+4 ^Z 3 7總結
以上是生活随笔為你收集整理的ANTLR使用访问器遍历语法树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 装饰模式(装饰设计模式)详解——小马同学
- 下一篇: 移动端h5实现摇一摇抽奖