ANTLR的運行庫提供了兩種遍歷樹的機制:語法分析樹監聽器與訪問器。通過它們,我們可以在遍歷樹的時候實現相應邏輯。在本章中,我們將通過編寫一個簡單的計算器來探究三種在事件方法中共享信息的途徑。
一、計算器語法文件
按照上一章“Antlr4入門(三)如何編寫語法文件”所學的內容,我們可以很輕鬆的寫出一個只支持加法和乘法的計算器語法文件。
grammar ExprTest;
cal : expr;
expr : expr MUL expr # Mul
| expr ADD expr # Add
| INT # Int
;
MUL : '*';
ADD : '+';
INT : '0' | [1-9][0-9]*;
NEWLINE : '\r'?'\n';
WS : [ \t\n] -> skip;
使用ANTLR工具來測試下語法規則是否正確。
測試無誤後,我們接着使用ANTLR工具來生成語法分析樹等代碼。
二、使用訪問器遍歷語法分析樹
ExprTestVisitor.java是個訪問器接口,定義了一些訪問RuleNode的接口方法,可以通過實現它來完成自定義的功能。而ExprTestBaseVisitor.java是該接口的默認實現,它爲每個接口方法提供了空實現。
爲構建一個基於訪問器的計算器程序,最簡單的方法是令expr規則中的事件方法返回子表達式的值。例如,visitAdd()將返回兩個子表達式相加的結果,visitInt()方法返回整數元素的值。按照說明,我們實現的訪問器如下所示:
package exprtest;
public class CalculatorVisitor extends ExprTestBaseVisitor<Integer> {
@Override
public Integer visitAdd(ExprTestParser.AddContext ctx) {
return visit(ctx.expr(0)) + visit(ctx.expr(1));
}
@Override
public Integer visitMul(ExprTestParser.MulContext ctx) {
return visit(ctx.expr(0)) * visit(ctx.expr(1));
}
@Override
public Integer visitInt(ExprTestParser.IntContext ctx) {
// return int value
return Integer.valueOf(ctx.getText());
}
}
讓我們編寫一個簡單的main方法去調用它
import exprtest.*;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
public class ExprTestMain {
public static void main(String[] args){
ANTLRInputStream inputStream = new ANTLRInputStream("1+2*3+4");
ExprTestLexer lexer = new ExprTestLexer(inputStream);
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
ExprTestParser parser = new ExprTestParser(tokenStream);
ParseTree parseTree = parser.cal();
System.out.println(parseTree.toStringTree(parser));
CalculatorVisitor visitor = new CalculatorVisitor();
int result = visitor.visit(parseTree);
System.out.println("Visitor calculate result: "+result);
}
}
運行結果如下:
到這裏,我們已經使用訪問器實現了加法和乘法功能,下面,讓我們來看看如何使用監聽器來實現它。
三、使用監聽器遍歷語法分析樹
ANTLR的運行庫提供了兩種遍歷樹的機制:監聽器機制與訪問器機制。與訪問器不同的是,監聽器的方法會被ANTLR提供的遍歷器對象(比如ParseTreeWalker)自動調用,而在訪問器的方法中,必須顯示調用visit方法來訪問子節點。如果沒有調用visit方法就會導致對應的子樹不被訪問。而且監聽器方法是沒有返回值的(即返回類型是void)。因此我們需要一種額外的數據結構來存儲我們的計算結果,供下一次計算調用。與Java虛擬機使用棧來臨時存儲返回值一樣,我們可以使用棧來存儲中間計算結果。
package exprtest;
import java.util.Stack;
// 監聽器方法是沒有返回值的,因此需要一個成員變量來存儲局部變量——棧(JVM也是使用棧來臨時存儲返回值的)
// 監聽器會自動訪問子樹
public class CalculatorListener extends ExprTestBaseListener {
// 定義一個棧(先進後出),存放中間計算結果
private Stack<Integer> result = new Stack<Integer>();
public int getResult(){
// 將最後的結果返回
return result.pop();
}
@Override
public void exitAdd(ExprTestParser.AddContext ctx) {
// 右邊的值會先出棧
int right = result.pop();
int left = result.pop();
// 再將計算後的值放入棧中
result.push(left + right);
}
@Override
public void exitMul(ExprTestParser.MulContext ctx) {
int right = result.pop();
int left = result.pop();
result.push(left * right);
}
@Override
public void exitInt(ExprTestParser.IntContext ctx) {
// 將INT的值放入棧中
result.push(Integer.valueOf(ctx.getText()));
}
}
以上就是一個完整的使用監聽器來實現計算功能的代碼,下面在main方法中測試它。
ParseTreeWalker walker = new ParseTreeWalker();
CalculatorListener listener = new CalculatorListener();
walker.walk(listener,parseTree);
int listenerResult = listener.getResult();
System.out.println("Listener calculate result: "+listenerResult);
這種使用棧的方法不夠優雅,但是非常有效。通過它,我們可以保證事件方法在所有的監聽器事件之間的執行順序是正確的。除此之外,我們還能將局部變量存儲在樹節點中。
三、標註(Annotate)語法分析樹
最簡單的標註語法分析樹節點的方法是使用Map來將任意值和節點一一對應起來。出於這個目的,ANTLR提供了一個名爲ParseTreeProperty的輔助類,通過查看它的源代碼,我們可以知道它實際上是一個Map<ParseTree, V>。
package org.antlr.v4.runtime.tree;
import java.util.IdentityHashMap;
import java.util.Map;
public class ParseTreeProperty<V> {
protected Map<ParseTree, V> annotations = new IdentityHashMap();
public ParseTreeProperty() {
}
public V get(ParseTree node) {
return this.annotations.get(node);
}
public void put(ParseTree node, V value) {
this.annotations.put(node, value);
}
public V removeFrom(ParseTree node) {
return this.annotations.remove(node);
}
}
需要注意的是,如果你想使用自己的Map來替代ParseTreeProperty,那麼需要保證它是從IdentityHashMap而不是普通的HashMap派生。而且不管是監聽器還是訪問器,它們都支持樹的標註。
package exprtest;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeProperty;
public class CalculatorWithProps extends ExprTestBaseListener{
// 使用Map<ParseTree, Integer>將節點映射到對應的結果值
ParseTreeProperty<Integer> result = new ParseTreeProperty<Integer>();
public void setValues(ParseTree node, int value){
result.put(node, value);
}
public int getValues(ParseTree node){
return result.get(node);
}
@Override
public void exitCal(ExprTestParser.CalContext ctx) {
setValues(ctx, getValues(ctx.expr()));
}
@Override
public void exitAdd(ExprTestParser.AddContext ctx) {
// 子樹節點有三個,兩個操作數和一個操作符,1 + 2
int left = getValues(ctx.getChild(0));
int right = getValues(ctx.getChild(2));
// int left = getValues(ctx.expr(0));
// int right = getValues(ctx.expr(1));
setValues(ctx, left + right);
}
@Override
public void exitMul(ExprTestParser.MulContext ctx) {
int left = getValues(ctx.getChild(0));
int right = getValues(ctx.getChild(2));
setValues(ctx, left * right);
}
@Override
public void exitInt(ExprTestParser.IntContext ctx) {
setValues(ctx, Integer.valueOf(ctx.getText()));
}
}
顯而易見,不管是使用Stack或者ParseTreeProperty,它們的代碼總是相似的,畢竟只是換了個存儲方式。
CalculatorWithProps calculatorWithProps = new CalculatorWithProps();
walker.walk(calculatorWithProps, parseTree);
int propsValues = calculatorWithProps.getValues(parseTree);
System.out.println("ParseTreeProperty calculate result: "+propsValues);
後記
到這裏,我們已經知道了如何使用樹監聽器和訪問器來實現基本的語言類應用程序,後面,我們將先通過幾個實戰例子來鞏固下所學的內容。