ESC
Type to search...
S
Soli Docs

Interpreter — tree-walking engine

Used by --dev, soli test, the REPL, and as a fallback when the VM refuses a handler.

The type you actually hold is executor::Interpreter, re-exported as interpreter::Interpreter.

src/interpreter/
  value.rs           # Value, Class, Instance, Function, NativeFunction
  environment.rs     # lexical scopes
  executor/          # interpret() + evaluate()
    statements.rs
    expressions.rs
    operators.rs
    calls/           # function, method, native, cascade
    access/          # member, index, qualified
    objects/         # arrays, hashes, classes
  builtins/          # File, HTTP, Model, session, …
  hidden_class.rs    # shape-based instance layouts
  inline_cache.rs    # property/method IC

Value — the runtime

Every Soli value is this enum (src/interpreter/value.rs). The VM uses the same Value.

VariantMeaningSharing
Int(i64)Integercopy
Float(f64)Floatcopy
Decimal(DecimalValue)Exact decimalclone of rust_decimal
String(SoliStr)String (EcoString)cheap clone
Symbol(SoliStr):namecheap clone
Bool / Nullcopy
Array(Rc<RefCell<Vec<Value>>>)Arrayshared, mutable
Hash(Rc<RefCell<HashPairs>>)Ordered hashshared, mutable
Function(Rc<Function>)Soli closure (AST)
NativeFunctionRust fn(&[Value]) -> Result<Value, String>
Class(Rc<Class>)Class object
Instance(Rc<RefCell<Instance>>)Object
DateTime(i64)ns since epoch, no allocation
FutureHTTP etc., auto-resolvesArc<Mutex<…>>
QueryBuilderUser.where… chain
VmClosureBytecode closure living in a Value
Deferredgrouped() batched read
Image / ImagePlanImage pipeline
MethodBound method arr.map
Supersuper.foo
Breakpoint / ContinueControl-flow sentinels

Truthiness: only false and null are falsy. 0 and "" are truthy. Don’t write Rust if value without going through the engine’s truth helper.

Hashes use HashKey (int, string, symbol, bool, null, decimal) — not arbitrary Values as keys.

Class / Instance

  • Class holds methods, class methods, superclass, field names, ORM metadata attached by builtins.
  • Instance holds a class pointer + field storage (often a hidden class / shape for fast slots).

NativeFunction

pub type NativeFn = Rc<dyn Fn(&[Value]) -> Result<Value, String>>;

Builtins register these into the global environment. They receive already-evaluated arguments. Returning Err(String) becomes a Soli exception.


Environment

Lexical scope chain (Rc<RefCell<Environment>>).

MethodRole
new() / with_builtins_capacity()Global / large global
with_enclosing(parent)Nested block / call frame
define(name, value)let / first assignment
define_constconst
get / get_localRead, walk enclosing
assignname = value if already bound
assign_or_defineBare assignment (idiomatic Soli)
is_constReject mutation of const
reset_for_call / reset_for_reuseRecycle frames (serve hot path)

Workers reuse environments to avoid allocating a HashMap per request.


Interpreter

impl Interpreter {
    pub fn new() -> Self                 // full builtins (scripts, tests)
    pub fn new_for_serve() -> Self       // no test DSL
    pub fn new_for_migrations() -> Self  // SQL DDL helpers
    pub fn with_environment(env) -> Self
    pub fn interpret(&mut self, program: &Program) -> RuntimeResult<()>
    pub fn global_env(&self) -> &Rc<RefCell<Environment>>
    pub fn get_stack_trace(&self) -> Vec<String>
    // coverage hooks…
}

interpret walks program.statements. Expressions go through evaluate in executor/expressions.rs, which matches ExprKind.

Call path (executor/calls/)

FileHandles
function.rsSoli Function calls
native / builtin dispatchNativeFunction
cascade.rsRails-style obj.foo.bar where missing methods might cascade
method lookupinstance + class + mixins

If a call looks “magic” (User.find_by_email), it is either a builtin on Class or method_missing.


Builtins (src/interpreter/builtins/)

This directory is the standard library and most of the framework.

AreaExamples
Collectionscollections/array.rs, hash.rs
HTTPhttp_class.rs (SSRF-hardened)
ORMmodel/ (huge — CRUD, query, callbacks, associations)
Webrouter.rs, controller/, session.rs, cookie_jar.rs, permit.rs
Mail / jobsmailer.rs, jobs.rs
Filesfile.rs (jailed), Trusted in system.rs / file
Securitysecurity_headers.rs, rate_limit.rs

How a builtin is registered: a register_* function inserts NativeFunctions into the global env or onto a Class. Model methods are registered in model/core.rs (register_model_class) — that function is thousands of lines on purpose; split only when a change is unreviewable.

When adding a method on Array/Hash/String, also add the VM twin (src/vm/vm_*_methods.rs) or production will miss it.


Hidden classes and inline caches

hidden_class.rs + inline_cache.rs make obj.field and obj.method() faster when many instances share a shape (same field insertion order). You rarely touch these unless you change instance layout.


Evaluating vs executing

  • Statements (interpret / execute_stmt) — if, while, return, declarations. Return RuntimeResult<()>.
  • Expressions (evaluate) — produce a Value.

Assignment is an expression (ExprKind::Assign) and a statement (expression statement). Bare name = 1 goes through assign_or_define.