Skip to content

Evaluator Internals

Sema's evaluator is a bytecode VM. Every entry point — the CLI, the REPL, the embedding API, eval, import/load, macros, and async/await — compiles to bytecode and runs on the VM.

For the architecture of the evaluator, see Bytecode VM.

The Evaluation Pipeline

All Sema code follows one path from source text to a result:

Source text
  → Reader        (tokenize + parse → Value AST)
  → Macro expand  (expand macros)
  → Lower         (Value AST → CoreExpr IR)
  → Optimize      (constant folding + simplification on CoreExpr)
  → Resolve       (CoreExpr → ResolvedExpr with slot/upvalue/global analysis)
  → Compile       (ResolvedExpr → bytecode Chunks)
  → VM execution  (stack-based dispatch loop)

Each phase is documented in Bytecode VM. Variables are resolved to direct slot/upvalue/global indices at compile time, closures use the Lua-style open-upvalue model, and tail calls reuse the current frame for tail-call optimization without growing the native Rust stack.

Environment Model

Sema uses a linked-list scope chain, where each scope is a hashbrown::HashMap keyed by Spur:

rust
// crates/sema-core/src/value.rs
pub struct Env {
    pub bindings: Rc<RefCell<SpurMap<Spur, Value>>>,
    pub parent: Option<Rc<Env>>,
    pub version: Cell<u64>,
}

Rc<RefCell<...>> makes each scope mutable and reference-counted. SpurMap is an alias for hashbrown::HashMap — keys are interned Spur handles (u32), so hashing is cheap integer hashing rather than string hashing. The version counter is bumped on every mutation; the VM's per-instruction inline caches use it to invalidate stale global lookups.

Operations

MethodBehavior
get(spur)Walk the parent chain, return first match
set(spur, val)Insert into the current (innermost) scope
set_existing(spur, val)Walk the chain, update where found (for set!)
take(spur)Remove from current scope only (for COW optimization)
take_anywhere(spur)Remove from any scope in the chain
update(spur, val)Overwrite an existing binding in the current scope (for hot loops)

The take method is critical for the copy-on-write map optimization described in the Performance page — by removing a value from the environment before passing it to a function, the Rc reference count drops to 1, enabling in-place mutation.

Literature: This is the standard lexical environment model described in Lisp in Small Pieces (Queinnec, 1996, Chapter 6) — a chain of frames linked by static (lexical) pointers. The alternative for lexical scoping — flat closures that copy all free variables into each closure — is faster for lookup but uses more memory when closures share large environments. Sema uses the chained model because closures are pervasive and lookup cost is dominated by the Spur integer comparison, not chain traversal.

Further Reading