Semantic Predicate が呼び込む恐怖の無限ループ

何だか随分前から、ANTLRWorksを使っていると、
デバッグモードでステップ実行出来ない事があったのだけど、
何が起きているのか分らなかったのだよね。


文法定義がおかしいのか、ANTLR3.1.1がバグっているのか、
良く分からないので、記録的な意味合いで晒しておく。
一応、回避的な措置は存在するので、何とも言えない感じ。



まず、文法定義。

grammar SemanticPredicate;

@lexer::members {
boolean inComment = false;
boolean inLineComment = false;
}

comment : blockcomment | linecomment;

blockcomment : C_ST charactors C_ED;

linecomment : C_LN_ST charactors C_LN_ED;

charactors : (IDENT | SYMBOLS)+ ;

SYMBOLS	: '*' | '/' | '-' | '#';

C_ST : {!inComment}? '/*' { inComment = true; };
C_ED : {inComment}? '*/' { inComment = false; };
C_LN_ST	: {!inComment}? ('--'|'#') { inLineComment = true; inComment = true; };
C_LN_ED	: {inLineComment}? ( LN_R? LN_N | EOF ) { inLineComment = false; inComment = false; };
IDENT	: CHAR+;

fragment WS : '\t' | ' ';
fragment LN_R	: '\r';
fragment LN_N	: '\n';
fragment CHAR	: ~(SYMBOLS  | LN_R | LN_N | WS);

// $<Hidden
LT : {!inLineComment}? (LN_R? LN_N)+ { $channel = HIDDEN; };
WHITE_SPACES	: (WS)+ { $channel = HIDDEN; };
// $>

はい、semantic predicate をブン回して、コメント部分をパースするパーザでつね。
こいつに、こんなテキストを食わせてみるます。

/* aaa /*

はい、返ってきません。ファンタスティック。
何が起きているのかと言うと、
内部的には、FailedPredicateException と言う例外がすっ飛んで、エラー出力に、メッセージ出力されているます。
しかし、そのメッセージは、どこか亜空間に飲み込まれてしまっている為、ANTLRWorksを使っていると確認する事が出来ません。
仕方がないので、テスト用のコードを書きます。
こんな感じ。

public class Main {
  public static void main(String[] args) {
    String string = "/* aaa /*";
    ANTLRStringStream src = new ANTLRStringStream(string);
    SemanticPredicateLexer lex = new SemanticPredicateLexer(src);
    CommonTokenStream stream = new CommonTokenStream(lex);
    Token t = stream.LT(1);
    System.out.println(t);
  }
}

実行すると半永久的に、こんなのが出ます。

line 1:7 rule C_ST failed predicate: {!inComment}?
line 1:7 rule C_ST failed predicate: {!inComment}?
line 1:7 rule C_ST failed predicate: {!inComment}?
line 1:7 rule C_ST failed predicate: {!inComment}?
line 1:7 rule C_ST failed predicate: {!inComment}?

あんびりーばぼー。では、何故この様な事になるのでしょうか。
まず直接的に例外が送出されている個所を見てみる事にします。

public final void mC_ST() throws RecognitionException {
    try {
        int _type = C_ST;
        int _channel = DEFAULT_TOKEN_CHANNEL;
        {
        if ( !((!inComment)) ) {
            throw new FailedPredicateException(input, "C_ST", "!inComment");
        }
        match("/*"); 

         inComment = true; 

        }
        state.type = _type;
        state.channel = _channel;
    }
    finally {
    }
}

圧縮されたDFAに基づいて、mC_STと言う状態にきた時、レキサーの状態がおかしな事になっていると、
FailedPredicateExceptionが送出されます。
リカバリする為のフック等はありません。ふざけとる。
っつうか、何だってfinalメソッドなのか。ふざけとる。
ちなみに、MismatchedSetExceptionが送出される時は、リカバリする為のフックがあります。
こんな感じ。

public final void mSYMBOLS() throws RecognitionException {
    try {
        int _type = SYMBOLS;
        int _channel = DEFAULT_TOKEN_CHANNEL;
        {
          if ( input.LA(1)=='#'||input.LA(1)=='*'||input.LA(1)=='-'||input.LA(1)=='/' ) {
              input.consume();
          }
          else {
              MismatchedSetException mse = new MismatchedSetException(null,input);
              recover(mse);
              throw mse;
          }
        }
        state.type = _type;
        state.channel = _channel;
    }
    finally {
    }
}

ははーん。
問題は、自動生成されるレキサーの親クラスであるorg.antlr.runtime.Lexer#nextTokenにあります。

public Token nextToken() {
  while (true) {
    state.token = null;
    state.channel = Token.DEFAULT_CHANNEL;
    state.tokenStartCharIndex = input.index();
    state.tokenStartCharPositionInLine = input.getCharPositionInLine();
    state.tokenStartLine = input.getLine();
    state.text = null;
    if ( input.LA(1)==CharStream.EOF ) {
      return Token.EOF_TOKEN;
    }
    try {
      mTokens();
      if ( state.token==null ) {
        emit();
      }
      else if ( state.token==Token.SKIP_TOKEN ) {
        continue;
      }
      return state.token;
    }
    catch (NoViableAltException nva) {
      reportError(nva);
      recover(nva); // throw out current char and try again
    }
    catch (RecognitionException re) {
      reportError(re);
      // match() routine has already called recover()
    }
  }
}

残念な事にリカバリされずに、RecognitionException が宣言されているcatch節に来る場合があるんだなぁ。
それが、FailedPredicateExceptionが送出される場合。あーあ。
何が残念かって言うとだ、このメソッドは例外を送出しない事になってるんだよねぇ。痺れるね。
で、何か情報がないかなぁ…と思って、FAQとか見る訳さ。

レキサーで、nextTokenメソッドをオーバーライドすればぁぁ?とか。ふざけとる。
こんな変りそうなトコロをオーバーライドしたら、ライブラリのバージョンアップ出来んくなるじゃないか。


と、言う訳。