Objective-Cでのプリプロセッサディレクティブの処理

この場合、ディレクティブの値を計算し、コンパイルされていないコードフラグメントを切り取り、クリーンなコードを解析する必要があるため、 プリプロセッサディレクティブを含むプログラミング言語は処理が困難です。 通常のコードの解析中にディレクティブ処理が発生する場合があります。 この記事では、Objective-C言語に関する両方のアプローチについて詳しく説明し、それらの利点と欠点を明らかにします。 これらのアプローチは、理論上だけでなく、SwiftifyやCodebeatなどのWebサービスですでに実装され、実際に使用されています。









Swiftifyは、ソースファイルをObjective-CからSwiftに変換するためのWebサービスです。 現時点では、サービスは単一ファイルとプロジェクト全体の両方の処理をサポートしています。 これにより、Appleの新しい言語を学びたい開発者の時間を節約できます。









Codebeatは、コードメトリックを計算し、Objective-Cを含むさまざまなプログラミング言語を分析するための自動化されたシステムです。













内容







はじめに



プリプロセッサディレクティブは、コード解析中に処理されます。 解析の基本概念については説明しませんが、ここでは、ANTLRとRoslynを使用したソースコードの理論と解析に関する記事の用語を使用します。 両方のサービスは、パーサージェネレーターとしてANTLRを使用し、Objective-C文法は、公式のANTLR文法リポジトリ( Objective-C文法 )に配置されます。







プリプロセッサディレクティブを処理する2つの方法を特定しました。











ワンステップ処理



ワンステップ処理には、メイン言語のディレクティブとトークンの同時解析が含まれます。 ANTLRには、さまざまなタイプのトークンを分離できるチャネルメカニズムがあります。たとえば、メイン言語のトークンと隠されたトークン(コメントとスペース)です。 ディレクティブトークンは、別の名前付きパイプに配置することもできます。







通常、ディレクティブトークンはポンド記号( #



またはシャープ)で始まり、改行文字( \r\n



)で終わります。 したがって、このようなトークンをキャプチャするには、異なるトークン認識モードを使用することをお勧めします。 ANTLRはこのようなモードをサポートします。それらは次のように説明されmode DIRECTIVE_MODE;



。 プリプロセッサディレクティブのモードセクションを持つレクサーのフラグメントは次のとおりです。







 SHARP: '#' -> channel(DIRECTIVE_CHANNEL), mode(DIRECTIVE_MODE); mode DIRECTIVE_MODE; DIRECTIVE_IMPORT: 'import' [ \t]+ -> channel(DIRECTIVE_CHANNEL), mode(DIRECTIVE_TEXT_MODE); DIRECTIVE_INCLUDE: 'include' [ \t]+ -> channel(DIRECTIVE_CHANNEL), mode(DIRECTIVE_TEXT_MODE); DIRECTIVE_PRAGMA: 'pragma' -> channel(DIRECTIVE_CHANNEL), mode(DIRECTIVE_TEXT_MODE);
      
      





Objective-Cプリプロセッサディレクティブの一部は、特定のSwiftコードに変換されます(たとえば、 let構文を使用)。一部は変更されずに残り、残りはコメントに変換されます。 次の表に例を示します。







Objective-c スイフト
#define SERVICE_UUID @ "c381de0d-32bb-8224-c540-e8ba9a620152"



let SERVICE_UUID = "c381de0d-32bb-8224-c540-e8ba9a620152"



#define ApplicationDelegate ((AppDelegate *)[UIApplication sharedApplication].delegate)



let ApplicationDelegate = (UIApplication.shared.delegate as? AppDelegate)



#define DEGREES_TO_RADIANS(degrees) (M_PI * (degrees) / 180)



func DEGREES_TO_RADIANS(degrees: Double) -> Double { return (.pi * degrees)/180; }



#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)



#if __IPHONE_OS_VERSION_MIN_REQUIRED



#pragma mark - Directive between comments.



// MARK: - Directive between comments.





コメントは、結果のSwiftコードの正しい位置にも配置する必要があります。 ただし、既に述べたように、非表示トークン自体は解析ツリーにありません。







解析ツリーに非表示のトークンを含めるとどうなりますか?

実際、隠されたトークンは文法に含めることができますが、このため、複雑すぎて冗長になります。 COMMENT



およびDIRECTIVE



トークンは、重要なトークンの間の各ルールに含まれます。







 declaration: property COMMENT* COLON COMMENT* expr COMMENT* prio?;
      
      





したがって、このアプローチをすぐに忘れることができます。







質問が発生します:解析ツリーを走査するときに、どのようにしてそのようなトークンを抽出できますか?







判明したように、この問題を解決するためのオプションがいくつかあります。このオプションでは、非表示のトークンが解析ツリーの非終端ノードまたは終端(終端)ノードに関連付けられます。









隠されたトークンと非終端ノードの関連付け



この方法は、比較的古い2012 ANTLR 3の記事から引用されています。







この場合、すべての非表示トークンは次のタイプのセットに分割されます。









これらのタイプの意味をよりよく理解するには、中括弧が終端文字であり、 statement



が末尾にセミコロンを含む任意の式であるという単純なルールを考えa = b;



例えば、 a = b;









 root : '{' statement* '}' ;
      
      





この場合、次のコードフラグメントのすべてのコメントが先頭のリストに含まれます。 ファイル内の最初のトークン、または解析ツリーの非終端ノードの前のトークン。







 /*First comment*/ '{' /*Precending1*/ a = b; /*Precending2*/ b = c; '}'
      
      





コメントがファイルの最後の場合、またはコメントがすべてのstatement



後に挿入された場合(その後に端末括弧が続く)、コメントは次のリストに分類されます。







 '{' a = b; b = c; /*Following*/ '}' /*Last comment*/
      
      





その他のコメントはすべて孤立リストに含まれます(これらは基本的にトークンで区切られ、この場合は中括弧で囲まれています)。







 '{' /*Orphan*/ '}'
      
      





この分割のおかげで、すべての隠されたトークンは一般的なVisit



メソッドで処理できます。 このメソッドはまだSwiftifyで使用されていますが、非常に複雑であり、それを使用して忠実度解析ツリーを構築するのは問題です。 ツリーの信頼性は、スペース、コメント、プリプロセッサディレクティブなど、文字から文字へのコードに変換できるという事実にあります。 将来的には、プリプロセッサディレクティブと他の隠されたトークンを処理する方法の使用に切り替える予定です。これについては、以下で説明します。









隠されたトークンとターミナルノードの関連付け



この場合、非表示トークンは特定の重要なトークンに関連付けられます。 同時に、隠されたトークンは先頭 (LeadingTrivia)と末尾 (TrailingTrivia)になります。 このメソッドは現在、Roslynパーサー(C#およびVisual Basic用)で使用されており、その中の隠されたトークンはトリビア(トリビア)と呼ばれています。







重要なトークンから次の重要なトークンまでの同じ行のすべてのトリビアは、後続のトークンのセットに分類されます。 他のすべての非表示トークンは、多くの先行トークンに分類され、次の重要なトークンに関連付けられます。 最初の重要なトークンには、ファイルの最初のトリビアが含まれています。 ファイルを閉じる隠しトークンは、長さがゼロの最後の特別なファイルの終わりトークンに関連付けられます。 ツリータイプとトリビアの解析の詳細については、 Roslynの公式ドキュメントを参照してください。







ANTLRでは、インデックスiのトークンに対して、左または右から特定のチャネルからすべてのトークンを返すメソッドがあります: getHiddenTokensToLeft(int tokenIndex, int channel)



getHiddenTokensToRight(int tokenIndex, int channel)



。 したがって、ANTLRに基づいたパーサーを、Roslyn構文解析ツリーと同様の信頼できる構文解析ツリーにすることができます。









無視されるマクロ



マクロは単一段階の処理中にObjective-Cコードフラグメントに置き換えられないため、無視したり、独立した分離チャネルに配置したりできます。 これにより、通常のObjective-Cコードの解析の問題や、文法ノードにマクロを含める必要がなくなります(コメントと同様)。 これは、 NS_ASSUME_NONNULL_BEGIN



NS_AVAILABLE_IOS(3_0)



などのデフォルトマクロにも適用されます。







 NS_ASSUME_NONNULL_BEGIN : 'NS_ASSUME_NONNULL_BEGIN' ~[\r\n]* -> channel(IGNORED_MACROS); IOS_SUFFIX : [_A-Z]+ '_IOS(' ~')'+ ')' -> channel(IGNORED_MACROS);
      
      







二段階処理



2段階処理アルゴリズムは、次の一連のステップとして表すことができます。







  1. プリプロセッサディレクティブのトークン化とコード解析。 このステップでの通常のコードフラグメントは、プレーンテキストとして認識されます。
  2. 条件ディレクティブの計算( #if



    #if



    #elif



    #if



    #else



    )およびコンパイルされたコードブロックの定義。
  3. コンパイルされたコードブロックの適切な場所での#define



    ディレクティブの値の計算と置換。
  4. ソースのディレクティブをスペース文字に置き換えます(ソースコード内のトークンの正しい位置を保持するため)。
  5. 削除されたディレクティブを使用した結果のテキストのトークン化と解析。


3番目のステップはスキップでき、少なくとも一部はマクロを直接文法に含めることができます。 ただし、この方法は、シングルステージ処理よりも実装がさらに困難です。この場合、最初のステップの後、通常のソースコードの正しいトークン位置を維持する必要がある場合は、プリプロセッサディレクティブのコードをスペースに置き換える必要があります。 それにもかかわらず、プリプロセッサディレクティブを処理するためのこのアルゴリズムも一度に実装され、現在はCodebeatで使用されています。 文法は、訪問者処理プリプロセッサディレクティブと共にGitHubにアップロードされます 。 この方法のもう1つの利点は、より構造化された形式で文法を表示できることです。







次のコンポーネントは、2段階処理に使用されます。







  1. プリプロセッサレクサー;
  2. プリプロセッサパーサー。
  3. プリプロセッサ;
  4. 字句解析器;
  5. パーサー。


レクサーは、ソースコードのシンボルをトークンまたはトークンと呼ばれる意味のあるシーケンスにグループ化することを思い出してください。 そして、 パーサーは、トークンストリームから解析ツリーと呼ばれる接続されたツリーのような構造を構築します。 訪問者 -各ツリーノードの処理ロジックを個別のメソッドに配置できるデザインパターン。









プリプロセッサレクサー



プリプロセッサディレクティブと通常のObjective-Cコードからトークンを分離するレクサー。 DEFAULT_MODE



通常のコードトークンに使用され、 DIRECTIVE_MODE



はディレクティブコードに使用されます。 以下はDEFAULT_MODE



からのトークンです。







 SHARP: '#' -> mode(DIRECTIVE_MODE); COMMENT: '/*' .*? '*/' -> type(CODE); LINE_COMMENT: '//' ~[\r\n]* -> type(CODE); SLASH: '/' -> type(CODE); CHARACTER_LITERAL: '\'' (EscapeSequence | ~('\''|'\\')) '\'' -> type(CODE); QUOTE_STRING: '\'' (EscapeSequence | ~('\''|'\\'))* '\'' -> type(CODE); STRING: StringFragment -> type(CODE); CODE: ~[#'"/]+;
      
      





このコードの断片を見ると、追加のトークン( COMMENT



QUOTE_STRING



など)の必要性に関する質問が発生する場合がありますが、Objective-Cコードの場合、使用されるトークンはCODE



のみです。 実際には、 #



文字は通常の行とコメント内に隠すことができます。 したがって、このようなトークンは個別に割り当てる必要があります。 ただし、これらのタイプはまだCODE



に変更されており、トークンを分離するためのプリプロセッサパーサーには次のルールが存在するため、これは問題ではありません。







 text : code | SHARP directive (NEW_LINE | EOF) ; code : CODE+ ;
      
      







プリプロセッサパーサー



Objective-Cコードトークンを分離し、プリプロセッサディレクティブトークンを処理するパーサー。 結果の解析ツリーは、プリプロセッサに渡されます。









プリプロセッサ



プリプロセッサディレクティブの値を計算する訪問者。 各ノードトラバーサルメソッドは文字列を返します。 計算されたディレクティブ値がtrue



場合、後続のObjective-Cコードフラグメントが返されます。 それ以外の場合、Objective-Cコードはスペースに置き換えられます。 前述のように、これはメインコードのトークンの正しい位置を維持するために必要です。 理解を容易にするために、次のObjective-Cコードスニペットの例を示します。







 BOOL trueFlag = #if DEBUG YES #else arc4random_uniform(100) > 95 ? YES : NO #endif ;
      
      





このフラグメントは、2段階処理を使用して、指定された条件シンボルDEBUG



を使用して次のObjective-Cコードに変換されます。







 BOOL trueFlag = YES ;
      
      





すべてのディレクティブとコンパイルされていないコードがスペースに変わったことに注意する価値があります。 ディレクティブはネストすることもできます:







 #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 #define MBLabelAlignmentCenter NSTextAlignmentCenter #else #define MBLabelAlignmentCenter UITextAlignmentCenter #endif
      
      







レクサー



プリプロセッサディレクティブを認識するトークンなしの通常のObjective-Cレクサー。 ソースファイルにディレクティブがない場合、同じ元のファイルが入力を入力します。









パーサー



通常のObjective-Cコードのパーサー。 このパーサーの文法は、ワンステップ処理のパーサーの文法と一致しています。









その他の処理方法



プリプロセッサディレクティブを処理する方法は他にもあります。たとえば、 lexlessパーサーを使用できます。 理論的には、そのようなパーサーでは、1段階処理と2段階処理の両方の利点を組み合わせることができます。つまり、パーサーはディレクティブの値を計算し、未コンパイルのコードブロックを1回のパスで決定します。 ただし、このようなパーサーには欠点もあります。理解およびデバッグがより困難です。







ANTLRはトークン化プロセスに非常に関係しているため、このようなソリューションは考慮されませんでした。 ただし、レクサー以外の文法を作成する可能性はすでに存在しており、将来さらに開発される予定です( 説明を参照)。









おわりに



この記事では、Cライクな言語の解析に使用できるプリプロセッサディレクティブを処理する方法を検討しました。 これらのアプローチは、Objective-Cコードを処理するために既に実装されており、SwiftifyやCodebeatなどの商用サービスで使用されています。 2段階処理のパーサーは、エラーのないファイルの数が全体の95%を超える20のプロジェクトでテストされています。 さらに、ワンステップ処理はC#の解析にも実装されており、オープンソース: C#grammarで利用できます。







Swiftifyは、プリプロセッサディレクティブのワンステップ処理を使用します。これは、解析エラーの可能性があるにもかかわらず、プリプロセッサを実行するのではなく、プリプロセッサディレクティブを対応するSwift言語構成に変換するためです。 たとえば、Objective-Cの#define



ディレクティブは、一般的にグローバル定数とマクロを宣言するために使用されます。 Swiftでは、定数( let )と関数( func )が同じ目的で使用されます。








All Articles