Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

- Implemented project assembly ([#2877](https://github.com/0xMiden/miden-vm/pull/2877)).
- Added `FastProcessor::into_parts()` to extract advice provider, memory, and precompile transcript after step-based execution ([#2901](https://github.com/0xMiden/miden-vm/pull/2901)).
- Added `FrameBase` variant to `DebugVarLocation` and `set_value_location` to `DebugVarInfo` for frame-pointer-relative debug variable locations ([#2955](https://github.com/0xMiden/miden-vm/pull/2955)).

## 0.22.0 (2026-03-18)

Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions core/src/mast/debuginfo/asm_op_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ impl OpToAsmOpId {
entries.first().map(|(_, id)| *id)
}

/// Returns all `(op_idx, AsmOpId)` pairs for the given node, or an empty vec if the
/// node has no asm ops.
pub fn asm_ops_for_node(&self, node_id: MastNodeId) -> Vec<(usize, AsmOpId)> {
self.inner.row(node_id).map(|r| r.to_vec()).unwrap_or_default()
}

/// Validates the CSR structure integrity.
///
/// # Arguments
Expand Down
65 changes: 65 additions & 0 deletions core/src/mast/debuginfo/debug_var_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,71 @@ impl OpToDebugVarIds {
Ok(result)
}

/// Returns all `(op_idx, DebugVarId)` pairs for the given node, or an empty vec if the
/// node has no debug vars.
pub fn debug_vars_for_node(&self, node: MastNodeId) -> Vec<(usize, DebugVarId)> {
let op_range = match self.operation_range_for_node(node) {
Ok(range) => range,
Err(_) => return Vec::new(),
};

let mut result = Vec::new();
for (op_offset, op_idx) in op_range.clone().enumerate() {
if op_idx + 1 >= self.op_indptr_for_var_ids.len() {
break;
}
let var_start = self.op_indptr_for_var_ids[op_idx];
let var_end = self.op_indptr_for_var_ids[op_idx + 1];
for &var_id in &self.debug_var_ids[var_start..var_end] {
result.push((op_offset, var_id));
}
}
result
}

/// Creates a new [`OpToDebugVarIds`] with remapped node IDs.
///
/// This is used when nodes are removed from a MastForest and the remaining nodes are
/// renumbered. The remapping maps old node IDs to new node IDs.
///
/// Nodes that are not in the remapping are considered removed and their debug var data
/// is discarded.
pub fn remap_nodes(
&self,
remapping: &alloc::collections::BTreeMap<MastNodeId, MastNodeId>,
) -> Self {
if self.is_empty() {
return Self::new();
}
if remapping.is_empty() {
return self.clone();
}

let max_new_id = remapping.values().map(|id| id.to_usize()).max().unwrap_or(0);
let num_new_nodes = max_new_id + 1;

let mut new_node_data: alloc::collections::BTreeMap<usize, Vec<(usize, DebugVarId)>> =
alloc::collections::BTreeMap::new();

for (old_id, new_id) in remapping {
let vars = self.debug_vars_for_node(*old_id);
if !vars.is_empty() {
new_node_data.insert(new_id.to_usize(), vars);
}
}

let mut new_storage = Self::new();
for idx in 0..num_new_nodes {
let node_id = MastNodeId::new_unchecked(idx as u32);
let vars = new_node_data.remove(&idx).unwrap_or_default();
new_storage
.add_debug_var_info_for_node(node_id, vars)
.expect("failed to remap debug var storage");
}

new_storage
}

/// Clears this storage.
pub fn clear(&mut self) {
self.debug_var_ids.clear();
Expand Down
19 changes: 19 additions & 0 deletions core/src/mast/debuginfo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ impl DebugInfo {
self.debug_vars.get(debug_var_id)
}

/// Returns all `(op_idx, DebugVarId)` pairs for the given node, or an empty vec if the
/// node has no debug vars.
pub fn debug_vars_for_node(&self, node_id: MastNodeId) -> Vec<(usize, DebugVarId)> {
self.op_debug_var_storage.debug_vars_for_node(node_id)
}

/// Returns debug variable IDs for a specific operation within a node.
pub fn debug_vars_for_operation(
&self,
Expand Down Expand Up @@ -364,6 +370,11 @@ impl DebugInfo {
self.asm_ops.get(asm_op_id)
}

/// Returns all `(op_idx, AsmOpId)` pairs for the given node.
pub fn asm_ops_for_node(&self, node_id: MastNodeId) -> Vec<(usize, AsmOpId)> {
self.asm_op_storage.asm_ops_for_node(node_id)
}

// ASSEMBLY OP MUTATORS
// --------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -413,6 +424,14 @@ impl DebugInfo {
self.asm_op_storage = self.asm_op_storage.remap_nodes(remapping);
}

/// Remaps the debug var storage to use new node IDs after nodes have been removed/reordered.
///
/// This should be called after nodes are removed from the MastForest to ensure the debug
/// var storage still references valid node IDs.
pub(super) fn remap_debug_var_storage(&mut self, remapping: &BTreeMap<MastNodeId, MastNodeId>) {
self.op_debug_var_storage = self.op_debug_var_storage.remap_nodes(remapping);
}

// DEBUG VARIABLE MUTATORS
// --------------------------------------------------------------------------------------------

Expand Down
166 changes: 163 additions & 3 deletions core/src/mast/merger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use alloc::{collections::BTreeMap, vec::Vec};
use crate::{
crypto::hash::Blake3Digest,
mast::{
DecoratorId, MastForest, MastForestContributor, MastForestError, MastNode, MastNodeBuilder,
MastNodeFingerprint, MastNodeId, MultiMastForestIteratorItem, MultiMastForestNodeIter,
AsmOpId, DebugVarId, DecoratorId, MastForest, MastForestContributor, MastForestError,
MastNode, MastNodeBuilder, MastNodeFingerprint, MastNodeId, MultiMastForestIteratorItem,
MultiMastForestNodeIter,
},
serde::Serializable,
utils::{DenseIdMap, IndexVec},
};

Expand Down Expand Up @@ -158,6 +160,8 @@ impl MastForestMerger {
self.merge_roots(forest_idx, forest)?;
}

self.merge_debug_metadata(&forests)?;

Ok(())
}

Expand Down Expand Up @@ -228,9 +232,19 @@ impl MastForestMerger {
&self.decorator_id_mappings[forest_idx],
)?;

let node_fingerprint =
let base_fingerprint =
remapped_builder.fingerprint_for_node(&self.mast_forest, &self.hash_by_node_id)?;

// Augment with the source node's debug vars so same-ops/different-vars
// blocks from different forests are not collapsed.
let debug_var_data =
serialize_debug_var_content_for_node(original_forests[forest_idx], merging_id);
let asm_op_data =
serialize_asm_op_content_for_node(original_forests[forest_idx], merging_id);
let node_fingerprint = base_fingerprint
.augment_with_data(&debug_var_data)
.augment_with_data(&asm_op_data);

match self.lookup_node_by_fingerprint(&node_fingerprint) {
Some(matching_node_id) => {
// If a node with a matching fingerprint exists, then the merging node is a
Expand Down Expand Up @@ -282,6 +296,95 @@ impl MastForestMerger {
Ok(())
}

/// Transfers procedure names, asm ops, and debug vars from the source forests
/// into the merged forest, remapping all IDs along the way.
///
/// Procedure names are merged separately by digest. Per-node asm-op and debug-var
/// metadata are remapped by node ID, and when two source nodes map to the same merged
/// node (dedup), the first forest's per-node metadata wins.
fn merge_debug_metadata(&mut self, forests: &[&MastForest]) -> Result<(), MastForestError> {
// Procedure names are keyed by digest. First name wins so that a later
// forest cannot silently rename an already-registered procedure.
for forest in forests.iter() {
for (digest, name) in forest.debug_info.procedure_names() {
if self.mast_forest.debug_info.procedure_name(&digest).is_none() {
self.mast_forest.debug_info.insert_procedure_name(digest, name.clone());
}
}
}

// Collect per-node asm-op and debug-var registrations across all forests.
// BTreeMap gives us sorted-by-node-id iteration, which the CSR requires.
let mut asm_entries: BTreeMap<MastNodeId, Vec<(usize, AsmOpId)>> = BTreeMap::new();
let mut dbg_entries: BTreeMap<MastNodeId, Vec<(usize, DebugVarId)>> = BTreeMap::new();

for (forest_idx, forest) in forests.iter().enumerate() {
// Copy AssemblyOp objects and build old→new AsmOpId remapping.
let mut asm_id_map: BTreeMap<AsmOpId, AsmOpId> = BTreeMap::new();
for (raw, asm_op) in forest.debug_info.asm_ops().iter().enumerate() {
let old_id = AsmOpId::new(raw as u32);
let new_id = self.mast_forest.debug_info.add_asm_op(asm_op.clone())?;
asm_id_map.insert(old_id, new_id);
}

// Copy DebugVarInfo objects and build old→new DebugVarId remapping.
let mut dbg_id_map: BTreeMap<DebugVarId, DebugVarId> = BTreeMap::new();
for (raw, dvar) in forest.debug_info.debug_vars().iter().enumerate() {
let old_id = DebugVarId::from(raw as u32);
let new_id = self.mast_forest.debug_info.add_debug_var(dvar.clone())?;
dbg_id_map.insert(old_id, new_id);
}

// For each source node, remap and store entries. First forest wins per node.
for old_raw in 0..forest.num_nodes() {
let old_id = MastNodeId::new_unchecked(old_raw);
let new_id = match self.node_id_mappings[forest_idx].get(old_id) {
Some(id) => id,
None => continue,
};

if let alloc::collections::btree_map::Entry::Vacant(e) = asm_entries.entry(new_id) {
let ops = forest.debug_info.asm_ops_for_node(old_id);
if !ops.is_empty() {
let remapped =
ops.into_iter().map(|(idx, id)| (idx, asm_id_map[&id])).collect();
e.insert(remapped);
}
}

if let alloc::collections::btree_map::Entry::Vacant(e) = dbg_entries.entry(new_id) {
let vars = forest.debug_info.debug_vars_for_node(old_id);
if !vars.is_empty() {
let remapped =
vars.into_iter().map(|(idx, id)| (idx, dbg_id_map[&id])).collect();
e.insert(remapped);
}
}
}
}

// Register in node-ID order (CSR sequential constraint).
for (node_id, entries) in asm_entries {
let num_ops = match &self.mast_forest[node_id] {
MastNode::Block(block) => block.num_operations() as usize,
_ => entries.iter().map(|(idx, _)| idx + 1).max().unwrap_or(0),
};
self.mast_forest
.debug_info
.register_asm_ops(node_id, num_ops, entries)
.map_err(|_| MastForestError::TooManyNodes)?;
}

for (node_id, entries) in dbg_entries {
self.mast_forest
.debug_info
.register_op_indexed_debug_vars(node_id, entries)
.map_err(|_| MastForestError::TooManyNodes)?;
}

Ok(())
}

// HELPERS
// ================================================================================================

Expand All @@ -304,6 +407,63 @@ impl MastForestMerger {
}
}

// HELPERS
// ================================================================================================

/// Serializes the actual debug var *content* (name, location, etc.) for a node,
/// producing a stable byte sequence suitable for fingerprint augmentation.
///
/// Unlike the assembler's `serialize_debug_vars` (which serializes `(op_idx, DebugVarId)` pairs),
/// this serializes the resolved DebugVarInfo so that two forests assigning different DebugVarIds
/// to identical variables still produce the same fingerprint contribution.
fn serialize_debug_var_content_for_node(forest: &MastForest, node_id: MastNodeId) -> Vec<u8> {
let entries = forest.debug_info().debug_vars_for_node(node_id);
if entries.is_empty() {
return Vec::new();
}

let mut data = Vec::new();
for (op_idx, var_id) in entries {
data.extend_from_slice(&op_idx.to_le_bytes());
if let Some(info) = forest.debug_info().debug_var(var_id) {
info.write_into(&mut data);
}
}
data
}

/// Serializes the actual asm-op content for a node, producing a stable byte
/// sequence suitable for fingerprint augmentation.
///
/// This ensures that nodes with identical structure but different source-mapping
/// metadata do not collapse during merge.
fn serialize_asm_op_content_for_node(forest: &MastForest, node_id: MastNodeId) -> Vec<u8> {
let entries = forest.debug_info().asm_ops_for_node(node_id);
if entries.is_empty() {
return Vec::new();
}

let mut data = Vec::new();
for (op_idx, asm_op_id) in entries {
data.extend_from_slice(&op_idx.to_le_bytes());
if let Some(asm_op) = forest.debug_info().asm_op(asm_op_id) {
asm_op.context_name().write_into(&mut data);
asm_op.op().write_into(&mut data);
asm_op.num_cycles().write_into(&mut data);
match asm_op.location() {
Some(location) => {
data.push(1);
location.uri.write_into(&mut data);
data.extend_from_slice(&u32::from(location.start).to_le_bytes());
data.extend_from_slice(&u32::from(location.end).to_le_bytes());
},
None => data.push(0),
}
}
}
data
}

// MAST FOREST ROOT MAP
// ================================================================================================

Expand Down
Loading
Loading