ANTLR快餐教程(2) - ANTLR其實很簡單

分類:IT技術 時間:2017-04-01

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.

ParseTree結構圖

為了節省空間,Token流之上都沒有復制字符流的內容,都是通過指向字符流區緩沖區來獲取內容。空白字符在Token流以上就不存在了。

既然有了ParseTree,後面的事情就好辦了。我們只要遍歷這棵ParseTree,就可以訪問所有的節點,然後繼續做代碼生成之類的後端的工作。

為了方便使用,ANTLR將這些節點,封裝成RuleNode的子類,前面代碼中我們看到的xxxContext類,就是這些子類。比如AssignContext,ExprContext等。

具體的接口,請看圖:

ANTLR ParseTree類結構圖

我們看個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 快餐 字母

文章來源:


ads
ads

相關文章
ads

相關文章

ad