roc/src/eval
2025-12-16 11:25:17 +11:00
..
test fix: transitive module imports in compile-time evaluation 2025-12-16 10:21:46 +11:00
builtin_loading.zig Fix some violations 2025-11-26 00:15:14 -05:00
BuiltinModules.zig Try renaming Result to Try 2025-10-30 20:18:53 -04:00
builtins.zig Eliminate more string comparisons 2025-11-26 01:08:20 -05:00
comptime_evaluator.zig initial implementation 2025-12-10 15:37:13 +11:00
crash_context.zig Fix lints 2025-09-29 08:21:46 -04:00
interpreter.zig add trace_modules feature 2025-12-16 11:25:17 +11:00
mod.zig Rename list_refcount_MINIMAL to not be yelling 2025-11-23 17:36:02 -05:00
README.md add -Dtrace-refcount=true and clear cache to zig build 2025-11-30 11:06:03 +11:00
render_helpers.zig only compare relevant union field 2025-12-12 13:06:34 +11:00
stack.zig Fix receiver bug 2025-12-07 07:18:26 -05:00
StackValue.zig Merge origin/main into fix-another-aoc 2025-12-11 23:18:39 -05:00
test_runner.zig Clean up some comments 2025-12-04 09:56:53 -05:00

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.zig exports Interpreter. Each instance owns the runtime state needed to evaluate expressions:

    • A runtime types.Store where compile-time Vars are translated and unified.
    • A runtime layout.Store plus an O(1) var_to_layout_slot cache 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.Stack used for temporary values, a binding list that models lexical scopes, and helper state for closures and boolean tags.
  • src/eval/StackValue.zig describes how values live in memory during evaluation. Each StackValue pairs 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.zig renders values using the same type information the interpreter carries. The interpreter delegates to these helpers for REPL output and tests.

Evaluation Flow

  1. Canonical inputs Consumers (REPL, tests, snapshot tool) parse and canonicalize Roc source, then hand a ModuleEnv and canonical expression idx to the interpreter.
  2. Initialization Interpreter.init translates 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).
  3. Minimal evaluation eval drives evaluation by calling evalExprMinimal. The interpreter pattern-matches on canonical expression tags (records, tuples, pattern matches, binops, calls, etc.), evaluates children recursively, and produces a StackValue annotated with layout.
  4. Type translation on demand When an expression needs type information (e.g. to render a value or create a layout), translateTypeVar copies the compile-time Var into the runtime store and caches the result.
  5. Layouts on demand getRuntimeLayout looks 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.
  6. Polymorphic calls Before a function call, prepareCall consults the poly cache. The interpreter only re-runs the runtime unifier if it has not seen that combination of function id + argument Vars before.
  7. Crash handling Crash/expect expressions delegate to the host via RocOps.crash. Hosts supply a CrashContext (see crash_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 in StackValue to initialize the result.
  • New data shapes Extend layout translation in translateTypeVar/getRuntimeLayout and teach StackValue how to copy or decref the shape.
  • Rendering Update render_helpers.zig and 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 interpreters 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 a ModuleEnv, 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.