diff --git a/.gitignore b/.gitignore index 408b20c577..23456b5d52 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ crates/lib/core/assets/core.masl # These are files generated by MacOS **/.DS_Store +._* # File present in Intellij IDE's. .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f49bb086..aa5c819a97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Re-exported `Continuation` from `miden-processor` to support the external debugger ([#2683](https://github.com/0xMiden/miden-vm/pull/2683)). +#### Enhancements + +- Added debug variable tracking for source-level variables via dedicated `DebugVarStorage` (CSR format) in `DebugInfo`, with `DebugVarInfo` describing variable name, type, location, and value location (stack, memory, local, constant, or expression). Also added `debug_types`, `debug_sources`, and `debug_functions` sections in MASP packages for storing type definitions, source file paths, and function metadata respectively, each with its own string table, to support source-level debugging (#[2471](https://github.com/0xMiden/miden-vm/pull/2471)). + ## 0.21.0 (2026-02-14) #### Major breaking changes diff --git a/Cargo.lock b/Cargo.lock index 53ad0f59ee..8db5bed62f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1388,6 +1388,7 @@ dependencies = [ "derive_more", "miden-assembly-syntax", "miden-core", + "miden-debug-types", "miden-test-serde-macros", "proptest", "proptest-derive", diff --git a/core/src/mast/debuginfo/debug_var_storage.rs b/core/src/mast/debuginfo/debug_var_storage.rs new file mode 100644 index 0000000000..cdcd006894 --- /dev/null +++ b/core/src/mast/debuginfo/debug_var_storage.rs @@ -0,0 +1,577 @@ +//! Dedicated storage for DebugVar decorators in a compressed sparse row (CSR) format. +//! +//! This module provides efficient storage and access for debug variable information, +//! separate from the main decorator storage. This allows debuggers to efficiently +//! query variable information without iterating through all decorators. + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use miden_utils_indexing::{Idx, IndexVec}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::DecoratorIndexError; +use crate::{ + mast::MastNodeId, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; + +// DEBUG VAR ID +// ================================================================================================ + +/// An identifier for a debug variable stored in [DebugInfo](super::DebugInfo). +/// +/// This is analogous to [DecoratorId](crate::mast::DecoratorId) but specifically for debug +/// variable information. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugVarId(u32); + +impl DebugVarId { + /// Returns a new `DebugVarId` with the provided inner value, or an error if the provided + /// `value` is greater than or equal to `bound`. + pub fn from_u32_bounded(value: u32, bound: usize) -> Result { + if (value as usize) < bound { + Ok(Self(value)) + } else { + Err(DeserializationError::InvalidValue(format!( + "DebugVarId {} exceeds bound {}", + value, bound + ))) + } + } + + /// Returns the inner value as a usize. + pub fn to_usize(self) -> usize { + self.0 as usize + } + + /// Returns the inner value as a u32. + pub fn as_u32(&self) -> u32 { + self.0 + } +} + +impl Idx for DebugVarId {} + +impl From for DebugVarId { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(value: DebugVarId) -> Self { + value.0 + } +} + +impl Serializable for DebugVarId { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for DebugVarId { + fn read_from(source: &mut R) -> Result { + let value = u32::read_from(source)?; + Ok(Self(value)) + } +} + +// OP TO DEBUG VAR IDS +// ================================================================================================ + +/// A two-level compressed sparse row (CSR) representation for indexing debug variable IDs +/// per operation per node. +/// +/// This structure is analogous to [OpToDecoratorIds](super::OpToDecoratorIds) but specifically for +/// debug variable information. It provides efficient access to debug variables in a hierarchical +/// manner: +/// 1. First level: Node -> Operations +/// 2. Second level: Operation -> DebugVarIds +/// +/// The actual `DebugVarInfo` values are stored separately in the `debug_vars` field of +/// `DebugInfo`, indexed by `DebugVarId`. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OpToDebugVarIds { + /// All the debug var IDs per operation per node + debug_var_ids: Vec, + /// Pointer indices for operations within debug_var_ids + op_indptr_for_var_ids: Vec, + /// Pointer indices for nodes within op_indptr_for_var_ids + node_indptr_for_op_idx: IndexVec, +} + +impl OpToDebugVarIds { + /// Create a new empty OpToDebugVarIds. + pub fn new() -> Self { + Self::with_capacity(0, 0, 0) + } + + /// Create a new empty OpToDebugVarIds with the specified capacity. + pub fn with_capacity( + nodes_capacity: usize, + operations_capacity: usize, + debug_var_ids_capacity: usize, + ) -> Self { + Self { + debug_var_ids: Vec::with_capacity(debug_var_ids_capacity), + op_indptr_for_var_ids: Vec::with_capacity(operations_capacity + 1), + node_indptr_for_op_idx: IndexVec::with_capacity(nodes_capacity + 1), + } + } + + /// Create an OpToDebugVarIds from raw CSR components. + pub(super) fn from_components( + debug_var_ids: Vec, + op_indptr_for_var_ids: Vec, + node_indptr_for_op_idx: IndexVec, + ) -> Result { + // Completely empty structures are valid + if debug_var_ids.is_empty() + && op_indptr_for_var_ids.is_empty() + && node_indptr_for_op_idx.is_empty() + { + return Ok(Self { + debug_var_ids, + op_indptr_for_var_ids, + node_indptr_for_op_idx, + }); + } + + // Nodes with no debug vars are valid + if debug_var_ids.is_empty() && op_indptr_for_var_ids.is_empty() { + if node_indptr_for_op_idx.iter().all(|&ptr| ptr == 0) { + return Ok(Self { + debug_var_ids, + op_indptr_for_var_ids, + node_indptr_for_op_idx, + }); + } else { + return Err(DecoratorIndexError::InternalStructure); + } + } + + // Validate the structure + if op_indptr_for_var_ids.is_empty() { + return Err(DecoratorIndexError::InternalStructure); + } + + if op_indptr_for_var_ids[0] != 0 { + return Err(DecoratorIndexError::InternalStructure); + } + + let Some(&last_op_ptr) = op_indptr_for_var_ids.last() else { + return Err(DecoratorIndexError::InternalStructure); + }; + if last_op_ptr > debug_var_ids.len() { + return Err(DecoratorIndexError::InternalStructure); + } + + if node_indptr_for_op_idx.is_empty() { + return Err(DecoratorIndexError::InternalStructure); + } + + let node_slice = node_indptr_for_op_idx.as_slice(); + + if node_slice[0] != 0 { + return Err(DecoratorIndexError::InternalStructure); + } + + let Some(&last_node_ptr) = node_slice.last() else { + return Err(DecoratorIndexError::InternalStructure); + }; + if last_node_ptr > op_indptr_for_var_ids.len() - 1 { + return Err(DecoratorIndexError::InternalStructure); + } + + // Ensure monotonicity + for window in op_indptr_for_var_ids.windows(2) { + if window[0] > window[1] { + return Err(DecoratorIndexError::InternalStructure); + } + } + + for window in node_slice.windows(2) { + if window[0] > window[1] { + return Err(DecoratorIndexError::InternalStructure); + } + } + + Ok(Self { + debug_var_ids, + op_indptr_for_var_ids, + node_indptr_for_op_idx, + }) + } + + /// Validate CSR structure integrity. + pub(super) fn validate_csr(&self, debug_var_count: usize) -> Result<(), String> { + // Completely empty structures are valid + if self.debug_var_ids.is_empty() + && self.op_indptr_for_var_ids.is_empty() + && self.node_indptr_for_op_idx.is_empty() + { + return Ok(()); + } + + // Nodes with no debug vars are valid + if self.debug_var_ids.is_empty() && self.op_indptr_for_var_ids.is_empty() { + if !self.node_indptr_for_op_idx.iter().all(|&ptr| ptr == 0) { + return Err("node pointers must all be 0 when there are no debug vars".to_string()); + } + return Ok(()); + } + + // Validate all debug var IDs + for &var_id in &self.debug_var_ids { + if var_id.to_usize() >= debug_var_count { + return Err(format!( + "Invalid debug var ID {}: exceeds count {}", + var_id.to_usize(), + debug_var_count + )); + } + } + + // Validate op_indptr_for_var_ids + if self.op_indptr_for_var_ids.is_empty() { + return Err("op_indptr_for_var_ids cannot be empty".to_string()); + } + + if self.op_indptr_for_var_ids[0] != 0 { + return Err("op_indptr_for_var_ids must start at 0".to_string()); + } + + for window in self.op_indptr_for_var_ids.windows(2) { + if window[0] > window[1] { + return Err(format!( + "op_indptr_for_var_ids not monotonic: {} > {}", + window[0], window[1] + )); + } + } + + if *self.op_indptr_for_var_ids.last().unwrap() != self.debug_var_ids.len() { + return Err(format!( + "op_indptr_for_var_ids end {} doesn't match debug_var_ids length {}", + self.op_indptr_for_var_ids.last().unwrap(), + self.debug_var_ids.len() + )); + } + + // Validate node_indptr_for_op_idx + let node_slice = self.node_indptr_for_op_idx.as_slice(); + if node_slice.is_empty() { + return Err("node_indptr_for_op_idx cannot be empty".to_string()); + } + + if node_slice[0] != 0 { + return Err("node_indptr_for_op_idx must start at 0".to_string()); + } + + for window in node_slice.windows(2) { + if window[0] > window[1] { + return Err(format!( + "node_indptr_for_op_idx not monotonic: {} > {}", + window[0], window[1] + )); + } + } + + let max_node_ptr = self.op_indptr_for_var_ids.len() - 1; + if *node_slice.last().unwrap() > max_node_ptr { + return Err(format!( + "node_indptr_for_op_idx end {} exceeds op_indptr bounds {}", + node_slice.last().unwrap(), + max_node_ptr + )); + } + + Ok(()) + } + + /// Returns true if this storage is empty. + pub fn is_empty(&self) -> bool { + self.node_indptr_for_op_idx.is_empty() + } + + /// Get the number of nodes in this storage. + pub fn num_nodes(&self) -> usize { + if self.node_indptr_for_op_idx.is_empty() { + 0 + } else { + self.node_indptr_for_op_idx.len() - 1 + } + } + + /// Get the total number of debug var IDs. + pub fn num_debug_var_ids(&self) -> usize { + self.debug_var_ids.len() + } + + /// Add debug variable information for a node incrementally. + /// + /// This method allows building up the structure by adding debug var IDs for nodes + /// in sequential order only. + pub fn add_debug_var_info_for_node( + &mut self, + node: MastNodeId, + debug_vars_info: Vec<(usize, DebugVarId)>, + ) -> Result<(), DecoratorIndexError> { + // Enforce sequential node ids + let expected = MastNodeId::new_unchecked(self.num_nodes() as u32); + if node < expected { + return Err(DecoratorIndexError::NodeIndex(node)); + } + // Create empty nodes for gaps + for idx in expected.0..node.0 { + self.add_debug_var_info_for_node(MastNodeId::new_unchecked(idx), vec![]) + .unwrap(); + } + + let op_start = self.op_indptr_for_var_ids.len(); + + if self.node_indptr_for_op_idx.is_empty() { + self.node_indptr_for_op_idx + .push(op_start) + .map_err(|_| DecoratorIndexError::OperationIndex { node, operation: op_start })?; + } else { + let last = MastNodeId::new_unchecked((self.node_indptr_for_op_idx.len() - 1) as u32); + self.node_indptr_for_op_idx[last] = op_start; + } + + if debug_vars_info.is_empty() { + if op_start == self.op_indptr_for_var_ids.len() + && !self.op_indptr_for_var_ids.is_empty() + { + self.op_indptr_for_var_ids.push(self.debug_var_ids.len()); + } + + self.node_indptr_for_op_idx + .push(op_start) + .map_err(|_| DecoratorIndexError::OperationIndex { node, operation: op_start })?; + } else { + let max_op_idx = debug_vars_info.last().unwrap().0; + let mut it = debug_vars_info.into_iter().peekable(); + + for op in 0..=max_op_idx { + self.op_indptr_for_var_ids.push(self.debug_var_ids.len()); + while it.peek().is_some_and(|(i, _)| *i == op) { + self.debug_var_ids.push(it.next().unwrap().1); + } + } + self.op_indptr_for_var_ids.push(self.debug_var_ids.len()); + + let end_ops = self.op_indptr_for_var_ids.len() - 1; + self.node_indptr_for_op_idx + .push(end_ops) + .map_err(|_| DecoratorIndexError::OperationIndex { node, operation: end_ops })?; + } + + Ok(()) + } + + /// Get all debug var IDs for a specific operation within a node. + pub fn debug_var_ids_for_operation( + &self, + node: MastNodeId, + operation: usize, + ) -> Result<&[DebugVarId], DecoratorIndexError> { + let op_range = self.operation_range_for_node(node)?; + if operation >= op_range.len() { + return Ok(&[]); + } + + let op_start_idx = op_range.start + operation; + if op_start_idx + 1 >= self.op_indptr_for_var_ids.len() { + return Err(DecoratorIndexError::InternalStructure); + } + + let var_start = self.op_indptr_for_var_ids[op_start_idx]; + let var_end = self.op_indptr_for_var_ids[op_start_idx + 1]; + + if var_start > var_end || var_end > self.debug_var_ids.len() { + return Err(DecoratorIndexError::InternalStructure); + } + + Ok(&self.debug_var_ids[var_start..var_end]) + } + + /// Get the range of operation indices for a given node. + pub fn operation_range_for_node( + &self, + node: MastNodeId, + ) -> Result, DecoratorIndexError> { + let node_slice = self.node_indptr_for_op_idx.as_slice(); + let node_idx = node.to_usize(); + + if node_idx + 1 >= node_slice.len() { + return Err(DecoratorIndexError::NodeIndex(node)); + } + + let start = node_slice[node_idx]; + let end = node_slice[node_idx + 1]; + + if start > end || end > self.op_indptr_for_var_ids.len() { + return Err(DecoratorIndexError::InternalStructure); + } + + Ok(start..end) + } + + /// Serialize this OpToDebugVarIds. + pub(super) fn write_into(&self, target: &mut W) { + self.debug_var_ids.write_into(target); + self.op_indptr_for_var_ids.write_into(target); + self.node_indptr_for_op_idx.write_into(target); + } + + /// Deserialize OpToDebugVarIds. + pub(super) fn read_from( + source: &mut R, + debug_var_count: usize, + ) -> Result { + let debug_var_ids: Vec = Deserializable::read_from(source)?; + let op_indptr_for_var_ids: Vec = Deserializable::read_from(source)?; + let node_indptr_for_op_idx: IndexVec = + Deserializable::read_from(source)?; + + let result = + Self::from_components(debug_var_ids, op_indptr_for_var_ids, node_indptr_for_op_idx) + .map_err(|e| DeserializationError::InvalidValue(e.to_string()))?; + + result.validate_csr(debug_var_count).map_err(|e| { + DeserializationError::InvalidValue(format!("OpToDebugVarIds validation failed: {e}")) + })?; + + Ok(result) + } + + /// Clears this storage. + pub fn clear(&mut self) { + self.debug_var_ids.clear(); + self.op_indptr_for_var_ids.clear(); + self.node_indptr_for_op_idx = IndexVec::new(); + } +} + +impl Default for OpToDebugVarIds { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use miden_utils_indexing::IndexVec; + + use super::*; + + fn test_var_id(value: u32) -> DebugVarId { + DebugVarId::from(value) + } + + fn test_node_id(value: u32) -> MastNodeId { + MastNodeId::new_unchecked(value) + } + + /// Helper: Node 0: Op 0 -> [var0, var1], Op 1 -> [var2]; Node 1: Op 0 -> [var3, var4, var5] + fn create_test_storage() -> OpToDebugVarIds { + let debug_var_ids = vec![ + test_var_id(0), + test_var_id(1), + test_var_id(2), + test_var_id(3), + test_var_id(4), + test_var_id(5), + ]; + let op_indptr = vec![0, 2, 3, 6]; + let mut node_indptr = IndexVec::new(); + node_indptr.push(0).unwrap(); + node_indptr.push(2).unwrap(); + node_indptr.push(3).unwrap(); + + OpToDebugVarIds::from_components(debug_var_ids, op_indptr, node_indptr).unwrap() + } + + #[test] + fn test_add_and_lookup() { + let mut storage = OpToDebugVarIds::new(); + + // Node 0: op 0 -> [var10, var11], op 2 -> [var12] + storage + .add_debug_var_info_for_node( + test_node_id(0), + vec![(0, test_var_id(10)), (0, test_var_id(11)), (2, test_var_id(12))], + ) + .unwrap(); + + // Node 1: op 0 -> [var20] + storage + .add_debug_var_info_for_node(test_node_id(1), vec![(0, test_var_id(20))]) + .unwrap(); + + assert_eq!(storage.num_nodes(), 2); + assert_eq!(storage.num_debug_var_ids(), 4); + + // Lookup node 0 + assert_eq!( + storage.debug_var_ids_for_operation(test_node_id(0), 0).unwrap(), + &[test_var_id(10), test_var_id(11)] + ); + assert_eq!(storage.debug_var_ids_for_operation(test_node_id(0), 1).unwrap(), &[]); + assert_eq!( + storage.debug_var_ids_for_operation(test_node_id(0), 2).unwrap(), + &[test_var_id(12)] + ); + + // Lookup node 1 + assert_eq!( + storage.debug_var_ids_for_operation(test_node_id(1), 0).unwrap(), + &[test_var_id(20)] + ); + + // Out-of-range operation returns empty + assert_eq!(storage.debug_var_ids_for_operation(test_node_id(0), 99).unwrap(), &[]); + } + + #[test] + fn test_from_components_and_validate() { + let storage = create_test_storage(); + assert_eq!(storage.num_nodes(), 2); + assert_eq!(storage.num_debug_var_ids(), 6); + assert!(storage.validate_csr(6).is_ok()); + + // Validation fails when var count is too low + assert!(storage.validate_csr(3).is_err()); + + // Invalid components are rejected + let result = OpToDebugVarIds::from_components( + vec![test_var_id(0)], + vec![0, 5], // points past end + IndexVec::new(), + ); + assert_eq!(result, Err(DecoratorIndexError::InternalStructure)); + } + + #[test] + fn test_serialization_round_trip() { + let storage = create_test_storage(); + + let mut bytes = Vec::new(); + storage.write_into(&mut bytes); + + let mut reader = crate::serde::SliceReader::new(&bytes); + let deserialized = OpToDebugVarIds::read_from(&mut reader, 6).unwrap(); + + assert_eq!(storage, deserialized); + } +} diff --git a/core/src/mast/debuginfo/decorator_storage/tests.rs b/core/src/mast/debuginfo/decorator_storage/tests.rs index d1bb017abd..ca8369b43d 100644 --- a/core/src/mast/debuginfo/decorator_storage/tests.rs +++ b/core/src/mast/debuginfo/decorator_storage/tests.rs @@ -772,6 +772,8 @@ fn test_sparse_debuginfo_round_trip() { asm_op_storage: crate::mast::OpToAsmOpId::new(), error_codes, procedure_names: BTreeMap::new(), + debug_vars: IndexVec::new(), + op_debug_var_storage: crate::mast::debuginfo::debug_var_storage::OpToDebugVarIds::default(), }; // Serialize and deserialize @@ -781,3 +783,88 @@ fn test_sparse_debuginfo_round_trip() { // Verify assert_eq!(debug_info.num_decorators(), deserialized.num_decorators()); } + +#[test] +fn test_debuginfo_with_debug_vars_round_trip() { + use crate::{ + mast::debuginfo::DebugInfo, + operations::{DebugVarInfo, DebugVarLocation, Decorator}, + serde::{Deserializable, Serializable}, + }; + + // Create DebugInfo with both decorators and debug vars + let mut debug_info = DebugInfo::new(); + + // Add some decorators + let dec_id0 = debug_info.add_decorator(Decorator::Trace(0)).unwrap(); + let dec_id1 = debug_info.add_decorator(Decorator::Trace(1)).unwrap(); + + // Add debug variables + let var0 = DebugVarInfo::new("x", DebugVarLocation::Stack(0)); + let mut var1 = DebugVarInfo::new("y", DebugVarLocation::Memory(100)); + var1.set_type_id(42); + let mut var2 = DebugVarInfo::new("param", DebugVarLocation::Local(-3)); + var2.set_arg_index(1); + + let var_id0 = debug_info.add_debug_var(var0.clone()).unwrap(); + let var_id1 = debug_info.add_debug_var(var1.clone()).unwrap(); + let var_id2 = debug_info.add_debug_var(var2.clone()).unwrap(); + + assert_eq!(debug_info.num_debug_vars(), 3); + + // Register decorators and debug vars for node 0 + debug_info + .register_op_indexed_decorators( + MastNodeId::new_unchecked(0), + vec![(0, dec_id0), (1, dec_id1)], + ) + .unwrap(); + + debug_info + .register_op_indexed_debug_vars( + MastNodeId::new_unchecked(0), + vec![(0, var_id0), (0, var_id1), (2, var_id2)], + ) + .unwrap(); + + // Verify accessors before serialization + assert_eq!(debug_info.debug_var(var_id0).unwrap().name(), "x"); + assert_eq!(debug_info.debug_var(var_id1).unwrap().name(), "y"); + assert_eq!(debug_info.debug_var(var_id2).unwrap().name(), "param"); + + let op0_vars = debug_info.debug_vars_for_operation(MastNodeId::new_unchecked(0), 0); + assert_eq!(op0_vars.len(), 2); + assert_eq!(op0_vars[0], var_id0); + assert_eq!(op0_vars[1], var_id1); + + let op2_vars = debug_info.debug_vars_for_operation(MastNodeId::new_unchecked(0), 2); + assert_eq!(op2_vars.len(), 1); + assert_eq!(op2_vars[0], var_id2); + + // No vars at op 1 + let op1_vars = debug_info.debug_vars_for_operation(MastNodeId::new_unchecked(0), 1); + assert_eq!(op1_vars.len(), 0); + + // Serialize and deserialize + let bytes = debug_info.to_bytes(); + let deserialized = DebugInfo::read_from_bytes(&bytes).expect("Should deserialize successfully"); + + // Verify decorators survived + assert_eq!(deserialized.num_decorators(), 2); + + // Verify debug vars survived + assert_eq!(deserialized.num_debug_vars(), 3); + assert_eq!(deserialized.debug_var(var_id0).unwrap(), &var0); + assert_eq!(deserialized.debug_var(var_id1).unwrap(), &var1); + assert_eq!(deserialized.debug_var(var_id2).unwrap(), &var2); + + // Verify debug var storage survived + let deser_op0 = deserialized.debug_vars_for_operation(MastNodeId::new_unchecked(0), 0); + assert_eq!(deser_op0, &[var_id0, var_id1]); + + let deser_op1 = deserialized.debug_vars_for_operation(MastNodeId::new_unchecked(0), 1); + assert_eq!(deser_op1, &[]); + + let deser_op2 = deserialized.debug_vars_for_operation(MastNodeId::new_unchecked(0), 2); + assert_eq!(deser_op2, &[var_id2]); +} diff --git a/core/src/mast/debuginfo/mod.rs b/core/src/mast/debuginfo/mod.rs index 1d32ebd037..18046c4ae1 100644 --- a/core/src/mast/debuginfo/mod.rs +++ b/core/src/mast/debuginfo/mod.rs @@ -57,7 +57,7 @@ use crate::{ asm_op::{AsmOpDataBuilder, AsmOpInfo}, decorator::{DecoratorDataBuilder, DecoratorInfo}, }, - operations::AssemblyOp, + operations::{AssemblyOp, DebugVarInfo}, serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, utils::{Idx, IndexVec}, }; @@ -70,6 +70,9 @@ pub use decorator_storage::{ DecoratedLinks, DecoratedLinksIter, DecoratorIndexError, OpToDecoratorIds, }; +mod debug_var_storage; +pub use debug_var_storage::{DebugVarId, OpToDebugVarIds}; + mod node_decorator_storage; pub use node_decorator_storage::NodeToDecoratorIds; @@ -95,6 +98,12 @@ pub struct DebugInfo { /// Efficient access to AssemblyOps per operation per node. asm_op_storage: OpToAsmOpId, + /// All debug variable information in the MAST forest. + debug_vars: IndexVec, + + /// Efficient access to debug variables per operation per node. + op_debug_var_storage: OpToDebugVarIds, + /// Maps error codes to error messages. error_codes: BTreeMap>, @@ -115,6 +124,8 @@ impl DebugInfo { node_decorator_storage: NodeToDecoratorIds::new(), asm_ops: IndexVec::new(), asm_op_storage: OpToAsmOpId::new(), + debug_vars: IndexVec::new(), + op_debug_var_storage: OpToDebugVarIds::new(), error_codes: BTreeMap::new(), procedure_names: BTreeMap::new(), } @@ -137,6 +148,8 @@ impl DebugInfo { node_decorator_storage: NodeToDecoratorIds::with_capacity(nodes_capacity, 0, 0), asm_ops: IndexVec::new(), asm_op_storage: OpToAsmOpId::new(), + debug_vars: IndexVec::new(), + op_debug_var_storage: OpToDebugVarIds::new(), error_codes: BTreeMap::new(), procedure_names: BTreeMap::new(), } @@ -157,6 +170,8 @@ impl DebugInfo { node_decorator_storage: NodeToDecoratorIds::new(), asm_ops: IndexVec::new(), asm_op_storage: OpToAsmOpId::new(), + debug_vars: IndexVec::new(), + op_debug_var_storage: OpToDebugVarIds::new(), error_codes: BTreeMap::new(), procedure_names: BTreeMap::new(), } @@ -165,16 +180,18 @@ impl DebugInfo { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns true if this [DebugInfo] has no decorators, asm_ops, error codes, or procedure - /// names. + /// Returns true if this [DebugInfo] has no decorators, asm_ops, debug vars, error codes, or + /// procedure names. pub fn is_empty(&self) -> bool { self.decorators.is_empty() && self.asm_ops.is_empty() + && self.debug_vars.is_empty() && self.error_codes.is_empty() && self.procedure_names.is_empty() } - /// Strips all debug information, removing decorators, asm_ops, error codes, and procedure + /// Strips all debug information, removing decorators, asm_ops, debug vars, error codes, and + /// procedure /// names. /// /// This is used for release builds where debug info is not needed. @@ -183,6 +200,8 @@ impl DebugInfo { self.decorators = IndexVec::new(); self.asm_ops = IndexVec::new(); self.asm_op_storage = OpToAsmOpId::new(); + self.debug_vars = IndexVec::new(); + self.op_debug_var_storage.clear(); self.error_codes.clear(); self.procedure_names.clear(); } @@ -234,6 +253,35 @@ impl DebugInfo { self.op_decorator_storage.decorator_links_for_node(node_id) } + // DEBUG VARIABLE ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the number of debug variables. + pub fn num_debug_vars(&self) -> usize { + self.debug_vars.len() + } + + /// Returns all debug variables as a slice. + pub fn debug_vars(&self) -> &[DebugVarInfo] { + self.debug_vars.as_slice() + } + + /// Returns the debug variable with the given ID, if it exists. + pub fn debug_var(&self, debug_var_id: DebugVarId) -> Option<&DebugVarInfo> { + self.debug_vars.get(debug_var_id) + } + + /// Returns debug variable IDs for a specific operation within a node. + pub fn debug_vars_for_operation( + &self, + node_id: MastNodeId, + local_op_idx: usize, + ) -> &[DebugVarId] { + self.op_debug_var_storage + .debug_var_ids_for_operation(node_id, local_op_idx) + .unwrap_or(&[]) + } + // DECORATOR MUTATORS // -------------------------------------------------------------------------------------------- @@ -345,6 +393,28 @@ impl DebugInfo { self.asm_op_storage = self.asm_op_storage.remap_nodes(remapping); } + // DEBUG VARIABLE MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Adds a debug variable and returns its ID. + pub fn add_debug_var( + &mut self, + debug_var: DebugVarInfo, + ) -> Result { + self.debug_vars.push(debug_var).map_err(|_| MastForestError::TooManyDecorators) + } + + /// Registers operation-indexed debug variables for a node. + /// + /// This associates already-added debug variables with specific operations within a node. + pub fn register_op_indexed_debug_vars( + &mut self, + node_id: MastNodeId, + debug_vars_info: Vec<(usize, DebugVarId)>, + ) -> Result<(), crate::mast::debuginfo::decorator_storage::DecoratorIndexError> { + self.op_debug_var_storage.add_debug_var_info_for_node(node_id, debug_vars_info) + } + // ERROR CODE METHODS // -------------------------------------------------------------------------------------------- @@ -426,8 +496,10 @@ impl DebugInfo { /// - All CSR structures in op_decorator_storage /// - All CSR structures in node_decorator_storage /// - All CSR structures in asm_op_storage + /// - All CSR structures in op_debug_var_storage /// - All decorator IDs reference valid decorators /// - All AsmOpIds reference valid AssemblyOps + /// - All debug var IDs reference valid debug vars pub(super) fn validate(&self) -> Result<(), String> { let decorator_count = self.decorators.len(); let asm_op_count = self.asm_ops.len(); @@ -441,6 +513,10 @@ impl DebugInfo { // Validate OpToAsmOpId CSR self.asm_op_storage.validate_csr(asm_op_count)?; + // Validate OpToDebugVarIds CSR + let debug_var_count = self.debug_vars.len(); + self.op_debug_var_storage.validate_csr(debug_var_count)?; + Ok(()) } @@ -499,6 +575,12 @@ impl Serializable for DebugInfo { // 7. Serialize OpToAsmOpId CSR (dense representation) self.asm_op_storage.write_into(target); + + // 8. Serialize debug variables + self.debug_vars.write_into(target); + + // 9. Serialize OpToDebugVarIds CSR + self.op_debug_var_storage.write_into(target); } } @@ -559,13 +641,21 @@ impl Deserializable for DebugInfo { // 9. Read OpToAsmOpId CSR (dense representation) let asm_op_storage = OpToAsmOpId::read_from(source, asm_ops.len())?; - // 10. Construct and validate DebugInfo + // 10. Read debug variables + let debug_vars: IndexVec = Deserializable::read_from(source)?; + + // 11. Read OpToDebugVarIds CSR + let op_debug_var_storage = OpToDebugVarIds::read_from(source, debug_vars.len())?; + + // 12. Construct and validate DebugInfo let debug_info = DebugInfo { decorators, op_decorator_storage, node_decorator_storage, asm_ops, asm_op_storage, + debug_vars, + op_debug_var_storage, error_codes, procedure_names, }; diff --git a/core/src/mast/mod.rs b/core/src/mast/mod.rs index 1c5b79dd11..b7d487dc88 100644 --- a/core/src/mast/mod.rs +++ b/core/src/mast/mod.rs @@ -57,7 +57,7 @@ use crate::{ Felt, LexicographicWord, Word, advice::AdviceMap, field::PrimeField64, - operations::{AssemblyOp, Decorator}, + operations::{AssemblyOp, DebugVarInfo, Decorator}, serde::{ BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader, @@ -67,8 +67,8 @@ use crate::{ mod debuginfo; pub use debuginfo::{ - AsmOpIndexError, DebugInfo, DecoratedLinks, DecoratedLinksIter, DecoratorIndexError, - NodeToDecoratorIds, OpToAsmOpId, OpToDecoratorIds, + AsmOpIndexError, DebugInfo, DebugVarId, DecoratedLinks, DecoratedLinksIter, + DecoratorIndexError, NodeToDecoratorIds, OpToAsmOpId, OpToDebugVarIds, OpToDecoratorIds, }; mod serialization; @@ -577,6 +577,28 @@ impl MastForest { self.debug_info.add_decorator(decorator) } + /// Adds a debug variable to the forest, and returns the associated [`DebugVarId`]. + pub fn add_debug_var( + &mut self, + debug_var: DebugVarInfo, + ) -> Result { + self.debug_info.add_debug_var(debug_var) + } + + /// Returns debug variable IDs for a specific operation within a node. + pub fn debug_vars_for_operation( + &self, + node_id: MastNodeId, + local_op_idx: usize, + ) -> &[DebugVarId] { + self.debug_info.debug_vars_for_operation(node_id, local_op_idx) + } + + /// Returns the debug variable with the given ID, if it exists. + pub fn debug_var(&self, debug_var_id: DebugVarId) -> Option<&DebugVarInfo> { + self.debug_info.debug_var(debug_var_id) + } + /// Adds decorator IDs for a node to the storage. /// /// Used when building nodes for efficient decorator access during execution. diff --git a/core/src/operations/decorators/debug_var.rs b/core/src/operations/decorators/debug_var.rs new file mode 100644 index 0000000000..e7f838910c --- /dev/null +++ b/core/src/operations/decorators/debug_var.rs @@ -0,0 +1,349 @@ +use alloc::{string::String, sync::Arc, vec::Vec}; +use core::{fmt, num::NonZeroU32}; + +use miden_crypto::field::PrimeField64; +use miden_debug_types::FileLineCol; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::{ + Felt, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; + +// DEBUG VARIABLE INFO +// ================================================================================================ + +/// Debug information for tracking a source-level variable. +/// +/// This decorator provides debuggers with information about where a variable's +/// value can be found at a particular point in the program execution. +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugVarInfo { + /// Variable name as it appears in source code. + #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_arc_str"))] + name: Arc, + /// Type information (encoded as type index in debug_info section) + type_id: Option, + /// If this is a function parameter, its 1-based index. + arg_index: Option, + /// Source file location (file:line:column). + /// This should only be set when the location differs from the AssemblyOp decorator + /// location associated with the same instruction, to avoid package bloat. + location: Option, + /// Where to find the variable's value at this point + value_location: DebugVarLocation, +} + +impl DebugVarInfo { + /// Creates a new [DebugVarInfo] with the specified variable name and location. + pub fn new(name: impl Into>, value_location: DebugVarLocation) -> Self { + Self { + name: name.into(), + type_id: None, + arg_index: None, + location: None, + value_location, + } + } + + /// Returns the variable name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the type ID if set. + pub fn type_id(&self) -> Option { + self.type_id + } + + /// Sets the type ID for this variable. + pub fn set_type_id(&mut self, type_id: u32) { + self.type_id = Some(type_id); + } + + /// Returns the argument index if this is a function parameter. + /// The index is 1-based. + pub fn arg_index(&self) -> Option { + self.arg_index + } + + /// Sets the argument index for this variable. + /// + /// # Panics + /// Panics if `arg_index` is 0, since argument indices are 1-based. + pub fn set_arg_index(&mut self, arg_index: u32) { + self.arg_index = + Some(NonZeroU32::new(arg_index).expect("argument index must be 1-based (non-zero)")); + } + + /// Returns the source location if set. + /// This is only set when the location differs from the AssemblyOp decorator location. + pub fn location(&self) -> Option<&FileLineCol> { + self.location.as_ref() + } + + /// Sets the source location for this variable. + /// Only set this when the location differs from the AssemblyOp decorator location + /// to avoid package bloat. + pub fn set_location(&mut self, location: FileLineCol) { + self.location = Some(location); + } + + /// Returns where the variable's value can be found. + pub fn value_location(&self) -> &DebugVarLocation { + &self.value_location + } +} + +/// Serde deserializer for `Arc`. +#[cfg(feature = "serde")] +fn deserialize_arc_str<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + use alloc::string::String; + let s = String::deserialize(deserializer)?; + Ok(Arc::from(s)) +} + +impl fmt::Display for DebugVarInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "var.{}", self.name)?; + + if let Some(arg_index) = self.arg_index { + write!(f, "[arg{}]", arg_index)?; + } + + write!(f, " = {}", self.value_location)?; + + if let Some(loc) = &self.location { + write!(f, " {}", loc)?; + } + + Ok(()) + } +} + +// DEBUG VARIABLE LOCATION +// ================================================================================================ + +/// Describes where a variable's value can be found during execution. +/// +/// This enum models the different ways a variable's value might be stored +/// during program execution, ranging from simple stack positions to complex +/// expressions. +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum DebugVarLocation { + /// Variable is at stack position N (0 = top of stack) + Stack(u8), + /// Variable is in memory at the given element address + Memory(u32), + /// Variable is a constant field element + Const(Felt), + /// Variable is in local memory at a signed offset from FMP. + /// + /// The actual memory address is computed as: `FMP + offset` + /// where offset is typically negative (locals are below FMP). + /// For example, with 3 locals: local\[0\] has offset -3, local\[2\] has offset -1. + Local(i16), + /// Complex location described by expression bytes. + /// This is used for variables that require computation to locate, + /// such as struct fields or array elements. + Expression(Vec), +} + +impl fmt::Display for DebugVarLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stack(pos) => write!(f, "stack[{}]", pos), + Self::Memory(addr) => write!(f, "mem[{}]", addr), + Self::Const(val) => write!(f, "const({})", val.as_canonical_u64()), + Self::Local(offset) => write!(f, "FMP{:+}", offset), + Self::Expression(bytes) => { + write!(f, "expr(")?; + for (i, byte) in bytes.iter().enumerate() { + if i > 0 { + write!(f, " ")?; + } + write!(f, "{:02x}", byte)?; + } + write!(f, ")") + }, + } + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugVarLocation { + fn write_into(&self, target: &mut W) { + match self { + Self::Stack(pos) => { + target.write_u8(0); + target.write_u8(*pos); + }, + Self::Memory(addr) => { + target.write_u8(1); + target.write_u32(*addr); + }, + Self::Const(felt) => { + target.write_u8(2); + target.write_u64(felt.as_canonical_u64()); + }, + Self::Local(offset) => { + target.write_u8(3); + target.write_bytes(&offset.to_le_bytes()); + }, + Self::Expression(bytes) => { + target.write_u8(4); + bytes.write_into(target); + }, + } + } +} + +impl Deserializable for DebugVarLocation { + fn read_from(source: &mut R) -> Result { + let tag = source.read_u8()?; + match tag { + 0 => Ok(Self::Stack(source.read_u8()?)), + 1 => Ok(Self::Memory(source.read_u32()?)), + 2 => { + let value = source.read_u64()?; + Ok(Self::Const(Felt::new(value))) + }, + 3 => { + let bytes = source.read_array::<2>()?; + Ok(Self::Local(i16::from_le_bytes(bytes))) + }, + 4 => { + let bytes = Vec::::read_from(source)?; + Ok(Self::Expression(bytes)) + }, + _ => Err(DeserializationError::InvalidValue(format!( + "invalid DebugVarLocation tag: {tag}" + ))), + } + } +} + +impl Serializable for DebugVarInfo { + fn write_into(&self, target: &mut W) { + (*self.name).write_into(target); + self.value_location.write_into(target); + self.type_id.write_into(target); + self.arg_index.map(|n| n.get()).write_into(target); + self.location.write_into(target); + } +} + +impl Deserializable for DebugVarInfo { + fn read_from(source: &mut R) -> Result { + let name: Arc = String::read_from(source)?.into(); + let value_location = DebugVarLocation::read_from(source)?; + let type_id = Option::::read_from(source)?; + let arg_index = Option::::read_from(source)? + .map(|n| { + NonZeroU32::new(n).ok_or_else(|| { + DeserializationError::InvalidValue("arg_index must be non-zero".into()) + }) + }) + .transpose()?; + let location = Option::::read_from(source)?; + + Ok(Self { + name, + type_id, + arg_index, + location, + value_location, + }) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use miden_debug_types::{ColumnNumber, LineNumber, Uri}; + + use super::*; + use crate::serde::{Deserializable, Serializable, SliceReader}; + + #[test] + fn debug_var_info_display_simple() { + let var = DebugVarInfo::new("x", DebugVarLocation::Stack(0)); + assert_eq!(var.to_string(), "var.x = stack[0]"); + } + + #[test] + fn debug_var_info_display_with_arg() { + let mut var = DebugVarInfo::new("param", DebugVarLocation::Stack(2)); + var.set_arg_index(1); + assert_eq!(var.to_string(), "var.param[arg1] = stack[2]"); + } + + #[test] + fn debug_var_info_display_with_location() { + let mut var = DebugVarInfo::new("y", DebugVarLocation::Memory(100)); + var.set_location(FileLineCol::new( + Uri::new("test.rs"), + LineNumber::new(42).unwrap(), + ColumnNumber::new(5).unwrap(), + )); + assert_eq!(var.to_string(), "var.y = mem[100] [test.rs@42:5]"); + } + + #[test] + fn debug_var_location_display() { + assert_eq!(DebugVarLocation::Stack(0).to_string(), "stack[0]"); + assert_eq!(DebugVarLocation::Memory(256).to_string(), "mem[256]"); + assert_eq!(DebugVarLocation::Const(Felt::new(42)).to_string(), "const(42)"); + assert_eq!(DebugVarLocation::Local(-3).to_string(), "FMP-3"); + assert_eq!( + DebugVarLocation::Expression(vec![0x10, 0x20, 0x30]).to_string(), + "expr(10 20 30)" + ); + } + + #[test] + fn debug_var_location_serialization_round_trip() { + let locations = [ + DebugVarLocation::Stack(7), + DebugVarLocation::Memory(0xdead_beef), + DebugVarLocation::Const(Felt::new(999)), + DebugVarLocation::Local(-3), + DebugVarLocation::Expression(vec![0x10, 0x20, 0x30]), + ]; + + for loc in &locations { + let mut bytes = Vec::new(); + loc.write_into(&mut bytes); + let mut reader = SliceReader::new(&bytes); + let deser = DebugVarLocation::read_from(&mut reader).unwrap(); + assert_eq!(&deser, loc); + } + } + + #[test] + fn debug_var_info_serialization_round_trip_all_fields() { + let mut var = DebugVarInfo::new("full", DebugVarLocation::Expression(vec![0xaa, 0xbb])); + var.set_type_id(7); + var.set_arg_index(2); + var.set_location(FileLineCol::new( + Uri::new("lib.rs"), + LineNumber::new(50).unwrap(), + ColumnNumber::new(10).unwrap(), + )); + + let mut bytes = Vec::new(); + var.write_into(&mut bytes); + let mut reader = SliceReader::new(&bytes); + let deser = DebugVarInfo::read_from(&mut reader).unwrap(); + assert_eq!(deser, var); + } +} diff --git a/core/src/operations/decorators/mod.rs b/core/src/operations/decorators/mod.rs index 12273f5a00..4700a67263 100644 --- a/core/src/operations/decorators/mod.rs +++ b/core/src/operations/decorators/mod.rs @@ -12,6 +12,9 @@ pub use assembly_op::AssemblyOp; mod debug; pub use debug::DebugOptions; +mod debug_var; +pub use debug_var::{DebugVarInfo, DebugVarLocation}; + use crate::mast::{DecoratedOpLink, DecoratorFingerprint}; // DECORATORS diff --git a/core/src/operations/mod.rs b/core/src/operations/mod.rs index 8c87172dc7..461efe69bb 100644 --- a/core/src/operations/mod.rs +++ b/core/src/operations/mod.rs @@ -5,7 +5,9 @@ use miden_crypto::field::PrimeField64; use serde::{Deserialize, Serialize}; mod decorators; -pub use decorators::{AssemblyOp, DebugOptions, Decorator, DecoratorList}; +pub use decorators::{ + AssemblyOp, DebugOptions, DebugVarInfo, DebugVarLocation, Decorator, DecoratorList, +}; pub use opcode_constants::*; use crate::{ diff --git a/crates/assembly-syntax/src/ast/instruction/mod.rs b/crates/assembly-syntax/src/ast/instruction/mod.rs index 01dfaac37a..5f5443eca4 100644 --- a/crates/assembly-syntax/src/ast/instruction/mod.rs +++ b/crates/assembly-syntax/src/ast/instruction/mod.rs @@ -275,6 +275,7 @@ pub enum Instruction { // ----- debug decorators -------------------------------------------------------------------- Debug(DebugOptions), + DebugVar(miden_core::operations::DebugVarInfo), // ----- event decorators -------------------------------------------------------------------- Emit, @@ -282,6 +283,16 @@ pub enum Instruction { Trace(ImmU32), } +impl Instruction { + /// Returns true if this instruction has a textual representation in Miden Assembly. + /// + /// Some instructions (like [`DebugVar`](Self::DebugVar)) are compiler-internal and have + /// no surface syntax. They should be skipped during pretty-printing. + pub const fn has_textual_representation(&self) -> bool { + !matches!(self, Self::DebugVar(_)) + } +} + impl core::fmt::Display for Instruction { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { use crate::prettier::PrettyPrint; diff --git a/crates/assembly-syntax/src/ast/instruction/print.rs b/crates/assembly-syntax/src/ast/instruction/print.rs index e0c82bad84..c28c716218 100644 --- a/crates/assembly-syntax/src/ast/instruction/print.rs +++ b/crates/assembly-syntax/src/ast/instruction/print.rs @@ -8,6 +8,10 @@ impl PrettyPrint for Instruction { fn render(&self) -> Document { use crate::prettier::*; + if !self.has_textual_representation() { + return Document::Empty; + } + match self { Self::Nop => const_text("nop"), Self::Assert => const_text("assert"), @@ -331,6 +335,9 @@ impl PrettyPrint for Instruction { Self::Emit => const_text("emit"), Self::EmitImm(value) => inst_with_felt_imm("emit", value), Self::Trace(value) => inst_with_imm("trace", value), + + // Handled by the early return for !has_textual_representation() + Self::DebugVar(_) => unreachable!(), } } } diff --git a/crates/assembly-syntax/src/ast/visit.rs b/crates/assembly-syntax/src/ast/visit.rs index e8dc5a6341..dcdc154d79 100644 --- a/crates/assembly-syntax/src/ast/visit.rs +++ b/crates/assembly-syntax/src/ast/visit.rs @@ -500,8 +500,8 @@ where | Reversew | Reversedw | CSwap | CSwapW | CDrop | CDropW | PushFeltList(_) | Sdepth | Caller | Clk | MemLoad | MemLoadWBe | MemLoadWLe | MemStore | MemStoreWBe | MemStoreWLe | MemStream | AdvPipe | AdvLoadW | Hash | HMerge | HPerm | MTreeGet - | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec | DynCall | HornerBase - | HornerExt | CryptoStream | EvalCircuit | LogPrecompile | Emit => { + | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec | DynCall | DebugVar(_) + | HornerBase | HornerExt | CryptoStream | EvalCircuit | LogPrecompile | Emit => { ControlFlow::Continue(()) }, } @@ -1092,8 +1092,8 @@ where | Reversew | Reversedw | CSwap | CSwapW | CDrop | CDropW | PushFeltList(_) | Sdepth | Caller | Clk | MemLoad | MemLoadWBe | MemLoadWLe | MemStore | MemStoreWBe | MemStoreWLe | MemStream | AdvPipe | AdvLoadW | Hash | HMerge | HPerm | MTreeGet - | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec | DynCall | HornerBase - | HornerExt | EvalCircuit | CryptoStream | LogPrecompile | Emit => { + | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec | DynCall | DebugVar(_) + | HornerBase | HornerExt | EvalCircuit | CryptoStream | LogPrecompile | Emit => { ControlFlow::Continue(()) }, } diff --git a/crates/assembly/src/basic_block_builder.rs b/crates/assembly/src/basic_block_builder.rs index b41b554270..9efb0c9562 100644 --- a/crates/assembly/src/basic_block_builder.rs +++ b/crates/assembly/src/basic_block_builder.rs @@ -13,8 +13,8 @@ use miden_assembly_syntax::{ use miden_core::{ Felt, events::SystemEvent, - mast::{DecoratorId, MastNodeId}, - operations::{AssemblyOp, Decorator, DecoratorList, Operation}, + mast::{DebugVarId, DecoratorId, MastNodeId}, + operations::{AssemblyOp, DebugVarInfo, Decorator, DecoratorList, Operation}, }; use crate::{ProcedureContext, assembler::BodyWrapper, mast_forest_builder::MastForestBuilder}; @@ -60,6 +60,9 @@ pub struct BasicBlockBuilder<'a> { pending_asm_op: Option, /// Finalized AssemblyOps with their operation indices (op_idx, AssemblyOp). asm_ops: Vec<(usize, AssemblyOp)>, + /// Debug variables attached to operations in this block. + /// Each entry is (op_index, debug_var_id) similar to decorators. + debug_vars: Vec<(usize, DebugVarId)>, mast_forest_builder: &'a mut MastForestBuilder, } @@ -81,6 +84,7 @@ impl<'a> BasicBlockBuilder<'a> { epilogue: wrapper.epilogue, pending_asm_op: None, asm_ops: Vec::new(), + debug_vars: Vec::new(), mast_forest_builder, }, None => Self { @@ -89,6 +93,7 @@ impl<'a> BasicBlockBuilder<'a> { epilogue: Default::default(), pending_asm_op: None, asm_ops: Vec::new(), + debug_vars: Default::default(), mast_forest_builder, }, } @@ -195,6 +200,17 @@ impl BasicBlockBuilder<'_> { }, } } + + /// Adds a debug variable to the list of debug variables for this basic block. + /// + /// Debug variables are stored in dedicated CSR storage (not as decorators) and are + /// only accessed by the debugger. They track source-level variable locations at + /// specific points in program execution. + pub fn push_debug_var(&mut self, debug_var: DebugVarInfo) -> Result<(), Report> { + let debug_var_id = self.mast_forest_builder.add_debug_var(debug_var)?; + self.debug_vars.push((self.ops.len(), debug_var_id)); + Ok(()) + } } /// Basic Block Constructors @@ -213,11 +229,18 @@ impl BasicBlockBuilder<'_> { let ops = self.ops.drain(..).collect(); let decorators = self.decorators.drain(..).collect(); let asm_ops = core::mem::take(&mut self.asm_ops); + let debug_vars: Vec<(usize, DebugVarId)> = self.debug_vars.drain(..).collect(); let basic_block_node_id = self.mast_forest_builder .ensure_block(ops, decorators, asm_ops, vec![], vec![])?; + // Register debug variables for this node (stored separately from decorators) + if !debug_vars.is_empty() { + self.mast_forest_builder + .register_debug_vars_for_node(basic_block_node_id, debug_vars)?; + } + Ok(Some(basic_block_node_id)) } else { Ok(None) diff --git a/crates/assembly/src/instruction/mod.rs b/crates/assembly/src/instruction/mod.rs index 917e3577a4..26fdc74b33 100644 --- a/crates/assembly/src/instruction/mod.rs +++ b/crates/assembly/src/instruction/mod.rs @@ -588,6 +588,10 @@ impl Assembler { .push_decorator(Decorator::Debug(debug::compile_options(options, proc_ctx)?))?; }, + Instruction::DebugVar(debug_var_info) => { + block_builder.push_debug_var(debug_var_info.clone())?; + }, + // ----- emit instruction ------------------------------------------------------------- // emit: reads event ID from top of stack and execute the corresponding handler. Instruction::Emit => { diff --git a/crates/assembly/src/mast_forest_builder.rs b/crates/assembly/src/mast_forest_builder.rs index cb602f4685..ddfc01b577 100644 --- a/crates/assembly/src/mast_forest_builder.rs +++ b/crates/assembly/src/mast_forest_builder.rs @@ -539,6 +539,21 @@ impl MastForestBuilder { } } + /// Adds a debug variable to the forest, and returns the [`DebugVarId`] associated with it. + /// + /// Unlike decorators, debug variables are not deduplicated since each occurrence + /// represents a specific point in program execution where the variable's location + /// is being tracked. + pub fn add_debug_var( + &mut self, + debug_var: miden_core::operations::DebugVarInfo, + ) -> Result { + self.mast_forest + .add_debug_var(debug_var) + .into_diagnostic() + .wrap_err("assembler failed to add debug variable") + } + /// Adds a node to the forest, and returns the [`MastNodeId`] associated with it. /// /// Note that only one copy of nodes that have the same MAST root and decorators is added to the @@ -879,6 +894,22 @@ impl MastForestBuilder { self.pending_asm_op_mappings.push((node_id, vec![(0, asm_op_id)])); Ok(()) } + + /// Registers debug variables for a specific node. + /// + /// This associates already-added debug variables with specific operations within a node. + /// Debug variables are stored in dedicated CSR storage and are only accessed by the debugger. + pub fn register_debug_vars_for_node( + &mut self, + node_id: MastNodeId, + debug_vars: Vec<(usize, miden_core::mast::DebugVarId)>, + ) -> Result<(), Report> { + self.mast_forest + .debug_info_mut() + .register_op_indexed_debug_vars(node_id, debug_vars) + .into_diagnostic() + .wrap_err("failed to register debug variables for node") + } } impl Index for MastForestBuilder { diff --git a/crates/debug-types/src/lib.rs b/crates/debug-types/src/lib.rs index b56b4aa007..52c11e6660 100644 --- a/crates/debug-types/src/lib.rs +++ b/crates/debug-types/src/lib.rs @@ -13,6 +13,9 @@ mod span; use alloc::{string::String, sync::Arc}; +use miden_crypto::utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; #[cfg(feature = "serde")] @@ -161,6 +164,18 @@ impl<'a> From<&'a std::path::Path> for Uri { } } +impl Serializable for Uri { + fn write_into(&self, target: &mut W) { + self.as_str().write_into(target); + } +} + +impl Deserializable for Uri { + fn read_from(source: &mut R) -> Result { + String::read_from(source).map(Self::from) + } +} + impl core::str::FromStr for Uri { type Err = (); diff --git a/crates/debug-types/src/location.rs b/crates/debug-types/src/location.rs index a7e96fe342..af369f41e0 100644 --- a/crates/debug-types/src/location.rs +++ b/crates/debug-types/src/location.rs @@ -1,5 +1,8 @@ use core::{fmt, ops::Range}; +use miden_crypto::utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -84,3 +87,20 @@ impl fmt::Display for FileLineCol { write!(f, "[{}@{}:{}]", &self.uri, self.line, self.column) } } + +impl Serializable for FileLineCol { + fn write_into(&self, target: &mut W) { + self.uri.write_into(target); + self.line.write_into(target); + self.column.write_into(target); + } +} + +impl Deserializable for FileLineCol { + fn read_from(source: &mut R) -> Result { + let uri = Uri::read_from(source)?; + let line = LineNumber::read_from(source)?; + let column = ColumnNumber::read_from(source)?; + Ok(Self::new(uri, line, column)) + } +} diff --git a/crates/debug-types/src/source_file.rs b/crates/debug-types/src/source_file.rs index db3623af54..c8b36a1261 100644 --- a/crates/debug-types/src/source_file.rs +++ b/crates/debug-types/src/source_file.rs @@ -1194,6 +1194,42 @@ macro_rules! declare_dual_number_and_index_type { declare_dual_number_and_index_type!(Line, "line"); declare_dual_number_and_index_type!(Column, "column"); +// SERIALIZATION FOR LINE/COLUMN NUMBERS +// ================================================================================================ + +use miden_crypto::utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, +}; + +impl Serializable for LineNumber { + fn write_into(&self, target: &mut W) { + target.write_u32(self.to_u32()); + } +} + +impl Deserializable for LineNumber { + fn read_from(source: &mut R) -> Result { + let value = source.read_u32()?; + Self::new(value) + .ok_or_else(|| DeserializationError::InvalidValue("line number cannot be zero".into())) + } +} + +impl Serializable for ColumnNumber { + fn write_into(&self, target: &mut W) { + target.write_u32(self.to_u32()); + } +} + +impl Deserializable for ColumnNumber { + fn read_from(source: &mut R) -> Result { + let value = source.read_u32()?; + Self::new(value).ok_or_else(|| { + DeserializationError::InvalidValue("column number cannot be zero".into()) + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/mast-package/Cargo.toml b/crates/mast-package/Cargo.toml index bf96692728..fef21ed858 100644 --- a/crates/mast-package/Cargo.toml +++ b/crates/mast-package/Cargo.toml @@ -27,6 +27,7 @@ serde = ["dep:serde", "miden-assembly-syntax/serde", "miden-core/serde"] # Miden dependencies miden-assembly-syntax.workspace = true miden-core.workspace = true +miden-debug-types.workspace = true # External dependencies derive_more.workspace = true diff --git a/crates/mast-package/src/debug_info/mod.rs b/crates/mast-package/src/debug_info/mod.rs new file mode 100644 index 0000000000..ac66bf1cc2 --- /dev/null +++ b/crates/mast-package/src/debug_info/mod.rs @@ -0,0 +1,11 @@ +//! Debug information sections for MASP packages. +//! +//! This module provides types for encoding source-level debug information in the +//! `debug_types`, `debug_sources`, and `debug_functions` custom sections of a MASP package. +//! This information is used by debuggers to map between the Miden VM execution state +//! and the original source code. + +mod serialization; +mod types; + +pub use types::*; diff --git a/crates/mast-package/src/debug_info/serialization.rs b/crates/mast-package/src/debug_info/serialization.rs new file mode 100644 index 0000000000..fc28f01c82 --- /dev/null +++ b/crates/mast-package/src/debug_info/serialization.rs @@ -0,0 +1,645 @@ +//! Serialization and deserialization for the debug_info section. + +use alloc::sync::Arc; + +use miden_core::{ + Word, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; +use miden_debug_types::{ColumnNumber, LineNumber}; + +use super::{ + DEBUG_FUNCTIONS_VERSION, DEBUG_SOURCES_VERSION, DEBUG_TYPES_VERSION, DebugFieldInfo, + DebugFileInfo, DebugFunctionInfo, DebugFunctionsSection, DebugInlinedCallInfo, + DebugPrimitiveType, DebugSourcesSection, DebugTypeIdx, DebugTypeInfo, DebugTypesSection, + DebugVariableInfo, +}; + +// DEBUG TYPES SECTION SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugTypesSection { + fn write_into(&self, target: &mut W) { + target.write_u8(self.version); + + // Write string table + target.write_usize(self.strings.len()); + for s in &self.strings { + write_string(target, s); + } + + // Write type table + target.write_usize(self.types.len()); + for ty in &self.types { + ty.write_into(target); + } + } +} + +impl Deserializable for DebugTypesSection { + fn read_from(source: &mut R) -> Result { + let version = source.read_u8()?; + if version != DEBUG_TYPES_VERSION { + return Err(DeserializationError::InvalidValue(alloc::format!( + "unsupported debug_types version: {version}, expected {DEBUG_TYPES_VERSION}" + ))); + } + + let strings_len = source.read_usize()?; + let mut strings = alloc::vec::Vec::with_capacity(strings_len); + for _ in 0..strings_len { + strings.push(read_string(source)?); + } + + let types_len = source.read_usize()?; + let types = source.read_many_iter(types_len)?.collect::>()?; + + Ok(Self { version, strings, types }) + } +} + +// DEBUG SOURCES SECTION SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugSourcesSection { + fn write_into(&self, target: &mut W) { + target.write_u8(self.version); + + // Write string table + target.write_usize(self.strings.len()); + for s in &self.strings { + write_string(target, s); + } + + // Write file table + target.write_usize(self.files.len()); + for file in &self.files { + file.write_into(target); + } + } +} + +impl Deserializable for DebugSourcesSection { + fn read_from(source: &mut R) -> Result { + let version = source.read_u8()?; + if version != DEBUG_SOURCES_VERSION { + return Err(DeserializationError::InvalidValue(alloc::format!( + "unsupported debug_sources version: {version}, expected {DEBUG_SOURCES_VERSION}" + ))); + } + + let strings_len = source.read_usize()?; + let mut strings = alloc::vec::Vec::with_capacity(strings_len); + for _ in 0..strings_len { + strings.push(read_string(source)?); + } + + let files_len = source.read_usize()?; + let files = source.read_many_iter(files_len)?.collect::>()?; + + Ok(Self { version, strings, files }) + } +} + +// DEBUG FUNCTIONS SECTION SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugFunctionsSection { + fn write_into(&self, target: &mut W) { + target.write_u8(self.version); + + // Write string table + target.write_usize(self.strings.len()); + for s in &self.strings { + write_string(target, s); + } + + // Write function table + target.write_usize(self.functions.len()); + for func in &self.functions { + func.write_into(target); + } + } +} + +impl Deserializable for DebugFunctionsSection { + fn read_from(source: &mut R) -> Result { + let version = source.read_u8()?; + if version != DEBUG_FUNCTIONS_VERSION { + return Err(DeserializationError::InvalidValue(alloc::format!( + "unsupported debug_functions version: {version}, expected {DEBUG_FUNCTIONS_VERSION}" + ))); + } + + let strings_len = source.read_usize()?; + let mut strings = alloc::vec::Vec::with_capacity(strings_len); + for _ in 0..strings_len { + strings.push(read_string(source)?); + } + + let functions_len = source.read_usize()?; + let functions = source.read_many_iter(functions_len)?.collect::>()?; + + Ok(Self { version, strings, functions }) + } +} + +// DEBUG TYPE INFO SERIALIZATION +// ================================================================================================ + +// Type tags for serialization +const TYPE_TAG_PRIMITIVE: u8 = 0; +const TYPE_TAG_POINTER: u8 = 1; +const TYPE_TAG_ARRAY: u8 = 2; +const TYPE_TAG_STRUCT: u8 = 3; +const TYPE_TAG_FUNCTION: u8 = 4; +const TYPE_TAG_UNKNOWN: u8 = 5; + +impl Serializable for DebugTypeInfo { + fn write_into(&self, target: &mut W) { + match self { + Self::Primitive(prim) => { + target.write_u8(TYPE_TAG_PRIMITIVE); + target.write_u8(*prim as u8); + }, + Self::Pointer { pointee_type_idx } => { + target.write_u8(TYPE_TAG_POINTER); + target.write_u32(pointee_type_idx.as_u32()); + }, + Self::Array { element_type_idx, count } => { + target.write_u8(TYPE_TAG_ARRAY); + target.write_u32(element_type_idx.as_u32()); + target.write_bool(count.is_some()); + if let Some(count) = count { + target.write_u32(*count); + } + }, + Self::Struct { name_idx, size, fields } => { + target.write_u8(TYPE_TAG_STRUCT); + target.write_u32(*name_idx); + target.write_u32(*size); + target.write_usize(fields.len()); + for field in fields { + field.write_into(target); + } + }, + Self::Function { return_type_idx, param_type_indices } => { + target.write_u8(TYPE_TAG_FUNCTION); + target.write_bool(return_type_idx.is_some()); + if let Some(idx) = return_type_idx { + target.write_u32(idx.as_u32()); + } + target.write_usize(param_type_indices.len()); + for idx in param_type_indices { + target.write_u32(idx.as_u32()); + } + }, + Self::Unknown => { + target.write_u8(TYPE_TAG_UNKNOWN); + }, + } + } +} + +impl Deserializable for DebugTypeInfo { + fn read_from(source: &mut R) -> Result { + let tag = source.read_u8()?; + match tag { + TYPE_TAG_PRIMITIVE => { + let prim_tag = source.read_u8()?; + let prim = DebugPrimitiveType::from_discriminant(prim_tag).ok_or_else(|| { + DeserializationError::InvalidValue(alloc::format!( + "invalid primitive type tag: {prim_tag}" + )) + })?; + Ok(Self::Primitive(prim)) + }, + TYPE_TAG_POINTER => { + let pointee_type_idx = DebugTypeIdx::from(source.read_u32()?); + Ok(Self::Pointer { pointee_type_idx }) + }, + TYPE_TAG_ARRAY => { + let element_type_idx = DebugTypeIdx::from(source.read_u32()?); + let has_count = source.read_bool()?; + let count = if has_count { Some(source.read_u32()?) } else { None }; + Ok(Self::Array { element_type_idx, count }) + }, + TYPE_TAG_STRUCT => { + let name_idx = source.read_u32()?; + let size = source.read_u32()?; + let fields_len = source.read_usize()?; + let fields = source.read_many_iter(fields_len)?.collect::>()?; + Ok(Self::Struct { name_idx, size, fields }) + }, + TYPE_TAG_FUNCTION => { + let has_return = source.read_bool()?; + let return_type_idx = if has_return { + Some(DebugTypeIdx::from(source.read_u32()?)) + } else { + None + }; + let params_len = source.read_usize()?; + let mut param_type_indices = alloc::vec::Vec::with_capacity(params_len); + for _ in 0..params_len { + param_type_indices.push(DebugTypeIdx::from(source.read_u32()?)); + } + Ok(Self::Function { return_type_idx, param_type_indices }) + }, + TYPE_TAG_UNKNOWN => Ok(Self::Unknown), + _ => Err(DeserializationError::InvalidValue(alloc::format!("invalid type tag: {tag}"))), + } + } +} + +// DEBUG FIELD INFO SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugFieldInfo { + fn write_into(&self, target: &mut W) { + target.write_u32(self.name_idx); + target.write_u32(self.type_idx.as_u32()); + target.write_u32(self.offset); + } +} + +impl Deserializable for DebugFieldInfo { + fn read_from(source: &mut R) -> Result { + let name_idx = source.read_u32()?; + let type_idx = DebugTypeIdx::from(source.read_u32()?); + let offset = source.read_u32()?; + Ok(Self { name_idx, type_idx, offset }) + } +} + +// DEBUG FILE INFO SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugFileInfo { + fn write_into(&self, target: &mut W) { + target.write_u32(self.path_idx); + + target.write_bool(self.checksum.is_some()); + if let Some(checksum) = &self.checksum { + target.write_bytes(checksum.as_ref()); + } + } +} + +impl Deserializable for DebugFileInfo { + fn read_from(source: &mut R) -> Result { + let path_idx = source.read_u32()?; + + let has_checksum = source.read_bool()?; + let checksum = if has_checksum { + let bytes = source.read_slice(32)?; + let mut arr = [0u8; 32]; + arr.copy_from_slice(bytes); + Some(alloc::boxed::Box::new(arr)) + } else { + None + }; + + Ok(Self { path_idx, checksum }) + } +} + +// DEBUG FUNCTION INFO SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugFunctionInfo { + fn write_into(&self, target: &mut W) { + target.write_u32(self.name_idx); + + target.write_bool(self.linkage_name_idx.is_some()); + if let Some(idx) = self.linkage_name_idx { + target.write_u32(idx); + } + + target.write_u32(self.file_idx); + target.write_u32(self.line.to_u32()); + target.write_u32(self.column.to_u32()); + + target.write_bool(self.type_idx.is_some()); + if let Some(idx) = self.type_idx { + target.write_u32(idx.as_u32()); + } + + target.write_bool(self.mast_root.is_some()); + if let Some(root) = &self.mast_root { + root.write_into(target); + } + + // Write variables + target.write_usize(self.variables.len()); + for var in &self.variables { + var.write_into(target); + } + + // Write inlined calls + target.write_usize(self.inlined_calls.len()); + for call in &self.inlined_calls { + call.write_into(target); + } + } +} + +impl Deserializable for DebugFunctionInfo { + fn read_from(source: &mut R) -> Result { + let name_idx = source.read_u32()?; + + let has_linkage_name = source.read_bool()?; + let linkage_name_idx = if has_linkage_name { + Some(source.read_u32()?) + } else { + None + }; + + let file_idx = source.read_u32()?; + let line_raw = source.read_u32()?; + let column_raw = source.read_u32()?; + let line = LineNumber::new(line_raw).unwrap_or_default(); + let column = ColumnNumber::new(column_raw).unwrap_or_default(); + + let has_type = source.read_bool()?; + let type_idx = if has_type { + Some(DebugTypeIdx::from(source.read_u32()?)) + } else { + None + }; + + let has_mast_root = source.read_bool()?; + let mast_root = if has_mast_root { + Some(Word::read_from(source)?) + } else { + None + }; + + // Read variables + let vars_len = source.read_usize()?; + let variables = source.read_many_iter(vars_len)?.collect::>()?; + + // Read inlined calls + let calls_len = source.read_usize()?; + let inlined_calls = source.read_many_iter(calls_len)?.collect::>()?; + + Ok(Self { + name_idx, + linkage_name_idx, + file_idx, + line, + column, + type_idx, + mast_root, + variables, + inlined_calls, + }) + } +} + +// DEBUG VARIABLE INFO SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugVariableInfo { + fn write_into(&self, target: &mut W) { + target.write_u32(self.name_idx); + target.write_u32(self.type_idx.as_u32()); + target.write_u32(self.arg_index); + target.write_u32(self.line.to_u32()); + target.write_u32(self.column.to_u32()); + target.write_u32(self.scope_depth); + } +} + +impl Deserializable for DebugVariableInfo { + fn read_from(source: &mut R) -> Result { + let name_idx = source.read_u32()?; + let type_idx = DebugTypeIdx::from(source.read_u32()?); + let arg_index = source.read_u32()?; + let line_raw = source.read_u32()?; + let column_raw = source.read_u32()?; + let line = LineNumber::new(line_raw).unwrap_or_default(); + let column = ColumnNumber::new(column_raw).unwrap_or_default(); + let scope_depth = source.read_u32()?; + Ok(Self { + name_idx, + type_idx, + arg_index, + line, + column, + scope_depth, + }) + } +} + +// DEBUG INLINED CALL INFO SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugInlinedCallInfo { + fn write_into(&self, target: &mut W) { + target.write_u32(self.callee_idx); + target.write_u32(self.file_idx); + target.write_u32(self.line.to_u32()); + target.write_u32(self.column.to_u32()); + } +} + +impl Deserializable for DebugInlinedCallInfo { + fn read_from(source: &mut R) -> Result { + let callee_idx = source.read_u32()?; + let file_idx = source.read_u32()?; + let line_raw = source.read_u32()?; + let column_raw = source.read_u32()?; + let line = LineNumber::new(line_raw).unwrap_or_default(); + let column = ColumnNumber::new(column_raw).unwrap_or_default(); + Ok(Self { callee_idx, file_idx, line, column }) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +fn write_string(target: &mut W, s: &str) { + let bytes = s.as_bytes(); + target.write_usize(bytes.len()); + target.write_bytes(bytes); +} + +fn read_string(source: &mut R) -> Result, DeserializationError> { + let len = source.read_usize()?; + let bytes = source.read_slice(len)?; + let s = core::str::from_utf8(bytes).map_err(|err| { + DeserializationError::InvalidValue(alloc::format!("invalid utf-8 in string: {err}")) + })?; + Ok(Arc::from(s)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn roundtrip(value: &T) { + let mut bytes = alloc::vec::Vec::new(); + value.write_into(&mut bytes); + let result = T::read_from(&mut miden_core::serde::SliceReader::new(&bytes)).unwrap(); + assert_eq!(value, &result); + } + + #[test] + fn test_debug_types_section_roundtrip() { + let mut section = DebugTypesSection::new(); + + // Add primitive types + let i32_type_idx = section.add_type(DebugTypeInfo::Primitive(DebugPrimitiveType::I32)); + let felt_type_idx = section.add_type(DebugTypeInfo::Primitive(DebugPrimitiveType::Felt)); + + // Add a pointer type + section.add_type(DebugTypeInfo::Pointer { pointee_type_idx: i32_type_idx }); + + // Add an array type + section.add_type(DebugTypeInfo::Array { + element_type_idx: felt_type_idx, + count: Some(4), + }); + + // Add a struct type + let x_idx = section.add_string(Arc::from("x")); + let y_idx = section.add_string(Arc::from("y")); + let point_idx = section.add_string(Arc::from("Point")); + section.add_type(DebugTypeInfo::Struct { + name_idx: point_idx, + size: 16, + fields: alloc::vec![ + DebugFieldInfo { + name_idx: x_idx, + type_idx: felt_type_idx, + offset: 0, + }, + DebugFieldInfo { + name_idx: y_idx, + type_idx: felt_type_idx, + offset: 8, + }, + ], + }); + + roundtrip(§ion); + } + + #[test] + fn test_debug_sources_section_roundtrip() { + let mut section = DebugSourcesSection::new(); + + let path_idx = section.add_string(Arc::from("test.rs")); + section.add_file(DebugFileInfo::new(path_idx)); + + let path2_idx = section.add_string(Arc::from("main.rs")); + section.add_file(DebugFileInfo::new(path2_idx).with_checksum([42u8; 32])); + + roundtrip(§ion); + } + + #[test] + fn test_debug_functions_section_roundtrip() { + let mut section = DebugFunctionsSection::new(); + + let name_idx = section.add_string(Arc::from("test_function")); + + let line = LineNumber::new(10).unwrap(); + let column = ColumnNumber::new(1).unwrap(); + let mut func = DebugFunctionInfo::new(name_idx, 0, line, column); + let var_name_idx = section.add_string(Arc::from("x")); + let var_line = LineNumber::new(10).unwrap(); + let var_column = ColumnNumber::new(5).unwrap(); + func.add_variable( + DebugVariableInfo::new(var_name_idx, DebugTypeIdx::from(0), var_line, var_column) + .with_arg_index(1), + ); + section.add_function(func); + + roundtrip(§ion); + } + + #[test] + fn test_empty_sections_roundtrip() { + roundtrip(&DebugTypesSection::new()); + roundtrip(&DebugSourcesSection::new()); + roundtrip(&DebugFunctionsSection::new()); + } + + #[test] + fn test_all_primitive_types_roundtrip() { + let mut section = DebugTypesSection::new(); + + for prim in [ + DebugPrimitiveType::Void, + DebugPrimitiveType::Bool, + DebugPrimitiveType::I8, + DebugPrimitiveType::U8, + DebugPrimitiveType::I16, + DebugPrimitiveType::U16, + DebugPrimitiveType::I32, + DebugPrimitiveType::U32, + DebugPrimitiveType::I64, + DebugPrimitiveType::U64, + DebugPrimitiveType::I128, + DebugPrimitiveType::U128, + DebugPrimitiveType::F32, + DebugPrimitiveType::F64, + DebugPrimitiveType::Felt, + DebugPrimitiveType::Word, + ] { + section.add_type(DebugTypeInfo::Primitive(prim)); + } + + roundtrip(§ion); + } + + #[test] + fn test_function_type_roundtrip() { + let ty = DebugTypeInfo::Function { + return_type_idx: Some(DebugTypeIdx::from(0)), + param_type_indices: alloc::vec![ + DebugTypeIdx::from(1), + DebugTypeIdx::from(2), + DebugTypeIdx::from(3) + ], + }; + roundtrip(&ty); + + let void_fn = DebugTypeInfo::Function { + return_type_idx: None, + param_type_indices: alloc::vec![], + }; + roundtrip(&void_fn); + } + + #[test] + fn test_file_info_with_checksum_roundtrip() { + let file = DebugFileInfo::new(0).with_checksum([42u8; 32]); + roundtrip(&file); + } + + #[test] + fn test_function_with_mast_root_roundtrip() { + let line1 = LineNumber::new(1).unwrap(); + let col1 = ColumnNumber::new(1).unwrap(); + let mut func = DebugFunctionInfo::new(0, 0, line1, col1) + .with_linkage_name(1) + .with_type(DebugTypeIdx::from(2)) + .with_mast_root(Word::default()); + + let var_line = LineNumber::new(5).unwrap(); + let var_col = ColumnNumber::new(10).unwrap(); + func.add_variable( + DebugVariableInfo::new(0, DebugTypeIdx::from(0), var_line, var_col) + .with_arg_index(1) + .with_scope_depth(2), + ); + + let call_line = LineNumber::new(20).unwrap(); + let call_col = ColumnNumber::new(5).unwrap(); + func.add_inlined_call(DebugInlinedCallInfo::new(0, 0, call_line, call_col)); + + roundtrip(&func); + } +} diff --git a/crates/mast-package/src/debug_info/types.rs b/crates/mast-package/src/debug_info/types.rs new file mode 100644 index 0000000000..be42fbf956 --- /dev/null +++ b/crates/mast-package/src/debug_info/types.rs @@ -0,0 +1,697 @@ +//! Type definitions for the debug_info section. +//! +//! This module provides types for storing debug information in MASP packages, +//! enabling debuggers to provide meaningful source-level debugging experiences. +//! +//! # Overview +//! +//! The debug info section contains: +//! - **Type definitions**: Describe the types of variables (primitives, structs, arrays, etc.) +//! - **Source file paths**: Deduplicated file paths for source locations +//! - **Function metadata**: Function signatures, local variables, and inline call sites +//! +//! # Usage +//! +//! Debuggers can use this information along with `DebugVar` decorators in the MAST +//! to provide source-level variable inspection, stepping, and call stack visualization. + +use alloc::{boxed::Box, sync::Arc, vec::Vec}; + +use miden_core::Word; +use miden_debug_types::{ColumnNumber, LineNumber}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +// DEBUG TYPE INDEX +// ================================================================================================ + +/// A strongly-typed index into the type table of a [`DebugTypesSection`]. +/// +/// This prevents accidental misuse of raw `u32` indices (e.g., using a string index +/// where a type index is expected). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct DebugTypeIdx(u32); + +impl DebugTypeIdx { + /// Returns the inner value as a `u32`. + pub fn as_u32(self) -> u32 { + self.0 + } +} + +impl From for DebugTypeIdx { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(value: DebugTypeIdx) -> Self { + value.0 + } +} + +// DEBUG TYPES SECTION +// ================================================================================================ + +/// The version of the debug_types section format. +pub const DEBUG_TYPES_VERSION: u8 = 1; + +/// Debug types section containing type definitions for a MASP package. +/// +/// This section stores type information (primitives, structs, arrays, pointers, +/// function types) that enables debuggers to properly display values. +/// +/// String indices in sub-types (e.g., `name_idx` in `DebugFieldInfo`) are relative +/// to this section's own string table. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugTypesSection { + /// Version of the debug types format + pub version: u8, + /// String table containing type names, field names + pub strings: Vec>, + /// Type table containing all type definitions + pub types: Vec, +} + +impl DebugTypesSection { + /// Creates a new empty debug types section. + pub fn new() -> Self { + Self { + version: DEBUG_TYPES_VERSION, + strings: Vec::new(), + types: Vec::new(), + } + } + + /// Adds a string to the string table and returns its index. + pub fn add_string(&mut self, s: Arc) -> u32 { + if let Some(idx) = self.strings.iter().position(|existing| **existing == *s) { + return idx as u32; + } + let idx = self.strings.len() as u32; + self.strings.push(s); + idx + } + + /// Gets a string by index. + pub fn get_string(&self, idx: u32) -> Option> { + self.strings.get(idx as usize).cloned() + } + + /// Adds a type to the type table and returns its index. + pub fn add_type(&mut self, ty: DebugTypeInfo) -> DebugTypeIdx { + let idx = DebugTypeIdx(self.types.len() as u32); + self.types.push(ty); + idx + } + + /// Gets a type by index. + pub fn get_type(&self, idx: DebugTypeIdx) -> Option<&DebugTypeInfo> { + self.types.get(idx.0 as usize) + } + + /// Returns true if the section is empty (no types). + pub fn is_empty(&self) -> bool { + self.types.is_empty() + } +} + +// DEBUG SOURCES SECTION +// ================================================================================================ + +/// The version of the debug_sources section format. +pub const DEBUG_SOURCES_VERSION: u8 = 1; + +/// Debug sources section containing source file paths and checksums. +/// +/// This section stores deduplicated source file information that is referenced +/// by the debug functions section. +/// +/// String indices in sub-types (e.g., `path_idx` in `DebugFileInfo`) are relative +/// to this section's own string table. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugSourcesSection { + /// Version of the debug sources format + pub version: u8, + /// String table containing file paths + pub strings: Vec>, + /// Source file table + pub files: Vec, +} + +impl DebugSourcesSection { + /// Creates a new empty debug sources section. + pub fn new() -> Self { + Self { + version: DEBUG_SOURCES_VERSION, + strings: Vec::new(), + files: Vec::new(), + } + } + + /// Adds a string to the string table and returns its index. + pub fn add_string(&mut self, s: Arc) -> u32 { + if let Some(idx) = self.strings.iter().position(|existing| **existing == *s) { + return idx as u32; + } + let idx = self.strings.len() as u32; + self.strings.push(s); + idx + } + + /// Gets a string by index. + pub fn get_string(&self, idx: u32) -> Option> { + self.strings.get(idx as usize).cloned() + } + + /// Adds a file to the file table and returns its index. + pub fn add_file(&mut self, file: DebugFileInfo) -> u32 { + if let Some(idx) = self.files.iter().position(|existing| existing.path_idx == file.path_idx) + { + return idx as u32; + } + let idx = self.files.len() as u32; + self.files.push(file); + idx + } + + /// Gets a file by index. + pub fn get_file(&self, idx: u32) -> Option<&DebugFileInfo> { + self.files.get(idx as usize) + } + + /// Returns true if the section is empty (no files). + pub fn is_empty(&self) -> bool { + self.files.is_empty() + } +} + +// DEBUG FUNCTIONS SECTION +// ================================================================================================ + +/// The version of the debug_functions section format. +pub const DEBUG_FUNCTIONS_VERSION: u8 = 1; + +/// Debug functions section containing function metadata, variables, and inlined calls. +/// +/// This section stores function debug information including local variables and +/// inlined call sites. +/// +/// String indices in sub-types (e.g., `name_idx` in `DebugFunctionInfo`) are relative +/// to this section's own string table. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugFunctionsSection { + /// Version of the debug functions format + pub version: u8, + /// String table containing function names, variable names, linkage names + pub strings: Vec>, + /// Function debug information + pub functions: Vec, +} + +impl DebugFunctionsSection { + /// Creates a new empty debug functions section. + pub fn new() -> Self { + Self { + version: DEBUG_FUNCTIONS_VERSION, + strings: Vec::new(), + functions: Vec::new(), + } + } + + /// Adds a string to the string table and returns its index. + pub fn add_string(&mut self, s: Arc) -> u32 { + if let Some(idx) = self.strings.iter().position(|existing| **existing == *s) { + return idx as u32; + } + let idx = self.strings.len() as u32; + self.strings.push(s); + idx + } + + /// Gets a string by index. + pub fn get_string(&self, idx: u32) -> Option> { + self.strings.get(idx as usize).cloned() + } + + /// Adds a function to the function table. + pub fn add_function(&mut self, func: DebugFunctionInfo) { + self.functions.push(func); + } + + /// Returns true if the section is empty (no functions). + pub fn is_empty(&self) -> bool { + self.functions.is_empty() + } +} + +// DEBUG TYPE INFO +// ================================================================================================ + +/// Type information for debug purposes. +/// +/// This encodes the type of a variable or expression, enabling debuggers to properly +/// display values on the stack or in memory. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum DebugTypeInfo { + /// A primitive type (e.g., i32, i64, felt, etc.) + Primitive(DebugPrimitiveType), + /// A pointer type pointing to another type + Pointer { + /// The type being pointed to (index into type table) + pointee_type_idx: DebugTypeIdx, + }, + /// An array type + Array { + /// The element type (index into type table) + element_type_idx: DebugTypeIdx, + /// Number of elements (None for dynamically-sized arrays) + count: Option, + }, + /// A struct type + Struct { + /// Name of the struct (index into string table) + name_idx: u32, + /// Size in bytes + size: u32, + /// Fields of the struct + fields: Vec, + }, + /// A function type + Function { + /// Return type (index into type table, None for void) + return_type_idx: Option, + /// Parameter types (indices into type table) + param_type_indices: Vec, + }, + /// An unknown or opaque type + Unknown, +} + +/// Primitive type variants supported by the debug info format. +/// +/// New variants must be added at the end to maintain backwards compatibility +/// with previously serialized debug info. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[repr(u8)] +pub enum DebugPrimitiveType { + /// Void type (0 bytes) + Void = 0, + /// Boolean (1 byte) + Bool, + /// Signed 8-bit integer + I8, + /// Unsigned 8-bit integer + U8, + /// Signed 16-bit integer + I16, + /// Unsigned 16-bit integer + U16, + /// Signed 32-bit integer + I32, + /// Unsigned 32-bit integer + U32, + /// Signed 64-bit integer + I64, + /// Unsigned 64-bit integer + U64, + /// Signed 128-bit integer + I128, + /// Unsigned 128-bit integer + U128, + /// 32-bit floating point + F32, + /// 64-bit floating point + F64, + /// Miden field element (64-bit, but with field semantics) + Felt, + /// Miden word (4 field elements) + Word, +} + +impl DebugPrimitiveType { + /// Returns the size of this primitive type in bytes. + pub const fn size_in_bytes(self) -> u32 { + match self { + Self::Void => 0, + Self::Bool | Self::I8 | Self::U8 => 1, + Self::I16 | Self::U16 => 2, + Self::I32 | Self::U32 | Self::F32 => 4, + Self::I64 | Self::U64 | Self::F64 | Self::Felt => 8, + Self::I128 | Self::U128 => 16, + Self::Word => 32, + } + } + + /// Returns the size of this primitive type in Miden stack elements (felts). + pub const fn size_in_felts(self) -> u32 { + match self { + Self::Void => 0, + Self::Bool + | Self::I8 + | Self::U8 + | Self::I16 + | Self::U16 + | Self::I32 + | Self::U32 + | Self::Felt => 1, + Self::I64 | Self::U64 | Self::F32 | Self::F64 => 2, + Self::I128 | Self::U128 | Self::Word => 4, + } + } + + /// Converts a discriminant byte to a primitive type. + pub fn from_discriminant(discriminant: u8) -> Option { + match discriminant { + 0 => Some(Self::Void), + 1 => Some(Self::Bool), + 2 => Some(Self::I8), + 3 => Some(Self::U8), + 4 => Some(Self::I16), + 5 => Some(Self::U16), + 6 => Some(Self::I32), + 7 => Some(Self::U32), + 8 => Some(Self::I64), + 9 => Some(Self::U64), + 10 => Some(Self::I128), + 11 => Some(Self::U128), + 12 => Some(Self::F32), + 13 => Some(Self::F64), + 14 => Some(Self::Felt), + 15 => Some(Self::Word), + _ => None, + } + } +} + +/// Field information within a struct type. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugFieldInfo { + /// Name of the field (index into string table) + pub name_idx: u32, + /// Type of the field (index into type table) + pub type_idx: DebugTypeIdx, + /// Byte offset within the struct + pub offset: u32, +} + +// DEBUG FILE INFO +// ================================================================================================ + +/// Source file information. +/// +/// Contains the path and optional metadata for a source file referenced by debug info. +/// +/// TODO: Consider adding `directory_idx: Option` to reduce serialized debug info size. +/// When `directory_idx` is set, `path_idx` would be a relative path; otherwise `path_idx` +/// is expected to be absolute. This would allow sharing common directory prefixes across +/// multiple files. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugFileInfo { + /// Full path to the source file (index into string table). + pub path_idx: u32, + /// Optional checksum of the file content for verification. + /// + /// When present, debuggers can use this to verify that the source file on disk + /// matches the version used during compilation. + /// + /// Boxed to reduce the size of `DebugFileInfo` when checksums are not used. + pub checksum: Option>, +} + +impl DebugFileInfo { + /// Creates a new file info with a path. + pub fn new(path_idx: u32) -> Self { + Self { path_idx, checksum: None } + } + + /// Sets the checksum. + pub fn with_checksum(mut self, checksum: [u8; 32]) -> Self { + self.checksum = Some(Box::new(checksum)); + self + } +} + +// DEBUG FUNCTION INFO +// ================================================================================================ + +/// Debug information for a function. +/// +/// Links source-level function information to the compiled MAST representation, +/// including local variables and inlined call sites. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugFunctionInfo { + /// Name of the function (index into string table) + pub name_idx: u32, + /// Linkage name / mangled name (index into string table, optional) + pub linkage_name_idx: Option, + /// File containing this function (index into file table) + pub file_idx: u32, + /// Line number where the function starts (1-indexed) + pub line: LineNumber, + /// Column number where the function starts (1-indexed) + pub column: ColumnNumber, + /// Type of this function (index into type table, optional) + pub type_idx: Option, + /// MAST root digest of this function (if known). + /// This links the debug info to the compiled code. + pub mast_root: Option, + /// Local variables declared in this function + pub variables: Vec, + /// Inline call sites within this function + pub inlined_calls: Vec, +} + +impl DebugFunctionInfo { + /// Creates a new function info. + pub fn new(name_idx: u32, file_idx: u32, line: LineNumber, column: ColumnNumber) -> Self { + Self { + name_idx, + linkage_name_idx: None, + file_idx, + line, + column, + type_idx: None, + mast_root: None, + variables: Vec::new(), + inlined_calls: Vec::new(), + } + } + + /// Sets the linkage name. + pub fn with_linkage_name(mut self, linkage_name_idx: u32) -> Self { + self.linkage_name_idx = Some(linkage_name_idx); + self + } + + /// Sets the type index. + pub fn with_type(mut self, type_idx: DebugTypeIdx) -> Self { + self.type_idx = Some(type_idx); + self + } + + /// Sets the MAST root digest. + pub fn with_mast_root(mut self, mast_root: Word) -> Self { + self.mast_root = Some(mast_root); + self + } + + /// Adds a variable to this function. + pub fn add_variable(&mut self, variable: DebugVariableInfo) { + self.variables.push(variable); + } + + /// Adds an inlined call site. + pub fn add_inlined_call(&mut self, call: DebugInlinedCallInfo) { + self.inlined_calls.push(call); + } +} + +// DEBUG VARIABLE INFO +// ================================================================================================ + +/// Debug information for a local variable or parameter. +/// +/// This struct captures the source-level information about a variable, enabling +/// debuggers to display variable names, types, and locations to users. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugVariableInfo { + /// Name of the variable (index into string table) + pub name_idx: u32, + /// Type of the variable (index into type table) + pub type_idx: DebugTypeIdx, + /// If this is a parameter, its 1-based index (0 = not a parameter) + pub arg_index: u32, + /// Line where the variable is declared (1-indexed) + pub line: LineNumber, + /// Column where the variable is declared (1-indexed) + pub column: ColumnNumber, + /// Scope depth indicating the lexical nesting level of this variable. + /// + /// - `0` = function-level scope (parameters and variables at function body level) + /// - `1` = first nested block (e.g., inside an `if` or `loop`) + /// - `2` = second nested block, and so on + /// + /// This is used by debuggers to: + /// 1. Determine variable visibility at a given execution point + /// 2. Handle variable shadowing (a variable with the same name but higher depth shadows one + /// with lower depth when both are in scope) + /// 3. Display variables grouped by their scope level + /// + /// For example, in: + /// ```text + /// fn foo(x: i32) { // x has scope_depth 0 + /// let y = 1; // y has scope_depth 0 + /// if condition { + /// let z = 2; // z has scope_depth 1 + /// let x = 3; // this x has scope_depth 1, shadows parameter x + /// } + /// } + /// ``` + pub scope_depth: u32, +} + +impl DebugVariableInfo { + /// Creates a new variable info. + pub fn new( + name_idx: u32, + type_idx: DebugTypeIdx, + line: LineNumber, + column: ColumnNumber, + ) -> Self { + Self { + name_idx, + type_idx, + arg_index: 0, + line, + column, + scope_depth: 0, + } + } + + /// Sets this variable as a parameter with the given 1-based index. + pub fn with_arg_index(mut self, arg_index: u32) -> Self { + self.arg_index = arg_index; + self + } + + /// Sets the scope depth. + pub fn with_scope_depth(mut self, scope_depth: u32) -> Self { + self.scope_depth = scope_depth; + self + } + + /// Returns true if this variable is a function parameter. + pub fn is_parameter(&self) -> bool { + self.arg_index > 0 + } +} + +// DEBUG INLINED CALL INFO +// ================================================================================================ + +/// Debug information for an inlined function call. +/// +/// Captures the call site location when a function has been inlined, +/// enabling debuggers to show the original call stack. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugInlinedCallInfo { + /// The function that was inlined (index into function table) + pub callee_idx: u32, + /// Call site file (index into file table) + pub file_idx: u32, + /// Call site line number (1-indexed) + pub line: LineNumber, + /// Call site column number (1-indexed) + pub column: ColumnNumber, +} + +impl DebugInlinedCallInfo { + /// Creates a new inlined call info. + pub fn new(callee_idx: u32, file_idx: u32, line: LineNumber, column: ColumnNumber) -> Self { + Self { callee_idx, file_idx, line, column } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_debug_types_section_string_dedup() { + let mut section = DebugTypesSection::new(); + + let idx1 = section.add_string(Arc::from("test.rs")); + let idx2 = section.add_string(Arc::from("main.rs")); + let idx3 = section.add_string(Arc::from("test.rs")); // Duplicate + + assert_eq!(idx1, 0); + assert_eq!(idx2, 1); + assert_eq!(idx3, 0); // Should return same index + assert_eq!(section.strings.len(), 2); + } + + #[test] + fn test_debug_sources_section_string_dedup() { + let mut section = DebugSourcesSection::new(); + + let idx1 = section.add_string(Arc::from("test.rs")); + let idx2 = section.add_string(Arc::from("main.rs")); + let idx3 = section.add_string(Arc::from("test.rs")); // Duplicate + + assert_eq!(idx1, 0); + assert_eq!(idx2, 1); + assert_eq!(idx3, 0); // Should return same index + assert_eq!(section.strings.len(), 2); + } + + #[test] + fn test_debug_functions_section_string_dedup() { + let mut section = DebugFunctionsSection::new(); + + let idx1 = section.add_string(Arc::from("foo")); + let idx2 = section.add_string(Arc::from("bar")); + let idx3 = section.add_string(Arc::from("foo")); // Duplicate + + assert_eq!(idx1, 0); + assert_eq!(idx2, 1); + assert_eq!(idx3, 0); // Should return same index + assert_eq!(section.strings.len(), 2); + } + + #[test] + fn test_primitive_type_sizes() { + assert_eq!(DebugPrimitiveType::Void.size_in_bytes(), 0); + assert_eq!(DebugPrimitiveType::I32.size_in_bytes(), 4); + assert_eq!(DebugPrimitiveType::I64.size_in_bytes(), 8); + assert_eq!(DebugPrimitiveType::Felt.size_in_bytes(), 8); + assert_eq!(DebugPrimitiveType::Word.size_in_bytes(), 32); + + assert_eq!(DebugPrimitiveType::Void.size_in_felts(), 0); + assert_eq!(DebugPrimitiveType::I32.size_in_felts(), 1); + assert_eq!(DebugPrimitiveType::I64.size_in_felts(), 2); + assert_eq!(DebugPrimitiveType::Word.size_in_felts(), 4); + } + + #[test] + fn test_primitive_type_roundtrip() { + for discriminant in 0..=15 { + let ty = DebugPrimitiveType::from_discriminant(discriminant).unwrap(); + assert_eq!(ty as u8, discriminant); + } + assert!(DebugPrimitiveType::from_discriminant(16).is_none()); + } +} diff --git a/crates/mast-package/src/lib.rs b/crates/mast-package/src/lib.rs index 4a689e2d1b..98119892bb 100644 --- a/crates/mast-package/src/lib.rs +++ b/crates/mast-package/src/lib.rs @@ -8,6 +8,7 @@ extern crate alloc; extern crate std; mod artifact; +pub mod debug_info; mod dependency; mod package; diff --git a/crates/mast-package/src/package/section.rs b/crates/mast-package/src/package/section.rs index b8350190a2..dfa4498c81 100644 --- a/crates/mast-package/src/package/section.rs +++ b/crates/mast-package/src/package/section.rs @@ -20,8 +20,13 @@ use serde::{Deserialize, Serialize}; pub struct SectionId(Cow<'static, str>); impl SectionId { - /// The section containing debug information (source locations, spans) - pub const DEBUG_INFO: Self = Self(Cow::Borrowed("debug_info")); + /// The section containing debug type definitions (primitives, structs, arrays, pointers, + /// function types) + pub const DEBUG_TYPES: Self = Self(Cow::Borrowed("debug_types")); + /// The section containing debug source file paths and checksums + pub const DEBUG_SOURCES: Self = Self(Cow::Borrowed("debug_sources")); + /// The section containing debug function metadata, variables, and inlined calls + pub const DEBUG_FUNCTIONS: Self = Self(Cow::Borrowed("debug_functions")); /// This section provides the encoded metadata for a compiled account component /// /// Currently, this corresponds to the serialized representation of @@ -68,7 +73,9 @@ impl FromStr for SectionId { type Err = InvalidSectionIdError; fn from_str(s: &str) -> Result { match s { - "debug_info" => Ok(Self::DEBUG_INFO), + "debug_types" => Ok(Self::DEBUG_TYPES), + "debug_sources" => Ok(Self::DEBUG_SOURCES), + "debug_functions" => Ok(Self::DEBUG_FUNCTIONS), "account_component_metadata" => Ok(Self::ACCOUNT_COMPONENT_METADATA), custom => Self::custom(custom), } diff --git a/processor/src/host/debug.rs b/processor/src/host/debug.rs index 74d2a53c6b..5835e389cb 100644 --- a/processor/src/host/debug.rs +++ b/processor/src/host/debug.rs @@ -17,9 +17,6 @@ pub struct StdoutWriter; impl fmt::Write for StdoutWriter { fn write_str(&mut self, _s: &str) -> fmt::Result { - // When the `std` feature is disabled, the parameter `_s` is unused because - // the std::print! macro is not available. We prefix with underscore to - // indicate this intentional unused state and suppress warnings. #[cfg(feature = "std")] std::print!("{}", _s); Ok(())