組み込み
本ドキュメントは、Keleusma のホスト向け組み込みインターフェースについて説明します。VM の構築、ネイティブ関数の登録、アリーナのサイズ設定、コルーチンスクリプトの呼び出しと再開のプロトコル、そしてエラー回復を扱います。このインターフェースのリファレンスは src/vm.rs です。実際に動作する例は examples/ にあります。
VM のライフサイクル
Keleusma の VM は、単一スレッドのコルーチンドライバです。ホストがバイトコードとアリーナを所有します。VM は、その存続期間にわたってアリーナを借用します。
最小限のライフサイクルは以下のとおりです。
#![allow(unused)]
fn main() {
use keleusma::compiler::compile;
use keleusma::lexer::tokenize;
use keleusma::parser::parse;
use keleusma::vm::{DEFAULT_ARENA_CAPACITY, Vm};
use keleusma::Arena;
let tokens = tokenize(SOURCE)?;
let program = parse(&tokens)?;
let module = compile(&program)?;
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena)?;
}
4 つのフェーズは、4 つの異なる値の型を生成します。tokenize は Vec<Token> を生成します。parse は Program 構文木を生成します。compile は Module バイトコードオブジェクトを生成します。Vm::new はモジュールを消費し、アリーナを借用し、構造検証とリソース境界検証を実行して、呼び出し可能な状態の VM を返します。
VM とモジュールは、アリーナのライフタイムを共有します。ホストは、少なくとも VM が存続する間はアリーナを生存させ続けなければなりません。これは、Vm の 'arena ライフタイムパラメータを通じてボローチェッカーによって強制されます。
プリコンパイル済みバイトコードの読み込み
プリコンパイル済みの .kel.bin ファイルを持つホストは、字句解析、構文解析、およびコンパイルの各ステップをスキップします。
#![allow(unused)]
fn main() {
let bytes = std::fs::read("script.kel.bin")?;
let mut vm = Vm::load_bytes(&bytes, &arena)?;
}
ワイヤフォーマットは自己記述的です。ヘッダはマジック、長さ、バージョン、ターゲットのワード、アドレス、フロート幅を運びます。Vm::load_bytesはフレーミングを検証し、構造検証を走らせ、資源境界検証を走らせて、VMを返します。検証の失敗はフレーミングの失敗にはVmError::LoadError、解析の失敗にはVmError::VerifyErrorとして返されます。
スクリプトを呼ぶ
VMは2つのエントリポイントを公開しています — 実行を始めるVm::call(args)と、yieldのあとに続けるVm::resume(value)です。両方ともResult<VmState, VmError>を返します。
#![allow(unused)]
fn main() {
pub enum VmState {
Finished(Value),
Yielded(Value),
Reset,
}
}
3つの状態は3つの関数カテゴリに対応します。
fn(原子的・全域)。スクリプトは終了して値を返します。callはVmState::Finished(value)を返します。yield(非原子的・全域)。スクリプトはホストに値をyieldします。callはVmState::Yielded(value)を返します。ホストはホスト提供の入力でresume(value)を呼びます。スクリプトはまたyieldするか、終了します。loop(生産的・発散)。スクリプトは反復ごとにyieldし、本体の終わりでリセットします。callはVmState::Yielded(value)を返します。ホストはresume(value)を呼んで次のyieldまで動かします。本体が完了したあと、次の呼び出しはVmState::Resetを返します。ホットコードスワップはリセットの境界で許容されます。
典型的なyield駆動ループは次のように見えます。
#![allow(unused)]
fn main() {
let mut state = vm.call(&[Value::Int(seed)])?;
loop {
match state {
VmState::Yielded(out) => {
let reply = compute_host_response(&out);
state = vm.resume(reply)?;
}
VmState::Reset => {
state = vm.resume(Value::Int(next_seed))?;
}
VmState::Finished(value) => {
handle_result(value);
break;
}
}
}
}
ネイティブ関数
ネイティブ関数はホストがVMに登録するRust関数で、スクリプトが名前で呼べます。ホストは関数名、関数ポインタまたはクロージャ、そして(任意で)WCETとWCMUの境界を宣言します。
人間工学的な型付き登録
推奨される経路はマーシャリング層を使います。引数の型と戻り値の型がKeleusmaTypeを実装する、任意のRust関数またはクロージャ(引数0〜4個)はregister_fnで登録します。
#![allow(unused)]
fn main() {
vm.register_fn("math::add", |a: i64, b: i64| -> i64 { a + b });
vm.register_fn("math::sin", |x: f64| -> f64 { libm::sin(x) });
vm.register_fn("strings::upper", |s: String| -> String { s.to_uppercase() });
}
失敗する可能性のある関数には、register_fn_fallibleがResult<R, VmError>を受け取ります。
#![allow(unused)]
fn main() {
vm.register_fn_fallible("io::read_setting", |key: String| -> Result<String, VmError> {
fetch(&key).map_err(|e| VmError::NativeError(format!("{}", e)))
});
}
引数の取り出し、引数数のチェック、戻り値の包装は自動的に行われます。境界での型不一致は実行時にVmError::TypeErrorとして表面化します。
deriveマクロによるカスタム型
ホストの構造体と列挙型はKeleusmaTypederive を通じてマーシャル可能になります。
#![allow(unused)]
fn main() {
use keleusma::KeleusmaType;
#[derive(KeleusmaType, Debug, Clone)]
struct Point {
x: f64,
y: f64,
}
vm.register_fn("geom::midpoint", |a: Point, b: Point| -> Point {
Point {
x: (a.x + b.x) / 2.0,
y: (a.y + b.y) / 2.0,
}
});
}
スクリプトは、境界を越えて正しく流れるように、ホストの Point に対して構造的に互換な型を宣言しなければなりません。許容される相互運用型については TYPE_SYSTEM.md を参照してください。
低レベルの登録
関数が生のValue列挙型を検査しなければならないときは、&[Value]を受け取ってResult<Value, VmError>を返す関数ポインタを直接登録します。
#![allow(unused)]
fn main() {
fn first_argument(args: &[Value]) -> Result<Value, VmError> {
args.first()
.cloned()
.ok_or_else(|| VmError::NativeError(String::from("missing arg")))
}
vm.register_native("debug::first_argument", first_argument);
}
ボックスされたクロージャヴァリアントのregister_native_closureがホスト状態を取り込みます。コンテキスト対応ヴァリアントのregister_native_with_ctxはアリーナの借りを運ぶNativeCtx<'a>を受け取り、アリーナメモリに動的文字列を割り当てるネイティブが使います。
バンドルされているネイティブ
V0.2.0はスクリプト側のテキスト合成機構(to_string、concat、slice、lengthユーティリティネイティブとf-string補間サーフェス)を引退させました。ランタイムは小さなバンドルセットを出荷しています。
keleusma::utility_natives::register_utility_nativesはprintln(no_stdターゲットでは無操作のデバッグプリントプリミティブ。出力が欲しいホストはregister_native_closureで上書き)を登録します。keleusma::audio_natives::register_audio_nativesはaudio::midi_to_freq、audio::freq_to_midi、audio::db_to_linear、audio::linear_to_db、および STANDARD_LIBRARY.md に列挙されたmath::*関数を登録します。keleusma::stddsl::Math、Audio、ShellはVm::register_libraryを通じて登録します(下の「標準DSLライブラリ」セクション参照)。
すべて内部的にregister_fnまたはregister_nativeを通じて登録します。ホストはバンドルされたネイティブすべてを登録することも、サブセットを登録することも、自分の実装で任意の関数を置き換えることもできます。
ホスト定義の文字列ヘルパー
言語は重い文字列操作に適した道具ではなく、V0.2.0は文字列標準ライブラリを出荷していません。アプリケーションが文脈の中で文字列作業を必要とする場合、Rustのネイティブ関数を登録し、スクリプトにuse宣言を通じて消費させてください。 Rustの標準ライブラリは、スクリプト内で構築するよりはるかにうまく、フォーマット、分割、正規表現、Unicode操作、エンコーディング変換を扱います。
#![allow(unused)]
fn main() {
vm.register_fn("text::upper", |s: String| -> String { s.to_uppercase() });
vm.register_fn("text::trim", |s: String| -> String { s.trim().to_string() });
vm.register_fn_fallible(
"text::split_first_word",
|s: String| -> Result<String, VmError> {
s.split_whitespace()
.next()
.map(|w| w.to_string())
.ok_or_else(|| VmError::NativeError("empty input".into()))
},
);
}
スクリプト側:
use text::upper
use text::trim
use text::split_first_word
fn greet(name: Text) -> Text {
text::upper(text::trim(name))
}
文字列に関するより広いフレーミングはFAQ.mdを参照してください。
標準DSLライブラリ
keleusma::stddslモジュールは、ホストが単一の呼び出しを通じて登録する3つのバンドルされたライブラリを出荷しています。各バンドルはLibraryトレイトを実装するユニット構造体です。トレイトのregisterメソッドがバンドルのネイティブ関数をVMに取り付けます。
#![allow(unused)]
fn main() {
use keleusma::stddsl;
let mut vm = Vm::new(module, &arena)?;
vm.register_library(stddsl::Math); // math::sqrt, math::floor, ...
vm.register_library(stddsl::Audio); // audio::midi_to_freq, ...
vm.register_library(stddsl::Shell); // shell::getenv, shell::run, shell::exit
}
stddsl::Mathとstddsl::Audioはfloats Cargo機能を必要とします。stddsl::Shellはshell機能を必要とし、これはstd依存を追加するのでno_stdビルドと互換性がありません。keleusma-cliクレートは両方の機能を有効にし、デフォルトで3つすべてのバンドルを登録します。バンドルされたテキスト合成が欲しいホストは、ホスト側のformat / to_string / concatネイティブをregister_verified_nativeを通じて登録するか(上の「ホスト定義の文字列ヘルパー」セクション参照)、自分のLibraryバンドルを実装します。
自分の再利用可能バンドルを出荷したいホストは、ホスト側の型にLibraryトレイトを実装します。トレイトが拡張性サーフェスで、バンドルされたライブラリはパターンの例であって閉じた集合ではありません。
#![allow(unused)]
fn main() {
use keleusma::stddsl::Library;
use keleusma::vm::Vm;
pub struct MyDsl;
impl Library for MyDsl {
fn register<'a, 'arena>(self, vm: &mut Vm<'a, 'arena>) {
vm.register_fn("mydsl::greet", |name: i64| -> i64 { name + 1 });
// ... さらにネイティブを登録 ...
}
}
// 使用箇所:
vm.register_library(MyDsl);
}
単一ファイルスクリプト
Keleusmaスクリプトは必然的に単一ファイルです。言語の内側にはimportやmodの機構はありません。スクリプト間の再利用は意図的にV0.2サーフェスの外です。アプリケーションのニーズがモジュール化を欲しがるところまで成長したら、推奨される経路はカスタムDSLライブラリを作ることです。スクリプトが呼ぶネイティブを登録するLibraryをホスト側のユニット構造体に実装して、各スクリプトにuse宣言を通じて同じ語彙を消費させます。再利用の単位はホスト側のライブラリで、スクリプトではありません。
不透明ホスト型
ホストがRust値を、その内部構造を明かさずにスクリプトに公開する必要がある場合、V0.2.0で導入されたHostOpaqueトレイトを使います。ホストは具体的なRust型に対してトレイトを実装します。スクリプトは関数シグネチャで型を名前で宣言し、型検査器は名前をType::Opaqueとして解決します。ネイティブ関数はhost_arcを通じて不透明値を生み、dyn HostOpaque::downcast_refを通じて型付き参照を抽出して消費します。
#![allow(unused)]
fn main() {
use keleusma::{host_arc, HostOpaque, Value};
// 外部型に外部トレイトを実装するときのRustの孤児ルール違反を
// 避けるために必要なnewtype。
struct RustString(String);
impl HostOpaque for RustString {
fn type_name(&self) -> &'static str { "RustString" }
}
vm.register_native("make_string", |args| {
// Rustの値から不透明を構築する。
Ok(Value::Opaque(host_arc(RustString(String::from("hello")))))
});
vm.register_native("upper_case", |args| {
// 不透明を消費して、新しい不透明を返す。
let opaque = match &args[0] {
Value::Opaque(o) => o.clone(),
other => return Err(VmError::TypeError(format!(
"expected RustString, got {}", other.type_name()))),
};
let s = opaque.as_ref().downcast_ref::<RustString>().ok_or_else(|| {
VmError::TypeError(format!(
"expected RustString, got opaque {}", opaque.type_name()))
})?;
Ok(Value::Opaque(host_arc(RustString(s.0.to_uppercase()))))
});
}
スクリプト側:
use make_string
use upper_case
fn main() -> RustString {
let s = make_string();
upper_case(s)
}
不透明値はArcを通じてホスト管理されるので、アリーナとは独立したライフタイムを持ちます。対話の型でyield境界を越えることができ、アリーナリセットを跨いで持続します。ポインタの同一性が等価意味論です。2つの不透明値は、同じArc割り当てを共有するときのみ等しいと比較されます。
不透明値は、割り当てがホスト管理されているのでスクリプト側のWCMU境界にゼロを貢献します。メモリフットプリントが重要な重い作業に対しては、Vm::set_native_boundsを通じてネイティブごとの計上を付けるので、検証器は境界づけられたホスト貢献を見ます。
std::string::Stringをスクリプトに公開する完全なウォークスルーはexamples/opaque_rust_string.rsを参照してください。
WCETとWCMUの計上
ネイティブ関数呼び出しはWCETとWCMU解析に参加します。デフォルトでは、ネイティブ呼び出しはサイクルでゼロコスト、ヒープでゼロバイトと計上されます。健全な境界が必要なホストは、VM構築の前に(あるいは既に構築されたVMにはverify_resourcesを呼ぶ前に)ネイティブごとの境界を宣言します。
#![allow(unused)]
fn main() {
// vm.set_native_bounds(name, wcet_cycles, wcmu_bytes)
vm.set_native_bounds("math::sin", 25, 0)?;
vm.set_native_bounds("strings::upper", 100, 256)?;
}
境界はホストの誓いです。wcetはワーストケースのパイプライン化サイクル数、wcmu_bytesはワーストケースのアリーナヒープ割り当てです。検証器は独立した測定なしに宣言された値を受け入れます。ホストが正確性に責任を負います。通常は測定か、ネイティブ関数の境界解析を通じてです。完全なウォークスルーはexamples/wcmu_attestation.rsを参照してください。
CPUサイクルでのキャリブレートされたWCET
バンドルされたNOMINAL_COST_MODELは、1つのプラットフォーム上での相対的順序付けに適した、オペコードごとのパイプライン化サイクル推定値を返します。値は特定のホストCPUに対して測定されていません。データの移動に1、算術に2、除算に3、複合構築に5、関数呼び出しに10を割り当てます。デプロイメントターゲット用の実際のCPUサイクルでのWCETが欲しいホストは、keleusma-benchワークスペースメンバーが生成したMEASURED_COST_MODELを消費します。
配線はkeleusma-bench/measured_cost_models/からの測定モデル断片のinclude!と、それからWCET APIの_with_cost_modelヴァリアントへの呼び出しです。
#![allow(unused)]
fn main() {
include!(concat!(env!("CARGO_MANIFEST_DIR"),
"/keleusma-bench/measured_cost_models/aarch64_apple_darwin.rs"));
use keleusma::verify::wcet_stream_iteration_with_cost_model;
let cycles = wcet_stream_iteration_with_cost_model(chunk, &MEASURED_COST_MODEL)?;
}
クックブックのセクション測定コストモデルでのキャリブレートされたWCETがレシピのウォークスルーです。examples/measured_wcet.rsが最小の動作例です。keleusma-bench/measured_cost_models/README.mdが事前に生成された断片と新しいターゲット用のキャプチャワークフローをカタログしています。
アリーナのサイズ
アリーナはオペランドスタックを底に、動的文字列をトップに保持します。Stream-to-Reset反復中の合計バイトはWCMU解析により境界づけられています。ホストには3つの選択肢があります。
選択肢A. デフォルト容量を使う。 DEFAULT_ARENA_CAPACITYは64キロバイトです。ほとんどのスクリプトに十分です。
#![allow(unused)]
fn main() {
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
}
選択肢B. VM構築前にモジュールから容量を計算する。 関数auto_arena_capacity_forはモジュールを歩いて境界を返します。
#![allow(unused)]
fn main() {
let cap = keleusma::vm::auto_arena_capacity_for(&module, &[])?;
let arena = Arena::with_capacity(cap);
let vm = Vm::new(module, &arena)?;
}
空のスライス引数はネイティブごとのヒープ計上を表します。スクリプトがヒープ割り当てネイティブを呼ぶ場合は計上値を渡します。自動サイズパターンはexamples/wcmu_basic.rsを参照してください。
選択肢C. 静的バッファを提供する。 ヒープを持たない組み込みターゲットでは、アリーナを.bss内のホスト所有バッファから動かせます。
#![allow(unused)]
fn main() {
static mut ARENA_BUFFER: [u8; 16 * 1024] = [0; 16 * 1024];
let arena = unsafe {
Arena::from_static_buffer(core::ptr::addr_of_mut!(ARENA_BUFFER))
};
}
選んだ容量が解析されたWCMUより小さい場合、Vm::newはVmError::VerifyErrorを返します。エラーはコードが走る前に表面化されます。
エラー回復
callまたはresume中のエラーはErr(VmError)を返します。VMは自動的にリセットされません。揮発性の状態がオペランドスタックとアリーナに残っているかもしれません。2つの経路があります。
経路1. VMを捨てる。 ドロップして再構築します。それに対して新しいVMが構築されると、アリーナはリセットされます。
経路2. 回復して続ける。 Vm::reset_after_errorを呼んで、データセグメントを保持しながら揮発性状態をクリアします。
#![allow(unused)]
fn main() {
match vm.call(&[arg]) {
Ok(state) => handle_state(state),
Err(VmError::TypeError(msg)) => {
eprintln!("script error: {}", msg);
vm.reset_after_error();
}
Err(other) => return Err(other.into()),
}
}
データセグメント(宣言されている場合)はエラーイベントを跨いで持続します。エラーを生き残らなければならない状態を累積する長時間動くストリームは、ローカル束縛ではなくデータセグメントに依存すべきです。
エラーヴァリアント
VmErrorは実行時のエラー条件を列挙しています。
| ヴァリアント | 条件 |
|---|---|
StackUnderflow | popで空のオペランドスタック |
TypeError(msg) | オペランド型が操作と一致しない |
DivisionByZero | ゼロでの整数除算または剰余 |
IndexOutOfBounds(idx, len) | 配列またはタプルのインデックスが範囲外 |
FieldNotFound(struct, field) | フィールドを宣言していない構造体でのフィールドアクセス |
NoMatch(value) | matchアームまたは多頭関数の頭が一致しなかった |
NativeError(msg) | ネイティブ関数がErrを返した |
InvalidBytecode(msg) | 実行時にバイトコードの形が予期外 |
Trap(msg) | Trap命令でスクリプトが停止 |
VerifyError(msg) | 構築時に構造または資源境界検証が失敗 |
LoadError(msg) | load_bytes中にワイヤフォーマットフレーミングが失敗 |
VerifyErrorはスクリプトコードが実行される前に発火する唯一のヴァリアントです。他のヴァリアントは実行中に発火します。VerifyErrorの解釈はWHY_REJECTED.mdを参照してください。
ホットコードスワッピング
VMはloopスクリプトのリセット境界で読み込まれたモジュールを置き換えるサポートをします。ホストはVmState::Resetを観測したあとにVm::replace_moduleを呼んで、Vm::callで新しいモジュールのエントリポイントを始めます。シグネチャは新しいモジュールと、長さが新しいモジュールの宣言されたスキーマと一致しなければならない初期データセグメントベクタを取ります。
#![allow(unused)]
fn main() {
match vm.resume(input)? {
VmState::Reset => {
let new_module = recompile_or_load_new_version()?;
// データセグメントを再初期化する。長さは新しいモジュールの
// 宣言された`data`ブロックサイズと一致しなければならない。
// 適切に値を保持または移行する。
let initial_data = vec![Value::Int(0); new_module_data_slot_count];
vm.replace_module(new_module, initial_data)?;
// スワップはコルーチン状態をクリアする。新しいモジュールを
// `resume`ではなくエントリポイントから動かす。
vm.call(&[Value::Int(next_seed)])?;
}
other => { /* ... */ }
}
}
対話型、すなわち yield される型と resume される型は、スワップをまたいで安定していなければなりません。データセグメントは前方に持ち越すことも(現在の値を渡す)、新しいスキーマに再初期化することも、ホストの移行コードによって置き換えることもできます。ネイティブ関数の登録はモジュールではなく仮想マシン上に存在し、スワップをまたいで持続します。ホットスワップの完全な仕様については EXECUTION_MODEL.md を、実行可能なエンドツーエンドの実演については examples/piano_roll.rs を参照してください。
署名付きモジュール
任意のsignatures Cargo機能がコンパイルされたバイトコードのEd25519署名を有効にします。ソーススクリプトはエントリ関数にsigned修飾子で要求を宣言します(signed fn main、signed yield main、signed loop main)。コンパイラはフレーミングヘッダにFLAG_REQUIRES_SIGNATUREを発行します。ランタイムは、その署名が読み込み前にホストが追加する信頼マトリクスに対して検証されない限り、そのようなモジュールの読み込みを拒否します。
ビルド時の署名
ホスト(またはビルドパイプライン)は32バイトのEd25519シードを取り、wire_format::module_to_signed_wire_bytesを使って署名付きバイトを生みます。CLIはこれをkeleusma compile --signing-key seed.bin -o out.binを通じて公開しています。keleusma keygen --seed seed.bin --public pub.binサブコマンドはOS RNGから新しい鍵ペアを生成します。シードファイルはUnixで0o600権限で書かれ、既存のファイルは上書きされません。
#![allow(unused)]
fn main() {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed_bytes);
let signed = keleusma::wire_format::module_to_signed_wire_bytes(&module, &signing_key)?;
std::fs::write("script.kel.bin", &signed)?;
}
検証と読み込み
ホストは署名付きモジュールをVm::load_signed_bytes(bytes, arena, &keys)を通じて読み込みます。鍵スライスは1つ以上の公開鍵を運びます。最初の一致する鍵がモジュールを許容します。空のスライスはすべての署名付きモジュールをLoadError::InvalidSignatureで却下します。マトリクスは構築されたVMにもコピーされるので、その後のVm::replace_module_from_bytes呼び出しは同じ鍵を継承します。
#![allow(unused)]
fn main() {
let pub_bytes: [u8; 32] = std::fs::read("pub.bin")?.try_into().unwrap();
let key = ed25519_dalek::VerifyingKey::from_bytes(&pub_bytes)?;
let mut vm = Vm::load_signed_bytes(&signed, &arena, &[key])?;
}
署名なしベースラインからブートストラップし、ホットスワップでだけ署名付きバイトコードを受け入れるホストは、通常通りVMを構築し、構築後に鍵を登録し、署名付きの更新をホットスワップします。
#![allow(unused)]
fn main() {
let mut vm = Vm::new(unsigned_baseline_module, &arena)?;
vm.register_verifying_key(signer_key);
// ... 後で、通信リンク上で署名付き更新を受け取ったあと ...
vm.replace_module_from_bytes(&update_bytes, initial_data)?;
}
Vm::load_bytesは署名付きモジュールを、正しいエントリポイントVm::load_signed_bytesを指す診断で拒否します。signatures機能なしでは、返されるヴァリアントはLoadError::SignaturesUnsupportedなので、運用者はビルドが検証できないこと(経路が間違っているだけでなく)を見ます。
署名メッセージ慣習は、署名ペイロードバイトとCRCトレーラバイトがゼロ化された、完全なフレーム付きバッファです。検証器は、暗号操作の前に自分のプライベートコピーで両方の領域をゼロ化することで、同じビューを再構築します。CRCトレーラは実署名を含むファイル全体をカバーするので、フレーミングレベルの改ざんはCRCだけで検出され、署名の変異はCRC修復後の暗号チェックで検出されます。
設計上の根拠については docs/decisions/RESOLVED.md の R42 を、ヘッダのレイアウトについては docs/spec/WIRE_FORMAT.md を参照してください。
トラストスキップ構築
検証コストをビルド時に払って毎回の読み込みでは払わないプログラムは、Vm::new_uncheckedを使って資源境界チェックを飛ばせます。構造検証は依然として走ります。
#![allow(unused)]
fn main() {
let vm = unsafe { Vm::new_unchecked(module, &arena) };
}
これは、安全検証器を通らないプログラムを許容するために用いた場合、意図的な誤用となります。意図された用途は、ホストがビルド時に一度検証済みのプリコンパイルされたバイトコードです。契約については LANGUAGE_DESIGN.md を参照してください。Vm::new_unchecked は署名付きモジュールフラグのチェックも省略します。呼び出し側は、いかなる署名検証もビルド時に実施済みであることを表明します。
相互参照
examples/wcmu_basic.rsが自動サイズパターンを最初から最後まで示します。examples/wcmu_attestation.rsがネイティブ境界宣言を示します。examples/wcmu_rejection.rsが検証器がサイズ不足のアリーナを却下する様子を示します。examples/string_ops.rsがホスト登録のテキスト連結とスライスネイティブを示します。examples/yield_error.rsがスクリプト定義のResult形状の列挙型でyieldを通じたエラー伝搬を示します。examples/method_call.rsがレシーバスタイル構文でのメソッドディスパッチを示します。examples/piano_roll.rsは機能ゲート付きのエンドツーエンドSDL3オーディオホストです。実時間オーディオ締切下での有界ステップ実行、Keleusmaメインスレッドと SDL3オーディオコールバックの間のスレッドセーフな受け渡し、データセグメントを通じたマルチヴォイス制御フロー、事前コンパイルされた楽曲一覧を跨ぐホットコードスワップ(piano_roll_<N>.kel、現在はpiano_roll_0.kel、piano_roll_1.kel、piano_roll_2.kel)を練習します。cargo run --release --example piano_roll --features sdl3-exampleで実行します。sで次の楽曲に切り替え、rで現在の楽曲を再開、数字で楽曲をインデックスで選択、Enterだけで終了します。長文マニュアルはPIANO_ROLL.mdで、楽曲の作成、ホストループを別のアプリケーションに持ち上げること、例を他の制御ループ領域でKeleusmaを組み込むアーキテクチャ参照として使うことを扱います。- LANGUAGE_DESIGN.md は言語モデルを説明します。
- EXECUTION_MODEL.md はランタイムモデルを説明します。
- WHY_REJECTED.mdは検証器の却下カテゴリを説明します。