mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
6dc505c88b
commit
49bd3c22f3
274
docs/rvm/architecture.md
Normal file
274
docs/rvm/architecture.md
Normal 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
238
docs/rvm/instruction-set.md
Normal 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
243
docs/rvm/vm-runtime.md
Normal 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user