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 abf12c2b25..a7c069e89f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Enhancements +- Added `DebugVar` decorator for tracking source-level variables during execution, and `debug_info` section in MASP packages for storing type definitions, source file paths, and function metadata to support source-level debugging. - Added `procedure_names` to `DebugInfo` for storing procedure name mappings by MAST root digest, enabling debuggers to resolve human-readable procedure names during execution (#[2474](https://github.com/0xMiden/miden-vm/pull/2474)). #### Fixes diff --git a/Cargo.lock b/Cargo.lock index 2a558accd5..67d1e7924f 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/lib.rs b/core/src/lib.rs index 98c92e77c8..72ad9e8977 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -117,7 +117,8 @@ pub mod prettier { mod operations; pub use operations::{ - AssemblyOp, DebugOptions, Decorator, DecoratorList, Operation, opcode_constants::*, + AssemblyOp, DebugOptions, DebugVarInfo, DebugVarLocation, Decorator, DecoratorList, Operation, + opcode_constants::*, }; pub mod stack; diff --git a/core/src/mast/serialization/decorator.rs b/core/src/mast/serialization/decorator.rs index b290de5305..d8e4aaa37b 100644 --- a/core/src/mast/serialization/decorator.rs +++ b/core/src/mast/serialization/decorator.rs @@ -11,7 +11,7 @@ use super::{ string_table::{StringTable, StringTableBuilder}, }; use crate::{ - AssemblyOp, DebugOptions, Decorator, + AssemblyOp, DebugOptions, DebugVarInfo, DebugVarLocation, Decorator, utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader, }, @@ -123,6 +123,65 @@ impl DecoratorInfo { let value = data_reader.read_u16()?; Ok(Decorator::Debug(DebugOptions::AdvStackTop(value))) }, + EncodedDecoratorVariant::DebugVar => { + // Read variable name + let name_idx = data_reader.read_usize()?; + let name = string_table.read_string(name_idx)?; + + // Read value location tag and data + let location_tag = data_reader.read_u8()?; + let value_location = match location_tag { + 0 => DebugVarLocation::Stack(data_reader.read_u8()?), + 1 => DebugVarLocation::Memory(data_reader.read_u32()?), + 2 => { + let value = data_reader.read_u64()?; + DebugVarLocation::Const(crate::Felt::new(value)) + }, + 3 => DebugVarLocation::Local(data_reader.read_u16()?), + 4 => { + let len = data_reader.read_u16()? as usize; + let bytes = data_reader.read_vec(len)?; + DebugVarLocation::Expression(bytes) + }, + _ => { + return Err(DeserializationError::InvalidValue(format!( + "invalid DebugVarLocation tag: {location_tag}" + ))); + }, + }; + + let mut debug_var = DebugVarInfo::new(name, value_location); + + // Read optional type_id + if data_reader.read_bool()? { + debug_var.set_type_id(data_reader.read_u32()?); + } + + // Read optional arg_index (1-based, stored as raw u32) + if data_reader.read_bool()? { + let arg_index = data_reader.read_u32()?; + // set_arg_index expects a non-zero value for 1-based indices + if arg_index > 0 { + debug_var.set_arg_index(arg_index); + } + } + + // Read optional source location + if data_reader.read_bool()? { + use miden_debug_types::{ColumnNumber, FileLineCol, LineNumber, Uri}; + let uri_idx = data_reader.read_usize()?; + let uri = Uri::from(string_table.read_arc_str(uri_idx)?); + let line = data_reader.read_u32()?; + let column = data_reader.read_u32()?; + debug_var.set_location(FileLineCol::new( + uri, + LineNumber::new(line).unwrap_or_default(), + ColumnNumber::new(column).unwrap_or_default(), + )); + } + + Ok(Decorator::DebugVar(debug_var)) + }, } } } @@ -168,6 +227,7 @@ pub enum EncodedDecoratorVariant { DebugOptionsLocalInterval, DebugOptionsAdvStackTop, Trace, + DebugVar, } impl EncodedDecoratorVariant { @@ -198,6 +258,7 @@ impl From<&Decorator> for EncodedDecoratorVariant { DebugOptions::AdvStackTop(_) => Self::DebugOptionsAdvStackTop, }, Decorator::Trace(_) => Self::Trace, + Decorator::DebugVar(_) => Self::DebugVar, } } } @@ -308,6 +369,60 @@ impl DecoratorDataBuilder { Decorator::Trace(value) => { self.decorator_data.extend(value.to_le_bytes()); + Some(data_offset) + }, + Decorator::DebugVar(debug_var) => { + // Write variable name + let name_offset = self.string_table_builder.add_string(debug_var.name()); + self.decorator_data.write_usize(name_offset); + + // Write value location with tag + match debug_var.value_location() { + DebugVarLocation::Stack(pos) => { + self.decorator_data.push(0); // tag + self.decorator_data.push(*pos); + }, + DebugVarLocation::Memory(addr) => { + self.decorator_data.push(1); // tag + self.decorator_data.extend(addr.to_le_bytes()); + }, + DebugVarLocation::Const(felt) => { + self.decorator_data.push(2); // tag + // Serialize Felt as u64 + self.decorator_data.extend(felt.as_int().to_le_bytes()); + }, + DebugVarLocation::Local(idx) => { + self.decorator_data.push(3); // tag + self.decorator_data.extend(idx.to_le_bytes()); + }, + DebugVarLocation::Expression(bytes) => { + self.decorator_data.push(4); // tag + self.decorator_data.extend((bytes.len() as u16).to_le_bytes()); + self.decorator_data.extend(bytes); + }, + } + + // Write optional type_id + self.decorator_data.write_bool(debug_var.type_id().is_some()); + if let Some(type_id) = debug_var.type_id() { + self.decorator_data.extend(type_id.to_le_bytes()); + } + + // Write optional arg_index (serialize NonZeroU32 as u32) + self.decorator_data.write_bool(debug_var.arg_index().is_some()); + if let Some(arg_index) = debug_var.arg_index() { + self.decorator_data.extend(arg_index.get().to_le_bytes()); + } + + // Write optional source location + self.decorator_data.write_bool(debug_var.location().is_some()); + if let Some(loc) = debug_var.location() { + let uri_offset = self.string_table_builder.add_string(loc.uri.as_str()); + self.decorator_data.write_usize(uri_offset); + self.decorator_data.extend(loc.line.to_u32().to_le_bytes()); + self.decorator_data.extend(loc.column.to_u32().to_le_bytes()); + } + Some(data_offset) }, } diff --git a/core/src/mast/serialization/tests.rs b/core/src/mast/serialization/tests.rs index b0a2fd5232..7484a17bc2 100644 --- a/core/src/mast/serialization/tests.rs +++ b/core/src/mast/serialization/tests.rs @@ -127,6 +127,7 @@ fn confirm_operation_and_decorator_structure() { DebugOptions::AdvStackTop(_) => (), }, Decorator::Trace(_) => (), + Decorator::DebugVar(_) => (), }; } diff --git a/core/src/operations/decorators/debug_var.rs b/core/src/operations/decorators/debug_var.rs new file mode 100644 index 0000000000..efc06d2f85 --- /dev/null +++ b/core/src/operations/decorators/debug_var.rs @@ -0,0 +1,214 @@ +use alloc::{sync::Arc, vec::Vec}; +use core::{fmt, num::NonZeroU32}; + +use miden_debug_types::FileLineCol; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::Felt; + +// 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 a local slot at the given index + Local(u16), + /// 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_int()), + Self::Local(idx) => write!(f, "local[{}]", idx), + Self::Expression(bytes) => { + write!(f, "expr(")?; + for (i, byte) in bytes.iter().enumerate() { + if i > 0 { + write!(f, " ")?; + } + write!(f, "{:02x}", byte)?; + } + write!(f, ")") + }, + } + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use miden_debug_types::{ColumnNumber, LineNumber, Uri}; + + use super::*; + + #[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(), "local[3]"); + assert_eq!( + DebugVarLocation::Expression(vec![0x10, 0x20, 0x30]).to_string(), + "expr(10 20 30)" + ); + } +} diff --git a/core/src/operations/decorators/mod.rs b/core/src/operations/decorators/mod.rs index 6b56120cb9..631b5f0876 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 @@ -36,6 +39,10 @@ pub enum Decorator { Debug(DebugOptions), /// Emits a trace to the host. Trace(u32), + /// Provides debug information about a source-level variable at this point in execution. + /// This decorator records where a variable's value can be found (stack position, memory + /// address, etc.) for use by debuggers. + DebugVar(DebugVarInfo), } impl Decorator { @@ -61,6 +68,10 @@ impl Decorator { }, Self::Debug(debug) => Blake3_256::hash(debug.to_string().as_bytes()), Self::Trace(trace) => Blake3_256::hash(&trace.to_le_bytes()), + Self::DebugVar(debug_var) => { + // Hash the debug variable info by its display representation + Blake3_256::hash(debug_var.to_string().as_bytes()) + }, } } } @@ -79,6 +90,7 @@ impl fmt::Display for Decorator { }, Self::Debug(options) => write!(f, "debug({options})"), Self::Trace(trace_id) => write!(f, "trace({trace_id})"), + Self::DebugVar(debug_var) => write!(f, "variable({})", debug_var), } } } diff --git a/core/src/operations/mod.rs b/core/src/operations/mod.rs index eb359d352b..4feb2af386 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, +}; 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 046e8986ac..94463198f8 100644 --- a/crates/assembly-syntax/src/ast/instruction/mod.rs +++ b/crates/assembly-syntax/src/ast/instruction/mod.rs @@ -273,6 +273,7 @@ pub enum Instruction { // ----- debug decorators -------------------------------------------------------------------- Breakpoint, Debug(DebugOptions), + DebugVar(miden_core::DebugVarInfo), // ----- event decorators -------------------------------------------------------------------- Emit, @@ -285,6 +286,17 @@ impl Instruction { pub const fn should_break(&self) -> bool { matches!(self, Self::Breakpoint) } + + /// Returns true if the instruction has a textual representation in MASM syntax. + /// + /// Some instructions (like `DebugVar`) are only emitted by the compiler as decorators + /// and are not present in the textual format of MASM. These instructions cannot be + /// parsed from text, and should not be pretty-printed like normal instructions. + pub const fn has_textual_representation(&self) -> bool { + // DebugVar is a decorator-only instruction emitted by the compiler, + // not something that can be written in MASM source code. + !matches!(self, Self::DebugVar(_)) + } } impl core::fmt::Display for Instruction { diff --git a/crates/assembly-syntax/src/ast/instruction/print.rs b/crates/assembly-syntax/src/ast/instruction/print.rs index faa4d8aee9..7e2debef2b 100644 --- a/crates/assembly-syntax/src/ast/instruction/print.rs +++ b/crates/assembly-syntax/src/ast/instruction/print.rs @@ -8,6 +8,12 @@ impl PrettyPrint for Instruction { fn render(&self) -> Document { use crate::prettier::*; + // Instructions without textual representation (e.g., DebugVar) should not be printed. + // They are decorator-only constructs emitted by the compiler. + if !self.has_textual_representation() { + return Document::Empty; + } + match self { Self::Nop => const_text("nop"), Self::Assert => const_text("assert"), @@ -324,6 +330,9 @@ impl PrettyPrint for Instruction { // ----- debug decorators ------------------------------------------------------------- Self::Breakpoint => const_text("breakpoint"), Self::Debug(options) => inst_with_imm("debug", options), + Self::DebugVar(_) => { + unreachable!("expected {self:#?} to have textual representation") + }, // ----- event decorators ------------------------------------------------------------- Self::Emit => const_text("emit"), diff --git a/crates/assembly-syntax/src/ast/visit.rs b/crates/assembly-syntax/src/ast/visit.rs index f73eabc084..b5472c6154 100644 --- a/crates/assembly-syntax/src/ast/visit.rs +++ b/crates/assembly-syntax/src/ast/visit.rs @@ -499,8 +499,8 @@ where | PushFeltList(_) | Sdepth | Caller | Clk | MemLoad | MemLoadWBe | MemLoadWLe | MemStore | MemStoreWBe | MemStoreWLe | MemStream | AdvPipe | AdvLoadW | Hash | HMerge | HPerm | MTreeGet | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec - | DynCall | Breakpoint | HornerBase | HornerExt | CryptoStream | EvalCircuit - | LogPrecompile | Emit => ControlFlow::Continue(()), + | DynCall | Breakpoint | DebugVar(_) | HornerBase | HornerExt | CryptoStream + | EvalCircuit | LogPrecompile | Emit => ControlFlow::Continue(()), } } @@ -1084,8 +1084,8 @@ where | PushFeltList(_) | Sdepth | Caller | Clk | MemLoad | MemLoadWBe | MemLoadWLe | MemStore | MemStoreWBe | MemStoreWLe | MemStream | AdvPipe | AdvLoadW | Hash | HMerge | HPerm | MTreeGet | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec - | DynCall | Breakpoint | HornerBase | HornerExt | EvalCircuit | CryptoStream - | LogPrecompile | Emit => ControlFlow::Continue(()), + | DynCall | Breakpoint | DebugVar(_) | HornerBase | HornerExt | CryptoStream + | EvalCircuit | LogPrecompile | Emit => ControlFlow::Continue(()), } } diff --git a/crates/assembly/src/instruction/mod.rs b/crates/assembly/src/instruction/mod.rs index 6365148569..b843afaaa0 100644 --- a/crates/assembly/src/instruction/mod.rs +++ b/crates/assembly/src/instruction/mod.rs @@ -587,6 +587,10 @@ impl Assembler { .push_decorator(Decorator::Debug(debug::compile_options(options, proc_ctx)?))?; }, + Instruction::DebugVar(debug_var_info) => { + block_builder.push_decorator(Decorator::DebugVar(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/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..cc08e5a0f4 --- /dev/null +++ b/crates/mast-package/src/debug_info/mod.rs @@ -0,0 +1,13 @@ +//! Debug information section for MASP packages. +//! +//! This module provides types for encoding source-level debug information in the `.debug_info` +//! custom section 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::*; + +// Re-export serialization traits for use by consumers +// (the impl blocks are in serialization.rs) 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..6b66492eb8 --- /dev/null +++ b/crates/mast-package/src/debug_info/serialization.rs @@ -0,0 +1,576 @@ +//! Serialization and deserialization for the debug_info section. + +use alloc::string::String; + +use miden_core::utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, +}; +use miden_debug_types::{ColumnNumber, LineNumber}; + +use super::{ + DEBUG_INFO_VERSION, DebugFieldInfo, DebugFileInfo, DebugFunctionInfo, DebugInfoSection, + DebugInlinedCallInfo, DebugPrimitiveType, DebugTypeInfo, DebugVariableInfo, +}; + +// DEBUG INFO SECTION SERIALIZATION +// ================================================================================================ + +impl Serializable for DebugInfoSection { + fn write_into(&self, target: &mut W) { + // Write version + 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); + } + + // Write file table + target.write_usize(self.files.len()); + for file in &self.files { + file.write_into(target); + } + + // Write function table + target.write_usize(self.functions.len()); + for func in &self.functions { + func.write_into(target); + } + } +} + +impl Deserializable for DebugInfoSection { + fn read_from(source: &mut R) -> Result { + let version = source.read_u8()?; + if version != DEBUG_INFO_VERSION { + return Err(DeserializationError::InvalidValue(alloc::format!( + "unsupported debug_info version: {version}, expected {DEBUG_INFO_VERSION}" + ))); + } + + // Read string table + 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)?); + } + + // Read type table + let types_len = source.read_usize()?; + let mut types = alloc::vec::Vec::with_capacity(types_len); + for _ in 0..types_len { + types.push(DebugTypeInfo::read_from(source)?); + } + + // Read file table + let files_len = source.read_usize()?; + let mut files = alloc::vec::Vec::with_capacity(files_len); + for _ in 0..files_len { + files.push(DebugFileInfo::read_from(source)?); + } + + // Read function table + let functions_len = source.read_usize()?; + let mut functions = alloc::vec::Vec::with_capacity(functions_len); + for _ in 0..functions_len { + functions.push(DebugFunctionInfo::read_from(source)?); + } + + Ok(Self { + version, + strings, + types, + files, + 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); + }, + Self::Array { element_type_idx, count } => { + target.write_u8(TYPE_TAG_ARRAY); + target.write_u32(*element_type_idx); + 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); + } + target.write_usize(param_type_indices.len()); + for idx in param_type_indices { + target.write_u32(*idx); + } + }, + 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 = source.read_u32()?; + Ok(Self::Pointer { pointee_type_idx }) + }, + TYPE_TAG_ARRAY => { + let element_type_idx = 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 mut fields = alloc::vec::Vec::with_capacity(fields_len); + for _ in 0..fields_len { + fields.push(DebugFieldInfo::read_from(source)?); + } + Ok(Self::Struct { name_idx, size, fields }) + }, + TYPE_TAG_FUNCTION => { + let has_return = source.read_bool()?; + let return_type_idx = if has_return { Some(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(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); + 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 = 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); + } + } +} + +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(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); + } + + target.write_bool(self.mast_root.is_some()); + if let Some(root) = &self.mast_root { + target.write_bytes(root); + } + + // 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(source.read_u32()?) } else { None }; + + let has_mast_root = source.read_bool()?; + let mast_root = if has_mast_root { + let bytes = source.read_slice(32)?; + let mut arr = [0u8; 32]; + arr.copy_from_slice(bytes); + Some(arr) + } else { + None + }; + + // Read variables + let vars_len = source.read_usize()?; + let mut variables = alloc::vec::Vec::with_capacity(vars_len); + for _ in 0..vars_len { + variables.push(DebugVariableInfo::read_from(source)?); + } + + // Read inlined calls + let calls_len = source.read_usize()?; + let mut inlined_calls = alloc::vec::Vec::with_capacity(calls_len); + for _ in 0..calls_len { + inlined_calls.push(DebugInlinedCallInfo::read_from(source)?); + } + + 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); + 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 = 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 { + let len = source.read_usize()?; + let bytes = source.read_slice(len)?; + String::from_utf8(bytes.to_vec()).map_err(|err| { + DeserializationError::InvalidValue(alloc::format!("invalid utf-8 in string: {err}")) + }) +} + +#[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::utils::SliceReader::new(&bytes)).unwrap(); + assert_eq!(value, &result); + } + + #[test] + fn test_debug_info_section_roundtrip() { + let mut section = DebugInfoSection::new(); + + // Add some strings + let name_idx = section.add_string("test_function"); + let file_idx_str = section.add_string("test.rs"); + + // 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("x"); + let y_idx = section.add_string("y"); + let point_idx = section.add_string("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, + }, + ], + }); + + // Add a file + let file_idx = section.add_file(DebugFileInfo::new(file_idx_str)); + + // Add a function + let line = LineNumber::new(10).unwrap(); + let column = ColumnNumber::new(1).unwrap(); + let mut func = DebugFunctionInfo::new(name_idx, file_idx, line, column); + let var_line = LineNumber::new(10).unwrap(); + let var_column = ColumnNumber::new(5).unwrap(); + func.add_variable( + DebugVariableInfo::new(x_idx, i32_type_idx, var_line, var_column).with_arg_index(1), + ); + section.add_function(func); + + roundtrip(§ion); + } + + #[test] + fn test_empty_section_roundtrip() { + let section = DebugInfoSection::new(); + roundtrip(§ion); + } + + #[test] + fn test_all_primitive_types_roundtrip() { + let mut section = DebugInfoSection::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(0), + param_type_indices: alloc::vec![1, 2, 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(2) + .with_mast_root([0xab; 32]); + + let var_line = LineNumber::new(5).unwrap(); + let var_col = ColumnNumber::new(10).unwrap(); + func.add_variable( + DebugVariableInfo::new(0, 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..4f108c04f7 --- /dev/null +++ b/crates/mast-package/src/debug_info/types.rs @@ -0,0 +1,527 @@ +//! 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::{string::String, vec::Vec}; + +use miden_debug_types::{ColumnNumber, LineNumber}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +// DEBUG INFO SECTION +// ================================================================================================ + +/// The version of the debug_info section format. +pub const DEBUG_INFO_VERSION: u8 = 1; + +/// Debug information section containing all debug metadata for a MASP package. +/// +/// This section provides a structured way to store debug information that augments +/// the `DebugVar` decorators embedded in the MAST. It includes: +/// - Type definitions (primitives, structs, arrays, etc.) +/// - Source file paths (deduplicated) +/// - Function debug metadata +/// - Global variable information +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebugInfoSection { + /// Version of the debug info format + pub version: u8, + /// String table containing all strings (file paths, type names, variable names) + pub strings: Vec, + /// Type table containing all type definitions + pub types: Vec, + /// Source file table (indices into string table) + pub files: Vec, + /// Function debug information + pub functions: Vec, +} + +impl DebugInfoSection { + /// Creates a new empty debug info section. + pub fn new() -> Self { + Self { + version: DEBUG_INFO_VERSION, + strings: Vec::new(), + types: Vec::new(), + files: Vec::new(), + functions: Vec::new(), + } + } + + /// Adds a string to the string table and returns its index. + pub fn add_string(&mut self, s: impl Into) -> u32 { + let s = s.into(); + // Check for existing string + 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<&str> { + self.strings.get(idx as usize).map(String::as_str) + } + + /// Adds a type to the type table and returns its index. + pub fn add_type(&mut self, ty: DebugTypeInfo) -> u32 { + let idx = self.types.len() as u32; + self.types.push(ty); + idx + } + + /// Gets a type by index. + pub fn get_type(&self, idx: u32) -> Option<&DebugTypeInfo> { + self.types.get(idx as usize) + } + + /// Adds a file to the file table and returns its index. + pub fn add_file(&mut self, file: DebugFileInfo) -> u32 { + // Check for existing file with same path + 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) + } + + /// 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 debug info). + pub fn is_empty(&self) -> bool { + self.types.is_empty() && self.files.is_empty() && 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: u32, + }, + /// An array type + Array { + /// The element type (index into type table) + element_type_idx: u32, + /// 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. +#[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 = 1, + /// Signed 8-bit integer + I8 = 2, + /// Unsigned 8-bit integer + U8 = 3, + /// Signed 16-bit integer + I16 = 4, + /// Unsigned 16-bit integer + U16 = 5, + /// Signed 32-bit integer + I32 = 6, + /// Unsigned 32-bit integer + U32 = 7, + /// Signed 64-bit integer + I64 = 8, + /// Unsigned 64-bit integer + U64 = 9, + /// Signed 128-bit integer + I128 = 10, + /// Unsigned 128-bit integer + U128 = 11, + /// 32-bit floating point + F32 = 12, + /// 64-bit floating point + F64 = 13, + /// Miden field element (64-bit, but with field semantics) + Felt = 14, + /// Miden word (4 field elements) + Word = 15, +} + +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: u32, + /// 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. + pub checksum: Option<[u8; 32]>, +} + +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(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<[u8; 32]>, + /// 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: u32) -> Self { + self.type_idx = Some(type_idx); + self + } + + /// Sets the MAST root digest. + pub fn with_mast_root(mut self, mast_root: [u8; 32]) -> 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: u32, + /// 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: u32, 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_info_section_string_dedup() { + let mut section = DebugInfoSection::new(); + + let idx1 = section.add_string("test.rs"); + let idx2 = section.add_string("main.rs"); + let idx3 = section.add_string("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_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 53ab34d31c..a7d50d5b23 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/processor/src/fast/mod.rs b/processor/src/fast/mod.rs index 785a5cc346..698e712092 100644 --- a/processor/src/fast/mod.rs +++ b/processor/src/fast/mod.rs @@ -792,6 +792,17 @@ impl FastProcessor { } } }, + Decorator::DebugVar(_debug_var) => { + // DebugVar is strictly a metadata-carrying decorator used by downstream + // tooling (e.g., debuggers). It is non-actionable in the processor itself. + // + // TODO: Consider adding a mechanism for debugger notification when variable + // info changes. Options include: + // - A raw trace event + // - Modifying the on_debug callback + // - A new on_variable callback specific to DebugVar + // Any such callback should only be invoked when tracing is enabled. + }, }; ControlFlow::Continue(()) }