Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

よくある質問

ナビゲーション: ガイド | ドキュメントルート

本ドキュメントは、早期採用者が遭遇した意外な点を集めたものです。その意図は、他のドキュメントがまだ想定していない疑問に答えることであり、網羅的であることではありません。

文字列

文字列は Keleusma の価値提案ではありません。 この言語の価値提案は、組み込みリアルタイムスクリプティングに向けた、確定的な最悪実行時間および最悪メモリ使用量の検証です。文字列を多用するスタンドアロンな作業には、豊富な標準ライブラリを備えた動的言語のほうが適したツールです。Python、Ruby、JavaScript、あるいは多数あるシェルおよびテキスト処理言語のいずれもが、Keleusma よりも人間工学的に、かつより多くの組み込みユーティリティとともに文字列を扱えます。Keleusma における文字列は、ホスト境界の型として、またデバッグの利便性として存在します。それらは最適化の対象とすべきサーフェスではありません。

V0.2.0 における Text サーフェス

V0.2.0 は、スクリプトレベルにおいて静的文字列サーフェスのみを同梱します。文字列リテラル("...")は、バイトコードの読み取り専用定数プール内の Value::StaticStr 定数へコンパイルされます。Text プリミティブ型は、静的文字列、ホストが生成する動的文字列(Value::KStr アリーナハンドル)、およびホスト境界を越える文字列型パラメータのサーフェス型を指します。バンドルされた to_stringconcatslicelength ユーティリティネイティブは、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::AddOpCost::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::newVm::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を使います。

その他の意外な振る舞い

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_exprparse_type_exprparse_pattern)に適用されます。

Keleusmaソースをプログラム的に生成するホスト(テンプレーティング、コード生成)は、式のネストを32レベルのずっと下に保つべきです。現実的な手書きのソースが制限に近づくことはまれです。境界は悪意のあるまたは偶然の入力がホストプロセスを殺すのを防ぐために存在します。

ローカルの束縛は不変です

letの束縛は再束縛も変更もできません。データセグメントがスクリプトに観測可能な唯一の可変状態の領域で、loop分類されたエントリポイントからのみアクセス可能です。原子的・全域なfn内のループ反復間の累積はしたがって、(a)データセグメントを使うloop mainスクリプトか、(b)ホスト側のフォールドネイティブ、のどちらか無しには不可能です。両方の書き換えの例は再帰的クロージャエントリの下のWHY_REJECTED.mdを参照してください。

クロージャは型検査段階で却下されます

V0.2.0 のフェーズ 4 はクロージャファミリーを廃止しました。Op::PushFuncOp::MakeClosureOp::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 mainsigned yield mainsigned 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.mdR42 を参照してください。

文の位置のif-elseは末尾のセミコロンを必要とする

パーサはセミコロンを自動挿入しません。文として使われるif-else式(別の文が続く場合)は、式がユニットに評価されても;が必要です。

if state.rem0 == 0 {
    /* ... */
} else {
    state.rem0 = state.rem0 - 1;
};   // <-- このセミコロンが必要
state.rem1 = state.rem1 - 1;

不透明型はネイティブ境界を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宣言を通じてそれを消費します。

loop-呼び出し-loopは語彙的生産性によって却下される

loopブロックを許容する生産性ルールは、純粋に語彙的な構造チェックによって強制されます。検証器は各loopの構文本体を歩き、1つの反復を通るすべての制御フロー経路が少なくとも1つのyieldを含むことを要求します。本体の唯一のyieldが呼ぶ関数の内側にあるloopブロックは却下されます。構造パスは呼び出しを追わないからです。

yield helper() -> Word { yield 1 }

loop main() -> Word {
    let v = helper();   // <-- 構造パスはyieldを見ない
    v
}

このプログラムはloop body has no yield on at least one pathで却下されます。ルールは意図的に保守的に間違えます。呼び出しを追う意味的チェックは、パラメータ依存ディスパッチやトレイトメソッド解決に対して健全でなく、相互に再帰的なコールグラフも扱わなければなりません。語彙的チェックは健全で速く、説明しやすい代わりに、yieldloop本体のトップレベルに置くことを強制します。

推奨されるパターンはyieldloop本体のトップに置いて、その周りでヘルパーを呼ぶことです。

yield helper() -> Word { yield 1 }

loop main() -> Word {
    let v = yield helper();   // 直接のyieldがルールを満たす
    v
}

同じ制約はloop本体内のif/elsematch分岐にも適用されます。すべての抜け落ち経路は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_typesTypeTag::Wordを運ぶので、Vm::call(&[Value::Float(1.5)])はAPI境界で却下されます。推論がパラメータを解決しない場合(制約なし)、チャンクはTypeTag::Compositeを記録し、ランタイムは任意の値を受け入れます。
  • 重複した関数頭は、エントリポイントであろうとなかろうと却下されます。 同じ名前を共有する2つの関数定義で、パラメータシグネチャが多頭パターンマッチング(同じ形、ガードなし)として明確に区別できないものは、以前は最初を残して残りを静かに捨てていました。コンパイラは今、2番目の定義でfunction head is dead codeを報告します。ルールはすべてのカテゴリ(fnyieldloop)、そしてエントリポイントだけでなくすべての関数に適用されます。
  • 多頭エントリポイントはfnyieldloopに対して受け入れられます。 3つすべての関数カテゴリが、パターンマッチングされたエントリポイントを許容します。多頭loop main(...)Streamブロックは、各マッチした頭の本体がOp::PopOp::Breakで終わるディスパッチをOp::Loop/Op::EndLoopで包んだものの周りの単一のOp::Streamと単一のOp::Resetエンベロープにコンパイルされるので、構造検証器のStream不変条件は成り立ちます。
  • エントリポイントのないモジュールは今VmError::VerifyErrorです。 fn mainyield mainloop mainを省くソースからコンパイルされたモジュールは、以前は最初のVm::callVmError::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ヴァリアントを返します。
  • 構造検証の却下は今ソーススパンを運びます。 CallIndirectMakeRecursiveClosureのコンパイルパイプライン却下は、以前Span::default()を付けていて、これは間違ったソース位置を隠していました。各却下は今、起源となる関数またはクロージャ宣言を指すので、エディタは構造に下線を引けます。

さらに見るべき場所

  • 完全な言語リファレンスは GRAMMAR.md です(説明的なものです。規範的なリファレンスは src/parser.rs のパーサーです)。
  • 検証器の却下カタログはWHY_REJECTED.md
  • 組み込みAPIサーフェスはEMBEDDING.md
  • 保守的検証の立場は LANGUAGE_DESIGN.md にあります。

ある振る舞いが意図されているのかバグなのか疑わしいとき、パーサ、型検査器、検証器が権威で、ドキュメントは記述的です。