Skip to main content

miden_core/operations/debug_metadata/
debug_var.rs

1use alloc::{format, sync::Arc, vec::Vec};
2use core::{fmt, num::NonZeroU32};
3
4use miden_debug_types::FileLineCol;
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    Felt,
10    serde::{
11        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
12        read_bounded_len,
13    },
14};
15
16// DEBUG VARIABLE INFO
17// ================================================================================================
18
19/// Debug information for tracking a source-level variable.
20///
21/// This record provides debuggers with information about where a variable's
22/// value can be found at a particular point in the program execution.
23#[derive(Clone, Debug, Eq, PartialEq)]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25pub struct DebugVarInfo {
26    /// Variable name as it appears in source code.
27    #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_arc_str"))]
28    name: Arc<str>,
29    /// Type information (encoded as type index in debug_info section)
30    type_id: Option<u32>,
31    /// If this is a function parameter, its 1-based index.
32    arg_index: Option<NonZeroU32>,
33    /// Source file location (file:line:column).
34    /// This should only be set when the location differs from the AssemblyOp location associated
35    /// with the same instruction, to avoid package bloat.
36    location: Option<FileLineCol>,
37    /// Where to find the variable's value at this point
38    value_location: DebugVarLocation,
39}
40
41impl DebugVarInfo {
42    /// Creates a new [DebugVarInfo] with the specified variable name and location.
43    pub fn new(name: impl Into<Arc<str>>, value_location: DebugVarLocation) -> Self {
44        Self {
45            name: name.into(),
46            type_id: None,
47            arg_index: None,
48            location: None,
49            value_location,
50        }
51    }
52
53    /// Returns the variable name.
54    pub fn name(&self) -> &str {
55        &self.name
56    }
57
58    /// Returns the type ID if set.
59    pub fn type_id(&self) -> Option<u32> {
60        self.type_id
61    }
62
63    /// Sets the type ID for this variable.
64    pub fn set_type_id(&mut self, type_id: u32) {
65        self.type_id = Some(type_id);
66    }
67
68    /// Returns the argument index if this is a function parameter.
69    /// The index is 1-based.
70    pub fn arg_index(&self) -> Option<NonZeroU32> {
71        self.arg_index
72    }
73
74    /// Sets the argument index for this variable.
75    ///
76    /// # Panics
77    /// Panics if `arg_index` is 0, since argument indices are 1-based.
78    pub fn set_arg_index(&mut self, arg_index: u32) {
79        self.arg_index =
80            Some(NonZeroU32::new(arg_index).expect("argument index must be 1-based (non-zero)"));
81    }
82
83    /// Returns the source location if set.
84    /// This is only set when the location differs from the AssemblyOp location.
85    pub fn location(&self) -> Option<&FileLineCol> {
86        self.location.as_ref()
87    }
88
89    /// Sets the source location for this variable.
90    /// Only set this when the location differs from the AssemblyOp location
91    /// to avoid package bloat.
92    pub fn set_location(&mut self, location: FileLineCol) {
93        self.location = Some(location);
94    }
95
96    /// Returns where the variable's value can be found.
97    pub fn value_location(&self) -> &DebugVarLocation {
98        &self.value_location
99    }
100
101    /// Replaces the value location in-place, preserving all other fields.
102    pub fn set_value_location(&mut self, value_location: DebugVarLocation) {
103        self.value_location = value_location;
104    }
105}
106
107/// Serde deserializer for `Arc<str>`.
108#[cfg(feature = "serde")]
109fn deserialize_arc_str<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
110where
111    D: serde::Deserializer<'de>,
112{
113    use alloc::string::String;
114    let s = String::deserialize(deserializer)?;
115    Ok(Arc::from(s))
116}
117
118impl fmt::Display for DebugVarInfo {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "var.{}", self.name)?;
121
122        if let Some(arg_index) = self.arg_index {
123            write!(f, "[arg{arg_index}]")?;
124        }
125
126        write!(f, " = {}", self.value_location)?;
127
128        if let Some(loc) = &self.location {
129            write!(f, " {loc}")?;
130        }
131
132        Ok(())
133    }
134}
135
136// DEBUG VARIABLE LOCATION
137// ================================================================================================
138
139/// Describes where a variable's value can be found during execution.
140///
141/// This enum models the different ways a variable's value might be stored
142/// during program execution, ranging from simple stack positions to complex
143/// expressions.
144#[derive(Clone, Debug, Eq, PartialEq)]
145#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
146pub enum DebugVarLocation {
147    /// Variable is at stack position N (0 = top of stack)
148    Stack(u8),
149    /// Variable is in memory at the given element address
150    Memory(u32),
151    /// Variable is a constant field element
152    Const(Felt),
153    /// Variable is in local memory at a signed offset from FMP.
154    ///
155    /// The actual memory address is computed as: `FMP + offset`
156    /// where offset is typically negative (locals are below FMP).
157    /// For example, with 3 locals: local\[0\] has offset -3, local\[2\] has offset -1.
158    Local(i16),
159    /// Variable is in WASM linear memory at an address computed from a global base
160    /// plus a byte offset: `value_of(global[global_index]) + byte_offset`.
161    ///
162    /// This corresponds to DWARF's `DW_OP_fbreg` where the frame base is a WASM
163    /// global (typically `__stack_pointer`). The byte offset is divided by the
164    /// element size (4 for i32) to get the Miden memory element address.
165    FrameBase {
166        /// WASM global index whose runtime value provides the base address.
167        global_index: u32,
168        /// Byte offset from the base (may be positive or negative).
169        byte_offset: i64,
170    },
171    /// Complex location described by expression bytes.
172    /// This is used for variables that require computation to locate,
173    /// such as struct fields or array elements.
174    Expression(Vec<u8>),
175}
176
177impl fmt::Display for DebugVarLocation {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::Stack(pos) => write!(f, "stack[{pos}]"),
181            Self::Memory(addr) => write!(f, "mem[{addr}]"),
182            Self::Const(val) => write!(f, "const({})", val.as_canonical_u64()),
183            Self::Local(offset) => write!(f, "FMP{offset:+}"),
184            Self::FrameBase { global_index, byte_offset } => {
185                write!(f, "global[{global_index}]{byte_offset:+}")
186            },
187            Self::Expression(bytes) => {
188                write!(f, "expr(")?;
189                for (i, byte) in bytes.iter().enumerate() {
190                    if i > 0 {
191                        write!(f, " ")?;
192                    }
193                    write!(f, "{byte:02x}")?;
194                }
195                write!(f, ")")
196            },
197        }
198    }
199}
200
201// SERIALIZATION
202// ================================================================================================
203
204impl Serializable for DebugVarLocation {
205    fn write_into<W: ByteWriter>(&self, target: &mut W) {
206        match self {
207            Self::Stack(pos) => {
208                target.write_u8(0);
209                target.write_u8(*pos);
210            },
211            Self::Memory(addr) => {
212                target.write_u8(1);
213                target.write_u32(*addr);
214            },
215            Self::Const(felt) => {
216                target.write_u8(2);
217                target.write_u64(felt.as_canonical_u64());
218            },
219            Self::Local(offset) => {
220                target.write_u8(3);
221                target.write_bytes(&offset.to_le_bytes());
222            },
223            Self::Expression(bytes) => {
224                target.write_u8(4);
225                bytes.write_into(target);
226            },
227            Self::FrameBase { global_index, byte_offset } => {
228                target.write_u8(5);
229                target.write_u32(*global_index);
230                target.write_bytes(&byte_offset.to_le_bytes());
231            },
232        }
233    }
234}
235
236impl Deserializable for DebugVarLocation {
237    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
238        let tag = source.read_u8()?;
239        match tag {
240            0 => Ok(Self::Stack(source.read_u8()?)),
241            1 => Ok(Self::Memory(source.read_u32()?)),
242            2 => {
243                let value = source.read_u64()?;
244                Ok(Self::Const(Felt::new_unchecked(value)))
245            },
246            3 => {
247                let bytes = source.read_array::<2>()?;
248                Ok(Self::Local(i16::from_le_bytes(bytes)))
249            },
250            4 => {
251                let bytes = read_bounded_bytes(source, "debug variable expression bytes")?;
252                Ok(Self::Expression(bytes))
253            },
254            5 => {
255                let global_index = source.read_u32()?;
256                let bytes = source.read_array::<8>()?;
257                let byte_offset = i64::from_le_bytes(bytes);
258                Ok(Self::FrameBase { global_index, byte_offset })
259            },
260            _ => Err(DeserializationError::InvalidValue(format!(
261                "invalid DebugVarLocation tag: {tag}"
262            ))),
263        }
264    }
265}
266
267impl Serializable for DebugVarInfo {
268    fn write_into<W: ByteWriter>(&self, target: &mut W) {
269        (*self.name).write_into(target);
270        self.value_location.write_into(target);
271        self.type_id.write_into(target);
272        self.arg_index.map(core::num::NonZero::get).write_into(target);
273        self.location.write_into(target);
274    }
275}
276
277impl Deserializable for DebugVarInfo {
278    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
279        let name = read_bounded_string(source, "debug variable name bytes")?;
280        let value_location = DebugVarLocation::read_from(source)?;
281        let type_id = Option::<u32>::read_from(source)?;
282        let arg_index = Option::<u32>::read_from(source)?
283            .map(|n| {
284                NonZeroU32::new(n).ok_or_else(|| {
285                    DeserializationError::InvalidValue("arg_index must be non-zero".into())
286                })
287            })
288            .transpose()?;
289        let location = Option::<FileLineCol>::read_from(source)?;
290
291        Ok(Self {
292            name,
293            type_id,
294            arg_index,
295            location,
296            value_location,
297        })
298    }
299}
300
301fn read_bounded_string<R: ByteReader>(
302    source: &mut R,
303    label: &str,
304) -> Result<Arc<str>, DeserializationError> {
305    let len = read_bounded_len(source, label, 1)?;
306    let bytes = source.read_slice(len)?;
307    let value = core::str::from_utf8(bytes)
308        .map_err(|err| DeserializationError::InvalidValue(format!("{err}")))?;
309    Ok(Arc::from(value))
310}
311
312fn read_bounded_bytes<R: ByteReader>(
313    source: &mut R,
314    label: &str,
315) -> Result<Vec<u8>, DeserializationError> {
316    let len = read_bounded_len(source, label, 1)?;
317    source.read_slice(len).map(<[u8]>::to_vec)
318}
319
320#[cfg(test)]
321mod tests {
322    use alloc::string::ToString;
323
324    use miden_debug_types::{ColumnNumber, LineNumber, Uri};
325
326    use super::*;
327    use crate::serde::{Deserializable, Serializable, SliceReader};
328
329    #[test]
330    fn debug_var_info_display_simple() {
331        let var = DebugVarInfo::new("x", DebugVarLocation::Stack(0));
332        assert_eq!(var.to_string(), "var.x = stack[0]");
333    }
334
335    #[test]
336    fn debug_var_info_rejects_oversized_name_length() {
337        let bytes = [0x08, 0x2a, 0xfe, 0xfe, 0x01];
338        let mut reader = SliceReader::new(&bytes);
339        let err = DebugVarInfo::read_from(&mut reader).unwrap_err();
340        let DeserializationError::InvalidValue(message) = err else {
341            panic!("expected InvalidValue error");
342        };
343        assert!(message.contains("debug variable name bytes count"));
344        assert!(message.contains("exceeds remaining input"));
345    }
346
347    #[test]
348    fn debug_var_location_rejects_oversized_expression_length() {
349        let bytes = [4, 0x08, 0x2a, 0xfe, 0xfe, 0x01];
350        let mut reader = SliceReader::new(&bytes);
351        let err = DebugVarLocation::read_from(&mut reader).unwrap_err();
352        let DeserializationError::InvalidValue(message) = err else {
353            panic!("expected InvalidValue error");
354        };
355        assert!(message.contains("debug variable expression bytes count"));
356        assert!(message.contains("exceeds remaining input"));
357    }
358
359    #[test]
360    fn debug_var_info_display_with_arg() {
361        let mut var = DebugVarInfo::new("param", DebugVarLocation::Stack(2));
362        var.set_arg_index(1);
363        assert_eq!(var.to_string(), "var.param[arg1] = stack[2]");
364    }
365
366    #[test]
367    fn debug_var_info_display_with_location() {
368        let mut var = DebugVarInfo::new("y", DebugVarLocation::Memory(100));
369        var.set_location(FileLineCol::new(
370            Uri::new("test.rs"),
371            LineNumber::new(42).unwrap(),
372            ColumnNumber::new(5).unwrap(),
373        ));
374        assert_eq!(var.to_string(), "var.y = mem[100] [test.rs@42:5]");
375    }
376
377    #[test]
378    fn debug_var_location_display() {
379        assert_eq!(DebugVarLocation::Stack(0).to_string(), "stack[0]");
380        assert_eq!(DebugVarLocation::Memory(256).to_string(), "mem[256]");
381        assert_eq!(DebugVarLocation::Const(Felt::new_unchecked(42)).to_string(), "const(42)");
382        assert_eq!(DebugVarLocation::Local(-3).to_string(), "FMP-3");
383        assert_eq!(
384            DebugVarLocation::FrameBase { global_index: 20, byte_offset: -12 }.to_string(),
385            "global[20]-12"
386        );
387        assert_eq!(
388            DebugVarLocation::Expression(vec![0x10, 0x20, 0x30]).to_string(),
389            "expr(10 20 30)"
390        );
391    }
392
393    #[test]
394    fn debug_var_location_serialization_round_trip() {
395        let locations = [
396            DebugVarLocation::Stack(7),
397            DebugVarLocation::Memory(0xdead_beef),
398            DebugVarLocation::Const(Felt::new_unchecked(999)),
399            DebugVarLocation::Local(-3),
400            DebugVarLocation::FrameBase { global_index: 20, byte_offset: -12 },
401            DebugVarLocation::Expression(vec![0x10, 0x20, 0x30]),
402        ];
403
404        for loc in &locations {
405            let mut bytes = Vec::new();
406            loc.write_into(&mut bytes);
407            let mut reader = SliceReader::new(&bytes);
408            let deser = DebugVarLocation::read_from(&mut reader).unwrap();
409            assert_eq!(&deser, loc);
410        }
411    }
412
413    #[test]
414    fn debug_var_info_serialization_round_trip_all_fields() {
415        let mut var = DebugVarInfo::new("full", DebugVarLocation::Expression(vec![0xaa, 0xbb]));
416        var.set_type_id(7);
417        var.set_arg_index(2);
418        var.set_location(FileLineCol::new(
419            Uri::new("lib.rs"),
420            LineNumber::new(50).unwrap(),
421            ColumnNumber::new(10).unwrap(),
422        ));
423
424        let mut bytes = Vec::new();
425        var.write_into(&mut bytes);
426        let mut reader = SliceReader::new(&bytes);
427        let deser = DebugVarInfo::read_from(&mut reader).unwrap();
428        assert_eq!(deser, var);
429    }
430
431    #[test]
432    fn debug_var_info_set_value_location() {
433        let mut var = DebugVarInfo::new("x", DebugVarLocation::Stack(0));
434        var.set_value_location(DebugVarLocation::FrameBase { global_index: 20, byte_offset: -12 });
435        assert_eq!(
436            var.value_location(),
437            &DebugVarLocation::FrameBase { global_index: 20, byte_offset: -12 }
438        );
439    }
440}