| .. | ||
| test | ||
| builtin_loading.zig | ||
| BuiltinModules.zig | ||
| builtins.zig | ||
| comptime_evaluator.zig | ||
| crash_context.zig | ||
| interpreter.zig | ||
| mod.zig | ||
| README.md | ||
| render_helpers.zig | ||
| stack.zig | ||
| StackValue.zig | ||
| test_runner.zig | ||
Interpreter Overview
This directory contains Roc's interpreter. It is the implementation that powers the REPL, snapshot tooling, and the evaluation tests that exercise the type-carrying runtime. This document introduces the core pieces so a new contributor can navigate the code without prior context.
High-Level Architecture
-
src/eval/interpreter.zigexportsInterpreter. Each instance owns the runtime state needed to evaluate expressions:- A runtime
types.Storewhere compile-time Vars are translated and unified. - A runtime
layout.Storeplus an O(1)var_to_layout_slotcache that maps runtime Vars to layouts. - A translation cache from
(ModuleEnv pointer, compile-time Var)to runtime Var so we never duplicate work across expressions. - A polymorphic instantiation cache keyed by function id + runtime arg Vars to avoid repeatedly re-unifying hot polymorphic calls.
- A small
stack.Stackused for temporary values, a binding list that models lexical scopes, and helper state for closures and boolean tags.
- A runtime
-
src/eval/StackValue.zigdescribes how values live in memory during evaluation. EachStackValuepairs a layout with a pointer (if any) and knows how to copy, move, and decref itself using the runtime layout store. -
src/eval/render_helpers.zigrenders values using the same type information the interpreter carries. The interpreter delegates to these helpers for REPL output and tests.
Evaluation Flow
- Canonical inputs – Consumers (REPL, tests, snapshot tool) parse and
canonicalize Roc source, then hand a
ModuleEnvand canonical expression idx to the interpreter. - Initialization –
Interpreter.inittranslates the initial module types into the runtime store, ensures the slot cache is sized appropriately, and sets up the auxiliary state (stack, binding list, poly cache). - Minimal evaluation –
evaldrives evaluation by callingevalExprMinimal. The interpreter pattern-matches on canonical expression tags (records, tuples, pattern matches, binops, calls, etc.), evaluates children recursively, and produces aStackValueannotated with layout. - Type translation on demand – When an expression needs type information
(e.g. to render a value or create a layout),
translateTypeVarcopies the compile-time Var into the runtime store and caches the result. - Layouts on demand –
getRuntimeLayoutlooks up or computes the layout for a runtime Var using the slot cache. Layouts are stored in the runtime layout store so subsequent lookups are cheap. - Polymorphic calls – Before a function call,
prepareCallconsults the poly cache. The interpreter only re-runs the runtime unifier if it has not seen that combination of function id + argument Vars before. - Crash handling – Crash/expect expressions delegate to the host via
RocOps.crash. Hosts supply aCrashContext(seecrash_context.zig) to record messages; the interpreter keeps no internal crash state.
All RocOps interactions (alloc, dealloc, crash, expect) happen through the
RocOps pointer passed into eval. This keeps host integrations (REPL,
snapshot tool, CLI) consistent.
Rendering
renderValueRoc and renderValueRocWithType assemble human-readable strings
using the same type information the interpreter evaluated with. Rendering only
reads from StackValue and the runtime layout store, so callers should decref
the evaluated value after rendering.
Extending the Interpreter
- New expression forms – Add cases to
evalExprMinimal. Most cases follow a pattern: translate sub-expressions, obtain or build layouts, then use the helpers inStackValueto initialize the result. - New data shapes – Extend layout translation in
translateTypeVar/getRuntimeLayoutand teachStackValuehow to copy or decref the shape. - Rendering – Update
render_helpers.zigand ensure the interpreter calls the appropriate helper.
When making changes, run zig build test. Interpreter-specific coverage lives
in:
src/eval/test/interpreter_style_test.zig– End-to-end Roc-syntax tests that parse, canonicalize, evaluate, and render.src/eval/test/interpreter_polymorphism_test.zig– Scenarios that exercise the polymorphism cache and runtime unifier.src/repl/repl_test.zig– Integration-style tests that ensure the REPL uses the interpreter correctly.
Host Integrations
- REPL (
src/repl/Repl.zig) constructs a fresh interpreter per evaluation, feeds it a canonical expression, then renders values through the interpreter’s helpers. - Snapshot tool (
src/snapshot_tool/main.zig) uses the same interpreter to evaluate each snapshot input with optional tracing. - Interpreter shim (
src/interpreter_shim/main.zig) provides a C-callable entry point that deserializes aModuleEnv, constructs an interpreter, and returns rendered output.
Debugging
The interpreter supports a compile-time tracing flag that enables detailed evaluation output. To build with tracing enabled:
zig build -Dtrace-eval=true
This flag is automatically enabled in Debug builds (-Doptimize=Debug). When
enabled, the interpreter outputs detailed information about evaluation steps,
which is useful for debugging issues in the interpreter or understanding how
expressions are evaluated.
For snapshot testing with tracing, use the --trace-eval flag:
./zig-out/bin/snapshot --trace-eval path/to/snapshot.md
Note: --trace-eval only works with REPL-type snapshots (type=repl).
Refcount Tracing
For debugging memory management issues, use the -Dtrace-refcount flag:
zig build -Dtrace-refcount=true
When enabled, this outputs detailed refcount operations to stderr:
[REFCOUNT] DECREF str ptr=0x1234 len=5 cap=32
[REFCOUNT] DECREF list ptr=0x5678 len=3 elems_rc=1 unique=1
[REFCOUNT] INCREF str ptr=0x1234 len=5 cap=32
This is useful for:
- Debugging segfaults in list/string operations
- Verifying correct refcounting in new builtins
- Understanding memory lifecycle during evaluation
Unlike -Dtrace-eval, this flag defaults to false even in Debug builds due to
the volume of output it produces.
Tips for Contributors
- Use the provided helpers (
StackValue.copyToPtr,StackValue.decref, render functions) instead of manipulating raw pointers—this keeps refcounting correct. - The runtime stores (
runtime_types,runtime_layout_store) are owned by the interpreter instance. Reuse the same interpreter when evaluating multiple expressions inside a single host context so caches pay off. - When debugging type translation, the
tests/interpreter_*suites have targeted examples that illustrate expected behaviour.