miden_processor/
debug.rs

1use alloc::{
2    boxed::Box,
3    string::{String, ToString},
4    vec::Vec,
5};
6use core::fmt;
7
8use miden_air::RowIndex;
9use miden_core::{AssemblyOp, FieldElement, Operation, StackOutputs};
10
11use crate::{
12    Chiplets, ChipletsLengths, Decoder, ExecutionError, Felt, MemoryAddress, Process, Stack,
13    System, TraceLenSummary, range::RangeChecker, system::ContextId,
14};
15
16/// VmState holds a current process state information at a specific clock cycle.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct VmState {
19    pub clk: RowIndex,
20    pub ctx: ContextId,
21    pub op: Option<Operation>,
22    pub asmop: Option<AsmOpInfo>,
23    pub stack: Vec<Felt>,
24    pub memory: Vec<(MemoryAddress, Felt)>,
25}
26
27impl fmt::Display for VmState {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        let stack: Vec<u64> = self.stack.iter().map(|x| x.as_int()).collect();
30        write!(
31            f,
32            "clk={}{}{}, stack={stack:?}, memory={:?}",
33            self.clk,
34            match self.op {
35                Some(op) => format!(", op={op}"),
36                None => "".to_string(),
37            },
38            match &self.asmop {
39                Some(op) => format!(", {op}"),
40                None => "".to_string(),
41            },
42            self.memory
43        )
44    }
45}
46
47/// Iterator that iterates through vm state at each step of the execution.
48///
49/// This allows debugging or replaying ability to view various process state at each clock cycle. If
50/// the execution returned an error, it returns that error on the clock cycle it stopped.
51pub struct VmStateIterator {
52    chiplets: Chiplets,
53    decoder: Decoder,
54    stack: Stack,
55    system: System,
56    error: Option<ExecutionError>,
57    clk: RowIndex,
58    asmop_idx: usize,
59    forward: bool,
60    trace_len_summary: TraceLenSummary,
61}
62
63impl VmStateIterator {
64    pub fn new(process: Process, result: Result<StackOutputs, ExecutionError>) -> Self {
65        let (system, decoder, stack, mut range, chiplets, _final_pc_state) = process.into_parts();
66        let trace_len_summary = Self::build_trace_len_summary(&system, &mut range, &chiplets);
67
68        Self {
69            chiplets,
70            decoder,
71            stack,
72            system,
73            error: result.err(),
74            clk: RowIndex::from(0),
75            asmop_idx: 0,
76            forward: true,
77            trace_len_summary,
78        }
79    }
80
81    /// Returns the asm op info corresponding to this vm state and whether this is the start of
82    /// operation sequence corresponding to current assembly instruction.
83    fn get_asmop(&self) -> (Option<AsmOpInfo>, bool) {
84        let assembly_ops = self.decoder.debug_info().assembly_ops();
85
86        if self.clk == 0 || assembly_ops.is_empty() || self.asmop_idx > assembly_ops.len() {
87            return (None, false);
88        }
89
90        // keeps track of the next assembly op in the list. It's the same as the current asmop
91        // when the current asmop is last in the list
92        let next_asmop = if self.forward && self.asmop_idx < assembly_ops.len() {
93            &assembly_ops[self.asmop_idx]
94        } else {
95            &assembly_ops[self.asmop_idx.saturating_sub(1)]
96        };
97
98        // keeps track of the current assembly op in the list. It's the same as the next asmop
99        // when the clock cycle is less than the clock cycle of the first asmop.
100        let (curr_asmop, cycle_idx) = if self.asmop_idx > 0 {
101            let a = self.clk;
102            let b = RowIndex::from(assembly_ops[self.asmop_idx - 1].0);
103            (
104                &assembly_ops[self.asmop_idx - 1],
105                // difference between current clock cycle and start clock cycle of the current
106                // asmop
107                (a.max(b) - a.min(b)) as u8,
108            )
109        } else {
110            (next_asmop, 0) //dummy value, never used.
111        };
112
113        // if this is the first op in the sequence corresponding to the next asmop, returns a new
114        // instance of [AsmOp] instantiated with next asmop, num_cycles and cycle_idx of 1.
115        if next_asmop.0 == (self.clk - 1).as_usize() {
116            // cycle_idx starts at 1 instead of 0 to remove ambiguity
117            let cycle_idx = 1;
118            let asmop = AsmOpInfo::new(next_asmop.1.clone(), cycle_idx);
119            (Some(asmop), true)
120        }
121        // if this is not the first asmop in the list and if this op is part of current asmop,
122        // returns a new instance of [AsmOp] instantiated with current asmop, num_cycles and
123        // cycle_idx of current op.
124        else if self.asmop_idx > 0 && cycle_idx <= curr_asmop.1.num_cycles() {
125            // diff between curr clock cycle and start clock cycle of the current asmop
126            let asmop = AsmOpInfo::new(curr_asmop.1.clone(), cycle_idx);
127            (Some(asmop), false)
128        }
129        // if the next asmop is the first in the list and is at a greater than current clock cycle
130        // or if the current op is not a part of any asmop, return None.
131        else {
132            (None, false)
133        }
134    }
135
136    pub fn back(&mut self) -> Option<VmState> {
137        if self.clk == 0 {
138            return None;
139        }
140
141        // if we are changing directions we must decrement the clk counter.
142        if self.forward {
143            self.clk = self.clk.saturating_sub(1);
144            self.forward = false;
145        }
146
147        let ctx = self.system.get_ctx_at(self.clk);
148
149        let op = if self.clk == 0 {
150            None
151        } else {
152            Some(self.decoder.debug_info().operations()[self.clk - 1])
153        };
154
155        let (asmop, is_start) = self.get_asmop();
156        if is_start {
157            self.asmop_idx -= 1;
158        }
159
160        let result = Some(VmState {
161            clk: self.clk,
162            ctx,
163            op,
164            asmop,
165            stack: self.stack.get_state_at(self.clk),
166            memory: self.chiplets.memory.get_state_at(ctx, self.clk),
167        });
168
169        // Use saturating_sub to prevent underflow when at clock 0
170        self.clk = self.clk.saturating_sub(1);
171
172        result
173    }
174
175    pub fn into_parts(self) -> (System, Decoder, Stack, Chiplets, Option<ExecutionError>) {
176        (self.system, self.decoder, self.stack, self.chiplets, self.error)
177    }
178
179    pub fn trace_len_summary(&self) -> &TraceLenSummary {
180        &self.trace_len_summary
181    }
182
183    /// Returns an instance of [TraceLenSummary] based on provided data.
184    fn build_trace_len_summary(
185        system: &System,
186        range: &mut RangeChecker,
187        chiplets: &Chiplets,
188    ) -> TraceLenSummary {
189        let clk = system.clk();
190        let range_table_len = range.get_number_range_checker_rows();
191        chiplets.append_range_checks(range);
192
193        TraceLenSummary::new(clk.into(), range_table_len, ChipletsLengths::new(chiplets))
194    }
195}
196
197impl Iterator for VmStateIterator {
198    type Item = Result<VmState, ExecutionError>;
199
200    fn next(&mut self) -> Option<Self::Item> {
201        if self.clk > self.system.clk() {
202            match &self.error {
203                Some(_) => {
204                    let error = core::mem::take(&mut self.error);
205                    return Some(Err(error.unwrap()));
206                },
207                None => return None,
208            }
209        }
210
211        // if we are changing iteration directions we must increment the clk counter
212        if !self.forward && self.clk < self.system.clk() {
213            self.clk += 1_u32;
214            self.forward = true;
215        }
216
217        let ctx = self.system.get_ctx_at(self.clk);
218
219        let op = if self.clk == 0 {
220            None
221        } else {
222            Some(self.decoder.debug_info().operations()[self.clk - 1])
223        };
224
225        let (asmop, is_start) = self.get_asmop();
226        if is_start {
227            self.asmop_idx += 1;
228        }
229
230        let result = Some(Ok(VmState {
231            clk: self.clk,
232            ctx,
233            op,
234            asmop,
235            stack: self.stack.get_state_at(self.clk),
236            memory: self.chiplets.memory.get_state_at(ctx, self.clk),
237        }));
238
239        self.clk += 1_u32;
240
241        result
242    }
243}
244
245/// Contains assembly instruction and operation index in the sequence corresponding to the specified
246/// AsmOp decorator. This index starts from 1 instead of 0.
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct AsmOpInfo {
249    asmop: AssemblyOp,
250    cycle_idx: u8,
251}
252
253// ASMOP STATE
254// =================================================================
255
256impl AsmOpInfo {
257    /// Returns [AsmOpInfo] instantiated with the specified assembly instruction string, number of
258    /// cycles it takes to execute the assembly instruction and op index in sequence of operations
259    /// corresponding to the current assembly instruction. The first index is 1 instead of 0.
260    pub fn new(asmop: AssemblyOp, cycle_idx: u8) -> Self {
261        Self { asmop, cycle_idx }
262    }
263
264    /// Returns the context name for this operation.
265    pub fn context_name(&self) -> &str {
266        self.asmop.context_name()
267    }
268
269    /// Returns the assembly instruction corresponding to this state.
270    pub fn op(&self) -> &str {
271        self.asmop.op()
272    }
273
274    /// Returns the gerneralized form of assembly instruction corresponding to this state.
275    pub fn op_generalized(&self) -> String {
276        let op_vec: Vec<&str> = self.op().split('.').collect();
277        let keep_params = matches!(op_vec[0], "movdn" | "movup");
278        if !keep_params && op_vec.last().unwrap().parse::<usize>().is_ok() {
279            op_vec.split_last().unwrap().1.join(".")
280        } else {
281            self.op().to_string()
282        }
283    }
284
285    /// Returns the number of VM cycles taken to execute the assembly instruction.
286    pub fn num_cycles(&self) -> u8 {
287        self.asmop.num_cycles()
288    }
289
290    /// Returns the operation index of the operation at the specified clock cycle in the sequence
291    /// of operations corresponding to the current assembly instruction.
292    pub fn cycle_idx(&self) -> u8 {
293        self.cycle_idx
294    }
295
296    /// Returns `true` if the debug should break for this line.
297    pub const fn should_break(&self) -> bool {
298        self.asmop.should_break()
299    }
300}
301
302impl fmt::Display for AsmOpInfo {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(f, "{}, cycles={}", self.asmop, self.cycle_idx)
305    }
306}
307
308impl AsRef<AssemblyOp> for AsmOpInfo {
309    #[inline]
310    fn as_ref(&self) -> &AssemblyOp {
311        &self.asmop
312    }
313}
314
315// BUS DEBUGGING
316// =================================================================
317
318/// A message that can be sent on a bus.
319pub(crate) trait BusMessage<E: FieldElement<BaseField = Felt>>: fmt::Display {
320    /// The concrete value that this message evaluates to.
321    fn value(&self, alphas: &[E]) -> E;
322
323    /// The source of this message (e.g. "mload" or "memory chiplet").
324    fn source(&self) -> &str;
325}
326
327/// A debugger for a bus that can be used to track outstanding requests and responses.
328///
329/// Note: we use `Vec` internally instead of a `BTreeMap`, since messages can have collisions (i.e.
330/// 2 messages sent with the same key), which results in relatively complex insertion/deletion
331/// logic. Since this is only used in debug/test code, the performance hit is acceptable.
332pub(crate) struct BusDebugger<E: FieldElement<BaseField = Felt>> {
333    pub bus_name: String,
334    pub outstanding_requests: Vec<(E, Box<dyn BusMessage<E>>)>,
335    pub outstanding_responses: Vec<(E, Box<dyn BusMessage<E>>)>,
336}
337
338impl<E> BusDebugger<E>
339where
340    E: FieldElement<BaseField = Felt>,
341{
342    pub fn new(bus_name: String) -> Self {
343        Self {
344            bus_name,
345            outstanding_requests: Vec::new(),
346            outstanding_responses: Vec::new(),
347        }
348    }
349}
350
351impl<E> BusDebugger<E>
352where
353    E: FieldElement<BaseField = Felt>,
354{
355    /// Attempts to match the request with an existing response. If a match is found, the response
356    /// is removed from the list of outstanding responses. Otherwise, the request is added to the
357    /// list of outstanding requests.
358    #[allow(dead_code)]
359    pub fn add_request(&mut self, request_msg: Box<dyn BusMessage<E>>, alphas: &[E]) {
360        let msg_value = request_msg.value(alphas);
361
362        if let Some(pos) =
363            self.outstanding_responses.iter().position(|(value, _)| *value == msg_value)
364        {
365            self.outstanding_responses.swap_remove(pos);
366        } else {
367            self.outstanding_requests.push((msg_value, request_msg));
368        }
369    }
370
371    /// Attempts to match the response with an existing request. If a match is found, the request is
372    /// removed from the list of outstanding requests. Otherwise, the response is added to the list
373    /// of outstanding responses.
374    #[allow(dead_code)]
375    pub fn add_response(&mut self, response_msg: Box<dyn BusMessage<E>>, alphas: &[E]) {
376        let msg_value = response_msg.value(alphas);
377
378        if let Some(pos) =
379            self.outstanding_requests.iter().position(|(value, _)| *value == msg_value)
380        {
381            self.outstanding_requests.swap_remove(pos);
382        } else {
383            self.outstanding_responses.push((msg_value, response_msg));
384        }
385    }
386
387    /// Returns true if there are no outstanding requests or responses.
388    ///
389    /// This is meant to be called at the end of filling the bus. If there are any outstanding
390    /// requests or responses, it means that there is a mismatch between the requests and responses,
391    /// and the test should fail. The `Debug` implementation for `BusDebugger` will print out the
392    /// outstanding requests and responses.
393    #[allow(dead_code)]
394    pub fn is_empty(&self) -> bool {
395        self.outstanding_requests.is_empty() && self.outstanding_responses.is_empty()
396    }
397}
398
399impl<E> fmt::Display for BusDebugger<E>
400where
401    E: FieldElement<BaseField = Felt>,
402{
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        if self.is_empty() {
405            writeln!(f, "Bus '{}' is empty.", self.bus_name)?;
406        } else {
407            writeln!(f, "Bus '{}' construction failed.", self.bus_name)?;
408
409            if !self.outstanding_requests.is_empty() {
410                writeln!(f, "The following requests are still outstanding:")?;
411                for (_value, msg) in &self.outstanding_requests {
412                    writeln!(f, "- {}: {}", msg.source(), msg)?;
413                }
414            }
415
416            if !self.outstanding_responses.is_empty() {
417                writeln!(f, "\nThe following responses are still outstanding:")?;
418                for (_value, msg) in &self.outstanding_responses {
419                    writeln!(f, "- {}: {}", msg.source(), msg)?;
420                }
421            }
422        }
423
424        Ok(())
425    }
426}