Skip to main content

miden_mast_package/debug_info/
mod.rs

1//! Debug information sections for MASP packages.
2//!
3//! This module provides types for encoding source-level debug information in the
4//! `debug_types`, `debug_sources`, and `debug_functions` custom sections of a MASP package.
5//! This information is used by debuggers to map between the Miden VM execution state
6//! and the original source code.
7
8#[cfg(feature = "arbitrary")]
9mod arbitrary;
10mod builder;
11mod serialization;
12mod types;
13
14use alloc::{sync::Arc, vec::Vec};
15
16pub use builder::*;
17use miden_core::mast::{MastForestRootMap, MastNodeId};
18#[cfg(all(feature = "arbitrary", test))]
19use miden_core::serde::{Deserializable, Serializable};
20use miden_debug_types::{Location, Uri};
21use miden_utils_indexing::{Idx, IndexVec};
22pub use types::*;
23
24type FxHashMap<K, V> = hashbrown::HashMap<K, V, rustc_hash::FxBuildHasher>;
25type FxHashSet<K> = hashbrown::HashSet<K, rustc_hash::FxBuildHasher>;
26
27pub const DEBUG_INFO_VERSION: u8 = 2;
28
29// PACKAGE DEBUG INFO
30// ================================================================================================
31
32/// Trusted package-owned debug information decoded from well-known debug sections.
33#[cfg_attr(
34    all(feature = "arbitrary", test),
35    miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false))
36)]
37pub type PackageDebugInfo = DebugInfo<MastNodeId, DebugSourceNodeId>;
38
39/// Represents debug information bound to a pending/finalized [`miden_core::mast::MastForest`].
40///
41/// This includes all debug information needed for source-level debugging, and recovery of program
42/// state during execution (such as the types of local variables in the source program, and their
43/// location in memory or on the operand stack).
44#[derive(Eq, PartialEq)]
45pub struct DebugInfo<Exec: Idx, Src: Idx> {
46    /// The version tag associated with this debug info instance
47    version: u8,
48    /// Strings referenced by records in this debug info instance
49    strings: IndexVec<DebugStringIdx, Arc<str>>,
50    /// Source file table
51    ///
52    /// Currently this maintains the set of source paths referenced by this debug info instance,
53    /// as well as an optional checksum of the content at the point its source was captured so it
54    /// can be compared later.
55    files: IndexVec<DebugFileIdx, DebugFileInfo>,
56    /// Source locations table
57    ///
58    /// Unique source locations referenced by this debug info instance.
59    locations: IndexVec<DebugLocIdx, DebugLoc>,
60    /// Type table containing uniqued type definitions referenced by this debug info instance.
61    types: IndexVec<DebugTypeIdx, DebugTypeInfo>,
62    /// Function debug information
63    ///
64    /// This information is used to map source-level function information on to source nodes, or
65    /// directly to a MAST root in cases where no source node is known, but the procedure root is.
66    ///
67    /// Function information includes, source-level name, linkage name, source file, line/column,
68    /// type signature and MAST root. A few of these are optional as they are not always available.
69    /// Information available is best-effort.
70    functions: IndexVec<DebugFunctionIdx, FunctionInfo<Src>>,
71    /// Source/debug occurrence nodes.
72    ///
73    /// This represents all instruction-level debug information for a given execution node in the
74    /// MAST forest. Multiple source nodes can exist for a given execution node, depending on how
75    /// many source occurances produced the same node (i.e. same MAST root).
76    nodes: IndexVec<Src, SourceNode<Exec, Src>>,
77    /// Source/debug occurrence roots.
78    ///
79    /// Roots are source nodes which correspond to procedure roots in the MAST forest.
80    roots: Vec<Src>,
81    /// Assertion error messages uniqued by runtime error code.
82    error_messages: Vec<DebugErrorMessage>,
83}
84
85/// Index remapping produced when importing the shared tables of one [`DebugInfo`] into another.
86///
87/// Source nodes are intentionally excluded because their indices depend on how the caller maps or
88/// filters the source graph.
89#[derive(Clone, Debug, Default)]
90pub struct DebugInfoTableRemapping {
91    strings: IndexVec<DebugStringIdx, DebugStringIdx>,
92    files: IndexVec<DebugFileIdx, DebugFileIdx>,
93    locations: IndexVec<DebugLocIdx, DebugLocIdx>,
94    types: IndexVec<DebugTypeIdx, DebugTypeIdx>,
95    /// While function records link a source node, for purposes of remapping, we strip the source
96    /// node information, and then restore it later when finalizing the debug info
97    functions: IndexVec<DebugFunctionIdx, DebugFunctionIdx>,
98}
99
100impl DebugInfoTableRemapping {
101    /// Returns the destination index for a source string index.
102    pub fn string(&self, index: DebugStringIdx) -> Option<DebugStringIdx> {
103        self.strings.get(index).copied()
104    }
105
106    /// Returns the destination index for a source file index.
107    pub fn file(&self, index: DebugFileIdx) -> Option<DebugFileIdx> {
108        self.files.get(index).copied()
109    }
110
111    /// Returns the destination index for a source location index.
112    pub fn location(&self, index: DebugLocIdx) -> Option<DebugLocIdx> {
113        self.locations.get(index).copied()
114    }
115
116    /// Returns the destination index for a source type index.
117    pub fn ty(&self, index: DebugTypeIdx) -> Option<DebugTypeIdx> {
118        self.types.get(index).copied()
119    }
120
121    /// Returns the destination index for a function index.
122    pub fn function(&self, index: DebugFunctionIdx) -> Option<DebugFunctionIdx> {
123        self.functions.get(index).copied()
124    }
125}
126
127// FUNDAMENTAL TRAIT IMPLS
128// ================================================================================================
129
130impl<Exec: Idx, Src: Idx> Default for DebugInfo<Exec, Src> {
131    fn default() -> Self {
132        Self {
133            version: DEBUG_INFO_VERSION,
134            strings: Default::default(),
135            files: Default::default(),
136            locations: Default::default(),
137            types: Default::default(),
138            functions: Default::default(),
139            nodes: Default::default(),
140            roots: Default::default(),
141            error_messages: Default::default(),
142        }
143    }
144}
145
146impl<Exec, Src> Clone for DebugInfo<Exec, Src>
147where
148    Exec: Idx + Clone,
149    Src: Idx + Clone,
150{
151    fn clone(&self) -> Self {
152        Self {
153            version: self.version,
154            strings: self.strings.clone(),
155            files: self.files.clone(),
156            locations: self.locations.clone(),
157            types: self.types.clone(),
158            functions: self.functions.clone(),
159            nodes: self.nodes.clone(),
160            roots: self.roots.clone(),
161            error_messages: self.error_messages.clone(),
162        }
163    }
164}
165
166impl<Exec, Src> core::fmt::Debug for DebugInfo<Exec, Src>
167where
168    Exec: Idx + core::fmt::Debug,
169    Src: Idx + core::fmt::Debug,
170{
171    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172        f.debug_struct("DebugInfo")
173            .field("version", &self.version)
174            .field("strings", &self.strings)
175            .field("files", &self.files)
176            .field("locations", &self.locations)
177            .field("types", &self.types)
178            .field("functions", &self.functions)
179            .field("nodes", &self.nodes)
180            .field("roots", &self.roots)
181            .field("error_messages", &self.error_messages)
182            .finish()
183    }
184}
185
186// INDEXING
187// ================================================================================================
188
189impl<Exec: Idx, Src: Idx> core::ops::Index<DebugStringIdx> for DebugInfo<Exec, Src> {
190    type Output = Arc<str>;
191
192    fn index(&self, index: DebugStringIdx) -> &Self::Output {
193        &self.strings[index]
194    }
195}
196
197impl<Exec: Idx, Src: Idx> core::ops::Index<DebugFileIdx> for DebugInfo<Exec, Src> {
198    type Output = DebugFileInfo;
199
200    fn index(&self, index: DebugFileIdx) -> &Self::Output {
201        &self.files[index]
202    }
203}
204
205impl<Exec: Idx, Src: Idx> core::ops::Index<DebugFunctionIdx> for DebugInfo<Exec, Src> {
206    type Output = FunctionInfo<Src>;
207
208    fn index(&self, index: DebugFunctionIdx) -> &Self::Output {
209        &self.functions[index]
210    }
211}
212
213impl<Exec: Idx, Src: Idx> core::ops::Index<DebugTypeIdx> for DebugInfo<Exec, Src> {
214    type Output = DebugTypeInfo;
215
216    fn index(&self, index: DebugTypeIdx) -> &Self::Output {
217        &self.types[index]
218    }
219}
220
221impl<Exec: Idx, Src: Idx> core::ops::Index<DebugLocIdx> for DebugInfo<Exec, Src> {
222    type Output = DebugLoc;
223
224    fn index(&self, index: DebugLocIdx) -> &Self::Output {
225        &self.locations[index]
226    }
227}
228
229/// A marker trait for [Idx] impls that may be used as a source node index with [DebugInfo]
230///
231/// This is needed to avoid coherence issues with [core::ops::Index] impls for [DebugInfo]
232pub trait SourceNodeIdMarker: Idx + core::hash::Hash {}
233
234impl<Exec: Idx, Src: SourceNodeIdMarker> core::ops::Index<Src> for DebugInfo<Exec, Src> {
235    type Output = SourceNode<Exec, Src>;
236
237    fn index(&self, index: Src) -> &Self::Output {
238        &self.nodes[index]
239    }
240}
241
242// ACCESSORS
243// ================================================================================================
244
245impl<Exec: Idx, Src: Idx> DebugInfo<Exec, Src> {
246    /// Get the version of this debug info instance
247    pub fn version(&self) -> u8 {
248        self.version
249    }
250
251    /// Get access to the strings table in this debug info
252    pub fn strings(&self) -> &IndexVec<DebugStringIdx, Arc<str>> {
253        &self.strings
254    }
255
256    /// Gets a string by index.
257    pub fn get_string(&self, idx: DebugStringIdx) -> Option<Arc<str>> {
258        self.strings.get(idx).cloned()
259    }
260
261    /// Get access to the files table in this debug info
262    pub fn files(&self) -> &IndexVec<DebugFileIdx, DebugFileInfo> {
263        &self.files
264    }
265
266    /// Gets a file by index.
267    pub fn get_file(&self, idx: DebugFileIdx) -> Option<&DebugFileInfo> {
268        self.files.get(idx)
269    }
270
271    /// Gets the [DebugFileIdx] for a source file whose URI is `uri`, if it is recorded in the
272    /// debug info built so far.
273    pub fn get_file_index_by_uri(&self, uri: &Uri) -> Option<DebugFileIdx> {
274        self.files
275            .iter()
276            .position(|file| {
277                self.strings
278                    .get(file.path_idx)
279                    .map(|path| path.as_ref() == uri.as_str())
280                    .unwrap_or(false)
281            })
282            .map(|pos| DebugFileIdx::from(pos as u32))
283    }
284
285    /// Apply `trimmer` to every distinct file path referenced by the file table.
286    ///
287    /// If `trimmer` returns `None`, the file path is left unmodified. Otherwise, the returned path
288    /// is interned and the corresponding file records are retargeted to it. Other debug records
289    /// which reference the original string are left unchanged.
290    pub fn trim_file_paths(&mut self, mut trimmer: impl FnMut(&str) -> Option<Arc<str>>) {
291        use hashbrown::hash_map::Entry;
292
293        let mut string_indices = FxHashMap::<Arc<str>, DebugStringIdx>::default();
294        for (index, string) in self.strings.iter().enumerate() {
295            string_indices
296                .entry(string.clone())
297                .or_insert_with(|| DebugStringIdx::from(index as u32));
298        }
299
300        // Multiple file rows may share a path string (for example, when they have different
301        // checksums). Apply the trimmer once per path and retarget each file row to the result.
302        // Appending/reusing a string rather than mutating the original preserves unrelated records
303        // which happen to reference the same globally-interned string.
304        let mut remapped_paths = FxHashMap::<DebugStringIdx, DebugStringIdx>::default();
305        for file in self.files.iter_mut() {
306            let old_path_idx = file.path_idx;
307            let new_path_idx = if let Some(new_path_idx) = remapped_paths.get(&old_path_idx) {
308                *new_path_idx
309            } else {
310                let path = self.strings[old_path_idx].clone();
311                let new_path_idx = match trimmer(path.as_ref()) {
312                    None => old_path_idx,
313                    Some(new_path) => match string_indices.entry(new_path.clone()) {
314                        Entry::Occupied(entry) => *entry.get(),
315                        Entry::Vacant(entry) => {
316                            let index =
317                                self.strings.push(new_path).expect("too many debug info strings");
318                            entry.insert(index);
319                            index
320                        },
321                    },
322                };
323                remapped_paths.insert(old_path_idx, new_path_idx);
324                new_path_idx
325            };
326            file.path_idx = new_path_idx;
327        }
328    }
329
330    /// Get access to the types table in this debug info
331    pub fn types(&self) -> &IndexVec<DebugTypeIdx, DebugTypeInfo> {
332        &self.types
333    }
334
335    /// Gets a type by index.
336    pub fn get_type(&self, idx: DebugTypeIdx) -> Option<&DebugTypeInfo> {
337        self.types.get(idx)
338    }
339
340    /// Get access to the locatinos table in this debug info
341    pub fn locations(&self) -> &IndexVec<DebugLocIdx, DebugLoc> {
342        &self.locations
343    }
344
345    /// Returns the deduplicated source locations referenced by assembly operation rows.
346    pub fn get_location(&self, idx: DebugLocIdx) -> Option<Location> {
347        let DebugLoc { file_idx, start, end } = self.locations.get(idx)?;
348        let file = &self.files[*file_idx];
349        let uri = self.strings[file.path_idx].clone();
350        Some(Location {
351            uri: Uri::from(uri),
352            start: *start,
353            end: *end,
354        })
355    }
356
357    /// Get access to the error messages table in this debug info
358    pub fn error_messages(&self) -> &[DebugErrorMessage] {
359        &self.error_messages
360    }
361
362    /// Returns the assertion error message for `err_code`, if present.
363    pub fn error_message(&self, err_code: u64) -> Option<Arc<str>> {
364        self.error_messages
365            .iter()
366            .find(|row| row.err_code == err_code)
367            .map(|row| self.strings[row.message].clone())
368    }
369
370    /// Returns source/debug occurrence nodes.
371    pub fn nodes(&self) -> &IndexVec<Src, SourceNode<Exec, Src>> {
372        &self.nodes
373    }
374
375    /// Returns source/debug occurrence roots.
376    pub fn roots(&self) -> &[Src] {
377        &self.roots
378    }
379
380    /// Returns a source/debug occurrence by ID.
381    pub fn source_node(&self, source_node: Src) -> Option<&SourceNode<Exec, Src>> {
382        self.nodes.get(source_node)
383    }
384
385    /// Get access to the functions table in this debug info
386    pub fn functions(&self) -> &[FunctionInfo<Src>] {
387        self.functions.as_slice()
388    }
389
390    /// Gets the function info for `idx`
391    pub fn get_function(&self, idx: DebugFunctionIdx) -> Option<&FunctionInfo<Src>> {
392        self.functions.get(idx)
393    }
394
395    /// Returns all source/debug roots that point at `exec_node`.
396    pub fn source_roots_for_exec_node(
397        &self,
398        exec_node: Exec,
399    ) -> impl Iterator<Item = (Src, &SourceNode<Exec, Src>)> {
400        self.roots.iter().copied().filter_map(move |source_node_id| {
401            let source_node = &self.nodes[source_node_id];
402            if source_node.exec_node == exec_node {
403                Some((source_node_id, source_node))
404            } else {
405                None
406            }
407        })
408    }
409
410    /// Returns the unique source/debug root that points at `exec_node`.
411    pub fn unique_source_root_for_exec_node(
412        &self,
413        exec_node: Exec,
414    ) -> Result<Option<Src>, SourceGraphLookupError<Exec, Src>> {
415        let mut roots = self
416            .source_roots_for_exec_node(exec_node)
417            .map(|(source_node_id, _)| source_node_id);
418        let first = roots.next();
419        if roots.next().is_some() {
420            return Err(SourceGraphLookupError::AmbiguousRoot { exec_node });
421        }
422        Ok(first)
423    }
424
425    /// Returns `parent`'s source/debug child at `child_index`, if present.
426    pub fn child_source_node(
427        &self,
428        parent: Src,
429        child_index: usize,
430    ) -> Result<Option<(Src, &SourceNode<Exec, Src>)>, SourceGraphLookupError<Exec, Src>> {
431        let parent_node = self
432            .source_node(parent)
433            .ok_or(SourceGraphLookupError::MissingSourceNode { source_node: parent })?;
434        let Some(child) = parent_node.children.get(child_index).copied() else {
435            return Ok(None);
436        };
437        let child_node = self
438            .source_node(child)
439            .ok_or(SourceGraphLookupError::MissingSourceNode { source_node: child })?;
440
441        Ok(Some((child, child_node)))
442    }
443
444    /// Returns assembly operation rows for a source/debug occurrence.
445    pub fn asm_ops_for_source_node(
446        &self,
447        source_node: Src,
448    ) -> impl Iterator<Item = &DebugSourceAsmOp> {
449        self.source_node(source_node).into_iter().flat_map(|node| node.asm_ops.iter())
450    }
451
452    /// Returns the first assembly operation row for `source_node`, if present.
453    pub fn first_asm_op_for_source_node(&self, source_node: Src) -> Option<&DebugSourceAsmOp> {
454        self.asm_ops_for_source_node(source_node).min_by_key(|row| row.op_idx)
455    }
456
457    /// Returns the assembly operation row for `source_node` at or before `op_idx`, if present.
458    pub fn asm_op_for_operation(&self, source_node: Src, op_idx: u32) -> Option<&DebugSourceAsmOp> {
459        self.asm_ops_for_source_node(source_node)
460            .filter(|row| row.op_idx <= op_idx)
461            .max_by_key(|row| row.op_idx)
462    }
463
464    /// Returns debug variable rows for a source/debug occurrence.
465    pub fn debug_vars_for_source_node(
466        &self,
467        source_node: Src,
468    ) -> impl Iterator<Item = &DebugSourceVar> {
469        self.source_node(source_node)
470            .into_iter()
471            .flat_map(|node| node.debug_vars.iter())
472    }
473
474    /// Returns debug variable rows for `source_node` at `op_idx`.
475    pub fn debug_vars_for_operation(
476        &self,
477        source_node: Src,
478        op_idx: u32,
479    ) -> impl Iterator<Item = &DebugSourceVar> {
480        self.debug_vars_for_source_node(source_node)
481            .filter(move |row| row.op_idx == op_idx)
482    }
483
484    /// Returns inline-call rows for a source/debug occurrence.
485    pub fn inline_calls_for_source_node(
486        &self,
487        source_node: Src,
488    ) -> impl Iterator<Item = &DebugSourceInlineCall> {
489        self.source_node(source_node)
490            .into_iter()
491            .flat_map(|node| node.inline_calls.iter())
492    }
493
494    /// Returns inline-call rows for `source_node` at `op_idx`.
495    pub fn inline_calls_for_operation(
496        &self,
497        source_node: Src,
498        op_idx: u32,
499    ) -> impl Iterator<Item = &DebugSourceInlineCall> {
500        self.inline_calls_for_source_node(source_node)
501            .filter(move |row| row.op_idx == op_idx)
502    }
503}
504
505impl<Exec: Idx, Src: Idx> DebugInfo<Exec, Src> {
506    /// Imports the shared string, type, file, location, and error-message tables into `target`.
507    ///
508    /// The returned map translates every source table index to its destination index. Type rows
509    /// are reserved as a complete batch before their payloads are rewritten, which preserves
510    /// forward and cyclic references. Functions are imported with their source-node associations
511    /// cleared; callers which also import source nodes must restore those associations after
512    /// establishing the source-node mapping.
513    pub fn merge_tables_into<TargetExec: Idx, TargetSrc: Idx>(
514        &self,
515        target: &mut DebugInfoBuilder<TargetExec, TargetSrc>,
516    ) -> Result<DebugInfoTableRemapping, DebugInfoTableRemapError> {
517        // Validate the complete source first so that an error never leaves `target` partially
518        // updated.
519        self.validate_shared_table_references()?;
520
521        let mut remapping = DebugInfoTableRemapping::default();
522
523        for (index, string) in self.strings.iter().enumerate() {
524            let source = DebugStringIdx::from(
525                u32::try_from(index).expect("invalid source string table index"),
526            );
527            let inserted = remapping
528                .strings
529                .push(target.add_string(string.clone()))
530                .expect("too many remapped strings");
531            debug_assert_eq!(inserted, source);
532        }
533
534        // Reserve the complete output range before rewriting any type row. Types may contain
535        // forward or cyclic references, so their mappings cannot be discovered incrementally.
536        let type_offset = target.debug_info().types.len();
537        for index in 0..self.types.len() {
538            let source =
539                DebugTypeIdx::from(u32::try_from(index).expect("invalid source type table index"));
540            let destination = DebugTypeIdx::from(
541                u32::try_from(type_offset + index).expect("too many types after merging"),
542            );
543            let inserted = remapping.types.push(destination).expect("too many remapped types");
544            debug_assert_eq!(inserted, source);
545        }
546        for (index, ty) in self.types.iter().enumerate() {
547            let source =
548                DebugTypeIdx::from(u32::try_from(index).expect("invalid source type table index"));
549            let ty = remap_debug_type_info(ty, &remapping.strings, &remapping.types)?;
550            let destination = target.push_type(ty);
551            debug_assert_eq!(destination, remapping.types[source]);
552        }
553
554        for (index, file) in self.files.iter().enumerate() {
555            let source =
556                DebugFileIdx::from(u32::try_from(index).expect("invalid source file table index"));
557            let path_idx = remapping.string(file.path_idx).ok_or(
558                DebugInfoTableRemapError::MissingSourceString { string_idx: file.path_idx },
559            )?;
560            let file = DebugFileInfo::new(path_idx)
561                .with_checksum(*file.checksum().unwrap_or(&DebugFileInfo::EMPTY_CHECKSUM));
562            let inserted = remapping
563                .files
564                .push(target.add_file_info(file))
565                .expect("too many remapped files");
566            debug_assert_eq!(inserted, source);
567        }
568
569        for (index, location) in self.locations.iter().enumerate() {
570            let source = DebugLocIdx::from(
571                u32::try_from(index).expect("invalid source location table index"),
572            );
573            let file_idx = remapping.file(location.file_idx).ok_or(
574                DebugInfoTableRemapError::MissingSourceFile { file_idx: location.file_idx },
575            )?;
576            let location = DebugLoc {
577                file_idx,
578                start: location.start,
579                end: location.end,
580            };
581            let inserted = remapping
582                .locations
583                .push(target.add_location_info(location))
584                .expect("too many remapped locations");
585            debug_assert_eq!(inserted, source);
586        }
587
588        for error_message in self.error_messages() {
589            let message = remapping.string(error_message.message).ok_or(
590                DebugInfoTableRemapError::MissingSourceString { string_idx: error_message.message },
591            )?;
592            target.add_error_message_with_index(error_message.err_code, message);
593        }
594
595        for (index, function) in self.functions().iter().enumerate() {
596            let source = DebugFunctionIdx::from(
597                u32::try_from(index).expect("invalid source function table index"),
598            );
599            let name_idx = remapping.string(function.name_idx).ok_or(
600                DebugInfoTableRemapError::MissingSourceString { string_idx: function.name_idx },
601            )?;
602            let linkage_name_idx = function
603                .linkage_name_idx
604                .try_into_option()
605                .map_err(|err| DebugInfoTableRemapError::InvalidOptionField {
606                    context: "debug function linkage name",
607                    err,
608                })?
609                .map(|index| {
610                    remapping
611                        .string(index)
612                        .ok_or(DebugInfoTableRemapError::MissingSourceString { string_idx: index })
613                })
614                .transpose()?;
615            let type_idx = function
616                .type_idx
617                .try_into_option()
618                .map_err(|err| DebugInfoTableRemapError::InvalidOptionField {
619                    context: "debug function type",
620                    err,
621                })?
622                .map(|tid| {
623                    remapping.ty(tid).ok_or(DebugInfoTableRemapError::MissingType { type_idx: tid })
624                })
625                .transpose()?;
626            let file_idx = remapping.file(function.file_idx).ok_or(
627                DebugInfoTableRemapError::MissingSourceFile { file_idx: function.file_idx },
628            )?;
629            let function_idx = target.add_function(FunctionInfo {
630                mast_root: function.mast_root,
631                source_node: None.into(),
632                type_idx: type_idx.into(),
633                linkage_name_idx: linkage_name_idx.into(),
634                name_idx,
635                file_idx,
636                line: function.line,
637                column: function.column,
638            });
639            let inserted =
640                remapping.functions.push(function_idx).expect("too many remapped functions");
641            debug_assert_eq!(inserted, source);
642        }
643
644        Ok(remapping)
645    }
646
647    fn validate_shared_table_references(&self) -> Result<(), DebugInfoTableRemapError> {
648        for ty in self.types.iter() {
649            validate_debug_type_info(ty, &self.strings, &self.types)?;
650        }
651        for file in self.files.iter() {
652            if self.strings.get(file.path_idx).is_none() {
653                return Err(DebugInfoTableRemapError::MissingSourceString {
654                    string_idx: file.path_idx,
655                });
656            }
657        }
658        for location in self.locations.iter() {
659            if self.files.get(location.file_idx).is_none() {
660                return Err(DebugInfoTableRemapError::MissingSourceFile {
661                    file_idx: location.file_idx,
662                });
663            }
664        }
665        for error_message in self.error_messages.iter() {
666            if self.strings.get(error_message.message).is_none() {
667                return Err(DebugInfoTableRemapError::MissingSourceString {
668                    string_idx: error_message.message,
669                });
670            }
671        }
672        for function in self.functions.iter() {
673            if self.files.get(function.file_idx).is_none() {
674                return Err(DebugInfoTableRemapError::MissingSourceFile {
675                    file_idx: function.file_idx,
676                });
677            }
678            if let Some(tid) = function.type_idx.try_into_option().map_err(|err| {
679                DebugInfoTableRemapError::InvalidOptionField { context: "debug function type", err }
680            })? && self.types.get(tid).is_none()
681            {
682                return Err(DebugInfoTableRemapError::MissingType { type_idx: tid });
683            }
684            if self.strings.get(function.name_idx).is_none() {
685                return Err(DebugInfoTableRemapError::MissingSourceString {
686                    string_idx: function.name_idx,
687                });
688            }
689            if let Some(linkage_name_idx) =
690                function.linkage_name_idx.try_into_option().map_err(|err| {
691                    DebugInfoTableRemapError::InvalidOptionField {
692                        context: "debug function linkage name",
693                        err,
694                    }
695                })?
696                && self.strings.get(linkage_name_idx).is_none()
697            {
698                return Err(DebugInfoTableRemapError::MissingSourceString {
699                    string_idx: linkage_name_idx,
700                });
701            }
702        }
703        Ok(())
704    }
705}
706
707impl<Src: SourceNodeIdMarker> DebugInfo<MastNodeId, Src> {
708    /// Merges package-owned source/debug metadata after a [`miden_core::mast::MastForest`] merge.
709    ///
710    /// [`miden_core::mast::MastForest::merge`] remains execution-only. This helper applies the
711    /// returned node mappings to package source/debug sections so callers can merge
712    /// `(MastForest, PackageDebugInfo)` pairs without reattaching debug metadata to the forest.
713    ///
714    /// This also merges the type, source-file, and function tables referenced by source-map
715    /// inline-call rows.
716    pub fn merge_source_debug<'a>(
717        inputs: impl IntoIterator<Item = (usize, &'a Self)>,
718        root_map: &MastForestRootMap,
719    ) -> Result<Self, DebugInfoMergeError<MastNodeId, Src>>
720    where
721        Src: 'a,
722    {
723        let mut builder = DebugInfoBuilder::default();
724        for (forest_index, debug_info) in inputs {
725            let tables = debug_info
726                .merge_tables_into(&mut builder)
727                .map_err(|error| table_remap_error(forest_index, error))?;
728
729            let mut remapped_nodes = FxHashMap::<Src, Src>::default();
730            let start_node_index = builder.debug_info().nodes().len();
731            for i in 0..debug_info.nodes.len() {
732                let prev_index = Src::from(u32::try_from(i).expect("too many nodes"));
733                let new_index = Src::from(
734                    u32::try_from(start_node_index + i).expect("too many nodes after merging"),
735                );
736                remapped_nodes.insert(prev_index, new_index);
737            }
738
739            for (i, source_node) in debug_info.nodes.iter().enumerate() {
740                let prev_index = Src::from(u32::try_from(i).expect("too many nodes"));
741
742                let exec_node = root_map.map_node(forest_index, &source_node.exec_node).ok_or(
743                    DebugInfoMergeError::MissingExecNodeMapping {
744                        forest_index,
745                        exec_node: source_node.exec_node,
746                    },
747                )?;
748                let children = source_node
749                    .children
750                    .iter()
751                    .map(|child| {
752                        remapped_nodes.get(child).copied().ok_or(
753                            DebugInfoMergeError::MissingSourceNodeMapping {
754                                forest_index,
755                                source_node: *child,
756                            },
757                        )
758                    })
759                    .collect::<Result<Vec<_>, _>>()?;
760
761                let mut asm_ops = Vec::with_capacity(source_node.asm_ops.len());
762                for row in source_node.asm_ops.iter() {
763                    let location_idx = row
764                        .location_idx
765                        .try_into_option()
766                        .map_err(|err| DebugInfoMergeError::InvalidOptionField {
767                            forest_index,
768                            context: "debug source assembly op location",
769                            err,
770                        })?
771                        .map(|location_idx| {
772                            tables.location(location_idx).ok_or(
773                                DebugInfoMergeError::MissingSourceLocationMapping {
774                                    forest_index,
775                                    location_idx,
776                                },
777                            )
778                        })
779                        .transpose()?;
780                    let context_name_idx = tables.string(row.context_name_idx).ok_or(
781                        DebugInfoMergeError::MissingSourceStringMapping {
782                            forest_index,
783                            string_idx: row.context_name_idx,
784                        },
785                    )?;
786                    let op_name_idx = tables.string(row.op_name_idx).ok_or(
787                        DebugInfoMergeError::MissingSourceStringMapping {
788                            forest_index,
789                            string_idx: row.op_name_idx,
790                        },
791                    )?;
792                    asm_ops.push(DebugSourceAsmOp::new(
793                        row.op_idx,
794                        location_idx,
795                        context_name_idx,
796                        op_name_idx,
797                        row.num_cycles,
798                    ));
799                }
800
801                let mut debug_vars = Vec::with_capacity(source_node.debug_vars.len());
802                for row in source_node.debug_vars.iter() {
803                    let name_idx = tables.string(row.name_idx).ok_or(
804                        DebugInfoMergeError::MissingSourceStringMapping {
805                            forest_index,
806                            string_idx: row.name_idx,
807                        },
808                    )?;
809
810                    let location_idx = row
811                        .location_idx
812                        .map(|idx| {
813                            tables.location(idx).ok_or(
814                                DebugInfoMergeError::MissingSourceLocationMapping {
815                                    forest_index,
816                                    location_idx: idx,
817                                },
818                            )
819                        })
820                        .transpose()?;
821                    let type_id = row
822                        .type_id
823                        .map(|idx| {
824                            tables.ty(idx).ok_or(DebugInfoMergeError::MissingTypeMapping {
825                                forest_index,
826                                type_idx: idx,
827                            })
828                        })
829                        .transpose()?;
830                    debug_vars.push(DebugSourceVar {
831                        op_idx: row.op_idx,
832                        name_idx,
833                        type_id,
834                        arg_idx: row.arg_idx,
835                        location_idx,
836                        value_location: row.value_location.clone(),
837                    });
838                }
839                let new_index = builder
840                    .debug_info_mut()
841                    .nodes
842                    .push(SourceNode {
843                        exec_node,
844                        children,
845                        op_start: source_node.op_start,
846                        op_end: source_node.op_end,
847                        asm_ops,
848                        debug_vars,
849                        inline_calls: Vec::with_capacity(source_node.inline_calls.len()),
850                    })
851                    .expect("too many nodes");
852                debug_assert_eq!(new_index, remapped_nodes[&prev_index],);
853            }
854
855            for (index, function) in debug_info.functions().iter().enumerate() {
856                let Some(previous_source_node) =
857                    function.source_node.try_into_option().map_err(|err| {
858                        DebugInfoMergeError::InvalidOptionField {
859                            forest_index,
860                            context: "debug function source node",
861                            err,
862                        }
863                    })?
864                else {
865                    continue;
866                };
867                let source_node = remapped_nodes.get(&previous_source_node).copied().ok_or(
868                    DebugInfoMergeError::MissingSourceNodeMapping {
869                        forest_index,
870                        source_node: previous_source_node,
871                    },
872                )?;
873                let previous_function = DebugFunctionIdx::from(
874                    u32::try_from(index).expect("invalid source function table index"),
875                );
876                let function = tables.function(previous_function).ok_or(
877                    DebugInfoMergeError::MissingFunctionMapping {
878                        forest_index,
879                        function_idx: previous_function,
880                    },
881                )?;
882                builder.set_function_source_node(function, source_node);
883            }
884
885            for root in debug_info.roots().iter().copied() {
886                builder.debug_info_mut().roots.push(remapped_nodes.get(&root).copied().ok_or(
887                    DebugInfoMergeError::MissingSourceNodeMapping {
888                        forest_index,
889                        source_node: root,
890                    },
891                )?);
892            }
893
894            for (prev, new) in remapped_nodes.iter() {
895                let source_node = debug_info.source_node(*prev).unwrap();
896                if source_node.inline_calls.is_empty() {
897                    continue;
898                }
899                let target_node = &mut builder[*new];
900                for row in source_node.inline_calls.iter() {
901                    let callee_idx = tables.function(row.callee_idx).ok_or(
902                        DebugInfoMergeError::MissingFunctionMapping {
903                            forest_index,
904                            function_idx: row.callee_idx,
905                        },
906                    )?;
907                    let loc_idx = tables.location(row.loc_idx).ok_or(
908                        DebugInfoMergeError::MissingSourceLocationMapping {
909                            forest_index,
910                            location_idx: row.loc_idx,
911                        },
912                    )?;
913                    target_node.inline_calls.push(DebugSourceInlineCall {
914                        op_idx: row.op_idx,
915                        callee_idx,
916                        loc_idx,
917                    });
918                }
919            }
920        }
921
922        Ok(*builder.build())
923    }
924}
925
926fn validate_debug_type_info(
927    ty: &DebugTypeInfo,
928    strings: &IndexVec<DebugStringIdx, Arc<str>>,
929    types: &IndexVec<DebugTypeIdx, DebugTypeInfo>,
930) -> Result<(), DebugInfoTableRemapError> {
931    match ty {
932        DebugTypeInfo::Primitive(_) | DebugTypeInfo::Unknown => {},
933        DebugTypeInfo::Pointer { pointee_type_idx } => {
934            validate_type_idx(*pointee_type_idx, types)?;
935        },
936        DebugTypeInfo::Array { element_type_idx, .. } => {
937            validate_type_idx(*element_type_idx, types)?;
938        },
939        DebugTypeInfo::Struct { name_idx, fields, .. } => {
940            validate_type_string(*name_idx, strings)?;
941            for field in fields {
942                validate_type_string(field.name_idx, strings)?;
943                validate_type_idx(field.type_idx, types)?;
944            }
945        },
946        DebugTypeInfo::Function { return_type_idx, param_type_indices } => {
947            if let Some(return_type_idx) = return_type_idx {
948                validate_type_idx(*return_type_idx, types)?;
949            }
950            for &param_type_idx in param_type_indices {
951                validate_type_idx(param_type_idx, types)?;
952            }
953        },
954        DebugTypeInfo::Enum {
955            name_idx,
956            discriminant_type_idx,
957            variants,
958            ..
959        } => {
960            validate_type_string(*name_idx, strings)?;
961            validate_type_idx(*discriminant_type_idx, types)?;
962            for variant in variants {
963                validate_type_string(variant.name_idx, strings)?;
964                if let Some(type_idx) = variant.type_idx {
965                    validate_type_idx(type_idx, types)?;
966                }
967            }
968        },
969    }
970    Ok(())
971}
972
973fn validate_type_string(
974    string_idx: DebugStringIdx,
975    strings: &IndexVec<DebugStringIdx, Arc<str>>,
976) -> Result<(), DebugInfoTableRemapError> {
977    if strings.get(string_idx).is_none() {
978        Err(DebugInfoTableRemapError::MissingTypeString { string_idx })
979    } else {
980        Ok(())
981    }
982}
983
984fn validate_type_idx(
985    type_idx: DebugTypeIdx,
986    types: &IndexVec<DebugTypeIdx, DebugTypeInfo>,
987) -> Result<(), DebugInfoTableRemapError> {
988    if types.get(type_idx).is_none() {
989        Err(DebugInfoTableRemapError::MissingType { type_idx })
990    } else {
991        Ok(())
992    }
993}
994
995fn remap_debug_type_info(
996    ty: &DebugTypeInfo,
997    string_map: &IndexVec<DebugStringIdx, DebugStringIdx>,
998    type_map: &IndexVec<DebugTypeIdx, DebugTypeIdx>,
999) -> Result<DebugTypeInfo, DebugInfoTableRemapError> {
1000    Ok(match ty {
1001        DebugTypeInfo::Primitive(primitive) => DebugTypeInfo::Primitive(*primitive),
1002        DebugTypeInfo::Pointer { pointee_type_idx } => DebugTypeInfo::Pointer {
1003            pointee_type_idx: remap_type_idx(*pointee_type_idx, type_map)?,
1004        },
1005        DebugTypeInfo::Array { element_type_idx, count } => DebugTypeInfo::Array {
1006            element_type_idx: remap_type_idx(*element_type_idx, type_map)?,
1007            count: *count,
1008        },
1009        DebugTypeInfo::Struct { name_idx, size, fields } => DebugTypeInfo::Struct {
1010            name_idx: remap_type_string(*name_idx, string_map)?,
1011            size: *size,
1012            fields: fields
1013                .iter()
1014                .map(|field| {
1015                    Ok(DebugFieldInfo {
1016                        name_idx: remap_type_string(field.name_idx, string_map)?,
1017                        type_idx: remap_type_idx(field.type_idx, type_map)?,
1018                        offset: field.offset,
1019                    })
1020                })
1021                .collect::<Result<_, DebugInfoTableRemapError>>()?,
1022        },
1023        DebugTypeInfo::Function { return_type_idx, param_type_indices } => {
1024            DebugTypeInfo::Function {
1025                return_type_idx: return_type_idx
1026                    .map(|idx| remap_type_idx(idx, type_map))
1027                    .transpose()?,
1028                param_type_indices: param_type_indices
1029                    .iter()
1030                    .map(|idx| remap_type_idx(*idx, type_map))
1031                    .collect::<Result<_, _>>()?,
1032            }
1033        },
1034        DebugTypeInfo::Enum {
1035            name_idx,
1036            size,
1037            discriminant_type_idx,
1038            variants,
1039        } => DebugTypeInfo::Enum {
1040            name_idx: remap_type_string(*name_idx, string_map)?,
1041            size: *size,
1042            discriminant_type_idx: remap_type_idx(*discriminant_type_idx, type_map)?,
1043            variants: variants
1044                .iter()
1045                .map(|variant| {
1046                    Ok(DebugVariantInfo {
1047                        name_idx: remap_type_string(variant.name_idx, string_map)?,
1048                        type_idx: variant
1049                            .type_idx
1050                            .map(|idx| remap_type_idx(idx, type_map))
1051                            .transpose()?,
1052                        payload_offset: variant.payload_offset,
1053                        discriminant: variant.discriminant,
1054                    })
1055                })
1056                .collect::<Result<_, DebugInfoTableRemapError>>()?,
1057        },
1058        DebugTypeInfo::Unknown => DebugTypeInfo::Unknown,
1059    })
1060}
1061
1062fn remap_type_string(
1063    string_idx: DebugStringIdx,
1064    string_map: &IndexVec<DebugStringIdx, DebugStringIdx>,
1065) -> Result<DebugStringIdx, DebugInfoTableRemapError> {
1066    string_map
1067        .get(string_idx)
1068        .copied()
1069        .ok_or(DebugInfoTableRemapError::MissingTypeString { string_idx })
1070}
1071
1072fn remap_type_idx(
1073    type_idx: DebugTypeIdx,
1074    type_map: &IndexVec<DebugTypeIdx, DebugTypeIdx>,
1075) -> Result<DebugTypeIdx, DebugInfoTableRemapError> {
1076    type_map
1077        .get(type_idx)
1078        .copied()
1079        .ok_or(DebugInfoTableRemapError::MissingType { type_idx })
1080}
1081
1082fn table_remap_error<Exec: Idx, Src: Idx>(
1083    forest_index: usize,
1084    error: DebugInfoTableRemapError,
1085) -> DebugInfoMergeError<Exec, Src> {
1086    match error {
1087        DebugInfoTableRemapError::InvalidOptionField { context, err } => {
1088            DebugInfoMergeError::InvalidOptionField { forest_index, context, err }
1089        },
1090        DebugInfoTableRemapError::MissingTypeString { string_idx } => {
1091            DebugInfoMergeError::MissingTypeStringMapping { forest_index, string_idx }
1092        },
1093        DebugInfoTableRemapError::MissingType { type_idx } => {
1094            DebugInfoMergeError::MissingTypeMapping { forest_index, type_idx }
1095        },
1096        DebugInfoTableRemapError::MissingSourceString { string_idx } => {
1097            DebugInfoMergeError::MissingSourceStringMapping { forest_index, string_idx }
1098        },
1099        DebugInfoTableRemapError::MissingSourceFile { file_idx } => {
1100            DebugInfoMergeError::MissingSourceFileMapping { forest_index, file_idx }
1101        },
1102    }
1103}