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

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)?;
}
  • tokenize turns source text into a Vec<Token>.
  • parse turns the tokens into a Program, the syntax tree.
  • compile turns the program into a Module, the bytecode object.
  • Vm::new consumes 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::new runs 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 returns VmState::Finished.

The next chapter registers the host functions a script calls.