Skip to main content

miden_debug_engine/debug/
variables.rs

1use alloc::{
2    collections::BTreeMap,
3    rc::Rc,
4    string::{String, ToString},
5    sync::Arc,
6    vec::Vec,
7};
8use core::cell::RefCell;
9
10use miden_assembly_syntax::ast::{
11    DebugFrameBase, DebugLocationExpression, DebugLocationExpressionOp, DebugVarInfo,
12    DebugVarLocation, types::Type,
13};
14use miden_core::Felt;
15use miden_processor::trace::RowIndex;
16
17type DebugVarEvents = Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>;
18type CapturedDebugValues = Rc<RefCell<BTreeMap<(RowIndex, Arc<str>), Vec<Felt>>>>;
19
20/// A snapshot of a debug variable at a specific clock cycle.
21#[derive(Debug, Clone)]
22pub struct DebugVarSnapshot {
23    /// The clock cycle when this variable info was recorded.
24    pub clk: RowIndex,
25    /// The debug variable information.
26    pub info: DebugVarInfo,
27}
28
29/// Tracks debug variable snapshots, mapping variable names to their most recent location info.
30pub struct DebugVarTracker {
31    /// All debug variable events recorded during execution, keyed by clock cycle.
32    events: DebugVarEvents,
33    /// Canonical values frozen at transient debug decorators.
34    captured_values: CapturedDebugValues,
35    /// Current view of variables - maps variable name to most recent info.
36    current_vars: BTreeMap<String, DebugVarSnapshot>,
37    /// Canonical values captured for currently visible transient variables.
38    current_captured_values: BTreeMap<String, Vec<Felt>>,
39    /// The clock cycle up to which we've processed events.
40    processed_up_to: RowIndex,
41}
42
43impl DebugVarTracker {
44    /// Create a new tracker using the given shared event store.
45    pub fn new(events: DebugVarEvents) -> Self {
46        Self {
47            events,
48            captured_values: Rc::new(Default::default()),
49            current_vars: BTreeMap::new(),
50            current_captured_values: BTreeMap::new(),
51            processed_up_to: RowIndex::from(0),
52        }
53    }
54
55    /// Record debug variable events at the given clock cycle.
56    pub fn record_events(&self, clk: RowIndex, infos: Vec<DebugVarInfo>) {
57        if !infos.is_empty() {
58            self.events.borrow_mut().entry(clk).or_default().extend(infos);
59        }
60    }
61
62    /// Record debug variable events after freezing transient stack-backed values.
63    pub fn record_events_with_stack(
64        &self,
65        clk: RowIndex,
66        mut infos: Vec<DebugVarInfo>,
67        stack: &[Felt],
68    ) {
69        let names = infos.iter().map(|info| info.name().clone()).collect::<Vec<_>>();
70        let captured_values = snapshot_transient_debug_values(&mut infos, stack);
71        {
72            let mut stored_values = self.captured_values.borrow_mut();
73            for name in names {
74                stored_values.remove(&(clk, name));
75            }
76            stored_values
77                .extend(captured_values.into_iter().map(|(name, values)| ((clk, name), values)));
78        }
79        self.record_events(clk, infos);
80    }
81
82    /// Process all events up to and including `clk`, updating current variable state.
83    pub fn update_to_cycle(&mut self, clk: RowIndex) {
84        let events = self.events.borrow();
85        let captured_values = self.captured_values.borrow();
86
87        // Process events from processed_up_to to clk
88        for (event_clk, var_infos) in events.range(self.processed_up_to..=clk) {
89            for info in var_infos {
90                if is_debug_var_kill(info) {
91                    self.current_vars.remove(info.name().as_ref());
92                    self.current_captured_values.remove(info.name().as_ref());
93                    continue;
94                }
95                let snapshot = DebugVarSnapshot {
96                    clk: *event_clk,
97                    info: info.clone(),
98                };
99                let name = info.name().to_string();
100                match captured_values.get(&(*event_clk, info.name().clone())) {
101                    Some(values) => {
102                        self.current_captured_values.insert(name.clone(), values.clone());
103                    }
104                    None => {
105                        self.current_captured_values.remove(&name);
106                    }
107                }
108                self.current_vars.insert(name, snapshot);
109            }
110        }
111
112        self.processed_up_to = clk;
113    }
114
115    /// Reset the tracker to the beginning of execution.
116    pub fn reset(&mut self) {
117        self.current_vars.clear();
118        self.current_captured_values.clear();
119        self.processed_up_to = RowIndex::from(0);
120    }
121
122    /// Get all currently visible variables.
123    pub fn current_variables(&self) -> impl Iterator<Item = &DebugVarSnapshot> {
124        self.current_vars.values()
125    }
126
127    /// Get a specific variable by name.
128    pub fn get_variable(&self, name: &str) -> Option<&DebugVarSnapshot> {
129        self.current_vars.get(name)
130    }
131
132    /// Returns canonical values captured at the variable's latest debug decorator.
133    pub fn captured_values(&self, name: &str) -> Option<&[Felt]> {
134        self.current_captured_values.get(name).map(Vec::as_slice)
135    }
136
137    /// Get the number of tracked variables.
138    pub fn variable_count(&self) -> usize {
139        self.current_vars.len()
140    }
141
142    /// Check if there are any tracked variables.
143    pub fn has_variables(&self) -> bool {
144        !self.current_vars.is_empty()
145    }
146}
147
148/// Snapshot transient debug locations at the decorator point.
149///
150/// Stack locations are only meaningful at the debug decorator itself. Keeping them live and
151/// resolving them against a later VM stack can report unrelated values. Memory, local, and
152/// frame-base declarations describe live storage and must be resolved against the current VM state
153/// when the user inspects variables.
154pub fn snapshot_transient_debug_values(
155    infos: &mut [DebugVarInfo],
156    stack: &[Felt],
157) -> BTreeMap<Arc<str>, Vec<Felt>> {
158    let mut captured_values = BTreeMap::new();
159
160    for info in infos {
161        captured_values.remove(info.name().as_ref());
162        match info.value_location() {
163            DebugVarLocation::Stack(position) => {
164                let count = info.ty().and_then(super::abi_types::value_felt_count).unwrap_or(1);
165                let start = *position as usize;
166                let values = start
167                    .checked_add(count)
168                    .and_then(|end| stack.get(start..end))
169                    .map(<[Felt]>::to_vec);
170                let location = values
171                    .as_ref()
172                    .and_then(|values| values.first().copied())
173                    .or_else(|| (count == 0).then(|| Felt::from_u32(0)))
174                    .map(DebugVarLocation::Const)
175                    .unwrap_or(DebugVarLocation::Unavailable);
176                if let Some(values) = values {
177                    captured_values.insert(info.name().clone(), values);
178                }
179                info.set_value_location(location);
180            }
181            DebugVarLocation::Expression(expression) => {
182                let operations = expression
183                    .operations()
184                    .iter()
185                    .map(|operation| match operation {
186                        DebugLocationExpressionOp::ReadStack(position) => stack
187                            .get(*position as usize)
188                            .map(|value| {
189                                DebugLocationExpressionOp::ConstU64(value.as_canonical_u64())
190                            })
191                            .ok_or(()),
192                        operation => Ok(*operation),
193                    })
194                    .collect::<Result<Vec<_>, _>>();
195                let location = operations
196                    .and_then(|ops| DebugLocationExpression::new(ops).map_err(|_| ()))
197                    .map(DebugVarLocation::Expression)
198                    .unwrap_or(DebugVarLocation::Unavailable);
199                info.set_value_location(location);
200            }
201            _ => {}
202        }
203    }
204
205    captured_values
206}
207
208fn is_debug_var_kill(info: &DebugVarInfo) -> bool {
209    matches!(info.value_location(), DebugVarLocation::Unavailable)
210}
211
212/// Resolve a debug variable's value given its location and the current VM state.
213pub fn resolve_variable_value(
214    location: &DebugVarLocation,
215    stack: &[Felt],
216    get_memory: impl Fn(u32) -> Option<Felt>,
217    get_local: impl Fn(i16) -> Option<Felt>,
218) -> Option<Felt> {
219    resolve_variable_values(location, 1, stack, get_memory, get_local)?.pop()
220}
221
222/// Resolve one or more consecutive felts for a debug variable.
223pub fn resolve_variable_values(
224    location: &DebugVarLocation,
225    count: usize,
226    stack: &[Felt],
227    get_memory: impl Fn(u32) -> Option<Felt>,
228    get_local: impl Fn(i16) -> Option<Felt>,
229) -> Option<Vec<Felt>> {
230    if count == 0 {
231        return Some(Vec::new());
232    }
233
234    match location {
235        DebugVarLocation::Stack(pos) => {
236            let start = *pos as usize;
237            let end = start.checked_add(count)?;
238            Some(stack.get(start..end)?.to_vec())
239        }
240        DebugVarLocation::Memory(addr) => resolve_consecutive_memory(*addr, count, &get_memory),
241        DebugVarLocation::Const(felt) => (count == 1).then_some(vec![*felt]),
242        DebugVarLocation::Local(offset) => {
243            let mut values = Vec::with_capacity(count);
244            for index in 0..count {
245                let index = i16::try_from(index).ok()?;
246                values.push(get_local(offset.checked_add(index)?)?);
247            }
248            Some(values)
249        }
250        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => {
251            resolve_frame_base_values(*base, *byte_offset, count, &get_memory, &get_local)
252        }
253        DebugVarLocation::Expression(expression) => {
254            match resolve_expression(expression.operations(), stack, &get_memory, &get_local)? {
255                ResolvedExpression::Scalar(value) => {
256                    (count == 1).then(|| integer_to_felt(value)).flatten().map(|value| vec![value])
257                }
258                ResolvedExpression::ByteAddress(address) => {
259                    let element_address = u32::try_from(address / 4).ok()?;
260                    resolve_consecutive_memory(element_address, count, &get_memory)
261                }
262            }
263        }
264        DebugVarLocation::Unavailable => None,
265    }
266}
267
268/// Resolve a typed debug variable into the canonical ABI felts consumed by the typed decoder.
269///
270/// Simple Miden locations already contain canonical stack values. Frame-base locations and a
271/// terminal [`DebugLocationExpressionOp::DerefBytes`] instead identify packed Rust memory, which
272/// must be read at byte granularity and lifted according to `ty` before it can be decoded.
273pub fn resolve_typed_variable_values(
274    location: &DebugVarLocation,
275    ty: &Type,
276    count: usize,
277    stack: &[Felt],
278    get_memory: impl Fn(u32) -> Option<Felt>,
279    get_local: impl Fn(i16) -> Option<Felt>,
280) -> Option<Vec<Felt>> {
281    match location {
282        DebugVarLocation::ResolvedFrameBase { base, byte_offset } => {
283            let byte_address =
284                resolve_frame_base_address(*base, *byte_offset, &get_memory, &get_local)?;
285            resolve_typed_memory_value(ty, byte_address, count, &get_memory)
286        }
287        DebugVarLocation::Expression(expression) => {
288            match resolve_expression(expression.operations(), stack, &get_memory, &get_local)? {
289                ResolvedExpression::ByteAddress(byte_address) => {
290                    resolve_typed_memory_value(ty, byte_address, count, &get_memory)
291                }
292                ResolvedExpression::Scalar(value) => {
293                    (count == 1).then(|| integer_to_felt(value)).flatten().map(|value| vec![value])
294                }
295            }
296        }
297        _ => resolve_variable_values(location, count, stack, get_memory, get_local),
298    }
299}
300
301fn resolve_consecutive_memory(
302    start_addr: u32,
303    count: usize,
304    get_memory: &impl Fn(u32) -> Option<Felt>,
305) -> Option<Vec<Felt>> {
306    let mut values = Vec::with_capacity(count);
307    for index in 0..count {
308        let addr = start_addr.checked_add(u32::try_from(index).ok()?)?;
309        values.push(get_memory(addr)?);
310    }
311    Some(values)
312}
313
314#[derive(Clone, Copy, Debug, Eq, PartialEq)]
315enum ResolvedExpression {
316    Scalar(i128),
317    ByteAddress(u64),
318}
319
320fn resolve_expression(
321    ops: &[DebugLocationExpressionOp],
322    stack: &[Felt],
323    get_memory: &impl Fn(u32) -> Option<Felt>,
324    get_local: &impl Fn(i16) -> Option<Felt>,
325) -> Option<ResolvedExpression> {
326    let mut values = Vec::<i128>::new();
327
328    for (index, op) in ops.iter().enumerate() {
329        match op {
330            DebugLocationExpressionOp::ReadStack(index) => {
331                values.push(i128::from(stack.get(*index as usize)?.as_canonical_u64()));
332            }
333            DebugLocationExpressionOp::ReadMemory(index) => {
334                values.push(i128::from(get_memory(*index)?.as_canonical_u64()));
335            }
336            DebugLocationExpressionOp::ReadLocal(index) => {
337                values.push(i128::from(get_local(*index)?.as_canonical_u64()));
338            }
339            DebugLocationExpressionOp::ConstU64(value) => {
340                values.push(i128::from(*value));
341            }
342            DebugLocationExpressionOp::ConstI64(value) => {
343                values.push(i128::from(*value));
344            }
345            DebugLocationExpressionOp::AddUnsigned(value) => {
346                let lhs = values.pop()?;
347                values.push(lhs.checked_add(i128::from(*value))?);
348            }
349            DebugLocationExpressionOp::Add => {
350                let rhs = values.pop()?;
351                let lhs = values.pop()?;
352                values.push(lhs.checked_add(rhs)?);
353            }
354            DebugLocationExpressionOp::Sub => {
355                let rhs = values.pop()?;
356                let lhs = values.pop()?;
357                values.push(lhs.checked_sub(rhs)?);
358            }
359            DebugLocationExpressionOp::DerefBytes => {
360                let byte_address = u64::try_from(values.pop()?).ok()?;
361                if index + 1 == ops.len() {
362                    return Some(ResolvedExpression::ByteAddress(byte_address));
363                }
364                let element_address = u32::try_from(byte_address / 4).ok()?;
365                values.push(i128::from(get_memory(element_address)?.as_canonical_u64()));
366            }
367            DebugLocationExpressionOp::FrameBaseAddress { base, byte_offset } => {
368                values.push(i128::from(resolve_frame_base_address(
369                    *base,
370                    *byte_offset,
371                    get_memory,
372                    get_local,
373                )?));
374            }
375        }
376    }
377
378    values.pop().map(ResolvedExpression::Scalar)
379}
380
381fn resolve_frame_base_values(
382    base: DebugFrameBase,
383    byte_offset: i64,
384    count: usize,
385    get_memory: &impl Fn(u32) -> Option<Felt>,
386    get_local: &impl Fn(i16) -> Option<Felt>,
387) -> Option<Vec<Felt>> {
388    let byte_address = resolve_frame_base_address(base, byte_offset, get_memory, get_local)?;
389    resolve_byte_address_values(byte_address, count, get_memory)
390}
391
392fn resolve_frame_base_address(
393    base: DebugFrameBase,
394    byte_offset: i64,
395    get_memory: &impl Fn(u32) -> Option<Felt>,
396    get_local: &impl Fn(i16) -> Option<Felt>,
397) -> Option<u64> {
398    let base = match base {
399        DebugFrameBase::Local(offset) => get_local(offset)?,
400        DebugFrameBase::Memory(address) => get_memory(address)?,
401    };
402    base.as_canonical_u64().checked_add_signed(byte_offset)
403}
404
405fn resolve_byte_address_values(
406    byte_address: u64,
407    count: usize,
408    get_memory: &impl Fn(u32) -> Option<Felt>,
409) -> Option<Vec<Felt>> {
410    if !byte_address.is_multiple_of(4) {
411        return None;
412    }
413    let element_address = u32::try_from(byte_address / 4).ok()?;
414    resolve_consecutive_memory(element_address, count, get_memory)
415}
416
417fn integer_to_felt(value: i128) -> Option<Felt> {
418    Felt::new(u64::try_from(value).ok()?).ok()
419}
420
421fn read_memory_bytes(
422    byte_address: u64,
423    size: usize,
424    get_memory: &impl Fn(u32) -> Option<Felt>,
425) -> Option<Vec<u8>> {
426    if size == 0 {
427        return Some(Vec::new());
428    }
429
430    let element_address = u32::try_from(byte_address / 4).ok()?;
431    let byte_offset = usize::try_from(byte_address % 4).ok()?;
432    let end = byte_offset.checked_add(size)?;
433    let element_count = end.div_ceil(4);
434    let mut bytes = Vec::with_capacity(element_count.checked_mul(4)?);
435
436    for index in 0..element_count {
437        let address = element_address.checked_add(u32::try_from(index).ok()?)?;
438        let value = get_memory(address)?.as_canonical_u64() as u32;
439        bytes.extend_from_slice(&value.to_le_bytes());
440    }
441
442    Some(bytes.get(byte_offset..end)?.to_vec())
443}
444
445fn resolve_typed_memory_value(
446    ty: &Type,
447    byte_address: u64,
448    expected_count: usize,
449    get_memory: &impl Fn(u32) -> Option<Felt>,
450) -> Option<Vec<Felt>> {
451    let mut values = Vec::with_capacity(expected_count);
452    append_typed_memory_value(ty, byte_address, get_memory, &mut values)?;
453    (values.len() == expected_count).then_some(values)
454}
455
456fn append_typed_memory_value(
457    ty: &Type,
458    byte_address: u64,
459    get_memory: &impl Fn(u32) -> Option<Felt>,
460    values: &mut Vec<Felt>,
461) -> Option<()> {
462    match ty {
463        Type::Felt => {
464            if !byte_address.is_multiple_of(4) {
465                return None;
466            }
467            values.push(get_memory(u32::try_from(byte_address / 4).ok()?)?);
468        }
469        Type::I1 => {
470            let byte = *read_memory_bytes(byte_address, 1, get_memory)?.first()?;
471            values.push(Felt::from_u32(u32::from(byte & 1)));
472        }
473        Type::I8 | Type::I16 | Type::I32 | Type::I64 | Type::I128 => {
474            let value = read_unsigned_integer(ty, byte_address, get_memory)?;
475            let bit_width = u32::try_from(ty.size_in_bits()).ok()?;
476            let shift = 128_u32.checked_sub(bit_width)?;
477            let signed = ((value << shift) as i128) >> shift;
478            let slot_bits = u32::try_from(ty.size_in_felts().checked_mul(32)?).ok()?;
479            let canonical = (signed as u128) & low_bits_mask(slot_bits);
480            append_u128_limbs(canonical, ty.size_in_felts(), values);
481        }
482        Type::U8 | Type::U16 | Type::U32 | Type::U64 | Type::U128 | Type::F64 => {
483            let value = read_unsigned_integer(ty, byte_address, get_memory)?;
484            append_u128_limbs(value, ty.size_in_felts(), values);
485        }
486        Type::Struct(struct_ty) => {
487            for field in struct_ty.get().fields() {
488                let field_address = byte_address.checked_add(u64::from(field.offset))?;
489                append_typed_memory_value(&field.ty, field_address, get_memory, values)?;
490            }
491        }
492        Type::Array(array_ty) => {
493            let element_size = array_ty.ty.size_in_bytes();
494            let alignment = array_ty.ty.min_alignment();
495            let stride = align_up(element_size, alignment)?;
496            for index in 0..array_ty.len {
497                let offset = index.checked_mul(stride)?;
498                let element_address = byte_address.checked_add(u64::try_from(offset).ok()?)?;
499                append_typed_memory_value(&array_ty.ty, element_address, get_memory, values)?;
500            }
501        }
502        Type::U256
503        | Type::List(_)
504        | Type::Ptr(_)
505        | Type::Function(_)
506        | Type::Enum(_)
507        | Type::Unknown
508        | Type::Never
509        | Type::Variadic => return None,
510    }
511
512    Some(())
513}
514
515fn read_unsigned_integer(
516    ty: &Type,
517    byte_address: u64,
518    get_memory: &impl Fn(u32) -> Option<Felt>,
519) -> Option<u128> {
520    let bytes = read_memory_bytes(byte_address, ty.size_in_bytes(), get_memory)?;
521    let mut value = 0_u128;
522    for (index, byte) in bytes.into_iter().enumerate() {
523        value |= u128::from(byte) << index.checked_mul(8)?;
524    }
525    Some(value)
526}
527
528fn append_u128_limbs(value: u128, count: usize, values: &mut Vec<Felt>) {
529    values.extend((0..count).map(|index| Felt::from_u32((value >> (index * 32)) as u32)));
530}
531
532fn low_bits_mask(bits: u32) -> u128 {
533    if bits == 128 {
534        u128::MAX
535    } else {
536        (1_u128 << bits) - 1
537    }
538}
539
540fn align_up(size: usize, alignment: usize) -> Option<usize> {
541    if size == 0 {
542        return Some(0);
543    }
544    size.checked_add(alignment.checked_sub(1)?)?
545        .checked_div(alignment)?
546        .checked_mul(alignment)
547}
548
549#[cfg(test)]
550mod tests;