Normative runtime contract: load, execution state, stepping, faults, snapshots, and host interfaces.
Implementation (partial): RoboInterpreter (run-to-end), RoboInterpreterSession / RoboInterpreterEngine (single shared evaluation stack across frames; not yet per-frame stacks as in §5 below). Structured faults: RuntimeFault. JSON load of executables: RoboExecutableJsonSerializer in Toolchain.
Related: il-instruction-set, interpreter, execution-model, pipeline boundaries.
The runtime loads a compiled RoboExecutable and executes it through a plain C# interpreter loop.
It is:
- deterministic
- stepable
- snapshot-friendly
- result-driven, not exception-driven
That direction is already established in the runtime and execution-model docs.
Load .roboexe
→ Validate manifest
→ Validate builtin/world compatibility
→ Build initial ExecutionState
→ Push entry frame
→ Run Step / Continue loop
→ Emit snapshots / stdout / stderr
Before execution, runtime must validate:
- executable format/version
- language version
- entry function exists
- function entry indices are valid
- instruction indices are valid
- builtin ids are known
- required builtin profile is satisfied
- required world kind is satisfied
This matches the earlier .roboexe contract.
The interpreter operates on a single ExecutionState containing:
- instruction pointer
- call stack
- heap
- world
- statistics
- stdout/stderr log
This matches the established runtime model.
Execution frames are slot-based, not symbol-based.
public sealed class CallFrame
{
public required int FunctionId { get; init; }
public required int ReturnAddress { get; init; }
public required Value[] Locals { get; init; }
public required Stack<Value> EvaluationStack { get; init; }
}Rules:
- one evaluation stack per frame
- locals array length equals compiled
LocalCount - parameters are copied into the first local slots on call
This follows the stronger runtime execution design already proposed.
On start:
- instruction pointer = entry function entry instruction
- heap empty
- statistics zeroed
- stdout/stderr empty
- call stack contains one frame for the entry function (the lowered top-level program body, implementation name
TopLevel)
That entry frame has:
- zero parameters
- locals array sized to compiled local count
- empty evaluation stack
- return address unused
Each Step() does exactly one instruction.
The step sequence is:
- validate instruction pointer
- fetch current instruction
- execute opcode handler
- mutate state
- update instruction pointer unless changed by control flow
- increment statistics
- return
StepResult
This matches the instruction-first execution model already defined for the debugger/runtime.
public enum ExecutionStatus
{
Running,
Completed,
Faulted
}Runtime faults are returned as structured data, not thrown exceptions. That is already a core design rule.
Each opcode handler must:
- validate required stack shape
- validate operand types if necessary
- perform state change
- return
StepResult
Push constant value onto current frame stack.
Push local slot value.
Pop top stack value into local slot.
Pop two numeric values, push result.
Set instruction pointer to target.
Pop bool; jump if false, otherwise continue.
Pop arguments, create new frame, jump to callee entry.
Invoke builtin runtime handler.
Pop optional return value, pop frame, either:
- push return value to caller and resume caller
- or complete execution if returning from entry frame
Allocate new heap array, push array reference.
Pop index and array ref, push element.
Pop value, index, array ref, assign.
Pop value and array ref, append.
Pop array ref, push current count.
Pop array ref, push last element.
Pop array ref, remove and push last element.
The exact opcode set is consistent with the existing IL/file-format direction.
Built-ins are invoked only through CallBuiltin.
Built-ins may:
- read or mutate world state
- read or mutate output streams
- return a value
- emit stderr warnings/faults
Built-ins must never throw for expected user-program failures.
They return structured results instead. That is already established in the C# execution model.
Minimum v1 fault codes:
InvalidInstructionInvalidFunctionInvalidBuiltinInvalidLocalSlotStackUnderflowInvalidArrayReferenceArrayIndexOutOfBoundsEmptyArrayStepLimitExceeded
World-related issues that are considered non-fatal lesson/runtime messages may go to stderr instead of faulting, depending on builtin policy.
Example policy:
- blocked
move()→ stderr, continue - invalid array access → stderr + fault
This matches the output-system direction.
The runtime contains two structured output streams:
StdOut= program outputStdErr= runtime messages / warnings / diagnostics
Each output line records instruction pointer metadata. This was already explicitly specified.
After each pause-worthy execution point, the runtime may capture a RuntimeSnapshot.
Minimum snapshot contains:
- instruction pointer
- current instruction text
- stack frames
- locals
- heap arrays
- world snapshot
- statistics
- stdout
- stderr
- fault if any
Snapshots are immutable copies for UI/debugging, not live state views. That is already a core design rule.
RunToEnd repeatedly calls Step until one of:
- completed
- faulted
- step limit exceeded
StepLimitExceeded should be a real dedicated fault code in v1.
Runtime must not use:
- threads
- timers
- random behavior unless explicitly modeled
- external IO during execution
This rule already exists in the runtime/debugger direction and should be hardened.
public interface IInterpreter
{
StepResult Step(ExecutionState state, RoboExecutable executable);
RunResult RunToEnd(ExecutionState state, RoboExecutable executable, int maxSteps);
RuntimeSnapshot CaptureSnapshot(ExecutionState state, RoboExecutable executable);
}public interface IRuntimeHost
{
ValueTask<RunResult> RunAsync(
RoboExecutable executable,
RuntimeLaunchOptions options,
CancellationToken cancellationToken = default);
}