diff --git a/CHANGELOG.md b/CHANGELOG.md index 08809a98c8..d9060de744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Fixed the release dry-run publish cycle between `miden-air` and `miden-ace-codegen`, and preserved leaf-only DAG imports with explicit snapshots ([#2931](https://github.com/0xMiden/miden-vm/pull/2931)). - Added regression coverage for the exact `max_num_continuations` continuation-stack boundary ([#2995](https://github.com/0xMiden/miden-vm/pull/2995)). - Fixed AEAD padding handling so encrypt does not overwrite memory next to the plaintext buffer and decrypt leaves the plaintext output tail untouched ([#3008](https://github.com/0xMiden/miden-vm/pull/3008)). +- Fixed MAST compaction after debug info is cleared so compiler-generated packages do not grow ([#3044](https://github.com/0xMiden/miden-vm/pull/3044)). #### Changes diff --git a/core/src/mast/debuginfo/mod.rs b/core/src/mast/debuginfo/mod.rs index 184a087415..c24c91fe63 100644 --- a/core/src/mast/debuginfo/mod.rs +++ b/core/src/mast/debuginfo/mod.rs @@ -566,6 +566,12 @@ impl DebugInfo { pub(crate) fn op_decorator_storage(&self) -> &OpToDecoratorIds { &self.op_decorator_storage } + + /// Returns the node decorator storage. + #[cfg(test)] + pub(crate) fn node_decorator_storage(&self) -> &NodeToDecoratorIds { + &self.node_decorator_storage + } } impl Serializable for DebugInfo { diff --git a/core/src/mast/merger/tests.rs b/core/src/mast/merger/tests.rs index 59f774f845..302b500675 100644 --- a/core/src/mast/merger/tests.rs +++ b/core/src/mast/merger/tests.rs @@ -138,6 +138,39 @@ fn mast_forest_merge_preserves_dyn_callness_and_digest() { assert_eq!(merged_dyncall.digest(), dyncall_digest, "dyncall digest should be preserved"); } +#[test] +fn mast_forest_merge_preserves_padded_basic_block_batches() { + let mut forest = MastForest::new(); + + let operations = vec![Operation::Add, Operation::Push(Felt::new_unchecked(100))]; + let block_id = BasicBlockNodeBuilder::new(operations.clone(), Vec::new()) + .add_to_forest(&mut forest) + .unwrap(); + forest.make_root(block_id); + + let original_block = forest[block_id].unwrap_basic_block(); + assert!( + original_block.operations().count() > original_block.raw_operations().count(), + "test input must create padded operations" + ); + let original_batches = original_block.op_batches().to_vec(); + + let (merged, root_maps) = MastForest::merge([&forest]).unwrap(); + + let merged_block_id = root_maps.map_root(0, &block_id).unwrap(); + let merged_block = merged[merged_block_id].unwrap_basic_block(); + assert_eq!( + merged_block.raw_operations().copied().collect::>(), + operations, + "merge must not treat padded operations as raw operations" + ); + assert_eq!( + merged_block.op_batches(), + original_batches, + "merge must preserve the original batch layout" + ); +} + /// Tests that Call(bar) still correctly calls the remapped bar block. /// /// [Block(foo), Call(foo)] diff --git a/core/src/mast/mod.rs b/core/src/mast/mod.rs index 9462200519..306e4f9d7b 100644 --- a/core/src/mast/mod.rs +++ b/core/src/mast/mod.rs @@ -614,6 +614,10 @@ impl MastForest { before_enter: &[DecoratorId], after_exit: &[DecoratorId], ) { + if before_enter.is_empty() && after_exit.is_empty() { + return; + } + self.debug_info.register_node_decorators(node_id, before_enter, after_exit); } diff --git a/core/src/mast/tests.rs b/core/src/mast/tests.rs index edcf0b72aa..45e1ef648c 100644 --- a/core/src/mast/tests.rs +++ b/core/src/mast/tests.rs @@ -298,6 +298,31 @@ fn test_clear_debug_info_multiple_node_types() { assert!(forest.decorator_links_for_node(block_id).unwrap().into_iter().next().is_none()); } +#[test] +fn test_compact_after_clear_debug_info_does_not_materialize_empty_node_decorators() { + let mut forest = MastForest::new(); + let decorator = forest.add_decorator(Decorator::Trace(1)).unwrap(); + let block_id = BasicBlockNodeBuilder::new( + vec![Operation::Push(Felt::new_unchecked(1)), Operation::Add], + vec![(0, decorator)], + ) + .with_before_enter(vec![decorator]) + .add_to_forest(&mut forest) + .unwrap(); + let call_id = CallNodeBuilder::new(block_id).add_to_forest(&mut forest).unwrap(); + forest.make_root(call_id); + + forest.clear_debug_info(); + let (compacted, _) = forest.compact(); + + assert!(compacted.debug_info.node_decorator_storage().is_empty()); + for node_idx in 0..compacted.nodes().len() { + let node_id = crate::mast::MastNodeId::new_unchecked(node_idx as u32); + assert!(compacted.before_enter_decorators(node_id).is_empty()); + assert!(compacted.after_exit_decorators(node_id).is_empty()); + } +} + #[test] fn test_mast_forest_roundtrip_with_basic_blocks_and_decorators() { use crate::mast::MastNode; diff --git a/crates/assembly/src/tests.rs b/crates/assembly/src/tests.rs index f36a5a3b56..0608a9aa35 100644 --- a/crates/assembly/src/tests.rs +++ b/crates/assembly/src/tests.rs @@ -4990,6 +4990,64 @@ fn regression_empty_kernel_library_is_rejected() { assert_diagnostic_lines!(err, "library must contain at least one exported procedure"); } +/// Reproduces issue #3035: a MAST with padded basic blocks grows when debug info is cleared and the +/// forest is compacted via self-merge. +#[test] +fn issue_3035_compact_after_clear_debug_info_does_not_grow_mast() -> TestResult { + let context = TestContext::default(); + let module = context.parse_module_with_path( + "issue_3035::repro", + source_file!( + &context, + " + pub proc repro + add + push.100 + end + " + ), + )?; + + let library = Assembler::new(context.source_manager()).assemble_library([module])?; + let mut forest = library.mast_forest().as_ref().clone(); + assert!( + forest + .nodes() + .iter() + .filter_map(|node| node.get_basic_block()) + .any(|block| { block.operations().count() > block.raw_operations().count() }), + "test input must create at least one padded basic block" + ); + + forest.clear_debug_info(); + let stripped_size = forest.to_bytes().len(); + let stripped_without_debug_info_size = { + let mut bytes = Vec::new(); + forest.write_stripped(&mut bytes); + bytes.len() + }; + let stripped_nodes = forest.nodes().len(); + let (compacted, _) = forest.compact(); + let compacted_size = compacted.to_bytes().len(); + let compacted_without_debug_info_size = { + let mut bytes = Vec::new(); + compacted.write_stripped(&mut bytes); + bytes.len() + }; + let compacted_nodes = compacted.nodes().len(); + + assert!( + compacted_size <= stripped_size, + "MastForest::compact increased serialized size after clear_debug_info(): \ + stripped={stripped_size}, compacted={compacted_size}, \ + stripped_without_debug_info={stripped_without_debug_info_size}, \ + compacted_without_debug_info={compacted_without_debug_info_size}, \ + stripped_nodes={stripped_nodes}, compacted_nodes={compacted_nodes}" + ); + + Ok(()) +} + /// Test for issue #1644: verify that single-forest merge doesn't preserves node digests #[test] fn issue_1644_single_forest_merge_identity() -> TestResult {