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