use crate::rvm::instructions::Instruction;
use crate::rvm::program::Program;
use crate::value::Value;
use alloc::string::String;
use core::convert::TryFrom as _;
use super::dispatch::InstructionOutcome;
use super::errors::{Result, VmError};
use super::execution_model::{
ExecutionFrame, ExecutionMode, ExecutionState, FrameKind, RuleFrameData, RuleFramePhase,
SuspendReason,
};
use super::machine::RegoVM;
impl RegoVM {
pub fn execute(&mut self) -> Result<Value> {
crate::utils::limits::without_memory_budget_scope(|| {
Self::ensure_memory_budget_execution_mode()?;
self.execution_mode = self.next_execution_mode;
match self.execution_mode {
ExecutionMode::RunToCompletion => self.execute_run_to_completion(),
ExecutionMode::Suspendable => self.execute_suspendable(),
}
})
}
pub fn execute_entry_point_by_index(&mut self, index: usize) -> Result<Value> {
crate::utils::limits::without_memory_budget_scope(|| {
Self::ensure_memory_budget_execution_mode()?;
let (entry_point_name, entry_point_pc) = {
let (name, &pc) = self.program.entry_points.get_index(index).ok_or(
VmError::InvalidEntryPointIndex {
index,
max_index: self.program.entry_points.len().saturating_sub(1),
pc: self.pc,
},
)?;
(name.clone(), pc)
};
if entry_point_pc >= self.program.instructions.len() {
return Err(VmError::EntryPointPcOutOfBounds {
pc: entry_point_pc,
instruction_count: self.program.instructions.len(),
entry_point: entry_point_name,
});
}
self.execution_mode = self.next_execution_mode;
match self.execution_mode {
ExecutionMode::RunToCompletion => {
let result = (|| {
self.reset_run_to_completion_state()?;
self.reset_execution_timer_state();
self.validate_vm_state()?;
let entry_point_pc_u32 = u32::try_from(entry_point_pc).map_err(|_| {
VmError::EntryPointPcOutOfBounds {
pc: entry_point_pc,
instruction_count: self.program.instructions.len(),
entry_point: entry_point_name.clone(),
}
})?;
self.check_initial_memory_limit()?;
let result = self
.jump_to(entry_point_pc_u32)
.map_err(|err| self.apply_memory_budget_precedence(err))?;
self.check_memory_budget()?;
Ok(result)
})();
match result {
Ok(value) => {
self.finish_implicit_memory_budget_execution();
Ok(value)
}
Err(err) => Err(self.fail_run_to_completion(err)),
}
}
ExecutionMode::Suspendable => self.execute_suspendable_entry(entry_point_pc),
}
})
}
pub fn execute_entry_point_by_name(&mut self, name: &str) -> Result<Value> {
crate::utils::limits::without_memory_budget_scope(|| {
Self::ensure_memory_budget_execution_mode()?;
let entry_point_pc =
self.program
.get_entry_point(name)
.ok_or_else(|| VmError::EntryPointNotFound {
name: String::from(name),
available: self.program.entry_points.keys().cloned().collect(),
pc: self.pc,
})?;
if entry_point_pc >= self.program.instructions.len() {
return Err(VmError::EntryPointPcOutOfBounds {
pc: entry_point_pc,
instruction_count: self.program.instructions.len(),
entry_point: String::from(name),
});
}
self.execution_mode = self.next_execution_mode;
match self.execution_mode {
ExecutionMode::RunToCompletion => {
let result = (|| {
self.reset_run_to_completion_state()?;
self.reset_execution_timer_state();
self.validate_vm_state()?;
let entry_point_pc_u32 = u32::try_from(entry_point_pc).map_err(|_| {
VmError::EntryPointPcOutOfBounds {
pc: entry_point_pc,
instruction_count: self.program.instructions.len(),
entry_point: String::from(name),
}
})?;
self.check_initial_memory_limit()?;
let result = self
.jump_to(entry_point_pc_u32)
.map_err(|err| self.apply_memory_budget_precedence(err))?;
self.check_memory_budget()?;
Ok(result)
})();
match result {
Ok(value) => {
self.finish_implicit_memory_budget_execution();
Ok(value)
}
Err(err) => Err(self.fail_run_to_completion(err)),
}
}
ExecutionMode::Suspendable => self.execute_suspendable_entry(entry_point_pc),
}
})
}
pub(super) fn jump_to(&mut self, target: u32) -> Result<Value> {
let program = self.program.clone();
let target = self.convert_pc(target, "jump target")?;
self.pc = target;
while self.pc < program.instructions.len() {
#[cfg(debug_assertions)]
self.assert_vm_invariants();
self.memory_check()?;
if self.executed_instructions >= self.max_instructions {
return Err(VmError::InstructionLimitExceeded {
limit: self.max_instructions,
executed: self.executed_instructions,
pc: self.pc,
});
}
self.execution_timer_tick(1)?;
self.executed_instructions = self.executed_instructions.saturating_add(1);
let instruction = program.instructions.get(self.pc).cloned().ok_or(
VmError::ProgramCounterOutOfBounds {
pc: self.pc,
instruction_count: program.instructions.len(),
},
)?;
match self.execute_instruction(&program, instruction)? {
InstructionOutcome::Continue => {
self.pc = self.pc.saturating_add(1);
}
InstructionOutcome::Return(value) => {
return Ok(value);
}
InstructionOutcome::Break => {
return Ok(self.registers.first().cloned().unwrap_or(Value::Undefined));
}
InstructionOutcome::Suspend { reason } => {
return Err(VmError::UnsupportedSuspendInRunToCompletion {
reason,
pc: self.pc,
});
}
}
}
Ok(self.registers.first().cloned().unwrap_or(Value::Undefined))
}
fn execute_run_to_completion(&mut self) -> Result<Value> {
let result = (|| {
self.reset_run_to_completion_state()?;
self.reset_execution_timer_state();
self.execution_state = ExecutionState::Running;
self.check_initial_memory_limit()?;
self.jump_to(0_u32)
.map_err(|err| self.apply_memory_budget_precedence(err))
.and_then(|value| {
self.check_memory_budget()?;
Ok(value)
})
})();
match result {
Ok(value) => {
self.execution_state = ExecutionState::Completed {
result: value.clone(),
};
self.finish_implicit_memory_budget_execution();
Ok(value)
}
Err(err) => Err(self.fail_run_to_completion(err)),
}
}
fn execute_suspendable(&mut self) -> Result<Value> {
self.execute_new_suspendable_execution(0, false)
}
fn execute_suspendable_entry(&mut self, entry_point_pc: usize) -> Result<Value> {
self.execute_new_suspendable_execution(entry_point_pc, true)
}
fn execute_new_suspendable_execution(
&mut self,
start_pc: usize,
validate_vm_state: bool,
) -> Result<Value> {
self.release_previous_execution_state();
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
self.begin_suspendable_memory_budget_execution();
let run = |vm: &mut Self| {
vm.initialize_execution_state();
#[cfg(debug_assertions)]
vm.debug_assert_state_is_clean();
vm.execution_state = ExecutionState::Running;
vm.reset_execution_timer_state();
vm.pc = start_pc;
let result = (|| {
if validate_vm_state {
vm.validate_vm_state()?;
}
vm.check_initial_memory_limit()
})()
.and_then(|()| vm.run_stackless_from(start_pc))
.and_then(|value| {
vm.check_memory_budget()?;
Ok(value)
});
match result {
Ok(value) => {
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
if !vm.ffi_memory_budget_scope_active
&& !matches!(vm.execution_state, ExecutionState::Suspended { .. })
{
vm.finish_active_memory_budget_execution();
}
Ok(value)
}
Err(err) => {
let err = vm.apply_memory_budget_precedence(err);
Err(vm.fail_initial_suspendable_execution(err))
}
}
};
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
{
self.with_suspendable_memory_budget_scope(run)
}
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]
{
run(self)
}
}
pub fn resume(&mut self, resume_value: Option<Value>) -> Result<Value> {
Self::ensure_memory_budget_resume_supported()?;
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
{
if self.ffi_memory_budget_scope_active {
self.resume_in_memory_budget_scope(resume_value)
} else {
self.with_suspendable_memory_budget_scope(|vm| {
vm.resume_in_memory_budget_scope(resume_value)
})
}
}
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]
{
crate::utils::limits::without_memory_budget_scope(|| {
self.resume_in_memory_budget_scope(resume_value)
})
}
}
fn resume_in_memory_budget_scope(&mut self, resume_value: Option<Value>) -> Result<Value> {
let result = self
.resume_suspendable_segment(resume_value)
.and_then(|value| {
self.check_memory_budget()?;
Ok(value)
});
match result {
Ok(value) => {
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
if !self.ffi_memory_budget_scope_active
&& !matches!(self.execution_state, ExecutionState::Suspended { .. })
{
self.finish_active_memory_budget_execution();
}
Ok(value)
}
Err(err)
if matches!(
err,
VmError::MissingResumeValue { .. }
| VmError::UnexpectedResumeValue { .. }
| VmError::InvalidResumeState { .. }
) =>
{
Err(err)
}
Err(err) => {
let err = self.apply_memory_budget_precedence(err);
Err(self.fail_suspendable_execution(err))
}
}
}
fn resume_suspendable_segment(&mut self, resume_value: Option<Value>) -> Result<Value> {
let (reason, mut last_result) = match self.execution_state.clone() {
ExecutionState::Suspended {
reason,
last_result,
..
} => (reason, last_result),
current_state => {
return Err(VmError::InvalidResumeState {
state: alloc::format!("{:?}", current_state),
pc: self.pc,
});
}
};
match reason {
SuspendReason::HostAwait {
dest,
argument,
identifier,
} => {
let value = resume_value.ok_or_else(|| VmError::MissingResumeValue {
reason: SuspendReason::HostAwait {
dest,
argument: argument.clone(),
identifier: identifier.clone(),
},
pc: self.pc,
})?;
let dest_index = usize::from(dest);
if self.registers.len() <= dest_index {
let new_len = dest_index.saturating_add(1);
self.registers.resize(new_len, Value::Undefined);
}
if let Some(slot) = self.registers.get_mut(dest_index) {
*slot = value;
}
self.check_memory_budget()?;
}
other_reason => {
if resume_value.is_some() {
return Err(VmError::UnexpectedResumeValue {
reason: other_reason.clone(),
pc: self.pc,
});
}
}
}
self.execution_state = ExecutionState::Running;
self.restore_execution_timer_after_resume();
let program = self.program.clone();
self.run_stackless_loop(&program, &mut last_result)?;
if matches!(self.execution_state, ExecutionState::Suspended { .. }) {
Ok(last_result)
} else {
self.execution_stack.clear();
self.execution_state = ExecutionState::Completed {
result: last_result.clone(),
};
Ok(last_result)
}
}
fn run_stackless_from(&mut self, start_pc: usize) -> Result<Value> {
let program = self.program.clone();
self.execution_stack.clear();
self.execution_stack.push(ExecutionFrame::main(start_pc, 0));
let mut last_result = self.registers.first().cloned().unwrap_or(Value::Undefined);
self.run_stackless_loop(&program, &mut last_result)?;
if matches!(self.execution_state, ExecutionState::Suspended { .. }) {
Ok(last_result)
} else {
self.execution_stack.clear();
self.execution_state = ExecutionState::Completed {
result: last_result.clone(),
};
Ok(last_result)
}
}
fn run_stackless_loop(&mut self, program: &Program, last_result: &mut Value) -> Result<()> {
while !self.execution_stack.is_empty() {
#[cfg(debug_assertions)]
self.assert_vm_invariants();
self.memory_check()?;
self.frame_pc_overridden = false;
let should_finalize_rule = self.execution_stack.last().is_some_and(|frame| {
matches!(
frame.kind,
FrameKind::Rule(RuleFrameData {
phase: RuleFramePhase::Finalizing,
..
})
)
});
if should_finalize_rule {
let frame = self
.execution_stack
.pop()
.ok_or(VmError::MissingExecutionFrame {
context: "finalizing rule",
pc: self.pc,
})?;
self.finalize_rule_execution_frame(frame, last_result)?;
if self.execution_stack.is_empty() {
break;
}
continue;
}
let frame_pc = {
let frame = self
.execution_stack
.last()
.ok_or(VmError::MissingExecutionFrame {
context: "determining pc",
pc: self.pc,
})?;
frame.pc
};
if self.execution_mode == ExecutionMode::Suspendable
&& self.breakpoints.contains(&frame_pc)
{
self.pc = frame_pc;
let snapshot = (*last_result).clone();
self.execution_state = ExecutionState::Suspended {
reason: SuspendReason::Breakpoint { pc: frame_pc },
pc: frame_pc,
last_result: snapshot,
};
return Ok(());
}
if frame_pc >= program.instructions.len() {
let frame = self
.execution_stack
.pop()
.ok_or(VmError::MissingExecutionFrame {
context: "finalizing out-of-range pc",
pc: self.pc,
})?;
self.finalize_rule_execution_frame(frame, last_result)?;
if self.execution_stack.is_empty() {
break;
}
continue;
}
if self.executed_instructions >= self.max_instructions {
self.execution_state = ExecutionState::Error {
error: VmError::InstructionLimitExceeded {
limit: self.max_instructions,
executed: self.executed_instructions,
pc: frame_pc,
},
};
return Err(VmError::InstructionLimitExceeded {
limit: self.max_instructions,
executed: self.executed_instructions,
pc: frame_pc,
});
}
self.pc = frame_pc;
if let Err(err) = self.execution_timer_tick(1) {
self.execution_state = ExecutionState::Error { error: err.clone() };
return Err(err);
}
let instruction = program.instructions.get(self.pc).cloned().ok_or(
VmError::ProgramCounterOutOfBounds {
pc: self.pc,
instruction_count: program.instructions.len(),
},
)?;
if let Some(frame_info) = self.execution_stack.last() {
if let FrameKind::Comprehension { ref context, .. } = frame_info.kind {
if context.iteration_state.is_none()
&& frame_pc == usize::from(context.comprehension_end)
&& !matches!(instruction, Instruction::ComprehensionEnd { .. })
{
let resume_pc = frame_pc;
let _completed =
self.execution_stack
.pop()
.ok_or(VmError::MissingExecutionFrame {
context: "unwinding comprehension",
pc: self.pc,
})?;
if let Some(parent) = self.execution_stack.last_mut() {
parent.pc = resume_pc;
self.frame_pc_overridden = true;
}
continue;
}
}
}
self.executed_instructions = self.executed_instructions.saturating_add(1);
let stack_depth_before = self.execution_stack.len();
match self.execute_instruction(program, instruction) {
Ok(InstructionOutcome::Continue) => {
let stack_depth_after = self.execution_stack.len();
if stack_depth_after == stack_depth_before && !self.frame_pc_overridden {
if let Some(frame) = self.execution_stack.last_mut() {
frame.pc = self.pc.saturating_add(1);
}
}
if self.step_mode {
self.handle_instruction_suspend(SuspendReason::Step, &*last_result);
return Ok(());
}
}
Ok(InstructionOutcome::Return(value)) => {
self.handle_instruction_return(value, last_result)?;
if self.execution_stack.is_empty() {
break;
}
}
Ok(InstructionOutcome::Break) => {
self.handle_instruction_break(last_result)?;
if self.execution_stack.is_empty() {
break;
}
}
Ok(InstructionOutcome::Suspend { reason }) => {
self.handle_instruction_suspend(reason, last_result);
return Ok(());
}
Err(err) => {
if self.handle_instruction_error(err.clone(), last_result)? {
if self.execution_stack.is_empty() {
break;
}
} else {
self.execution_stack.clear();
return Err(err);
}
}
}
}
Ok(())
}
fn handle_instruction_suspend(&mut self, reason: SuspendReason, last_result: &Value) {
if let Some(frame) = self.execution_stack.last_mut() {
if !self.frame_pc_overridden && frame.pc <= self.pc {
frame.pc = self.pc.saturating_add(1);
}
}
self.snapshot_execution_timer_on_suspend();
self.execution_state = ExecutionState::Suspended {
reason,
pc: self.pc,
last_result: last_result.clone(),
};
}
fn handle_completed_frame_kind(&mut self, kind: FrameKind, last_result: &mut Value) {
match kind {
FrameKind::Main {
return_value_register,
} => {
*last_result = self
.registers
.get(usize::from(return_value_register))
.cloned()
.unwrap_or(Value::Undefined);
}
FrameKind::Loop { .. } | FrameKind::Comprehension { .. } => {
*last_result = self.registers.first().cloned().unwrap_or(Value::Undefined);
}
FrameKind::Rule(_) => {
*last_result = Value::Undefined;
}
}
}
fn handle_instruction_return(&mut self, value: Value, last_result: &mut Value) -> Result<()> {
loop {
let frame = match self.execution_stack.pop() {
Some(frame) => frame,
None => {
*last_result = value;
return Ok(());
}
};
match frame.kind {
FrameKind::Rule(mut data) => {
let result_index = usize::from(data.result_reg);
if self.registers.len() <= result_index {
let new_len = result_index.saturating_add(1);
self.registers.resize(new_len, Value::Undefined);
}
if let Some(slot) = self.registers.get_mut(result_index) {
*slot = value.clone();
}
data.accumulated_result = Some(value.clone());
data.any_body_succeeded = true;
let result = self.finalize_rule_frame_data(data)?;
*last_result = result.clone();
if let Some(parent_frame) = self.execution_stack.last_mut() {
parent_frame.pc = self.pc.saturating_add(1);
}
return Ok(());
}
FrameKind::Main {
return_value_register,
} => {
let ret_index = usize::from(return_value_register);
if self.registers.len() <= ret_index {
let new_len = ret_index.saturating_add(1);
self.registers.resize(new_len, Value::Undefined);
}
if let Some(slot) = self.registers.get_mut(ret_index) {
*slot = value.clone();
}
*last_result = value;
return Ok(());
}
FrameKind::Loop { return_pc, .. } | FrameKind::Comprehension { return_pc, .. } => {
if let Some(parent_frame) = self.execution_stack.last_mut() {
parent_frame.pc = return_pc;
}
}
}
}
}
fn handle_instruction_break(&mut self, last_result: &mut Value) -> Result<()> {
if let Some(frame) = self.execution_stack.pop() {
match frame.kind {
FrameKind::Rule(mut data) => {
let current_pc = frame.pc;
let next_pc = self.handle_rule_break_event(&mut data)?;
if let Some(pc) = next_pc {
self.execution_stack
.push(ExecutionFrame::new(pc, FrameKind::Rule(data)));
} else {
self.finalize_rule_execution_frame(
ExecutionFrame::new(current_pc, FrameKind::Rule(data)),
last_result,
)?;
}
}
other_kind => {
self.handle_break_non_rule(
ExecutionFrame::new(frame.pc, other_kind),
last_result,
);
}
}
}
Ok(())
}
fn handle_instruction_error(&mut self, err: VmError, last_result: &mut Value) -> Result<bool> {
if RegoVM::is_fatal_vm_error(&err) {
return Ok(false);
}
if let Some(frame) = self.execution_stack.pop() {
match frame.kind {
FrameKind::Rule(mut data) => {
let current_pc = frame.pc;
let next_pc = self.handle_rule_error_event(&mut data)?;
if let Some(pc) = next_pc {
self.execution_stack
.push(ExecutionFrame::new(pc, FrameKind::Rule(data)));
} else {
self.finalize_rule_execution_frame(
ExecutionFrame::new(current_pc, FrameKind::Rule(data)),
last_result,
)?;
}
return Ok(true);
}
other_kind => {
self.execution_stack
.push(ExecutionFrame::new(frame.pc, other_kind));
}
}
}
Ok(false)
}
fn handle_break_non_rule(&mut self, frame: ExecutionFrame, last_result: &mut Value) {
match frame.kind {
FrameKind::Main {
return_value_register,
} => {
*last_result = self
.registers
.get(usize::from(return_value_register))
.cloned()
.unwrap_or(Value::Undefined);
}
FrameKind::Loop { return_pc, .. } | FrameKind::Comprehension { return_pc, .. } => {
if let Some(parent_frame) = self.execution_stack.last_mut() {
parent_frame.pc = return_pc;
}
*last_result = self.registers.first().cloned().unwrap_or(Value::Undefined);
}
FrameKind::Rule(_) => {}
}
}
fn finalize_rule_execution_frame(
&mut self,
frame: ExecutionFrame,
last_result: &mut Value,
) -> Result<()> {
match frame.kind {
FrameKind::Rule(data) => {
let result = self.finalize_rule_frame_data(data)?;
*last_result = result.clone();
if let Some(parent_frame) = self.execution_stack.last_mut() {
parent_frame.pc = self.pc.saturating_add(1);
}
}
other_kind => {
self.handle_completed_frame_kind(other_kind, last_result);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::errors::VmError;
use super::{ExecutionMode, ExecutionState, Instruction, Program, RegoVM, Value};
use alloc::sync::Arc;
fn two_instruction_program() -> Arc<Program> {
let mut program = Program::new();
program.add_instruction(Instruction::LoadTrue { dest: 0 }, None);
program.add_instruction(Instruction::Return { value: 0 }, None);
Arc::new(program)
}
fn two_instruction_program_with_entry_point() -> Arc<Program> {
let mut program = Program::new();
program.add_instruction(Instruction::LoadTrue { dest: 0 }, None);
program.add_instruction(Instruction::Return { value: 0 }, None);
program.entry_points.insert("entry".into(), 0);
Arc::new(program)
}
fn host_await_program() -> Arc<Program> {
let mut program = Program::new();
program.add_instruction(Instruction::LoadInput { dest: 0 }, None);
program.add_instruction(Instruction::LoadNull { dest: 1 }, None);
program.add_instruction(
Instruction::HostAwait {
dest: 0,
arg: 0,
id: 1,
},
None,
);
program.add_instruction(Instruction::Return { value: 0 }, None);
Arc::new(program)
}
#[allow(clippy::expect_used)]
#[test]
fn execution_loops_check_memory_once_per_dispatched_instruction() {
let program = two_instruction_program();
for mode in [ExecutionMode::RunToCompletion, ExecutionMode::Suspendable] {
let mut vm = RegoVM::new();
vm.set_execution_mode(mode);
vm.load_program(program.clone());
assert_eq!(vm.execute().expect("execution succeeds"), Value::Bool(true));
assert_eq!(vm.executed_instructions, 2);
assert_eq!(vm.memory_check_count, vm.executed_instructions);
}
}
#[test]
fn instruction_budget_exact_boundaries_are_consistent_across_modes() {
let program = two_instruction_program();
let default_vm = RegoVM::new();
assert_eq!(default_vm.max_instructions, 25_000);
for mode in [ExecutionMode::RunToCompletion, ExecutionMode::Suspendable] {
for (limit, expected_executed, expected_pc) in [(0, 0, 0), (1, 1, 1)] {
let mut vm = RegoVM::new();
vm.set_execution_mode(mode);
vm.set_max_instructions(limit);
vm.load_program(program.clone());
assert_eq!(
vm.execute(),
Err(VmError::InstructionLimitExceeded {
limit,
executed: expected_executed,
pc: expected_pc,
})
);
}
for limit in [2, 3] {
let mut vm = RegoVM::new();
vm.set_execution_mode(mode);
vm.set_max_instructions(limit);
vm.load_program(program.clone());
assert_eq!(vm.execute(), Ok(Value::Bool(true)));
assert_eq!(vm.executed_instructions, 2);
}
}
}
#[test]
fn instruction_budget_entry_points_reset_consumption_in_both_modes() {
for mode in [ExecutionMode::RunToCompletion, ExecutionMode::Suspendable] {
for by_name in [true, false] {
let mut vm = RegoVM::new();
vm.set_execution_mode(mode);
vm.set_max_instructions(1);
vm.load_program(two_instruction_program_with_entry_point());
let first = if by_name {
vm.execute_entry_point_by_name("entry")
} else {
vm.execute_entry_point_by_index(0)
};
assert_eq!(
first,
Err(VmError::InstructionLimitExceeded {
limit: 1,
executed: 1,
pc: 1,
})
);
vm.set_max_instructions(2);
let second = if by_name {
vm.execute_entry_point_by_name("entry")
} else {
vm.execute_entry_point_by_index(0)
};
assert_eq!(second, Ok(Value::Bool(true)));
assert_eq!(vm.executed_instructions, 2);
}
}
}
#[test]
fn instruction_budget_counts_a_dispatched_failing_instruction() {
let mut program = Program::new();
program.add_instruction(Instruction::LoadTrue { dest: 0 }, None);
program.add_instruction(Instruction::LoadNull { dest: 1 }, None);
program.add_instruction(
Instruction::Lt {
dest: 2,
left: 0,
right: 1,
},
None,
);
let mut vm = RegoVM::new();
vm.set_strict_builtin_errors(true);
vm.load_program(Arc::new(program));
let result = vm.execute();
assert!(matches!(
result,
Err(VmError::ArithmeticError { pc: 2, .. })
));
assert_eq!(vm.executed_instructions, 3);
}
#[test]
fn load_program_resets_consumption_after_a_successful_execution() {
let program = two_instruction_program();
let mut vm = RegoVM::new();
vm.set_max_instructions(2);
vm.load_program(program.clone());
assert_eq!(vm.execute(), Ok(Value::Bool(true)));
assert_eq!(vm.executed_instructions, 2);
vm.load_program(program);
assert_eq!(vm.executed_instructions, 0);
assert_eq!(vm.max_instructions, 2);
assert_eq!(vm.execute(), Ok(Value::Bool(true)));
assert_eq!(vm.executed_instructions, 2);
}
#[test]
fn successful_execution_can_be_followed_by_a_fresh_execution() {
let program = two_instruction_program();
let mut vm = RegoVM::new();
vm.set_max_instructions(2);
vm.load_program(program);
assert_eq!(vm.execute(), Ok(Value::Bool(true)));
assert_eq!(vm.executed_instructions, 2);
assert_eq!(vm.execute(), Ok(Value::Bool(true)));
assert_eq!(vm.executed_instructions, 2);
}
#[test]
fn instruction_budget_rejects_the_next_suspendable_instruction_without_executing_it() {
let mut program = Program::new();
program.add_instruction(Instruction::LoadTrue { dest: 0 }, None);
program.add_instruction(Instruction::LoadFalse { dest: 1 }, None);
program.add_instruction(Instruction::Return { value: 0 }, None);
let mut vm = RegoVM::new();
vm.set_execution_mode(ExecutionMode::Suspendable);
vm.set_max_instructions(1);
vm.load_program(Arc::new(program));
assert_eq!(
vm.execute(),
Err(VmError::InstructionLimitExceeded {
limit: 1,
executed: 1,
pc: 1,
})
);
assert_eq!(vm.get_registers().get(1), Some(&Value::Undefined));
}
#[test]
fn instruction_budget_replacement_preserves_consumption_while_suspended() {
let program = host_await_program();
let mut lowered = RegoVM::new();
lowered.set_execution_mode(ExecutionMode::Suspendable);
lowered.set_max_instructions(3);
lowered.load_program(program.clone());
assert_eq!(lowered.execute(), Ok(Value::Undefined));
assert_eq!(lowered.executed_instructions, 3);
lowered.set_max_instructions(3);
assert_eq!(
lowered.resume(Some(Value::Bool(true))),
Err(VmError::InstructionLimitExceeded {
limit: 3,
executed: 3,
pc: 3,
})
);
assert!(matches!(
lowered.execution_state(),
ExecutionState::Error {
error: VmError::InstructionLimitExceeded { .. }
}
));
assert!(matches!(
lowered.resume(Some(Value::Bool(true))),
Err(VmError::InvalidResumeState { .. })
));
lowered.set_max_instructions(4);
lowered.set_input(Value::String("second".into()));
assert_eq!(lowered.execute(), Ok(Value::Undefined));
assert_eq!(
lowered.get_host_await_argument(),
Some(&Value::String("second".into()))
);
assert_eq!(
lowered.resume(Some(Value::Bool(true))),
Ok(Value::Bool(true))
);
let mut lowered_below = RegoVM::new();
lowered_below.set_execution_mode(ExecutionMode::Suspendable);
lowered_below.set_max_instructions(3);
lowered_below.load_program(host_await_program());
assert_eq!(lowered_below.execute(), Ok(Value::Undefined));
lowered_below.set_max_instructions(2);
assert_eq!(
lowered_below.resume(Some(Value::Bool(true))),
Err(VmError::InstructionLimitExceeded {
limit: 2,
executed: 3,
pc: 3,
})
);
let mut raised = RegoVM::new();
raised.set_execution_mode(ExecutionMode::Suspendable);
raised.set_max_instructions(3);
raised.load_program(program);
assert_eq!(raised.execute(), Ok(Value::Undefined));
raised.set_max_instructions(4);
assert_eq!(
raised.resume(Some(Value::Bool(true))),
Ok(Value::Bool(true))
);
assert_eq!(raised.executed_instructions, 4);
}
}