Skip to main content

miden_core/mast/debuginfo/
mod.rs

1//! Debug information management for MAST forests.
2//!
3//! This module provides the [`DebugInfo`] struct which consolidates all debug-related information
4//! for a MAST forest in a single location. This includes:
5//!
6//! - All decorators (debug, trace, and assembly operation metadata)
7//! - Operation-indexed decorator mappings for efficient lookup
8//! - Node-level decorator storage (before_enter/after_exit)
9//! - Error code mappings for descriptive error messages
10//!
11//! The debug info is always available at the `MastForest` level (as per issue 1821), but may be
12//! conditionally included during assembly to maintain backward compatibility. Decorators are only
13//! executed when the processor is running in debug mode, allowing debug information to be available
14//! for debugging and error reporting without impacting performance in production execution.
15//!
16//! # Debug Mode Semantics
17//!
18//! Debug mode is controlled via [`ExecutionOptions`](air::options::ExecutionOptions):
19//! - `with_debugging(true)` enables debug mode explicitly
20//! - `with_tracing()` automatically enables debug mode (tracing requires debug info)
21//! - By default, debug mode is disabled for maximum performance
22//!
23//! When debug mode is disabled:
24//! - Debug decorators are not executed
25//! - Trace decorators are not executed
26//! - Assembly operation decorators are not recorded
27//! - before_enter/after_exit decorators are not executed
28//!
29//! When debug mode is enabled:
30//! - All decorator types are executed according to their semantics
31//! - Debug decorators trigger host callbacks for breakpoints
32//! - Trace decorators trigger host callbacks for tracing
33//! - Assembly operation decorators provide source mapping information
34//! - before_enter/after_exit decorators execute around node execution
35//!
36//! # Production Builds
37//!
38//! The `DebugInfo` can be stripped for production builds using the [`clear()`](Self::clear) method,
39//! which removes decorators while preserving critical information. This allows backward
40//! compatibility while enabling size optimization for deployment.
41
42use alloc::{
43    collections::BTreeMap,
44    string::{String, ToString},
45    sync::Arc,
46    vec::Vec,
47};
48
49#[cfg(feature = "serde")]
50use serde::{Deserialize, Serialize};
51
52use super::{AsmOpId, Decorator, DecoratorId, MastForestError, MastNodeId};
53use crate::{
54    LexicographicWord, Word,
55    mast::serialization::{
56        StringTable,
57        asm_op::{AsmOpDataBuilder, AsmOpInfo},
58        decorator::{DecoratorDataBuilder, DecoratorInfo},
59    },
60    operations::AssemblyOp,
61    serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
62    utils::{Idx, IndexVec},
63};
64
65mod asm_op_storage;
66pub use asm_op_storage::{AsmOpIndexError, OpToAsmOpId};
67
68mod decorator_storage;
69pub use decorator_storage::{
70    DecoratedLinks, DecoratedLinksIter, DecoratorIndexError, OpToDecoratorIds,
71};
72
73mod node_decorator_storage;
74pub use node_decorator_storage::NodeToDecoratorIds;
75
76// DEBUG INFO
77// ================================================================================================
78
79/// Debug information for a MAST forest, containing decorators and error messages.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82pub struct DebugInfo {
83    /// All decorators in the MAST forest (Debug and Trace only, no AsmOp).
84    decorators: IndexVec<DecoratorId, Decorator>,
85
86    /// Efficient access to decorators per operation per node.
87    op_decorator_storage: OpToDecoratorIds,
88
89    /// Efficient storage for node-level decorators (before_enter and after_exit).
90    node_decorator_storage: NodeToDecoratorIds,
91
92    /// All AssemblyOps in the MAST forest.
93    asm_ops: IndexVec<AsmOpId, AssemblyOp>,
94
95    /// Efficient access to AssemblyOps per operation per node.
96    asm_op_storage: OpToAsmOpId,
97
98    /// Maps error codes to error messages.
99    error_codes: BTreeMap<u64, Arc<str>>,
100
101    /// Maps MAST root digests to procedure names for debugging purposes.
102    #[cfg_attr(feature = "serde", serde(skip))]
103    procedure_names: BTreeMap<LexicographicWord, Arc<str>>,
104}
105
106impl DebugInfo {
107    // CONSTRUCTORS
108    // --------------------------------------------------------------------------------------------
109
110    /// Creates a new empty [DebugInfo].
111    pub fn new() -> Self {
112        Self {
113            decorators: IndexVec::new(),
114            op_decorator_storage: OpToDecoratorIds::new(),
115            node_decorator_storage: NodeToDecoratorIds::new(),
116            asm_ops: IndexVec::new(),
117            asm_op_storage: OpToAsmOpId::new(),
118            error_codes: BTreeMap::new(),
119            procedure_names: BTreeMap::new(),
120        }
121    }
122
123    /// Creates an empty [DebugInfo] with specified capacities.
124    pub fn with_capacity(
125        decorators_capacity: usize,
126        nodes_capacity: usize,
127        operations_capacity: usize,
128        decorator_ids_capacity: usize,
129    ) -> Self {
130        Self {
131            decorators: IndexVec::with_capacity(decorators_capacity),
132            op_decorator_storage: OpToDecoratorIds::with_capacity(
133                nodes_capacity,
134                operations_capacity,
135                decorator_ids_capacity,
136            ),
137            node_decorator_storage: NodeToDecoratorIds::with_capacity(nodes_capacity, 0, 0),
138            asm_ops: IndexVec::new(),
139            asm_op_storage: OpToAsmOpId::new(),
140            error_codes: BTreeMap::new(),
141            procedure_names: BTreeMap::new(),
142        }
143    }
144
145    /// Creates an empty [DebugInfo] with valid CSR structures for N nodes.
146    pub fn empty_for_nodes(num_nodes: usize) -> Self {
147        let node_indptr_for_op_idx = IndexVec::try_from(vec![0; num_nodes + 1])
148            .expect("num_nodes should not exceed u32::MAX");
149
150        let op_decorator_storage =
151            OpToDecoratorIds::from_components(Vec::new(), Vec::new(), node_indptr_for_op_idx)
152                .expect("Empty CSR structure should be valid");
153
154        Self {
155            decorators: IndexVec::new(),
156            op_decorator_storage,
157            node_decorator_storage: NodeToDecoratorIds::new(),
158            asm_ops: IndexVec::new(),
159            asm_op_storage: OpToAsmOpId::new(),
160            error_codes: BTreeMap::new(),
161            procedure_names: BTreeMap::new(),
162        }
163    }
164
165    // PUBLIC ACCESSORS
166    // --------------------------------------------------------------------------------------------
167
168    /// Returns true if this [DebugInfo] has no decorators, asm_ops, error codes, or procedure
169    /// names.
170    pub fn is_empty(&self) -> bool {
171        self.decorators.is_empty()
172            && self.asm_ops.is_empty()
173            && self.error_codes.is_empty()
174            && self.procedure_names.is_empty()
175    }
176
177    /// Strips all debug information, removing decorators, asm_ops, error codes, and procedure
178    /// names.
179    ///
180    /// This is used for release builds where debug info is not needed.
181    pub fn clear(&mut self) {
182        self.clear_mappings();
183        self.decorators = IndexVec::new();
184        self.asm_ops = IndexVec::new();
185        self.asm_op_storage = OpToAsmOpId::new();
186        self.error_codes.clear();
187        self.procedure_names.clear();
188    }
189
190    // DECORATOR ACCESSORS
191    // --------------------------------------------------------------------------------------------
192
193    /// Returns the number of decorators.
194    pub fn num_decorators(&self) -> usize {
195        self.decorators.len()
196    }
197
198    /// Returns all decorators as a slice.
199    pub fn decorators(&self) -> &[Decorator] {
200        self.decorators.as_slice()
201    }
202
203    /// Returns the decorator with the given ID, if it exists.
204    pub fn decorator(&self, decorator_id: DecoratorId) -> Option<&Decorator> {
205        self.decorators.get(decorator_id)
206    }
207
208    /// Returns the before-enter decorators for the given node.
209    pub fn before_enter_decorators(&self, node_id: MastNodeId) -> &[DecoratorId] {
210        self.node_decorator_storage.get_before_decorators(node_id)
211    }
212
213    /// Returns the after-exit decorators for the given node.
214    pub fn after_exit_decorators(&self, node_id: MastNodeId) -> &[DecoratorId] {
215        self.node_decorator_storage.get_after_decorators(node_id)
216    }
217
218    /// Returns decorators for a specific operation within a node.
219    pub fn decorators_for_operation(
220        &self,
221        node_id: MastNodeId,
222        local_op_idx: usize,
223    ) -> &[DecoratorId] {
224        self.op_decorator_storage
225            .decorator_ids_for_operation(node_id, local_op_idx)
226            .unwrap_or(&[])
227    }
228
229    /// Returns decorator links for a node, including operation indices.
230    pub(super) fn decorator_links_for_node(
231        &self,
232        node_id: MastNodeId,
233    ) -> Result<DecoratedLinks<'_>, DecoratorIndexError> {
234        self.op_decorator_storage.decorator_links_for_node(node_id)
235    }
236
237    // DECORATOR MUTATORS
238    // --------------------------------------------------------------------------------------------
239
240    /// Adds a decorator and returns its ID.
241    pub fn add_decorator(&mut self, decorator: Decorator) -> Result<DecoratorId, MastForestError> {
242        self.decorators.push(decorator).map_err(|_| MastForestError::TooManyDecorators)
243    }
244
245    /// Returns a mutable reference the decorator with the given ID, if it exists.
246    pub(super) fn decorator_mut(&mut self, decorator_id: DecoratorId) -> Option<&mut Decorator> {
247        if decorator_id.to_usize() < self.decorators.len() {
248            Some(&mut self.decorators[decorator_id])
249        } else {
250            None
251        }
252    }
253
254    /// Adds node-level decorators (before_enter and after_exit) for the given node.
255    ///
256    /// # Note
257    /// This method does not validate decorator IDs immediately. Validation occurs during
258    /// operations that need to access the actual decorator data (e.g., merging, serialization).
259    pub(super) fn register_node_decorators(
260        &mut self,
261        node_id: MastNodeId,
262        before_enter: &[DecoratorId],
263        after_exit: &[DecoratorId],
264    ) {
265        self.node_decorator_storage
266            .add_node_decorators(node_id, before_enter, after_exit);
267    }
268
269    /// Registers operation-indexed decorators for a node.
270    ///
271    /// This associates already-added decorators with specific operations within a node.
272    pub(crate) fn register_op_indexed_decorators(
273        &mut self,
274        node_id: MastNodeId,
275        decorators_info: Vec<(usize, DecoratorId)>,
276    ) -> Result<(), crate::mast::debuginfo::decorator_storage::DecoratorIndexError> {
277        self.op_decorator_storage.add_decorator_info_for_node(node_id, decorators_info)
278    }
279
280    /// Clears all decorator information while preserving error codes.
281    ///
282    /// This is used when rebuilding decorator information from nodes.
283    pub fn clear_mappings(&mut self) {
284        self.op_decorator_storage = OpToDecoratorIds::new();
285        self.node_decorator_storage.clear();
286    }
287
288    // ASSEMBLY OP ACCESSORS
289    // --------------------------------------------------------------------------------------------
290
291    /// Returns the number of AssemblyOps.
292    pub fn num_asm_ops(&self) -> usize {
293        self.asm_ops.len()
294    }
295
296    /// Returns all AssemblyOps as a slice.
297    pub fn asm_ops(&self) -> &[AssemblyOp] {
298        self.asm_ops.as_slice()
299    }
300
301    /// Returns the AssemblyOp with the given ID, if it exists.
302    pub fn asm_op(&self, asm_op_id: AsmOpId) -> Option<&AssemblyOp> {
303        self.asm_ops.get(asm_op_id)
304    }
305
306    /// Returns the AssemblyOp for a specific operation within a node, if any.
307    pub fn asm_op_for_operation(&self, node_id: MastNodeId, op_idx: usize) -> Option<&AssemblyOp> {
308        let asm_op_id = self.asm_op_storage.asm_op_id_for_operation(node_id, op_idx)?;
309        self.asm_ops.get(asm_op_id)
310    }
311
312    /// Returns the first AssemblyOp for a node, if any.
313    pub fn first_asm_op_for_node(&self, node_id: MastNodeId) -> Option<&AssemblyOp> {
314        let asm_op_id = self.asm_op_storage.first_asm_op_for_node(node_id)?;
315        self.asm_ops.get(asm_op_id)
316    }
317
318    // ASSEMBLY OP MUTATORS
319    // --------------------------------------------------------------------------------------------
320
321    /// Adds an AssemblyOp and returns its ID.
322    pub fn add_asm_op(&mut self, asm_op: AssemblyOp) -> Result<AsmOpId, MastForestError> {
323        self.asm_ops.push(asm_op).map_err(|_| MastForestError::TooManyDecorators)
324    }
325
326    /// Registers operation-indexed AssemblyOps for a node.
327    ///
328    /// The `num_operations` parameter must be the total number of operations in the node. This is
329    /// needed to allocate enough space for all operations, even those without AssemblyOps, so that
330    /// lookups at any valid operation index will work correctly.
331    pub fn register_asm_ops(
332        &mut self,
333        node_id: MastNodeId,
334        num_operations: usize,
335        asm_ops: Vec<(usize, AsmOpId)>,
336    ) -> Result<(), AsmOpIndexError> {
337        self.asm_op_storage.add_asm_ops_for_node(node_id, num_operations, asm_ops)
338    }
339
340    /// Remaps the asm_op_storage to use new node IDs after nodes have been removed/reordered.
341    ///
342    /// This should be called after nodes are removed from the MastForest to ensure the asm_op
343    /// storage still references valid node IDs.
344    pub(super) fn remap_asm_op_storage(&mut self, remapping: &BTreeMap<MastNodeId, MastNodeId>) {
345        self.asm_op_storage = self.asm_op_storage.remap_nodes(remapping);
346    }
347
348    // ERROR CODE METHODS
349    // --------------------------------------------------------------------------------------------
350
351    /// Returns an error message by code.
352    pub fn error_message(&self, code: u64) -> Option<Arc<str>> {
353        self.error_codes.get(&code).cloned()
354    }
355
356    /// Returns an iterator over error codes.
357    pub fn error_codes(&self) -> impl Iterator<Item = (&u64, &Arc<str>)> {
358        self.error_codes.iter()
359    }
360
361    /// Inserts an error code with its message.
362    pub fn insert_error_code(&mut self, code: u64, msg: Arc<str>) {
363        self.error_codes.insert(code, msg);
364    }
365
366    /// Inserts multiple error codes at once.
367    ///
368    /// This is used when bulk error code insertion is needed.
369    pub fn extend_error_codes<I>(&mut self, error_codes: I)
370    where
371        I: IntoIterator<Item = (u64, Arc<str>)>,
372    {
373        self.error_codes.extend(error_codes);
374    }
375
376    /// Clears all error codes.
377    ///
378    /// This is used when error code information needs to be reset.
379    pub fn clear_error_codes(&mut self) {
380        self.error_codes.clear();
381    }
382
383    // PROCEDURE NAME METHODS
384    // --------------------------------------------------------------------------------------------
385
386    /// Returns the procedure name for the given MAST root digest, if present.
387    pub fn procedure_name(&self, digest: &Word) -> Option<&str> {
388        self.procedure_names.get(&LexicographicWord::from(*digest)).map(|s| s.as_ref())
389    }
390
391    /// Returns an iterator over all (digest, name) pairs.
392    pub fn procedure_names(&self) -> impl Iterator<Item = (Word, &Arc<str>)> {
393        self.procedure_names.iter().map(|(key, name)| (key.into_inner(), name))
394    }
395
396    /// Returns the number of procedure names.
397    pub fn num_procedure_names(&self) -> usize {
398        self.procedure_names.len()
399    }
400
401    /// Inserts a procedure name for the given MAST root digest.
402    pub fn insert_procedure_name(&mut self, digest: Word, name: Arc<str>) {
403        self.procedure_names.insert(LexicographicWord::from(digest), name);
404    }
405
406    /// Inserts multiple procedure names at once.
407    pub fn extend_procedure_names<I>(&mut self, names: I)
408    where
409        I: IntoIterator<Item = (Word, Arc<str>)>,
410    {
411        self.procedure_names
412            .extend(names.into_iter().map(|(d, n)| (LexicographicWord::from(d), n)));
413    }
414
415    /// Clears all procedure names.
416    pub fn clear_procedure_names(&mut self) {
417        self.procedure_names.clear();
418    }
419
420    // VALIDATION
421    // --------------------------------------------------------------------------------------------
422
423    /// Validate the integrity of the DebugInfo structure.
424    ///
425    /// This validates:
426    /// - All CSR structures in op_decorator_storage
427    /// - All CSR structures in node_decorator_storage
428    /// - All CSR structures in asm_op_storage
429    /// - All decorator IDs reference valid decorators
430    /// - All AsmOpIds reference valid AssemblyOps
431    pub(super) fn validate(&self) -> Result<(), String> {
432        let decorator_count = self.decorators.len();
433        let asm_op_count = self.asm_ops.len();
434
435        // Validate OpToDecoratorIds CSR
436        self.op_decorator_storage.validate_csr(decorator_count)?;
437
438        // Validate NodeToDecoratorIds CSR
439        self.node_decorator_storage.validate_csr(decorator_count)?;
440
441        // Validate OpToAsmOpId CSR
442        self.asm_op_storage.validate_csr(asm_op_count)?;
443
444        Ok(())
445    }
446
447    // TEST HELPERS
448    // --------------------------------------------------------------------------------------------
449
450    /// Returns the operation decorator storage.
451    #[cfg(test)]
452    pub(crate) fn op_decorator_storage(&self) -> &OpToDecoratorIds {
453        &self.op_decorator_storage
454    }
455}
456
457impl Serializable for DebugInfo {
458    fn write_into<W: ByteWriter>(&self, target: &mut W) {
459        // 1. Serialize decorators (data, string table, infos)
460        let mut decorator_data_builder = DecoratorDataBuilder::new();
461        for decorator in self.decorators.iter() {
462            decorator_data_builder.add_decorator(decorator);
463        }
464        let (decorator_data, decorator_infos, string_table) = decorator_data_builder.finalize();
465
466        decorator_data.write_into(target);
467        string_table.write_into(target);
468        decorator_infos.write_into(target);
469
470        // 2. Serialize error codes
471        let error_codes: BTreeMap<u64, String> =
472            self.error_codes.iter().map(|(k, v)| (*k, v.to_string())).collect();
473        error_codes.write_into(target);
474
475        // 3. Serialize OpToDecoratorIds CSR (dense representation)
476        // Dense representation: serialize indptr arrays as-is (no sparse encoding).
477        // Analysis shows sparse saves <1KB even with 90% empty nodes, not worth complexity.
478        // See measurement: https://gist.github.com/huitseeker/7379e2eecffd7020ae577e986057a400
479        self.op_decorator_storage.write_into(target);
480
481        // 4. Serialize NodeToDecoratorIds CSR (dense representation)
482        self.node_decorator_storage.write_into(target);
483
484        // 5. Serialize procedure names
485        let procedure_names: BTreeMap<Word, String> =
486            self.procedure_names().map(|(k, v)| (k, v.to_string())).collect();
487        procedure_names.write_into(target);
488
489        // 6. Serialize AssemblyOps (data, string table, infos)
490        let mut asm_op_data_builder = AsmOpDataBuilder::new();
491        for asm_op in self.asm_ops.iter() {
492            asm_op_data_builder.add_asm_op(asm_op);
493        }
494        let (asm_op_data, asm_op_infos, asm_op_string_table) = asm_op_data_builder.finalize();
495
496        asm_op_data.write_into(target);
497        asm_op_string_table.write_into(target);
498        asm_op_infos.write_into(target);
499
500        // 7. Serialize OpToAsmOpId CSR (dense representation)
501        self.asm_op_storage.write_into(target);
502    }
503}
504
505impl Deserializable for DebugInfo {
506    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
507        // 1. Read decorator data and string table
508        let decorator_data: Vec<u8> = Deserializable::read_from(source)?;
509        let string_table: StringTable = Deserializable::read_from(source)?;
510        let decorator_infos: Vec<DecoratorInfo> = Deserializable::read_from(source)?;
511
512        // 2. Reconstruct decorators
513        let mut decorators = IndexVec::new();
514        for decorator_info in decorator_infos {
515            let decorator = decorator_info.try_into_decorator(&string_table, &decorator_data)?;
516            decorators.push(decorator).map_err(|_| {
517                DeserializationError::InvalidValue(
518                    "Failed to add decorator to IndexVec".to_string(),
519                )
520            })?;
521        }
522
523        // 3. Read error codes
524        let error_codes_raw: BTreeMap<u64, String> = Deserializable::read_from(source)?;
525        let error_codes: BTreeMap<u64, Arc<str>> =
526            error_codes_raw.into_iter().map(|(k, v)| (k, Arc::from(v.as_str()))).collect();
527
528        // 4. Read OpToDecoratorIds CSR (dense representation)
529        let op_decorator_storage = OpToDecoratorIds::read_from(source, decorators.len())?;
530
531        // 5. Read NodeToDecoratorIds CSR (dense representation)
532        let node_decorator_storage = NodeToDecoratorIds::read_from(source, decorators.len())?;
533
534        // 6. Read procedure names
535        // Note: Procedure name digests are validated at the MastForest level (in
536        // MastForest::validate) to ensure they reference actual procedures in the forest.
537        let procedure_names_raw: BTreeMap<Word, String> = Deserializable::read_from(source)?;
538        let procedure_names: BTreeMap<LexicographicWord, Arc<str>> = procedure_names_raw
539            .into_iter()
540            .map(|(k, v)| (LexicographicWord::from(k), Arc::from(v.as_str())))
541            .collect();
542
543        // 7. Read AssemblyOps (data, string table, infos)
544        let asm_op_data: Vec<u8> = Deserializable::read_from(source)?;
545        let asm_op_string_table: StringTable = Deserializable::read_from(source)?;
546        let asm_op_infos: Vec<AsmOpInfo> = Deserializable::read_from(source)?;
547
548        // 8. Reconstruct AssemblyOps
549        let mut asm_ops = IndexVec::new();
550        for asm_op_info in asm_op_infos {
551            let asm_op = asm_op_info.try_into_asm_op(&asm_op_string_table, &asm_op_data)?;
552            asm_ops.push(asm_op).map_err(|_| {
553                DeserializationError::InvalidValue(
554                    "Failed to add AssemblyOp to IndexVec".to_string(),
555                )
556            })?;
557        }
558
559        // 9. Read OpToAsmOpId CSR (dense representation)
560        let asm_op_storage = OpToAsmOpId::read_from(source, asm_ops.len())?;
561
562        // 10. Construct and validate DebugInfo
563        let debug_info = DebugInfo {
564            decorators,
565            op_decorator_storage,
566            node_decorator_storage,
567            asm_ops,
568            asm_op_storage,
569            error_codes,
570            procedure_names,
571        };
572
573        debug_info.validate().map_err(|e| {
574            DeserializationError::InvalidValue(format!("DebugInfo validation failed: {}", e))
575        })?;
576
577        Ok(debug_info)
578    }
579}
580
581impl Default for DebugInfo {
582    fn default() -> Self {
583        Self::new()
584    }
585}