Skip to main content

miden_processor/
errors.rs

1// Allow unused assignments - required by miette::Diagnostic derive macro
2#![allow(unused_assignments)]
3
4use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec};
5
6use miden_core::program::MIN_STACK_DEPTH;
7use miden_debug_types::{Location, SourceFile, SourceSpan};
8use miden_mast_package::{
9    PackageDebugInfoError,
10    debug_info::{DebugSourceNodeId, PackageDebugInfo},
11};
12use miden_utils_diagnostics::{Diagnostic, miette};
13
14use crate::{
15    BaseHost, ContextId, Felt, Word,
16    advice::AdviceError,
17    event::{EventError, EventId, EventName},
18    fast::SystemEventError,
19    utils::to_hex,
20};
21
22// EXECUTION ERROR
23// ================================================================================================
24
25#[derive(Debug, thiserror::Error, Diagnostic)]
26pub enum ExecutionError {
27    #[error("failed to execute arithmetic circuit evaluation operation: {error}")]
28    #[diagnostic()]
29    AceChipError {
30        #[label("this call failed")]
31        label: SourceSpan,
32        #[source_code]
33        source_file: Option<Arc<SourceFile>>,
34        error: AceError,
35    },
36    #[error("{err}")]
37    #[diagnostic(forward(err))]
38    AdviceError {
39        #[label]
40        label: SourceSpan,
41        #[source_code]
42        source_file: Option<Arc<SourceFile>>,
43        err: AdviceError,
44    },
45    #[error("exceeded the allowed number of max cycles {0}")]
46    CycleLimitExceeded(u32),
47    #[error("error during processing of event {}", match event_name {
48        Some(name) => format!("'{name}' (ID: {event_id})"),
49        None => format!("with ID: {event_id}"),
50    })]
51    #[diagnostic()]
52    EventError {
53        #[label]
54        label: SourceSpan,
55        #[source_code]
56        source_file: Option<Arc<SourceFile>>,
57        event_id: EventId,
58        event_name: Option<EventName>,
59        #[source]
60        error: EventError,
61    },
62    #[error("failed to execute the program for internal reason: {0}")]
63    Internal(&'static str),
64    #[error("operand stack depth {depth} exceeds the maximum of {max}")]
65    StackDepthLimitExceeded { depth: usize, max: usize },
66    /// This means trace generation would go over the configured row limit.
67    ///
68    /// In parallel trace building, this is used for core-row prechecks and chiplet overflow.
69    #[error("trace length exceeded the maximum of {0} rows")]
70    TraceLenExceeded(usize),
71    /// Memory error with source context for diagnostics.
72    ///
73    /// Use `MemoryResultExt::map_mem_err` to convert `Result<T, MemoryError>` with context.
74    #[error("{err}")]
75    #[diagnostic(forward(err))]
76    MemoryError {
77        #[label]
78        label: SourceSpan,
79        #[source_code]
80        source_file: Option<Arc<SourceFile>>,
81        err: MemoryError,
82    },
83    /// Memory error without source context (for internal operations like FMP initialization).
84    ///
85    /// Use `ExecutionError::MemoryErrorNoCtx` for memory errors that don't have error context
86    /// available (e.g., during call/syscall context initialization).
87    #[error(transparent)]
88    #[diagnostic(transparent)]
89    MemoryErrorNoCtx(MemoryError),
90    #[error("{err}")]
91    #[diagnostic(forward(err))]
92    OperationError {
93        #[label]
94        label: SourceSpan,
95        #[source_code]
96        source_file: Option<Arc<SourceFile>>,
97        err: OperationError,
98    },
99    #[error("stack should have at most {MIN_STACK_DEPTH} elements at the end of program execution, but had {} elements", MIN_STACK_DEPTH + .0)]
100    OutputStackOverflow(usize),
101    #[error("procedure with root digest {root_digest} could not be found")]
102    #[diagnostic()]
103    ProcedureNotFound {
104        #[label]
105        label: SourceSpan,
106        #[source_code]
107        source_file: Option<Arc<SourceFile>>,
108        root_digest: Word,
109    },
110    #[error("failed to generate STARK proof: {0}")]
111    ProvingError(String),
112    #[error(transparent)]
113    HostError(#[from] HostError),
114    #[error(transparent)]
115    PackageDebugInfoError(#[from] PackageDebugInfoError),
116}
117
118impl ExecutionError {
119    /// Wraps an advice error without source-location context.
120    pub fn advice_error_no_context(err: AdviceError) -> Self {
121        Self::AdviceError {
122            label: SourceSpan::UNKNOWN,
123            source_file: None,
124            err,
125        }
126    }
127}
128
129impl AsRef<dyn Diagnostic> for ExecutionError {
130    fn as_ref(&self) -> &(dyn Diagnostic + 'static) {
131        self
132    }
133}
134
135// ACE ERROR
136// ================================================================================================
137
138#[derive(Debug, thiserror::Error)]
139#[error("ace circuit evaluation failed: {0}")]
140pub struct AceError(pub String);
141
142// ACE EVAL ERROR
143// ================================================================================================
144
145/// Context-free error type for ACE circuit evaluation operations.
146///
147/// This enum wraps errors from ACE evaluation and memory subsystems without
148/// carrying source location context. Context is added at the call site via
149/// `AceEvalResultExt::map_ace_eval_err`.
150#[derive(Debug, thiserror::Error)]
151pub enum AceEvalError {
152    #[error(transparent)]
153    Ace(#[from] AceError),
154    #[error(transparent)]
155    Memory(#[from] MemoryError),
156}
157
158// HOST ERROR
159// ================================================================================================
160
161/// Error type for host-related operations.
162#[derive(Debug, thiserror::Error)]
163pub enum HostError {
164    #[error("attempted to add event handler for '{event}' (already registered)")]
165    DuplicateEventHandler { event: EventName },
166    #[error("attempted to add event handler for '{event}' (reserved system event)")]
167    ReservedEventNamespace { event: EventName },
168}
169
170// IO ERROR
171// ================================================================================================
172
173/// Context-free error type for IO operations.
174///
175/// This enum wraps errors from the advice provider and memory subsystems without
176/// carrying source location context. Context is added at the call site via
177/// `IoResultExt::map_io_err`.
178#[derive(Debug, thiserror::Error, Diagnostic)]
179pub enum IoError {
180    #[error(transparent)]
181    Advice(#[from] AdviceError),
182    #[error(transparent)]
183    Memory(#[from] MemoryError),
184    #[error(transparent)]
185    #[diagnostic(transparent)]
186    Operation(#[from] OperationError),
187    /// Stack operation error (increment/decrement size failures).
188    ///
189    /// These are internal execution errors that don't need additional context
190    /// since they already carry their own error information.
191    #[error(transparent)]
192    #[diagnostic(transparent)]
193    Execution(Box<ExecutionError>),
194}
195
196impl From<ExecutionError> for IoError {
197    fn from(err: ExecutionError) -> Self {
198        IoError::Execution(Box::new(err))
199    }
200}
201
202// MEMORY ERROR
203// ================================================================================================
204
205/// Lightweight error type for memory operations.
206///
207/// This enum captures error conditions without expensive context information (no source location,
208/// no file references). When a `MemoryError` propagates up to become an `ExecutionError`, the
209/// context is resolved lazily via `MapExecErr::map_exec_err`.
210#[derive(Debug, thiserror::Error, Diagnostic)]
211pub enum MemoryError {
212    #[error("memory address cannot exceed 2^32 but was {addr}")]
213    AddressOutOfBounds { addr: u64 },
214    #[error(
215        "memory address {addr} in context {ctx} was read and written, or written twice, in the same clock cycle {clk}"
216    )]
217    IllegalMemoryAccess { ctx: ContextId, addr: u32, clk: Felt },
218    #[error(
219        "memory range start address cannot exceed end address, but was ({start_addr}, {end_addr})"
220    )]
221    InvalidMemoryRange { start_addr: u64, end_addr: u64 },
222    #[error(
223        "word access at memory address {addr} in context {ctx} is unaligned: word accesses require addresses that are multiples of 4"
224    )]
225    UnalignedWordAccess { addr: u32, ctx: ContextId },
226    #[error("failed to read from memory: {0}")]
227    MemoryReadFailed(String),
228    #[error(
229        "writing to memory address {addr} in context {ctx} would exceed the maximum number of memory elements {max}"
230    )]
231    #[diagnostic(help(
232        "increase the limit via `ExecutionOptions::with_max_memory_elements`, or reduce the number of distinct memory addresses the program writes to"
233    ))]
234    MemoryElementLimitExceeded { ctx: ContextId, addr: u32, max: usize },
235}
236
237// CRYPTO ERROR
238// ================================================================================================
239
240/// Context-free error type for cryptographic operations (Merkle path verification, updates).
241///
242/// This enum wraps errors from the advice provider and operation subsystems without carrying
243/// source location context. Context is added at the call site via
244/// `CryptoResultExt::map_crypto_err`.
245#[derive(Debug, thiserror::Error, Diagnostic)]
246pub enum CryptoError {
247    #[error(transparent)]
248    Advice(#[from] AdviceError),
249    #[error(transparent)]
250    #[diagnostic(transparent)]
251    Operation(#[from] OperationError),
252}
253
254// OPERATION ERROR
255// ================================================================================================
256
257/// Lightweight error type for operations that can fail.
258///
259/// This enum captures error conditions without expensive context information (no source location,
260/// no file references). When an `OperationError` propagates up to become an `ExecutionError`, the
261/// context is resolved lazily via extension traits like `OperationResultExt::map_exec_err`.
262///
263/// # Adding new errors (for contributors)
264///
265/// **Use `OperationError` when:**
266/// - The error occurs during operation execution (e.g., assertion failures, type mismatches)
267/// - Context can be resolved at the call site via the extension traits
268/// - The error needs both a human-readable message and optional diagnostic help
269///
270/// **Avoid duplicating error context.** Context is added by the extension traits,
271/// so do NOT add `label` or `source_file` fields to the variant.
272///
273/// **Pattern at call sites:**
274/// ```ignore
275/// // Return OperationError and let the caller wrap it:
276/// fn some_op() -> Result<(), OperationError> {
277///     Err(OperationError::DivideByZero)
278/// }
279///
280/// // Caller wraps with context lazily:
281/// some_op().map_exec_err()?;
282/// ```
283///
284/// For wrapper errors (`AdviceError`, `EventError`, `AceError`), use the corresponding extension
285/// traits (`AdviceResultExt`, `AceResultExt`) or helper functions (`advice_error_with_context`,
286/// `event_error_with_context`).
287#[derive(Debug, Clone, thiserror::Error, Diagnostic)]
288pub enum OperationError {
289    #[error("external node with mast root {0} resolved to an external node")]
290    CircularExternalNode(Word),
291    #[error("division by zero: divisor must be non-zero for division or modulo operations")]
292    DivideByZero,
293    #[error(
294        "assertion failed with error {}",
295        match err_msg {
296            Some(msg) => format!("message: {msg}"),
297            None => format!("code: {err_code}"),
298        }
299    )]
300    FailedAssertion {
301        err_code: Felt,
302        err_msg: Option<Arc<str>>,
303    },
304    #[error(
305        "u32 assertion failed: u32assert2 requires both stack values to be valid 32-bit unsigned integers; error {}; invalid values: {invalid_values:?}",
306        match err_msg {
307            Some(msg) => format!("message: {msg}"),
308            None => format!("code: {err_code}"),
309        }
310    )]
311    U32AssertionFailed {
312        err_code: Felt,
313        err_msg: Option<Arc<str>>,
314        invalid_values: Vec<Felt>,
315    },
316    #[error("FRI operation failed: {0}")]
317    FriError(String),
318    #[error(
319        "invalid crypto operation: Merkle path length {path_len} does not match expected depth {depth}"
320    )]
321    InvalidMerklePathLength { path_len: usize, depth: Felt },
322    #[error("when returning from a call, stack depth must be {MIN_STACK_DEPTH}, but was {depth}")]
323    InvalidStackDepthOnReturn { depth: usize },
324    #[error("ilog2 requires a non-zero argument")]
325    LogArgumentZero,
326    #[error(
327        "MAST forest in host indexed by procedure root {root_digest} doesn't contain that root"
328    )]
329    MalformedMastForestInHost { root_digest: Word },
330    #[error("merkle path verification failed for value {value} at index {index} in the Merkle tree with root {root} (error {err})",
331      value = to_hex(inner.value.as_bytes()),
332      root = to_hex(inner.root.as_bytes()),
333      index = inner.index,
334      err = match &inner.err_msg {
335        Some(msg) => format!("message: {msg}"),
336        None => format!("code: {}", inner.err_code),
337      }
338    )]
339    MerklePathVerificationFailed {
340        inner: Box<MerklePathVerificationFailedInner>,
341    },
342    #[error("{message}, but got {value}", message = context.message())]
343    NotBinaryValue {
344        context: BinaryValueErrorContext,
345        value: Felt,
346    },
347    #[error("operation expected u32 values, but got values: {values:?}")]
348    NotU32Values { values: Vec<Felt> },
349    #[error("syscall failed: procedure with root {proc_root} was not found in the kernel")]
350    SyscallTargetNotInKernel { proc_root: Word },
351    #[error("failed to execute the operation for internal reason: {0}")]
352    Internal(&'static str),
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum BinaryValueErrorContext {
357    Operation,
358    If,
359    Loop,
360}
361
362impl BinaryValueErrorContext {
363    const fn message(self) -> &'static str {
364        match self {
365            Self::Operation => "operation expected a binary value",
366            Self::If => "if statement expected a binary value on top of the stack",
367            Self::Loop => {
368                "loop condition must be a binary value on entry and each subsequent iteration"
369            },
370        }
371    }
372}
373
374impl OperationError {
375    /// Wraps this error with execution context to produce an `ExecutionError`.
376    ///
377    /// This is useful when working with `ControlFlow` or other non-`Result` return types
378    /// where the `OperationResultExt::map_exec_err` extension trait cannot be used directly.
379    pub fn with_context(self) -> ExecutionError {
380        let (label, source_file) = get_label_and_source_file();
381        ExecutionError::OperationError { label, source_file, err: self }
382    }
383
384    /// Wraps this error with package-owned source-occurrence execution context.
385    ///
386    /// Unlike [`Self::with_context`], this resolves source metadata from package debug sections
387    /// keyed by a source/debug MAST occurrence rather than by the reduced execution MAST node.
388    pub fn with_package_source_context(
389        self,
390        context: PackageSourceDebugContext<'_>,
391        host: &(dyn BaseHost + '_),
392        op_idx: Option<usize>,
393    ) -> ExecutionError {
394        let (label, source_file) =
395            label_and_source_file_from_location(context.assembly_location(op_idx), host);
396        ExecutionError::OperationError {
397            label,
398            source_file,
399            err: self.with_package_debug_info(context.debug_info()),
400        }
401    }
402
403    fn with_package_debug_info(self, debug_info: &PackageDebugInfo) -> Self {
404        match self {
405            Self::FailedAssertion { err_code, err_msg: None } => Self::FailedAssertion {
406                err_msg: debug_info.error_message(err_code.as_canonical_u64()),
407                err_code,
408            },
409            Self::U32AssertionFailed { err_code, err_msg: None, invalid_values } => {
410                Self::U32AssertionFailed {
411                    err_msg: debug_info.error_message(err_code.as_canonical_u64()),
412                    err_code,
413                    invalid_values,
414                }
415            },
416            Self::MerklePathVerificationFailed { mut inner } if inner.err_msg.is_none() => {
417                inner.err_msg = debug_info.error_message(inner.err_code.as_canonical_u64());
418                Self::MerklePathVerificationFailed { inner }
419            },
420            err => err,
421        }
422    }
423}
424
425/// Inner data for `OperationError::MerklePathVerificationFailed`.
426///
427/// Boxed to reduce the size of `OperationError`.
428#[derive(Debug, Clone)]
429pub struct MerklePathVerificationFailedInner {
430    pub value: Word,
431    pub index: Felt,
432    pub root: Word,
433    pub err_code: Felt,
434    pub err_msg: Option<Arc<str>>,
435}
436
437// EXTENSION TRAITS
438// ================================================================================================
439
440/// Source-occurrence debug context decoded from package debug sections.
441///
442/// This keeps diagnostic lookup keyed by [`DebugSourceNodeId`] so two source occurrences that
443/// reduce to the same executable MAST node can still report distinct source locations.
444#[derive(Clone, Copy, Debug)]
445pub struct PackageSourceDebugContext<'a> {
446    debug_info: &'a PackageDebugInfo,
447    source_node_id: Option<DebugSourceNodeId>,
448}
449
450impl<'a> PackageSourceDebugContext<'a> {
451    /// Creates a source debug context for one package-owned source/debug MAST occurrence.
452    pub fn new(debug_info: &'a PackageDebugInfo, source_node_id: DebugSourceNodeId) -> Self {
453        Self {
454            debug_info,
455            source_node_id: Some(source_node_id),
456        }
457    }
458
459    /// Creates a package debug context when source location metadata may be unavailable.
460    pub(crate) fn new_optional(
461        debug_info: &'a PackageDebugInfo,
462        source_node_id: Option<DebugSourceNodeId>,
463    ) -> Self {
464        Self { debug_info, source_node_id }
465    }
466
467    /// Returns the source/debug MAST occurrence associated with this context, if known.
468    pub fn source_node_id(&self) -> Option<DebugSourceNodeId> {
469        self.source_node_id
470    }
471
472    /// Returns the package debug info backing this context.
473    pub fn debug_info(&self) -> &'a PackageDebugInfo {
474        self.debug_info
475    }
476
477    /// Returns source location metadata for `op_idx`, if present.
478    ///
479    /// If `op_idx` is absent, this falls back to the first operation row for the source occurrence.
480    pub fn assembly_location(&self, op_idx: Option<usize>) -> Option<&'a Location> {
481        let source_node_id = self.source_node_id?;
482        let assembly_op = match op_idx {
483            Some(op_idx) => u32::try_from(op_idx)
484                .ok()
485                .and_then(|op_idx| self.debug_info.asm_op_for_operation(source_node_id, op_idx)),
486            None => self.debug_info.first_asm_op_for_source_node(source_node_id),
487        }?;
488
489        assembly_op.location.as_ref()
490    }
491}
492
493fn label_and_source_file_from_location(
494    location: Option<&Location>,
495    host: &(dyn BaseHost + '_),
496) -> (SourceSpan, Option<Arc<SourceFile>>) {
497    location.map_or_else(
498        || (SourceSpan::UNKNOWN, None),
499        |location| host.get_label_and_source_file(location),
500    )
501}
502
503/// Computes the label and source file for error context.
504///
505/// This function is called by the extension traits to compute source location
506/// only when an error occurs. Since errors are rare, the cost of source metadata lookup is
507/// acceptable.
508fn get_label_and_source_file() -> (SourceSpan, Option<Arc<SourceFile>>) {
509    (SourceSpan::UNKNOWN, None)
510}
511
512/// Wraps an `AdviceError` with execution context to produce an `ExecutionError`.
513///
514/// This is useful when working with `ControlFlow` or other non-`Result` return types
515/// where the extension traits cannot be used directly.
516pub fn advice_error_with_context(err: AdviceError) -> ExecutionError {
517    let (label, source_file) = get_label_and_source_file();
518    ExecutionError::AdviceError { label, source_file, err }
519}
520
521/// Wraps an `AdviceError` with package-owned source-occurrence execution context.
522pub fn advice_error_with_package_source_context(
523    err: AdviceError,
524    context: PackageSourceDebugContext<'_>,
525    host: &(dyn BaseHost + '_),
526    op_idx: Option<usize>,
527) -> ExecutionError {
528    let (label, source_file) =
529        label_and_source_file_from_location(context.assembly_location(op_idx), host);
530    ExecutionError::AdviceError { label, source_file, err }
531}
532
533/// Wraps an `EventError` with execution context to produce an `ExecutionError`.
534///
535/// This is useful when working with `ControlFlow` or other non-`Result` return types
536/// where an extension trait on `Result` cannot be used directly.
537pub fn event_error_with_context(
538    error: EventError,
539    event_id: EventId,
540    event_name: Option<EventName>,
541) -> ExecutionError {
542    let (label, source_file) = get_label_and_source_file();
543    ExecutionError::EventError {
544        label,
545        source_file,
546        event_id,
547        event_name,
548        error,
549    }
550}
551
552/// Wraps an `EventError` with package-owned source-occurrence execution context.
553pub fn event_error_with_package_source_context(
554    error: EventError,
555    context: PackageSourceDebugContext<'_>,
556    host: &(dyn BaseHost + '_),
557    op_idx: Option<usize>,
558    event_id: EventId,
559    event_name: Option<EventName>,
560) -> ExecutionError {
561    let (label, source_file) =
562        label_and_source_file_from_location(context.assembly_location(op_idx), host);
563    ExecutionError::EventError {
564        label,
565        source_file,
566        event_id,
567        event_name,
568        error,
569    }
570}
571
572/// Creates a `ProcedureNotFound` error with execution context.
573pub fn procedure_not_found_with_context(root_digest: Word) -> ExecutionError {
574    let (label, source_file) = get_label_and_source_file();
575    ExecutionError::ProcedureNotFound { label, source_file, root_digest }
576}
577
578/// Creates a `ProcedureNotFound` error with package-owned source-occurrence execution context.
579pub fn procedure_not_found_with_package_source_context(
580    root_digest: Word,
581    context: PackageSourceDebugContext<'_>,
582    host: &(dyn BaseHost + '_),
583) -> ExecutionError {
584    let (label, source_file) =
585        label_and_source_file_from_location(context.assembly_location(None), host);
586    ExecutionError::ProcedureNotFound { label, source_file, root_digest }
587}
588
589/// Creates a `MalformedMastForestInHost` operation error with execution context.
590pub fn malformed_mast_forest_with_context(
591    root_digest: Word,
592    context: Option<PackageSourceDebugContext<'_>>,
593    host: &(dyn BaseHost + '_),
594) -> ExecutionError {
595    let err = OperationError::MalformedMastForestInHost { root_digest };
596    match context {
597        Some(context) => err.with_package_source_context(context, host, None),
598        None => err.with_context(),
599    }
600}
601
602// CONSOLIDATED EXTENSION TRAITS (plafer's approach)
603// ================================================================================================
604//
605// Three traits organized by method signature rather than by error type:
606// 1. MapExecErr - for errors with basic context
607// 2. MapExecErrWithOpIdx - for errors in basic blocks that may need op_idx
608// 3. MapExecErrNoCtx - for errors without any context
609
610/// Extension trait for mapping errors to `ExecutionError`.
611///
612/// Legacy MAST-local debug metadata no longer provides source locations here; callers that have
613/// package-owned source context should use `map_exec_err_with_package_source_op_idx`.
614pub trait MapExecErr<T> {
615    fn map_exec_err(self) -> Result<T, ExecutionError>;
616}
617
618/// Extension trait for mapping errors to `ExecutionError` with op index context.
619///
620/// Implement this for error types that occur within basic blocks where the
621/// operation index is available for more precise source location.
622pub trait MapExecErrWithOpIdx<T> {
623    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError>;
624
625    fn map_exec_err_with_package_source_op_idx(
626        self,
627        context: Option<PackageSourceDebugContext<'_>>,
628        _host: &(dyn BaseHost + '_),
629        _op_idx: usize,
630    ) -> Result<T, ExecutionError>
631    where
632        Self: Sized,
633    {
634        if context.is_some() {
635            return Err(ExecutionError::Internal(
636                "package source context is unsupported for this error type",
637            ));
638        }
639
640        self.map_exec_err_with_op_idx()
641    }
642}
643
644/// Extension trait for mapping errors to `ExecutionError` without context.
645///
646/// Implement this for error types that may need to be converted when no
647/// error context is available (e.g., during initialization).
648pub trait MapExecErrNoCtx<T> {
649    fn map_exec_err_no_ctx(self) -> Result<T, ExecutionError>;
650}
651
652// OperationError implementations
653impl<T> MapExecErr<T> for Result<T, OperationError> {
654    #[inline(always)]
655    fn map_exec_err(self) -> Result<T, ExecutionError> {
656        match self {
657            Ok(v) => Ok(v),
658            Err(err) => {
659                let (label, source_file) = get_label_and_source_file();
660                Err(ExecutionError::OperationError { label, source_file, err })
661            },
662        }
663    }
664}
665
666impl<T> MapExecErrWithOpIdx<T> for Result<T, OperationError> {
667    #[inline(always)]
668    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
669        match self {
670            Ok(v) => Ok(v),
671            Err(err) => {
672                let (label, source_file) = get_label_and_source_file();
673                Err(ExecutionError::OperationError { label, source_file, err })
674            },
675        }
676    }
677
678    #[inline(always)]
679    fn map_exec_err_with_package_source_op_idx(
680        self,
681        context: Option<PackageSourceDebugContext<'_>>,
682        host: &(dyn BaseHost + '_),
683        op_idx: usize,
684    ) -> Result<T, ExecutionError> {
685        match (self, context) {
686            (Ok(v), _) => Ok(v),
687            (Err(err), Some(context)) => {
688                Err(err.with_package_source_context(context, host, Some(op_idx)))
689            },
690            (Err(err), None) => {
691                let (label, source_file) = get_label_and_source_file();
692                Err(ExecutionError::OperationError { label, source_file, err })
693            },
694        }
695    }
696}
697
698impl<T> MapExecErrNoCtx<T> for Result<T, OperationError> {
699    #[inline(always)]
700    fn map_exec_err_no_ctx(self) -> Result<T, ExecutionError> {
701        match self {
702            Ok(v) => Ok(v),
703            Err(err) => Err(ExecutionError::OperationError {
704                label: SourceSpan::UNKNOWN,
705                source_file: None,
706                err,
707            }),
708        }
709    }
710}
711
712// AdviceError implementations
713impl<T> MapExecErr<T> for Result<T, AdviceError> {
714    #[inline(always)]
715    fn map_exec_err(self) -> Result<T, ExecutionError> {
716        match self {
717            Ok(v) => Ok(v),
718            Err(err) => Err(advice_error_with_context(err)),
719        }
720    }
721}
722
723impl<T> MapExecErrNoCtx<T> for Result<T, AdviceError> {
724    #[inline(always)]
725    fn map_exec_err_no_ctx(self) -> Result<T, ExecutionError> {
726        match self {
727            Ok(v) => Ok(v),
728            Err(err) => Err(ExecutionError::AdviceError {
729                label: SourceSpan::UNKNOWN,
730                source_file: None,
731                err,
732            }),
733        }
734    }
735}
736
737// MemoryError implementations
738impl<T> MapExecErr<T> for Result<T, MemoryError> {
739    #[inline(always)]
740    fn map_exec_err(self) -> Result<T, ExecutionError> {
741        match self {
742            Ok(v) => Ok(v),
743            Err(err) => {
744                let (label, source_file) = get_label_and_source_file();
745                Err(ExecutionError::MemoryError { label, source_file, err })
746            },
747        }
748    }
749}
750
751impl<T> MapExecErrWithOpIdx<T> for Result<T, MemoryError> {
752    #[inline(always)]
753    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
754        match self {
755            Ok(v) => Ok(v),
756            Err(err) => {
757                let (label, source_file) = get_label_and_source_file();
758                Err(ExecutionError::MemoryError { label, source_file, err })
759            },
760        }
761    }
762
763    #[inline(always)]
764    fn map_exec_err_with_package_source_op_idx(
765        self,
766        context: Option<PackageSourceDebugContext<'_>>,
767        host: &(dyn BaseHost + '_),
768        op_idx: usize,
769    ) -> Result<T, ExecutionError> {
770        match (self, context) {
771            (Ok(v), _) => Ok(v),
772            (Err(err), Some(context)) => {
773                let (label, source_file) = label_and_source_file_from_location(
774                    context.assembly_location(Some(op_idx)),
775                    host,
776                );
777                Err(ExecutionError::MemoryError { label, source_file, err })
778            },
779            (Err(err), None) => {
780                let (label, source_file) = get_label_and_source_file();
781                Err(ExecutionError::MemoryError { label, source_file, err })
782            },
783        }
784    }
785}
786
787// SystemEventError implementations
788impl<T> MapExecErr<T> for Result<T, SystemEventError> {
789    #[inline(always)]
790    fn map_exec_err(self) -> Result<T, ExecutionError> {
791        match self {
792            Ok(v) => Ok(v),
793            Err(err) => {
794                let (label, source_file) = get_label_and_source_file();
795                Err(match err {
796                    SystemEventError::Advice(err) => {
797                        ExecutionError::AdviceError { label, source_file, err }
798                    },
799                    SystemEventError::Operation(err) => {
800                        ExecutionError::OperationError { label, source_file, err }
801                    },
802                    SystemEventError::Memory(err) => {
803                        ExecutionError::MemoryError { label, source_file, err }
804                    },
805                })
806            },
807        }
808    }
809}
810
811impl<T> MapExecErrWithOpIdx<T> for Result<T, SystemEventError> {
812    #[inline(always)]
813    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
814        match self {
815            Ok(v) => Ok(v),
816            Err(err) => {
817                let (label, source_file) = get_label_and_source_file();
818                Err(match err {
819                    SystemEventError::Advice(err) => {
820                        ExecutionError::AdviceError { label, source_file, err }
821                    },
822                    SystemEventError::Operation(err) => {
823                        ExecutionError::OperationError { label, source_file, err }
824                    },
825                    SystemEventError::Memory(err) => {
826                        ExecutionError::MemoryError { label, source_file, err }
827                    },
828                })
829            },
830        }
831    }
832
833    #[inline(always)]
834    fn map_exec_err_with_package_source_op_idx(
835        self,
836        context: Option<PackageSourceDebugContext<'_>>,
837        host: &(dyn BaseHost + '_),
838        op_idx: usize,
839    ) -> Result<T, ExecutionError> {
840        match (self, context) {
841            (Ok(v), _) => Ok(v),
842            (Err(err), Some(context)) => {
843                let (label, source_file) = label_and_source_file_from_location(
844                    context.assembly_location(Some(op_idx)),
845                    host,
846                );
847                Err(match err {
848                    SystemEventError::Advice(err) => {
849                        ExecutionError::AdviceError { label, source_file, err }
850                    },
851                    SystemEventError::Operation(err) => {
852                        ExecutionError::OperationError { label, source_file, err }
853                    },
854                    SystemEventError::Memory(err) => {
855                        ExecutionError::MemoryError { label, source_file, err }
856                    },
857                })
858            },
859            (Err(err), None) => {
860                let (label, source_file) = get_label_and_source_file();
861                Err(match err {
862                    SystemEventError::Advice(err) => {
863                        ExecutionError::AdviceError { label, source_file, err }
864                    },
865                    SystemEventError::Operation(err) => {
866                        ExecutionError::OperationError { label, source_file, err }
867                    },
868                    SystemEventError::Memory(err) => {
869                        ExecutionError::MemoryError { label, source_file, err }
870                    },
871                })
872            },
873        }
874    }
875}
876
877// IoError implementations
878impl<T> MapExecErrWithOpIdx<T> for Result<T, IoError> {
879    #[inline(always)]
880    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
881        match self {
882            Ok(v) => Ok(v),
883            Err(err) => {
884                let (label, source_file) = get_label_and_source_file();
885                Err(match err {
886                    IoError::Advice(err) => ExecutionError::AdviceError { label, source_file, err },
887                    IoError::Memory(err) => ExecutionError::MemoryError { label, source_file, err },
888                    IoError::Operation(err) => {
889                        ExecutionError::OperationError { label, source_file, err }
890                    },
891                    // Execution errors are already fully formed with their own message.
892                    IoError::Execution(boxed_err) => *boxed_err,
893                })
894            },
895        }
896    }
897
898    #[inline(always)]
899    fn map_exec_err_with_package_source_op_idx(
900        self,
901        context: Option<PackageSourceDebugContext<'_>>,
902        host: &(dyn BaseHost + '_),
903        op_idx: usize,
904    ) -> Result<T, ExecutionError> {
905        match (self, context) {
906            (Ok(v), _) => Ok(v),
907            (Err(IoError::Execution(boxed_err)), _) => Err(*boxed_err),
908            (Err(err), Some(context)) => {
909                let (label, source_file) = label_and_source_file_from_location(
910                    context.assembly_location(Some(op_idx)),
911                    host,
912                );
913                Err(match err {
914                    IoError::Advice(err) => ExecutionError::AdviceError { label, source_file, err },
915                    IoError::Memory(err) => ExecutionError::MemoryError { label, source_file, err },
916                    IoError::Operation(err) => {
917                        ExecutionError::OperationError { label, source_file, err }
918                    },
919                    IoError::Execution(_) => unreachable!("handled above"),
920                })
921            },
922            (Err(err), None) => {
923                let (label, source_file) = get_label_and_source_file();
924                Err(match err {
925                    IoError::Advice(err) => ExecutionError::AdviceError { label, source_file, err },
926                    IoError::Memory(err) => ExecutionError::MemoryError { label, source_file, err },
927                    IoError::Operation(err) => {
928                        ExecutionError::OperationError { label, source_file, err }
929                    },
930                    IoError::Execution(_) => unreachable!("handled above"),
931                })
932            },
933        }
934    }
935}
936
937// CryptoError implementations
938impl<T> MapExecErrWithOpIdx<T> for Result<T, CryptoError> {
939    #[inline(always)]
940    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
941        match self {
942            Ok(v) => Ok(v),
943            Err(err) => {
944                let (label, source_file) = get_label_and_source_file();
945                Err(match err {
946                    CryptoError::Advice(err) => {
947                        ExecutionError::AdviceError { label, source_file, err }
948                    },
949                    CryptoError::Operation(err) => {
950                        ExecutionError::OperationError { label, source_file, err }
951                    },
952                })
953            },
954        }
955    }
956
957    #[inline(always)]
958    fn map_exec_err_with_package_source_op_idx(
959        self,
960        context: Option<PackageSourceDebugContext<'_>>,
961        host: &(dyn BaseHost + '_),
962        op_idx: usize,
963    ) -> Result<T, ExecutionError> {
964        match (self, context) {
965            (Ok(v), _) => Ok(v),
966            (Err(err), Some(context)) => {
967                let (label, source_file) = label_and_source_file_from_location(
968                    context.assembly_location(Some(op_idx)),
969                    host,
970                );
971                Err(match err {
972                    CryptoError::Advice(err) => {
973                        ExecutionError::AdviceError { label, source_file, err }
974                    },
975                    CryptoError::Operation(err) => ExecutionError::OperationError {
976                        label,
977                        source_file,
978                        err: err.with_package_debug_info(context.debug_info()),
979                    },
980                })
981            },
982            (Err(err), None) => {
983                let (label, source_file) = get_label_and_source_file();
984                Err(match err {
985                    CryptoError::Advice(err) => {
986                        ExecutionError::AdviceError { label, source_file, err }
987                    },
988                    CryptoError::Operation(err) => {
989                        ExecutionError::OperationError { label, source_file, err }
990                    },
991                })
992            },
993        }
994    }
995}
996
997// AceEvalError implementations
998impl<T> MapExecErrWithOpIdx<T> for Result<T, AceEvalError> {
999    #[inline(always)]
1000    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
1001        match self {
1002            Ok(v) => Ok(v),
1003            Err(err) => {
1004                let (label, source_file) = get_label_and_source_file();
1005                Err(match err {
1006                    AceEvalError::Ace(error) => {
1007                        ExecutionError::AceChipError { label, source_file, error }
1008                    },
1009                    AceEvalError::Memory(err) => {
1010                        ExecutionError::MemoryError { label, source_file, err }
1011                    },
1012                })
1013            },
1014        }
1015    }
1016
1017    #[inline(always)]
1018    fn map_exec_err_with_package_source_op_idx(
1019        self,
1020        context: Option<PackageSourceDebugContext<'_>>,
1021        host: &(dyn BaseHost + '_),
1022        op_idx: usize,
1023    ) -> Result<T, ExecutionError> {
1024        match (self, context) {
1025            (Ok(v), _) => Ok(v),
1026            (Err(err), Some(context)) => {
1027                let (label, source_file) = label_and_source_file_from_location(
1028                    context.assembly_location(Some(op_idx)),
1029                    host,
1030                );
1031                Err(match err {
1032                    AceEvalError::Ace(error) => {
1033                        ExecutionError::AceChipError { label, source_file, error }
1034                    },
1035                    AceEvalError::Memory(err) => {
1036                        ExecutionError::MemoryError { label, source_file, err }
1037                    },
1038                })
1039            },
1040            (Err(err), None) => {
1041                let (label, source_file) = get_label_and_source_file();
1042                Err(match err {
1043                    AceEvalError::Ace(error) => {
1044                        ExecutionError::AceChipError { label, source_file, error }
1045                    },
1046                    AceEvalError::Memory(err) => {
1047                        ExecutionError::MemoryError { label, source_file, err }
1048                    },
1049                })
1050            },
1051        }
1052    }
1053}
1054
1055// TESTS
1056// ================================================================================================
1057
1058#[cfg(test)]
1059mod error_assertions {
1060    use alloc::sync::Arc;
1061
1062    use miden_debug_types::{ByteIndex, SourceId, Uri};
1063    use miden_mast_package::debug_info::{
1064        DebugErrorMessage, DebugErrorMessagesSection, DebugSourceAsmOp, DebugSourceMapSection,
1065        PackageDebugInfo,
1066    };
1067
1068    use super::*;
1069
1070    /// Asserts at compile time that the passed error has Send + Sync + 'static bounds.
1071    fn _assert_error_is_send_sync_static<E: core::error::Error + Send + Sync + 'static>(_: E) {}
1072
1073    fn _assert_execution_error_bounds(err: ExecutionError) {
1074        _assert_error_is_send_sync_static(err);
1075    }
1076
1077    struct RecordingHost {
1078        expected_location: Location,
1079        returned_span: SourceSpan,
1080    }
1081
1082    impl BaseHost for RecordingHost {
1083        fn get_label_and_source_file(
1084            &self,
1085            location: &Location,
1086        ) -> (SourceSpan, Option<Arc<SourceFile>>) {
1087            assert_eq!(location, &self.expected_location);
1088            (self.returned_span, None)
1089        }
1090    }
1091
1092    #[test]
1093    fn package_source_context_resolves_by_source_occurrence() {
1094        let source_a = DebugSourceNodeId::from(0);
1095        let source_b = DebugSourceNodeId::from(1);
1096        let location_a = Location::new(
1097            Uri::new("file://pkg/first.masm"),
1098            ByteIndex::new(10),
1099            ByteIndex::new(13),
1100        );
1101        let location_b = Location::new(
1102            Uri::new("file://pkg/second.masm"),
1103            ByteIndex::new(20),
1104            ByteIndex::new(24),
1105        );
1106        let later_location_b = Location::new(
1107            Uri::new("file://pkg/second-later.masm"),
1108            ByteIndex::new(30),
1109            ByteIndex::new(35),
1110        );
1111        let debug_info =
1112            PackageDebugInfo::default().with_source_map(DebugSourceMapSection::from_parts(
1113                vec![
1114                    DebugSourceAsmOp::new(
1115                        source_a,
1116                        0,
1117                        Some(location_a),
1118                        "first".into(),
1119                        "add".into(),
1120                        1,
1121                    ),
1122                    DebugSourceAsmOp::new(
1123                        source_b,
1124                        2,
1125                        Some(later_location_b),
1126                        "second_later".into(),
1127                        "mul".into(),
1128                        1,
1129                    ),
1130                    DebugSourceAsmOp::new(
1131                        source_b,
1132                        0,
1133                        Some(location_b.clone()),
1134                        "second".into(),
1135                        "add".into(),
1136                        1,
1137                    ),
1138                ],
1139                Vec::new(),
1140            ));
1141        let host = RecordingHost {
1142            expected_location: location_b,
1143            returned_span: SourceSpan::new(SourceId::new(7), 20u32..24),
1144        };
1145        let context = PackageSourceDebugContext::new(&debug_info, source_b);
1146
1147        assert_eq!(context.assembly_location(None), Some(&host.expected_location));
1148
1149        let err = OperationError::DivideByZero.with_package_source_context(context, &host, Some(0));
1150
1151        match err {
1152            ExecutionError::OperationError { label, source_file, err } => {
1153                assert_eq!(label, host.returned_span);
1154                assert!(source_file.is_none());
1155                assert!(matches!(err, OperationError::DivideByZero));
1156            },
1157            err => panic!("expected operation error, got {err:?}"),
1158        }
1159    }
1160
1161    #[test]
1162    fn package_source_context_without_location_uses_unknown_span() {
1163        let source_node_id = DebugSourceNodeId::from(0);
1164        let debug_info =
1165            PackageDebugInfo::default().with_source_map(DebugSourceMapSection::from_parts(
1166                vec![DebugSourceAsmOp::new(
1167                    source_node_id,
1168                    0,
1169                    None,
1170                    "missing_location".into(),
1171                    "add".into(),
1172                    1,
1173                )],
1174                Vec::new(),
1175            ));
1176        let host = RecordingHost {
1177            expected_location: Location::new(
1178                Uri::new("file://unused.masm"),
1179                ByteIndex::new(0),
1180                ByteIndex::new(0),
1181            ),
1182            returned_span: SourceSpan::new(SourceId::new(7), 20u32..24),
1183        };
1184        let context = PackageSourceDebugContext::new(&debug_info, source_node_id);
1185
1186        let err = advice_error_with_package_source_context(
1187            AdviceError::StackReadFailed,
1188            context,
1189            &host,
1190            Some(0),
1191        );
1192
1193        match err {
1194            ExecutionError::AdviceError { label, source_file, err } => {
1195                assert_eq!(label, SourceSpan::UNKNOWN);
1196                assert!(source_file.is_none());
1197                assert!(matches!(err, AdviceError::StackReadFailed));
1198            },
1199            err => panic!("expected advice error, got {err:?}"),
1200        }
1201    }
1202
1203    #[test]
1204    fn package_debug_info_without_source_node_restores_error_message() {
1205        let debug_info =
1206            PackageDebugInfo::default().with_error_messages(DebugErrorMessagesSection::from_parts(
1207                vec![DebugErrorMessage::new(7, Arc::from("some error message"))],
1208            ));
1209        let context = PackageSourceDebugContext::new_optional(&debug_info, None);
1210        let err = Err::<(), _>(OperationError::FailedAssertion {
1211            err_code: Felt::from_u32(7),
1212            err_msg: None,
1213        })
1214        .map_exec_err_with_package_source_op_idx(
1215            Some(context),
1216            &RecordingHost {
1217                expected_location: Location::new(
1218                    Uri::new("file://unused.masm"),
1219                    ByteIndex::new(0),
1220                    ByteIndex::new(0),
1221                ),
1222                returned_span: SourceSpan::new(SourceId::new(7), 20u32..24),
1223            },
1224            0,
1225        )
1226        .unwrap_err();
1227
1228        match err {
1229            ExecutionError::OperationError {
1230                label,
1231                source_file,
1232                err: OperationError::FailedAssertion { err_msg, .. },
1233            } => {
1234                assert_eq!(label, SourceSpan::UNKNOWN);
1235                assert!(source_file.is_none());
1236                assert_eq!(err_msg.as_deref(), Some("some error message"));
1237            },
1238            err => panic!("expected failed assertion operation error, got {err:?}"),
1239        }
1240    }
1241
1242    #[test]
1243    fn package_debug_info_restores_merkle_path_error_message() {
1244        let debug_info =
1245            PackageDebugInfo::default().with_error_messages(DebugErrorMessagesSection::from_parts(
1246                vec![DebugErrorMessage::new(7, Arc::from("some error message"))],
1247            ));
1248        let err = OperationError::MerklePathVerificationFailed {
1249            inner: Box::new(MerklePathVerificationFailedInner {
1250                value: Word::default(),
1251                index: Felt::from_u32(3),
1252                root: Word::default(),
1253                err_code: Felt::from_u32(7),
1254                err_msg: None,
1255            }),
1256        }
1257        .with_package_debug_info(&debug_info);
1258
1259        match err {
1260            OperationError::MerklePathVerificationFailed { inner } => {
1261                assert_eq!(inner.err_msg.as_deref(), Some("some error message"));
1262            },
1263            err => panic!("expected MerklePathVerificationFailed, got {err:?}"),
1264        }
1265    }
1266}