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>
This commit is contained in:
Anand Krishnamoorthi
2025-11-14 11:43:19 -06:00
committed by GitHub
parent 6dc505c88b
commit 49bd3c22f3
89 changed files with 19158 additions and 313 deletions

274
docs/rvm/architecture.md Normal file
View File

@@ -0,0 +1,274 @@
# Regorus Virtual Machine Architecture
This document explains how Rego source becomes executable bytecode and how the
runtime evaluates it. It is meant for three audiences:
- **Engine developers** working on the RVM execution core and runtime subsystems.
- **Policy front-end authors** targeting the VM from alternate policy languages.
- **Operators/tools** wanting to reason about execution behaviour and
troubleshooting output.
The high-level pipeline looks like this:
```
┌───────────┐ emit Program ┌────────────┐ load & run ┌─────────┐
│ Parser & │ ───────────────▶ │ Program │ ─────────────▶ │ Rego VM │
│ Compiler │ (bytecode) │ Artifact │ (instructions│ Runtime │
└───────────┘ │ │ + metadata) │ │
└────────────┘ └─────────┘
```
Each step feeds the next via well-defined data structures described below.
---
## RVM in context
The Rego VM uses a register-based architecture with the following traits:
- **Register windows per frame**: Each rule or function call receives a
compile-time-sized register window. Windows are pooled and reused to keep the
runtime allocation profile predictable.
- **Sequential bytecode stream**: Fixed-width 32-bit instructions execute from
a linear program counter with optional jumps. Complex instructions reference
shared tables (`InstructionData`) that carry literals, loop metadata and call
parameters.
- **Literal and builtin tables**: Literal pools and builtin dispatch tables are
resolved at load time so bytecode stays compact and symbol lookups remain
constant-time during execution.
- **Extended control stacks**: Loop, rule-cache and comprehension stacks sit
alongside the core call stack, enabling suspension, short-circuiting and
deterministic rule caching without growing the register windows themselves.
---
## 1. Compilation Outputs
A successful compilation produces a `Program` (`src/rvm/program/core.rs`). The
layout is deliberately split:
- **Stable artifact section**: Always serialised and treated as canonical. It
captures the original policy sources, entry-points, compiler options,
etc.
- **Synthesised execution section**: It contains the
compiled instruction stream, instruction parameter tables, literal tables, etc.
It can be recreated from the stable artifact section if a future RVM version is
note able to deserialize it.
| Field | Purpose | Notes |
| :---------------------------------------------- | :--------------------------------------------------------------- | :---- |
| `instructions: Vec<Instruction>` | Ordered bytecode emitted by the compiler. | Each opcode is defined in `src/rvm/instructions/mod.rs` and executed by the dispatch tree. |
| `literals: Vec<Value>` | Literal constants shared across instructions. | Skipped by serde but written in the binary format via `BinaryValueSlice`; avoids duplicating large value graphs. |
| `instruction_data: InstructionData` | Parameter tables for complex opcodes. | Tables are indexed by `params_index` values stored in instructions. |
| `builtin_info_table: Vec<BuiltinInfo>` | Metadata for builtin calls. | Enforced and resolved by `Program::initialize_resolved_builtins`. |
| `entry_points: IndexMap<String, usize>` | Maps path names (e.g. `data.pkg.rule`) to starting PCs. | Preserves declaration order for tooling and serialized in the artifact section. |
| `sources: Vec<SourceFile>` | Captures original policy sources. | Stored in the stable artifact section alongside entry-points. |
| `rule_infos: Vec<RuleInfo>` | Metadata for every rule. | Includes register windows, default values, destructuring blocks. |
| `instruction_spans: Vec<Option<SpanInfo>>` | Optional span info for diagnostics. | Lines/columns mapped back into the source table when present. |
| `main_entry_point: usize` | Default bytecode entry point. | Used by loaders to jump into the top-level policy. |
| `max_rule_window_size` / `dispatch_window_size` | Register window sizing hints. | The VM uses these to size register banks up-front. |
| `metadata: ProgramMetadata` | Compilation metadata (`compiler_version`, etc.). | Helps operators verify provenance and tooling compatibility. |
| `rule_tree: Value` | Map of rule labels for conflict detection and lookups. | Serialized via `BinaryValueRef`; rebuilt into a `Value::Object` during load. |
| `resolved_builtins: Vec<BuiltinFcn>` | Resolved builtin function pointers. | Not serialized; repopulated by the host at load time. |
| `needs_runtime_recursion_check: bool` | Flags when `VirtualDataDocumentLookup` requires runtime guards. | Ensures the VM short-circuits recursion before hitting the instruction budget ceiling. |
| `needs_recompilation: bool` | Indicates partial deserialization of execution data. | Set when the extensible section fails; signals the loader to recompile. |
| `rego_v0: bool` | Records whether the policy targeted Rego v0 semantics. | Ensures recompilation preserves language-version behaviour. |
Additional helpers such as `Program::add_*`, `Program::update_*`, and
`Program::display_instruction_with_params` are used by the compiler and
inspection tooling to populate and render the program.
### Serialization layout
The module `src/rvm/program/serialization` writes `Program` instances into a
compact binary envelope that stays forward-compatible within a major format
version:
1. **Header**: magic `REGO` bytes followed by `SERIALIZATION_VERSION` (currently
`3`).
2. **Section manifest**: four little-endian `u32` lengths for entry points,
sources, literals, and the rule tree, plus a single-byte `rego_v0` flag.
3. **Preamble payloads**: each section is encoded with `bincode` using helper
wrappers (`BinaryValueSlice`, `BinaryValueRef`) to stream complex `Value`
graphs without cloning.
4. **Program core**: the remaining `Program` struct is serialized once more via
`bincode`; fields skipped by serde (entry points, literals, sources,
rule_tree, resolved builtins) are re-inserted from the preamble when the
program is reconstructed.
During deserialization the loader sanity-checks the header, lengths, and
version before decoding each preamble section. Any failure while decoding the
core payload downgrades the result to `DeserializationResult::Partial`,
preserving enough artifact data to trigger a recompilation. Successful loads
call `Program::initialize_resolved_builtins` so host runtimes can plug in their
builtin implementations.
---
## 2. Runtime Subsystems
At evaluation time the `RegoVM` (`src/rvm/vm/machine.rs`) consumes a `Program`
and exposes execution APIs. The VM separates concerns through specialised
stacks and caches.
````text
Runtime stacks (run-to-completion)
┌──────────────────────────── RegoVM ─────────────────────────────┐
│ Registers (active window) ─────┐ │
│ Program counter (pc) ───────┐ │ │
│ ▼ ▼ │
│ Control flow dispatcher ───────────────▶ Instruction stream │
│ ▲ ▲ │
│ Rule cache ────────┐ │ │ Loop stack (LoopContext) │
│ Evaluation cache │ │ └──▶ Comprehension stack │
│ Host await queue ──┴─▶ Return values / suspensions │
└─────────────────────────────────────────────────────────────────┘
Suspendable mode frame stack
┌───────────────────────────────────────────────────────────────────────┐
│ Frame stack │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────────────┐ │
│ │ RuleFrame │ → │ LoopFrame │ → │ ComprehensionFrame │ │
│ └────────────────┘ └────────────────┘ └──────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ push frame │ push frame │ push frame│
│ ▼ ▼ ▼ │
│ allow { ... } │
│ some user in input.users │
│ [x | ... ] │
└─────────┴─────────────────────────────────────────────────────────────┘
Execution state machine (suspendable)
┌──────────────────────┐
│ Suspended │
└─────▲────────────┬───┘
│ │
| │
│ │
│ │
HostAwait/Breakpoint/Step │ | resume
│ │
│ │
| ▼
┌──────────┐ ┌───────────────────────────┐ Return ┌────────────┐
│ Ready ├─────────────▶│ Running │────────────▶│ Completed │
└──────────┘ └────────────┬──────────────┘ └────────────┘
│ VmError
┌──────────┐
│ Error │
└──────────┘
````
Key state:
- **Registers**: The active register window for the current frame. Windows are
allocated per rule call using a register pool to minimise allocations.
- **Program counter (`pc`)**: The bytecode index for run-to-completion mode. In
suspendable mode, each frame tracks its own `pc`.
- **Rule cache**: Stores results and completion flags per rule to avoid
recomputation.
- **Loop/comprehension stacks**: Track iteration state, completion criteria, and
pending yields.
- **Execution stack**: Present in suspendable mode. Stores `ExecutionFrame`
objects (`FrameKind::Rule`, `Loop`, `Comprehension`) so that the VM can pause
and resume evaluation cleanly.
- **Host await responses**: For run-to-completion execution, pre-defined values
keyed by identifier. Suspendable mode instead returns a
`SuspendReason::HostAwait` to the caller.
- **Evaluation cache**: Used by `VirtualDataDocumentLookup` to memoise path
results.
---
## 3. Execution Modes
The VM supports two execution styles selected via `set_execution_mode`.
### Run-to-completion
- Entry point: `RegoVM::execute` or `execute_entry_point_by_{index,name}`.
- Control loop: `execute_run_to_completion` → `jump_to` which iterates the
instruction stream sequentially.
- Suspension: Unsupported. Any instruction that would suspend emits a runtime
error because the host cannot resume.
- Traps: Instruction budget enforced via `max_instructions`; exceeding the limit
returns `VmError::InstructionLimitExceeded`.
### Suspendable
- Entry point: same as above, but the VM calls `run_stackless_from` which pushes
a main `ExecutionFrame` and dispatches instructions through
`run_stackless_loop`.
- Frames: Each instruction can adjust the currently active frame or push/pop
new frames (rule calls, loops, comprehensions).
- Suspension: `InstructionOutcome::Suspend` transitions the VM into
`ExecutionState::Suspended` with a `SuspendReason` (host await, breakpoint,
single-step). The host must call `resume` with an optional value to continue.
- Breakpoints & step mode: Configured via `set_step_mode` and breakpoint
mutators on `ExecutionState`. Execution halts when a frame `pc` matches a
registered breakpoint.
In both modes the VM constantly validates safety conditions: parameter indices
must resolve, register windows must exist, and results must stay inside the
supported `Value` lattice. Errors are reported as `VmError` variants that
include formatted state snapshots where possible.
---
## 4. Data-flow Walkthrough
1. **Rule entry**: The compiler emits a `CallRule` instruction referencing a rule
index. The VM first consults `rule_cache[rule_index]`; non-function rules that
have already executed within the current top-level run reuse the cached
result. When the cache is cold, the VM pushes a new rule frame, allocates a
register window and jumps to the rule entry point. Function rules always run
afresh today—per-specialisation memoization is not yet implemented.
2. **Literal loads**: `Load` and `Load*` instructions fill registers from the
literal table or other sources (`LoadData`, `LoadInput`).
3. **Loops**: `LoopStart` fetches `LoopStartParams` from `InstructionData`,
initialises a `LoopContext`, and either pushes a new execution frame (for
suspendable mode) or updates `loop_stack`. `LoopNext` consults loop mode
(`Any`, `Every`, `ForEach`) to decide whether to continue or short-circuit.
4. **Comprehensions**: `ComprehensionBegin`/`Yield`/`End` manage collection
builders stored in a `ComprehensionContext`. Nested comprehensions stack
cleanly with loops.
5. **Assertions**: `AssertCondition` and `AssertNotUndefined` enforce Rego's
truthiness semantics. Inside loops/comprehensions they flag the current
iteration as failed (or short-circuit `every` loops to `false`); outside loop
contexts they raise `VmError::AssertionFailed`, mirroring Rego's runtime
errors for failed guards.
6. **Builtins & functions**: `BuiltinCall` reads `BuiltinCallParams`, resolves
the host function via `get_resolved_builtin`, and writes the result. Function
rules use `FunctionCallParams` to marshal arguments and run in the same
pipeline; repeat invocations with the same arguments are recomputed until the
VM grows specialisation-aware caching.
7. **Host await**: In run-to-completion mode, `HostAwait` consumes a response
from `host_await_responses`. Suspendable mode yields control with a
`SuspendReason::HostAwait { dest, argument, identifier }` that the host must
service.
8. **Completion**: `Return` wraps the selected register value into
`InstructionOutcome::Return`, unwinding frames until the entry frame is
cleared. `RuleReturn` is a specialised variant used by rule execution
helpers.
Throughout execution, diagnostics (register snapshots, loop counters, cache
hits) can be collected via `RegoVM` accessors. Integration tests in
`tests/rvm/vm/suites` exercise the most complex combinations of loops,
comprehensions and host calls; `complex.yaml` is a good starting point for
understanding real-world instruction streams.
---
## 5. Related Documentation
- [Instruction Set Reference](instruction-set.md)
- [VM Runtime Walkthrough](vm-runtime.md)

238
docs/rvm/instruction-set.md Normal file
View File

@@ -0,0 +1,238 @@
# RVM Instruction Set Reference
This reference captures every opcode emitted by the compiler and executed by
`RegoVM`. Each instruction is defined in `src/rvm/instructions/mod.rs` and
implemented by the dispatcher tree in `src/rvm/vm/dispatch.rs` plus specialised
submodules (`arithmetic.rs`, `loops.rs`, `functions.rs`, `rules.rs`,
`comprehension.rs`, `virtual_data.rs`).
Use this guide to understand operand semantics, parameter tables, and runtime
side effects.
---
## Reading the tables
- **Operands**: registers (`rX`), literals (`litY`), parameter indices (`pZ`) and
immediate values.
- **Parameters**: links into `InstructionData` (`src/rvm/instructions/params.rs`).
The compiler stores complex metadata here; instructions reference it by index.
- **Outcome**: mentioned in prose where relevant (`Continue`, `Return`, `Break`,
`Suspend`).
---
## Load and Move instructions
| Mnemonic | Operands | Behaviour |
| :--------- | :-------------------------- | :--------------------------------------------------- |
| `Load` | `dest=rD, literal_idx=litN` | Copies literal `N` into register `D`. |
| `LoadTrue` | `dest=rD` | Stores boolean `true`. |
| `LoadFalse`| `dest=rD` | Stores boolean `false`. |
| `LoadNull` | `dest=rD` | Stores `Value::Null`. |
| `LoadBool` | `dest=rD, value` | Stores inline boolean literal. |
| `LoadData` | `dest=rD` | Stores the VM's `data` value. |
| `LoadInput`| `dest=rD` | Stores the VM's `input` value. |
| `Move` | `dest=rD, src=rS` | Copies register `S` into register `D`. |
Out-of-range literal indices raise `VmError::LiteralIndexOutOfBounds`. Registers
must have been allocated by the current frame.
---
## Arithmetic and comparison instructions
| Mnemonic | Operands | Behaviour |
| :------- | :---------------------- | :------------------------------------------------------------ |
| `Add` | `dest, left, right` | Numeric addition; undefined operands trigger loop condition checks. |
| `Sub` | `dest, left, right` | Numeric subtraction. |
| `Mul` | `dest, left, right` | Numeric multiplication. |
| `Div` | `dest, left, right` | Numeric division with runtime checks (division by zero errors). |
| `Mod` | `dest, left, right` | Modulo. |
| `Eq` | `dest, left, right` | Equality comparison resulting in `Value::Bool`. |
| `Ne` | `dest, left, right` | Inequality. |
| `Lt`/`Le`/`Gt`/`Ge` | `dest, left, right` | Ordering comparisons. |
| `And` | `dest, left, right` | Logical conjunction (truthiness semantics). |
| `Or` | `dest, left, right` | Logical disjunction. |
| `Not` | `dest, operand` | Logical negation. |
| `AssertCondition` | `condition` | Fails current loop/rule when the condition is falsey. |
| `AssertNotUndefined` | `register` | Fails when register holds `Value::Undefined`. |
`handle_condition` routes through `loops.rs` to propagate failures to loop and
comprehension contexts. Outside loops it aborts the current rule.
---
## Collection and indexing instructions
| Mnemonic | Operands / Params | Behaviour |
| :------------------------- | :---------------------------- | :---------------------------------------------------------- |
| `ObjectSet` | `obj, key, value` | Mutates object in `obj` with key/value from registers. |
| `ObjectCreate` | `params_index=pN` | Builds object from literal template and register entries. |
| `ArrayNew` | `dest` | Creates empty array. |
| `ArrayPush` | `arr, value` | Appends to array. |
| `ArrayCreate` | `params_index=pN` | Builds array from register list; undefined element ⇒ result undefined. |
| `SetNew` | `dest` | Creates empty set. |
| `SetAdd` | `set, value` | Adds element to set. |
| `SetCreate` | `params_index=pN` | Builds set from register list; undefined element ⇒ result undefined. |
| `Index` | `dest, container, key` | Indexes container with runtime key. |
| `IndexLiteral` | `dest, container, literal_idx`| Indexes container using literal stored in program. |
| `ChainedIndex` | `params_index=pN` | Resolves multi-hop path from root register. |
| `Contains` | `dest, collection, value` | Checks membership; returns `Value::Bool`. |
| `Count` | `dest, collection` | Returns length or `Value::Undefined` for unsupported types. |
| `VirtualDataDocumentLookup`| `params_index=pN` | Evaluates `data` path, invoking rules lazily. |
Parameter structures:
- `ObjectCreateParams` reuses arrays of literal key/value pairs and register
pairs. Literal keys must be sorted to match template order.
- `ArrayCreateParams` and `SetCreateParams` store register lists. The VM checks
all referenced registers for `Value::Undefined` before constructing the
collection.
- `VirtualDataDocumentLookupParams` and `ChainedIndexParams` encode `Vec<LiteralOrRegister>`
path components. `LiteralOrRegister` is defined in `src/rvm/instructions/types.rs`.
---
## Loop instructions
Loops use dedicated parameter tables (`LoopStartParams`) and the `LoopMode`
enum.
| Mnemonic | Operands / Params | Behaviour |
| :---------- | :----------------------- | :------------------------------------------------------------- |
| `LoopStart` | `params_index=pN` | Initialises loop context and decides first body iteration. |
| `LoopNext` | `body_start`, `loop_end` | Finalises iteration, updates accumulators, advances to next element. |
`LoopMode` values:
- `Any`: succeed on first passing iteration, short-circuit on success.
- `Every`: fail on first failing iteration.
- `ForEach`: evaluate all iterations, typically for comprehensions or complete
rules.
`LoopStartParams` fields:
- `collection`: source register.
- `key_reg` / `value_reg`: iteration registers (for arrays, key is index).
- `result_reg`: accumulator storing loop outcome (`bool` for quantifiers).
- `body_start` / `loop_end`: PCs identifying loop boundaries.
The dispatcher converts `LoopStartParams` into a VM-specific `LoopParams` used by
both execution modes. In suspendable mode, loops own their own `ExecutionFrame`.
---
## Comprehension instructions
| Mnemonic | Operands / Params | Behaviour |
| :------------------- | :---------------------- | :------------------------------------------------- |
| `ComprehensionBegin` | `params_index=pN` | Allocates collection builder and iteration context. |
| `ComprehensionYield` | `value_reg`, `key_reg?` | Emits value (and optional key) into builder. |
| `ComprehensionEnd` | — | Finalises collection and stores result. |
`ComprehensionBeginParams` captures:
- `mode: ComprehensionMode` (Set, Array, Object)
- `collection_reg`: source register for iteration
- `result_reg`: register that will hold the final collection
- `key_reg` / `value_reg`: iteration registers
- `body_start` / `comprehension_end`: branch targets
Comprehensions manage their own stack (`ComprehensionContext`) to maintain
ordering guarantees (arrays), uniqueness (sets) or key/value pairing (objects).
---
## Call and return instructions
| Mnemonic | Operands / Params | Behaviour |
| :-------------------- | :------------------------ | :---------------------------------------------- |
| `BuiltinCall` | `params_index=pN` | Invokes builtin via resolved function pointer. |
| `FunctionCall` | `params_index=pN` | Invokes function rule. |
| `CallRule` | `dest, rule_index` | Requests rule evaluation with caching. |
| `RuleInit` | `result_reg, rule_index` | Prepares rule accumulator and cache state. |
| `Return` | `value_reg` | Returns value from current function body. |
| `RuleReturn` | — | Finalises rule evaluation frame. |
| `DestructuringSuccess`| — | Signals successful destructuring, breaks rule block. |
Parameter tables:
- `BuiltinCallParams` / `FunctionCallParams` store destination register, index
into builtin table / rule index, argument count and up to eight argument
register numbers.
- The VM dynamically resizes registers when a callee requires a larger window
using program metadata (`max_rule_window_size`).
---
## Host interaction
| Mnemonic | Operands | Behaviour |
| :--------- | :---------------- | :--------------------------------------- |
| `HostAwait`| `dest, arg, id` | Yields control to host with payload value. |
- Run-to-completion: consumes a response from `host_await_responses` keyed by
the identifier register. Missing responses raise `VmError::HostAwaitResponseMissing`.
- Suspendable: emits `InstructionOutcome::Suspend` with `SuspendReason::HostAwait`.
The host must resume with a value that will be written into `dest`.
---
## Halt instruction
| Mnemonic | Behaviour | Notes |
| :------- | :-------------------------------- | :---- |
| `Halt` | Terminates execution immediately. | Used during debugging or emitted for guard rails. |
When encountered during run-to-completion execution, `Halt` returns the current
value in register `0`.
---
## Parameter data overview
`InstructionData` (`src/rvm/instructions/params.rs`) collects all complex
parameter types. Each `add_*` method returns a `u16` index suitable for storing
inside instructions. The VM retrieves tables via `get_*` accessors.
| Struct | Field | Purpose |
| :----------------------- | :---------------------------------------- | :------------------------------------------------------------------------- |
| `LoopStartParams` | `mode` | Loop semantics (`Any`, `Every`, `ForEach`). |
| | `collection` | Register holding the iterable collection. |
| | `key_reg` / `value_reg` | Registers populated with the current key/value each iteration. |
| | `result_reg` | Accumulator for loop outcome (`bool` for quantifiers). |
| | `body_start` / `loop_end` | Instruction pointers delimiting the loop body and exit. |
| `BuiltinCallParams` | `dest` | Register that receives the builtin result. |
| | `builtin_index` | Slot into `builtin_info_table` for dispatch. |
| | `num_args` | Count of argument registers actually populated. |
| | `args[8]` | Up to eight registers supplying builtin arguments. |
| `FunctionCallParams` | `dest` | Register that receives the function rule result. |
| | `func_rule_index` | Rule index for the target function definition. |
| | `num_args` | Number of argument registers provided. |
| | `args[8]` | Argument register numbers (unused slots ignored). |
| `ObjectCreateParams` | `dest` | Destination register for the constructed object. |
| | `template_literal_idx` | Literal template containing all expected keys. |
| | `literal_key_fields: Vec<(u16, u8)>` | Mapping of literal-key indices to value registers. |
| | `fields: Vec<(u8, u8)>` | Dynamic key/value register pairs for non-literal keys. |
| `ArrayCreateParams` | `dest` | Destination register for the array literal. |
| | `elements: Vec<u8>` | Registers providing array elements (order preserved). |
| `SetCreateParams` | `dest` | Destination register for the set literal. |
| | `elements: Vec<u8>` | Registers providing set members (duplicates dropped at runtime). |
| `VirtualDataDocumentLookupParams` | `dest` | Destination register for lookup result. |
| | `path_components: Vec<LiteralOrRegister>` | Ordered path traversal steps; mix of literals and register-based keys. |
| `ChainedIndexParams` | `dest` | Destination register for resolved value. |
| | `root` | Register containing the root object/collection. |
| | `path_components: Vec<LiteralOrRegister>` | Path components applied relative to the root register. |
| `ComprehensionBeginParams` | `mode` | Comprehension output type (array, set, object). |
| | `collection_reg` | Source collection register for iteration. |
| | `result_reg` | Register receiving the final collection. |
| | `key_reg` / `value_reg` | Iteration registers (keys optional for arrays/sets). |
| | `body_start` / `comprehension_end` | Instruction pointers framing comprehension body and exit. |
All parameter structs derive `Serialize`/`Deserialize` and can be stored inside
artifacts. Some contain `Vec` fields; the compiler is responsible for ensuring
indices remain valid and stable across serialization boundaries.
---

243
docs/rvm/vm-runtime.md Normal file
View File

@@ -0,0 +1,243 @@
# 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.
---