Files
regorus/docs/rvm/vm-runtime.md
Anand Krishnamoorthi 49bd3c22f3 feat!: add Rego Virtual Machine (RVM) implementation (#495)
* feat!: add Rego Virtual Machine (RVM) implementation

This commit introduces a register-based virtual machine for executing Rego
policies with bytecode-style instructions. Unlike the existing tree-walking
interpreter, the RVM compiles policies into instruction sequences that operate
on virtual registers, offering better performance and optimization potential.

Core Components:

Instruction Set Architecture:
- Define instruction types for data operations, control flow, and builtins
- Implement instruction parameter encoding and display formatting
- Add instruction parser with comprehensive test coverage

Virtual Machine Engine:
- Register-based execution model with program counter management
- Loop execution supporting iterators, comprehensions, and quantifiers
- Function call handling with argument evaluation and context management
- Rule evaluation with default value resolution and virtual data support
- Arithmetic and comparison operation implementations

Program Representation:
- Program listing builder with instruction sequencing
- Rule tree construction for organizing policy rules
- Binary and JSON serialization for compiled programs
- Recompilation support for program modification

Testing Infrastructure:
- Extensive YAML test suites covering all VM features
- Rust unit tests for VM execution and instruction parsing
- Test suites for loops, comprehensions, builtins, and control flow

BREAKING CHANGE: Introduces new VM execution path alongside interpreter

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* docs: add detailed RVM architecture references

Introduce architecture.md explaining program artifacts, serialization, and runtime subsystems.
Document the full opcode catalog in instruction-set.md, including operands, parameter tables, and outcomes.
Walk through execution flow, stacks, and operational guidance in vm-runtime.md, tying the runtime to the new architecture docs.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-11-14 11:43:19 -06:00

12 KiB

VM Runtime Walkthrough

This document explains the runtime architecture implemented under src/rvm/vm. It focuses on the RegoVM struct, execution modes, and the responsibilities of each support module.


1. RegoVM structure

src/rvm/vm/machine.rs defines the public entry point. The table below maps its fields to responsibilities.

Field Purpose Related modules
registers: Vec<Value> Active register window for the current frame. execution.rs, dispatch.rs
pc: usize Instruction pointer in run-to-completion mode. execution.rs
program: Arc<Program> Loaded program artifact. program/core.rs
compiled_policy Optional legacy default-rule support. crate::CompiledPolicy
rule_cache: Vec<(bool, Value)> Memoized rule results (bool = computed). rules.rs
data, input Global documents injected by host. dispatch.rs, virtual_data.rs
loop_stack Stack of LoopContext for run-to-completion loops. loops.rs
call_rule_stack Stack of CallRuleContext for nested rule calls. rules.rs
register_stack Saves prior register windows during run-to-completion rule calls. rules.rs, state.rs
comprehension_stack Active ComprehensionContext objects. comprehension.rs
base_register_count Root window size derived from program metadata. load_program
register_window_pool Recycled register vectors to reduce allocations. state.rs, rules.rs
max_instructions, executed_instructions Instruction budget and counter. execution.rs
evaluated Cache for virtual document lookups. virtual_data.rs
cache_hits Counters aiding diagnostics. virtual_data.rs
execution_stack Explicit frame stack for suspendable mode. execution_model.rs
execution_state ExecutionState enum capturing Ready/Running/Suspended/Error/Completed. execution_model.rs, execution.rs
breakpoints Set of PCs that trigger suspension. execution_model.rs
step_mode Enables single-step suspension after each instruction. execution.rs
host_await_responses Pre-scripted responses keyed by identifier (run-to-completion). dispatch.rs
execution_mode RunToCompletion or Suspendable. execution.rs
frame_pc_overridden Tracks manual PC updates inside frames. execution.rs, loops.rs, comprehension.rs
strict_builtin_errors Configures builtin failure handling (error vs undefined). machine.rs, arithmetic.rs, dispatch.rs

Key methods

  • new / new_with_policy: initialise VM with default register windows and instruction limits.
  • load_program: attaches a compiled Program, resizes registers, seeds rule cache and resets counters.
  • set_data / set_input: inject host documents. set_data runs Program::check_rule_data_conflicts to guard against rule/data collisions.
  • set_max_instructions, set_execution_mode, set_step_mode: configure runtime policy.
  • set_host_await_responses: used in run-to-completion mode when host await responses are known ahead of time.
  • set_strict_builtin_errors: toggles builtin failure semantics between VmError::ArithmeticError and returning Value::Undefined.
  • Accessors (get_pc, get_registers, get_loop_stack, etc.) aid debugging and visualisation tooling.

2. Execution modes

Run-to-completion

  • Entry path: execute() or execute_entry_point_by_* when ExecutionMode::RunToCompletion.
  • execute_run_to_completion resets state, marks ExecutionState::Running and calls jump_to(start_pc).
  • jump_to loops over instructions, updating pc and calling execute_instruction. The loop stops on Return, Break, or VmError. Break (emitted by RuleReturn and DestructuringSuccess) returns register 0 to the caller for compatibility with rule evaluation.
  • Suspension is not allowed; encountering an instruction that would suspend (e.g. HostAwait) raises an internal error.
  • Instruction budgets trigger VmError::InstructionLimitExceeded and switch the state to ExecutionState::Error.

Suspendable

  • Entry path: same public API, but the VM calls run_stackless_from.
  • run_stackless_from pushes an initial ExecutionFrame::main(start_pc, 0) onto execution_stack and dispatches instructions via run_stackless_loop.
  • Each frame tracks its own pc and FrameKind (Main, Rule, Loop, Comprehension).
  • RuleFrameData carries scheduling cursors, register window sizing, and saved copies of the caller's registers and stacks so finalisation can restore the original context.
  • Suspension: InstructionOutcome::Suspend records a SuspendReason (host await, breakpoint, step) and stores the last result snapshot. Host code calls resume(resume_value) to continue.
  • Completion: when execution_stack becomes empty the VM sets ExecutionState::Completed { result }.

execution_model.rs defines the frame types and state machine:

  • ExecutionFrame: captures the frame-local pc together with its FrameKind payload.
  • RuleFrameData: tracks rule index, scheduling phase, register window sizing, and the saved caller state (saved_registers, saved_loop_stack, saved_comprehension_stack).
  • SuspendReason: currently surfaced values are host await, breakpoint, and step; additional variants (SuspendInstruction, InstructionLimit, External) are reserved for future instructions.

3. Instruction dispatch

dispatch.rs routes each Instruction variant through layered helpers (execute_load_and_move, execute_arithmetic_instruction, execute_call_instruction, etc.). Control flow hinges on the InstructionOutcome enum:

  • Continue: normal execution; the caller increments the frame pc.
  • Return(Value): unwinds the current rule/function frame, propagating the value upward.
  • Break: used for rule-specific constructs (destructuring success, rule return) to exit to the owning frame without returning a value.
  • Suspend { reason }: used exclusively in suspendable mode.

Arithmetic and comparison opcodes live in arithmetic.rs, honouring strict_builtin_errors when operand types differ. Collection, loop, and virtual-data operations share helpers that convert Value variants with runtime type checking. Errors become VmError variants to ensure consistent reporting. Halt returns the value stored in register 0, allowing bytecode to terminate early without suspending.


4. Loops and comprehensions

loops.rs implements iteration. Major components:

  • LoopContext: stores iteration state (IterationState enum), key/value/result registers, body and exit PCs, counters, and loop mode.
  • IterationState: variants for arrays, objects, sets. Tracks progress for both execution modes.
  • LoopMode: Any, Every, ForEach controls short-circuit behaviour.

execute_loop_start initialises iteration, pushing the context onto loop_stack (run-to-completion) or embedding it into a FrameKind::Loop (suspendable). LoopParams carries the bytecode offsets, registers, and destinations required by the instruction. LoopNext evaluates the previous iteration outcome, updates success_count, advances the iterator, overrides the caller's PC when needed, and decides whether to continue or exit.

comprehension.rs parallels loops.rs but maintains builder collections in ComprehensionContext. The context stores:

  • Builder value (array, set, object).
  • Pending key/value registers.
  • body_start / comprehension_end PCs.
  • iteration_state for nested loops bound to the comprehension.

ComprehensionYield writes to the builder, respecting set uniqueness and object key/value pairing. ComprehensionEnd publishes the result to result_reg and pops the context.


5. Rule execution and caching

rules.rs and functions.rs coordinate rule calls:

  • execute_call_rule dispatches based on execution mode. Both paths consult rule_cache and short-circuit if the result is already available.
  • Run-to-completion (execute_call_rule_common): swaps the active register, loop, and comprehension stacks; pushes them onto register_stack; and drives bodies via jump_to. Successful results are cached for non-function rules.
  • Suspendable (execute_call_rule_suspendable): builds a RuleFrameData containing saved registers/stacks and pushes a FrameKind::Rule so the stackless loop can schedule destructuring, bodies, and finalisation.
  • execute_rule_init writes the rule result register to Value::Undefined (or initialises sets/objects) before running the bodies and records the result register in the active CallRuleContext.
  • execute_rule_return lets the scheduler finalise the frame and propagate the cached value to the caller.
  • functions.rs::execute_function_call prepares argument registers and delegates to execute_call_rule*, enforcing arity via BuiltinInfo metadata.

Rule destructuring relies on CallRuleContext, RuleFramePhase::ExecutingDestructuring, and the DestructuringSuccess instruction to detect when pattern matching succeeded before entering the body.


6. Virtual data lookups

virtual_data.rs implements VirtualDataDocumentLookup and caches intermediate path results in the VM's evaluated field (using Value::Undefined as a sentinel) to avoid repeated rule evaluations. The compiler may set Program::needs_runtime_recursion_check; the runtime currently relies on the instruction budget and caching to prevent runaway recursion. Paths consist of literals and register values supplied via VirtualDataDocumentLookupParams.

ChainedIndex follows a similar pattern but operates on register roots instead of the global data namespace.


7. Error handling and diagnostics

errors.rs defines the VmError enum. Common variants include:

  • InstructionLimitExceeded
  • LiteralIndexOutOfBounds
  • RegisterNotArray / RegisterNotObject
  • InvalidEntryPointIndex / EntryPointNotFound
  • ArithmeticError
  • RuleDataConflict
  • HostAwaitResponseMissing
  • Internal(String) for invariant violations

execution.rs::handle_instruction_error centralises error propagation. In suspendable mode it unwinds frames while preserving partial results where possible. Run-to-completion mode returns the error immediately.

state.rs provides helpers to reset the VM and emit debug snapshots used in assertion messages. When an internal invariant fails, the error message includes self.get_debug_state() to help diagnose the issue.


8. Operational guidance

  • Instruction budgets: adjust via set_max_instructions when running untrusted policies. Inspect executed_instructions after completion.
  • Breakpoints & stepping: populate breakpoints with bytecode PCs (see the assembly listing) and enable set_step_mode(true) to pause after each instruction.
  • Host await: in run-to-completion mode, configure set_host_await_responses before execution. In suspendable mode, expect ExecutionState::Suspended { reason: HostAwait { .. } } and resume with the chosen value.
  • Builtin strictness: set_strict_builtin_errors(true) reports type mismatches as VmError::ArithmeticError; leave it false to coerce results to Value::Undefined.
  • State inspection: use getters (get_registers, get_call_stack, get_loop_stack, get_cache_hits) to instrument evaluation or build debugging UIs. get_debug_state() provides a concise snapshot for logs.
  • Testing: YAML suites under tests/rvm/vm/suites exercise loops, comprehensions, virtual data, host awaits, and serialization. complex.yaml combines nested loops, comprehensions, function calls, and host awaits.