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 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        // Use saturating_sub to prevent underflow when at clock 0
173        self.clk = self.clk.saturating_sub(1);
174
175        result
176    }
177
178    pub fn into_parts(self) -> (System, Decoder, Stack, Chiplets, Option<ExecutionError>) {
179        (self.system, self.decoder, self.stack, self.chiplets, self.error)
180    }
181
182    pub fn trace_len_summary(&self) -> &TraceLenSummary {
183        &self.trace_len_summary
184    }
185
186    /// Returns an instance of [TraceLenSummary] based on provided data.
187    fn build_trace_len_summary(
188        system: &System,
189        range: &mut RangeChecker,
190        chiplets: &Chiplets,
191    ) -> TraceLenSummary {
192        let clk = system.clk();
193        let range_table_len = range.get_number_range_checker_rows();
194        chiplets.append_range_checks(range);
195
196        TraceLenSummary::new(clk.into(), range_table_len, ChipletsLengths::new(chiplets))
197    }
198}
199
200impl Iterator for VmStateIterator {
201    type Item = Result<VmState, ExecutionError>;
202
203    fn next(&mut self) -> Option<Self::Item> {
204        if self.clk > self.system.clk() {
205            match &self.error {
206                Some(_) => {
207                    let error = core::mem::take(&mut self.error);
208                    return Some(Err(error.unwrap()));
209                },
210                None => return None,
211            }
212        }
213
214        // if we are changing iteration directions we must increment the clk counter
215        if !self.forward && self.clk < self.system.clk() {
216            self.clk += 1_u32;
217            self.forward = true;
218        }
219
220        let ctx = self.system.get_ctx_at(self.clk);
221
222        let op = if self.clk == 0 {
223            None
224        } else {
225            Some(self.decoder.debug_info().operations()[self.clk - 1])
226        };
227
228        let (asmop, is_start) = self.get_asmop();
229        if is_start {
230            self.asmop_idx += 1;
231        }
232
233        let result = Some(Ok(VmState {
234            clk: self.clk,
235            ctx,
236            op,
237            asmop,
238            fmp: self.system.get_fmp_at(self.clk),
239            stack: self.stack.get_state_at(self.clk),
240            memory: self.chiplets.memory.get_state_at(ctx, self.clk),
241        }));
242
243        self.clk += 1_u32;
244
245        result
246    }
247}
248
249/// Contains assembly instruction and operation index in the sequence corresponding to the specified
250/// AsmOp decorator. This index starts from 1 instead of 0.
251#[derive(Clone, Debug, Eq, PartialEq)]
252pub struct AsmOpInfo {
253    asmop: AssemblyOp,
254    cycle_idx: u8,
255}
256
257// ASMOP STATE
258// =================================================================
259
260impl AsmOpInfo {
261    /// Returns [AsmOpInfo] instantiated with the specified assembly instruction string, number of
262    /// cycles it takes to execute the assembly instruction and op index in sequence of operations
263    /// corresponding to the current assembly instruction. The first index is 1 instead of 0.
264    pub fn new(asmop: AssemblyOp, cycle_idx: u8) -> Self {
265        Self { asmop, cycle_idx }
266    }
267
268    /// Returns the context name for this operation.
269    pub fn context_name(&self) -> &str {
270        self.asmop.context_name()
271    }
272
273    /// Returns the assembly instruction corresponding to this state.
274    pub fn op(&self) -> &str {
275        self.asmop.op()
276    }
277
278    /// Returns the gerneralized form of assembly instruction corresponding to this state.
279    pub fn op_generalized(&self) -> String {
280        let op_vec: Vec<&str> = self.op().split('.').collect();
281        let keep_params = matches!(op_vec[0], "movdn" | "movup");
282        if !keep_params && op_vec.last().unwrap().parse::<usize>().is_ok() {
283            op_vec.split_last().unwrap().1.join(".")
284        } else {
285            self.op().to_string()
286        }
287    }
288
289    /// Returns the number of VM cycles taken to execute the assembly instruction.
290    pub fn num_cycles(&self) -> u8 {
291        self.asmop.num_cycles()
292    }
293
294    /// Returns the operation index of the operation at the specified clock cycle in the sequence
295    /// of operations corresponding to the current assembly instruction.
296    pub fn cycle_idx(&self) -> u8 {
297        self.cycle_idx
298    }
299
300    /// Returns `true` if the debug should break for this line.
301    pub const fn should_break(&self) -> bool {
302        self.asmop.should_break()
303    }
304}
305
306impl fmt::Display for AsmOpInfo {
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        write!(f, "{}, cycles={}", self.asmop, self.cycle_idx)
309    }
310}
311
312impl AsRef<AssemblyOp> for AsmOpInfo {
313    #[inline]
314    fn as_ref(&self) -> &AssemblyOp {
315        &self.asmop
316    }
317}
318
319// BUS DEBUGGING
320// =================================================================
321
322/// A message that can be sent on a bus.
323pub(crate) trait BusMessage<E: FieldElement<BaseField = Felt>>: fmt::Display {
324    /// The concrete value that this message evaluates to.
325    fn value(&self, alphas: &[E]) -> E;
326
327    /// The source of this message (e.g. "mload" or "memory chiplet").
328    fn source(&self) -> &str;
329}
330
331/// A debugger for a bus that can be used to track outstanding requests and responses.
332///
333/// Note: we use `Vec` internally instead of a `BTreeMap`, since messages can have collisions (i.e.
334/// 2 messages sent with the same key), which results in relatively complex insertion/deletion
335/// logic. Since this is only used in debug/test code, the performance hit is acceptable.
336pub(crate) struct BusDebugger<E: FieldElement<BaseField = Felt>> {
337    pub bus_name: String,
338    pub outstanding_requests: Vec<(E, Box<dyn BusMessage<E>>)>,
339    pub outstanding_responses: Vec<(E, Box<dyn BusMessage<E>>)>,
340}
341
342impl<E> BusDebugger<E>
343where
344    E: FieldElement<BaseField = Felt>,
345{
346    pub fn new(bus_name: String) -> Self {
347        Self {
348            bus_name,
349            outstanding_requests: Vec::new(),
350            outstanding_responses: Vec::new(),
351        }
352    }
353}
354
355impl<E> BusDebugger<E>
356where
357    E: FieldElement<BaseField = Felt>,
358{
359    /// Attempts to match the request with an existing response. If a match is found, the response
360    /// is removed from the list of outstanding responses. Otherwise, the request is added to the
361    /// list of outstanding requests.
362    #[allow(dead_code)]
363    pub fn add_request(&mut self, request_msg: Box<dyn BusMessage<E>>, alphas: &[E]) {
364        let msg_value = request_msg.value(alphas);
365
366        if let Some(pos) =
367            self.outstanding_responses.iter().position(|(value, _)| *value == msg_value)
368        {
369            self.outstanding_responses.swap_remove(pos);
370        } else {
371            self.outstanding_requests.push((msg_value, request_msg));
372        }
373    }
374
375    /// Attempts to match the response with an existing request. If a match is found, the request is
376    /// removed from the list of outstanding requests. Otherwise, the response is added to the list
377    /// of outstanding responses.
378    #[allow(dead_code)]
379    pub fn add_response(&mut self, response_msg: Box<dyn BusMessage<E>>, alphas: &[E]) {
380        let msg_value = response_msg.value(alphas);
381
382        if let Some(pos) =
383            self.outstanding_requests.iter().position(|(value, _)| *value == msg_value)
384        {
385            self.outstanding_requests.swap_remove(pos);
386        } else {
387            self.outstanding_responses.push((msg_value, response_msg));
388        }
389    }
390
391    /// Returns true if there are no outstanding requests or responses.
392    ///
393    /// This is meant to be called at the end of filling the bus. If there are any outstanding
394    /// requests or responses, it means that there is a mismatch between the requests and responses,
395    /// and the test should fail. The `Debug` implementation for `BusDebugger` will print out the
396    /// outstanding requests and responses.
397    #[allow(dead_code)]
398    pub fn is_empty(&self) -> bool {
399        self.outstanding_requests.is_empty() && self.outstanding_responses.is_empty()
400    }
401}
402
403impl<E> fmt::Display for BusDebugger<E>
404where
405    E: FieldElement<BaseField = Felt>,
406{
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        if self.is_empty() {
409            writeln!(f, "Bus '{}' is empty.", self.bus_name)?;
410        } else {
411            writeln!(f, "Bus '{}' construction failed.", self.bus_name)?;
412
413            if !self.outstanding_requests.is_empty() {
414                writeln!(f, "The following requests are still outstanding:")?;
415                for (_value, msg) in &self.outstanding_requests {
416                    writeln!(f, "- {}: {}", msg.source(), msg)?;
417                }
418            }
419
420            if !self.outstanding_responses.is_empty() {
421                writeln!(f, "\nThe following responses are still outstanding:")?;
422                for (_value, msg) in &self.outstanding_responses {
423                    writeln!(f, "- {}: {}", msg.source(), msg)?;
424                }
425            }
426        }
427
428        Ok(())
429    }
430}