第34章. ホスト側のコルーチンプロトコル
この章の目的
この章を読み終える頃には、ホストからyieldやloopスクリプトを動かせるようになり、実行時エラーから回復できるようになります。この章を読み終える頃には、ホストからyieldやloopスクリプトを動かせるようになり、実行時エラーから回復できるようになります。
call, resume, VmState
ホストはVMに対して2つの入口を持ちます。call(&[Value])が実行を始め、resume(Value)がyieldのあとに続けます。両方ともResult<VmState, VmError>を返し、VmStateは3つのヴァリアントを持ちます。ホストはVMに対して2つの入口を持ちます。call(&[Value])が実行を始め、resume(Value)がyieldのあとに続けます。両方ともResult<VmState, VmError>を返し、VmStateは3つのヴァリアントを持ちます。
#![allow(unused)]
fn main() {
pub enum VmState {
Finished(Value),
Yielded(Value),
Reset,
}
}
これらは第15章の3つの関数カテゴリに対応します。
- 原子的な
fnスクリプトは最後まで走ります —callがFinished(value)を返します。原子的なfnスクリプトは最後まで走ります —callがFinished(value)を返します。 yieldスクリプトは値を渡します —callがYielded(value)を返し、ホストはresume(input)で続けます。yieldスクリプトは値を渡します —callがYielded(value)を返し、ホストはresume(input)で続けます。loopスクリプトはサイクルごとにyieldし、本体の終わりでリセットします。callがYieldedを返し、resumeが次のyieldまで動かし、本体が1周完了すると次の呼び出しがResetを返します。loopスクリプトはサイクルごとにyieldし、本体の終わりでリセットします。callがYieldedを返し、resumeが次のyieldまで動かし、本体が1周完了すると次の呼び出しがResetを返します。
ドライブループ
ホストはVmStateに対するmatchでyieldするスクリプトを動かします。
#![allow(unused)]
fn main() {
let mut state = vm.call(&[Value::Int(seed)])?;
loop {
match state {
VmState::Yielded(out) => {
let reply = host_response(&out);
state = vm.resume(reply)?;
}
VmState::Reset => {
state = vm.resume(Value::Int(next_input))?;
}
VmState::Finished(value) => {
handle_result(value);
break;
}
}
}
}
ピアノロールのティックループはこのパターンです。16分音符ティックごとに1回、現在のティック番号でresumeを呼び、スクリプトが1サイクル走ってyieldし、ホストは次のティック境界まで眠ります。スクリプトの本体が1サイクル完了すると、状態はResetになります。これがホットスワップが起こりうる境界、第37章の話題です。ピアノロールのティックループはこのパターンです。16分音符ティックごとに1回、現在のティック番号でresumeを呼び、スクリプトが1サイクル走ってyieldし、ホストは次のティック境界まで眠ります。スクリプトの本体が1サイクル完了すると、状態はResetになります。これがホットスワップが起こりうる境界、第37章の話題です。
対話の型
resumeに渡す値とYieldedが運ぶ値は、第16章で導入したスクリプトの「対話」の両半分です。ホストとスクリプトはこれら2つの型に同意していなければなりません。同意はRustコンパイラには確認されません。どちらも実行時のValue列挙型として運ばれるからです。スクリプトが期待する型の再開値を渡すのは、ホスト作者の責任です。resumeに渡す値とYieldedが運ぶ値は、第16章で導入したスクリプトの「対話」の両半分です。ホストとスクリプトはこれら2つの型に同意していなければなりません。同意はRustコンパイラには確認されません。どちらも実行時のValue列挙型として運ばれるからです。スクリプトが期待する型の再開値を渡すのは、ホスト作者の責任です。
エラーからの回復
callやresumeからの実行時エラーはErr(VmError)を返します。VMは中間状態に残されます。ホストには2つの選択肢があります。callやresumeからの実行時エラーはErr(VmError)を返します。VMは中間状態に残されます。ホストには2つの選択肢があります。
- VMを捨てて作り直す。アリーナに対して新しいVMを構築すると、アリーナがリセットされます。VMを捨てて作り直す。アリーナに対して新しいVMを構築すると、アリーナがリセットされます。
vm.reset_after_error()を呼ぶ。これがオペランドスタック、コールフレーム、アリーナをクリアしつつ、データセグメントは保持します。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、TypeError、DivisionByZero、IndexOutOfBounds、FieldNotFound、NoMatch、NativeError、InvalidBytecode、Trap、VerifyError、LoadError。このうち、VerifyErrorとLoadErrorは構築のとき、スクリプトコードが走る前に発生します。それ以外は実行中に発生します。VmErrorは実行時の状態を列挙しています — StackUnderflow、TypeError、DivisionByZero、IndexOutOfBounds、FieldNotFound、NoMatch、NativeError、InvalidBytecode、Trap、VerifyError、LoadError。このうち、VerifyErrorとLoadErrorは構築のとき、スクリプトコードが走る前に発生します。それ以外は実行中に発生します。
この章のまとめ
callがVMを始動し、resumeがyieldのあとに続けます。VmStateはFinished、Yielded、Resetで、3つの関数カテゴリに対応します。VmStateはFinished、Yielded、Resetで、3つの関数カテゴリに対応します。- ホストは
yieldスクリプトをmatch-then-resumeループで動かします。 - 対話の型を守るのはホストの責任です。
reset_after_errorはデータセグメントを保持しながらVMを回復します。
次の章ではアリーナの大きさを決めます。