antlr4
本文包括:
- antlr4基本操作:下載、安裝、測試
- Listener模式和Visitor模式比較
- 通過增加操作修飾文法
- antlr4 優先級、左遞歸及相關性
- antlr4 實現的簡單計算器(java版)
基本操作
- 下載安裝antlr
sudo curl -O http://www.antlr.org/download/antlr-4.7-complete.jar
alias antlr4='java -jar /usr/local/lib/antlr-4.7-complete.jar'
alias grun='java org.antlr.v4.gui.TestRig'
設置antlr4和grun別名的兩句:直接寫在命令行,重啟就會被抹去,失去效果;推薦寫在用戶配置文件中(Mac OS 下
vi ~/.bash_profile
,寫在文件末尾即可)
- 編寫一個文法,保存為hello.g4
grammar hello;
//tokens
s: 'hello' + ID;
ID: [a-z]+;
WS: [ \t\n\r]+ -> skip;
- 利用antlr生成這個文法的識別器(默認生成的識別器由java編寫)
//進入hello.g4所在文件夾后執行
antlr4 hello.g4
測試字符串是否屬於這個文法
在使用生成的識別器前要先編譯,因為我們生成的識別器是用默認的java語言編寫的,所以我們用javac來編譯:
javac -g *.java
。如果不編譯就使用這個識別器,會發生
Can't load hello as lexer or parser
的錯誤,原因顯而易見,不再贅述
以下方式都可以用來識別字符串是否能夠被這個文法識別,敲入下列命令,回車,輸入要識別的字符串,以 EOF(UNIX/Mac OS下Ctrl+D,windows下Ctrl+Z)結尾
grun [文法名] [標識符] [TestRig參數]
grun hello s -tokens
-
grun hello s -tree
-
grun hello s -gui
-
上面列出了常見TestRig參數,其他參數可見antlr官方文檔
Listener & Visitor
- Visitor和Listener是antlr提供的兩種樹遍歷機制。Listener是默認的機制,可以被antlr提供的遍歷器對象調用;如果要用Visitor機制,在生成識別器時就需要顯式說明
antlr4 -no-listener -visitor Calc.g4
,並且必須顯示的調用visitor方法遍歷它們的子節點,在一個節點的子節點上如果忘記調用visit方法,就意味着那些子數沒有得到訪問
Listener
文件結構
hello.tokens
helloBaseListener.java
helloLexer.java
helloLexer.tokens
helloListener.java
helloParser.java
ParserTreeWalker類是ANTLR運行時提供的用於遍歷語法分析樹和觸發Listener中回調方法的樹遍歷器。ANTLR工具根據hello.g4文法自動生成ParserTreeListener接口的子接口helloListener和默認實現helloBaseListener,其中含有針對語法中每個規則的enter和exit方法。
helloListener是語法和Listener對象之間的關鍵接口 public interface helloListener extends ParserTreeListener
helloBaseListener是ANTLR生成的一組空的默認實現。ANTLR內建的樹遍歷器會去觸發在Listener中像enterProg()和exitProg()這樣的一串回調方法
Visitor
文件結構
執行antlr4 -no-listener -visiter hello.g4
后,生成以下文件
hello.tokens
helloBaseVisitor.java
helloLexer.java
helloLexer.tokens
helloParser.java
helloVisitor.java
增加操作
@parser
grammer Rows;
@parser::members {
int col;
public RowsParser(TokenStream input, int col){
this(input);
this.col = col;
}
}
file: (row NL)+;
row
locals [int i=0]
: ( STUFF
{
$i++;
if ($i == col) System.out.println($STUFF.text);
}
)+
;
TAB : '\t' -> skip;
NL : '\r'? '\n';
STUFF: ~[\t\r\n]+;
- 操作時被花括號括起來的代碼段
- 上例中members操作的代碼將會被注入到生成的語法分析器類中的成員區;
- 規則row中的操作訪問\(i是由locals子句定義的局部變量,該操作也用\)STUFF.text獲取最近匹配的STUFF記號的文本內容
public class Rows{
public static void main(String[] args) throws Exception {
ANTLRInputStream input = new ANTLRInputStream(System.in);
RowsLexer lexer = new RowsLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
int col = Integer.valueOf(args[0]);
RowsParser parser = new RowsParser(tokens, col);
parser.setBuildParserTree(false);
parser.file();
}
}
語義謂詞
grammer IData;
file : group+;
group : INT sequence[$INT.int];
sequence[int n]
locals [int i = 1]
: ({$i <= $n}? INT {$i++})*
;
INT :[0-9]+;
WS :[ \t\n\r]+ -> skip;
被稱為語義謂詞的布爾值操作:{$i <= $n},當謂詞計算結果為true時,語法分析器匹配整數直到超過序列規則參數n要求的數量;當謂詞計算結果為false時,謂詞讓相關的選項從生成的語法分析器中“消失”。在這里,值為false的謂詞讓(...)*循環從規則序列里終止而退出。
注意:一定不能寫成以下的語法,語義偏差很大,因為\(INT總是返回最近匹配的INT,在下面的錯誤代碼中,匹配sequence時的第一個\)INT確實是sequence之前的INT,但之后都將變成上一個sequence操作中匹配的INT
grammer IData;
file : group+;
group : INT sequence;
sequence
locals [int i = 1]
: ({$i <= $INT.int}? INT {$i++})*
;
INT :[0-9]+;
WS :[ \t\n\r]+ -> skip;
詞法模型
同一文件的不同格式
基本思路:當詞法分析器看到特殊的哨兵字符序列時,讓它在不同模式之間切換
lexer grammar XMLLexer;
//默認模式
OPEN : '<' -> pushMode(INSIDE);
COMMEND : '<!--' .* '-->' -> skip;
EntityRef : '&' [a-z] ';';
TEXT : ~('<'|'&')+;
//INSIDE模式
mode INSIDE;
CLOSE : '>' -> popMode;
SLASH_CLOSE : '/>' -> popMode;
EQUALS : '=' ;
STRING : '"' .* '"';
SlashName : '/' Name;
Name : ALPHA (ALPHA|DIGIT)*;
S : [ \t\r\n] -> skip;
fragment
ALPHA : [a-zA-Z];
fragment
DIGIT : [0-9];
重寫輸入流
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.TokenStreamRewriter;
public class RewriteListener extends IDataBaseListener{
TokenStreamRewrite rewriter;
public RewriteListener(TokenStream tokens) {
rewriter = new TokentreamRewriter(tokens);
}
@Override
public void enterGroup(IDataParser.GroupContext ctx){
rewriter.insertAfter(ctx.stop, '\n');
}
}
以上代碼實現在捕獲到group的時候把換行符插到它末尾。
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import java.io.FileInputStream;
import java.io.InputStream;
public class IData {
InputStream is = args.length > 0 ? new FileInputStream(args[0]):System.in;
ANTLRInputStream input = new ANTLRInputStream(is); //CharStream
IDataLexer lexer = new IDataLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
IDataParser parser = new IDataParser(tokens);
ParserTree tree = parser.file();
RewriterListener listener = new RewriteListener(tokens);
System.out.println("Before Rewriting");
System.out.println(listener.reewriter.getText());
ParserTreeWalker walker = new ParserTreeWalker();
walker.walk(listener, tree);
System.out.println("After rewriting");
System.out.println(listener.rewriter.getText());
}
發送記號到不同的通道
COMMENT
: '/*' .*? '*/' -> channel(HIDDEN)
;
WS : [ \r\n\t]+ -> channel(HIDDEN)
;
優先級,左遞歸以及相關性
相關性
默認情況下,ANTLR從左到右結合運算符。使用assoc手動指定運算符記號上的相關性。
expr : <assoc=right>expr '^' expr
| INT
;
//指數表達式選項放在其他表達式選項之前,因為它的運算符比乘法和加法都有更高的優先級
expr : <assco=right>expr '^' expr
| expr '*' expr
| expr '+' expr
| INT
;
左遞歸
左遞歸規則是指直接或者間接調用在選項左邊緣的自身的規則。
ANTLR可以處理直接左遞歸,不能處理間接左遞歸
優先級
ANTLR詞法分析器通過偏愛首先指定的規則來解決詞法規則間的二義性,這意味着ID規則應該定義在所有的關鍵詞規則之后。
比如,乘除比加減優先級高,寫在加減的前面
expr: expr op=(MUL|DIV) expr # md_expr
| expr op=(ADD|SUB) expr # as_expr
| sign=(ADD|SUB)? NUM # number
| ID # id
| '(' expr ')' # parens
;
ANTLR把隱式的為字面量生成的詞法規則放在顯式的詞法規則之前,因此它們總是具有更高的優先級。
簡單計算器案例
- 用 visitor 模式實現的簡單計算器
- 實現支持加、減、乘、除、取余、冪運算的浮點數計算器,其中規定取余只能對有整數意義的對象操作,優先級及結合律的實現無誤
- 支持print語句、行注釋、塊注釋
grammar Calc;
prog: stat+;
stat: expr ';' # cal_stat
| ID '=' expr ';' # assign
| 'print' '(' expr ')' ';' # print
;
expr: <assco=right>expr POW expr # pow_expr
| expr op=(MUL|DIV|MOD) expr # md_expr
| expr op=(ADD|SUB) expr # as_expr
| sign=(ADD|SUB)? NUM # number
| ID # id
| '(' expr ')' # parens
;
NUM: INT
| FLOAT
;
MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
POW: '^';
MOD: '%';
ID: [a-zA-Z]+[0-9a-zA-Z]*;
ZERO: '0';
INT: [1-9][0-9]*
| ZERO
;
FLOAT: INT '.' [0-9]+
;
COMMENT_LINE: '//' .*? '\r'? '\n' -> skip;
COMMENT_BLOCK: '/*' .*? '*/' -> skip;
WS: [ \t\r\n] -> skip;
具體實現
- 浮點型的比較用BigDecimal提高精確度
- 浮點型格式化輸出用DecimalFormat實現
- 對除零、浮點數取余做了錯誤檢查
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
/**
* Created by wangqingyue on 2017/9/21.
*/
public class Calc extends CalcBaseVisitor<Double>{
Map<String, Double> memory = new HashMap<String, Double>();
// ID '=' expr ';'
@Override
public Double visitAssign(CalcParser.AssignContext ctx){
String id = ctx.ID().getText();
Double value = visit(ctx.expr());
memory.put(id, value);
return value;
}
// expr ';'
@Override
public Double visitCal_stat(CalcParser.Cal_statContext ctx){
Double value = visit(ctx.expr());
return value;
}
// 'print' '(' expr ')' ';'
@Override
public Double visitPrint(CalcParser.PrintContext ctx){
Double value = visit(ctx.expr());
DecimalFormat df = new DecimalFormat("#.###");
System.out.println(df.format(value));
return value;
}
// <assco=right>expr POW expr
@Override
public Double visitPow_expr(CalcParser.Pow_exprContext ctx){
Double truth = visit(ctx.expr(0));
Double power = visit(ctx.expr(1));
return Math.pow(truth, power);
}
// expr op=(MUL|DIV) expr
@Override
public Double visitMd_expr(CalcParser.Md_exprContext ctx){
Double left = visit(ctx.expr(0));
Double right = visit(ctx.expr(1));
if (ctx.op.getType() == CalcParser.MUL) return left * right;
else if (ctx.op.getType() == CalcParser.DIV){
if (new BigDecimal(right).compareTo(new BigDecimal(0.0)) == 0){
return 0.0;
}
return left / right;
}
else {
int left_int = (new Double(left)).intValue();
int right_int = (new Double(right).intValue());
if (new BigDecimal(right_int).compareTo(new BigDecimal(right)) == 0
&& new BigDecimal(left_int).compareTo(new BigDecimal(left_int)) == 0){
return Double.valueOf(left_int % right_int);
}
System.out.println("Mod\'%\' operations should be integer.");
return 0.0;
}
}
// expr op=(ADD|SUB) expr
@Override
public Double visitAs_expr(CalcParser.As_exprContext ctx){
Double left = visit(ctx.expr(0));
Double right = visit(ctx.expr(1));
if (ctx.op.getType() == CalcParser.ADD) return left + right;
else return left - right;
}
// sign=(ADD|SUB)? NUM
@Override
public Double visitNumber(CalcParser.NumberContext ctx){
int child = ctx.getChildCount();
Double value = Double.valueOf(ctx.NUM().getText());
if (child == 2 && ctx.sign.getType() == CalcParser.SUB){
return 0 - value;
}
return value;
}
// ID
@Override
public Double visitId(CalcParser.IdContext ctx){
String id = ctx.ID().getText();
if (memory.containsKey(id)) {
return memory.get(id);
}
System.out.println("undefined identifier \"" + id + "\".");
return 0.0;
}
//'(' expr ')'
@Override
public Double visitParens(CalcParser.ParensContext ctx){
Double value = visit(ctx.expr());
return value;
}
}
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;
/**
* Created by wangqingyue on 2017/9/21.
*/
public class Main {
public static void main(String[] args) throws Exception{
CharStream input = args.length > 0? CharStreams.fromFileName(args[0]): CharStreams.fromStream(System.in);
CalcLexer lexer = new CalcLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
CalcParser parser = new CalcParser(tokens);
ParseTree tree = parser.prog();
Calc calculator = new Calc();
calculator.visit(tree);
}
}
update at 2017/9/22
by 一顆球