Skip to main content

miden_debug_engine/debug/
variables.rs

1use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
2
3use miden_core::{
4    Felt,
5    operations::{DebugVarInfo, DebugVarLocation},
6    serde::{ByteReader, Deserializable, SliceReader},
7};
8use miden_processor::trace::RowIndex;
9
10const FRAME_BASE_LOCAL_MARKER: u32 = 1 << 31;
11const DEBUG_VAR_KILL_SENTINEL: &[u8] = b"\0miden.debug.kill";
12
13fn decode_frame_base_local_offset(encoded: u32) -> Option<i16> {
14    if encoded & FRAME_BASE_LOCAL_MARKER == 0 {
15        return None;
16    }
17
18    let low_bits = (encoded & 0xffff) as u16;
19    Some(i16::from_le_bytes(low_bits.to_le_bytes()))
20}
21
22/// A snapshot of a debug variable at a specific clock cycle.
23#[derive(Debug, Clone)]
24pub struct DebugVarSnapshot {
25    /// The clock cycle when this variable info was recorded.
26    pub clk: RowIndex,
27    /// The debug variable information.
28    pub info: DebugVarInfo,
29}
30
31/// Tracks debug variable snapshots, mapping variable names to their most recent location info.
32pub struct DebugVarTracker {
33    /// All debug variable events recorded during execution, keyed by clock cycle.
34    events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>,
35    /// Current view of variables - maps variable name to most recent info.
36    current_vars: BTreeMap<String, DebugVarSnapshot>,
37    /// The clock cycle up to which we've processed events.
38    processed_up_to: RowIndex,
39}
40
41impl DebugVarTracker {
42    /// Create a new tracker using the given shared event store.
43    pub fn new(events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>) -> Self {
44        Self {
45            events,
46            current_vars: BTreeMap::new(),
47            processed_up_to: RowIndex::from(0),
48        }
49    }
50
51    /// Record debug variable events at the given clock cycle.
52    pub fn record_events(&self, clk: RowIndex, infos: Vec<DebugVarInfo>) {
53        if !infos.is_empty() {
54            self.events.borrow_mut().entry(clk).or_default().extend(infos);
55        }
56    }
57
58    /// Process all events up to and including `clk`, updating current variable state.
59    pub fn update_to_cycle(&mut self, clk: RowIndex) {
60        let events = self.events.borrow();
61
62        // Process events from processed_up_to to clk
63        for (event_clk, var_infos) in events.range(self.processed_up_to..=clk) {
64            for info in var_infos {
65                if is_debug_var_kill(info) {
66                    self.current_vars.remove(info.name());
67                    continue;
68                }
69                let snapshot = DebugVarSnapshot {
70                    clk: *event_clk,
71                    info: info.clone(),
72                };
73                self.current_vars.insert(info.name().to_string(), snapshot);
74            }
75        }
76
77        self.processed_up_to = clk;
78    }
79
80    /// Reset the tracker to the beginning of execution.
81    pub fn reset(&mut self) {
82        self.current_vars.clear();
83        self.processed_up_to = RowIndex::from(0);
84    }
85
86    /// Get all currently visible variables.
87    pub fn current_variables(&self) -> impl Iterator<Item = &DebugVarSnapshot> {
88        self.current_vars.values()
89    }
90
91    /// Get a specific variable by name.
92    pub fn get_variable(&self, name: &str) -> Option<&DebugVarSnapshot> {
93        self.current_vars.get(name)
94    }
95
96    /// Get the number of tracked variables.
97    pub fn variable_count(&self) -> usize {
98        self.current_vars.len()
99    }
100
101    /// Check if there are any tracked variables.
102    pub fn has_variables(&self) -> bool {
103        !self.current_vars.is_empty()
104    }
105}
106
107/// Snapshot transient debug locations at the decorator point.
108///
109/// Stack locations are only meaningful at the debug decorator itself. Keeping them live and
110/// resolving them against a later VM stack can report unrelated values. Memory, local, and
111/// frame-base declarations describe live storage and must be resolved against the current VM state
112/// when the user inspects variables.
113pub fn snapshot_transient_debug_values(infos: &mut [DebugVarInfo], stack: &[Felt]) {
114    for info in infos {
115        if let DebugVarLocation::Stack(pos) = info.value_location()
116            && let Some(value) = stack.get(*pos as usize).copied()
117        {
118            info.set_value_location(DebugVarLocation::Const(value));
119        }
120    }
121}
122
123fn is_debug_var_kill(info: &DebugVarInfo) -> bool {
124    matches!(
125        info.value_location(),
126        DebugVarLocation::Expression(expression) if expression == DEBUG_VAR_KILL_SENTINEL
127    )
128}
129
130/// Resolve a debug variable's value given its location and the current VM state.
131pub fn resolve_variable_value(
132    location: &DebugVarLocation,
133    stack: &[Felt],
134    get_memory: impl Fn(u32) -> Option<Felt>,
135    get_local: impl Fn(i16) -> Option<Felt>,
136) -> Option<Felt> {
137    match location {
138        DebugVarLocation::Stack(pos) => stack.get(*pos as usize).copied(),
139        DebugVarLocation::Memory(addr) => get_memory(*addr),
140        DebugVarLocation::Const(felt) => Some(*felt),
141        DebugVarLocation::Local(offset) => get_local(*offset),
142        DebugVarLocation::FrameBase {
143            global_index,
144            byte_offset,
145        } => resolve_frame_base_value(*global_index, *byte_offset, &get_memory, &get_local),
146        DebugVarLocation::Expression(expression) => {
147            resolve_expression_value(expression, stack, &get_memory, &get_local)
148        }
149    }
150}
151
152fn resolve_frame_base_value(
153    global_index: u32,
154    byte_offset: i64,
155    get_memory: &impl Fn(u32) -> Option<Felt>,
156    get_local: &impl Fn(i16) -> Option<Felt>,
157) -> Option<Felt> {
158    if let Some(local_offset) = decode_frame_base_local_offset(global_index) {
159        let base = get_local(local_offset)?;
160        let byte_addr = base.as_canonical_u64() as i64 + byte_offset;
161        let elem_addr = byte_addr / 4;
162        let elem_addr = u32::try_from(elem_addr).ok()?;
163        return get_memory(elem_addr);
164    }
165
166    // global_index was resolved to a Miden byte address during compilation.
167    // Convert to element address (รท4) to read the stack pointer value.
168    let sp_elem_addr = global_index / 4;
169    let base = get_memory(sp_elem_addr)?;
170    // The stack pointer value is also a byte address; apply byte_offset,
171    // then convert to element address to read the variable's value.
172    let byte_addr = base.as_canonical_u64() as i64 + byte_offset;
173    let elem_addr = (byte_addr / 4) as u32;
174    get_memory(elem_addr)
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178enum DebugExpressionOp {
179    WasmLocal(u32),
180    WasmGlobal(u32),
181    WasmStack(u32),
182    ConstU64(u64),
183    ConstS64(i64),
184    PlusUConst(u64),
185    Minus,
186    Plus,
187    Deref,
188    StackValue,
189    Piece,
190    BitPiece,
191    FrameBase { global_index: u32, byte_offset: i64 },
192    Address(u64),
193    Unsupported,
194}
195
196fn resolve_expression_value(
197    expression: &[u8],
198    stack: &[Felt],
199    get_memory: &impl Fn(u32) -> Option<Felt>,
200    get_local: &impl Fn(i16) -> Option<Felt>,
201) -> Option<Felt> {
202    let ops = read_expression(expression)?;
203    let mut values = Vec::<Felt>::new();
204
205    for op in ops {
206        match op {
207            DebugExpressionOp::WasmLocal(index) => {
208                values.push(get_local(i16::try_from(index).ok()?)?);
209            }
210            DebugExpressionOp::WasmStack(index) => {
211                values.push(stack.get(index as usize).copied()?);
212            }
213            DebugExpressionOp::ConstU64(value) => {
214                values.push(Felt::new(value).expect("value exceeds field modulus"));
215            }
216            DebugExpressionOp::ConstS64(value) => {
217                values.push(Felt::new(value as u64).expect("value exceeds field modulus"));
218            }
219            DebugExpressionOp::PlusUConst(value) => {
220                let lhs = values.pop()?;
221                values.push(
222                    Felt::new(lhs.as_canonical_u64().wrapping_add(value))
223                        .expect("value exceeds field modulus"),
224                );
225            }
226            DebugExpressionOp::Minus => {
227                let rhs = values.pop()?.as_canonical_u64();
228                let lhs = values.pop()?.as_canonical_u64();
229                values.push(Felt::new(lhs.wrapping_sub(rhs)).expect("value exceeds field modulus"));
230            }
231            DebugExpressionOp::Plus => {
232                let rhs = values.pop()?.as_canonical_u64();
233                let lhs = values.pop()?.as_canonical_u64();
234                values.push(Felt::new(lhs.wrapping_add(rhs)).expect("value exceeds field modulus"));
235            }
236            DebugExpressionOp::Deref => {
237                let addr = u32::try_from(values.pop()?.as_canonical_u64()).ok()?;
238                values.push(get_memory(addr)?);
239            }
240            DebugExpressionOp::StackValue => {}
241            DebugExpressionOp::FrameBase {
242                global_index,
243                byte_offset,
244            } => {
245                values.push(resolve_frame_base_value(
246                    global_index,
247                    byte_offset,
248                    get_memory,
249                    get_local,
250                )?);
251            }
252            DebugExpressionOp::Address(address) => {
253                values.push(Felt::new(address).expect("value exceeds field modulus"));
254            }
255            DebugExpressionOp::WasmGlobal(_)
256            | DebugExpressionOp::Piece
257            | DebugExpressionOp::BitPiece
258            | DebugExpressionOp::Unsupported => return None,
259        }
260    }
261
262    values.pop()
263}
264
265fn read_expression(expression: &[u8]) -> Option<Vec<DebugExpressionOp>> {
266    let mut reader = SliceReader::new(expression);
267    let len = usize::read_from(&mut reader).ok()?;
268    let mut ops = Vec::with_capacity(len);
269    for _ in 0..len {
270        ops.push(read_expression_op(&mut reader)?);
271    }
272    Some(ops)
273}
274
275fn read_expression_op(reader: &mut SliceReader<'_>) -> Option<DebugExpressionOp> {
276    Some(match reader.read_u8().ok()? {
277        0 => DebugExpressionOp::WasmLocal(u32::read_from(reader).ok()?),
278        1 => DebugExpressionOp::WasmGlobal(u32::read_from(reader).ok()?),
279        2 => DebugExpressionOp::WasmStack(u32::read_from(reader).ok()?),
280        3 => DebugExpressionOp::ConstU64(u64::read_from(reader).ok()?),
281        4 => DebugExpressionOp::ConstS64(u64::read_from(reader).ok()? as i64),
282        5 => DebugExpressionOp::PlusUConst(u64::read_from(reader).ok()?),
283        6 => DebugExpressionOp::Minus,
284        7 => DebugExpressionOp::Plus,
285        8 => DebugExpressionOp::Deref,
286        9 => DebugExpressionOp::StackValue,
287        10 => {
288            let _size = u64::read_from(reader).ok()?;
289            DebugExpressionOp::Piece
290        }
291        11 => {
292            let _size = u64::read_from(reader).ok()?;
293            let _offset = u64::read_from(reader).ok()?;
294            DebugExpressionOp::BitPiece
295        }
296        12 => {
297            let global_index = u32::read_from(reader).ok()?;
298            let byte_offset = u64::read_from(reader).ok()? as i64;
299            DebugExpressionOp::FrameBase {
300                global_index,
301                byte_offset,
302            }
303        }
304        13 => DebugExpressionOp::Address(u64::read_from(reader).ok()?),
305        u8::MAX => {
306            let len = usize::read_from(reader).ok()?;
307            let _name = reader.read_slice(len).ok()?;
308            DebugExpressionOp::Unsupported
309        }
310        _ => return None,
311    })
312}
313
314#[cfg(test)]
315mod tests {
316    use miden_core::serde::ByteWriter;
317
318    use super::*;
319
320    #[test]
321    fn test_tracker_basic() {
322        let events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
323            Rc::new(Default::default());
324
325        // Add some events
326        {
327            let mut events_mut = events.borrow_mut();
328            events_mut.insert(
329                RowIndex::from(1),
330                vec![DebugVarInfo::new("x", DebugVarLocation::Stack(0))],
331            );
332            events_mut.insert(
333                RowIndex::from(5),
334                vec![DebugVarInfo::new("y", DebugVarLocation::Stack(1))],
335            );
336        }
337
338        let mut tracker = DebugVarTracker::new(events);
339
340        // Initially no variables
341        assert_eq!(tracker.variable_count(), 0);
342
343        // Process up to cycle 3
344        tracker.update_to_cycle(RowIndex::from(3));
345        assert_eq!(tracker.variable_count(), 1);
346        assert!(tracker.get_variable("x").is_some());
347        assert!(tracker.get_variable("y").is_none());
348
349        // Process up to cycle 10
350        tracker.update_to_cycle(RowIndex::from(10));
351        assert_eq!(tracker.variable_count(), 2);
352        assert!(tracker.get_variable("x").is_some());
353        assert!(tracker.get_variable("y").is_some());
354
355        // Verify resolve_variable_value resolves stack values
356        let x_snapshot = tracker.get_variable("x").unwrap();
357        let value = resolve_variable_value(
358            x_snapshot.info.value_location(),
359            &[Felt::new(42).expect("value exceeds field modulus")],
360            |_| None,
361            |_| None,
362        );
363        assert_eq!(value, Some(Felt::new(42).expect("value exceeds field modulus")));
364    }
365
366    #[test]
367    fn snapshots_transient_stack_locations_as_constants() {
368        let mut infos = vec![
369            DebugVarInfo::new("a", DebugVarLocation::Stack(0)),
370            DebugVarInfo::new("b", DebugVarLocation::Local(-1)),
371        ];
372
373        snapshot_transient_debug_values(
374            &mut infos,
375            &[Felt::new(7).expect("value exceeds field modulus")],
376        );
377
378        assert_eq!(
379            infos[0].value_location(),
380            &DebugVarLocation::Const(Felt::new(7).expect("value exceeds field modulus"))
381        );
382        assert_eq!(infos[1].value_location(), &DebugVarLocation::Local(-1));
383    }
384
385    #[test]
386    fn resolves_local_frame_base_as_byte_address() {
387        let encoded =
388            FRAME_BASE_LOCAL_MARKER | u32::from(u16::from_le_bytes((-7i16).to_le_bytes()));
389
390        let value = resolve_variable_value(
391            &DebugVarLocation::FrameBase {
392                global_index: encoded,
393                byte_offset: 28,
394            },
395            &[],
396            |addr| (addr == 262_139).then_some(Felt::new(13).expect("value exceeds field modulus")),
397            |offset| {
398                (offset == -7).then_some(Felt::new(1_048_528).expect("value exceeds field modulus"))
399            },
400        );
401
402        assert_eq!(value, Some(Felt::new(13).expect("value exceeds field modulus")));
403    }
404
405    #[test]
406    fn debug_kill_removes_current_variable() {
407        let events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
408            Rc::new(Default::default());
409        {
410            let mut events = events.borrow_mut();
411            events.insert(
412                RowIndex::from(1),
413                vec![DebugVarInfo::new(
414                    "x",
415                    DebugVarLocation::Const(Felt::new(1).expect("value exceeds field modulus")),
416                )],
417            );
418            events.insert(
419                RowIndex::from(2),
420                vec![DebugVarInfo::new(
421                    "x",
422                    DebugVarLocation::Expression(DEBUG_VAR_KILL_SENTINEL.to_vec()),
423                )],
424            );
425        }
426
427        let mut tracker = DebugVarTracker::new(events);
428        tracker.update_to_cycle(RowIndex::from(1));
429        assert!(tracker.get_variable("x").is_some());
430
431        tracker.update_to_cycle(RowIndex::from(2));
432        assert!(tracker.get_variable("x").is_none());
433    }
434
435    #[test]
436    fn resolves_const_stack_value_expression() {
437        let expression =
438            expression_bytes(&[TestExpressionOp::ConstU64(7), TestExpressionOp::StackValue]);
439
440        let value = resolve_variable_value(
441            &DebugVarLocation::Expression(expression),
442            &[],
443            |_| None,
444            |_| None,
445        );
446
447        assert_eq!(value, Some(Felt::new(7).expect("value exceeds field modulus")));
448    }
449
450    #[test]
451    fn resolves_local_stack_value_expression() {
452        let expression =
453            expression_bytes(&[TestExpressionOp::WasmLocal(0), TestExpressionOp::StackValue]);
454
455        let value = resolve_variable_value(
456            &DebugVarLocation::Expression(expression),
457            &[],
458            |_| None,
459            |offset| (offset == 0).then_some(Felt::new(11).expect("value exceeds field modulus")),
460        );
461
462        assert_eq!(value, Some(Felt::new(11).expect("value exceeds field modulus")));
463    }
464
465    enum TestExpressionOp {
466        WasmLocal(u32),
467        ConstU64(u64),
468        StackValue,
469    }
470
471    fn expression_bytes(ops: &[TestExpressionOp]) -> Vec<u8> {
472        let mut bytes = Vec::new();
473        bytes.write_usize(ops.len());
474        for op in ops {
475            match op {
476                TestExpressionOp::WasmLocal(index) => {
477                    bytes.write_u8(0);
478                    bytes.write_u32(*index);
479                }
480                TestExpressionOp::ConstU64(value) => {
481                    bytes.write_u8(3);
482                    bytes.write_u64(*value);
483                }
484                TestExpressionOp::StackValue => {
485                    bytes.write_u8(9);
486                }
487            }
488        }
489        bytes
490    }
491}