Evaluator Overview
The Ori evaluator is a tree-walking interpreter that executes typed ASTs. It handles expression evaluation, function calls, pattern execution, and module loading.
Architecture
The evaluator is split between two crates:
ori_eval- Core interpreter logic (reusable, no Salsa dependency)oric/src/eval/- High-level orchestration (module loading, prelude, Salsa integration)
Location
Core Interpreter (ori_eval)
compiler/ori_eval/src/
├── lib.rs # Module exports, re-exports from ori_patterns
├── environment.rs # Environment, Scope, LocalScope
├── errors.rs # EvalError factory functions
├── operators.rs # Binary operator dispatch
├── unary_operators.rs # Unary operator dispatch
├── methods.rs # Built-in method dispatch, EVAL_BUILTIN_METHODS constant
├── function_val.rs # Type conversions (int, float, str, byte)
├── user_methods.rs # UserMethodRegistry for user-defined methods
├── print_handler.rs # Print output capture (stdout/buffer handlers)
├── shared.rs # SharedRegistry, SharedMutableRegistry
├── stack.rs # Stack safety (stacker integration)
├── method_key.rs # MethodKey newtype
├── exec/ # Expression execution
│ ├── mod.rs # Module exports
│ ├── expr.rs # Expression evaluation
│ ├── call.rs # Function call evaluation
│ ├── control.rs # Control flow (if, for, loop)
│ └── pattern.rs # Pattern matching
└── interpreter/ # Core interpreter
├── mod.rs # Interpreter struct, main eval dispatch
├── builder.rs # InterpreterBuilder
├── scope_guard.rs # RAII scope management
├── function_call.rs # User function calls
├── function_seq.rs # run/try/match evaluation
├── method_dispatch.rs # Method resolution, iterator helpers
├── derived_methods.rs # Derived trait method evaluation
└── resolvers/ # Method resolution chain
├── mod.rs # MethodDispatcher, MethodResolver trait
├── user_registry.rs # UserRegistryResolver (user + derived)
├── collection.rs # CollectionMethodResolver (map, filter, fold)
└── builtin.rs # BuiltinMethodResolver
High-Level Evaluator (oric/src/eval/)
compiler/oric/src/eval/
├── mod.rs # Re-exports, value module alias
├── output.rs # EvalOutput, ModuleEvalResult
├── evaluator/ # Evaluator wrapper
│ ├── mod.rs # Evaluator struct (wraps Interpreter)
│ ├── builder.rs # EvaluatorBuilder
│ ├── module_loading.rs # load_module, load_prelude, method collection
│ └── tests.rs # Unit tests
└── module/ # Import resolution
├── mod.rs # Module exports
└── import.rs # Module import handling
Design Goals
- Correctness - Match language specification exactly
- Clear error messages - Track source locations, provide context
- Modularity - Separate concerns into focused modules
- Testability - Dependency injection for registries
Evaluation Flow
TypedModule { Module, ExprArena, expr_types }
│
│ create Evaluator
▼
Evaluator {
env: Environment, // Variables
pattern_registry: ..., // Pattern handlers
type_registry: ..., // User types
output: EvalOutput, // Captured output
}
│
│ find and call @main (or evaluate top-level)
▼
ModuleEvalResult {
value: Value, // Final result
output: EvalOutput, // Captured stdout/stderr
}
Core Components
Interpreter (core, ori_eval)
The core tree-walking interpreter, portable and reusable without Salsa:
pub struct Interpreter<'a> {
/// String interner for name lookup
pub interner: &'a StringInterner,
/// Expression arena
pub arena: &'a ExprArena,
/// Current environment
pub env: Environment,
/// Pattern registry for `function_exp` evaluation
pub registry: SharedRegistry<PatternRegistry>,
/// User-defined method registry for impl block methods
pub user_method_registry: SharedMutableRegistry<UserMethodRegistry>,
/// Cached method dispatcher for efficient method resolution
pub method_dispatcher: resolvers::MethodDispatcher,
/// Arena reference for imported functions
pub imported_arena: Option<SharedArena>,
/// Whether the prelude has been auto-loaded
pub prelude_loaded: bool,
/// Print handler for output capture
pub print_handler: SharedPrintHandler,
}
Evaluator (high-level, oric)
The high-level evaluator wraps Interpreter and adds module loading and Salsa integration:
pub struct Evaluator<'a> {
/// Core interpreter
pub interpreter: Interpreter<'a>,
/// Captured output (stdout/stderr)
pub output: EvalOutput,
/// Module cache for import resolution
module_cache: HashMap<PathBuf, ModuleEvalResult>,
}
Why SharedMutableRegistry?
The MethodDispatcher is constructed once in InterpreterBuilder with references to
the UserMethodRegistry. However, load_module() needs to register new methods
(from impl blocks, extends, and derives) after the Interpreter is created.
Using SharedMutableRegistry<T> (which wraps Arc<RwLock<T>>) allows:
- The cached
MethodDispatcherto see newly registered methods - Efficient read access during method resolution (no rebuilding)
- Thread-safe method registration during module loading
// In load_module():
self.interpreter.user_method_registry.write().merge(new_methods);
// In method resolution (via MethodDispatcher):
if let Some(method) = self.registry.read().lookup(type_name, method_name) { ... }
Evaluation Entry Point
impl Evaluator {
pub fn evaluate(&mut self, module: &Module) -> Result<Value, EvalError> {
// Register module-level items
self.register_functions(module)?;
self.register_types(module)?;
// Find and call @main
if let Some(main_fn) = module.find_function("main") {
self.call_function(main_fn, vec![])
} else {
// No main - evaluate top-level expression
self.eval_module_expression(module)
}
}
fn eval_expr(&mut self, id: ExprId) -> Result<Value, EvalError> {
let expr = self.arena.get(id);
match &expr.kind {
ExprKind::Literal(lit) => self.eval_literal(lit),
ExprKind::Ident(name) => self.eval_ident(*name),
ExprKind::Binary { left, op, right } => {
self.eval_binary(*left, *op, *right)
}
ExprKind::Call { func, args } => {
self.eval_call(*func, args)
}
ExprKind::If { cond, then, else_ } => {
self.eval_if(*cond, *then, *else_)
}
ExprKind::Pattern { name, args } => {
self.eval_pattern(*name, args)
}
// ... more cases
}
}
}
Key Features
Value System
Runtime values with Arc-based sharing:
pub enum Value {
Int(i64),
Float(f64),
Bool(bool),
String(Arc<String>),
List(Arc<Vec<Value>>),
Map(Arc<HashMap<Value, Value>>),
Function(FunctionValue),
Option(Option<Arc<Value>>),
Result(Result<Arc<Value>, Arc<Value>>),
Struct { name: Name, fields: Arc<HashMap<Name, Value>> },
Void,
}
Environment
Stack-based lexical scoping:
let x = 1 // Outer scope: x = 1
run(
let x = 2, // Inner scope: x = 2
x + outer_x, // Can't access outer x directly
)
// x = 1 again
RAII Scope Guards
The evaluator uses RAII-style scope guards for safe scope management:
// Execute within a new environment scope (auto-cleanup)
self.with_env_scope(|eval| {
eval.env.define(name, value, mutable);
eval.eval(body)
})
// Execute with pre-defined bindings
self.with_bindings(bindings, |eval| eval.eval(body))
// Execute with match bindings (immutable)
self.with_match_bindings(pattern_bindings, |eval| eval.eval(arm_body))
// Execute with a single binding
self.with_binding(name, value, mutable, |eval| eval.eval(body))
These guards guarantee cleanup even on early returns or errors.
Pattern Delegation
Patterns are evaluated via the registry:
fn eval_pattern(&mut self, name: Name, args: &[NamedArg]) -> Result<Value, EvalError> {
let pattern = self.pattern_registry.get(name)?;
let eval_args = self.eval_pattern_args(args)?;
pattern.evaluate(&eval_args, self)
}
Module Loading
Imports load and cache modules:
fn load_module(&mut self, path: &Path) -> Result<ModuleEvalResult, EvalError> {
if let Some(cached) = self.module_cache.get(path) {
return Ok(cached.clone());
}
let source = fs::read_to_string(path)?;
let result = compile_and_evaluate(&source)?;
self.module_cache.insert(path.to_path_buf(), result.clone());
Ok(result)
}
Cross-Crate Method Consistency
The evaluator exports an EVAL_BUILTIN_METHODS constant listing all (type_name, method_name) pairs it handles. The type checker exports a corresponding TYPECK_BUILTIN_METHODS constant. A consistency test in oric/src/eval/tests/methods_tests.rs verifies:
- Every eval method is a subset of typeck methods (no runtime-only methods that the type checker doesn’t know about)
- Both lists are sorted for maintainability
Known exceptions (methods in eval but not yet in typeck) are tracked in a KNOWN_EVAL_ONLY list.
Method Dispatch Architecture
The evaluator uses a Chain of Responsibility pattern for method resolution:
flowchart TB
A["receiver.method(args)"] --> B["MethodDispatcher.resolve()"]
B --> C["UserRegistryResolver"]
C -->|"User + derived methods"| D{"Found?"}
D -->|No| E["CollectionMethodResolver"]
E -->|"map, filter, fold"| F{"Found?"}
F -->|No| G["BuiltinMethodResolver"]
G -->|"len, push, etc."| H["Return method"]
D -->|Yes| H
F -->|Yes| H
The UserRegistryResolver is a unified resolver that checks both user-defined methods
(from impl blocks) and derived methods (from #[derive(...)]) in a single lookup.
Iterator Helpers
Collection methods share logic via internal iterator helpers:
// Shared iterator-based implementations
fn map_iterator(&mut self, iter: impl Iterator<Item=Value>, transform: &Value) -> EvalResult
fn filter_iterator(&mut self, iter: impl Iterator<Item=Value>, predicate: &Value) -> EvalResult
fn fold_iterator(&mut self, iter: impl Iterator<Item=Value>, acc: Value, op: &Value) -> EvalResult
fn find_in_iterator(&mut self, iter: impl Iterator<Item=Value>, predicate: &Value) -> EvalResult
fn any_in_iterator(&mut self, iter: impl Iterator<Item=Value>, predicate: &Value) -> EvalResult
fn all_in_iterator(&mut self, iter: impl Iterator<Item=Value>, predicate: &Value) -> EvalResult
// Used by both list and range methods:
fn eval_list_map(&mut self, items: &[Value], args: &[Value]) -> EvalResult {
self.map_iterator(items.iter().cloned(), &args[0])
}
fn eval_range_map(&mut self, range: &RangeValue, args: &[Value]) -> EvalResult {
self.map_iterator(range.iter().map(Value::Int), &args[0])
}
Type Name Resolution
The get_value_type_name() method uses the StringLookup trait for unified type name resolution:
pub(super) fn get_value_type_name(&self, value: &Value) -> String {
value.type_name_with_interner(self.interner).into_owned()
}
This handles struct type names (which require interner lookup) while delegating
to Value::type_name() for primitives and built-in types.
Builder Pattern
Both Interpreter and Evaluator use the builder pattern for construction.
InterpreterBuilder (core, ori_eval)
let interpreter = InterpreterBuilder::new(interner, arena)
.env(call_env) // Optional: custom environment
.imported_arena(shared_arena) // Optional: cross-module arena
.user_method_registry(registry) // Optional: user methods
.print_handler(handler) // Optional: output handler
.build();
EvaluatorBuilder (high-level, oric)
let evaluator = EvaluatorBuilder::new(interner, arena)
.env(call_env)
.imported_arena(shared_arena)
.user_method_registry(registry)
.build(); // Creates Evaluator wrapping Interpreter
Benefits:
- Single entry point for all configuration options
- Clearer intent—named methods describe what each option does
- Easier to add new configuration options without API changes
- Defaults work for common cases; override only what you need
Arena Threading Pattern
When evaluating functions or methods from different modules, the interpreter must
use the correct arena for expression lookups. The create_function_interpreter
helper ensures this:
/// Create a new interpreter for function/method evaluation with the correct arena.
///
/// This is critical for cross-module calls: functions from imported modules
/// carry their own SharedArena, and we must use that arena when evaluating
/// their body expressions.
pub(super) fn create_function_interpreter<'b>(
&self,
func_arena: &'b ExprArena,
call_env: Environment,
) -> Interpreter<'b>
where
'a: 'b,
{
let imported_arena = SharedArena::new(func_arena.clone());
InterpreterBuilder::new(self.interner, func_arena)
.env(call_env)
.imported_arena(imported_arena)
.user_method_registry(self.user_method_registry.clone())
.build()
}
This pattern appears in ori_eval/src/interpreter/:
function_call.rs- calling user functionsmethod_dispatch.rs- calling user methods
Related Documents
- Tree Walking - Execution strategy
- Environment - Variable scoping
- Value System - Runtime values
- Module Loading - Import resolution