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).as_ref(), 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<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
490            .location_idx
491            .into_option()
492            .and_then(|idx| self.debug_info.get_location(idx))
493    }
494}
495
496fn label_and_source_file_from_location(
497    location: Option<&Location>,
498    host: &(dyn BaseHost + '_),
499) -> (SourceSpan, Option<Arc<SourceFile>>) {
500    location.map_or_else(
501        || (SourceSpan::UNKNOWN, None),
502        |location| host.get_label_and_source_file(location),
503    )
504}
505
506/// Computes the label and source file for error context.
507///
508/// This function is called by the extension traits to compute source location
509/// only when an error occurs. Since errors are rare, the cost of source metadata lookup is
510/// acceptable.
511fn get_label_and_source_file() -> (SourceSpan, Option<Arc<SourceFile>>) {
512    (SourceSpan::UNKNOWN, None)
513}
514
515/// Wraps an `AdviceError` with execution context to produce an `ExecutionError`.
516///
517/// This is useful when working with `ControlFlow` or other non-`Result` return types
518/// where the extension traits cannot be used directly.
519pub fn advice_error_with_context(err: AdviceError) -> ExecutionError {
520    let (label, source_file) = get_label_and_source_file();
521    ExecutionError::AdviceError { label, source_file, err }
522}
523
524/// Wraps an `AdviceError` with package-owned source-occurrence execution context.
525pub fn advice_error_with_package_source_context(
526    err: AdviceError,
527    context: PackageSourceDebugContext<'_>,
528    host: &(dyn BaseHost + '_),
529    op_idx: Option<usize>,
530) -> ExecutionError {
531    let (label, source_file) =
532        label_and_source_file_from_location(context.assembly_location(op_idx).as_ref(), host);
533    ExecutionError::AdviceError { label, source_file, err }
534}
535
536/// Wraps an `EventError` with execution context to produce an `ExecutionError`.
537///
538/// This is useful when working with `ControlFlow` or other non-`Result` return types
539/// where an extension trait on `Result` cannot be used directly.
540pub fn event_error_with_context(
541    error: EventError,
542    event_id: EventId,
543    event_name: Option<EventName>,
544) -> ExecutionError {
545    let (label, source_file) = get_label_and_source_file();
546    ExecutionError::EventError {
547        label,
548        source_file,
549        event_id,
550        event_name,
551        error,
552    }
553}
554
555/// Wraps an `EventError` with package-owned source-occurrence execution context.
556pub fn event_error_with_package_source_context(
557    error: EventError,
558    context: PackageSourceDebugContext<'_>,
559    host: &(dyn BaseHost + '_),
560    op_idx: Option<usize>,
561    event_id: EventId,
562    event_name: Option<EventName>,
563) -> ExecutionError {
564    let (label, source_file) =
565        label_and_source_file_from_location(context.assembly_location(op_idx).as_ref(), host);
566    ExecutionError::EventError {
567        label,
568        source_file,
569        event_id,
570        event_name,
571        error,
572    }
573}
574
575/// Creates a `ProcedureNotFound` error with execution context.
576pub fn procedure_not_found_with_context(root_digest: Word) -> ExecutionError {
577    let (label, source_file) = get_label_and_source_file();
578    ExecutionError::ProcedureNotFound { label, source_file, root_digest }
579}
580
581/// Creates a `ProcedureNotFound` error with package-owned source-occurrence execution context.
582pub fn procedure_not_found_with_package_source_context(
583    root_digest: Word,
584    context: PackageSourceDebugContext<'_>,
585    host: &(dyn BaseHost + '_),
586) -> ExecutionError {
587    let (label, source_file) =
588        label_and_source_file_from_location(context.assembly_location(None).as_ref(), host);
589    ExecutionError::ProcedureNotFound { label, source_file, root_digest }
590}
591
592/// Creates a `MalformedMastForestInHost` operation error with execution context.
593pub fn malformed_mast_forest_with_context(
594    root_digest: Word,
595    context: Option<PackageSourceDebugContext<'_>>,
596    host: &(dyn BaseHost + '_),
597) -> ExecutionError {
598    let err = OperationError::MalformedMastForestInHost { root_digest };
599    match context {
600        Some(context) => err.with_package_source_context(context, host, None),
601        None => err.with_context(),
602    }
603}
604
605// CONSOLIDATED EXTENSION TRAITS (plafer's approach)
606// ================================================================================================
607//
608// Three traits organized by method signature rather than by error type:
609// 1. MapExecErr - for errors with basic context
610// 2. MapExecErrWithOpIdx - for errors in basic blocks that may need op_idx
611// 3. MapExecErrNoCtx - for errors without any context
612
613/// Extension trait for mapping errors to `ExecutionError`.
614///
615/// Legacy MAST-local debug metadata no longer provides source locations here; callers that have
616/// package-owned source context should use `map_exec_err_with_package_source_op_idx`.
617pub trait MapExecErr<T> {
618    fn map_exec_err(self) -> Result<T, ExecutionError>;
619}
620
621/// Extension trait for mapping errors to `ExecutionError` with op index context.
622///
623/// Implement this for error types that occur within basic blocks where the
624/// operation index is available for more precise source location.
625pub trait MapExecErrWithOpIdx<T> {
626    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError>;
627
628    fn map_exec_err_with_package_source_op_idx(
629        self,
630        context: Option<PackageSourceDebugContext<'_>>,
631        _host: &(dyn BaseHost + '_),
632        _op_idx: usize,
633    ) -> Result<T, ExecutionError>
634    where
635        Self: Sized,
636    {
637        if context.is_some() {
638            return Err(ExecutionError::Internal(
639                "package source context is unsupported for this error type",
640            ));
641        }
642
643        self.map_exec_err_with_op_idx()
644    }
645}
646
647/// Extension trait for mapping errors to `ExecutionError` without context.
648///
649/// Implement this for error types that may need to be converted when no
650/// error context is available (e.g., during initialization).
651pub trait MapExecErrNoCtx<T> {
652    fn map_exec_err_no_ctx(self) -> Result<T, ExecutionError>;
653}
654
655// OperationError implementations
656impl<T> MapExecErr<T> for Result<T, OperationError> {
657    #[inline(always)]
658    fn map_exec_err(self) -> Result<T, ExecutionError> {
659        match self {
660            Ok(v) => Ok(v),
661            Err(err) => {
662                let (label, source_file) = get_label_and_source_file();
663                Err(ExecutionError::OperationError { label, source_file, err })
664            },
665        }
666    }
667}
668
669impl<T> MapExecErrWithOpIdx<T> for Result<T, OperationError> {
670    #[inline(always)]
671    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
672        match self {
673            Ok(v) => Ok(v),
674            Err(err) => {
675                let (label, source_file) = get_label_and_source_file();
676                Err(ExecutionError::OperationError { label, source_file, err })
677            },
678        }
679    }
680
681    #[inline(always)]
682    fn map_exec_err_with_package_source_op_idx(
683        self,
684        context: Option<PackageSourceDebugContext<'_>>,
685        host: &(dyn BaseHost + '_),
686        op_idx: usize,
687    ) -> Result<T, ExecutionError> {
688        match (self, context) {
689            (Ok(v), _) => Ok(v),
690            (Err(err), Some(context)) => {
691                Err(err.with_package_source_context(context, host, Some(op_idx)))
692            },
693            (Err(err), None) => {
694                let (label, source_file) = get_label_and_source_file();
695                Err(ExecutionError::OperationError { label, source_file, err })
696            },
697        }
698    }
699}
700
701impl<T> MapExecErrNoCtx<T> for Result<T, OperationError> {
702    #[inline(always)]
703    fn map_exec_err_no_ctx(self) -> Result<T, ExecutionError> {
704        match self {
705            Ok(v) => Ok(v),
706            Err(err) => Err(ExecutionError::OperationError {
707                label: SourceSpan::UNKNOWN,
708                source_file: None,
709                err,
710            }),
711        }
712    }
713}
714
715// AdviceError implementations
716impl<T> MapExecErr<T> for Result<T, AdviceError> {
717    #[inline(always)]
718    fn map_exec_err(self) -> Result<T, ExecutionError> {
719        match self {
720            Ok(v) => Ok(v),
721            Err(err) => Err(advice_error_with_context(err)),
722        }
723    }
724}
725
726impl<T> MapExecErrNoCtx<T> for Result<T, AdviceError> {
727    #[inline(always)]
728    fn map_exec_err_no_ctx(self) -> Result<T, ExecutionError> {
729        match self {
730            Ok(v) => Ok(v),
731            Err(err) => Err(ExecutionError::AdviceError {
732                label: SourceSpan::UNKNOWN,
733                source_file: None,
734                err,
735            }),
736        }
737    }
738}
739
740// MemoryError implementations
741impl<T> MapExecErr<T> for Result<T, MemoryError> {
742    #[inline(always)]
743    fn map_exec_err(self) -> Result<T, ExecutionError> {
744        match self {
745            Ok(v) => Ok(v),
746            Err(err) => {
747                let (label, source_file) = get_label_and_source_file();
748                Err(ExecutionError::MemoryError { label, source_file, err })
749            },
750        }
751    }
752}
753
754impl<T> MapExecErrWithOpIdx<T> for Result<T, MemoryError> {
755    #[inline(always)]
756    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
757        match self {
758            Ok(v) => Ok(v),
759            Err(err) => {
760                let (label, source_file) = get_label_and_source_file();
761                Err(ExecutionError::MemoryError { label, source_file, err })
762            },
763        }
764    }
765
766    #[inline(always)]
767    fn map_exec_err_with_package_source_op_idx(
768        self,
769        context: Option<PackageSourceDebugContext<'_>>,
770        host: &(dyn BaseHost + '_),
771        op_idx: usize,
772    ) -> Result<T, ExecutionError> {
773        match (self, context) {
774            (Ok(v), _) => Ok(v),
775            (Err(err), Some(context)) => {
776                let (label, source_file) = label_and_source_file_from_location(
777                    context.assembly_location(Some(op_idx)).as_ref(),
778                    host,
779                );
780                Err(ExecutionError::MemoryError { label, source_file, err })
781            },
782            (Err(err), None) => {
783                let (label, source_file) = get_label_and_source_file();
784                Err(ExecutionError::MemoryError { label, source_file, err })
785            },
786        }
787    }
788}
789
790// SystemEventError implementations
791impl<T> MapExecErr<T> for Result<T, SystemEventError> {
792    #[inline(always)]
793    fn map_exec_err(self) -> Result<T, ExecutionError> {
794        match self {
795            Ok(v) => Ok(v),
796            Err(err) => {
797                let (label, source_file) = get_label_and_source_file();
798                Err(match err {
799                    SystemEventError::Advice(err) => {
800                        ExecutionError::AdviceError { label, source_file, err }
801                    },
802                    SystemEventError::Operation(err) => {
803                        ExecutionError::OperationError { label, source_file, err }
804                    },
805                    SystemEventError::Memory(err) => {
806                        ExecutionError::MemoryError { label, source_file, err }
807                    },
808                })
809            },
810        }
811    }
812}
813
814impl<T> MapExecErrWithOpIdx<T> for Result<T, SystemEventError> {
815    #[inline(always)]
816    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
817        match self {
818            Ok(v) => Ok(v),
819            Err(err) => {
820                let (label, source_file) = get_label_and_source_file();
821                Err(match err {
822                    SystemEventError::Advice(err) => {
823                        ExecutionError::AdviceError { label, source_file, err }
824                    },
825                    SystemEventError::Operation(err) => {
826                        ExecutionError::OperationError { label, source_file, err }
827                    },
828                    SystemEventError::Memory(err) => {
829                        ExecutionError::MemoryError { label, source_file, err }
830                    },
831                })
832            },
833        }
834    }
835
836    #[inline(always)]
837    fn map_exec_err_with_package_source_op_idx(
838        self,
839        context: Option<PackageSourceDebugContext<'_>>,
840        host: &(dyn BaseHost + '_),
841        op_idx: usize,
842    ) -> Result<T, ExecutionError> {
843        match (self, context) {
844            (Ok(v), _) => Ok(v),
845            (Err(err), Some(context)) => {
846                let (label, source_file) = label_and_source_file_from_location(
847                    context.assembly_location(Some(op_idx)).as_ref(),
848                    host,
849                );
850                Err(match err {
851                    SystemEventError::Advice(err) => {
852                        ExecutionError::AdviceError { label, source_file, err }
853                    },
854                    SystemEventError::Operation(err) => {
855                        ExecutionError::OperationError { label, source_file, err }
856                    },
857                    SystemEventError::Memory(err) => {
858                        ExecutionError::MemoryError { label, source_file, err }
859                    },
860                })
861            },
862            (Err(err), None) => {
863                let (label, source_file) = get_label_and_source_file();
864                Err(match err {
865                    SystemEventError::Advice(err) => {
866                        ExecutionError::AdviceError { label, source_file, err }
867                    },
868                    SystemEventError::Operation(err) => {
869                        ExecutionError::OperationError { label, source_file, err }
870                    },
871                    SystemEventError::Memory(err) => {
872                        ExecutionError::MemoryError { label, source_file, err }
873                    },
874                })
875            },
876        }
877    }
878}
879
880// IoError implementations
881impl<T> MapExecErrWithOpIdx<T> for Result<T, IoError> {
882    #[inline(always)]
883    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
884        match self {
885            Ok(v) => Ok(v),
886            Err(err) => {
887                let (label, source_file) = get_label_and_source_file();
888                Err(match err {
889                    IoError::Advice(err) => ExecutionError::AdviceError { label, source_file, err },
890                    IoError::Memory(err) => ExecutionError::MemoryError { label, source_file, err },
891                    IoError::Operation(err) => {
892                        ExecutionError::OperationError { label, source_file, err }
893                    },
894                    // Execution errors are already fully formed with their own message.
895                    IoError::Execution(boxed_err) => *boxed_err,
896                })
897            },
898        }
899    }
900
901    #[inline(always)]
902    fn map_exec_err_with_package_source_op_idx(
903        self,
904        context: Option<PackageSourceDebugContext<'_>>,
905        host: &(dyn BaseHost + '_),
906        op_idx: usize,
907    ) -> Result<T, ExecutionError> {
908        match (self, context) {
909            (Ok(v), _) => Ok(v),
910            (Err(IoError::Execution(boxed_err)), _) => Err(*boxed_err),
911            (Err(err), Some(context)) => {
912                let (label, source_file) = label_and_source_file_from_location(
913                    context.assembly_location(Some(op_idx)).as_ref(),
914                    host,
915                );
916                Err(match err {
917                    IoError::Advice(err) => ExecutionError::AdviceError { label, source_file, err },
918                    IoError::Memory(err) => ExecutionError::MemoryError { label, source_file, err },
919                    IoError::Operation(err) => {
920                        ExecutionError::OperationError { label, source_file, err }
921                    },
922                    IoError::Execution(_) => unreachable!("handled above"),
923                })
924            },
925            (Err(err), None) => {
926                let (label, source_file) = get_label_and_source_file();
927                Err(match err {
928                    IoError::Advice(err) => ExecutionError::AdviceError { label, source_file, err },
929                    IoError::Memory(err) => ExecutionError::MemoryError { label, source_file, err },
930                    IoError::Operation(err) => {
931                        ExecutionError::OperationError { label, source_file, err }
932                    },
933                    IoError::Execution(_) => unreachable!("handled above"),
934                })
935            },
936        }
937    }
938}
939
940// CryptoError implementations
941impl<T> MapExecErrWithOpIdx<T> for Result<T, CryptoError> {
942    #[inline(always)]
943    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
944        match self {
945            Ok(v) => Ok(v),
946            Err(err) => {
947                let (label, source_file) = get_label_and_source_file();
948                Err(match err {
949                    CryptoError::Advice(err) => {
950                        ExecutionError::AdviceError { label, source_file, err }
951                    },
952                    CryptoError::Operation(err) => {
953                        ExecutionError::OperationError { label, source_file, err }
954                    },
955                })
956            },
957        }
958    }
959
960    #[inline(always)]
961    fn map_exec_err_with_package_source_op_idx(
962        self,
963        context: Option<PackageSourceDebugContext<'_>>,
964        host: &(dyn BaseHost + '_),
965        op_idx: usize,
966    ) -> Result<T, ExecutionError> {
967        match (self, context) {
968            (Ok(v), _) => Ok(v),
969            (Err(err), Some(context)) => {
970                let (label, source_file) = label_and_source_file_from_location(
971                    context.assembly_location(Some(op_idx)).as_ref(),
972                    host,
973                );
974                Err(match err {
975                    CryptoError::Advice(err) => {
976                        ExecutionError::AdviceError { label, source_file, err }
977                    },
978                    CryptoError::Operation(err) => ExecutionError::OperationError {
979                        label,
980                        source_file,
981                        err: err.with_package_debug_info(context.debug_info()),
982                    },
983                })
984            },
985            (Err(err), None) => {
986                let (label, source_file) = get_label_and_source_file();
987                Err(match err {
988                    CryptoError::Advice(err) => {
989                        ExecutionError::AdviceError { label, source_file, err }
990                    },
991                    CryptoError::Operation(err) => {
992                        ExecutionError::OperationError { label, source_file, err }
993                    },
994                })
995            },
996        }
997    }
998}
999
1000// AceEvalError implementations
1001impl<T> MapExecErrWithOpIdx<T> for Result<T, AceEvalError> {
1002    #[inline(always)]
1003    fn map_exec_err_with_op_idx(self) -> Result<T, ExecutionError> {
1004        match self {
1005            Ok(v) => Ok(v),
1006            Err(err) => {
1007                let (label, source_file) = get_label_and_source_file();
1008                Err(match err {
1009                    AceEvalError::Ace(error) => {
1010                        ExecutionError::AceChipError { label, source_file, error }
1011                    },
1012                    AceEvalError::Memory(err) => {
1013                        ExecutionError::MemoryError { label, source_file, err }
1014                    },
1015                })
1016            },
1017        }
1018    }
1019
1020    #[inline(always)]
1021    fn map_exec_err_with_package_source_op_idx(
1022        self,
1023        context: Option<PackageSourceDebugContext<'_>>,
1024        host: &(dyn BaseHost + '_),
1025        op_idx: usize,
1026    ) -> Result<T, ExecutionError> {
1027        match (self, context) {
1028            (Ok(v), _) => Ok(v),
1029            (Err(err), Some(context)) => {
1030                let (label, source_file) = label_and_source_file_from_location(
1031                    context.assembly_location(Some(op_idx)).as_ref(),
1032                    host,
1033                );
1034                Err(match err {
1035                    AceEvalError::Ace(error) => {
1036                        ExecutionError::AceChipError { label, source_file, error }
1037                    },
1038                    AceEvalError::Memory(err) => {
1039                        ExecutionError::MemoryError { label, source_file, err }
1040                    },
1041                })
1042            },
1043            (Err(err), None) => {
1044                let (label, source_file) = get_label_and_source_file();
1045                Err(match err {
1046                    AceEvalError::Ace(error) => {
1047                        ExecutionError::AceChipError { label, source_file, error }
1048                    },
1049                    AceEvalError::Memory(err) => {
1050                        ExecutionError::MemoryError { label, source_file, err }
1051                    },
1052                })
1053            },
1054        }
1055    }
1056}
1057
1058// TESTS
1059// ================================================================================================
1060
1061#[cfg(test)]
1062mod error_assertions {
1063    use alloc::sync::Arc;
1064
1065    use miden_core::mast::MastNodeId;
1066    use miden_debug_types::{ByteIndex, SourceId, Uri};
1067    use miden_mast_package::debug_info::{
1068        DebugSourceAsmOp, DebugSourceNode, PackageDebugInfoBuilder,
1069    };
1070
1071    use super::*;
1072
1073    /// Asserts at compile time that the passed error has Send + Sync + 'static bounds.
1074    fn _assert_error_is_send_sync_static<E: core::error::Error + Send + Sync + 'static>(_: E) {}
1075
1076    fn _assert_execution_error_bounds(err: ExecutionError) {
1077        _assert_error_is_send_sync_static(err);
1078    }
1079
1080    fn debug_asm_op(
1081        builder: &mut PackageDebugInfoBuilder,
1082        op_idx: u32,
1083        location: Option<Location>,
1084        context_name: &str,
1085        op_name: &str,
1086    ) -> DebugSourceAsmOp {
1087        DebugSourceAsmOp::new(
1088            op_idx,
1089            location.map(|location| builder.add_location(location)),
1090            builder.add_string(context_name),
1091            builder.add_string(op_name),
1092            1,
1093        )
1094    }
1095
1096    fn debug_source_node(exec_node: u32, asm_ops: Vec<DebugSourceAsmOp>) -> DebugSourceNode {
1097        let op_end = asm_ops.iter().map(|row| row.op_idx + 1).max().unwrap_or(1);
1098        DebugSourceNode {
1099            exec_node: MastNodeId::from(exec_node),
1100            children: Vec::new(),
1101            op_start: 0,
1102            op_end,
1103            asm_ops,
1104            debug_vars: Vec::new(),
1105            inline_calls: Vec::new(),
1106        }
1107    }
1108
1109    struct RecordingHost {
1110        expected_location: Location,
1111        returned_span: SourceSpan,
1112    }
1113
1114    impl BaseHost for RecordingHost {
1115        fn get_label_and_source_file(
1116            &self,
1117            location: &Location,
1118        ) -> (SourceSpan, Option<Arc<SourceFile>>) {
1119            assert_eq!(location, &self.expected_location);
1120            (self.returned_span, None)
1121        }
1122    }
1123
1124    #[test]
1125    fn package_source_context_resolves_by_source_occurrence() {
1126        let location_a = Location::new(
1127            Uri::new("file://pkg/first.masm"),
1128            ByteIndex::new(10),
1129            ByteIndex::new(13),
1130        );
1131        let location_b = Location::new(
1132            Uri::new("file://pkg/second.masm"),
1133            ByteIndex::new(20),
1134            ByteIndex::new(24),
1135        );
1136        let later_location_b = Location::new(
1137            Uri::new("file://pkg/second-later.masm"),
1138            ByteIndex::new(30),
1139            ByteIndex::new(35),
1140        );
1141        let mut builder = PackageDebugInfoBuilder::default();
1142        let source_a_op = debug_asm_op(&mut builder, 0, Some(location_a), "first", "add");
1143        let _source_a = builder.add_node(debug_source_node(0, vec![source_a_op])).unwrap();
1144        let source_b_later_op =
1145            debug_asm_op(&mut builder, 2, Some(later_location_b), "second_later", "mul");
1146        let source_b_op = debug_asm_op(&mut builder, 0, Some(location_b.clone()), "second", "add");
1147        let source_b = builder
1148            .add_node(debug_source_node(1, vec![source_b_later_op, source_b_op]))
1149            .unwrap();
1150        let debug_info = *builder.build();
1151        let host = RecordingHost {
1152            expected_location: location_b,
1153            returned_span: SourceSpan::new(SourceId::new(7), 20u32..24),
1154        };
1155        let context = PackageSourceDebugContext::new(&debug_info, source_b);
1156
1157        assert_eq!(context.assembly_location(None), Some(host.expected_location.clone()));
1158
1159        let err = OperationError::DivideByZero.with_package_source_context(context, &host, Some(0));
1160
1161        match err {
1162            ExecutionError::OperationError { label, source_file, err } => {
1163                assert_eq!(label, host.returned_span);
1164                assert!(source_file.is_none());
1165                assert!(matches!(err, OperationError::DivideByZero));
1166            },
1167            err => panic!("expected operation error, got {err:?}"),
1168        }
1169    }
1170
1171    #[test]
1172    fn package_source_context_without_location_uses_unknown_span() {
1173        let mut builder = PackageDebugInfoBuilder::default();
1174        let asm_op = debug_asm_op(&mut builder, 0, None, "missing_location", "add");
1175        let source_node_id = builder.add_node(debug_source_node(0, vec![asm_op])).unwrap();
1176        let debug_info = *builder.build();
1177        let host = RecordingHost {
1178            expected_location: Location::new(
1179                Uri::new("file://unused.masm"),
1180                ByteIndex::new(0),
1181                ByteIndex::new(0),
1182            ),
1183            returned_span: SourceSpan::new(SourceId::new(7), 20u32..24),
1184        };
1185        let context = PackageSourceDebugContext::new(&debug_info, source_node_id);
1186
1187        let err = advice_error_with_package_source_context(
1188            AdviceError::StackReadFailed,
1189            context,
1190            &host,
1191            Some(0),
1192        );
1193
1194        match err {
1195            ExecutionError::AdviceError { label, source_file, err } => {
1196                assert_eq!(label, SourceSpan::UNKNOWN);
1197                assert!(source_file.is_none());
1198                assert!(matches!(err, AdviceError::StackReadFailed));
1199            },
1200            err => panic!("expected advice error, got {err:?}"),
1201        }
1202    }
1203
1204    #[test]
1205    fn package_debug_info_without_source_node_restores_error_message() {
1206        let mut builder = PackageDebugInfoBuilder::default();
1207        assert!(builder.add_error_message(7, Arc::from("some error message")));
1208        let debug_info = *builder.build();
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 mut builder = PackageDebugInfoBuilder::default();
1245        assert!(builder.add_error_message(7, Arc::from("some error message")));
1246        let debug_info = *builder.build();
1247        let err = OperationError::MerklePathVerificationFailed {
1248            inner: Box::new(MerklePathVerificationFailedInner {
1249                value: Word::default(),
1250                index: Felt::from_u32(3),
1251                root: Word::default(),
1252                err_code: Felt::from_u32(7),
1253                err_msg: None,
1254            }),
1255        }
1256        .with_package_debug_info(&debug_info);
1257
1258        match err {
1259            OperationError::MerklePathVerificationFailed { inner } => {
1260                assert_eq!(inner.err_msg.as_deref(), Some("some error message"));
1261            },
1262            err => panic!("expected MerklePathVerificationFailed, got {err:?}"),
1263        }
1264    }
1265}