Chapter 32. Constructing a VM and Running a Module
Goal
By the end of this chapter you will understand each phase of VM construction and the lifetime relationship between the VM and the arena.
The four phases
Constructing a VM from source is four phases, each producing a distinct type.
#![allow(unused)]
fn main() {
let tokens = tokenize(SOURCE)?; // Vec<Token>
let program = parse(&tokens)?; // Program
let module = compile(&program)?; // Module
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena)?;
}
tokenizeturns source text into aVec<Token>.parseturns the tokens into aProgram, the syntax tree.compileturns the program into aModule, the bytecode object.Vm::newconsumes the module, borrows the arena, runs structural verification and resource-bounds verification, and returns a VM ready to call.
Each phase returns a Result. A failure in any phase is a typed error:
LexError, ParseError, CompileError, or, from Vm::new,
VmError::VerifyError.
Verification happens at construction
Vm::new is where the guarantees of Part V are enforced. It runs the
structural verifier and the resource-bounds verifier before returning. A
program the verifier rejects never yields a VM; Vm::new returns
Err(VmError::VerifyError(message)), and the message is the one
documented in WHY_REJECTED.md. No script code has run at that point.
The host learns a program is unacceptable at construction, not partway
through execution.
The arena and its lifetime
The arena is the bounded working memory the VM uses for its operand stack and for dynamic strings. The host creates it and the VM borrows it.
The borrow is enforced by the Rust borrow checker. Vm carries an
'arena lifetime parameter, and the arena must outlive the VM. In
practice this means the arena is declared before the VM and dropped
after it, which ordinary block scoping gives for free:
#![allow(unused)]
fn main() {
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena)?;
// vm is used here; arena outlives it
}
Chapter 35 covers how to choose the arena capacity. For now,
DEFAULT_ARENA_CAPACITY, sixty-four kilobytes, serves.
Running an atomic module
Once constructed, an atomic fn main module is run with call:
#![allow(unused)]
fn main() {
match vm.call(&[])? {
VmState::Finished(value) => { /* use value */ }
other => panic!("expected Finished, got {:?}", other),
}
}
call takes a slice of arguments. An fn main that takes no parameters
is called with &[]. The atomic module runs to completion and call
returns VmState::Finished(value), carrying the return value.
A yield or loop module does not finish on the first call. It
returns VmState::Yielded instead, and driving it requires the resume
protocol. Chapter 34 covers that. Native functions, which most real
scripts need, come first, in the next chapter.
What you now know
- VM construction is four phases:
tokenize,parse,compile,Vm::new. Vm::newruns verification; a rejected program fails here, before any code runs.- The VM borrows the arena, and the arena must outlive the VM.
call(&[])runs an atomic module and returnsVmState::Finished.
The next chapter registers the host functions a script calls.