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
@@ -0,0 +1,995 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rvm::tests::instruction_parser::{parse_instruction, parse_loop_mode};
|
||||
use crate::rvm::tests::test_utils::test_round_trip_serialization;
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
struct RuleInfoSpec {
|
||||
rule_type: String,
|
||||
definitions: Vec<Vec<u16>>,
|
||||
#[serde(default)]
|
||||
default_rule_index: Option<u16>,
|
||||
#[serde(default)]
|
||||
default_literal_index: Option<u16>,
|
||||
#[serde(default)]
|
||||
destructuring_blocks: Option<Vec<Option<u16>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct DefaultRuleSpec {
|
||||
rule_name: String,
|
||||
default_value: crate::Value,
|
||||
}
|
||||
|
||||
use crate::rvm::vm::{ExecutionMode, ExecutionState, RegoVM, SuspendReason, VmError};
|
||||
use crate::tests::interpreter::process_value;
|
||||
use crate::value::Value;
|
||||
use alloc::collections::{BTreeMap, VecDeque};
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use test_generator::test_resources;
|
||||
|
||||
extern crate alloc;
|
||||
extern crate std;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct HostAwaitResponseSpec {
|
||||
id: crate::Value,
|
||||
#[serde(default)]
|
||||
value: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
values: Vec<crate::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct VmTestCase {
|
||||
note: String,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
example_rego: Option<String>,
|
||||
#[serde(default)]
|
||||
data: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
input: Option<crate::Value>,
|
||||
literals: Vec<crate::Value>,
|
||||
#[serde(default)]
|
||||
rule_infos: Vec<RuleInfoSpec>,
|
||||
#[serde(default)]
|
||||
default_rules: Vec<DefaultRuleSpec>,
|
||||
#[serde(default)]
|
||||
rule_tree: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
instruction_params: Option<InstructionParamsSpec>,
|
||||
#[serde(default)]
|
||||
max_instructions: Option<usize>,
|
||||
#[serde(default)]
|
||||
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
|
||||
#[serde(default)]
|
||||
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
|
||||
#[serde(default)]
|
||||
host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
|
||||
#[serde(default)]
|
||||
ignore_run_to_completion_hostawait_failure: bool,
|
||||
instructions: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_value")]
|
||||
want_result: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
want_error: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_value")]
|
||||
want_result_strict: Option<crate::Value>,
|
||||
#[serde(default)]
|
||||
want_error_strict: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_optional_value<'de, D>(deserializer: D) -> Result<Option<crate::Value>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
// If the field is present, always return Some, even if the value is null
|
||||
crate::Value::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
struct InstructionParamsSpec {
|
||||
#[serde(default)]
|
||||
loop_params: Vec<LoopStartParamsSpec>,
|
||||
#[serde(default)]
|
||||
call_params: Vec<CallParamsSpec>,
|
||||
#[serde(default)]
|
||||
builtin_call_params: Vec<BuiltinCallParamsSpec>,
|
||||
#[serde(default)]
|
||||
function_call_params: Vec<FunctionCallParamsSpec>,
|
||||
#[serde(default)]
|
||||
builtin_infos: Vec<BuiltinInfoSpec>,
|
||||
#[serde(default)]
|
||||
object_create_params: Vec<ObjectCreateParamsSpec>,
|
||||
#[serde(default)]
|
||||
array_create_params: Vec<ArrayCreateParamsSpec>,
|
||||
#[serde(default)]
|
||||
set_create_params: Vec<SetCreateParamsSpec>,
|
||||
#[serde(default)]
|
||||
virtual_data_document_lookup_params: Vec<VirtualDataDocumentLookupParamsSpec>,
|
||||
#[serde(default)]
|
||||
chained_index_params: Vec<ChainedIndexParamsSpec>,
|
||||
#[serde(default, alias = "comprehension_start_params")]
|
||||
comprehension_begin_params: Vec<ComprehensionBeginParamsSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct LoopStartParamsSpec {
|
||||
mode: String,
|
||||
collection: u16,
|
||||
key_reg: u16,
|
||||
value_reg: u16,
|
||||
result_reg: u16,
|
||||
body_start: u16,
|
||||
loop_end: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct CallParamsSpec {
|
||||
dest: u16,
|
||||
func: u16,
|
||||
args_start: u16,
|
||||
args_count: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct BuiltinCallParamsSpec {
|
||||
dest: u16,
|
||||
builtin_index: u16,
|
||||
args: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct FunctionCallParamsSpec {
|
||||
func: u16,
|
||||
dest: u16,
|
||||
args: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct BuiltinInfoSpec {
|
||||
name: String,
|
||||
num_args: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ObjectCreateParamsSpec {
|
||||
dest: u16,
|
||||
template_literal_idx: u16,
|
||||
literal_key_fields: Vec<(u16, u16)>,
|
||||
fields: Vec<(u16, u16)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ArrayCreateParamsSpec {
|
||||
dest: u16,
|
||||
elements: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct SetCreateParamsSpec {
|
||||
dest: u16,
|
||||
elements: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct LiteralOrRegisterSpec {
|
||||
#[serde(default, alias = "literal")]
|
||||
literal_idx: Option<u16>,
|
||||
#[serde(default, alias = "reg")]
|
||||
register: Option<u16>,
|
||||
}
|
||||
|
||||
impl LiteralOrRegisterSpec {
|
||||
fn into_literal_or_register(self) -> crate::rvm::instructions::LiteralOrRegister {
|
||||
if let Some(literal_idx) = self.literal_idx {
|
||||
crate::rvm::instructions::LiteralOrRegister::Literal(literal_idx)
|
||||
} else if let Some(register) = self.register {
|
||||
crate::rvm::instructions::LiteralOrRegister::Register(register.try_into().unwrap())
|
||||
} else {
|
||||
panic!("LiteralOrRegisterSpec must specify either literal_idx or register");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct VirtualDataDocumentLookupParamsSpec {
|
||||
dest: u16,
|
||||
path_components: Vec<LiteralOrRegisterSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ChainedIndexParamsSpec {
|
||||
dest: u16,
|
||||
root: u16,
|
||||
path_components: Vec<LiteralOrRegisterSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct ComprehensionBeginParamsSpec {
|
||||
mode: String,
|
||||
collection_reg: u16,
|
||||
#[serde(default)]
|
||||
result_reg: Option<u16>,
|
||||
key_reg: u16,
|
||||
value_reg: u16,
|
||||
body_start: u16,
|
||||
comprehension_end: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct VmTestSuite {
|
||||
cases: Vec<VmTestCase>,
|
||||
}
|
||||
|
||||
/// Execute VM instructions directly from parsed instructions and literals
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn execute_vm_instructions(
|
||||
instructions: Vec<crate::rvm::instructions::Instruction>,
|
||||
literals: Vec<Value>,
|
||||
rule_infos: Vec<RuleInfoSpec>,
|
||||
rule_tree: Option<Value>,
|
||||
instruction_params: Option<InstructionParamsSpec>,
|
||||
data: Option<Value>,
|
||||
input: Option<Value>,
|
||||
max_instructions: Option<usize>,
|
||||
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
|
||||
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
|
||||
host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
|
||||
ignore_run_to_completion_hostawait_failure: bool,
|
||||
strict: bool,
|
||||
) -> Result<Value> {
|
||||
let processed_data = if let Some(ref data_value) = data {
|
||||
Some(process_value(data_value)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let processed_input = if let Some(ref input_value) = input {
|
||||
Some(process_value(input_value)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let processed_rule_tree = if let Some(ref tree_value) = rule_tree {
|
||||
Some(process_value(tree_value)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let process_responses =
|
||||
|responses: Vec<HostAwaitResponseSpec>| -> Result<BTreeMap<Value, Vec<Value>>> {
|
||||
let mut processed: BTreeMap<Value, Vec<Value>> = BTreeMap::new();
|
||||
|
||||
for spec in responses {
|
||||
let identifier = process_value(&spec.id)?;
|
||||
let mut values: Vec<Value> = Vec::new();
|
||||
|
||||
if let Some(single) = spec.value.as_ref() {
|
||||
values.push(process_value(single)?);
|
||||
}
|
||||
|
||||
for value in &spec.values {
|
||||
values.push(process_value(value)?);
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"HostAwait response specification for id {:?} has no values",
|
||||
identifier
|
||||
));
|
||||
}
|
||||
|
||||
processed.entry(identifier).or_default().extend(values);
|
||||
}
|
||||
|
||||
Ok(processed)
|
||||
};
|
||||
|
||||
let processed_host_responses = if let Some(responses) = host_await_responses {
|
||||
Some(process_responses(responses)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let processed_host_responses_run_to_completion =
|
||||
if let Some(responses) = host_await_responses_run_to_completion {
|
||||
Some(process_responses(responses)?)
|
||||
} else {
|
||||
processed_host_responses.clone()
|
||||
};
|
||||
|
||||
let processed_host_responses_suspendable =
|
||||
if let Some(responses) = host_await_responses_suspendable {
|
||||
Some(process_responses(responses)?)
|
||||
} else {
|
||||
processed_host_responses.clone()
|
||||
};
|
||||
|
||||
// Create a Program from instructions and literals
|
||||
let mut program = crate::rvm::program::Program::new();
|
||||
program.instructions = instructions;
|
||||
|
||||
// Process literals through the value converter to handle special syntax like set!
|
||||
let mut processed_literals = Vec::new();
|
||||
for literal in &literals {
|
||||
processed_literals.push(process_value(literal)?);
|
||||
}
|
||||
program.literals = processed_literals;
|
||||
|
||||
if let Some(tree) = processed_rule_tree {
|
||||
program.rule_tree = tree;
|
||||
} else {
|
||||
program.rule_tree = Value::new_object();
|
||||
}
|
||||
|
||||
// Convert rule infos
|
||||
for rule_info_spec in rule_infos.iter() {
|
||||
use crate::rvm::program::{RuleInfo, RuleType};
|
||||
|
||||
let rule_type = match rule_info_spec.rule_type.as_str() {
|
||||
"Complete" => RuleType::Complete,
|
||||
"PartialSet" => RuleType::PartialSet,
|
||||
"PartialObject" => RuleType::PartialObject,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unknown rule type: {}",
|
||||
rule_info_spec.rule_type
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Convert Vec<Vec<u16>> to Vec<Vec<u32>>
|
||||
let definitions: Vec<Vec<u32>> = rule_info_spec
|
||||
.definitions
|
||||
.iter()
|
||||
.map(|def| def.iter().map(|&x| x as u32).collect())
|
||||
.collect();
|
||||
|
||||
let mut destructuring_blocks: Vec<Option<u32>> = rule_info_spec
|
||||
.destructuring_blocks
|
||||
.clone()
|
||||
.map(|blocks| {
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|entry| entry.map(|value| value as u32))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| alloc::vec![None; definitions.len()]);
|
||||
|
||||
if destructuring_blocks.len() != definitions.len() {
|
||||
destructuring_blocks.resize(definitions.len(), None);
|
||||
}
|
||||
|
||||
// For function calls, use result_reg 0; for other rules, use result_reg 1
|
||||
let result_reg = if instruction_params
|
||||
.as_ref()
|
||||
.is_some_and(|params| !params.function_call_params.is_empty())
|
||||
{
|
||||
0 // Function calls use register 0 as return register
|
||||
} else {
|
||||
1 // Regular rules use register 1
|
||||
};
|
||||
|
||||
let rule_info = RuleInfo {
|
||||
name: String::from("test_rule"),
|
||||
rule_type,
|
||||
definitions: crate::Rc::new(definitions.clone()),
|
||||
function_info: None,
|
||||
default_literal_index: rule_info_spec.default_literal_index,
|
||||
result_reg,
|
||||
num_registers: 50, // Increased to accommodate test cases with higher register indices
|
||||
destructuring_blocks,
|
||||
};
|
||||
|
||||
program.rule_infos.push(rule_info);
|
||||
}
|
||||
|
||||
// Build instruction data from params specification
|
||||
if let Some(params_spec) = instruction_params {
|
||||
// Convert loop params
|
||||
for loop_param_spec in params_spec.loop_params {
|
||||
let mode = parse_loop_mode(&loop_param_spec.mode)?;
|
||||
let loop_params = crate::rvm::instructions::LoopStartParams {
|
||||
mode,
|
||||
collection: loop_param_spec.collection.try_into().unwrap(),
|
||||
key_reg: loop_param_spec.key_reg.try_into().unwrap(),
|
||||
value_reg: loop_param_spec.value_reg.try_into().unwrap(),
|
||||
result_reg: loop_param_spec.result_reg.try_into().unwrap(),
|
||||
body_start: loop_param_spec.body_start,
|
||||
loop_end: loop_param_spec.loop_end,
|
||||
};
|
||||
program.add_loop_params(loop_params);
|
||||
}
|
||||
|
||||
// Convert call params
|
||||
// Legacy call_params support removed - use builtin_call_params or function_call_params instead
|
||||
if !params_spec.call_params.is_empty() {
|
||||
// Legacy call parameters are no longer supported
|
||||
// Convert to BuiltinCall or FunctionCall instructions instead
|
||||
panic!("Legacy call_params are no longer supported. Use builtin_call_params or function_call_params instead.");
|
||||
}
|
||||
|
||||
// Convert builtin info specs to program builtin info table
|
||||
for builtin_info_spec in params_spec.builtin_infos {
|
||||
let builtin_info = crate::rvm::program::BuiltinInfo {
|
||||
name: builtin_info_spec.name,
|
||||
num_args: builtin_info_spec.num_args,
|
||||
};
|
||||
program.add_builtin_info(builtin_info);
|
||||
}
|
||||
|
||||
// Convert builtin call params
|
||||
for builtin_call_spec in params_spec.builtin_call_params {
|
||||
use crate::rvm::instructions::BuiltinCallParams;
|
||||
|
||||
// Convert Vec<u16> to fixed array (unused slots are irrelevant due to num_args)
|
||||
let mut args_array = [0u8; 8];
|
||||
for (i, &arg) in builtin_call_spec.args.iter().enumerate() {
|
||||
if i < 8 {
|
||||
args_array[i] = arg.try_into().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let builtin_call_params = BuiltinCallParams {
|
||||
dest: builtin_call_spec.dest.try_into().unwrap(),
|
||||
builtin_index: builtin_call_spec.builtin_index,
|
||||
num_args: builtin_call_spec.args.len() as u8,
|
||||
args: args_array,
|
||||
};
|
||||
program.add_builtin_call_params(builtin_call_params);
|
||||
}
|
||||
|
||||
// Convert function call params
|
||||
for function_call_spec in params_spec.function_call_params {
|
||||
use crate::rvm::instructions::FunctionCallParams;
|
||||
|
||||
// Convert Vec<u16> to fixed array (unused slots are irrelevant due to num_args)
|
||||
let mut args_array = [0u8; 8];
|
||||
for (i, &arg) in function_call_spec.args.iter().enumerate() {
|
||||
if i < 8 {
|
||||
args_array[i] = arg.try_into().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let function_call_params = FunctionCallParams {
|
||||
func_rule_index: function_call_spec.func,
|
||||
dest: function_call_spec.dest.try_into().unwrap(),
|
||||
num_args: function_call_spec.args.len() as u8,
|
||||
args: args_array,
|
||||
};
|
||||
program.add_function_call_params(function_call_params);
|
||||
}
|
||||
|
||||
// Convert object create params
|
||||
for object_create_spec in params_spec.object_create_params {
|
||||
use crate::rvm::instructions::ObjectCreateParams;
|
||||
|
||||
let object_create_params = ObjectCreateParams {
|
||||
dest: object_create_spec.dest.try_into().unwrap(),
|
||||
template_literal_idx: object_create_spec.template_literal_idx,
|
||||
literal_key_fields: object_create_spec
|
||||
.literal_key_fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.try_into().unwrap()))
|
||||
.collect(),
|
||||
fields: object_create_spec
|
||||
.fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.try_into().unwrap(), v.try_into().unwrap()))
|
||||
.collect(),
|
||||
};
|
||||
program
|
||||
.instruction_data
|
||||
.add_object_create_params(object_create_params);
|
||||
}
|
||||
|
||||
// Convert array create params
|
||||
for array_create_spec in params_spec.array_create_params {
|
||||
use crate::rvm::instructions::ArrayCreateParams;
|
||||
|
||||
let array_create_params = ArrayCreateParams {
|
||||
dest: array_create_spec.dest.try_into().unwrap(),
|
||||
elements: array_create_spec
|
||||
.elements
|
||||
.into_iter()
|
||||
.map(|reg| reg.try_into().unwrap())
|
||||
.collect(),
|
||||
};
|
||||
program
|
||||
.instruction_data
|
||||
.add_array_create_params(array_create_params);
|
||||
}
|
||||
|
||||
// Convert set create params
|
||||
for set_create_spec in params_spec.set_create_params {
|
||||
use crate::rvm::instructions::SetCreateParams;
|
||||
|
||||
let set_create_params = SetCreateParams {
|
||||
dest: set_create_spec.dest.try_into().unwrap(),
|
||||
elements: set_create_spec
|
||||
.elements
|
||||
.into_iter()
|
||||
.map(|reg| reg.try_into().unwrap())
|
||||
.collect(),
|
||||
};
|
||||
program
|
||||
.instruction_data
|
||||
.add_set_create_params(set_create_params);
|
||||
}
|
||||
|
||||
// Convert virtual data document lookup params
|
||||
for virtual_spec in params_spec.virtual_data_document_lookup_params {
|
||||
use crate::rvm::instructions::{
|
||||
LiteralOrRegister, VirtualDataDocumentLookupParams,
|
||||
};
|
||||
|
||||
let path_components: Vec<LiteralOrRegister> = virtual_spec
|
||||
.path_components
|
||||
.into_iter()
|
||||
.map(|component| component.into_literal_or_register())
|
||||
.collect();
|
||||
|
||||
let params = VirtualDataDocumentLookupParams {
|
||||
dest: virtual_spec.dest.try_into().unwrap(),
|
||||
path_components,
|
||||
};
|
||||
|
||||
program
|
||||
.instruction_data
|
||||
.add_virtual_data_document_lookup_params(params);
|
||||
}
|
||||
|
||||
// Convert chained index params
|
||||
for chained_spec in params_spec.chained_index_params {
|
||||
use crate::rvm::instructions::{ChainedIndexParams, LiteralOrRegister};
|
||||
|
||||
let path_components: Vec<LiteralOrRegister> = chained_spec
|
||||
.path_components
|
||||
.into_iter()
|
||||
.map(|component| component.into_literal_or_register())
|
||||
.collect();
|
||||
|
||||
let params = ChainedIndexParams {
|
||||
dest: chained_spec.dest.try_into().unwrap(),
|
||||
root: chained_spec.root.try_into().unwrap(),
|
||||
path_components,
|
||||
};
|
||||
|
||||
program.instruction_data.add_chained_index_params(params);
|
||||
}
|
||||
|
||||
// Convert comprehension start params
|
||||
for comprehension_spec in params_spec.comprehension_begin_params {
|
||||
use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode};
|
||||
|
||||
let mode = match comprehension_spec.mode.as_str() {
|
||||
"Array" => ComprehensionMode::Array,
|
||||
"Set" => ComprehensionMode::Set,
|
||||
"Object" => ComprehensionMode::Object,
|
||||
_ => panic!("Invalid comprehension mode: {}", comprehension_spec.mode),
|
||||
};
|
||||
|
||||
let comprehension_params = ComprehensionBeginParams {
|
||||
mode,
|
||||
collection_reg: comprehension_spec.collection_reg.try_into().unwrap(),
|
||||
result_reg: comprehension_spec
|
||||
.result_reg
|
||||
.unwrap_or(comprehension_spec.collection_reg)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
key_reg: comprehension_spec.key_reg.try_into().unwrap(),
|
||||
value_reg: comprehension_spec.value_reg.try_into().unwrap(),
|
||||
body_start: comprehension_spec.body_start,
|
||||
comprehension_end: comprehension_spec.comprehension_end,
|
||||
};
|
||||
program
|
||||
.instruction_data
|
||||
.add_comprehension_begin_params(comprehension_params);
|
||||
}
|
||||
}
|
||||
|
||||
program.main_entry_point = 0;
|
||||
|
||||
// Set a reasonable default for register window size in VM tests
|
||||
// Most tests use registers 0-10, so we'll allocate 256 registers to be safe
|
||||
program.max_rule_window_size = 256;
|
||||
program.dispatch_window_size = 50;
|
||||
|
||||
// Initialize resolved builtins if we have builtin info
|
||||
if !program.builtin_info_table.is_empty() {
|
||||
if let Err(e) = program.initialize_resolved_builtins() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to initialize resolved builtins: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure program artifacts survive binary round-tripping
|
||||
test_round_trip_serialization(&program)
|
||||
.map_err(|e| anyhow::anyhow!("Program serialization round-trip failed: {}", e))?;
|
||||
|
||||
let program = Arc::new(program);
|
||||
|
||||
let run_with_mode = |mode: ExecutionMode,
|
||||
use_step_mode: bool,
|
||||
host_responses_template: Option<BTreeMap<Value, Vec<Value>>>|
|
||||
-> Result<Result<Value>> {
|
||||
let mut vm = RegoVM::new();
|
||||
vm.set_execution_mode(mode);
|
||||
vm.set_step_mode(use_step_mode);
|
||||
vm.set_strict_builtin_errors(strict);
|
||||
|
||||
if let Some(data_value) = processed_data.clone() {
|
||||
vm.set_data(data_value)?;
|
||||
}
|
||||
if let Some(input_value) = processed_input.clone() {
|
||||
vm.set_input(input_value);
|
||||
}
|
||||
|
||||
if let Some(limit) = max_instructions {
|
||||
vm.set_max_instructions(limit);
|
||||
}
|
||||
|
||||
if matches!(mode, ExecutionMode::RunToCompletion) {
|
||||
if let Some(responses) = host_responses_template.clone() {
|
||||
vm.set_host_await_responses(responses);
|
||||
}
|
||||
}
|
||||
|
||||
vm.load_program(program.clone());
|
||||
|
||||
let mut response_map = host_responses_template.clone().map(|map| {
|
||||
map.into_iter()
|
||||
.map(|(identifier, values)| (identifier, VecDeque::from(values)))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
});
|
||||
|
||||
let mut last_result = vm.execute().map_err(|e| anyhow::anyhow!("{}", e));
|
||||
|
||||
loop {
|
||||
match vm.execution_state() {
|
||||
ExecutionState::Completed { result } => {
|
||||
return Ok(Ok(result.clone()));
|
||||
}
|
||||
ExecutionState::Error { error } => {
|
||||
return Ok(Err(anyhow::anyhow!("{}", error)));
|
||||
}
|
||||
ExecutionState::Suspended { reason, .. } => {
|
||||
if mode != ExecutionMode::Suspendable {
|
||||
return Ok(Err(anyhow::anyhow!(
|
||||
"Run-to-completion execution unexpectedly suspended: {:?}",
|
||||
reason
|
||||
)));
|
||||
}
|
||||
|
||||
match reason {
|
||||
SuspendReason::HostAwait {
|
||||
dest, identifier, ..
|
||||
} => {
|
||||
let dest = *dest;
|
||||
let identifier = identifier.clone();
|
||||
|
||||
let response = {
|
||||
let map = response_map.as_mut().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"{}",
|
||||
VmError::HostAwaitResponseMissing {
|
||||
dest,
|
||||
identifier: identifier.clone(),
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
let queue = map.get_mut(&identifier).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"{}",
|
||||
VmError::HostAwaitResponseMissing {
|
||||
dest,
|
||||
identifier: identifier.clone(),
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
let response = queue.pop_front().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"{}",
|
||||
VmError::HostAwaitResponseMissing {
|
||||
dest,
|
||||
identifier: identifier.clone(),
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
if queue.is_empty() {
|
||||
map.remove(&identifier);
|
||||
}
|
||||
|
||||
response
|
||||
};
|
||||
|
||||
last_result = vm
|
||||
.resume(Some(response))
|
||||
.map_err(|e| anyhow::anyhow!("{}", e));
|
||||
}
|
||||
SuspendReason::Step => {
|
||||
if !use_step_mode {
|
||||
return Ok(Err(anyhow::anyhow!(
|
||||
"Suspendable execution unexpectedly suspended: {:?}",
|
||||
reason
|
||||
)));
|
||||
}
|
||||
last_result = vm.resume(None).map_err(|e| anyhow::anyhow!("{}", e));
|
||||
}
|
||||
_ => {
|
||||
last_result = vm.resume(None).map_err(|e| anyhow::anyhow!("{}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => match &last_result {
|
||||
Ok(value) => return Ok(Ok(value.clone())),
|
||||
Err(err) => return Ok(Err(anyhow::anyhow!("{}", err))),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let compare_results = |baseline_name: &str,
|
||||
baseline: &Result<Value>,
|
||||
other_name: &str,
|
||||
other: &Result<Value>|
|
||||
-> Result<()> {
|
||||
match (baseline, other) {
|
||||
(Ok(expected), Ok(actual)) => {
|
||||
if expected != actual {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} execution result {:?} differed from {} {:?}",
|
||||
other_name,
|
||||
actual,
|
||||
baseline_name,
|
||||
expected
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(Err(expected_err), Err(other_err)) => {
|
||||
let expected_msg = expected_err.to_string();
|
||||
let other_msg = other_err.to_string();
|
||||
if expected_msg != other_msg {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} execution error '{}' differed from {} '{}'",
|
||||
other_name,
|
||||
other_msg,
|
||||
baseline_name,
|
||||
expected_msg
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(Ok(expected), Err(other_err)) => Err(anyhow::anyhow!(
|
||||
"{} execution failed with '{}' while {} succeeded with {:?}",
|
||||
other_name,
|
||||
other_err,
|
||||
baseline_name,
|
||||
expected
|
||||
)),
|
||||
(Err(expected_err), Ok(actual)) => Err(anyhow::anyhow!(
|
||||
"{} execution succeeded with {:?} while {} failed with '{}'",
|
||||
other_name,
|
||||
actual,
|
||||
baseline_name,
|
||||
expected_err
|
||||
)),
|
||||
}
|
||||
};
|
||||
|
||||
let run_to_completion = run_with_mode(
|
||||
ExecutionMode::RunToCompletion,
|
||||
false,
|
||||
processed_host_responses_run_to_completion.clone(),
|
||||
)?;
|
||||
let suspendable = run_with_mode(
|
||||
ExecutionMode::Suspendable,
|
||||
false,
|
||||
processed_host_responses_suspendable.clone(),
|
||||
)?;
|
||||
let stepwise = run_with_mode(
|
||||
ExecutionMode::Suspendable,
|
||||
true,
|
||||
processed_host_responses_suspendable.clone(),
|
||||
)?;
|
||||
|
||||
const HOST_AWAIT_RESPONSE_MISSING: &str = "HostAwait executed but no response provided";
|
||||
|
||||
let ignore_run_to_completion = ignore_run_to_completion_hostawait_failure
|
||||
&& matches!(
|
||||
&run_to_completion,
|
||||
Err(err) if err.to_string().contains(HOST_AWAIT_RESPONSE_MISSING)
|
||||
);
|
||||
|
||||
if ignore_run_to_completion {
|
||||
compare_results("suspendable", &suspendable, "step-by-step", &stepwise)?;
|
||||
return suspendable;
|
||||
}
|
||||
|
||||
compare_results(
|
||||
"run-to-completion",
|
||||
&run_to_completion,
|
||||
"suspendable",
|
||||
&suspendable,
|
||||
)?;
|
||||
compare_results(
|
||||
"run-to-completion",
|
||||
&run_to_completion,
|
||||
"step-by-step",
|
||||
&stepwise,
|
||||
)?;
|
||||
|
||||
run_to_completion
|
||||
}
|
||||
|
||||
fn run_vm_test_suite(file: &str) -> Result<()> {
|
||||
std::println!("Running VM test suite: {}", file);
|
||||
let yaml_content = fs::read_to_string(file)?;
|
||||
let test_suite: VmTestSuite = serde_yaml::from_str(&yaml_content)?;
|
||||
|
||||
for test_case in test_suite.cases {
|
||||
std::println!("Running VM test case: {}", test_case.note);
|
||||
|
||||
let instructions = test_case
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction_str| parse_instruction(instruction_str))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let ignore_hostawait_failure = test_case.ignore_run_to_completion_hostawait_failure;
|
||||
|
||||
struct ModeExpectation<'a> {
|
||||
strict: bool,
|
||||
want_result: Option<&'a crate::Value>,
|
||||
want_error: Option<&'a String>,
|
||||
}
|
||||
|
||||
let mut expectations = Vec::new();
|
||||
|
||||
if test_case.want_result.is_some() || test_case.want_error.is_some() {
|
||||
expectations.push(ModeExpectation {
|
||||
strict: false,
|
||||
want_result: test_case.want_result.as_ref(),
|
||||
want_error: test_case.want_error.as_ref(),
|
||||
});
|
||||
}
|
||||
|
||||
if test_case.want_result_strict.is_some() || test_case.want_error_strict.is_some() {
|
||||
expectations.push(ModeExpectation {
|
||||
strict: true,
|
||||
want_result: test_case.want_result_strict.as_ref(),
|
||||
want_error: test_case.want_error_strict.as_ref(),
|
||||
});
|
||||
}
|
||||
|
||||
if expectations.is_empty() {
|
||||
panic!(
|
||||
"Test case '{}' must specify expectations for at least one mode",
|
||||
test_case.note
|
||||
);
|
||||
}
|
||||
|
||||
for expectation in expectations {
|
||||
let mode_label = if expectation.strict {
|
||||
"strict"
|
||||
} else {
|
||||
"non-strict"
|
||||
};
|
||||
std::println!(" Mode: {}", mode_label);
|
||||
|
||||
let execution_result = execute_vm_instructions(
|
||||
instructions.clone(),
|
||||
test_case.literals.clone(),
|
||||
test_case.rule_infos.clone(),
|
||||
test_case.rule_tree.clone(),
|
||||
test_case.instruction_params.clone(),
|
||||
test_case.data.clone(),
|
||||
test_case.input.clone(),
|
||||
test_case.max_instructions,
|
||||
test_case.host_await_responses.clone(),
|
||||
test_case.host_await_responses_run_to_completion.clone(),
|
||||
test_case.host_await_responses_suspendable.clone(),
|
||||
ignore_hostawait_failure,
|
||||
expectation.strict,
|
||||
);
|
||||
|
||||
if expectation.want_error.is_some() && expectation.want_result.is_some() {
|
||||
panic!(
|
||||
"Test case '{}' cannot specify both want_result and want_error for {} mode",
|
||||
test_case.note, mode_label
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(expected_error) = expectation.want_error {
|
||||
match execution_result {
|
||||
Err(e) => {
|
||||
let error_msg = std::format!("{}", e);
|
||||
if !error_msg.contains(expected_error) {
|
||||
std::println!(
|
||||
"Test case '{}' failed ({} mode):",
|
||||
test_case.note,
|
||||
mode_label
|
||||
);
|
||||
std::println!(" Expected error containing: '{}'", expected_error);
|
||||
std::println!(" Actual error: '{}'", error_msg);
|
||||
panic!("VM test case failed: {}", test_case.note);
|
||||
}
|
||||
}
|
||||
Ok(result) => {
|
||||
std::println!(
|
||||
"Test case '{}' failed ({} mode):",
|
||||
test_case.note,
|
||||
mode_label
|
||||
);
|
||||
std::println!(" Expected error containing: '{}'", expected_error);
|
||||
std::println!(" But got successful result: {:?}", result);
|
||||
panic!("VM test case failed: {}", test_case.note);
|
||||
}
|
||||
}
|
||||
} else if let Some(want_result) = expectation.want_result {
|
||||
let expected_result = process_value(want_result)?;
|
||||
|
||||
let actual_result = match execution_result {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
if std::format!("{}", e).contains("Assertion failed") {
|
||||
Value::Undefined
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if actual_result != expected_result {
|
||||
std::println!(
|
||||
"Test case '{}' failed ({} mode):",
|
||||
test_case.note,
|
||||
mode_label
|
||||
);
|
||||
std::println!(" Expected: {:?}", expected_result);
|
||||
std::println!(" Actual: {:?}", actual_result);
|
||||
panic!("VM test case failed: {}", test_case.note);
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
"Test case '{}' must specify either want_result or want_error for {} mode",
|
||||
test_case.note, mode_label
|
||||
);
|
||||
}
|
||||
|
||||
std::println!(" ✓ {} mode passed", mode_label);
|
||||
}
|
||||
|
||||
std::println!("✓ Test case '{}' passed", test_case.note);
|
||||
}
|
||||
std::println!("✓ Test suite '{}' completed successfully", file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_resources("tests/rvm/vm/suites/*.yaml")]
|
||||
fn run_vm_test_file(file: &str) {
|
||||
run_vm_test_suite(file).unwrap()
|
||||
}
|
||||
|
||||
#[test_resources("tests/rvm/vm/suites/loops/*.yaml")]
|
||||
fn run_loop_test_file(file: &str) {
|
||||
run_vm_test_suite(file).unwrap()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user