空白文字のスキップ

引き続きASTは、タダじゃ作ってくれない。 - 設計と実装の狭間で。を添削。

予想以上に大変。

普通のやり方

Lexerのルールで、スキップしたいトークンに対して "{ $channel = HIDDEN; }" を指定すればOK。

WHITE_SPACE
  : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; }
  ;

これはLexerからParserにトークンが渡される際に乗せるチャネルを選ぶ機能で、HIDDENという傍流のチャネルにトークンを乗せればParserからは自動的に無視される。

しかし、これがヤヴァイ。

linecomment :
	C_LN (IDENT)+ (LT|EOF)
	;
...
LT	: '\n'		// Line feed.
	| '\r'		// Carriage return.
	| '\u2028'	// Line separator.
	| '\u2029'	// Paragraph separator.
	;

$channel = HIDDEN;なやり方だとLTがどうしてもParserに渡されないので、終端が分からなくなる。

Semantic Predicate

ANTLRにはSemantic Predicateという機能があって、要はLexerやらParserのルールを外側から捻じ曲げてあげることができます。

使い方は、ルールの中で "{}?" というガードを入れる感じ。このガード条件を満たさないと、ルールは先に進めなくなる。LLの挙動をしっかり理解するまでは、Semantic PredicateはLexerの中だけで使ったほうが安全かも。

ということで、行コメントの中にいる場合のみLTをHIDDENにしないようにする。

predicateしてみる

まず、行コメントのなかかどうかを表すフラグをLexerに追加。

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

次に、行コメントの開始でこのフラグをセット。

LINE_COMMENT_START
  : '--' { inLineComment = true; }
  ;

最後に、Semantic Predicateをうまく利用して、行コメントの終了とスキップするルールを作る。
行コメントが終了したら、フラグはリセットしておく。

WHITE_SPACE
  : ( ' ' | '\t' )+ { $channel = HIDDEN; }
  ;
NEW_LINE
  : {!inLineComment}? ( '\r' | '\n' )+ { $channel = HIDDEN; }
  ;
LINE_COMMENT_END
  : {inLineComment}? ( '\r\n' | '\r' | '\n' ) { inLineComment = false; }
  ;

さんぷる

読むというより、Parse Treeを眺めていただければと。

サンプル入力は↓。意図的に添削先とは文法を似せつつ趣旨を変えてます。

/* PRT 0 */
if (/* 100 */ hoge)
  -- IF a
    print 200;
  -- ELSE
    print /* 300 + 2 * 3 */ foo;
else
  print /* 400 + 500 */ bar;

ちょっと力入れすぎた。

grammar SimpleTwoway;

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

//// Way-1 
program
  : w1_statement EOF
  ;

w1_statement
  :
    w2_statement_decorator?
    ( w1_print_statement
    | w1_if_statement
    | w1_empty_statement
    | w2_statement
    )
  ;

w1_print_statement
  : 'print' w1_expression ';'
  ;

w1_if_statement
  : 'if' '(' w1_expression ')' w1_statement
    ( 'else' w1_statement
    | /* empty */
    )
  ;

w1_empty_statement
  : ';'
  ;

w1_expression
  : w1_term
    ( '+' w1_term
    | '-' w1_term
    )*
  ;

w1_term
  : w1_unary
    ( '*' w1_unary
    | '/' w1_unary
    | '%' w1_unary
    )*
  ;

w1_unary
  : w1_factor
  | '-' w1_factor
  | '+' w1_factor
  ;

w1_factor
  :
    w2_expression_decorator?
    ( ident
    | number
    | '(' w1_expression ')'
    )
  ;

ident
  : IDENT
  ;
  
number
  : NUMBER
  ;

// Way-2
w2_statement
  :
    ( BLOCK_COMMENT_START 'IF' w2_expression BLOCK_COMMENT_END
    | LINE_COMMENT_START 'IF' w2_expression LINE_COMMENT_END
    )
    w1_statement
    (
      ( BLOCK_COMMENT_START 'ELSE' 'IF' w2_expression BLOCK_COMMENT_END
      | LINE_COMMENT_START 'ELSE' 'IF' w2_expression LINE_COMMENT_END
      )
      w1_statement
    )*
    ( BLOCK_COMMENT_START 'ELSE' BLOCK_COMMENT_END
    | LINE_COMMENT_START 'ELSE' LINE_COMMENT_END
    )
    w1_statement
  ;

w2_statement_decorator
  : w2_print_statement
  ;

w2_expression_decorator
  : BLOCK_COMMENT_START w2_expression BLOCK_COMMENT_END
  ;

w2_print_statement
  : BLOCK_COMMENT_START 'PRT' w2_expression BLOCK_COMMENT_END
  | LINE_COMMENT_START 'PRT' w2_expression LINE_COMMENT_END
  ;

w2_expression
  : w2_term
    ( '+' w2_term
    | '-' w2_term
    )*
  ;

w2_term
  : w2_unary
    ( '*' w2_unary
    | '/' w2_unary
    | '%' w2_unary
    )*
  ;

w2_unary
  : w2_factor
  | '-' w2_factor
  | '+' w2_factor
  ;

w2_factor
  : ident
  | number
  | '(' w2_expression ')'
  ;

//// Common tokens
IDENT
  : IDENT_START ( IDENT_START | DIGIT )*
  ;

NUMBER
  : '0'
  | NONZERO_DIGIT DIGIT*
  ;
  
fragment
IDENT_START
  : 'A'..'Z' | 'a'..'z' | '_'
  ;

fragment
DIGIT
  : '0' | NONZERO_DIGIT
  ;

fragment
NONZERO_DIGIT
  : '1'..'9'
  ;

//// comment bounds

// cannot be declared in other comments
BLOCK_COMMENT_START
  : {!inComment}? '/*' { inComment = true; }
  ;

// cannot be declared in line comments
BLOCK_COMMENT_END
  : {!inLineComment}? '*/' { inComment = false; }
  ;

// cannot be declared in other comments
LINE_COMMENT_START
  : {!inComment}? '--' { inLineComment = true; inComment = true; }
  ;

// cannot be declared in other line comments
LINE_COMMENT_END
  : {inLineComment}? ( '\r\n' | '\r' | '\n' | EOF ) { inLineComment = false; inComment = false; }
  ;

//// whitespaces
WHITE_SPACE
  : ( ' ' | '\t' )+ { $channel = HIDDEN; }
  ;

NEW_LINE
  : {!inLineComment}? ( '\r' | '\n' )+ { $channel = HIDDEN; }
  ;