Skip to main content

miden_assembly_syntax/ast/instruction/
debug_var.rs

1use alloc::{format, string::ToString, 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/// A frame base resolved into Miden execution coordinates.
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
138pub enum DebugFrameBase {
139    /// The base value is stored in local memory at this signed FMP-relative offset.
140    Local(i16),
141    /// The base value is stored at this Miden memory element address.
142    Memory(u32),
143}
144
145/// A location expression in Miden runtime coordinates.
146///
147/// This is the package-level escape hatch for locations which cannot be represented by one of the
148/// simple [`DebugVarLocation`] variants. Producers must resolve source-specific coordinates, such
149/// as Wasm local/global indices, before constructing this expression.
150#[derive(Clone, Debug, Eq, PartialEq)]
151#[cfg_attr(feature = "serde", derive(Serialize))]
152#[cfg_attr(
153    all(feature = "arbitrary", test),
154    miden_test_serialization_macros::serialization_test
155)]
156pub struct DebugLocationExpression {
157    operations: Vec<DebugLocationExpressionOp>,
158}
159
160/// Error returned when a structured debug location expression exceeds the wire-format limit.
161#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
162#[error(
163    "debug location expression has {operation_count} operations, but at most {MAX_DEBUG_LOCATION_EXPRESSION_OPS} are supported"
164)]
165pub struct DebugLocationExpressionError {
166    operation_count: usize,
167}
168
169impl DebugLocationExpressionError {
170    /// Returns the rejected operation count.
171    pub fn operation_count(&self) -> usize {
172        self.operation_count
173    }
174}
175
176#[cfg(feature = "serde")]
177#[derive(Deserialize)]
178struct DebugLocationExpressionSerde {
179    #[serde(deserialize_with = "deserialize_debug_location_expression_operations")]
180    operations: Vec<DebugLocationExpressionOp>,
181}
182
183#[cfg(feature = "serde")]
184fn deserialize_debug_location_expression_operations<'de, D>(
185    deserializer: D,
186) -> Result<Vec<DebugLocationExpressionOp>, D::Error>
187where
188    D: serde::Deserializer<'de>,
189{
190    struct OperationsVisitor;
191
192    impl<'de> serde::de::Visitor<'de> for OperationsVisitor {
193        type Value = Vec<DebugLocationExpressionOp>;
194
195        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196            write!(
197                formatter,
198                "at most {MAX_DEBUG_LOCATION_EXPRESSION_OPS} debug location operations"
199            )
200        }
201
202        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
203        where
204            A: serde::de::SeqAccess<'de>,
205        {
206            if let Some(operation_count) = sequence.size_hint()
207                && operation_count > MAX_DEBUG_LOCATION_EXPRESSION_OPS
208            {
209                return Err(serde::de::Error::custom(DebugLocationExpressionError {
210                    operation_count,
211                }));
212            }
213
214            let mut operations = Vec::with_capacity(sequence.size_hint().unwrap_or(0).min(8));
215            while let Some(operation) = sequence.next_element()? {
216                if operations.len() == MAX_DEBUG_LOCATION_EXPRESSION_OPS {
217                    return Err(serde::de::Error::custom(DebugLocationExpressionError {
218                        operation_count: operations.len() + 1,
219                    }));
220                }
221                operations.push(operation);
222            }
223            Ok(operations)
224        }
225    }
226
227    deserializer.deserialize_seq(OperationsVisitor)
228}
229
230const MAX_DEBUG_LOCATION_EXPRESSION_OPS: usize = 256;
231
232impl DebugLocationExpression {
233    /// Creates a location expression from runtime-resolved operations.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error when the expression exceeds the maximum operation count accepted by the
238    /// package wire format.
239    pub fn new(
240        operations: Vec<DebugLocationExpressionOp>,
241    ) -> Result<Self, DebugLocationExpressionError> {
242        validate_debug_location_expression_len(operations.len())?;
243        Ok(Self { operations })
244    }
245
246    /// Returns the operations in evaluation order.
247    pub fn operations(&self) -> &[DebugLocationExpressionOp] {
248        &self.operations
249    }
250
251    /// Returns true if this expression contains no operations.
252    pub fn is_empty(&self) -> bool {
253        self.operations.is_empty()
254    }
255}
256
257#[cfg(feature = "serde")]
258impl<'de> Deserialize<'de> for DebugLocationExpression {
259    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
260    where
261        D: serde::Deserializer<'de>,
262    {
263        let expression = DebugLocationExpressionSerde::deserialize(deserializer)?;
264        Self::new(expression.operations).map_err(serde::de::Error::custom)
265    }
266}
267
268/// An operation in a [`DebugLocationExpression`].
269///
270/// Operations evaluate on a signed integer stack. Read operations push canonical field element
271/// values as integers, arithmetic operations transform those values, and the final integer is
272/// converted back to a field element. Invalid arithmetic or field conversions make the location
273/// unavailable rather than wrapping.
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
276#[cfg_attr(
277    all(feature = "arbitrary", test),
278    miden_test_serialization_macros::serialization_test
279)]
280pub enum DebugLocationExpressionOp {
281    /// Push the value at this Miden operand-stack position (0 is the top).
282    ReadStack(u8),
283    /// Push the value stored at this Miden memory element address.
284    ReadMemory(u32),
285    /// Push the value stored at this signed FMP-relative local offset.
286    ReadLocal(i16),
287    /// Push an unsigned integer constant.
288    ConstU64(u64),
289    /// Push a signed integer constant.
290    ConstI64(i64),
291    /// Add an unsigned integer constant to the top value.
292    AddUnsigned(u64),
293    /// Pop two values and push `lhs + rhs`.
294    Add,
295    /// Pop two values and push `lhs - rhs`.
296    Sub,
297    /// Interpret the top value as a Wasm byte address, convert it to a Miden element address, and
298    /// push the value stored at that address.
299    DerefBytes,
300    /// Resolve and push a byte address relative to a runtime frame base.
301    FrameBaseAddress {
302        /// Resolved location containing the frame-base byte address.
303        base: DebugFrameBase,
304        /// Byte offset from the base.
305        byte_offset: i64,
306    },
307}
308
309/// Describes where a variable's value can be found during execution.
310///
311/// This enum models the different ways a variable's value might be stored
312/// during program execution, ranging from simple stack positions to complex
313/// expressions.
314#[derive(Clone, Debug, Eq, PartialEq)]
315#[cfg_attr(
316    all(feature = "arbitrary", test),
317    miden_test_serialization_macros::serialization_test
318)]
319pub enum DebugVarLocation {
320    /// Variable is at stack position N (0 = top of stack)
321    Stack(u8),
322    /// Variable is in memory at the given element address
323    Memory(u32),
324    /// Variable is a constant field element
325    Const(Felt),
326    /// Variable is in local memory at a signed offset from FMP.
327    ///
328    /// The actual memory address is computed as: `FMP + offset`
329    /// where offset is typically negative (locals are below FMP).
330    /// For example, with 3 locals: local\[0\] has offset -3, local\[2\] has offset -1.
331    Local(i16),
332    /// The variable has no representable location at this program point.
333    Unavailable,
334    /// Variable is in Wasm linear memory at `value_of(base) + byte_offset`.
335    ///
336    /// The base is expressed entirely in Miden execution coordinates. Its runtime value and the
337    /// offset are byte addresses; the debugger converts the resulting address to a Miden memory
338    /// element address before reading the variable.
339    ResolvedFrameBase {
340        /// Resolved location containing the frame-base byte address.
341        base: DebugFrameBase,
342        /// Byte offset from the base (may be positive or negative).
343        byte_offset: i64,
344    },
345    /// A compound location expressed entirely in Miden runtime coordinates.
346    Expression(DebugLocationExpression),
347}
348
349impl fmt::Display for DebugVarLocation {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        match self {
352            Self::Stack(pos) => write!(f, "stack[{pos}]"),
353            Self::Memory(addr) => write!(f, "mem[{addr}]"),
354            Self::Const(val) => write!(f, "const({})", val.as_canonical_u64()),
355            Self::Local(offset) => write!(f, "FMP{offset:+}"),
356            Self::Unavailable => f.write_str("unavailable"),
357            Self::ResolvedFrameBase { base, byte_offset } => match base {
358                DebugFrameBase::Local(offset) => {
359                    write!(f, "frame-base(FMP{offset:+}){byte_offset:+}")
360                },
361                DebugFrameBase::Memory(address) => {
362                    write!(f, "frame-base(mem[{address}]){byte_offset:+}")
363                },
364            },
365            Self::Expression(expression) => {
366                f.write_str("expr(")?;
367                f.debug_list().entries(expression.operations()).finish()?;
368                f.write_str(")")
369            },
370        }
371    }
372}
373
374// SERIALIZATION
375// ================================================================================================
376
377impl Serializable for DebugVarLocation {
378    fn write_into<W: ByteWriter>(&self, target: &mut W) {
379        match self {
380            Self::Stack(pos) => {
381                target.write_u8(0);
382                target.write_u8(*pos);
383            },
384            Self::Memory(addr) => {
385                target.write_u8(1);
386                target.write_u32(*addr);
387            },
388            Self::Const(felt) => {
389                target.write_u8(2);
390                target.write_u64(felt.as_canonical_u64());
391            },
392            Self::Local(offset) => {
393                target.write_u8(3);
394                target.write_bytes(&offset.to_le_bytes());
395            },
396            Self::Unavailable => {
397                target.write_u8(4);
398            },
399            Self::ResolvedFrameBase { base, byte_offset } => {
400                target.write_u8(5);
401                write_debug_frame_base(*base, target);
402                target.write_bytes(&byte_offset.to_le_bytes());
403            },
404            Self::Expression(expression) => {
405                target.write_u8(6);
406                expression.write_into(target);
407            },
408        }
409    }
410}
411
412impl Deserializable for DebugVarLocation {
413    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
414        let tag = source.read_u8()?;
415        match tag {
416            0 => Ok(Self::Stack(source.read_u8()?)),
417            1 => Ok(Self::Memory(source.read_u32()?)),
418            2 => {
419                let value = source.read_u64()?;
420                Ok(Self::Const(Felt::new_unchecked(value)))
421            },
422            3 => {
423                let bytes = source.read_array::<2>()?;
424                Ok(Self::Local(i16::from_le_bytes(bytes)))
425            },
426            4 => Ok(Self::Unavailable),
427            5 => {
428                let base = read_debug_frame_base(source)?;
429                let bytes = source.read_array::<8>()?;
430                let byte_offset = i64::from_le_bytes(bytes);
431                Ok(Self::ResolvedFrameBase { base, byte_offset })
432            },
433            6 => Ok(Self::Expression(DebugLocationExpression::read_from(source)?)),
434            _ => Err(DeserializationError::InvalidValue(format!(
435                "invalid DebugVarLocation tag: {tag}"
436            ))),
437        }
438    }
439
440    fn min_serialized_size() -> usize {
441        // `Unavailable` is encoded as a one-byte tag with no payload.
442        u8::min_serialized_size()
443    }
444}
445
446impl Serializable for DebugLocationExpression {
447    fn write_into<W: ByteWriter>(&self, target: &mut W) {
448        target.write_usize(self.operations.len());
449        for operation in &self.operations {
450            operation.write_into(target);
451        }
452    }
453}
454
455impl Deserializable for DebugLocationExpression {
456    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
457        let count = read_bounded_len(source, "debug location expression operations", 1)?;
458        validate_debug_location_expression_len(count)
459            .map_err(|error| DeserializationError::InvalidValue(error.to_string()))?;
460        let mut operations = Vec::with_capacity(count.min(8));
461        for _ in 0..count {
462            operations.push(DebugLocationExpressionOp::read_from(source)?);
463        }
464        Ok(Self { operations })
465    }
466
467    fn min_serialized_size() -> usize {
468        usize::min_serialized_size()
469    }
470}
471
472fn validate_debug_location_expression_len(
473    operation_count: usize,
474) -> Result<(), DebugLocationExpressionError> {
475    if operation_count > MAX_DEBUG_LOCATION_EXPRESSION_OPS {
476        return Err(DebugLocationExpressionError { operation_count });
477    }
478    Ok(())
479}
480
481impl Serializable for DebugLocationExpressionOp {
482    fn write_into<W: ByteWriter>(&self, target: &mut W) {
483        match self {
484            Self::ReadStack(position) => {
485                target.write_u8(0);
486                target.write_u8(*position);
487            },
488            Self::ReadMemory(address) => {
489                target.write_u8(1);
490                target.write_u32(*address);
491            },
492            Self::ReadLocal(offset) => {
493                target.write_u8(2);
494                target.write_bytes(&offset.to_le_bytes());
495            },
496            Self::ConstU64(value) => {
497                target.write_u8(3);
498                target.write_u64(*value);
499            },
500            Self::ConstI64(value) => {
501                target.write_u8(4);
502                target.write_bytes(&value.to_le_bytes());
503            },
504            Self::AddUnsigned(value) => {
505                target.write_u8(5);
506                target.write_u64(*value);
507            },
508            Self::Add => target.write_u8(6),
509            Self::Sub => target.write_u8(7),
510            Self::DerefBytes => target.write_u8(8),
511            Self::FrameBaseAddress { base, byte_offset } => {
512                target.write_u8(9);
513                write_debug_frame_base(*base, target);
514                target.write_bytes(&byte_offset.to_le_bytes());
515            },
516        }
517    }
518}
519
520impl Deserializable for DebugLocationExpressionOp {
521    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
522        match source.read_u8()? {
523            0 => Ok(Self::ReadStack(source.read_u8()?)),
524            1 => Ok(Self::ReadMemory(source.read_u32()?)),
525            2 => Ok(Self::ReadLocal(i16::from_le_bytes(source.read_array::<2>()?))),
526            3 => Ok(Self::ConstU64(source.read_u64()?)),
527            4 => Ok(Self::ConstI64(i64::from_le_bytes(source.read_array::<8>()?))),
528            5 => Ok(Self::AddUnsigned(source.read_u64()?)),
529            6 => Ok(Self::Add),
530            7 => Ok(Self::Sub),
531            8 => Ok(Self::DerefBytes),
532            9 => {
533                let base = read_debug_frame_base(source)?;
534                let byte_offset = i64::from_le_bytes(source.read_array::<8>()?);
535                Ok(Self::FrameBaseAddress { base, byte_offset })
536            },
537            tag => Err(DeserializationError::InvalidValue(format!(
538                "invalid DebugLocationExpressionOp tag: {tag}"
539            ))),
540        }
541    }
542
543    fn min_serialized_size() -> usize {
544        u8::min_serialized_size()
545    }
546}
547
548fn write_debug_frame_base<W: ByteWriter>(base: DebugFrameBase, target: &mut W) {
549    match base {
550        DebugFrameBase::Local(offset) => {
551            target.write_u8(0);
552            target.write_bytes(&offset.to_le_bytes());
553        },
554        DebugFrameBase::Memory(address) => {
555            target.write_u8(1);
556            target.write_u32(address);
557        },
558    }
559}
560
561fn read_debug_frame_base<R: ByteReader>(
562    source: &mut R,
563) -> Result<DebugFrameBase, DeserializationError> {
564    match source.read_u8()? {
565        0 => Ok(DebugFrameBase::Local(i16::from_le_bytes(source.read_array::<2>()?))),
566        1 => Ok(DebugFrameBase::Memory(source.read_u32()?)),
567        tag => Err(DeserializationError::InvalidValue(format!(
568            "invalid resolved debug frame-base tag: {tag}"
569        ))),
570    }
571}
572
573// ARBITRARY (proptest)
574// ===============================================================================================
575
576#[cfg(any(test, feature = "arbitrary"))]
577mod arbitrary {
578    use proptest::prelude::*;
579
580    use super::{
581        DebugFrameBase, DebugLocationExpression, DebugLocationExpressionOp, DebugVarLocation,
582    };
583    use crate::Felt;
584
585    impl Arbitrary for DebugFrameBase {
586        type Parameters = ();
587        type Strategy = BoxedStrategy<Self>;
588
589        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
590            prop_oneof![
591                any::<i16>().prop_map(Self::Local).boxed(),
592                any::<u32>().prop_map(Self::Memory).boxed(),
593            ]
594            .boxed()
595        }
596    }
597
598    impl Arbitrary for DebugLocationExpressionOp {
599        type Parameters = ();
600        type Strategy = BoxedStrategy<Self>;
601
602        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
603            // Every operation is any-valid on the wire; prop_oneof exercises all ten tags.
604            prop_oneof![
605                any::<u8>().prop_map(Self::ReadStack).boxed(),
606                any::<u32>().prop_map(Self::ReadMemory).boxed(),
607                any::<i16>().prop_map(Self::ReadLocal).boxed(),
608                any::<u64>().prop_map(Self::ConstU64).boxed(),
609                any::<i64>().prop_map(Self::ConstI64).boxed(),
610                any::<u64>().prop_map(Self::AddUnsigned).boxed(),
611                Just(Self::Add),
612                Just(Self::Sub),
613                Just(Self::DerefBytes),
614                (any::<DebugFrameBase>(), any::<i64>())
615                    .prop_map(|(base, byte_offset)| Self::FrameBaseAddress { base, byte_offset })
616                    .boxed(),
617            ]
618            .boxed()
619        }
620    }
621
622    impl Arbitrary for DebugLocationExpression {
623        type Parameters = ();
624        type Strategy = BoxedStrategy<Self>;
625
626        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
627            // The reader validates the op count against MAX_DEBUG_LOCATION_EXPRESSION_OPS (256).
628            // Small lengths keep shrinking cheap; the boundary branch hits the vint64 128
629            // transition and the accepted maximum (255/256) deliberately - a 256-op expression
630            // is only a few KB, so boundary coverage is nearly free.
631            let length = prop_oneof![
632                (0usize..=8).boxed(),
633                Just(127usize),
634                Just(128usize),
635                Just(255usize),
636                Just(256usize),
637            ];
638            length
639                .prop_flat_map(|len| {
640                    proptest::collection::vec(any::<DebugLocationExpressionOp>(), len)
641                })
642                .prop_map(|operations| Self { operations })
643                .boxed()
644        }
645    }
646
647    impl Arbitrary for DebugVarLocation {
648        type Parameters = ();
649        type Strategy = BoxedStrategy<Self>;
650
651        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
652            // Const is built from a real Felt: the wire carries its canonical u64, so arbitrary
653            // u64s >= the field order would be invalid input (the reader uses new_unchecked).
654            prop_oneof![
655                any::<u8>().prop_map(Self::Stack).boxed(),
656                any::<u32>().prop_map(Self::Memory).boxed(),
657                any::<Felt>().prop_map(Self::Const).boxed(),
658                any::<i16>().prop_map(Self::Local).boxed(),
659                Just(Self::Unavailable),
660                (any::<DebugFrameBase>(), any::<i64>())
661                    .prop_map(|(base, byte_offset)| Self::ResolvedFrameBase { base, byte_offset })
662                    .boxed(),
663                any::<DebugLocationExpression>().prop_map(Self::Expression).boxed(),
664            ]
665            .boxed()
666        }
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use alloc::{string::ToString, vec::Vec};
673
674    use miden_core::serde::{Deserializable, Serializable, SliceReader};
675    use miden_debug_types::{ByteIndex, Uri};
676
677    use super::*;
678
679    #[test]
680    fn debug_var_info_display_simple() {
681        let var = DebugVarInfo::new("x", DebugVarLocation::Stack(0));
682        assert_eq!(var.to_string(), "var.x = stack[0]");
683    }
684
685    #[test]
686    fn debug_var_info_display_with_arg() {
687        let mut var = DebugVarInfo::new("param", DebugVarLocation::Stack(2));
688        var.set_arg_index(1);
689        assert_eq!(var.to_string(), "var.param[arg1] = stack[2]");
690    }
691
692    #[test]
693    fn debug_var_info_display_with_location() {
694        let mut var = DebugVarInfo::new("y", DebugVarLocation::Memory(100));
695        var.set_location(Location::new(
696            Uri::new("test.rs"),
697            ByteIndex::from(0u32),
698            ByteIndex::from(5u32),
699        ));
700        assert_eq!(var.to_string(), "var.y = mem[100] [test.rs@0..5]");
701    }
702
703    #[test]
704    fn debug_var_location_display() {
705        assert_eq!(DebugVarLocation::Stack(0).to_string(), "stack[0]");
706        assert_eq!(DebugVarLocation::Memory(256).to_string(), "mem[256]");
707        assert_eq!(DebugVarLocation::Const(Felt::new_unchecked(42)).to_string(), "const(42)");
708        assert_eq!(DebugVarLocation::Local(-3).to_string(), "FMP-3");
709        assert_eq!(
710            DebugVarLocation::ResolvedFrameBase {
711                base: DebugFrameBase::Local(-3),
712                byte_offset: 12,
713            }
714            .to_string(),
715            "frame-base(FMP-3)+12"
716        );
717        assert_eq!(DebugVarLocation::Unavailable.to_string(), "unavailable");
718        assert_eq!(
719            DebugVarLocation::Expression(
720                DebugLocationExpression::new(vec![
721                    DebugLocationExpressionOp::FrameBaseAddress {
722                        base: DebugFrameBase::Local(-2),
723                        byte_offset: 4,
724                    },
725                    DebugLocationExpressionOp::AddUnsigned(8),
726                    DebugLocationExpressionOp::DerefBytes,
727                ])
728                .unwrap(),
729            )
730            .to_string(),
731            "expr([FrameBaseAddress { base: Local(-2), byte_offset: 4 }, AddUnsigned(8), DerefBytes])"
732        );
733    }
734
735    #[test]
736    fn debug_var_location_serialization_round_trip() {
737        let locations = [
738            DebugVarLocation::Stack(7),
739            DebugVarLocation::Memory(0xdead_beef),
740            DebugVarLocation::Const(Felt::new_unchecked(999)),
741            DebugVarLocation::Local(-3),
742            DebugVarLocation::Unavailable,
743            DebugVarLocation::ResolvedFrameBase {
744                base: DebugFrameBase::Local(-3),
745                byte_offset: 28,
746            },
747            DebugVarLocation::ResolvedFrameBase {
748                base: DebugFrameBase::Memory(100),
749                byte_offset: -16,
750            },
751            DebugVarLocation::Expression(
752                DebugLocationExpression::new(vec![
753                    DebugLocationExpressionOp::ReadStack(2),
754                    DebugLocationExpressionOp::ConstI64(-4),
755                    DebugLocationExpressionOp::Add,
756                    DebugLocationExpressionOp::DerefBytes,
757                ])
758                .unwrap(),
759            ),
760        ];
761
762        for loc in &locations {
763            let mut bytes = Vec::new();
764            loc.write_into(&mut bytes);
765            let mut reader = SliceReader::new(&bytes);
766            let deser = DebugVarLocation::read_from(&mut reader).unwrap();
767            assert_eq!(&deser, loc);
768        }
769    }
770
771    #[test]
772    fn debug_location_expression_wire_encoding_is_stable() {
773        let expression = DebugLocationExpression::new(vec![
774            DebugLocationExpressionOp::ReadStack(0x2a),
775            DebugLocationExpressionOp::ReadMemory(0x1234_5678),
776            DebugLocationExpressionOp::ReadLocal(-2),
777            DebugLocationExpressionOp::ConstU64(0x0102_0304_0506_0708),
778            DebugLocationExpressionOp::ConstI64(-2),
779            DebugLocationExpressionOp::AddUnsigned(0x1112_1314_1516_1718),
780            DebugLocationExpressionOp::Add,
781            DebugLocationExpressionOp::Sub,
782            DebugLocationExpressionOp::DerefBytes,
783            DebugLocationExpressionOp::FrameBaseAddress {
784                base: DebugFrameBase::Local(-4),
785                byte_offset: 0x0102_0304_0506_0708,
786            },
787            DebugLocationExpressionOp::FrameBaseAddress {
788                base: DebugFrameBase::Memory(0xa1b2_c3d4),
789                byte_offset: -3,
790            },
791        ])
792        .unwrap();
793        let expected = vec![
794            0x17, 0x00, 0x2a, 0x01, 0x78, 0x56, 0x34, 0x12, 0x02, 0xfe, 0xff, 0x03, 0x08, 0x07,
795            0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x04, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
796            0xff, 0x05, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x06, 0x07, 0x08, 0x09,
797            0x00, 0xfc, 0xff, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x09, 0x01, 0xd4,
798            0xc3, 0xb2, 0xa1, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
799        ];
800
801        let mut bytes = Vec::new();
802        expression.write_into(&mut bytes);
803        assert_eq!(bytes, expected);
804
805        let mut reader = SliceReader::new(&expected);
806        assert_eq!(DebugLocationExpression::read_from(&mut reader).unwrap(), expression);
807    }
808
809    #[test]
810    fn debug_var_location_min_serialized_size_matches_shortest_variant() {
811        let location = DebugVarLocation::Unavailable;
812        let min_serialized_size = DebugVarLocation::min_serialized_size();
813        let mut bytes = Vec::new();
814        location.write_into(&mut bytes);
815
816        assert_eq!(min_serialized_size, 1);
817        assert_eq!(bytes.len(), min_serialized_size);
818    }
819
820    #[test]
821    fn debug_location_expression_rejects_unknown_operation() {
822        let mut bytes = Vec::new();
823        bytes.write_usize(1);
824        bytes.write_u8(u8::MAX);
825
826        let mut reader = SliceReader::new(&bytes);
827        let err = DebugLocationExpression::read_from(&mut reader).unwrap_err();
828        let DeserializationError::InvalidValue(message) = err else {
829            panic!("expected InvalidValue error");
830        };
831        assert!(message.contains("invalid DebugLocationExpressionOp tag"));
832    }
833
834    #[test]
835    fn debug_location_expression_caps_operation_count_before_allocation() {
836        let count = MAX_DEBUG_LOCATION_EXPRESSION_OPS + 1;
837        let mut bytes = Vec::new();
838        bytes.write_usize(count);
839        bytes.resize(bytes.len() + count, 0);
840
841        let mut reader = SliceReader::new(&bytes);
842        let err = DebugLocationExpression::read_from(&mut reader).unwrap_err();
843        let DeserializationError::InvalidValue(message) = err else {
844            panic!("expected InvalidValue error");
845        };
846        assert!(message.contains("at most 256"));
847    }
848
849    #[test]
850    fn debug_location_expression_constructor_rejects_oversized_input() {
851        let operations =
852            vec![DebugLocationExpressionOp::Add; MAX_DEBUG_LOCATION_EXPRESSION_OPS + 1];
853        let error = DebugLocationExpression::new(operations).unwrap_err();
854
855        assert_eq!(error.operation_count(), MAX_DEBUG_LOCATION_EXPRESSION_OPS + 1);
856    }
857
858    #[test]
859    fn debug_var_info_set_value_location() {
860        let mut var = DebugVarInfo::new("x", DebugVarLocation::Stack(0));
861        var.set_value_location(DebugVarLocation::ResolvedFrameBase {
862            base: DebugFrameBase::Local(-2),
863            byte_offset: 12,
864        });
865        assert_eq!(
866            var.value_location(),
867            &DebugVarLocation::ResolvedFrameBase {
868                base: DebugFrameBase::Local(-2),
869                byte_offset: 12,
870            }
871        );
872    }
873
874    #[cfg(feature = "serde")]
875    #[test]
876    fn serde_round_trips_location_expressions() {
877        let expression = DebugLocationExpression::new(vec![
878            DebugLocationExpressionOp::ReadLocal(-2),
879            DebugLocationExpressionOp::DerefBytes,
880        ])
881        .unwrap();
882        let json = serde_json::to_string(&expression).unwrap();
883
884        assert_eq!(serde_json::from_str::<DebugLocationExpression>(&json).unwrap(), expression);
885    }
886
887    #[cfg(feature = "serde")]
888    #[test]
889    fn serde_rejects_oversized_location_expressions() {
890        let expression = DebugLocationExpression {
891            operations: vec![DebugLocationExpressionOp::Add; MAX_DEBUG_LOCATION_EXPRESSION_OPS + 1],
892        };
893        let json = serde_json::to_string(&expression).unwrap();
894        let error = serde_json::from_str::<DebugLocationExpression>(&json).unwrap_err();
895
896        assert!(error.to_string().contains("at most 256"));
897    }
898}