MPS 2026.1 ヘルプ

データフローエンジンとの統合方法

データフロー

データフロー分析は、静的制約またはタイプチェックを使用して「モデルを見る」ことでは簡単に見つけることができないエラーの検出をサポートします。例としては、デッドコードの検出、メソッド本体の一部のブランチでの戻り値の欠落、null 値の静的な検索、null ポインター例外の防止などがあります。

データフロー分析の基盤は、いわゆるデータフローグラフです。これは、プログラムのコードを介したデータの流れを記述するデータ構造です。例: int i = 42; j = i + 1; では、42 はローカル変数宣言の init 式から変数 i に「流れ」、1 を追加した後 j に流れます。データフロー分析は、プログラムのデータフローグラフを作成することと、このデータフローグラフで分析を実行してプログラムの問題を検出することの 2 つのタスクで構成されます。

MPS には、データフローグラフ用の事前定義されたデータ構造、言語の概念(したがってプログラム)からグラフを導出する方法を定義するための DSL、グラフ上で独自の分析を定義するためのフレームワーク、およびあなたの言語に統合することができます。このセクションでは、これらすべての成分を見ていきます。

データフローグラフを操作するには、Java プログラムでメソッドを選択してから、そのメソッドのコンテキストメニューを使用します。{{Language Debug-> Show Data Flow Graph}} を選択します。これにより、データフローグラフがグラフィカルにレンダリングされ、独自のデータフローグラフや分析を作成する際の優れたデバッグツールが構成されます。

データ Flow グラフの作成

単純な線形データフロー

このセクションでは、C および Java に類似した言語(実際には、mbeddr.com C 基本言語用)のデータフローグラフを作成するために作成する必要のあるコードについて説明します。

データフローは、言語定義のデータフローアスペクトで指定されます。その側面の中で、言語の概念に合わせてデータフロービルダー(DFB)を追加できます。これらは、プログラム内のこれらの概念のインスタンスのデータフローグラフを作成するプログラムです。はじめに、LocalVariableDeclaration の DFB を次に示します。

data flow builder for LocalVariableDeclaration { (node)->void { if (node.init != null) { code for node.init write node = node.init } else { nop } } }

これを詳しく調べてみましょう。

フレームワークは、この DFB が定義されているコンセプトの現在のインスタンス (ここでは LocalVariableDecaration) を参照する方法として、node 変数を渡します。LocalVariableDecaration に init 式がある場合 (オプションです)、init 式の DFB を実行する必要があります。code for ステートメントはこれを実行します。引数として渡されたノードの DFB を「呼び出し」ます。次に、実際のデータフロー定義を実行します。write node = node.init は、現在のノードで書き込みアクセスが実行されることを指定します (read ステートメントもあり、これは書き込み前の読み取りエラーの検出をサポートします)。このステートメントは、init 式にあった値がノード自体にあることも表します。

init 式がない場合でも、LocalVariableDeclaration ノードを訪問済みとしてマークする必要があります。プログラムフローはこのノードに到達しています。その後の分析では、DFB がアクセスしていないすべてのプログラムノードがデッドコードとして報告されます。ノードがプログラムのデータフローにそれ以上影響を与えない場合でも、nop を使用して訪問済みとしてマークする必要があります。

read ステートメントを説明するために、参照先の変数に読み取りアクセスする LocalVariableRef 式を見てみましょう。そのデータフローは read node.var と定義され、var は参照先の変数を指す参照の名前です。

コードは次のとおりです。

data flow builder for LocalVarRef { (node)->void { read node.var } }

AssignmentStatement の場合、データフローは次のとおりです。

data flow builder for AssigmentStatement { (node)->void { code for node.rvalue write node.lvalue = node.rvalue } }

最初に rvalue の DFB を実行し、次に rvaluelvalue に「フロー」する方法に注意してください — 割り当ての目的です。

StatementList の場合、リストに訪問済みのマークを付けてから、各ステートメントの DFB を実行します。

nop foreach statement in node.statements { code for statement }

最後に、C 関数の場合、少なくとも今のところ、引数を無視して、DFB は本体の DFB である StatementList を呼び出すだけです。

これで、データフローグラフを調べて簡単な関数を探す準備ができました。以下は関数のグラフです。

dfgexample.jpg

データフロー分析は通常、1 つの関数、メソッド、同様の概念に限定されます。それらの終了を示すには、ret ステートメントを使用する必要があります。これを説明するために、ReturnStatement の DFB を以下に示します。

if (node.expression != null) { code for node.expression } ret

ブランチ

前述のように、線形データフローは比較的単純です(ダジャレではありません)。しかし、最も興味深いデータフロー解析はループと分岐に関するものです。そのため、ifswitchfor のようなものに対して適切な DFB を指定することが重要です。また、それほど単純ではありません。

IfStatement の DFB を段階的に見ていきましょう。

まず、必須の nop を実行してノードを訪問済みにします。次に、条件に対して DFB を実行します。これは、条件が常に評価されるためです。ここからが興味深いところです。条件が真か偽かによって、thenPart を実行するか、else if の部分が始まる場所に移動します。これまでのコードは以下のとおりです。

nop code for node.condition ifjump after elseIfBlock // elseIfBlock is a label defined later code for node.thenPart { jump after node }

ifjump ステートメントは、指定されたラベルに移動できることを意味します (つまり、else if を実行します)。そうでない場合 (ifjmp を単に「実行」する場合)、thenPart を実行します。thenPart を実行すると、IfStatement 全体が終了し、else ifelse の部分は関係ないため、現在のノード (IfStatement) の後に移動して終了します。ただし、注意点があります。thenPart には return ステートメントが含まれている可能性があります。そのため、jump after node ステートメントに実際に到達しない可能性があります。これが、波括弧で囲まれている理由です。これは、中括弧内のコードがオプションであることを示しています。データフローがそこを訪れなくても問題ありません (通常は、このコードを実行する機会を得る前にメソッドから戻るため)。

else if について引き続き説明します。条件が偽、つまり上記の ifjump が実際に発生した場合、elseIfBlock ラベルに到達します。次に、elseIf を反復処理し、それぞれの DFB を実行します。その後、elsePart が存在する場合は、そのコードを実行します。以下のコードは、else if のいずれかを実行すると、IfStatement 全体の後に移動することがわかっている場合にのみ理解できます。これは、後述する ElseIfPart の DFB で指定されています。以下は、IfStatement の DFB の残りのコードです。

label elseIfBlock foreach elseIf in node.elseIfs { code for elseIf } if (node.elsePart != null) { code for node.elsePart }

これで、ElseIfPart の DFB を調べることができます。まず、条件の DFB を実行します。次に、条件が偽である可能性があり、次の else if があれば試したいため、その else if の後に移動します。あるいは、条件が真であれば、ElseIfPart の本体の DFB を実行します。すると、2 つのことが起こり得ます。1 つは IfStatement 全体の後に移動します (結局、真である else if が見つかったため)、もう 1 つは現在の else if に return 文が含まれているため、何も行いません。if 全体の後に移動するために、波括弧を再度使用する必要があります。コードは次のとおりです。

code for node.condition ifjump after node code for node.body { jump after node.ancestor<concept = IfStatement> }

得られたデータフローグラフを以下に示します。

ifdfg.jpg

ループ

データフローグラフの構築をまとめるために、for ループを見てみましょう。結局のところ、ループは分岐と移動にリファクタリングできるため、これは再び分岐に関連しています。for ループの DFB は次のとおりです。

code for node.iterator label start code for node.condition ifjump after node code for node.body code for node.incr jump after start

まず、iterator の DFB を実行します (これは LocalVariableDeclaration の一種なので、上記の DFB が機能します)。次に、ラベル start を定義して、さらに下からこの場所に移動できるようにします。次に、condition を実行します。次に、ループ全体の後に ifjmp があります (これは、条件が偽でループが終了する場合をカバーします)。もう一方のケース (条件がまだ真の場合) では、bodyfor ループの incr 部分のコードを実行します。次に、上記で定義した start ラベルの後に移動します。

データ Flow チェックをあなたの言語に統合する

データフローチェックは NonTypesystemRules からトリガーされます。記述する必要のある手続き型コードが少しあるため、型システムアスペクトモデルでクラス DataflowUtil を作成します。

public class DataflowUtil extends <none> implements <none> { private Program prog; public DataflowUtil(node<> root) { // build a program object and store it prog = DataFlow.buildProgram(root); } @CheckingMethod public void checkForUnreachableNodes() { // grab all instructions that are unreachable (predefined functionality) sequence<Instruction> allUnreachableInstructions = ((sequence<Instruction>) prog.getUnreachableInstructions()); // remove those that may legally be unreachable sequence<Instruction> allWithoutMayBeUnreachable = allUnreachableInstructions.where({~instruction => !(Boolean.TRUE.equals(instruction. getUserObject("mayBeUnreachable"))); }); // get the program nodes that correspond to the unreachable instructions sequence<node<>> unreachableNodes = allWithoutMayBeUnreachable. select({~instruction => ((node<>) instruction.getSource()); }); // output errors for each of those unreachable nodes foreach unreachableNode in unreachableNodes { error "unreachable code" -> unreachableNode; } } }

このクラスは、コンストラクターで Program オブジェクトを構築します。Program はデータフローグラフのラッパーであり、グラフへのアクセスと、グラフに対する一連の事前定義された分析へのアクセスを提供します。ここでは、checkForUnreachableNodes メソッドでそのうちの 1 つを使用します。このメソッドは、グラフから到達不可能なノードをすべて抽出し (上記のコードのコメントを参照)、それらのエラーを報告します。error ステートメントを使用できるようにするには、メソッドに @CheckingMethod アノテーションを付ける必要があります。

実際にチェックを実行するには、C 関数の NonTypesystemRule からこのメソッドを呼び出します。

checking rule check_DataFlow { applicable for concept = Function as fct overrides false do { new DataflowUtil(fct.body).checkForUnreachableNodes(); } }

Program クラスを調べると、既存の他のデータフロー分析のセットを確認できます。初期化されていない読み取り(書き込み前の読み取り)、到達不能な命令(デッドコード)、未使用の割り当てです。次のセクションでは、それらが十分でない場合の対処方法を見ていきます。

独自のアナライザーを構築する

データフロー分析は決して簡単なテーマではありません。有意義な分析を行うには、この分野に関する基礎知識、あるいは関連文献の読解が必要となるでしょう。

カスタムデータフロー分析の実際の統合については、後で追加の記事を提供します。

2026 年 5 月 06 日