ANTLR其實很簡單
ANTLR是通過遞歸下降的方式來解析一個語法的。
所謂的遞歸下降,其實很簡單,不過就是一些模式匹配而己。
簡單的模式匹配
我們看下官方的一個簡單的例子,這是一個賦值表達式的例子。
語法這樣寫:
assign : ID '=' expr ';' ;
解析器的代碼類似於下面這樣:
void assign() { match(ID); match('='); expr(); match(';');
解析只分為兩種情況:第一種情況是直接模式匹配,第二種情況是調用其它函數繼續分析。
我們寫個完整的賦值語句的語法吧。我們簡化一下,先不做遞歸下降,將表達式化簡成只支持數字:
grammar assign; assign : ID '=' expr ';' ; ID : [a-z]+ ; expr : NUMBER ; NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;
ID我們簡化成只支持小寫字母的組合,數字我們寫個比較詳細的。
上面的代碼存成assign.g4,用antlr4 assign.g4命令,就可以生成Java解析器代碼了。
我們來看看生成的parser中的片段,跟上面的像不像:
public final AssignContext assign() throws RecognitionException { AssignContext _localctx = new AssignContext(_ctx, getState()); enterRule(_localctx, 0, RULE_assign); try { enterOuterAlt(_localctx, 1); { setState(4); match(ID); setState(5); match(T__0); setState(6); expr(); setState(7); match(T__1); } } catch (RecognitionException re) { _localctx.exception = re; _errHandler.reportError(this, re); _errHandler.recover(this, re); } finally { exitRule(); } return _localctx; }
下面是解析expr的情況:
public final ExprContext expr() throws RecognitionException { ExprContext _localctx = new ExprContext(_ctx, getState()); enterRule(_localctx, 2, RULE_expr); try { enterOuterAlt(_localctx, 1); { setState(9); match(NUMBER); } } catch (RecognitionException re) { _localctx.exception = re; _errHandler.reportError(this, re); _errHandler.recover(this, re); } finally { exitRule(); } return _localctx; }
多種分支的情況
如果有多種可能的話,在語法裏用”|”符號分別列出來就是了。ANTLR會把它翻譯成switch case一樣的語句。
我們把我們上面的例子擴展一下,不光支持’=’還支持’:=’賦值
grammar assign2; assign : ID '=' expr ';' | ID ':=' expr ';' ; ID : [a-z]+ ; expr : NUMBER ; NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;
生成的Parser就變成switch case了:
public final AssignContext assign() throws RecognitionException { AssignContext _localctx = new AssignContext(_ctx, getState()); enterRule(_localctx, 0, RULE_assign); try { setState(14); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,0,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { setState(4); match(ID); setState(5); match(T__0); setState(6); expr(); setState(7); match(T__1); } break; case 2: enterOuterAlt(_localctx, 2); { setState(9); match(ID); setState(10); match(T__2); setState(11); expr(); setState(12); match(T__1); } break; } } catch (RecognitionException re) { _localctx.exception = re; _errHandler.reportError(this, re); _errHandler.recover(this, re); } finally { exitRule(); } return _localctx; }
這次我們直接看java語法的例子:
typeDeclaration : classOrinterfaceModifier* classDeclaration | classOrInterfaceModifier* enumDeclaration | classOrInterfaceModifier* interfaceDeclaration | classOrInterfaceModifier* annotationTypeDeclaration | ';' ;
上面的語法在:https://github.com/antlr/grammars-v4/blob/master/java/Java.g4 中,我們把它下載下來,用antlr4 Java.g4運行一下,就生成了Lexer和Parser類。
由於是真的語法,翻出來比起純粹的例子自然是復雜一些,不過不考慮細節,整個結構上還是很好懂的。大家只要理解這套switch case的結構就好:
... try { int _alt; setState(269); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,10,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { setState(243); _errHandler.sync(this); _la = _input.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << ABSTRACT) | (1L << FINAL) | (1L << PRIVATE) | (1L << PROTECTED) | (1L << PUBLIC) | (1L << STATIC) | (1L << STRICTFP))) != 0) || _la==AT) { { { setState(240); classOrInterfaceModifier(); } } setState(245); _errHandler.sync(this); _la = _input.LA(1); } setState(246); classDeclaration(); } break; case 2: enterOuterAlt(_localctx, 2); { setState(250); _errHandler.sync(this); _la = _input.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << ABSTRACT) | (1L << FINAL) | (1L << PRIVATE) | (1L << PROTECTED) | (1L << PUBLIC) | (1L << STATIC) | (1L << STRICTFP))) != 0) || _la==AT) { { { setState(247); classOrInterfaceModifier(); } } setState(252); _errHandler.sync(this); _la = _input.LA(1); } setState(253); enumDeclaration(); } break; case 3: enterOuterAlt(_localctx, 3); { setState(257); _errHandler.sync(this); _la = _input.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << ABSTRACT) | (1L << FINAL) | (1L << PRIVATE) | (1L << PROTECTED) | (1L << PUBLIC) | (1L << STATIC) | (1L << STRICTFP))) != 0) || _la==AT) { { { setState(254); classOrInterfaceModifier(); } } setState(259); _errHandler.sync(this); _la = _input.LA(1); } setState(260); interfaceDeclaration(); } break; case 4: enterOuterAlt(_localctx, 4); { setState(264); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,9,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { setState(261); classOrInterfaceModifier(); } } } setState(266); _errHandler.sync(this); _alt = getInterpreter().adaptivePredict(_input,9,_ctx); } setState(267); annotationTypeDeclaration(); } break; case 5: enterOuterAlt(_localctx, 5); { setState(268); match(SEMI); } break; } } ...
二義性文法
選擇太多了也未必見得是好事兒,有一種副作用就是選擇不是唯一的,這叫做『二義性文法』。
最簡單的二義性文法就是把同一條規則寫兩遍,比如上面例子的”:=”我們就改成”=”,讓”|”之前和之後兩條都一樣。
grammar assign2; assign : ID '=' expr ';' | ID '=' expr ';' ; ID : [a-z]+ ; expr : NUMBER ; NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;
但是ANTLR4是兼容這種情況的,不報錯。在實際應用的時候,它選擇第一條符合條件的規則,請看生成的代碼
try { setState(14); _errHandler.sync(this); switch ( getInterpreter().adaptivePredict(_input,0,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { setState(4); match(ID); setState(5); match(T__0); setState(6); expr(); setState(7); match(T__1); } break; case 2: enterOuterAlt(_localctx, 2); { setState(9); match(ID); setState(10); match(T__0); setState(11); expr(); setState(12); match(T__1); } break; } }
最著名的二義性的例子就是關鍵字。在常見的編程語言中,關鍵字都是和標識符沖突的.
比如我們定義一個if關鍵字:
IF : 'if' ; ID : [a-z]+ ;
明顯,IF和ID兩個規則都可以解析’if’這個串,那到底是按IF算,還是按ID算呢?在ANTLR裏,規則很簡單,按照可以匹配的第一條處理。
但是,光靠第一條優先,也還是解決不了所有的問題。
我們看兩類新的問題
第一類:
1 + 2 * 3。這個如何處理,是先算+還是先算*?
前人想出了三種辦法來解決:
* 從左到右:管人是如何理解乘除加減的,我就從左到右算。Smalltalk就是這樣做的
* 中綴轉前綴:帶來問題的是中綴表達式,我們給換個形式不就OK了嗎,比如改成這樣
(+ 1 (* 2 3)),lisp就是這麽做的
* 運算符優先級:最常用的一種作法,後面我們詳情分析。基本上大部分常見的語言都有一個運算符優先級的表。
第二類,是一些語言的設計所導致的,給詞法分析階段帶來困難。
比如”*”運算符,在大部分語言中都只表示乘法,但是在C語言中表示指針,當
i*j時,表示乘法,但是當
int *j;時,就變成表示指針。
所以像Go語言在設計時,就把類型定義移到了後面。我們入門階段暫時也不打算解析這麽復雜的,將來用到了再說。
下一步做啥
經過前面學習的寫grammar的過程,我們可以把字符流CharStream,轉換成一棵ParseTree。
CharStream是字符流,經過詞法分析會變成Token流。
Token流再最終組裝成一棵ParseTree,葉子節點是TerminalNode,非葉子節點是RuleNode.
為了節省空間,Token流之上都沒有復制字符流的內容,都是通過指向字符流區緩沖區來獲取內容。空白字符在Token流以上就不存在了。
既然有了ParseTree,後面的事情就好辦了。我們只要遍歷這棵ParseTree,就可以訪問所有的節點,然後繼續做代碼生成之類的後端的工作。
為了方便使用,ANTLR將這些節點,封裝成RuleNode的子類,前面代碼中我們看到的xxxContext類,就是這些子類。比如AssignContext,ExprContext等。
具體的接口,請看圖:
我們看個AssignContext是如何被實現的:
public static class AssignContext extends ParserRuleContext { public TerminalNode ID() { return getToken(assign2Parser.ID, 0); } public ExprContext expr() { return getRuleContext(ExprContext.class,0); } public TerminalNode IF() { return getToken(assign2Parser.IF, 0); } public AssignContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_assign; } @Override public void enterRule(ParseTreeListener listener) { if ( listener instanceof assign2Listener ) ((assign2Listener)listener).enterAssign(this); } @Override public void exitRule(ParseTreeListener listener) { if ( listener instanceof assign2Listener ) ((assign2Listener)listener).exitAssign(this); } }
兩種訪問ParserTree的方法
ANTLR提供了兩種方法來訪問ParseTree:
* 一種是通過Parse-Tree Listener的方法
* 另一種是通過Parse-Tree Visitor的方法
Listener方法有點類似於解析XML的SAX方法。
廢話不多說了,這篇文章已經有點長了,我們直接上代碼:
// Generated from assign2.g4 by ANTLR 4.6 import org.antlr.v4.runtime.tree.ParseTreeListener; /** * This interface defines a complete listener for a parse tree produced by * {@link assign2Parser}. */ public interface assign2Listener extends ParseTreeListener { /** * Enter a parse tree produced by {@link assign2Parser#assign}. * @param ctx the parse tree */ void enterAssign(assign2Parser.AssignContext ctx); /** * Exit a parse tree produced by {@link assign2Parser#assign}. * @param ctx the parse tree */ void exitAssign(assign2Parser.AssignContext ctx); /** * Enter a parse tree produced by {@link assign2Parser#expr}. * @param ctx the parse tree */ void enterExpr(assign2Parser.ExprContext ctx); /** * Exit a parse tree produced by {@link assign2Parser#expr}. * @param ctx the parse tree */ void exitExpr(assign2Parser.ExprContext ctx); }
開始解析Assign的時候,會回調etnterAssign方法,結束時回調exitAssign方法。
另一種是采用visitor模式的方法,我們調用antlr4的時候要增加
-visitor參數來生成。
Visitor仍然非常簡單,我們直接看代碼:
// Generated from assign2.g4 by ANTLR 4.6 import org.antlr.v4.runtime.tree.ParseTreeVisitor; /** * This interface defines a complete generic visitor for a parse tree produced * by {@link assign2Parser}. * * @param <T> The return type of the visit operation. Use {@link Void} for * operations with no return type. */ public interface assign2Visitor<T> extends ParseTreeVisitor<T> { /** * Visit a parse tree produced by {@link assign2Parser#assign}. * @param ctx the parse tree * @return the visitor result */ T visitAssign(assign2Parser.AssignContext ctx); /** * Visit a parse tree produced by {@link assign2Parser#expr}. * @param ctx the parse tree * @return the visitor result */ T visitExpr(assign2Parser.ExprContext ctx); }
好的,基本概念已經準備好了,下一篇教程我們就正式利用這些組件來實現了一個解析器。
結束之前,我們搞個能運行的調用前面語法解析器的例子,最終生成一棵ParseTree.
語法文件再列一遍,省得大家向上翻了:
grammar Assign; assign : ID '=' expr ';' | ID ':=' expr ';' ; ID : [a-z]+ ; expr : NUMBER ; NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ; WS : [ \t\r\n]+ -> skip ;
調用
antlr4 Assign.g4,然後寫個調用的main方法吧:
import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.tree.*; public class TestAssign { public static void main(String[] args) throws Exception { ANTLRInputStream input = new ANTLRInputStream(system.in); AssignLexer lexer = new AssignLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); AssignParser parser = new AssignParser(tokens); ParseTree tree = parser.assign(); System.out.println(tree.toStringTree(parser)); } }
試試靈不靈吧:
java TestAssign a = 1;
輸出如下:
(assign a = (expr 1) ;)
再試一個用:=賦值的:
java TestAssign b := 0;
輸出如下:
(assign b := (expr 0) ;)
很好玩吧?雖然例子很簡單,但是我們已經完成了從寫語法規則到使用ParseTree的全過程。
Tags: public 表達式 java 快餐 字母
文章來源: