Tips - String literal in block comments

どこかで役に立てば、という程度の無駄知識。

ポイントは、

/*eLsE ccc + 44 '*/' */

で、コメントが変な風に中断したり

空白のスキップとクォート出来る様にしてみた。 - 設計と実装の狭間で。

というところで、逆にコメントを変な風に中断する方法について。
実はコメントを変な風に中断するほうが難しい例です。

Javaでの文字列リテラル

Javaでの文字列リテラルはこんな感じで書きます。

STRING
  : '"' STRING_ELEMENT* '"'
  ;
  
fragment
STRING_ELEMENT
  : ~( '"' | '\\' | '\r' | '\n' )
  | ESCAPE_SEQUENCE
  ;

fragment
ESCAPE_SEQUENCE
  : '\\'
    ( ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    | 'u'+ HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
    | OCT_DIGIT
    | OCT_DIGIT OCT_DIGIT
    | ( '0'..'3' ) OCT_DIGIT OCT_DIGIT
    )
  ;

fragment
OCT_DIGIT
  : '0'..'7'
  ;

fragment
HEX_DIGIT
  : '0'..'9' | 'a'..'f' | 'A'..'F'
  ;

STRING_ELEMENTの解釈は下のような感じで、""内に出現する1文字分と解釈します。

fragment
STRING_ELEMENT
  : ~( '"' | '\\' | '\r' | '\n' )
  | ESCAPE_SEQUENCE
  ;

fragment
ESCAPE_SEQUENCE
  : '\\'
    ( ('b'|'t'|'n' ...

つまり、

  1. ", \, [CR], [LF] のいずれでもない : ~( '"' | '\\' | '\r' | '\n' )
  2. \から始まるエスケープシーケンスである : '\\' ...

のいずれかです。

ただしこれだと、コメント内に"*/"という文字列があった場合、

/* "*/"".toString(); /* */

セマンティクスが盛大に変わってしまいます。

「*/」を含まない文字列

ブロックコメント内で 「*/」 が出現した場合、そこでブロックコメントが終了します。
まず、sematic predictionを利用して、ブロックコメントにいる場合とそうでない場合のルールを分岐します。

STRING
  : { !inComment || inLineComment }? '"' STRING_ELEMENT* '"'
  | { inComment && !inLineComment }? '"' STRING_BODY_IN_COMMENT '"'
  ;

これで、STRING_BODY_IN_COMMENTがブロックコメント内での""内に出現する内容になります。

2文字以上からなる例外をANTLRの構文で書くのは実は非常にめんどうで、下のようには書けません

fragment
STRING_BODY_IN_COMMENT
  : STRING_ELEMENT_IN_COMMENT*
  ;

fragment
STRING_ELEMENT_IN_COMMENT
  : ~( '"' | '\\' | '\r' | '\n' | '*/' ) // BAD RULE
  | ESCAPE_SEQUENCE
  ;

1文字を除くだけなら ~X の形式で書けばよいのですが、2文字以上の場合はうまくいきません。
ということで、ちゃんとオートマトンを意識して書かないとだめぽ。

オートマトン

文字の遷移状態を元に、ブロックコメント内に出現可能なサブ文字列を考えます。

  1. 1文字目が"*"でないサブ文字列
    1. ", \, [CR], [LF], * のいずれでもない : ~( '"' | '\\' | '\r' | '\n' | '*' )
    2. \から始まるエスケープシーケンスである : '\\' ...
  2. 1文字目が"*"であるサブ文字列
    1. "*" から始まり、末尾が ", \, [CR], [LF], /, * のいずれでもない : '*'+ ~( '"' | '\\' | '\r' | '\n' | '/' | '*' )
    2. "*" から始まり、末尾が エスケープシーケンスである : '*'+ '\\' ...
    3. "*" から始まり、後続する文字列が存在しない : '*'+ (終端)

上記を網羅すれば、「*/」というパターンを排除した文字列リテラルを指定できます。

fragment
STRING_BODY_IN_COMMENT
  : STRING_ELEMENT_IN_COMMENT* ( '*'+ )? // 2-3
  ;

fragment
STRING_ELEMENT_IN_COMMENT
  : ~( '"' | '\\' | '\r' | '\n' | '*' ) // 1-1
  | ESCAPE_SEQUENCE // 1-2
  | '*'+ ~( '"' | '\\' | '\r' | '\n' | '*' | '/' ) // 2-1
  | '*'+ ESCAPE_SEQUENCE // 2-2
  ;

2-3がちょっとトリッキーな感じです。STRING_BODY_IN_COMMENTの最後に( '*'+ )?と書くことによって、末尾に1文字以上の"*"があってもよい、ということで「"*" から始まり、後続する文字列が存在しない」というルールの可能性を表現します。

読みやすいようにリファクタリングすると、最終的にこうなります。

STRING
  : { !inComment || inLineComment }? '"' STRING_ELEMENT* '"'
  | { inComment && !inLineComment }? '"' STRING_ELEMENT_IN_COMMENT* '*'* '"'
  ;
  
fragment
STRING_ELEMENT
  : ~( '"' | '\\' | '\r' | '\n' )
  | ESCAPE_SEQUENCE
  ;

fragment
STRING_ELEMENT_IN_COMMENT
  : ~( '"' | '\\' | '\r' | '\n' | '*' )
  | ESCAPE_SEQUENCE
  | '*'+ ~( '"' | '\\' | '\r' | '\n' | '*' | '/' )
  | '*'+ ESCAPE_SEQUENCE
  ;

こんなのでテストしました。

"OK"
-- "OK"
/* "OK" */
"OK: */"
-- "OK: */"
/* "OK: /*" */
/* "OK: *?" */
/* "NG!: */" */

最後のルールだけ、"NG!: */"というトークンでエラーになります。