よくある質問
本ドキュメントは、早期採用者が遭遇した意外な点を集めたものです。その意図は、他のドキュメントがまだ想定していない疑問に答えることであり、網羅的であることではありません。
文字列
文字列は Keleusma の価値提案ではありません。 この言語の価値提案は、組み込みリアルタイムスクリプティングに向けた、確定的な最悪実行時間および最悪メモリ使用量の検証です。文字列を多用するスタンドアロンな作業には、豊富な標準ライブラリを備えた動的言語のほうが適したツールです。Python、Ruby、JavaScript、あるいは多数あるシェルおよびテキスト処理言語のいずれもが、Keleusma よりも人間工学的に、かつより多くの組み込みユーティリティとともに文字列を扱えます。Keleusma における文字列は、ホスト境界の型として、またデバッグの利便性として存在します。それらは最適化の対象とすべきサーフェスではありません。
V0.2.0 における Text サーフェス
V0.2.0 は、スクリプトレベルにおいて静的文字列サーフェスのみを同梱します。文字列リテラル("...")は、バイトコードの読み取り専用定数プール内の Value::StaticStr 定数へコンパイルされます。Text プリミティブ型は、静的文字列、ホストが生成する動的文字列(Value::KStr アリーナハンドル)、およびホスト境界を越える文字列型パラメータのサーフェス型を指します。バンドルされた to_string、concat、slice、length ユーティリティネイティブは、V0.2.0 のフェーズ 3.5 におけるテキスト合成の削除に伴い、f 文字列補間とともに廃止されました。ランタイムは、Text の背後で静的(StaticStr)と動的(KStr)のバリアントを依然として区別します。yield をまたぐ禁止事項は、引き続き動的文字列に適用されます。
推奨されるパターンは、文字列処理を実行するネイティブ Rust 関数を登録し、それらをスクリプトへ公開することです。Rust の標準ライブラリは、書式設定、分割、正規表現、エンコーディング変換、および Unicode 操作を、スクリプト内部で構築する妥当なものよりもはるかに巧みに扱います。ホストは小さな Rust 関数を記述し、1 回の register_fn 呼び出しでそれを登録します。すると、スクリプトはネイティブ性能と Rust エコシステムへの完全なアクセスをもたらす単一の use 宣言を得ます。
#![allow(unused)]
fn main() {
// Rust host code.
use keleusma::{Arena, Value, vm::Vm};
let mut vm = Vm::new(module, &arena)?;
// Host-defined string helpers using Rust's standard library.
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, keleusma::VmError> {
s.split_whitespace()
.next()
.map(|w| w.to_string())
.ok_or_else(|| keleusma::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))
// }
}
ホストが文字列処理の語彙を所有し、スクリプトはuse宣言を通じてそれを消費します。完全なネイティブ登録サーフェスはEMBEDDING.mdを参照してください。
テキストが動く場所
- 静的文字列リテラル。
Value::StaticStrにコンパイルされ、バイトコードの定数プールに住みます。許容されるどこにでも流れられ、対話型のyield境界を跨ぐこともできます。 - アリーナ常駐の動的文字列。 ホスト登録されたネイティブ関数によって
KString::allocアリーナ境界を通じて生成されます。ホスト所有のアリーナを通じて解決されるValue::KStrハンドルとして保持され、次回のアリーナリセット時に無効になります。yield をまたぐ禁止の対象です。完全なテキスト型の規律については TYPE_SYSTEM.md を参照してください。
V0.1.xに存在したValue::DynStrグローバルヒープヴァリアントはV0.2.0で削除されました。すべての動的テキストはアリーナ常駐です。
静的文字列リテラルのエスケープ表
| エスケープ | 結果 |
|---|---|
\n | 改行(U+000A) |
\t | タブ(U+0009) |
\r | キャリッジリターン(U+000D) |
\\ | リテラルのバックスラッシュ |
\" | リテラルの二重引用符 |
\0 | ヌルバイト |
その他のすべての文字はエスケープなしでそのまま現れます。その他のバックスラッシュシーケンスはlexエラーです。V0.2.0はf-string補間と並んでf-string特有の\{と\}エスケープを引退しました。{と}は"..."内では普通の文字です。
WCMUのカバレッジ
指数的な文字列連結はWCMU境界を迂回する
次のようなプログラム
fn main() -> Text {
let s = "a";
let s = s + s;
let s = s + s;
/* 60回の倍化のあと */
s
}
はV0.1.xではコンパイルされて、Vm::newに受け入れられて、実行時にホストプロセスを使い果たしていたでしょう。V0.2.0はアロケータと静的解析の両方の次元に対処しています。
問題1: 文字列の+は以前はアリーナではなくグローバルアロケータから割り当てていました。 V0.2.0で解決されました。テキストオペランドに対するOp::Addは今、KString::allocを通じてアリーナのトップ領域に割り当てられたValue::KStrを生みます。割り当て失敗はホストプロセスを使い果たすのではなく、VmError::OutOfArenaとして表面化します。Value::DynStrヴァリアントは完全に削除されました。
問題2: WCMUパスは以前テキストサイズを静的に追跡していませんでした。 V0.2.0で解決されました。検証器は今、各チャンクに対してテキストサイズの抽象解釈パスを走らせ、スロット別のTextSize::{NotText, Known(u32), Unbounded}格子をバイトコードを通じて追跡し、テキストに対するOp::AddのOpCost::Dynamicコストをオペランド境界に対して評価し、結果をチャンクのWCMUヒープ合計に累積します。テキスト値を2倍に拡大するプログラムは累積で境界をu32::MAXに飽和させ、安全なコンストラクタはデフォルトのOverflowPolicy::Rejectのもとでこれを却下します。上の倍化文字列の例は、Streamブロックとして表現されたときVm::newで却下されるようになりました。
V0.2.0のテキストサイズ解析の制限。
- ループはテキスト値を
Unboundedに広げます。forまたはloop本体内のテキスト操作は保守的なu32::MAXの貢献を生みます。パスは線形で反復的ではないからです。ストリーム反復ごとに1回テキスト連結を行うプログラムは正確に扱われ、ループとテキストを混ぜるプログラムは保守的になります。 - 分岐はテキスト値を
Unboundedに広げます。if/elseの内側で条件的に書かれたテキスト値は精密な境界を失います。パスは条件の外で書かれたテキストを引き続き正しく境界づけます。 - ネイティブの戻り値は
Unboundedです。 登録されたネイティブ関数から返されたテキストは無界として追跡されます。その後のOp::Addはそれに対して飽和的に貢献します。ネイティブにより厳しい境界が必要なホストは、Vm::set_native_boundsを通じてネイティブごとのヒープ計上を提供します。 - 原子的・全域プログラム(Streamブロックなし)は反復ごとのWCMU境界の対象になりません。
fn main() -> Text { let s = "a"; let s = s + s; ... }はコンパイルされて走ります。リソース境界チェックがStreamチャンクにのみ適用されるからです。VmError::OutOfArenaを通じたアリーナ枯渇経路が、これらのプログラムのための優雅な失敗保証を提供します。
テキスト中心の作業に対する推奨。 重いテキスト作業はホスト計上で帯域外と扱ってください。境界づけられたやり方で作業を行うRustのネイティブ関数(上のセクション参照)を登録して、スクリプトに消費させます。ホスト側のテキストヘルパーは、無制限に割り当てるのではなく、大きな入力で安全に失敗するように実装できます。
アリーナ常駐の割り当てに対するホスト計上パターンの作業例はexamples/wcmu_attestation.rsにあります。
V0.2.0は小さすぎるアリーナで早めに失敗する
以前のリリースはArena::with_capacity(0)をVm::newに通し、最初のプッシュでhandle_alloc_errorを介してホストプロセスを中断していました。V0.2.0はこれを2層で変えました。
構築時の最小予約。 Vm::newとVm::new_uncheckedは、アリーナのボトム領域に小さな最小オペランドスタックとコールフレーム割り当てを事前予約します。アリーナが最小を保持できない場合、両方のコンストラクタは中断ではなく新しいVmError::OutOfArenaヴァリアントを返します。最小は保守的(4個のスタックスロットと1個のコールフレーム)です。約500バイト以上のアリーナは通ります。
実行時のプッシュパスがOutOfArenaを返す。 VMの実行ループ内のすべてのオペランドスタックとコールフレームのプッシュは今、Vec::try_reserveをまず呼んで割り当て失敗でVmError::OutOfArenaを返す内部のsp!とfp!マクロを通じて経路指定されます。実行時の使用がアリーナを超えるプログラムはもうホストプロセスを中断しません。ホストは型付きエラーを得て、どう扱うかを決められます(VMを捨てる、Vm::reset_after_errorで状態をリセット、より大きなアリーナで再試行、ユーザーにエラーを表面化)。
組み合わせは、実行のアリーナ常駐部分 — オペランドスタックとコールフレーム — が、優雅な失敗とともに完全にアリーナで境界づけられていることを意味します。
#![allow(unused)]
fn main() {
let arena = Arena::with_capacity(2 * 1024);
let mut vm = Vm::new(module, &arena)?;
// ...
match vm.call(&[]) {
Ok(state) => /* 状態を扱う */,
Err(VmError::OutOfArena(msg)) => {
eprintln!("arena exhausted: {}", msg);
// 回復または再構成
}
Err(other) => /* 他のエラーを扱う */,
}
}
アリーナのサイズを決めるには、ホストが渡す複合入力を数えるauto_arena_capacity_forを使うか、典型的な組み込みスクリプト向けに同梱の64キロバイトのDEFAULT_ARENA_CAPACITYを使います。複合入力がTextを含むホストは、auto_arena_capacity_with_input_textを使い、そのテキストの上界を与えます。
その他の意外な振る舞い
Vm::callは引数の数または型の間違いを先に却下する
RustからKeleusmaスクリプトを動かすホストは、vm.call(&[Value::Int(1), Value::Int(2)])で引数を渡します。ランタイムは、どのバイトコードも走る前に、引数の数をエントリチャンクのparam_countと照合し、各引数のランタイム型をパラメータの宣言されたTypeTagと照合します。引数が少なすぎる、多すぎる、または型が間違っている引数は、後の混乱した算術エラーではなく、呼び出しの境界でVmError::TypeErrorを生みます。
典型的な再現:
// スクリプト: fn main(a: Word, b: Word) -> Word { a + b }
vm.call(&[Value::Int(1)])
// -> VmError::TypeError("function `main` expected 2 arguments, got 1")
vm.call(&[Value::Int(1), Value::Float(2.5)])
// -> VmError::TypeError("function `main` parameter 1 expected Word, got Float")
不透明または複合値を本当に渡したいホストはTypeTag::Composite検証を受け取り、これは任意のValueを受け入れます。各チャンクのparam_typesフィールドが、ランタイムが何を受け入れるかについての真実のソースです。コンパイラは関数の宣言されたパラメータ型からそれを埋めます。
Vm::resumeはStreamブロックの再開値の型を検証する
loop main(x: T) -> Rスクリプトは型Rの値をyieldし、次の反復の型Tの値で再開します。ホストはvm.resume(value)を呼んで次の反復を駆動します。ランタイムはvalueをループのパラメータ型と照合してから、パラメータスロットに押し込みます。
#![allow(unused)]
fn main() {
// スクリプト: loop main(x: Word) -> Word { let z = yield x; z }
vm.call(&[Value::Int(11)]) // Ok(Yielded(Int(11)))
vm.resume(Value::Float(1.5)) // VmError::TypeError(
// "loop `main` resume expected Word, got Float")
}
yield式の型と再開値の型は言語設計により同じ(パラメータ型)です。だからチャンクレベルの1つのタグが対話の両方向をカバーします。
パーサは深くネストされた式を却下する
パーサは再帰下降ウォーカーです。深くネストされたパレン(約千以上)は以前ホストプロセスのスタックをオーバーフローしました。パーサは今、MAX_PARSE_DEPTH = 32レベルのネストで型付きParseErrorで中断します。制限は3つの再帰エントリポイント(parse_expr、parse_type_expr、parse_pattern)に適用されます。
Keleusmaソースをプログラム的に生成するホスト(テンプレーティング、コード生成)は、式のネストを32レベルのずっと下に保つべきです。現実的な手書きのソースが制限に近づくことはまれです。境界は悪意のあるまたは偶然の入力がホストプロセスを殺すのを防ぐために存在します。
ローカルの束縛は不変です
letの束縛は再束縛も変更もできません。データセグメントがスクリプトに観測可能な唯一の可変状態の領域で、loop分類されたエントリポイントからのみアクセス可能です。原子的・全域なfn内のループ反復間の累積はしたがって、(a)データセグメントを使うloop mainスクリプトか、(b)ホスト側のフォールドネイティブ、のどちらか無しには不可能です。両方の書き換えの例は再帰的クロージャエントリの下のWHY_REJECTED.mdを参照してください。
クロージャは型検査段階で却下されます
V0.2.0 のフェーズ 4 はクロージャファミリーを廃止しました。Op::PushFunc、Op::MakeClosure、Op::MakeRecursiveClosure、および Op::CallIndirect の各オペコードは削除され、Value::Func ランタイムバリアントは削除され、クロージャホイスティングコンパイラパスは削除されました。型検査器は現在、Expr::Closure を診断メッセージ closures are not supported; V0.2.0 admits only direct calls and trait dispatch under the conservative-verification stance. Rewrite as a top-level fn or trait method. とともに拒否します。第一級関数参照(例えば let f = my_func;)も同様にコンパイラによって拒否されます。これは LANGUAGE_DESIGN.md に記載された保守的検証の立場です。境界のない実行の有効な形式は、生産性規則によって強制されるトップレベルの loop ブロックです。
パイプライン演算子は括弧を必要とする
|>の右側は、関数が追加の引数を取らない場合でも、括弧付きの関数呼び出しでなければなりません。expr |> fはパースエラーで、expr |> f()が正しいです。
signed修飾子は何をするのか?
V0.2.0はエントリ関数宣言にsigned修飾子を導入しました(signed fn main、signed yield main、signed loop main)。これはフレーミングヘッダにFLAG_REQUIRES_SIGNATUREを設定するので、読み込み時のランタイムは、暗号学的署名が添付されてホストの信頼マトリクスに対して検証されない限り、モジュールを拒否します。
署名操作自体はコンパイラから独立したツールチェーンステップです。keleusma compile script.kel --signing-key seed.binはEd25519署名されたバイトコードファイルを生み、消費者は対応する公開鍵をVMに登録し(Vm::register_verifying_key)、Vm::load_signed_bytesを通じて読み込む、またはVm::replace_module_from_bytesを通じて署名付きの更新をホットスワップします。Vm::load_bytesは署名付きモジュールを、代替エントリポイントを名指しする診断で拒否します。
この機能はsignatures Cargo機能を必要とします。これはデフォルトでオフで、ed25519-dalekを引き込みます。機能なしのビルドは署名なしモジュールを通常通り受け入れ、署名付きモジュールをLoadError::SignaturesUnsupportedで拒否します。signedサーフェスキーワードは機能なしでも依然としてパースされるので、ソースファイルは移植可能なままです。
ユースケース: 組み込みターゲットへの複数当事者によるモジュール配信。署名者はスクリプトをコンパイルして署名します。署名者の公開鍵でフラッシュされたデバイスは、ロード前に検証します。署名済みバイトコードの配布 のクックブックレシピと RESOLVED.md の R42 を参照してください。
A trailing semicolon after a block-form statement is optional
The parser does not auto-insert semicolons, but a block-form if, if-else, match, loop, or for used as a statement does not need one. Both forms below parse, and they mean the same thing.
if state.rem0 == 0 {
/* ... */
} else {
state.rem0 = state.rem0 - 1;
} // <-- no semicolon needed
state.rem1 = state.rem1 - 1;
if state.rem0 == 0 {
/* ... */
} else {
state.rem0 = state.rem0 - 1;
}; // <-- also accepted
state.rem1 = state.rem1 - 1;
An earlier revision of this entry stated the semicolon was required. It was not, and had not been; the behaviour is pinned by an_if_else_before_another_statement_needs_no_semicolon in tests/block_form_statements.rs.
The semicolon does still matter in one position. Omitting it makes a block-form expression the block’s tail expression, so its value is the block’s value; writing it makes the construct a statement and the block’s value comes from whatever follows.
Ordinary expression statements, such as a call made for its side effect, do require a terminator.
不透明型はネイティブ境界をValue::Opaqueとして流れる
V0.2.0はkeleusma::opaqueモジュールのHostOpaqueトレイトを通じて、第一級の不透明型サポートを導入しました。ホストは公開したい値の周りのRustnewtypeに対してトレイトを実装します。ネイティブ関数はhost_arc(...)を通じて不透明値を生み、dyn HostOpaque::downcast_ref::<T>()を通じて型付き参照を抽出して消費します。スクリプトは関数シグネチャで型を名前で宣言し、型検査器は名前をType::Opaqueとして解決します。不透明値はArcを通じてホスト管理され、アリーナとは独立したライフタイムを持ち、yield境界を跨いで運ばれることができ、スクリプト側のWCMU境界に対してゼロを貢献します。EMBEDDING.mdの「Opaque Host Types」セクションとexamples/opaque_rust_string.rsの作業例を参照してください。
整数算術はターゲットのワード幅にラップする
KeleusmaのWordはターゲット記述子で宣言された幅の固定幅符号付き整数です。算術操作は結果をその幅にマスクし、各ステップで符号拡張シフトを使います。オーバーフローは型付きエラーを生みません。結果は宣言された幅が許容する剰余的な意味で静かにラップします。
fn main() -> Word {
let max = 9223372036854775807;
max + 1
}
// 64ビットターゲットでは-9223372036854775808を返します。
この選択は意図的です。言語が保証するワーストケース実行時間とワーストケースメモリ使用量の境界は、すべての算術操作が固定ステップ数を持つことに依存しています。トラップオーバーフローの意味論は、すべての操作のワーストケースコストを膨張させるか、静的解析が列挙しなければならない制御フローのエッジを導入するかのどちらかです。ラップ意味論は予測可能なステップ数と、解析が一様に推論できる閉じた結果ドメインを与えます。
オーバーフロー検出が必要なホストは、より広いRust整数に対して確認された操作を行ってVmError::NativeErrorを通じてエラーを表面化するネイティブを登録します。ホストが確認算術の語彙を所有し、スクリプトはuse宣言を通じてそれを消費します。
How the productivity rule treats a yield inside a called function
The productivity rule that admits loop blocks requires that every control-flow path through one iteration reaches a yield. The yield does not have to be written in the loop body itself. A call to a function that yields on every one of its own fall-through paths satisfies the rule, and the set of such functions is computed as a fixpoint over the call graph, so a chain of delegations is admitted as well.
This entry said the opposite until 2026-09-26. It stated that the check was purely lexical, that it did not chase calls, and that the program below was rejected. None of those is true of the current implementation, whose own diagnostic names delegation explicitly. The entry was wrong in the direction that costs a reader real work, because it advised restructuring code that needs no restructuring.
yield helper() -> Word { yield 1 }
loop main() -> Word {
let v = helper(); // helper yields on every path, so the rule is satisfied
v
}
What is still rejected is a body with a path that reaches neither a yield nor a call to a function that always yields. WHY_REJECTED.md shows that case and quotes its diagnostic.
Keeping the yield at the top of the loop body remains the clearest pattern, and is now a matter of readability rather than a requirement.
yield helper() -> Word { yield 1 }
loop main() -> Word {
let v = yield helper(); // 直接のyieldがルールを満たす
v
}
同じ制約はloop本体内のif/elseとmatch分岐にも適用されます。すべての抜け落ち経路はyieldを含むか、分岐はbreakで抜けなければなりません。
V0.2.0の境界診断
構築と呼び出しサーフェスはV0.2.0で厳しくなり、以前は静かか紛らわしかった複数のケースが、適切な境界で型付き診断を生むようになりました。
i64をオーバーフローする整数リテラルは今LexErrorです。 以前の振る舞いは99999999999999999999999999999のようなリテラルに対して静かにValue::Int(0)を生んでいました。lexerは今、リテラルのソーススパンとともにinteger literal does not fit in i64を報告します。- 型なしパラメータは文脈が解決するとき推論されます。
fn main(x) -> Word { x }と書くと、以前はパースされてxが型なしで登録され、後で混乱したエラーをつまずきました。型検査器は今、推論されたプリミティブ型をASTに書き戻します。fn main(x) -> Word { x }では戻り型制約がx: Wordを強制し、チャンクのparam_typesがTypeTag::Wordを運ぶので、Vm::call(&[Value::Float(1.5)])はAPI境界で却下されます。推論がパラメータを解決しない場合(制約なし)、チャンクはTypeTag::Compositeを記録し、ランタイムは任意の値を受け入れます。 - 重複した関数頭は、エントリポイントであろうとなかろうと却下されます。 同じ名前を共有する2つの関数定義で、パラメータシグネチャが多頭パターンマッチング(同じ形、ガードなし)として明確に区別できないものは、以前は最初を残して残りを静かに捨てていました。コンパイラは今、2番目の定義で
function head is dead codeを報告します。ルールはすべてのカテゴリ(fn、yield、loop)、そしてエントリポイントだけでなくすべての関数に適用されます。 - 多頭エントリポイントは
fn、yield、loopに対して受け入れられます。 3つすべての関数カテゴリが、パターンマッチングされたエントリポイントを許容します。多頭loop main(...)Streamブロックは、各マッチした頭の本体がOp::PopとOp::Breakで終わるディスパッチをOp::Loop/Op::EndLoopで包んだものの周りの単一のOp::Streamと単一のOp::Resetエンベロープにコンパイルされるので、構造検証器のStream不変条件は成り立ちます。 - エントリポイントのないモジュールは今
VmError::VerifyErrorです。fn main、yield main、loop mainを省くソースからコンパイルされたモジュールは、以前は最初のVm::callでVmError::InvalidBytecode("no entry point")として表面化していました。コンストラクタVm::new(とVm::new_unchecked)は今、モジュールをAPI境界でmodule has no entry pointで却下します。 - 時期尚早の
Vm::resumeは今VmError::NotSuspendedです。vm.call(args)の前にvm.resume(value)を呼ぶことは、以前はVmError::InvalidBytecode("cannot resume: VM not suspended")として表面化し、APIの誤用と壊れたバイトコードを混同していました。ランタイムは今、専用のVmError::NotSuspendedヴァリアントを返します。 - 構造検証の却下は今ソーススパンを運びます。
CallIndirectとMakeRecursiveClosureのコンパイルパイプライン却下は、以前Span::default()を付けていて、これは間違ったソース位置を隠していました。各却下は今、起源となる関数またはクロージャ宣言を指すので、エディタは構造に下線を引けます。
さらに見るべき場所
- 完全な言語リファレンスは GRAMMAR.md です(説明的なものです。規範的なリファレンスは
src/parser.rsのパーサーです)。 - 検証器の却下カタログはWHY_REJECTED.md。
- 組み込みAPIサーフェスはEMBEDDING.md。
- 保守的検証の立場は LANGUAGE_DESIGN.md にあります。
ある振る舞いが意図されているのかバグなのか疑わしいとき、パーサ、型検査器、検証器が権威で、ドキュメントは記述的です。