Runtime & Memory
Execution model
Section titled “Execution model”Writ is a bytecode VM — scripts are compiled to bytecode at load time and executed on a lightweight VM embedded in the Rust host.
Pipeline
Section titled “Pipeline”Source → Lexer → Parser → AST → Type Checker → Bytecode → VMKey properties
Section titled “Key properties”- Scripts loaded and executed at runtime — no ahead-of-time compilation required
- Static typing means all type checking happens before bytecode is emitted — no runtime type checks
- Near-zero marshalling cost — script types map directly to Rust types in memory
- VM starts with no external access — host explicitly registers what scripts can use
Memory management
Section titled “Memory management”Three-tier model — no garbage collector, no GC pauses.
Stack allocation
Section titled “Stack allocation”- Temporaries and local variables
- Automatic, zero overhead
- Freed when function returns
Reference counting
Section titled “Reference counting”- Heap-allocated objects — collections, script-created instances
- Freed when no more references exist
- Predictable, no pause spikes
- Circular references handled via weak references
Borrow safety
Section titled “Borrow safety”The VM guarantees that no script operation can cause a borrow panic internally. When the same object appears as both a method receiver and an argument (e.g., q.dot(q)) or when two arguments to a native function are the same object (e.g., swap(player, player)), the VM automatically detects the aliasing via pointer comparison and clones the conflicting value before dispatching. Detection is zero-cost in the common (non-aliasing) case. All internal borrow sites use fallible borrows so any missed case produces a RuntimeError instead of a process crash. Neither script authors nor host developers need to handle this — it is fully automatic.
Host ownership
Section titled “Host ownership”- Entity-bound script objects owned by Rust host
- Rust’s ownership system manages lifetime
- When entity is destroyed, script goes with it
- Zero VM overhead for host-owned objects
Type mapping
Section titled “Type mapping”All primitive types map directly to Rust equivalents with identical memory layout — no conversion cost at the boundary.
| Script type | Rust type |
|---|---|
int | i32 / i64 |
float | f32 / f64 |
bool | bool |
string | String |
Array<T> | Vec<T> |
Dictionary<K, V> | HashMap<K, V> |
Optional<T> | Option<T> |
Result<T> | Result<T, String> |
User-defined types compile to first-class Rust types, not VM-managed wrappers.
Inheritance compilation
Section titled “Inheritance compilation”Single inheritance via extends compiles to composition with Deref in Rust. The compiler generates this automatically — neither the scripter nor the host developer sees the implementation.
// Generated from: class Player extends Entity { pub health: float }pub struct Player { base: Entity, pub health: f32,}
impl std::ops::Deref for Player { type Target = Entity; fn deref(&self) -> &Entity { &self.base }}
impl std::ops::DerefMut for Player { fn deref_mut(&mut self) -> &mut Entity { &mut self.base }}Traits
Section titled “Traits”Script traits compile to Rust traits. Default implementations are preserved.
Coroutine scheduler
Section titled “Coroutine scheduler”If scripts use coroutines (yield, start), register a tick source so the VM knows how to measure time. Coroutines then advance automatically whenever you call into the VM.
Tick source (recommended)
Section titled “Tick source (recommended)”Register once during setup:
// Game engine — use your engine's delta timevm.set_tick_source(|| engine.delta_time());After this, every call() or load() auto-ticks coroutines before executing. No per-frame tick call needed.
The callback controls what “time” means to coroutines:
- Return
0.0to freeze coroutines (pause) - Return
delta * 0.5for slow motion - Return a fixed value for deterministic playback
For non-game applications, use wall-clock time:
vm.use_wall_clock();Manual tick (advanced)
Section titled “Manual tick (advanced)”For direct control over timing, skip set_tick_source and call tick() explicitly each frame:
vm.tick(delta_seconds).unwrap();Structured lifetime
Section titled “Structured lifetime”When a game object is destroyed, cancel its coroutines to avoid dangling execution:
fn on_destroy(vm: &mut Writ, entity_id: u64) { vm.cancel_coroutines_for_owner(entity_id);}Cancellation propagates to child coroutines automatically. See Coroutines for the script-side API.