MPS 2025.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}, where {{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 を指定することが重要です。また、それほど単純ではありません \ ldots。

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

まず、ノードを訪問済みにするための必須の nop から始めます。次に、条件に対して DFB を実行します。これは、どのような場合でも評価されるためです。次に、興味深いことになります。条件が真であるか偽であるかに応じて、thenPart} or we jump to where the {{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}), then we execute the {{thenPart を「オーバーオーバー」するだけです。thenPart}, we are finished with the whole {{IfStatement を実行する場合 — else if}s or {{else パーツは関連しないため、現在のノード(IfStatement)の後に移動して完了します。ただし、追加のキャッチがあります。thenPart}, there may be a {{return ステートメント。実際に jump after node ステートメントに到達することはないかもしれません。これが波括弧で囲まれている理由です。これは、中括弧内のコードがオプションであることを示しています。データフローがアクセスしない場合は、問題ありません(通常はこのコードを実行する機会を得る前に、メソッドから戻ります)。

条件が false の場合、つまり上記の ifjump が実際に発生した場合は、else if}s. We arrive at the {{elseIfBlock ラベルを続行します。次に、{{elseIf}} を繰り返し処理し、DFB を実行します。その後、elsePart のコードがあれば、それを実行します。次のコードは、{{else if}} の 1 つを実行すると、 IfStatement 全体の後に移動することがわかっている場合にのみ理解できます。これは、ElseIfPart の DFB で指定されています。これについては、以下で説明します。IfStatement の DFB の残りのコードは次のとおりです。

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

これで、DFB の ElseIfPart をインスペクションできます。まず、条件に対して DFB を実行します。次に、条件が false である可能性があり、次の else if がある場合はそれを試したいため、その else if の後に移動します。または、条件が true の場合、ElseIfPart の本体に対して DFB を実行します。次に、2 つのことが起こります。IfStatement} (after all, we have found an {{else if 全体が真の後に移動するか、現在の 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}. Then we have an {{ifjmp を実行します(これは、条件が false でループが終了する場合をカバーします)。それ以外の場合(条件はまだ真です)、body および incr} part of the {{for ループのコードを実行します。次に、上記で定義した 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}} は、データフローグラフのラッパーであり、グラフへのアクセスと、グラフ上の事前定義された分析のセットへのアクセスを提供します。ここでは、そのうちの 1 つを checkForUnreachableNodes メソッドで使用します。このメソッドは、グラフから到達不能なノードをすべて抽出し(上記のコードのコメントを参照)、それらのエラーを報告します。error ステートメントを使用できるようにするには、メソッドに @CheckingMethod アノテーションを付ける必要があります。

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

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

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

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

データフロー分析は重要なトピックです。意味のある分析を構築するには、おそらくこのトピックの背景が必要になるか、それぞれの文献を読む必要があります。

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