diff --git a/CHANGELOG.md b/CHANGELOG.md index a6682e664e..69a4c1ad59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ #### Fixes - Fixed stale `ReplayProcessor` doc comment links to `ExecutionTracer` after module-structure refactors. +- Preserved `AssemblyOp` source mappings when merging `MastForest`s, preventing source-location loss after node deduplication. ## 0.22.0 (2025-03-18) diff --git a/core/src/mast/debuginfo/asm_op_storage.rs b/core/src/mast/debuginfo/asm_op_storage.rs index b1108ccb6b..23648b367e 100644 --- a/core/src/mast/debuginfo/asm_op_storage.rs +++ b/core/src/mast/debuginfo/asm_op_storage.rs @@ -88,6 +88,11 @@ impl OpToAsmOpId { self.inner.num_elements() } + /// Returns all sparse `(op_idx, asm_op_id)` entries for a node, if the node exists. + pub fn asm_ops_for_node(&self, node_id: MastNodeId) -> Option<&[(usize, AsmOpId)]> { + self.inner.row(node_id) + } + /// Registers AssemblyOps for a node's operations. /// /// `asm_ops` is a list of `(op_idx, asm_op_id)` pairs. The `op_idx` values must be strictly diff --git a/core/src/mast/debuginfo/mod.rs b/core/src/mast/debuginfo/mod.rs index 18046c4ae1..f879f63f03 100644 --- a/core/src/mast/debuginfo/mod.rs +++ b/core/src/mast/debuginfo/mod.rs @@ -363,6 +363,11 @@ impl DebugInfo { self.asm_ops.get(asm_op_id) } + /// Returns all sparse `(op_idx, asm_op_id)` entries registered for a node. + pub fn asm_ops_for_node(&self, node_id: MastNodeId) -> Option<&[(usize, AsmOpId)]> { + self.asm_op_storage.asm_ops_for_node(node_id) + } + // ASSEMBLY OP MUTATORS // -------------------------------------------------------------------------------------------- diff --git a/core/src/mast/merger/mod.rs b/core/src/mast/merger/mod.rs index 67aec6f8e7..5a776115b8 100644 --- a/core/src/mast/merger/mod.rs +++ b/core/src/mast/merger/mod.rs @@ -1,17 +1,28 @@ -use alloc::{collections::BTreeMap, vec::Vec}; +use alloc::{ + collections::{BTreeMap, btree_map::Entry}, + string::String, + vec::Vec, +}; +use core::cmp::Ordering; + +use miden_debug_types::Location; use crate::{ crypto::hash::Blake3Digest, mast::{ - DecoratorId, MastForest, MastForestContributor, MastForestError, MastNode, MastNodeBuilder, - MastNodeFingerprint, MastNodeId, MultiMastForestIteratorItem, MultiMastForestNodeIter, + AsmOpId, DecoratorId, MastForest, MastForestContributor, MastForestError, MastNode, + MastNodeBuilder, MastNodeFingerprint, MastNodeId, MultiMastForestIteratorItem, + MultiMastForestNodeIter, }, + operations::AssemblyOp, utils::{DenseIdMap, IndexVec}, }; #[cfg(test)] mod tests; +type AssemblyOpKey = (Option, String, u8, String); + /// A type that allows merging [`MastForest`]s. /// /// This functionality is exposed via [`MastForest::merge`]. See its documentation for more details. @@ -25,6 +36,8 @@ pub(crate) struct MastForestMerger { node_id_by_hash: BTreeMap, hash_by_node_id: IndexVec, decorators_by_hash: BTreeMap, DecoratorId>, + asm_op_id_by_value: BTreeMap, + asm_op_value_by_id: BTreeMap, /// Mappings from old decorator and node ids to their new ids. /// /// Any decorator in `mast_forest` is present as the target of some mapping in this map. @@ -33,6 +46,10 @@ pub(crate) struct MastForestMerger { /// /// Any `MastNodeId` in `mast_forest` is present as the target of some mapping in this map. node_id_mappings: Vec>, + /// AssemblyOp mappings to register after all nodes have been merged. + /// + /// This is keyed by merged node id and stores `(num_operations, [(op_idx, asm_op_id)])`. + pending_asm_op_mappings: BTreeMap)>, } impl MastForestMerger { @@ -63,9 +80,12 @@ impl MastForestMerger { node_id_by_hash: BTreeMap::new(), hash_by_node_id: IndexVec::new(), decorators_by_hash: BTreeMap::new(), + asm_op_id_by_value: BTreeMap::new(), + asm_op_value_by_id: BTreeMap::new(), mast_forest: MastForest::new(), decorator_id_mappings, node_id_mappings, + pending_asm_op_mappings: BTreeMap::new(), }; merger.merge_inner(forests.clone())?; @@ -79,13 +99,14 @@ impl MastForestMerger { /// Merges all `forests` into self. /// - /// It does this in three steps: + /// It does this in six steps: /// /// 1. Merge all advice maps, checking for key collisions. /// 2. Merge all decorators, which is a case of deduplication and creating a decorator id /// mapping which contains how existing [`DecoratorId`]s map to [`DecoratorId`]s in the /// merged forest. - /// 3. Merge all nodes of forests. + /// 3. Merge all error codes. + /// 4. Merge all nodes of forests. /// - Similar to decorators, node indices might move during merging, so the merger keeps a /// node id mapping as it merges nodes. /// - This is a depth-first traversal over all forests to ensure all children are processed @@ -107,7 +128,11 @@ impl MastForestMerger { /// `replacement` node. Now we can simply add a mapping from the external node to the /// `replacement` node in our node id mapping which means all nodes that referenced the /// external node will point to the `replacement` instead. - /// 4. Finally, we merge all roots of all forests. Here we map the existing root indices to + /// 5. Merge all AssemblyOp source mappings for merged nodes. + /// - AssemblyOps are deduplicated by value and remapped to merged ids. + /// - Op-indexed source mappings are registered after node merge, when all node remappings + /// are known. + /// 6. Finally, we merge all roots of all forests. Here we map the existing root indices to /// their potentially new indices in the merged forest and add them to the forest, /// deduplicating in the process, too. fn merge_inner(&mut self, forests: Vec<&MastForest>) -> Result<(), MastForestError> { @@ -146,6 +171,12 @@ impl MastForestMerger { .get(replacement_mast_node_id) .expect("every merged node id should be mapped"); + self.merge_node_asm_ops( + forests[replaced_forest_idx], + replaced_mast_node_id, + mapped_replacement, + )?; + // SAFETY: The iterator only yields valid forest indices, so it is safe to index // directly. self.node_id_mappings[replaced_forest_idx] @@ -154,6 +185,8 @@ impl MastForestMerger { } } + self.register_asm_op_mappings(); + for (forest_idx, forest) in forests.iter().enumerate() { self.merge_roots(forest_idx, forest)?; } @@ -231,11 +264,12 @@ impl MastForestMerger { let node_fingerprint = remapped_builder.fingerprint_for_node(&self.mast_forest, &self.hash_by_node_id)?; - match self.lookup_node_by_fingerprint(&node_fingerprint) { + let mapped_node_id = 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 // duplicate and we remap it to the existing node. self.node_id_mappings[forest_idx].insert(merging_id, matching_node_id); + matching_node_id }, None => { // If no node with a matching fingerprint exists, then the merging node is @@ -257,8 +291,11 @@ impl MastForestMerger { returned_id, new_node_id, "hash_by_node_id push() should return the same node IDs as node_id_by_hash" ); + new_node_id }, - } + }; + + self.merge_node_asm_ops(original_forests[forest_idx], merging_id, mapped_node_id)?; Ok(()) } @@ -291,6 +328,286 @@ impl MastForestMerger { self.node_id_by_hash.get(fingerprint).copied() } + /// Merges AssemblyOp source mappings for a single node. + /// + /// For basic blocks we preserve op-indexed source mapping transitions. For non-basic-block + /// nodes we preserve all sparse transitions as stored in debug info. + fn merge_node_asm_ops( + &mut self, + source_forest: &MastForest, + source_node_id: MastNodeId, + merged_node_id: MastNodeId, + ) -> Result<(), MastForestError> { + let (num_operations, asm_ops) = match &source_forest[source_node_id] { + MastNode::Block(block) => { + let num_operations = block.num_operations() as usize; + let mut asm_ops = Vec::new(); + let mut previous_asm_op: Option = None; + + for op_idx in 0..num_operations { + let asm_op = + source_forest.debug_info.asm_op_for_operation(source_node_id, op_idx); + let asm_op_key = asm_op.map(Self::asm_op_key); + + if asm_op_key == previous_asm_op { + continue; + } + + if let Some(asm_op) = asm_op { + let merged_asm_op_id = self.intern_asm_op(asm_op)?; + asm_ops.push((op_idx, merged_asm_op_id)); + } + + previous_asm_op = asm_op_key; + } + + (num_operations, asm_ops) + }, + _ => { + let Some(source_asm_ops) = + source_forest.debug_info.asm_ops_for_node(source_node_id) + else { + return Ok(()); + }; + + let mut asm_ops = Vec::with_capacity(source_asm_ops.len()); + for (op_idx, asm_op_id) in source_asm_ops.iter().copied() { + let asm_op = source_forest + .debug_info + .asm_op(asm_op_id) + .expect("asm-op mapping should reference a valid assembly op"); + let merged_asm_op_id = self.intern_asm_op(asm_op)?; + asm_ops.push((op_idx, merged_asm_op_id)); + } + + let num_operations = + source_asm_ops.last().map(|(op_idx, _)| op_idx + 1).unwrap_or(0); + + (num_operations, asm_ops) + }, + }; + + if asm_ops.is_empty() { + return Ok(()); + } + + self.merge_pending_asm_op_mapping(merged_node_id, num_operations, asm_ops); + + Ok(()) + } + + /// Adds or merges asm-op mappings for a merged node. + /// + /// Nodes can be visited multiple times due to deduplication across input forests. In that + /// case, we merge compatible mappings and resolve conflicts deterministically, favoring the + /// richer source mapping. + fn merge_pending_asm_op_mapping( + &mut self, + merged_node_id: MastNodeId, + num_operations: usize, + asm_ops: Vec<(usize, AsmOpId)>, + ) { + match self.pending_asm_op_mappings.entry(merged_node_id) { + Entry::Vacant(entry) => { + entry.insert((num_operations, asm_ops)); + }, + Entry::Occupied(mut entry) => { + let (existing_num_operations, existing_asm_ops) = entry.get_mut(); + let merged_num_operations = + core::cmp::max(*existing_num_operations, num_operations); + let merged_asm_ops = Self::merge_asm_op_mappings( + merged_num_operations, + existing_asm_ops, + &asm_ops, + &self.asm_op_value_by_id, + ); + *existing_num_operations = merged_num_operations; + *existing_asm_ops = merged_asm_ops; + }, + } + } + + /// Merges two sparse asm-op mappings for the same node. + /// + /// Compatible entries are unified. Conflicts are resolved deterministically by preferring the + /// richer mapping. + fn merge_asm_op_mappings( + num_operations: usize, + lhs: &[(usize, AsmOpId)], + rhs: &[(usize, AsmOpId)], + asm_op_value_by_id: &BTreeMap, + ) -> Vec<(usize, AsmOpId)> { + let lhs_expanded = Self::expand_asm_op_mapping(num_operations, lhs); + let rhs_expanded = Self::expand_asm_op_mapping(num_operations, rhs); + let preference = Self::compare_asm_op_mapping_specificity(num_operations, lhs, rhs); + + if preference.is_eq() + && Self::has_conflicting_asm_op_assignments(&lhs_expanded, &rhs_expanded) + { + return match Self::compare_asm_op_mapping_value_key(lhs, rhs, asm_op_value_by_id) { + Ordering::Greater | Ordering::Equal => lhs.to_vec(), + Ordering::Less => rhs.to_vec(), + }; + } + + let mut merged = Vec::with_capacity(num_operations); + for op_idx in 0..num_operations { + let merged_asm_op = match (lhs_expanded[op_idx], rhs_expanded[op_idx]) { + (Some(lhs_asm_op), Some(rhs_asm_op)) if lhs_asm_op == rhs_asm_op => { + Some(lhs_asm_op) + }, + (Some(lhs_asm_op), Some(rhs_asm_op)) => Some(match preference { + Ordering::Greater => lhs_asm_op, + Ordering::Less => rhs_asm_op, + Ordering::Equal => lhs_asm_op, + }), + (Some(asm_op), None) | (None, Some(asm_op)) => Some(asm_op), + (None, None) => None, + }; + merged.push(merged_asm_op); + } + + Self::compress_asm_op_mapping(&merged) + } + + fn has_conflicting_asm_op_assignments( + lhs_expanded: &[Option], + rhs_expanded: &[Option], + ) -> bool { + lhs_expanded.iter().zip(rhs_expanded.iter()).any(|(lhs_entry, rhs_entry)| { + match (lhs_entry, rhs_entry) { + (Some(lhs_asm_op), Some(rhs_asm_op)) => lhs_asm_op != rhs_asm_op, + _ => false, + } + }) + } + + fn compare_asm_op_mapping_value_key( + lhs: &[(usize, AsmOpId)], + rhs: &[(usize, AsmOpId)], + asm_op_value_by_id: &BTreeMap, + ) -> Ordering { + for ((lhs_op_idx, lhs_asm_op), (rhs_op_idx, rhs_asm_op)) in lhs.iter().zip(rhs.iter()) { + let op_idx_cmp = lhs_op_idx.cmp(rhs_op_idx); + if !op_idx_cmp.is_eq() { + return op_idx_cmp; + } + + let lhs_key = asm_op_value_by_id + .get(lhs_asm_op) + .expect("asm-op id should resolve to a value key"); + let rhs_key = asm_op_value_by_id + .get(rhs_asm_op) + .expect("asm-op id should resolve to a value key"); + let key_cmp = lhs_key.cmp(rhs_key); + if !key_cmp.is_eq() { + return key_cmp; + } + } + + lhs.len().cmp(&rhs.len()) + } + + /// Expands sparse mapping transitions into per-operation mapping. + fn expand_asm_op_mapping( + num_operations: usize, + asm_ops: &[(usize, AsmOpId)], + ) -> Vec> { + let mut expanded = vec![None; num_operations]; + for (i, (start_op_idx, asm_op_id)) in asm_ops.iter().copied().enumerate() { + if start_op_idx >= num_operations { + break; + } + let end_op_idx = + asm_ops.get(i + 1).map(|(op_idx, _)| *op_idx).unwrap_or(num_operations); + expanded[start_op_idx..end_op_idx].fill(Some(asm_op_id)); + } + expanded + } + + /// Compresses per-operation mapping into sparse transition points. + fn compress_asm_op_mapping(asm_ops: &[Option]) -> Vec<(usize, AsmOpId)> { + let mut compressed = Vec::new(); + let mut previous_asm_op = None; + + for (op_idx, asm_op) in asm_ops.iter().copied().enumerate() { + if asm_op == previous_asm_op { + continue; + } + + if let Some(asm_op) = asm_op { + compressed.push((op_idx, asm_op)); + } + previous_asm_op = asm_op; + } + + compressed + } + + /// Compares mapping richness for deterministic conflict resolution. + /// + /// Richer mapping means: + /// 1. More transition points. + /// 2. If tied, larger covered suffix of operations. + fn compare_asm_op_mapping_specificity( + num_operations: usize, + lhs: &[(usize, AsmOpId)], + rhs: &[(usize, AsmOpId)], + ) -> Ordering { + let transitions_cmp = lhs.len().cmp(&rhs.len()); + if !transitions_cmp.is_eq() { + return transitions_cmp; + } + + let coverage = |mapping: &[(usize, AsmOpId)]| { + mapping + .first() + .map(|(op_idx, _)| num_operations.saturating_sub(*op_idx)) + .unwrap_or(0) + }; + let coverage_cmp = coverage(lhs).cmp(&coverage(rhs)); + if !coverage_cmp.is_eq() { + return coverage_cmp; + } + + Ordering::Equal + } + + /// Registers all merged asm-op mappings into the merged forest. + fn register_asm_op_mappings(&mut self) { + for (node_id, (num_operations, asm_ops)) in + core::mem::take(&mut self.pending_asm_op_mappings) + { + self.mast_forest + .debug_info + .register_asm_ops(node_id, num_operations, asm_ops) + .expect("asm-op mappings should be registered in increasing node id order"); + } + } + + /// Adds the provided AssemblyOp to the merged forest if not present and returns its ID. + fn intern_asm_op(&mut self, asm_op: &AssemblyOp) -> Result { + let key = Self::asm_op_key(asm_op); + if let Some(existing_id) = self.asm_op_id_by_value.get(&key) { + return Ok(*existing_id); + } + + let asm_op_id = self.mast_forest.debug_info.add_asm_op(asm_op.clone())?; + self.asm_op_id_by_value.insert(key.clone(), asm_op_id); + self.asm_op_value_by_id.insert(asm_op_id, key); + + Ok(asm_op_id) + } + + fn asm_op_key(asm_op: &AssemblyOp) -> AssemblyOpKey { + ( + asm_op.location().cloned(), + String::from(asm_op.context_name()), + asm_op.num_cycles(), + String::from(asm_op.op()), + ) + } + /// Builds a new node with remapped children and decorators using the provided mappings. fn build_with_remapped_children( &self, diff --git a/core/src/mast/merger/tests.rs b/core/src/mast/merger/tests.rs index bcffd6f917..c4b6cce122 100644 --- a/core/src/mast/merger/tests.rs +++ b/core/src/mast/merger/tests.rs @@ -6,7 +6,7 @@ use crate::{ LoopNodeBuilder, node::{MastForestContributor, MastNodeExt}, }, - operations::{DebugOptions, Decorator, Operation}, + operations::{AssemblyOp, DebugOptions, Decorator, Operation}, utils::Idx, }; @@ -765,6 +765,46 @@ fn mast_forest_merge_multiple_external_nodes_with_decorator() { } } +#[test] +fn mast_forest_merge_preserves_asm_op_mappings_from_external_replacement() { + let mut forest_with_external = MastForest::new(); + let foo_digest = block_foo().build().unwrap().digest(); + let external_id = ExternalNodeBuilder::new(foo_digest) + .add_to_forest(&mut forest_with_external) + .unwrap(); + forest_with_external.make_root(external_id); + + let external_asm_op = AssemblyOp::new(None, "proc::caller".into(), 1, "call.foo".into()); + let external_asm_op_id = forest_with_external + .debug_info_mut() + .add_asm_op(external_asm_op.clone()) + .unwrap(); + forest_with_external + .debug_info_mut() + .register_asm_ops(external_id, 1, vec![(0, external_asm_op_id)]) + .unwrap(); + + let mut forest_with_block = MastForest::new(); + let block_id = block_foo().add_to_forest(&mut forest_with_block).unwrap(); + forest_with_block.make_root(block_id); + + let (merged_ext_then_block, root_maps_ext_then_block) = + MastForest::merge([&forest_with_external, &forest_with_block]).unwrap(); + let mapped_external_root = root_maps_ext_then_block.map_root(0, &external_id).unwrap(); + assert_eq!( + merged_ext_then_block.get_assembly_op(mapped_external_root, None), + Some(&external_asm_op), + ); + + let (merged_block_then_ext, root_maps_block_then_ext) = + MastForest::merge([&forest_with_block, &forest_with_external]).unwrap(); + let mapped_external_root = root_maps_block_then_ext.map_root(1, &external_id).unwrap(); + assert_eq!( + merged_block_then_ext.get_assembly_op(mapped_external_root, None), + Some(&external_asm_op), + ); +} + /// Tests that dependencies between External nodes are correctly resolved. /// /// [External(foo), Call(0) = qux] @@ -1114,3 +1154,200 @@ fn mast_forest_merge_op_indexed_decorators_preservation() { "Every decorator in merged forest should be referenced at least once (no orphans)" ); } + +#[test] +fn mast_forest_merge_preserves_asm_op_mappings_for_deduplicated_nodes() { + let mut forest_without_asm = MastForest::new(); + let without_asm_block_id = block_foo().add_to_forest(&mut forest_without_asm).unwrap(); + forest_without_asm.make_root(without_asm_block_id); + + let mut forest_with_asm = MastForest::new(); + let with_asm_block_id = block_foo().add_to_forest(&mut forest_with_asm).unwrap(); + forest_with_asm.make_root(with_asm_block_id); + + let asm_op = AssemblyOp::new(None, "proc::foo".into(), 1, "mul".into()); + let asm_op_id = forest_with_asm.debug_info_mut().add_asm_op(asm_op.clone()).unwrap(); + forest_with_asm + .debug_info_mut() + .register_asm_ops(with_asm_block_id, 2, vec![(0, asm_op_id)]) + .unwrap(); + + // Mapping from the second forest must be preserved even when the node was already deduped + // after merging the first forest. + let (merged_without_then_with, root_maps_without_then_with) = + MastForest::merge([&forest_without_asm, &forest_with_asm]).unwrap(); + let mapped_with_asm_root = root_maps_without_then_with.map_root(1, &with_asm_block_id).unwrap(); + + assert_eq!( + merged_without_then_with.get_assembly_op(mapped_with_asm_root, Some(0)), + Some(&asm_op), + ); + + // Reverse order should behave identically. + let (merged_with_then_without, root_maps_with_then_without) = + MastForest::merge([&forest_with_asm, &forest_without_asm]).unwrap(); + let mapped_with_asm_root = root_maps_with_then_without.map_root(0, &with_asm_block_id).unwrap(); + + assert_eq!( + merged_with_then_without.get_assembly_op(mapped_with_asm_root, Some(0)), + Some(&asm_op), + ); +} + +#[test] +fn mast_forest_merge_prefers_richer_asm_op_mappings_for_deduplicated_nodes() { + let mut forest_coarse = MastForest::new(); + let coarse_block_id = block_foo().add_to_forest(&mut forest_coarse).unwrap(); + forest_coarse.make_root(coarse_block_id); + + let mut forest_rich = MastForest::new(); + let rich_block_id = block_foo().add_to_forest(&mut forest_rich).unwrap(); + forest_rich.make_root(rich_block_id); + + let asm_mul = AssemblyOp::new(None, "proc::foo".into(), 1, "mul".into()); + let asm_add = AssemblyOp::new(None, "proc::foo".into(), 1, "add".into()); + + let coarse_mul_id = forest_coarse.debug_info_mut().add_asm_op(asm_mul.clone()).unwrap(); + forest_coarse + .debug_info_mut() + .register_asm_ops(coarse_block_id, 2, vec![(0, coarse_mul_id)]) + .unwrap(); + + let rich_mul_id = forest_rich.debug_info_mut().add_asm_op(asm_mul.clone()).unwrap(); + let rich_add_id = forest_rich.debug_info_mut().add_asm_op(asm_add.clone()).unwrap(); + forest_rich + .debug_info_mut() + .register_asm_ops(rich_block_id, 2, vec![(0, rich_mul_id), (1, rich_add_id)]) + .unwrap(); + + // Coarse + rich should keep the richer mapping at op 1. + let (merged_coarse_then_rich, root_maps_coarse_then_rich) = + MastForest::merge([&forest_coarse, &forest_rich]).unwrap(); + let mapped_rich_root = root_maps_coarse_then_rich.map_root(1, &rich_block_id).unwrap(); + assert_eq!( + merged_coarse_then_rich.get_assembly_op(mapped_rich_root, Some(0)), + Some(&asm_mul) + ); + assert_eq!( + merged_coarse_then_rich.get_assembly_op(mapped_rich_root, Some(1)), + Some(&asm_add) + ); + + // Rich + coarse should be identical. + let (merged_rich_then_coarse, root_maps_rich_then_coarse) = + MastForest::merge([&forest_rich, &forest_coarse]).unwrap(); + let mapped_rich_root = root_maps_rich_then_coarse.map_root(0, &rich_block_id).unwrap(); + assert_eq!( + merged_rich_then_coarse.get_assembly_op(mapped_rich_root, Some(0)), + Some(&asm_mul) + ); + assert_eq!( + merged_rich_then_coarse.get_assembly_op(mapped_rich_root, Some(1)), + Some(&asm_add) + ); +} + +#[test] +fn mast_forest_merge_preserves_sparse_non_block_asm_op_mappings() { + let mut forest_without_asm = MastForest::new(); + let without_asm_callee_id = block_foo().add_to_forest(&mut forest_without_asm).unwrap(); + let without_asm_call_id = CallNodeBuilder::new(without_asm_callee_id) + .add_to_forest(&mut forest_without_asm) + .unwrap(); + forest_without_asm.make_root(without_asm_call_id); + + let mut forest_with_asm = MastForest::new(); + let with_asm_callee_id = block_foo().add_to_forest(&mut forest_with_asm).unwrap(); + let with_asm_call_id = CallNodeBuilder::new(with_asm_callee_id) + .add_to_forest(&mut forest_with_asm) + .unwrap(); + forest_with_asm.make_root(with_asm_call_id); + + let asm_enter = AssemblyOp::new(None, "proc::caller".into(), 1, "call.enter".into()); + let asm_exit = AssemblyOp::new(None, "proc::caller".into(), 1, "call.exit".into()); + let asm_enter_id = forest_with_asm.debug_info_mut().add_asm_op(asm_enter.clone()).unwrap(); + let asm_exit_id = forest_with_asm.debug_info_mut().add_asm_op(asm_exit.clone()).unwrap(); + forest_with_asm + .debug_info_mut() + .register_asm_ops(with_asm_call_id, 4, vec![(1, asm_enter_id), (3, asm_exit_id)]) + .unwrap(); + + let (merged_without_then_with, root_maps_without_then_with) = + MastForest::merge([&forest_without_asm, &forest_with_asm]).unwrap(); + let mapped_call = root_maps_without_then_with.map_root(1, &with_asm_call_id).unwrap(); + assert_eq!(merged_without_then_with.get_assembly_op(mapped_call, Some(0)), None); + assert_eq!(merged_without_then_with.get_assembly_op(mapped_call, Some(1)), Some(&asm_enter)); + assert_eq!(merged_without_then_with.get_assembly_op(mapped_call, Some(2)), Some(&asm_enter)); + assert_eq!(merged_without_then_with.get_assembly_op(mapped_call, Some(3)), Some(&asm_exit)); + + let (merged_with_then_without, root_maps_with_then_without) = + MastForest::merge([&forest_with_asm, &forest_without_asm]).unwrap(); + let mapped_call = root_maps_with_then_without.map_root(0, &with_asm_call_id).unwrap(); + assert_eq!(merged_with_then_without.get_assembly_op(mapped_call, Some(0)), None); + assert_eq!(merged_with_then_without.get_assembly_op(mapped_call, Some(1)), Some(&asm_enter)); + assert_eq!(merged_with_then_without.get_assembly_op(mapped_call, Some(2)), Some(&asm_enter)); + assert_eq!(merged_with_then_without.get_assembly_op(mapped_call, Some(3)), Some(&asm_exit)); +} + +#[test] +fn mast_forest_merge_equal_richness_asm_op_conflicts_choose_whole_mapping() { + let mut forest_lhs = MastForest::new(); + let lhs_block_id = block_foo().add_to_forest(&mut forest_lhs).unwrap(); + forest_lhs.make_root(lhs_block_id); + + let mut forest_rhs = MastForest::new(); + let rhs_block_id = block_foo().add_to_forest(&mut forest_rhs).unwrap(); + forest_rhs.make_root(rhs_block_id); + + let asm_shared = AssemblyOp::new(None, "proc::foo".into(), 1, "shared".into()); + let asm_lhs_only = AssemblyOp::new(None, "proc::foo".into(), 1, "lhs-only".into()); + let asm_rhs_only = AssemblyOp::new(None, "proc::foo".into(), 1, "rhs-only".into()); + + let lhs_shared_id = forest_lhs.debug_info_mut().add_asm_op(asm_shared.clone()).unwrap(); + let lhs_only_id = forest_lhs.debug_info_mut().add_asm_op(asm_lhs_only.clone()).unwrap(); + forest_lhs + .debug_info_mut() + .register_asm_ops(lhs_block_id, 2, vec![(0, lhs_shared_id), (1, lhs_only_id)]) + .unwrap(); + + let rhs_only_id = forest_rhs.debug_info_mut().add_asm_op(asm_rhs_only.clone()).unwrap(); + let rhs_shared_id = forest_rhs.debug_info_mut().add_asm_op(asm_shared.clone()).unwrap(); + forest_rhs + .debug_info_mut() + .register_asm_ops(rhs_block_id, 2, vec![(0, rhs_only_id), (1, rhs_shared_id)]) + .unwrap(); + + let selected_mapping = |forest: &MastForest, node_id| { + ( + forest.get_assembly_op(node_id, Some(0)).map(|asm_op| String::from(asm_op.op())), + forest.get_assembly_op(node_id, Some(1)).map(|asm_op| String::from(asm_op.op())), + ) + }; + + let (merged_lhs_then_rhs, root_maps_lhs_then_rhs) = + MastForest::merge([&forest_lhs, &forest_rhs]).unwrap(); + let mapped_lhs_block = root_maps_lhs_then_rhs.map_root(0, &lhs_block_id).unwrap(); + let mapping_lhs_then_rhs = selected_mapping(&merged_lhs_then_rhs, mapped_lhs_block); + + let (merged_rhs_then_lhs, root_maps_rhs_then_lhs) = + MastForest::merge([&forest_rhs, &forest_lhs]).unwrap(); + let mapped_lhs_block = root_maps_rhs_then_lhs.map_root(1, &lhs_block_id).unwrap(); + let mapping_rhs_then_lhs = selected_mapping(&merged_rhs_then_lhs, mapped_lhs_block); + + assert_eq!( + mapping_lhs_then_rhs, mapping_rhs_then_lhs, + "equal-richness conflicts should resolve deterministically independent of merge order" + ); + + let lhs_mapping = (Some(String::from("shared")), Some(String::from("lhs-only"))); + let rhs_mapping = (Some(String::from("rhs-only")), Some(String::from("shared"))); + assert!( + mapping_lhs_then_rhs == lhs_mapping || mapping_lhs_then_rhs == rhs_mapping, + "merged mapping should match one full input mapping" + ); + assert_ne!( + mapping_lhs_then_rhs, + (Some(String::from("shared")), Some(String::from("shared"))), + "merged mapping should not synthesize a mixed mapping" + ); +}