Carp’s dynamic evaluator is VM-backed.
This document is both a high-level architecture overview and a maintainer guide for future VM work.
The dynamic evaluator is the compile-time execution engine used for:
defndynamic, commands, primitives),let, if, while, set!, and function calls.src/Eval.hs is the public API surface (evalDynamic, evalStatic, eval) and delegates execution to the VM path.
The evaluator pipeline is:
XObj -> EvalIR lowering (lowerExpr in src/EvalIR.hs).EvalIR -> EvalCode bytecode compilation (compileEvalIR in src/EvalVM.hs).runEvalCode in src/EvalVM.hs).Callables may be:
VMPrecompiled),VMCompileOnCall) with cached compiled code.src/Eval.hs: public evaluator entry points and integration with expansion.src/EvalIR.hs: evaluator IR, lowering, and raising (raiseExpr).src/EvalCode.hs: bytecode instruction and resolver-handle definitions.src/EvalVM.hs: compile pipeline, lookup logic, dispatch, opcode loop, caches.src/EvalVMCore.hs: low-level frame/code-store execution for registered callable code.src/EvalSlotLowering.hs: function-local slot lowering for fast local variable access.src/EvalBound.hs, src/EvalBind.hs: bound reference representation/helpers.src/EvalTypes.hs: evaluator lookup and execution-mode types.Evaluator behavior is mode-driven via LookupPreference:
PreferDynamicPreferGlobalPreferLocal ... ExecFunction|ExecDynamic|ExecMacroMode controls lookup and callable compilation policy:
EvalCode is a list plus array form of instructions. Important instruction classes:
IPushConst, IMakeArray, IMakeStaticArray, IDrop,IJumpIfFalseRel, IJumpRel, IHalt, ITrap,IResolveSymbol, IExecCallSymbol, IExecCall,IExecLet, IExecFn, IExecWhile, IExecWith, IExecSet.IExecCallSymbol and IExecCall carry both raw argument IR and precompiled argument code, so non-macro argument evaluation avoids recursive IR re-entry in hot paths.
The VM introduces VMClosure for executable callable payloads, but language-level semantics should remain transparent.
Current contract:
(dynamic|macro name params body),There are three relevant caches:
Info.infoIdentifier (evalIRCacheKey),(contextBindingEpoch, symbolId),contextBindingEpoch is used to invalidate cached bindings when environment state changes.
Compilation assigns each symbol a ResolverHandle:
RHLocalSlotRHGlobalRHDynamicRHQualifiedRHUnqualifiedThe opcode loop resolves by handle shape first and only falls back to broader lookup where required by semantics.
Function mode relies on slot lowering:
set! updates sync back into local slot state to keep reads coherent after mutation.If you change binding/lowering behavior, keep these invariants intact or update tests and docs together.
When changing evaluator behavior:
test/TestEvalIR.hs, test/TestEvalSlotLowering.hs, test/TestEvalVM.hs, test/TestEvalVMCore.hs),test/macros.carp, test/dynamic-closures.carp),./bench/run-evaluator-bench.sh) and compare medians, not single runs.Primary benchmark script:
./bench/run-evaluator-bench.shUse medians from repeated runs and compare at least:
bench/evaluator.carp),test/macros.carp).The evaluator VM is internal compiler infrastructure. It is not a user runtime VM and does not change Carp’s compiled-code execution model.