Chapter 34. The Coroutine Protocol from the Host Side
Goal
By the end of this chapter you will be able to drive a yield or loop
script from the host, and recover from a runtime error.
call, resume, and VmState
The host has two entry points into a VM. call(&[Value]) starts
execution. resume(Value) continues it after a yield. Both return
Result<VmState, VmError>, where VmState has three variants:
#![allow(unused)]
fn main() {
pub enum VmState {
Finished(Value),
Yielded(Value),
Reset,
}
}
The variants correspond to the three function categories of Chapter 15.
- An atomic
fnscript runs to completion:callreturnsFinished(value). - A
yieldscript hands a value out:callreturnsYielded(value), and the host callsresume(input)to continue. - A
loopscript yields every cycle and resets at the end of its body.callreturnsYielded,resumedrives the next yield, and once a body completes the next call returnsReset.
The drive loop
A host drives a yielding script with a match on VmState:
#![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;
}
}
}
}
The piano roll’s tick loop is this pattern. Once per sixteenth-note tick
it calls resume with the current tick number, the script runs one
cycle and yields, and the host sleeps until the next tick boundary. When
the script’s body completes a cycle, the state is Reset, which is the
boundary where a hot swap may happen, the subject of Chapter 37.
The dialogue type
The value passed to resume and the value carried by Yielded are the
two halves of the script’s dialogue, introduced in Chapter 16. The host
and the script must agree on these two types. The agreement is not
checked by the Rust compiler, because both are carried as the runtime
Value enum; it is the host author’s responsibility to supply resume
values of the type the script expects.
Error recovery
A runtime error from call or resume returns Err(VmError). The VM is
left in an intermediate state. The host has two choices.
- Discard the VM and reconstruct it. Constructing a new VM against the arena resets the arena.
- Call
vm.reset_after_error(), which clears the operand stack, the call frames, and the arena, while preserving the data segment.
#![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 enumerates the runtime conditions: StackUnderflow,
TypeError, DivisionByZero, IndexOutOfBounds, FieldNotFound,
NoMatch, NativeError, InvalidBytecode, Trap, VerifyError, and
LoadError. Of these, VerifyError and LoadError fire at construction,
before any script code runs; the rest fire during execution.
What you now know
callstarts a VM,resumecontinues it after a yield.VmStateisFinished,Yielded, orReset, matching the three function categories.- A host drives a yielding script with a match-and-resume loop.
- The dialogue types are the host’s responsibility to honor.
reset_after_errorrecovers a VM while preserving the data segment.
The next chapter sizes the arena.