Whale IR リファレンス

型、識別子、関数の妥当性、評価順序、および交換形式について説明します。

Wave Foundation

モジュールと識別子

モジュールは、ターゲット情報、グローバル定義、関数で構成されます。値には明示的な型があります。フロントエンドはソース言語の名前・タイプ・オーバーロード・ジェネリックを解決した後、typedIRを生成します。

関数とグローバル変数は異なる内部名前空間を使用します。したがって、関数と変数は同じ名前を持つことができます。内部識別子は外部接続名link_nameと区別され、外部名はフロントエンドによって指定されます。 Whaleは新しい名前を自動生成して外部競合を解決しません。 シンボルとリンクを参照してください。

各値定義には識別子があります。定義は重複することはできず、型メタデータは定義に指定された型と一致する必要があります。同じ名前の宣言が互いを隠す場合でも、名前だけで定義を識別しません。

IR構成と読み取り

以下は、irクレートで関数を構成して検証した後、typedIRを出力する完全なRust例です。

use ir::{ModuleBuilder, Target, Type};

fn main() {
    let target = Target::lookup("x86_64-whale-linux").unwrap();
    let mut module = ModuleBuilder::new(target.name(), target.data_layout());
    let mut function = module.begin_function("answer", vec![], Type::I32);
    let left = function.const_i32(40);
    let right = function.const_i32(2);
    let answer = function.add(Type::I32, left, right);
    function.ret(Some(answer));
    function.finish();
    let module = module.finish();
    ir::verify_module(&module).unwrap();
    print!("{}", ir::print_module(&module));
}

プリンタは次のIRを出力します。

module {
  format_version 2
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "answer": whale () -> i32, linkage internal

  fn @answer() -> i32, id @f0 {
  entry:
    %v0: i32 = const i32 40
    %v1: i32 = const i32 2
    %v2: i32 = add i32 %v0, %v1
    ret i32 %v2
  }

}

%v0と%v1はi32定数の定義です。 addが定義した%v2を関数のi32戻り値として使用します。戻り値をBoolに置き換えると、関数署名に合わず検証エラーになります。 2つのオペランドが定数であってもO0では加算命令を保持します。

このコードは実際のプリンタ出力であり、テキストパーサに渡す入力ファイルではありません。テキスト解析とIR実行はまだサポートされておらず、現在はRustbuilderでこのモジュールを設定できます。

タイプ

タイプ 意味
bool 論理値falseまたはtrue
i1, i8, i16, i32, i64, i128 指定されたビット幅の符号付き整数
u1, u8, u16, u32, u64, u128 指定されたビット幅の符号なし整数
f16, f32, f64 指定されたビット幅の浮動小数点値
ptr<T> Tタイプ値を指すポインタ
fnptr<signature> 正確なパラメーター/結果の型と呼び出し規約を持つ呼び出し可能なポインター
array<T, N> 同じタイプの元素N個
struct{T, ...} 順序付き構造体フィールド
tuple<T, ...> 順序付きタプル要素
void 結果なし

bool、i1、u1は異なるタイプです。 signedi1は−1と0、unsignedu1は0と1を表します。整数 1 は暗黙的に論理条件にはなりません。条件分岐、Selectの条件、trap_ifはBoolオペランドを要求します。

記憶サイズは値のビット数だけで決まらず、ターゲットレイアウトに従います。たとえば、i1の値は1ビットですが、メモリでは少なくとも1バイトを占めます。

関数と呼び出し

関数には、全パラメータ・結果タイプ、呼び出し規約、linkageを指定します。直接呼び出しと間接呼び出しは、呼び出し先の署名と一致する必要があります。 void呼び出しには結果IDはありません。 nonvoid呼び出しは、O0で結果を使用しなくても結果定義を保持します。

戻り値は関数の結果型と一致する必要があります。 void戻りは値を渡さず、nonvoid戻りは宣言された結果型の値を渡します。

宣言、識別子、呼び出し

Module.declarations は、各関数の FunctionId、名前、完全な署名、リンク、外部リンク名を記録します。定義はこのアイデンティティを指します。パラメータと戻り値の型はその宣言と一致する必要があります。同一の繰り返し宣言は、declare_function を通じて同じ ID に解決されます。競合や重複した定義はエラーです。内部宣言にはモジュール内に本体が必要です。外部宣言は、リンクするまで未解決であるか、エクスポートされた本文を持つ可能性があります。内部関数には link_name はありません。外部関数には、NUL のない明示的な空ではない名前が必要です。 2 つの異なる関数宣言が同じ外部名を要求することはできません。グローバルと関数は依然として別の内部名前空間を使用します。

前方呼び出しと再帰をサポートするには、begin_declared_function で本体を構築する前に宣言を登録します。 begin_function は、新しい内部 Whale 関数を作るための便利な API です。チェックされた declare_function、begin_declared_function、function_addr、null_function、call API は Result を返します。拒否された呼び出しでは、命令は追加されず、結果 ID も割り当てられません。

次の完全な Rust プログラムは、外部関数を宣言し、その型指定されたアドレスを取得し、直接呼び出しと間接呼び出しの両方を発行します。

use ir::{Callee, CallingConvention, DataLayout, FunctionSignature, Linkage, ModuleBuilder, Type};

fn main() {
    let mut module = ModuleBuilder::new("x86_64-whale-linux", DataLayout::default_64bit_le());
    let signature = FunctionSignature {
        params: vec![Type::I32], ret: Type::I32,
        convention: CallingConvention::SysV64, variadic: false,
    };
    let identity = module.declare_function(
        "identity", signature, Linkage::External, Some("identity_i32".into()),
    ).unwrap();
    let mut function = module.begin_function("answer", vec![], Type::I32);
    let input = function.const_i32(42);
    let callback = function.function_addr(identity).unwrap();
    // The direct call's result remains defined even though it is unused.
    function.call(Callee::Direct(identity), vec![input]).unwrap();
    let result = function.call(Callee::Indirect(callback), vec![input]).unwrap().unwrap();
    function.ret(Some(result));
    function.finish();
    let module = module.finish();
    ir::verify_module(&module).unwrap();
    print!("{}", ir::print_module(&module));
}
module {
  format_version 2
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "identity": sysv64 (i32) -> i32, linkage external, link_name "identity_i32"
  declare @f1 "answer": whale () -> i32, linkage internal

  fn @answer() -> i32, id @f1 {
  entry:
    %v0: i32 = const i32 42
    %v1: fnptr<sysv64 (i32) -> i32> = function_addr @f0
    %v2: i32 = call sysv64 i32 @f0(%v0)
    %v3: i32 = call sysv64 i32 indirect %v1(%v0)
    ret i32 %v3
  }

}

Callee::Direct(FunctionId) は宣言テーブルを通じて解決されます。 Callee::Indirect(ValueId) には Type::FnPtr(FunctionSignature) の値が必要です。署名には、すべてのパラメータのタイプ、結果のタイプ、および CallingConvention::{Whale, SysV64} が含まれます。これは、コピー、ストレージ、パラメータ、リターン、ファイ、選択を通じて保持されます。データ ポインターと整数値は呼び出すことができません。関数ポインター型を含むキャストは拒否されます。型アノテーションを変更しても、呼び出し可能なシグネチャを変更することはできません。関数ポインタには、このターゲット上に 64 ビットのアドレス ストレージがあります。これ自体はランタイム シャドウ メタデータを実装しません。

アリティ、正確な引数/結果の型、結果 ID の存在、および呼び出し規約が一致する必要があります。暗黙的な変換はありません。間接的な呼び出し先は、その引数と同じように呼び出しを支配する必要があります。 variadic: true、void 型のパラメータおよび SysV64 集約パラメータ/結果の署名は拒否されます。 Whale 集約署名は IR で表すことができます。ネイティブ ABI 分類とマシン コールの発行は、どちらの規則でもまだ利用できません。

null_function(signature) は、型情報を持つ null 関数ポインターです。この値を呼び出す IR は型としては有効ですが、実行時には呼び出し先へ入る前に必ず trap しなければなりません。null でなくても、無効な対象、有効期間が終了した対象、検査済みのシグネチャと互換性のない対象への呼び出しは trap が必要です。これらの実行時検査と外部コールバックの有効期間管理は、インタプリタ・ネイティブ実行層で今後実装する機能です。検証に成功しても、任意の外部アドレスが安全になるわけではありません。

AST 呼び出しの形式

これらは、AST フォーマット 2 プログラム内の式フラグメントです。

{"Call":{"callee":{"Direct":"increment"},"args":[{"Lit":{"Int":{"bits":32,"signed":true,"value":"41"}}}]}}
{"Call":{"callee":{"Indirect":{"FunctionRef":"increment"}},"args":[{"Lit":{"Int":{"bits":32,"signed":true,"value":"41"}}}]}}

Direct と FunctionRef は、変数が同じ名前であっても関数名前空間を使用します。 Indirect は最初に式を評価し、次に引数を左から右に評価します。 void 呼び出しは、ExprStmt として有効ですが、変数初期化子、引数、オペランド、または戻り値としては有効ではありません。呼び出しと関数参照はコンパイル時の数値定数式ではありません。 NullFunction は、params、ret、convention、variadic フィールドを持つ署名オブジェクトを受け取ります。

完全な JSON の例 はコールバックを保存し、外部呼び出しの前にそれを呼び出します。次のように下げます。

cargo run --locked --features socket-cli -- ir lower ir/tests/fixtures/ast-v2-calls.json

その予想IRは、降下テストでチェックされます。関数 ID とリンク名は IR 境界で表されます。ネイティブ オブジェクトの生成とリンクを通じてそれらを保存することは、依然として別の作業です。

ブロックと値の利用可能性

各ブロックには一意の識別子と正確に1つのterminatorがあります。分岐先は同じ関数に属する必要があります。エントリブロックは必ず存在し、先行幹線とphiを持つことはできません。ループを作成するときは、エントリブロックから別のループヘッダに分岐します。

実行可能パス内の値の定義は、一般的な使用ポイントを支配する必要があります。つまり、エントリポイントから使用ポイントへのすべてのパスがその定義を通過する必要があります。同じブロックでは、定義は使用の前になければなりません。ブロックの記憶順序は支配関係を決定しない。

エントリーポイントからleftまたはrightに分岐し、joinに参加するとします。 leftでのみ定義した値は、joinで一般値として使用できません。 rightを通るパスには定義がないからです。各先行ブロックの値を入力として受け取るphiで合計する必要があります。

到達不能なブロックもモジュールに保存されます。検証器は、該当ブロックの識別子・タイプ・オペランド・分岐構造を検査し続けます。到達不能なブロックの定義は、到達可能なパスの一般的な使用に値を供給できません。

Phi コマンド

phiは、ブロック内のすべての一般コマンドの前に配置されます。異なる先行ブロックごとに正確に1つの入力が必要です。入力値はphiと同じタイプでなければならず、対応する先行ブロックの終わりで利用可能でなければなりません。

1つの先行ブロックから複数の幹線が入っていても、入力は1つである。ループphiは、モジュールの保存順序で後で出てくるブロックの値でも繰り返し幹線で計算される値であれば参照できます。欠落・重複・無関係な先行ブロックと誤ったタイプの入力は検証エラーです。

評価と選択

WhaleASTは、呼び出し先とサブ式を指定された左から評価します。フロントエンドは、短絡評価を制御フロー分岐として表します。

Selectは、すでに計算された値の1つを選択します。どちらの入力の計算も省略しません。たとえば、安全な値を選択しても、他の入力を計算しながら発生したtrapを避けることはできません。特定のパスでのみ実行する必要がある計算は、条件付きブロック内に配置する必要があります。

検証課 trap

無効なIRは検証フェーズで拒否されます。実行条件違反は定義されたtrapとして扱われ、undefとpoisonは許容値ではありません。 builder誤用、重複定義、2番目のterminator追加は構造エラーとして返される必要があります。

trapは理由・ソース位置・IRIDを含み、以降の実行を中断します。 native実行ではプログラムを終了し、インタプリタAPIではTrapエラーを返します。以前の副作用は保存されますが、バッファflush・デストラクタ呼び出し・スタックunwindingは保証しません。

この保証は、実績のあるIRおよびトレースメモリに適用されます。外部C・生アドレス・インラインアセンブリは別途契約を有し、その境界外の違反まで常に検出するわけではありません。 メモリモデルを参照してください。

交換形式とテキスト表現

ASTとtypedIRはそれぞれのformatversionと共通semanticsversion読む方は、無バージョン・不明なバージョン・フィールド・機能・重複JSONキーを拒否しなければなりません。コンストラクタは、サポートしていない属性が静かに無視されると仮定してはいけません。

整数はビット幅・signedness・文字列数で渡します。浮動小数点定数は幅と正確なビット列で渡されます。テキストIRのround-tripは、名前・ID・タイプ・定数・順序・属性・メタデータを保存する必要があります。スペースとコメントの配置は保存対象ではありません。

以下のスカラーASTJSON契約が利用可能です。出力される typed IRにはバージョン情報が含まれますが、テキストパーサとの完全な往復交換はまだ未サポートです。

バージョンが指定された AST JSON

以下をprogram.jsonとして保存します。 4 つのエンベロープ フィールドはすべて必須です。 program には必須の declarations、globals、functions の配列が含まれていますが、空の場合もあります。関数名、パラメータ、戻り値の型、本体、convention および linkage は必須です。内部関数の場合、link_name は存在しないか null である可能性があり、外部関数の場合は、NUL のない空でない文字列である必要があります。各列挙型は、そのユニット名または単一のバリアント キー オブジェクトのいずれかを使用します。ユニットのバリアントは、{"Void":null} などの null 値のオブジェクトも受け入れます。エンコーダーはユニット名 "Void" を出力します。 VarDecl.init は存在しないか null の可能性があります。他の必須フィールドが存在する必要があります。

{
  "format_version": 2,
  "semantics_version": 1,
  "features": [],
  "program": {
    "globals": [],
    "functions": [
      {
        "name": "answer",
        "parameters": [],
        "return_type": {
          "Int": {
            "bits": 128,
            "signed": false
          }
        },
        "body": [
          {
            "Return": {
              "Lit": {
                "Int": {
                  "bits": 128,
                  "signed": false,
                  "value": "340282366920938463463374607431768211455"
                }
              }
            }
          }
        ],
        "convention": "Whale",
        "linkage": "Internal",
        "link_name": null
      }
    ],
    "declarations": []
  }
}
cargo run --locked --features socket-cli -- ir lower program.json
module {
  format_version 2
  semantics_version 1
  target "x86_64-whale-linux"
  datalayout { ptr=64, endian=little }

  declare @f0 "answer": whale () -> u128, linkage internal

  fn @answer() -> u128, id @f0 {
  entry:
    %v0: u128 = const u128 340282366920938463463374607431768211455
    ret u128 %v0
  }

}

整数valueは10進文字列です。 Signed 整数の選択的なマイナスの後に数字を書き、空白・プラス・指数・区切り文字は許可しません。許容範囲は、宣言した幅とsignednessで決定します。上記のu128::MAXはJSONとloweringを経てそのまま保存されます。負のunsigned値または範囲外の値はwrapなしでエラーです。 Float値には、数値演算で説明されている正確な幅の16進ビット文字列を使用してください。

format_version は、この AST 形式では 2 です。 semantics_version は 1 です。 features は空の配列でなければなりません。不明なフィールド、バージョン、機能、重複した生の JSON キー (エスケープされた等価キーを含む)、および末尾の値は、--no-verify であってもエラーとなります。ライブラリのエントリ ポイントは ir::lower_ast::interchange::decode です。 encode はエンベロープを放射します。 decode のデフォルトのソースバイト制限は 8 MiB です。 decode_with_limit は発信者制限を受け入れます。 JSON ネストは制限されています。すでに重複キーを破棄している可能性がある汎用マップに解析するのではなく、この生のデコーダーを使用します。

完全な JSON スキーマ は、形状、必須フィールド、およびバリアントを指定します。範囲/タイプのチェックと重複キーの検出も追加で適用されます。スカラーを下げるサブセットには、リテラル、変数/定数、加算/減算/乗算、比較、代入、if/while、return、break/Continue が含まれます。関数参照、直接呼び出し、間接呼び出しがサポートされています。集計式はサポートされていません。 Opaque はスキーマで表現できますが、引き下げることはサポートされていません。

移行には、古い裸のプログラム ペイロードをラップし、数値 JSON リテラルを 10 進整数文字列または浮動小数点ビット文字列に置き換える必要があります。バージョン管理されていない古いペイロードは拒否されます。形式 1 のペイロードは形式 2 に移行する必要があります。定義に program.declarations (未使用の場合は空の配列) を追加し、明示的に convention/linkage を追加します。 AST と入力された IR のバージョン番号は独立しています。現在は両方とも 2 で、セマンティクス バージョンは 1 です。

拒否される入力とCLI回復

次の完全な入力をinvalid.jsonとして保存します。

{
  "format_version": 99,
  "semantics_version": 1,
  "features": [],
  "program": {
    "globals": [],
    "functions": [],
    "declarations": []
  }
}
cargo run --locked --features socket-cli -- ir lower invalid.json -o rejected.wir
Failed to parse socket JSON: unsupported AST format_version 99; expected 2

コマンドはゼロ以外の状態で終了し、新しい出力を作成したり既存のファイルを上書きしたりしません。型の不一致も出力発行前に失敗します。 socket-cliなしでビルドしたバイナリは状態2で終了し、--features socket-cliを含むリカバリコマンドを出力します。