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