web-dev-qa-db-ja.com

バイトコードでジャンプターゲットを作成するときの一般的な手順は何ですか?

過去数日間、私はバイトコードでジャンプターゲットを正しく計算するためにさまざまな方法を試してきましたが、実用的でも信頼できるものでもありませんでした。さらに、私が試した方法では、あらゆる言語の重要な機能であるネストされたifステートメントやElifステートメント、あるいはその両方を使用できませんでした。

抽象構文ツリーを再帰的に歩き、各ノードのタイプをテストして、適切な命令を生成しています。ただし、条件付きブランチノードの命令を適切に生成する方法がわかりません。私が生成する命令が従うべき基本的なロジックを理解しています:

  • If/Elifステートメントがテストしているという条件が真である場合、if/Elifステートメントの本体を実行し、このif/Elifの下にある他のすべてのロジックブランチ(Elif/else)を通過します。ステートメント。
  • それ以外の場合は、このif/Elifステートメントの下にある次の最も近い論理分岐にジャンプします。

「次の最も近いブランチ」への正しいジャンプまたは過去のジャンプ「他のすべての論理分岐」

また、Python- compile.c に実装されている-のバイトコードコンパイラのソースコードを調べようとしましたが、Cのスキルがないため、使用されているコンセプトをよく理解していませんでした。

しかし、以前は複数の言語で条件付きロジックが実装されており、このようなものを実装する際に使用される一般的な方法があることはかなり確信しています。もしそうなら、この方法は何ですか?また、複数の分岐命令のコードを生成するためにどのように実装できますか?

6
Christian Dean

すべての仮想マシンが線形バイトコードを使用するわけではありません。バイトコードがグラフデータ構造を形成する場合、条件付き演算はすべての分岐を直接参照でき、明示的なジャンプターゲットを計算する必要はありません(実際、ジャンプターゲットの概念は、このようなバイトコードではあまり意味がありません)。私はこのアプローチをいくつかのおもちゃ言語実装で使用しました。

ほとんどの「実際の」バイトコードは、マルチパスアプローチを使用してジャンプターゲットを計算します。まず、必要なバイトコード構造を視覚化しましょう。

_    ,-------------------------.
   |                          v
branch, then-branch..., jump, else-branch..., end
                          |                   ^ 
                          `-------------------'
_

Then分岐を直接継続するか、else分岐にジャンプする分岐オペコードがいくつかあります。 then-branchの終わりに、無条件ジャンプが最後を指します。

バイトコードを出力するとき、ブランチとジャンプ命令をダミー値でジャンプし、後で戻ってそれらを修正することができます。

_branch, then-branch..., jump, else-branch..., end
   |                      |
   v                      v
  ???                    ???
_

バイトコードが出力されると、then-branchとelse-branchの長さが正確にわかります。その後、すべてのジャンプを正しいターゲットで上書きできます。 pseudcode:

_compile_conditional(cond, then, else):
   compile(cond)
   branch = emit(branch, null)

   compile_all(then)
   jump = emit(jump, null)

   branch.target = current_location

   compile_all(else)

   jump.target = current_location
_

絶対的なターゲットの代わりに、バイトコードを自由に移動できるように、通常は相対的なジャンプを好みます。

リンクしたCPythonコードは次のようなものです。ジャンプターゲットはジャンプの前にコンパイルされ、ターゲットが認識されます。トリックは、最初に基本ブロックのリンクリストを生成することです(上記のバイトコードグラフと同様)。各ジャンプは1つの基本ブロックをターゲットとします。その後、ブロックには命令が含まれます。 endの基本ブロック、else-branchのブロック、then-branchのブロックがあります(compiler_if()を参照)。アセンブリパスassemble_jump_offsets()は、ブロックを反復処理し、バイトコードでブロックオフセットを計算します。これが完了すると、すべてのブロックのすべての命令を反復処理し、相対ターゲットでジャンプ命令を更新できます。

5
amon

与えられたASTを評価するルーチンを使用して、目的のラベルに分岐します(ブール値は、ジャンプ条件の正常または反転を示します)。

擬似コードでは(コーディングスタイルではなく、アルゴリズムを伝えようとしています)ifステートメントのコードを次のように生成します。

_void generateCodeForIfStatement ( ifNode ) {
    var targetForFalseCondition = generateLabel ();
    generateConditionalTestAndBranch ( ifNode.conditionalExpression, 
                                       targetForFalseCondition,
                                       false );
    generateCodeForStatement ( ifNode.thenPart );
    if ( ! ifNode.hasElsePart () ) {
        placeLabelHere ( targetForFalseCondition );
    }
    else {
        var targetAfterElse = generateLabel ();
        jumpTo ( targetAfterElse );
        placeLabelHere ( targetForFalseCondition );
        generateCodeForStatement ( ifNode.elsePart );
        placeLabelHere ( targetAfterElse );
    }
}
_

これは_if-then_と_if-then-else_の両方を処理します。条件式は評価され、式がfalseと評価された場合、else部分(またはelse-partがない場合は少なくともthen-partの周囲)に分岐する必要があります。したがって、falseパラメータにgenerateConditionalTestAndBranchjumpIfTrueに渡して、物事を開始します。

また、フォワードラベルを生成する機能も必要です。つまり、ここでラベルを使用する必要がありますが、まだ不明/未定義の場所にあります。後で、生成されたコードに配置(場所を定義)できるようになります。うまくいけば、疑似コードでそれを見ることができます。

より詳細には、生成された命令でまだ未定義のラベルが使用される場合は常に、関連付けられたバイトコード命令をリストに入れることができるため、後でラベルの場所が定義されたときに、そのリスト(命令の)が修正されます(これは、(フォワード)ブランチターゲットオペランドがラベルから変換される部分です。ラベルは最終的には最終的なマシン/バイトコードから消えます)、生成されたコードのパスを使用する必要はありません。

(可変サイズの命令が分岐に使用できる場合(たとえば、短い距離の短い命令または長い距離の長い命令)、前方分岐の分岐命令サイズを最適化する機会があります。)


次に、この条件式エバリュエーターは、条件がtrueと評価されたときに指定されたjumpTargetパラメーターに移動し、次のコード(配置されます)条件がfalseに評価された後

_void generateConditionalTestAndBranch ( AST conditionalExpression,                      
                                       BranchTarget jumpTarget, 
                                       bool jumpOnTrue ) {
    switch ( conditionalExpression.nodeType ) {
        ...
        case "!" :
            generateConditionalTestAndBranch ( ast.child, target, ! jumpOnTrue );
            break;
        case "&&" :
            if ( jumpOnTrue )
                generateTestAndJumpAround ( ast, target, jumpOnTrue );
            else
                generateTestAndJumpThru ( ast, target, jumpOnTrue );
            break;
        case "||" :
            if ( jumpOnTrue )
                generateTestAndJumpThru ( ast, target, jumpOnTrue );
            else
                generateTestAndJumpAround ( ast, target, jumpOnTrue );
           break;
        ...
    }
}
_

ご覧のとおり、_!_演算子がある場合、コードは単にjumpIfTrueフラグを逆にして、残りを評価し続けます。

_&&_および_||_にも、分岐とネストされた条件(式)を導入する短絡評価があり、互いに非常によく似ています。これは、それらが関連しているため(たとえば、demorganによる)それらはそれぞれ、現在の値jumpIfTrueが与えられたコンテキストで、左の子と右の子を評価します。

最後に、以下は、_&&_および_||_で共有される短絡評価用の簡単なヘルパー関数です。もちろん、これらのヘルパーは_&&_、_||_および_!_式の構成をサポートするように設計されており、必要に応じてジャンプ方向を逆にします。


_void generateTestAndJumpThru ( AST binaryNode, BranchTarget target, bool jumpIfTrue ) {
    generateConditionalTestAndBranch ( binaryNode.leftChild, target, jumpIfTrue );
    generateConditionalTestAndBranch ( binaryNode.rightChild, target, jumpIfTrue );
}
_

if ( a && b )を評価するには、いずれかの評価がfalseの場合はelse部分に分岐する必要があるため、aをテストし、falseの場合はelse部分に分岐してから、bであり、falseの場合はelse部分に分岐します。


_void generateTestAndJumpAround ( AST binaryNode, BranchTarget target, bool jumpIfTrue ) {
    var around = generateLabel ();
    generateConditionalTestAndBranch ( binaryNode.leftChild, around, ! jumpIfTrue );
    generateConditionalTestAndBranch ( binaryNode.rightChild, target, jumpIfTrue );
    placeLabelHere ( around );
}
_

if ( a || b )を評価するには、else部分に分岐する必要があります。両方ともfalseであるため、最初にaをテストします。trueの場合、b評価をスキップできます。 aがfalseの場合は、bのテストに進み、それもfalseの場合は、else部分に進みます。


また、then-partまたはelse-partの唯一の(または最初の)ステートメントがbreak、continue、return、またはgotoステートメントである場合、ここで簡単に実行できる追加のコード生成最適化があります(分岐を無視する)ブランチ。あるいは、後でクリーンアップできます)。

2
Erik Eidt