mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Encode VM stack/context/register lifecycle invariants as debug_assert!s. Zero cost in release; surfaces violations during debug-mode tests and CI. Invariants covered: - reset_execution_state postcondition: all stacks empty, registers resized to base and Undefined, rule_cache reset, pc/executed counters zeroed, builtins_cache cleared, execution_state Ready. - Per-opcode invariant check (assert_vm_invariants) invoked at the top of run_stackless_loop and jump_to iterations: state is Ready/Running, registers non-empty, rule_cache sized to program, execution stack bounded by a debug-only sanity ceiling (DEBUG_MAX_EXECUTION_STACK_DEPTH = 4096; not a production limit). - resume() precondition: execution_state is Suspended. - execute_suspendable_entry precondition: clean state (callers reset immediately before). - Rule finalize: call_rule_stack pop matches the finalized rule_index. - IterationState::advance: Single iterator not advanced past consumption, Array index not at usize::MAX before saturating_add. All assertions are gated by #[cfg(debug_assertions)] (directly or via debug_assert!) so release builds are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
345 lines
10 KiB
Rust
345 lines
10 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
use super::execution_model::SuspendReason;
|
|
use crate::value::Value;
|
|
use alloc::string::String;
|
|
use alloc::vec::Vec;
|
|
use core::time::Duration;
|
|
use thiserror::Error;
|
|
|
|
/// VM execution errors
|
|
#[derive(Error, Debug, Clone, PartialEq)]
|
|
pub enum VmError {
|
|
#[error("Execution stopped: exceeded maximum instruction limit of {limit} after {executed} instructions (pc={pc})")]
|
|
InstructionLimitExceeded {
|
|
limit: usize,
|
|
executed: usize,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Execution exceeded time limit (elapsed={elapsed:?}, limit={limit:?}, pc={pc})")]
|
|
TimeLimitExceeded {
|
|
elapsed: Duration,
|
|
limit: Duration,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Execution exceeded memory limit (usage={usage} bytes, limit={limit} bytes, pc={pc})")]
|
|
MemoryLimitExceeded { usage: u64, limit: u64, pc: usize },
|
|
|
|
#[error("Compiled regex exceeded size limit ({limit} bytes, pc={pc})")]
|
|
RegexSizeLimitExceeded { limit: usize, pc: usize },
|
|
|
|
#[error("Literal index {index} out of bounds (pc={pc})")]
|
|
LiteralIndexOutOfBounds { index: u16, pc: usize },
|
|
|
|
#[error("Register {register} does not contain an object (value={value:?}, pc={pc})")]
|
|
RegisterNotObject {
|
|
register: u8,
|
|
value: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("ObjectCreate: template is not an object (pc={pc}, template={template:?})")]
|
|
ObjectCreateInvalidTemplate { template: Value, pc: usize },
|
|
|
|
#[error("Register {register} does not contain an array (value={value:?}, pc={pc})")]
|
|
RegisterNotArray {
|
|
register: u8,
|
|
value: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Register {register} does not contain a set (value={value:?}, pc={pc})")]
|
|
RegisterNotSet {
|
|
register: u8,
|
|
value: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Register index {index} out of bounds (pc={pc}, register_count={register_count})")]
|
|
RegisterIndexOutOfBounds {
|
|
index: u8,
|
|
pc: usize,
|
|
register_count: usize,
|
|
},
|
|
|
|
#[error("Rule index {index} out of bounds (pc={pc}, available={available})")]
|
|
RuleIndexOutOfBounds {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Rule index {index} has no info (pc={pc}, available={available})")]
|
|
RuleInfoMissing {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid object create params index: {index} (pc={pc}, available={available})")]
|
|
InvalidObjectCreateParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid template literal index: {index} (pc={pc}, available={available})")]
|
|
InvalidTemplateLiteralIndex {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid chained index params index: {index} (pc={pc}, available={available})")]
|
|
InvalidChainedIndexParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid array create params index: {index} (pc={pc}, available={available})")]
|
|
InvalidArrayCreateParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid set create params index: {index} (pc={pc}, available={available})")]
|
|
InvalidSetCreateParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid virtual data document lookup params index: {index} (pc={pc}, available={available})")]
|
|
InvalidVirtualDataDocumentLookupParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid comprehension start params index: {index} (pc={pc}, available={available})")]
|
|
InvalidComprehensionBeginParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid loop params index: {index} (pc={pc}, available={available})")]
|
|
InvalidLoopParams {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid rule index: {rule_index:?} (pc={pc})")]
|
|
InvalidRuleIndex { rule_index: Value, pc: usize },
|
|
|
|
#[error("Invalid rule tree entry: {value:?} (pc={pc})")]
|
|
InvalidRuleTreeEntry { value: Value, pc: usize },
|
|
|
|
#[error("Builtin function expects exactly {expected} arguments, got {actual} (pc={pc})")]
|
|
BuiltinArgumentMismatch {
|
|
expected: u16,
|
|
actual: usize,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Builtin function not resolved: {name} (pc={pc})")]
|
|
BuiltinNotResolved { name: String, pc: usize },
|
|
|
|
#[error("Cannot add {left:?} and {right:?} (pc={pc})")]
|
|
InvalidAddition {
|
|
left: Value,
|
|
right: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Cannot subtract {left:?} and {right:?} (pc={pc})")]
|
|
InvalidSubtraction {
|
|
left: Value,
|
|
right: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Cannot multiply {left:?} and {right:?} (pc={pc})")]
|
|
InvalidMultiplication {
|
|
left: Value,
|
|
right: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Cannot divide {left:?} and {right:?} (pc={pc})")]
|
|
InvalidDivision {
|
|
left: Value,
|
|
right: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("modulo on floating-point number (left={left:?}, right={right:?}, pc={pc})")]
|
|
ModuloOnFloat {
|
|
left: Value,
|
|
right: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Cannot modulo {left:?} and {right:?} (pc={pc})")]
|
|
InvalidModulo {
|
|
left: Value,
|
|
right: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Cannot iterate over {value:?} (pc={pc})")]
|
|
InvalidIteration { value: Value, pc: usize },
|
|
|
|
#[error("HostAwait executed but no response provided for destination register {dest} (id: {identifier:?}, pc={pc})")]
|
|
HostAwaitResponseMissing {
|
|
dest: u8,
|
|
identifier: Value,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Assertion failed (pc={pc})")]
|
|
AssertionFailed { pc: usize },
|
|
|
|
#[error("Rule-data conflict: {message} (pc={pc})")]
|
|
RuleDataConflict { message: String, pc: usize },
|
|
|
|
#[error("Arithmetic error: {message} (pc={pc})")]
|
|
ArithmeticError { message: String, pc: usize },
|
|
|
|
#[error("Entry point index {index} out of bounds (max: {max_index}, pc={pc})")]
|
|
InvalidEntryPointIndex {
|
|
index: usize,
|
|
max_index: usize,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Entry point '{name}' not found (pc={pc}). Available entry points: {available:?}")]
|
|
EntryPointNotFound {
|
|
name: String,
|
|
available: Vec<String>,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Entry point PC {pc} >= instruction count {instruction_count} for entry point '{entry_point}'")]
|
|
EntryPointPcOutOfBounds {
|
|
pc: usize,
|
|
instruction_count: usize,
|
|
entry_point: String,
|
|
},
|
|
|
|
#[error("Register count {register_count} below base count {base_count} (pc={pc})")]
|
|
RegisterCountBelowBase {
|
|
register_count: usize,
|
|
base_count: usize,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Program counter {pc} out of bounds for instruction count {instruction_count}")]
|
|
ProgramCounterOutOfBounds { pc: usize, instruction_count: usize },
|
|
|
|
#[error("Rule cache size {cache_size} != rule info count {rule_info_count} (pc={pc})")]
|
|
RuleCacheSizeMismatch {
|
|
cache_size: usize,
|
|
rule_info_count: usize,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Suspend reason {reason:?} is not supported in run-to-completion execution (pc={pc})")]
|
|
UnsupportedSuspendInRunToCompletion { reason: SuspendReason, pc: usize },
|
|
|
|
#[error("Cannot resume VM when execution state is {state} (pc={pc})")]
|
|
InvalidResumeState { state: String, pc: usize },
|
|
|
|
#[error("HostAwait suspension requires a resume value for reason {reason:?} (pc={pc})")]
|
|
MissingResumeValue { reason: SuspendReason, pc: usize },
|
|
|
|
#[error("Unexpected resume value supplied for reason {reason:?} (pc={pc})")]
|
|
UnexpectedResumeValue { reason: SuspendReason, pc: usize },
|
|
|
|
#[error("Missing execution frame: {context} (pc={pc})")]
|
|
MissingExecutionFrame { context: &'static str, pc: usize },
|
|
|
|
#[error("Unhandled instruction variant: {instruction} (pc={pc})")]
|
|
UnhandledInstruction { instruction: String, pc: usize },
|
|
|
|
#[error("Invalid function call params index: {index} (pc={pc}, available={available})")]
|
|
InvalidFunctionCallParamsIndex {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid builtin call params index: {index} (pc={pc}, available={available})")]
|
|
InvalidBuiltinCallParamsIndex {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Invalid builtin info index: {index} (pc={pc}, available={available})")]
|
|
InvalidBuiltinInfoIndex {
|
|
index: u16,
|
|
pc: usize,
|
|
available: usize,
|
|
},
|
|
|
|
#[error("Rule frame has no initial PC (pc={pc})")]
|
|
RuleFrameMissingInitialPc { pc: usize },
|
|
|
|
#[error("Call rule stack underflow during rule finalization (pc={pc})")]
|
|
CallRuleStackUnderflow { pc: usize },
|
|
|
|
#[error("Call rule stack mismatch during rule finalization: expected rule_index {expected}, popped {actual} (pc={pc})")]
|
|
CallRuleStackMismatch {
|
|
expected: u16,
|
|
actual: u16,
|
|
pc: usize,
|
|
},
|
|
|
|
#[error("Internal VM error: {message} (pc={pc})")]
|
|
Internal { message: String, pc: usize },
|
|
}
|
|
|
|
impl From<anyhow::Error> for VmError {
|
|
fn from(err: anyhow::Error) -> Self {
|
|
// Preserve LimitError identity so that resource-limit violations are
|
|
// never silently swallowed to Undefined in non-strict mode.
|
|
// Note: pc is set to 0 because this conversion lacks instruction context.
|
|
// The error message itself (which includes the limit value) provides
|
|
// sufficient diagnostic information for users.
|
|
if let Some(limit_err) = err.downcast_ref::<crate::LimitError>() {
|
|
return match *limit_err {
|
|
crate::LimitError::TimeLimitExceeded { elapsed, limit } => {
|
|
VmError::TimeLimitExceeded {
|
|
elapsed,
|
|
limit,
|
|
pc: 0,
|
|
}
|
|
}
|
|
crate::LimitError::MemoryLimitExceeded { usage, limit } => {
|
|
VmError::MemoryLimitExceeded {
|
|
usage,
|
|
limit,
|
|
pc: 0,
|
|
}
|
|
}
|
|
crate::LimitError::RegexSizeLimitExceeded { limit } => {
|
|
VmError::RegexSizeLimitExceeded { limit, pc: 0 }
|
|
}
|
|
};
|
|
}
|
|
VmError::ArithmeticError {
|
|
message: alloc::format!("{}", err),
|
|
pc: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type Result<T> = core::result::Result<T, VmError>;
|