use crate::value::Value;
use alloc::vec::Vec;
use super::errors::{Result, VmError};
use super::execution_model::ExecutionState;
use super::machine::RegoVM;
impl RegoVM {
pub(super) fn reset_execution_state(&mut self) {
self.release_previous_execution_state();
self.initialize_execution_state();
}
pub(super) fn reset_run_to_completion_state(&mut self) -> Result<()> {
self.release_previous_execution_state();
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
if matches!(
self.memory_budget_lifecycle,
super::machine::MemoryBudgetLifecycle::FfiResultSerialization
) {
self.check_memory_budget()?;
} else {
self.reset_memory_budget_state();
}
self.initialize_execution_state();
Ok(())
}
fn release_previous_execution_state(&mut self) {
self.evaluated = Value::Undefined;
self.execution_state = ExecutionState::Ready;
self.execution_stack.clear();
self.return_to_pools();
self.rule_cache.clear();
self.registers.clear();
self.builtins_cache.clear();
self.cached_builtin_args.clear();
}
pub(super) fn fail_run_to_completion(&mut self, error: VmError) -> VmError {
self.release_previous_execution_state();
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
{
self.finish_active_memory_budget_execution();
}
self.execution_state = ExecutionState::Error {
error: error.clone(),
};
error
}
fn initialize_execution_state(&mut self) {
self.executed_instructions = 0;
self.pc = 0;
self.evaluated = Value::new_object();
self.cache_hits = 0;
self.execution_state = ExecutionState::Ready;
self.rule_cache = alloc::vec![(false, Value::Undefined); self.program.rule_infos.len()];
self.registers
.resize(self.base_register_count, Value::Undefined);
#[cfg(debug_assertions)]
self.debug_assert_state_is_clean();
}
#[inline]
#[cfg(debug_assertions)]
pub(super) fn debug_assert_state_is_clean(&self) {
debug_assert!(
self.execution_stack.is_empty(),
"reset_execution_state postcondition: execution_stack must be empty"
);
debug_assert!(
self.loop_stack.is_empty(),
"reset_execution_state postcondition: loop_stack must be empty"
);
debug_assert!(
self.comprehension_stack.is_empty(),
"reset_execution_state postcondition: comprehension_stack must be empty"
);
debug_assert!(
self.call_rule_stack.is_empty(),
"reset_execution_state postcondition: call_rule_stack must be empty"
);
debug_assert!(
self.register_stack.is_empty(),
"reset_execution_state postcondition: register_stack must be empty"
);
debug_assert!(
self.builtins_cache.is_empty(),
"reset_execution_state postcondition: builtins_cache must be empty"
);
debug_assert_eq!(
self.registers.len(),
self.base_register_count,
"reset_execution_state postcondition: registers must be sized to base_register_count"
);
debug_assert!(
self.registers.iter().all(|v| matches!(v, Value::Undefined)),
"reset_execution_state postcondition: all registers must be Undefined"
);
debug_assert_eq!(
self.rule_cache.len(),
self.program.rule_infos.len(),
"reset_execution_state postcondition: rule_cache size must match program rule_infos"
);
debug_assert!(
self.rule_cache.iter().all(|entry| !entry.0),
"reset_execution_state postcondition: rule_cache entries must be uncomputed"
);
debug_assert_eq!(
self.pc, 0,
"reset_execution_state postcondition: pc must be 0"
);
debug_assert_eq!(
self.executed_instructions, 0,
"reset_execution_state postcondition: executed_instructions must be 0"
);
debug_assert!(
matches!(self.execution_state, ExecutionState::Ready),
"reset_execution_state postcondition: execution_state must be Ready"
);
}
#[inline]
#[cfg(debug_assertions)]
pub(super) fn assert_vm_invariants(&self) {
debug_assert!(
matches!(
self.execution_state,
ExecutionState::Ready | ExecutionState::Running
),
"vm invariant: execution_state must be Ready or Running inside the dispatch loop, was {:?}",
self.execution_state
);
debug_assert_eq!(
self.rule_cache.len(),
self.program.rule_infos.len(),
"vm invariant: rule_cache size must equal program.rule_infos size"
);
}
pub(super) fn return_to_pools(&mut self) {
self.loop_stack.clear();
self.call_rule_stack.clear();
self.comprehension_stack.clear();
while let Some(registers) = self.register_stack.pop() {
self.return_register_window(registers);
}
}
pub(super) fn new_register_window(&mut self) -> Vec<Value> {
self.register_window_pool.pop().unwrap_or_default()
}
pub(super) fn return_register_window(&mut self, mut window: Vec<Value>) {
window.clear(); self.register_window_pool.push(window);
}
pub(super) fn validate_vm_state(&self) -> Result<()> {
if self.registers.len() < self.base_register_count {
return Err(VmError::RegisterCountBelowBase {
register_count: self.registers.len(),
base_count: self.base_register_count,
pc: self.pc,
});
}
if self.pc >= self.program.instructions.len() {
return Err(VmError::ProgramCounterOutOfBounds {
pc: self.pc,
instruction_count: self.program.instructions.len(),
});
}
if self.rule_cache.len() != self.program.rule_infos.len() {
return Err(VmError::RuleCacheSizeMismatch {
cache_size: self.rule_cache.len(),
rule_info_count: self.program.rule_infos.len(),
pc: self.pc,
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use super::super::machine::MemoryBudgetLifecycle;
use super::{ExecutionState, RegoVM, Value, VmError};
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use crate::rvm::program::{RuleInfo, RuleType};
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use crate::rvm::{instructions::Instruction, program::Program};
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use crate::MemoryBudgetConfig;
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use alloc::sync::Arc;
use alloc::vec;
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
use core::num::NonZeroU64;
#[test]
fn failed_run_to_completion_releases_retained_values() {
let mut vm = RegoVM::new();
let retained = Value::from("retained");
vm.evaluated = retained.clone();
vm.registers = vec![retained.clone()];
vm.rule_cache = vec![(true, retained.clone())];
vm.register_stack.push(vec![retained.clone()]);
vm.cached_builtin_args = vec![retained.clone()];
vm.execution_state = ExecutionState::Completed { result: retained };
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
{
vm.memory_budget_lifecycle = MemoryBudgetLifecycle::ImplicitExecution;
}
let error = VmError::MemoryBudgetExceeded {
usage: 2,
budget: 1,
pc: 3,
};
assert_eq!(vm.fail_run_to_completion(error.clone()), error);
assert!(matches!(vm.evaluated, Value::Undefined));
assert!(vm.registers.is_empty());
assert!(vm.rule_cache.is_empty());
assert!(vm.register_stack.is_empty());
assert!(vm
.register_window_pool
.iter()
.all(alloc::vec::Vec::is_empty));
assert!(vm.cached_builtin_args.is_empty());
assert_eq!(
vm.execution_state,
ExecutionState::Error {
error: error.clone()
}
);
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
assert!(matches!(
vm.memory_budget_lifecycle,
MemoryBudgetLifecycle::Inactive
));
}
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
#[allow(clippy::expect_used)]
#[test]
fn successful_ordinary_executions_finish_their_implicit_memory_budget() {
let mut program = Program::new();
program.entry_points.insert("main".into(), 0);
program.instructions = vec![Instruction::Return { value: 0 }];
program.instruction_spans = vec![None];
let mut vm = RegoVM::new();
vm.load_program(Arc::new(program));
vm.set_memory_budget_config(Some(MemoryBudgetConfig {
limit: NonZeroU64::new(1024 * 1024).unwrap_or(NonZeroU64::MIN),
}));
assert_eq!(
vm.execute().expect("main execution succeeds"),
Value::Undefined
);
assert!(matches!(
vm.memory_budget_lifecycle,
MemoryBudgetLifecycle::Inactive
));
assert_eq!(
vm.execute_entry_point_by_name("main")
.expect("named execution succeeds"),
Value::Undefined
);
assert!(matches!(
vm.memory_budget_lifecycle,
MemoryBudgetLifecycle::Inactive
));
assert_eq!(
vm.execute_entry_point_by_index(0)
.expect("indexed execution succeeds"),
Value::Undefined
);
assert!(matches!(
vm.memory_budget_lifecycle,
MemoryBudgetLifecycle::Inactive
));
}
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
#[test]
fn ffi_result_serialization_counts_fresh_initialization_after_releasing_previous_state() {
let rule_info = RuleInfo::new(
"unused".into(),
RuleType::Complete,
crate::Rc::new(alloc::vec![]),
0,
0,
);
let mut program = Program::new();
program.dispatch_window_size = 1;
program.max_rule_window_size = 1;
program.entry_points.insert("main".into(), 0);
program.rule_infos = alloc::vec![rule_info; Program::MAX_RULES];
program.instructions = alloc::vec![Instruction::Return { value: 0 }];
program.instruction_spans = alloc::vec![None];
let mut vm = RegoVM::new();
vm.load_program(Arc::new(program));
vm.rule_cache = alloc::vec![];
let retained = Value::from(
(0..(Program::MAX_RULES * 4))
.map(Value::from)
.collect::<alloc::vec::Vec<_>>(),
);
vm.evaluated = retained.clone();
vm.execution_state = ExecutionState::Completed { result: retained };
vm.set_memory_budget_config(Some(MemoryBudgetConfig {
limit: NonZeroU64::new(32 * 1024).unwrap_or(NonZeroU64::MIN),
}));
assert!(matches!(
vm.execute_to_c_string_for_ffi(),
Err(VmError::MemoryBudgetExceeded { .. })
));
assert!(matches!(
vm.execution_state,
ExecutionState::Error {
error: VmError::MemoryBudgetExceeded { .. }
}
));
}
}