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メソッドをオーバーライドすればぁぁ?とか。ふざけとる。
こんな変りそうなトコロをオーバーライドしたら、ライブラリのバージョンアップ出来んくなるじゃないか。
と、言う訳。