Skip to main content

miden_assembly_syntax/ast/instruction/
debug_var.rs

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