Skip to main content

miden_debug_engine/debug/
variables.rs

1use std::{cell::RefCell, collections::BTreeMap, rc::Rc, sync::Arc};
2
3use miden_assembly::ast::{DebugFrameBase, DebugLocationExpression, DebugLocationExpressionOp};
4use miden_assembly_syntax::ast::{DebugVarInfo, DebugVarLocation, types::Type};
5use miden_core::Felt;
6use miden_processor::trace::RowIndex;
7
8type DebugVarEvents = Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>;
9type CapturedDebugValues = Rc<RefCell<BTreeMap<(RowIndex, Arc<str>), Vec<Felt>>>>;
10
11/// A snapshot of a debug variable at a specific clock cycle.
12#[derive(Debug, Clone)]
13pub struct DebugVarSnapshot {
14    /// The clock cycle when this variable info was recorded.
15    pub clk: RowIndex,
16    /// The debug variable information.
17    pub info: DebugVarInfo,
18}
19
20/// Tracks debug variable snapshots, mapping variable names to their most recent location info.
21pub struct DebugVarTracker {
22    /// All debug variable events recorded during execution, keyed by clock cycle.
23    events: DebugVarEvents,
24    /// Canonical values frozen at transient debug decorators.
25    captured_values: CapturedDebugValues,
26    /// Current view of variables - maps variable name to most recent info.
27    current_vars: BTreeMap<String, DebugVarSnapshot>,
28    /// Canonical values captured for currently visible transient variables.
29    current_captured_values: BTreeMap<String, Vec<Felt>>,
30    /// The clock cycle up to which we've processed events.
31    processed_up_to: RowIndex,
32}
33
34impl DebugVarTracker {
35    /// Create a new tracker using the given shared event store.
36    pub fn new(events: DebugVarEvents) -> Self {
37        Self {
38            events,
39            captured_values: Rc::new(Default::default()),
40            current_vars: BTreeMap::new(),
41            current_captured_values: BTreeMap::new(),
42            processed_up_to: RowIndex::from(0),
43        }
44    }
45
46    /// Record debug variable events at the given clock cycle.
47    pub fn record_events(&self, clk: RowIndex, infos: Vec<DebugVarInfo>) {
48        if !infos.is_empty() {
49            self.events.borrow_mut().entry(clk).or_default().extend(infos);
50        }
51    }
52
53    /// Record debug variable events after freezing transient stack-backed values.
54    pub fn record_events_with_stack(
55        &self,
56        clk: RowIndex,
57        mut infos: Vec<DebugVarInfo>,
58        stack: &[Felt],
59    ) {
60        let names = infos.iter().map(|info| info.name().clone()).collect::<Vec<_>>();
61        let captured_values = snapshot_transient_debug_values(&mut infos, stack);
62        {
63            let mut stored_values = self.captured_values.borrow_mut();
64            for name in names {
65                stored_values.remove(&(clk, name));
66            }
67            stored_values
68                .extend(captured_values.into_iter().map(|(name, values)| ((clk, name), values)));
69        }
70        self.record_events(clk, infos);
71    }
72
73    /// Process all events up to and including `clk`, updating current variable state.
74    pub fn update_to_cycle(&mut self, clk: RowIndex) {
75        let events = self.events.borrow();
76        let captured_values = self.captured_values.borrow();
77
78        // Process events from processed_up_to to clk
79        for (event_clk, var_infos) in events.range(self.processed_up_to..=clk) {
80            for info in var_infos {
81                if is_debug_var_kill(info) {
82                    self.current_vars.remove(info.name().as_ref());
83                    self.current_captured_values.remove(info.name().as_ref());
84                    continue;
85                }
86                let snapshot = DebugVarSnapshot {
87                    clk: *event_clk,
88                    info: info.clone(),
89                };
90                let name = info.name().to_string();
91                match captured_values.get(&(*event_clk, info.name().clone())) {
92                    Some(values) => {
93                        self.current_captured_values.insert(name.clone(), values.clone());
94                    }
95                    None => {
96                        self.current_captured_values.remove(&name);
97                    }
98                }
99                self.current_vars.insert(name, snapshot);
100            }
101        }
102
103        self.processed_up_to = clk;
104    }
105
106    /// Reset the tracker to the beginning of execution.
107    pub fn reset(&mut self) {
108        self.current_vars.clear();
109        self.current_captured_values.clear();
110        self.processed_up_to = RowIndex::from(0);
111    }
112
113    /// Get all currently visible variables.
114    pub fn current_variables(&self) -> impl Iterator<Item = &DebugVarSnapshot> {
115        self.current_vars.values()
116    }
117
118    /// Get a specific variable by name.
119    pub fn get_variable(&self, name: &str) -> Option<&DebugVarSnapshot> {
120        self.current_vars.get(name)
121    }
122
123    /// Returns canonical values captured at the variable's latest debug decorator.
124    pub fn captured_values(&self, name: &str) -> Option<&[Felt]> {
125        self.current_captured_values.get(name).map(Vec::as_slice)
126    }
127
128    /// Get the number of tracked variables.
129    pub fn variable_count(&self) -> usize {
130        self.current_vars.len()
131    }
132
133    /// Check if there are any tracked variables.
134    pub fn has_variables(&self) -> bool {
135        !self.current_vars.is_empty()
136    }
137}
138
139/// Snapshot transient debug locations at the decorator point.
140///
141/// Stack locations are only meaningful at the debug decorator itself. Keeping them live and
142/// resolving them against a later VM stack can report unrelated values. Memory, local, and
143/// frame-base declarations describe live storage and must be resolved against the current VM state
144/// when the user inspects variables.
145pub fn snapshot_transient_debug_values(
146    infos: &mut [DebugVarInfo],
147    stack: &[Felt],
148) -> BTreeMap<Arc<str>, Vec<Felt>> {
149    let mut captured_values = BTreeMap::new();
150
151    for info in infos {
152        captured_values.remove(info.name().as_ref());
153        match info.value_location() {
154            DebugVarLocation::Stack(position) => {
155                let count = info.ty().and_then(super::abi_types::value_felt_count).unwrap_or(1);
156                let start = *position as usize;
157                let values = start
158                    .checked_add(count)
159                    .and_then(|end| stack.get(start..end))
160                    .map(<[Felt]>::to_vec);
161                let location = values
162                    .as_ref()
163                    .and_then(|values| values.first().copied())
164                    .or_else(|| (count == 0).then(|| Felt::from_u32(0)))
165                    .map(DebugVarLocation::Const)
166                    .unwrap_or(DebugVarLocation::Unavailable);
167                if let Some(values) = values {
168                    captured_values.insert(info.name().clone(), values);
169                }
170                info.set_value_location(location);
171            }
172            DebugVarLocation::Expression(expression) => {
173                let operations = expression
174                    .operations()
175                    .iter()
176                    .map(|operation| match operation {
177                        DebugLocationExpressionOp::ReadStack(position) => stack
178                            .get(*position as usize)
179                            .map(|value| {
180                                DebugLocationExpressionOp::ConstU64(value.as_canonical_u64())
181                            })
182                            .ok_or(()),
183                        operation => Ok(*operation),
184                    })
185                    .collect::<Result<Vec<_>, _>>();
186                let location = operations
187                    .and_then(|ops| DebugLocationExpression::new(ops).map_err(|_| ()))
188                    .map(DebugVarLocation::Expression)
189                    .unwrap_or(DebugVarLocation::Unavailable);
190                info.set_value_location(location);
191            }
192            _ => {}
193        }
194    }
195
196    captured_values
197}
198
199fn is_debug_var_kill(info: &DebugVarInfo) -> bool {
200    matches!(info.value_location(), DebugVarLocation::Unavailable)
201}
202
203/// Resolve a debug variable's value given its location and the current VM state.
204pub fn resolve_variable_value(
205    location: &DebugVarLocation,
206    stack: &[Felt],
207    get_memory: impl Fn(u32) -> Option<Felt>,
208    get_local: impl Fn(i16) -> Option<Felt>,
209) -> Option<Felt> {
210    resolve_variable_values(location, 1, stack, get_memory, get_local)?.pop()
211}
212
213/// Resolve one or more consecutive felts for a debug variable.
214pub fn resolve_variable_values(
215    location: &DebugVarLocation,
216    count: usize,
217    stack: &[Felt],
218    get_memory: impl Fn(u32) -> Option<Felt>,
219    get_local: impl Fn(i16) -> Option<Felt>,
220) -> Option<Vec<Felt>> {
221    if count == 0 {
222        return Some(Vec::new());
223    }
224
225    match location {
226        DebugVarLocation::Stack(pos) => {
227            let start = *pos as usize;
228            let end = start.checked_add(count)?;
229            Some(stack.get(start..end)?.to_vec())
230        }
231        DebugVarLocation::Memory(addr) => resolve_consecutive_memory(*addr, count, &get_memory),
232        DebugVarLocation::Const(felt) => (count == 1).then_some(vec![*felt]),
233        DebugVarLocation::Local(offset) => {
234            let mut values = Vec::with_capacity(count);
235            for index in 0..count {
236                let index = i16::try_from(index).ok()?;
237                values.push(get_local(offset.checked_add(index)?)?);
238            }
239            Some(values)
240        }
241        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => {
242            resolve_frame_base_values(*base, *byte_offset, count, &get_memory, &get_local)
243        }
244        DebugVarLocation::Expression(expression) => {
245            match resolve_expression(expression.operations(), stack, &get_memory, &get_local)? {
246                ResolvedExpression::Scalar(value) => {
247                    (count == 1).then(|| integer_to_felt(value)).flatten().map(|value| vec![value])
248                }
249                ResolvedExpression::ByteAddress(address) => {
250                    let element_address = u32::try_from(address / 4).ok()?;
251                    resolve_consecutive_memory(element_address, count, &get_memory)
252                }
253            }
254        }
255        DebugVarLocation::Unavailable => None,
256    }
257}
258
259/// Resolve a typed debug variable into the canonical ABI felts consumed by the typed decoder.
260///
261/// Simple Miden locations already contain canonical stack values. Frame-base locations and a
262/// terminal [`DebugLocationExpressionOp::DerefBytes`] instead identify packed Rust memory, which
263/// must be read at byte granularity and lifted according to `ty` before it can be decoded.
264pub fn resolve_typed_variable_values(
265    location: &DebugVarLocation,
266    ty: &Type,
267    count: usize,
268    stack: &[Felt],
269    get_memory: impl Fn(u32) -> Option<Felt>,
270    get_local: impl Fn(i16) -> Option<Felt>,
271) -> Option<Vec<Felt>> {
272    match location {
273        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => {
274            let byte_address =
275                resolve_frame_base_address(*base, *byte_offset, &get_memory, &get_local)?;
276            resolve_typed_memory_value(ty, byte_address, count, &get_memory)
277        }
278        DebugVarLocation::Expression(expression) => {
279            match resolve_expression(expression.operations(), stack, &get_memory, &get_local)? {
280                ResolvedExpression::ByteAddress(byte_address) => {
281                    resolve_typed_memory_value(ty, byte_address, count, &get_memory)
282                }
283                ResolvedExpression::Scalar(value) => {
284                    (count == 1).then(|| integer_to_felt(value)).flatten().map(|value| vec![value])
285                }
286            }
287        }
288        _ => resolve_variable_values(location, count, stack, get_memory, get_local),
289    }
290}
291
292fn resolve_consecutive_memory(
293    start_addr: u32,
294    count: usize,
295    get_memory: &impl Fn(u32) -> Option<Felt>,
296) -> Option<Vec<Felt>> {
297    let mut values = Vec::with_capacity(count);
298    for index in 0..count {
299        let addr = start_addr.checked_add(u32::try_from(index).ok()?)?;
300        values.push(get_memory(addr)?);
301    }
302    Some(values)
303}
304
305#[derive(Clone, Copy, Debug, Eq, PartialEq)]
306enum ResolvedExpression {
307    Scalar(i128),
308    ByteAddress(u64),
309}
310
311fn resolve_expression(
312    ops: &[DebugLocationExpressionOp],
313    stack: &[Felt],
314    get_memory: &impl Fn(u32) -> Option<Felt>,
315    get_local: &impl Fn(i16) -> Option<Felt>,
316) -> Option<ResolvedExpression> {
317    let mut values = Vec::<i128>::new();
318
319    for (index, op) in ops.iter().enumerate() {
320        match op {
321            DebugLocationExpressionOp::ReadStack(index) => {
322                values.push(i128::from(stack.get(*index as usize)?.as_canonical_u64()));
323            }
324            DebugLocationExpressionOp::ReadMemory(index) => {
325                values.push(i128::from(get_memory(*index)?.as_canonical_u64()));
326            }
327            DebugLocationExpressionOp::ReadLocal(index) => {
328                values.push(i128::from(get_local(*index)?.as_canonical_u64()));
329            }
330            DebugLocationExpressionOp::ConstU64(value) => {
331                values.push(i128::from(*value));
332            }
333            DebugLocationExpressionOp::ConstI64(value) => {
334                values.push(i128::from(*value));
335            }
336            DebugLocationExpressionOp::AddUnsigned(value) => {
337                let lhs = values.pop()?;
338                values.push(lhs.checked_add(i128::from(*value))?);
339            }
340            DebugLocationExpressionOp::Add => {
341                let rhs = values.pop()?;
342                let lhs = values.pop()?;
343                values.push(lhs.checked_add(rhs)?);
344            }
345            DebugLocationExpressionOp::Sub => {
346                let rhs = values.pop()?;
347                let lhs = values.pop()?;
348                values.push(lhs.checked_sub(rhs)?);
349            }
350            DebugLocationExpressionOp::DerefBytes => {
351                let byte_address = u64::try_from(values.pop()?).ok()?;
352                if index + 1 == ops.len() {
353                    return Some(ResolvedExpression::ByteAddress(byte_address));
354                }
355                let element_address = u32::try_from(byte_address / 4).ok()?;
356                values.push(i128::from(get_memory(element_address)?.as_canonical_u64()));
357            }
358            DebugLocationExpressionOp::FrameBaseAddress { base, byte_offset } => {
359                values.push(i128::from(resolve_frame_base_address(
360                    *base,
361                    *byte_offset,
362                    get_memory,
363                    get_local,
364                )?));
365            }
366        }
367    }
368
369    values.pop().map(ResolvedExpression::Scalar)
370}
371
372fn resolve_frame_base_values(
373    base: DebugFrameBase,
374    byte_offset: i64,
375    count: usize,
376    get_memory: &impl Fn(u32) -> Option<Felt>,
377    get_local: &impl Fn(i16) -> Option<Felt>,
378) -> Option<Vec<Felt>> {
379    let byte_address = resolve_frame_base_address(base, byte_offset, get_memory, get_local)?;
380    resolve_byte_address_values(byte_address, count, get_memory)
381}
382
383fn resolve_frame_base_address(
384    base: DebugFrameBase,
385    byte_offset: i64,
386    get_memory: &impl Fn(u32) -> Option<Felt>,
387    get_local: &impl Fn(i16) -> Option<Felt>,
388) -> Option<u64> {
389    let base = match base {
390        DebugFrameBase::Local(offset) => get_local(offset)?,
391        DebugFrameBase::Memory(address) => get_memory(address)?,
392    };
393    base.as_canonical_u64().checked_add_signed(byte_offset)
394}
395
396fn resolve_byte_address_values(
397    byte_address: u64,
398    count: usize,
399    get_memory: &impl Fn(u32) -> Option<Felt>,
400) -> Option<Vec<Felt>> {
401    if !byte_address.is_multiple_of(4) {
402        return None;
403    }
404    let element_address = u32::try_from(byte_address / 4).ok()?;
405    resolve_consecutive_memory(element_address, count, get_memory)
406}
407
408fn integer_to_felt(value: i128) -> Option<Felt> {
409    Felt::new(u64::try_from(value).ok()?).ok()
410}
411
412fn read_memory_bytes(
413    byte_address: u64,
414    size: usize,
415    get_memory: &impl Fn(u32) -> Option<Felt>,
416) -> Option<Vec<u8>> {
417    if size == 0 {
418        return Some(Vec::new());
419    }
420
421    let element_address = u32::try_from(byte_address / 4).ok()?;
422    let byte_offset = usize::try_from(byte_address % 4).ok()?;
423    let end = byte_offset.checked_add(size)?;
424    let element_count = end.div_ceil(4);
425    let mut bytes = Vec::with_capacity(element_count.checked_mul(4)?);
426
427    for index in 0..element_count {
428        let address = element_address.checked_add(u32::try_from(index).ok()?)?;
429        let value = get_memory(address)?.as_canonical_u64() as u32;
430        bytes.extend_from_slice(&value.to_le_bytes());
431    }
432
433    Some(bytes.get(byte_offset..end)?.to_vec())
434}
435
436fn resolve_typed_memory_value(
437    ty: &Type,
438    byte_address: u64,
439    expected_count: usize,
440    get_memory: &impl Fn(u32) -> Option<Felt>,
441) -> Option<Vec<Felt>> {
442    let mut values = Vec::with_capacity(expected_count);
443    append_typed_memory_value(ty, byte_address, get_memory, &mut values)?;
444    (values.len() == expected_count).then_some(values)
445}
446
447fn append_typed_memory_value(
448    ty: &Type,
449    byte_address: u64,
450    get_memory: &impl Fn(u32) -> Option<Felt>,
451    values: &mut Vec<Felt>,
452) -> Option<()> {
453    match ty {
454        Type::Felt => {
455            if !byte_address.is_multiple_of(4) {
456                return None;
457            }
458            values.push(get_memory(u32::try_from(byte_address / 4).ok()?)?);
459        }
460        Type::I1 => {
461            let byte = *read_memory_bytes(byte_address, 1, get_memory)?.first()?;
462            values.push(Felt::from_u32(u32::from(byte & 1)));
463        }
464        Type::I8 | Type::I16 | Type::I32 | Type::I64 | Type::I128 => {
465            let value = read_unsigned_integer(ty, byte_address, get_memory)?;
466            let bit_width = u32::try_from(ty.size_in_bits()).ok()?;
467            let shift = 128_u32.checked_sub(bit_width)?;
468            let signed = ((value << shift) as i128) >> shift;
469            let slot_bits = u32::try_from(ty.size_in_felts().checked_mul(32)?).ok()?;
470            let canonical = (signed as u128) & low_bits_mask(slot_bits);
471            append_u128_limbs(canonical, ty.size_in_felts(), values);
472        }
473        Type::U8 | Type::U16 | Type::U32 | Type::U64 | Type::U128 | Type::F64 => {
474            let value = read_unsigned_integer(ty, byte_address, get_memory)?;
475            append_u128_limbs(value, ty.size_in_felts(), values);
476        }
477        Type::Struct(struct_ty) => {
478            for field in struct_ty.get().fields() {
479                let field_address = byte_address.checked_add(u64::from(field.offset))?;
480                append_typed_memory_value(&field.ty, field_address, get_memory, values)?;
481            }
482        }
483        Type::Array(array_ty) => {
484            let element_size = array_ty.ty.size_in_bytes();
485            let alignment = array_ty.ty.min_alignment();
486            let stride = align_up(element_size, alignment)?;
487            for index in 0..array_ty.len {
488                let offset = index.checked_mul(stride)?;
489                let element_address = byte_address.checked_add(u64::try_from(offset).ok()?)?;
490                append_typed_memory_value(&array_ty.ty, element_address, get_memory, values)?;
491            }
492        }
493        Type::U256
494        | Type::List(_)
495        | Type::Ptr(_)
496        | Type::Function(_)
497        | Type::Enum(_)
498        | Type::Unknown
499        | Type::Never
500        | Type::Variadic => return None,
501    }
502
503    Some(())
504}
505
506fn read_unsigned_integer(
507    ty: &Type,
508    byte_address: u64,
509    get_memory: &impl Fn(u32) -> Option<Felt>,
510) -> Option<u128> {
511    let bytes = read_memory_bytes(byte_address, ty.size_in_bytes(), get_memory)?;
512    let mut value = 0_u128;
513    for (index, byte) in bytes.into_iter().enumerate() {
514        value |= u128::from(byte) << index.checked_mul(8)?;
515    }
516    Some(value)
517}
518
519fn append_u128_limbs(value: u128, count: usize, values: &mut Vec<Felt>) {
520    values.extend((0..count).map(|index| Felt::from_u32((value >> (index * 32)) as u32)));
521}
522
523fn low_bits_mask(bits: u32) -> u128 {
524    if bits == 128 {
525        u128::MAX
526    } else {
527        (1_u128 << bits) - 1
528    }
529}
530
531fn align_up(size: usize, alignment: usize) -> Option<usize> {
532    if size == 0 {
533        return Some(0);
534    }
535    size.checked_add(alignment.checked_sub(1)?)?
536        .checked_div(alignment)?
537        .checked_mul(alignment)
538}
539
540#[cfg(test)]
541mod tests {
542    use std::sync::Arc;
543
544    use miden_assembly_syntax::ast::types::{StructType, TypeRepr};
545
546    use super::*;
547
548    #[test]
549    fn test_tracker_basic() {
550        let events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
551            Rc::new(Default::default());
552
553        // Add some events
554        {
555            let mut events_mut = events.borrow_mut();
556            events_mut.insert(
557                RowIndex::from(1),
558                vec![DebugVarInfo::new("x", DebugVarLocation::Stack(0))],
559            );
560            events_mut.insert(
561                RowIndex::from(5),
562                vec![DebugVarInfo::new("y", DebugVarLocation::Stack(1))],
563            );
564        }
565
566        let mut tracker = DebugVarTracker::new(events);
567
568        // Initially no variables
569        assert_eq!(tracker.variable_count(), 0);
570
571        // Process up to cycle 3
572        tracker.update_to_cycle(RowIndex::from(3));
573        assert_eq!(tracker.variable_count(), 1);
574        assert!(tracker.get_variable("x").is_some());
575        assert!(tracker.get_variable("y").is_none());
576
577        // Process up to cycle 10
578        tracker.update_to_cycle(RowIndex::from(10));
579        assert_eq!(tracker.variable_count(), 2);
580        assert!(tracker.get_variable("x").is_some());
581        assert!(tracker.get_variable("y").is_some());
582
583        // Verify resolve_variable_value resolves stack values
584        let x_snapshot = tracker.get_variable("x").unwrap();
585        let value = resolve_variable_value(
586            x_snapshot.info.value_location(),
587            &[Felt::new(42).expect("value exceeds field modulus")],
588            |_| None,
589            |_| None,
590        );
591        assert_eq!(value, Some(Felt::new(42).expect("value exceeds field modulus")));
592    }
593
594    #[test]
595    fn snapshots_transient_stack_locations_as_constants() {
596        let mut infos = vec![
597            DebugVarInfo::new("a", DebugVarLocation::Stack(0)),
598            DebugVarInfo::new("b", DebugVarLocation::Local(-1)),
599            DebugVarInfo::new(
600                "c",
601                DebugVarLocation::Expression(
602                    DebugLocationExpression::new(vec![
603                        DebugLocationExpressionOp::ReadStack(0),
604                        DebugLocationExpressionOp::AddUnsigned(3),
605                    ])
606                    .unwrap(),
607                ),
608            ),
609            DebugVarInfo::new(
610                "missing",
611                DebugVarLocation::Expression(
612                    DebugLocationExpression::new(vec![DebugLocationExpressionOp::ReadStack(1)])
613                        .unwrap(),
614                ),
615            ),
616        ];
617
618        let captured_values = snapshot_transient_debug_values(
619            &mut infos,
620            &[Felt::new(7).expect("value exceeds field modulus")],
621        );
622
623        assert_eq!(
624            infos[0].value_location(),
625            &DebugVarLocation::Const(Felt::new(7).expect("value exceeds field modulus"))
626        );
627        assert_eq!(infos[1].value_location(), &DebugVarLocation::Local(-1));
628        assert_eq!(
629            infos[2].value_location(),
630            &DebugVarLocation::Expression(
631                DebugLocationExpression::new(vec![
632                    DebugLocationExpressionOp::ConstU64(7),
633                    DebugLocationExpressionOp::AddUnsigned(3)
634                ])
635                .unwrap()
636            )
637        );
638        assert_eq!(infos[3].value_location(), &DebugVarLocation::Unavailable);
639        assert_eq!(captured_values.get("a" as &str), Some(&vec![Felt::from_u32(7)]));
640    }
641
642    #[test]
643    fn snapshots_all_felts_for_typed_stack_locations() {
644        let events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
645            Rc::new(Default::default());
646        let mut tracker = DebugVarTracker::new(events);
647        let mut info = DebugVarInfo::new("wide", DebugVarLocation::Stack(0));
648        info.set_ty(Type::U64, None);
649
650        tracker.record_events_with_stack(
651            RowIndex::from(1),
652            vec![info],
653            &[Felt::from_u32(7), Felt::from_u32(1)],
654        );
655        tracker.update_to_cycle(RowIndex::from(1));
656
657        let snapshot = tracker.get_variable("wide").unwrap();
658        assert_eq!(snapshot.info.value_location(), &DebugVarLocation::Const(Felt::from_u32(7)));
659        assert_eq!(
660            tracker.captured_values("wide"),
661            Some([Felt::from_u32(7), Felt::from_u32(1)].as_slice())
662        );
663    }
664
665    #[test]
666    fn resolves_explicit_frame_bases() {
667        let expected = Felt::new(4_294_967_303).unwrap();
668        for (base, memory_base) in
669            [(DebugFrameBase::Local(-7), false), (DebugFrameBase::Memory(9), true)]
670        {
671            let value = resolve_variable_value(
672                &DebugVarLocation::ResolvedFrameBase {
673                    base,
674                    byte_offset: 28,
675                },
676                &[],
677                |address| {
678                    if memory_base && address == 9 {
679                        Some(Felt::new(1_048_528).unwrap())
680                    } else if address == 262_139 {
681                        Some(expected)
682                    } else {
683                        None
684                    }
685                },
686                |offset| (!memory_base && offset == -7).then_some(Felt::new(1_048_528).unwrap()),
687            );
688            assert_eq!(value, Some(expected));
689        }
690    }
691
692    #[test]
693    fn resolves_structured_location_expressions() {
694        let expression = DebugLocationExpression::new(vec![
695            DebugLocationExpressionOp::FrameBaseAddress {
696                base: DebugFrameBase::Local(-2),
697                byte_offset: 4,
698            },
699            DebugLocationExpressionOp::AddUnsigned(8),
700            DebugLocationExpressionOp::DerefBytes,
701        ])
702        .unwrap();
703        let value = resolve_variable_value(
704            &DebugVarLocation::Expression(expression),
705            &[],
706            |address| (address == 27).then_some(Felt::new(13).unwrap()),
707            |offset| (offset == -2).then_some(Felt::new(96).unwrap()),
708        );
709
710        assert_eq!(value, Some(Felt::new(13).unwrap()));
711    }
712
713    #[test]
714    fn resolves_untyped_byte_dereferences_as_memory_elements() {
715        let expression = DebugLocationExpression::new(vec![
716            DebugLocationExpressionOp::ConstU64(1),
717            DebugLocationExpressionOp::DerefBytes,
718        ])
719        .unwrap();
720        let expected = Felt::new(4_294_967_303).unwrap();
721        let value = resolve_variable_value(
722            &DebugVarLocation::Expression(expression),
723            &[],
724            |address| (address == 0).then_some(expected),
725            |_| None,
726        );
727
728        assert_eq!(value, Some(expected));
729    }
730
731    #[test]
732    fn preserves_whole_felts_after_nonterminal_byte_dereferences() {
733        let expression = DebugLocationExpression::new(vec![
734            DebugLocationExpressionOp::ConstU64(1),
735            DebugLocationExpressionOp::DerefBytes,
736            DebugLocationExpressionOp::AddUnsigned(1),
737        ])
738        .unwrap();
739        let value = resolve_variable_value(
740            &DebugVarLocation::Expression(expression),
741            &[],
742            |address| (address == 0).then(|| Felt::new(4_294_967_303).unwrap()),
743            |_| None,
744        );
745
746        assert_eq!(value, Some(Felt::new(4_294_967_304).unwrap()));
747    }
748
749    #[test]
750    fn resolves_wide_typed_values_from_unaligned_byte_addresses() {
751        let expression = DebugLocationExpression::new(vec![
752            DebugLocationExpressionOp::ConstU64(3),
753            DebugLocationExpressionOp::DerefBytes,
754        ])
755        .unwrap();
756        let values = resolve_typed_variable_values(
757            &DebugVarLocation::Expression(expression),
758            &Type::U64,
759            2,
760            &[],
761            |address| match address {
762                0 => Some(Felt::from_u32(0x3322_11aa)),
763                1 => Some(Felt::from_u32(0x7766_5544)),
764                2 => Some(Felt::from_u32(0xbbaa_9988)),
765                _ => None,
766            },
767            |_| None,
768        );
769
770        assert_eq!(values, Some(vec![Felt::from_u32(0x6655_4433), Felt::from_u32(0xaa99_8877)]));
771    }
772
773    #[test]
774    fn lifts_packed_struct_fields_into_canonical_abi_felts() {
775        let packed = Type::from(StructType::new_with_repr(
776            TypeRepr::packed(1),
777            [(Arc::from("tiny"), Type::U8), (Arc::from("half"), Type::U16)],
778        ));
779        let values = resolve_typed_variable_values(
780            &DebugVarLocation::ResolvedFrameBase {
781                base: DebugFrameBase::Local(-1),
782                byte_offset: 0,
783            },
784            &packed,
785            2,
786            &[],
787            |address| (address == 0).then_some(Felt::from_u32(0x3322_11aa)),
788            |offset| (offset == -1).then_some(Felt::from_u32(1)),
789        );
790
791        assert_eq!(values, Some(vec![Felt::from_u32(0x11), Felt::from_u32(0x3322)]));
792        assert_eq!(
793            crate::debug::format_value(&packed, |count| {
794                resolve_typed_variable_values(
795                    &DebugVarLocation::ResolvedFrameBase {
796                        base: DebugFrameBase::Local(-1),
797                        byte_offset: 0,
798                    },
799                    &packed,
800                    count,
801                    &[],
802                    |address| (address == 0).then_some(Felt::from_u32(0x3322_11aa)),
803                    |offset| (offset == -1).then_some(Felt::from_u32(1)),
804                )
805            })
806            .as_deref(),
807            Some("{ tiny: 17, half: 13090 }")
808        );
809    }
810
811    #[test]
812    fn sign_extends_typed_integers_to_their_canonical_slots() {
813        let values = resolve_typed_variable_values(
814            &DebugVarLocation::ResolvedFrameBase {
815                base: DebugFrameBase::Local(-1),
816                byte_offset: 0,
817            },
818            &Type::I8,
819            1,
820            &[],
821            |address| (address == 0).then_some(Felt::from_u32(0x0000_ff00)),
822            |offset| (offset == -1).then_some(Felt::from_u32(1)),
823        );
824
825        assert_eq!(values, Some(vec![Felt::from_u32(u32::MAX)]));
826    }
827
828    #[test]
829    fn rejects_invalid_location_expression_results() {
830        for expression in [
831            DebugLocationExpression::new(vec![DebugLocationExpressionOp::ConstI64(-1)]).unwrap(),
832            DebugLocationExpression::new(vec![
833                DebugLocationExpressionOp::ConstU64(u64::MAX),
834                DebugLocationExpressionOp::ConstU64(1),
835                DebugLocationExpressionOp::Add,
836            ])
837            .unwrap(),
838        ] {
839            assert_eq!(
840                resolve_variable_value(
841                    &DebugVarLocation::Expression(expression),
842                    &[],
843                    |_| None,
844                    |_| None,
845                ),
846                None
847            );
848        }
849    }
850
851    #[test]
852    fn resolves_consecutive_memory_values() {
853        let values = resolve_variable_values(
854            &DebugVarLocation::Memory(10),
855            2,
856            &[],
857            |address| match address {
858                10 => Some(Felt::new(1).unwrap()),
859                11 => Some(Felt::new(2).unwrap()),
860                _ => None,
861            },
862            |_| None,
863        );
864
865        assert_eq!(values, Some(vec![Felt::new(1).unwrap(), Felt::new(2).unwrap()]));
866    }
867
868    #[test]
869    fn debug_kill_removes_current_variable() {
870        let events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
871            Rc::new(Default::default());
872        {
873            let mut events = events.borrow_mut();
874            events.insert(
875                RowIndex::from(1),
876                vec![DebugVarInfo::new(
877                    "x",
878                    DebugVarLocation::Const(Felt::new(1).expect("value exceeds field modulus")),
879                )],
880            );
881            events.insert(
882                RowIndex::from(2),
883                vec![DebugVarInfo::new("x", DebugVarLocation::Unavailable)],
884            );
885        }
886
887        let mut tracker = DebugVarTracker::new(events);
888        tracker.update_to_cycle(RowIndex::from(1));
889        assert!(tracker.get_variable("x").is_some());
890
891        tracker.update_to_cycle(RowIndex::from(2));
892        assert!(tracker.get_variable("x").is_none());
893    }
894}