extern 関数(プラグイン)¶
実験的機能
プラグインシステムは実験段階で、まだ十分に成熟していません。WASM ランタイムは新しく、実際の利用実績は少なく、ABI、マニフェスト形式、診断、ロックファイルによる固定の規則は、どのリリースでも変更される可能性があります。プラグインの結果には、未レビューの外部コードと同様の慎重さで接してください。問題があれば報告してください。
extern 関数を使うと、graphcal プロジェクトから言語の外部で実装された量の関数を呼び出せます。プロジェクト内にベンダリングされた WebAssembly プラグインモジュールや、埋め込み側(CLI、言語サーバー、評価エンジンを組み込むプログラム)が提供するネイティブ関数が対象です。反復ソルバー、特殊関数、物性ライブラリ、座標変換など、dag ブロックでは表現できない計算のための手段です。
純粋性と呼び出し順序¶
埋め込み側が提供するネイティブ実装も含め、プラグイン関数は純粋でなければなりません。結果は明示的な入力だけに依存し、呼び出し履歴、共有可変状態、他の関数の実行順序に依存してはいけません。内部キャッシュは結果を変えない場合に限り許されます。
Graphcal はデータ依存関係を尊重しますが、独立したプラグイン呼び出しの順序は保証しません。観測された順序は契約ではありません。ルートとインライン呼び出しの間、評価の間、バージョンの間、実行戦略の間で変わる可能性があります。計算を連携させるために宣言順序やプラグインの副作用を利用しないでください。
純粋でなく順序に依存する実装はプラグインのバグであり、Graphcal の評価結果は未定義になります。Graphcal は、その計算値について何も保証しません。これはメモリ安全性を損なうことや、適用される ABI 検証、サンドボックス、リソース制限を回避することを許可するものではありません。ネイティブ実装は引き続き信頼された埋め込み側のコードであり、Graphcal はその純粋性を証明できません。
プラグインの宣言¶
import plugin ブロックは、プラグインが提供する関数と、特に重要な、その完全な次元シグネチャを、インポート箇所で graphcal の語彙を使って宣言します。
import plugin "plugins/fluids.wasm" as fluids {
fn density(p: Pressure, t: Temperature) -> Mass * Length^-3;
fn lerp<D: Dim>(a: D, b: D, t: Dimensionless) -> D;
fn geometric_mean<D1: Dim, D2: Dim>(x: D1, y: D2) -> D1^(1/2) * D2^(1/2);
}
- パス文字列はプラグインを識別します。
.wasmで終わるパスは WebAssembly モジュールファイルを指し、インポート元ファイルではなく必ずプロジェクトルートを基準に解決され、その内部に収まる必要があります。それ以外の表記(組み込みの"graphcal:demo"など)は、埋め込み側のホスト関数レジストリがネイティブに提供する識別子です。 - エイリアス(
as fluids)は必須です。extern 関数はfluids::density(...)のようにエイリアスで修飾してのみ呼び出せ、裸の名前では呼び出せません。これにより、モジュールのインポートと同じ明示性が保たれ、組み込み関数の名前空間は閉じたままになります。 - 各
fnは名前付きパラメーターと結果の型を宣言します。パラメーターと結果の型には、Bool、Int、次元式で記述する量の型、または 1 つ以上の宣言済みインデックス変数を軸とする、それらのスカラー種類の配列(flags: Bool[I]、counts: Int[I]、xs: D[I]、matrix: D[I, J])を指定できます。結果にはさらに、スコープ内のレコード型も指定できます(レコード形の結果を参照)。
シグネチャはプラグインから推論せず、明示的に宣言します。ソース内の宣言がプロジェクトの型検査で使う契約となり、プレーンテキストの差分でレビューでき、バイナリがなくてもエディターツールで利用できます。読み込み時には、各宣言が .wasm モジュールに埋め込まれたマニフェストに対して構造的に検証されます。次元変数やパラメーターの改名は許されますが、次元の構造に相違があればコンパイルエラー(P005)になるため、ソースとバイナリのずれが黙って別の意味に解釈されることはありません。次元シグネチャに未知の修飾パスがある場合、診断はその完全なパスを保持し、次元の末端名へと平坦化しません。
次元変数¶
シグネチャは、明示的な山括弧の束縛子で次元変数を宣言し、次元に関する多相関数にできます。
各呼び出し箇所で、D は実際の引数の次元に束縛されます。他のすべての D パラメーターもそれに一致しなければならず、結果の次元はその束縛から計算されます。不一致の診断には、その束縛を確定させた先行パラメーターの名前が表示されます。検証済みシグネチャのメタデータに矛盾がある場合でも、Graphcal は仮のパラメーター名に置き換えません。結果の型では複数の変数を有理数のべき乗で組み合わせることができ、変数間の完全な次元代数が使えます。
検査を決定可能に保つ規則が 1 つあります。すべての次元変数は、複合形(D^2、D1 * D2)や結果で使う前に、裸のパラメーター(x: D)として出現しなければなりません。 fn sq<D: Dim>(x: D^2) -> D のようなシグネチャは拒否されます。単に束縛するのではなく、D について方程式を解く必要が生じるためです。
次元多相性は意図的にパラメトリックです。プラグインは D に束縛された次元を知ることがないため、単位に基づいて分岐できません。graphcal が禁止する暗黙的な動作は、プラグイン境界を越えても禁止されたままです。
インデックス変数を軸とする配列¶
シグネチャはインデックス変数(I: Index)も宣言でき、それを軸とする量、真偽値、整数の配列を受け取り、返すことができます。
import plugin "plugins/dsp.wasm" as dsp {
fn smooth<D: Dim, I: Index>(xs: D[I], window: Dimensionless) -> D[I];
fn total<D: Dim, I: Index>(xs: D[I]) -> D;
fn transpose<D: Dim, I: Index, J: Index>(matrix: D[I, J]) -> D[J, I];
fn invert<I: Index>(flags: Bool[I]) -> Bool[I];
fn increment<I: Index>(counts: Int[I]) -> Int[I];
}
index Maneuver = { Departure, Correction, Insertion };
node dv: Velocity[Maneuver] = { Maneuver#Departure: 2.0 km/s, Maneuver#Correction: 0.5 km/s, Maneuver#Insertion: 1.5 km/s };
node dv_smooth: Velocity[Maneuver] = dsp::smooth(@dv, 3.0);
インデックス変数にも、次元変数と同じ明示的な規律が適用されます。
- 配列の各軸には、宣言された
Index束縛子のいずれかを指定しなければなりません。具体的な宣言済みインデックス(Velocity[Maneuver])や構造的インデックス(D[Fin(3)])は extern シグネチャに直接記述できませんが、呼び出し箇所ではどちらも変数に束縛できます。インデックス変数はパラメトリックです。プラグインに見えるのは順序付きの軸長と密な行優先の型付き値だけで、インデックスの同一性やラベルは見えません。 - 同じインデックス変数を共有する 2 つのパラメーターには、同じインデックスを軸とする配列を渡さなければなりません。
- 結果の各軸は、いずれかのパラメーターの軸となるインデックス変数を再利用しなければなりません。 軸長は入力で決まるため、プラグインは出力の軸長を独自に作れません。結果の軸は並べ替えられます(
D[I, J] -> D[J, I])。束縛された引数の型付きインデックスとキーを厳密に使って再構成され、他の配列と同様に添字アクセスやfor内包表記に使えます。 - 裸の量配列の要素(
xs: D[I])は、裸の量パラメーターと同じくDの束縛箇所になります。Bool 配列と Int 配列は次元変数の束縛に関与しません。 - 要素の種類は厳密に検査されます。
Bool[I]、Int[I]、Dimensionless[I]は別々の型で、暗黙の変換はありません。配列は 1 つ以上の軸を持ち、各軸は空であってはなりません。
レコード形の結果¶
関数は、結果としてスコープ内のレコード型を宣言することで、複数の名前付き値を一度に返せます。
import plugin "plugins/stats.wasm" as stats {
fn span<D: Dim, I: Index>(xs: D[I]) -> DvSpan;
}
type DvSpan { DvSpan(lo: Velocity, hi: Velocity) }
node dv_span: DvSpan = stats::span(@dv);
node spread: Velocity = @dv_span.lo - @dv_span.hi;
プラグインのマニフェストは型の名前を知りません。平坦化されたフィールド構造(名前、順序、種類)を宣言し、ソースの宣言がその構造を公称レコード型に束縛します。フィールド名と順序は契約の一部です。{min, max} を宣言するプラグインは、レコードが {lo, hi} である宣言とは一致しません(P005)。結果は通常のレコード形の代数的な値として評価され、フィールドアクセスとパターンマッチが使えます。プラグイン ABI はその平坦表現を struct 形と呼びますが、これは Graphcal における別の型カテゴリではありません。
この段階では以下の制約があり、それぞれ専用のコンパイルエラーがあります。
- 名前付きの型はレコード形、つまり型と同名のコンストラクターを 1 つだけ持つ必要があります。複数のコンストラクターを持つ型には、境界を越えるための単一の平坦なレイアウトがありません。
- フィールドは
Bool、Int、または具体的な量の型でなければなりません。ジェネリックレコードや次元変数のフィールドは、まだ境界を越えられません。 - レコード形の代数的な値は結果にのみ使えます。このようなパラメーターは、代わりに個別の量パラメーターとして渡してください。
extern 関数の呼び出し¶
extern 呼び出しは修飾された関数呼び出しの形を取り、他の式と同様にグラフに参加します。
param v0: Velocity = 100.0 m/s;
param v1: Velocity = 300.0 m/s;
node v_mid: Velocity = demo::lerp(@v0, @v1, 0.25);
以下の制約はすべてコンパイル時に検査されます。
- extern 関数は実行時に提供されるため、
const式、定義域の境界、単位のスケール式には使えません(P004)。 - 呼び出しはエイリアスで修飾しなければなりません。裸の
lerp(...)は未知の関数になります。 - インデックス付きの値に対する自動リフティングはありません。extern の量関数を要素ごとに適用するには、明示的な
for内包表記を使い、反復をソース上に見える形にします。
index Sample = { A, B };
node xs: Length[Sample] = { Sample#A: 1.0 m, Sample#B: 2.0 m };
node mids: Length[Sample] = for s: Sample {
demo::lerp(@xs[s], 10.0 m, 0.5)
};
WASM プラグインモジュール¶
graphcal プラグインはコア WebAssembly モジュールです。プロジェクトにベンダリング(ソースと一緒にコミット)され、組み込みの決定論的インタープリターで実行されます。モジュールは ABI を満たさなければならず、以下はすべて、プラグインのコードが実行される前の読み込み時に検査されます。
- マニフェスト。 モジュールは
graphcal-manifestというカスタムセクションに JSON マニフェストを埋め込み、abi_version: 5と各提供関数の次元シグネチャ(次元変数、インデックス変数、名前付きパラメーター、配列の種類と struct のフィールドレイアウトを含む結果)を宣言します。固定次元は、プレリュードの 8 つの基本次元(Length、Time、Mass、Temperature、ElectricCurrent、Amount、LuminousIntensity、Angle)を有理数指数で組み合わせた構造として記述します。たとえばVelocityはLength^1 * Time^-1です。量の種類には JSON タグ"quantity"を使います。配列のエントリーには、量、"bool"、"int"のいずれかの明示的な要素種類があります。ユーザー定義の基本次元はバイナリ境界を越えられません。JSON ペイロードの上限は 256 KiB、関数数の上限は 256 です。名前は UTF-8 で最大 256 バイトです。各関数が宣言できる次元変数、インデックス変数、パラメーターはそれぞれ最大 32 個です。配列の軸は最大 31、struct の結果の平坦化されたフィールドは最大 256、各単項式の変数因子と固定次元因子の合計は最大 64 です。 - 値の ABI。 各関数の wasm エクスポート型はシグネチャに従います。量/
Bool/Intパラメーターはそれぞれ 1 つのf64ABI スロットを使います(量は生の SI 基本単位、Intは正確に表現可能な整数、Boolは1.0/0.0)。ランクRの配列パラメーターはi32ポインターと、それに続くR個のi32軸長で表現され、形状の積に等しい個数の密なリトルエンディアン・行優先のf64要素を指します。量の要素は有限でなければなりません。Bool の要素は数値の0.0または1.0(-0.0は false)で、Int の要素にはスカラー Int と同じ厳密な binary64 ポリシーが適用されます。すべての要素は、型付きのランタイム/プラグインコードに入る前と、結果を受け取る際の両方で検証されます。量/Bool/Intの結果は単一のf64戻り値です。配列または struct の結果では戻り値の代わりに末尾のi32出力ポインターを使い、プラグインがそこへ書き込みます。配列ではシグネチャで束縛された結果の軸長の積、struct ではフィールドごとに 1 スロットです。変換後の完全な関数シグネチャで使える生の WebAssembly パラメーターは最大 32 個で、配列ポインター、各軸の長さ、出力ポインターはそれぞれ別々に数えます。これはマニフェストと Rust SDK によりコード生成前に検査されます。非有限の量には graphcal の通常の非有限値の封じ込めが適用されます。 - アロケーターのエクスポート。 配列または struct を受け取るか返すモジュールは、メモリを
"memory"として、さらにgraphcal_alloc(size: i32) -> i32(8 バイト境界に整列)とgraphcal_free(ptr: i32, size: i32)をエクスポートしなければなりません。ホストは呼び出し前にすべてのバッファを確保し、入力を書き込み、結果を読み取った後ですべて解放します。プラグインが呼び出しをまたいでバッファを保持することはありません。 - インポートの禁止。 モジュールは何もインポートできません。ただし、ホストが提供する失敗報告用の
graphcal::fail(ptr: i32, len: i32)だけは例外です。インポートの禁止によりプラグインは I/O を行えません。また、ホストは論理的な呼び出しごとに新しいインスタンスを作るため、可変グローバル、テーブル、線形メモリ、アロケーター状態、startの副作用が、あるグラフノードや再評価から別のものへ履歴を持ち越せません。これらの規則により、境界の純粋性が構造的に保たれます。WASI やその他のホスト API をインポートするモジュールは、専用の診断(P007)で拒否されます。graphcal::failをインポートするモジュールは、失敗メッセージを読めるよう、線形メモリを"memory"としてエクスポートしなければなりません。 - リソースの上限。 プラグインモジュールのサイズは、デフォルトで最大 16 MiB です。呼び出しには fuel 予算(おおむね命令数)とメモリの上限があり、超えるとエラーになります。fuel 予算の設定はプロジェクトの fuel ポリシーを参照してください。
- 決定性。 プラグインの算術は IEEE-754 に従って決定論的であり、数学処理もモジュール内にコンパイルされるため、結果はプラットフォーム間でビット単位で一致します。
プロジェクトの fuel ポリシー¶
各プラグイン呼び出しには、デフォルトで 100,000,000 fuel 単位が割り当てられます。レビュー済みの複数ファイルプロジェクトでは、graphcal.toml で、この予算をプロジェクト全体、または名前を指定した重い関数だけについて増減できます。
[package]
name = "simulation"
[plugins]
fuel_per_call = 250_000_000
[[plugins.function_limits]]
plugin = "plugins/solver.wasm"
function = "solve"
fuel_per_call = 1_500_000_000
関数ごとのエントリーは [plugins].fuel_per_call より優先されます。どちらもない場合は、埋め込み側のデフォルトが適用されます。各設定値は 1 以上 2,000,000,000 以下でなければなりません。この厳格な上限により、言語サーバーでプロジェクトを開く場合にも、処理が可用性に及ぼす影響を有限に抑えます。
各パッケージのマニフェストは、自身のプラグインのデフォルトと関数ごとの上限だけを制御します。 アプリケーションのパッケージが依存関係の予算を黙って上書きすることはなく、依存関係も インタープリターの厳格な上限を変更できません。同じパッケージの異なるバージョンは、 別々のプラグイン同一性とポリシーを持ちます。
プラグインのパスは、移植可能な、ルートを基準とする .wasm パスでなければなりません。そのプラグインがエントリーポイントから読み込まれる際、セレクターは宣言済みの extern 関数のいずれかに一致する必要があります。有効なプラグインに対して、古い関数名や綴り間違いを指定すると、マニフェストエラーになります。選択された予算は、新しいインスタンス化と start、アロケーターの呼び出し、カーネル本体、解放まで、論理的な 1 回の呼び出し全体に適用されます。メモリ、テーブル、エンコード済みモジュール、厳格なコンパイル、キャッシュの上限は変わりません。
上書きの範囲は狭く保ち、ベンチマークで確認してください。fuel の上限は決定論的な遮断器として働くもので、実時間の期限ではありません。大きな値を指定すると、サンドボックス内での実行であっても、エディターでの再評価の応答性が低下し得ます。
定義域に関する失敗(たとえば範囲外の物性検索)を報告するには、プラグインから UTF-8 メッセージを付けて graphcal::fail を呼び出します。呼び出しは中断され、メッセージはノードの診断に表示されます。トラップや fuel の枯渇も同様に報告されますが、独自のメッセージは付きません。
作成方法¶
graphcal-plugin Rust SDK では、シグネチャを graphcal の extern 宣言構文、関数本体を Rust で、各関数を一度だけ宣言します。その単一の情報源から、wasm エクスポートと埋め込みマニフェストの両方を生成します。
graphcal_plugin::plugin! {
/// Linear interpolation between `a` and `b`.
fn lerp<D: Dim>(a: D, b: D, t: Dimensionless) -> D {
(b - a).mul_add(t, a)
}
}
ABI v5 SDK では、配列パラメーターは借用された型付きビューとして渡されます。量には ArrayView<'_, f64>、Bool には ArrayView<'_, bool>、Int には ArrayView<'_, i64> を使います。これらは順序付きの形状と平坦化された行優先の型付きデータを公開し、配列を返す関数本体は、対応する検証済みの graphcal_plugin::Array<T> を返します。
Rust の宣言では、たとえば -> { lo: Pressure, hi: Pressure } のようにレコードの結果を構造的に記述できます。マクロは、本体が使う対応する名前付き出力型(span なら SpanOutput)を生成します。Bool、Int、量の本体では、それぞれ bool、i64、SI の f64 を使います。
graphcal plugin new はすぐにビルドできるクレートの雛形を作り、graphcal plugin test はビルドしたモジュールを検証して呼び出します。失敗の報告、ネイティブテスト、SDK を使わない作成方法を含む一連の手順は、プラグインの作成を参照してください。プラグインは、上記のモジュール契約を満たす任意のツールチェーンの出力でかまいません。graphcal-plugin-abi クレートは、ビルドツール向けにマニフェストモデルと embed_manifest ヘルパーを提供します。
信頼: ロックファイルによる固定¶
graphcal.toml を持つプロジェクトでは、ロックファイルがプラグインコードの信頼境界です。graphcal deps lock は、パッケージのソースから wasm プラグインのインポートを探し、各ファイルの SHA-256 を graphcal.lock に記録します。
[[plugin]]
path = "plugins/fluids.wasm"
sha256 = "3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855c"
読み込み時には固定値を強制し、確認プロンプトではなく必ずエラーにします。固定値のないプラグインは P009(「graphcal deps lock を実行してください」)で失敗し、バイト列のハッシュが固定値と異なるプラグインは P010 で失敗します。したがって、新規または変更されたプラグインコードがプロジェクトに入るには、レビュー可能な graphcal.lock の差分を通過する必要があります。プラグイン成果物はパッケージルート内の通常ファイルでなければならず、シンボリックリンクは使えません。プロジェクトの読み込み上限も適用されます。
パッケージごとの扱いは次のとおりです。
- アドホックなファイル(上位のどこにも
graphcal.tomlがない)は、プラグインを固定せずに読み込みます。監査の基準となるロックの仕組みはありませんが、サンドボックスとリソースの上限は引き続き適用されます。 - 依存パッケージも Wasm プラグインを宣言でき、個別の
[[plugin]]エントリーは不要です。 ロックされたツリーハッシュには、マニフェスト、ソースディレクトリ、およびそのソースが インポートするすべての Wasm 成果物が含まれます。ネストした DAG 内のインポートや、 ソースディレクトリ外のバイナリーも対象です。パスは宣言元パッケージのルート内で解決され、 ディレクトリの脱出やシンボリックリンクは拒否されます。ネイティブ評価と LSP 解析は、 検証済みの取得済みバイト列だけを実行します。バイナリーの変更には、レビュー済みの新しい 依存関係ロックが必要です。 - パッケージインスタンスが Wasm の同一性のスコープになります。異なる 2 つのバージョンが
plugins/solver.wasmをインポートしても衝突しません。埋め込み側が提供するホスト関数は、 引き続きグローバルな同一性を持ちます。
失敗の意味論¶
静的検査は、ホスト関数を呼び出さずに関数メタデータを使います。extern 呼び出しは実行時専用であり、定数式や定義域の境界からは呼び出せません。
extern 関数は実行時に失敗することがあります(プラグインによる失敗報告、トラップ、fuel の枯渇、ホスト関数によるエラー返却)。失敗には graphcal のノード単位の封じ込めモデルが適用されます。
- 失敗したノードは、エイリアス、関数、プラグインを示す評価エラーを報告します(たとえば
extern function `inv.inverse` (plugin "plugins/inv.wasm") failed: division by zero)。 - そのノードに依存するノードは
dependency failedを報告します。 - 無関係のノードは評価を続けます。呼び出しが失敗するとプラグインのインスタンスも破棄されるため、壊れたプラグインが後続の呼び出しを破損させることはありません。
宣言された extern 関数がまったく存在しない場合(プラグインファイルがない、検証に失敗する、マニフェストにその関数がない)は、評価開始前に宣言上で報告される読み込み時エラーです(原因に応じて P003、P005–P010)。
ホスト関数レジストリ¶
埋め込み側は HostFunctionRegistry を注入することでネイティブ実装を提供します。これは (plugin path, function name) から、fn(&[HostFnValue]) -> Result<HostFnValue, HostFnError> という形の関数へのマップです。HostFnValue は、単一の f64、形状を持つ行優先の HostArray、または固定レイアウトのレコードスロットです。WASM プラグインも同じインターフェースを通じて登録されます(graphcal-plugin-host クレートが、プロジェクトにベンダリングされたモジュールをレジストリに読み込みます)。そのため、評価器自体は WASM に依存しません。
use graphcal_eval::eval::compile_and_eval_from_project_with_host_fns;
use graphcal_eval::host_fns::demo_registry;
use graphcal_plugin_host::{PluginHost, register_project_plugins};
let mut registry = demo_registry();
register_project_plugins(&PluginHost::new(), &project, &mut registry);
let result = compile_and_eval_from_project_with_host_fns(&project, &overrides, ®istry)?;
ネイティブのレジストリエントリーにはマニフェストがないため、その宣言はそのまま信頼されます。これは埋め込み側が管理する関数に適した扱いです。CLI と言語サーバーは組み込みのデモプラグイン("graphcal:demo": lerp、inverse、geometric_mean、normalize、matrix_transpose、dv_range)を注入するため、配列や struct を返すものを含め、プラグインファイルなしで extern 宣言を試せます。