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