Skip to main content

miden_core_lib/handlers/
debug.rs

1//! Event handlers backing the `miden::core::debug` print-debugging module.
2//!
3//! Each `miden::core::debug::print_*` procedure emits a well-known event. This module registers a
4//! single [`DebugPrinter`] handler for all of those events; when one fires, the handler reads the
5//! requested piece of VM state (operand stack, memory, advice stack, or advice map) and prints it
6//! using the VM's tree-style debug formatting via [`miden_processor::write_stack`] /
7//! [`miden_processor::write_interval`]. A range-based procedure may share an event with its
8//! full-state variant when the full-state behavior can be represented as an unbounded range (e.g.
9//! the advice stack); memory uses a dedicated full-state event because `print_mem` enumerates its
10//! (capped) range while `print_mem_all` lists only initialized cells.
11//!
12//! These are ordinary `emit` events: they carry no MAST/decorator cost and print whenever the
13//! procedure is executed.
14
15use alloc::{
16    format,
17    string::{String, ToString},
18    sync::Arc,
19    vec,
20    vec::Vec,
21};
22use core::fmt;
23
24use miden_core::{Felt, Word};
25use miden_processor::{
26    MemoryError, ProcessorState, StdoutWriter,
27    advice::AdviceMutation,
28    event::{EventError, EventHandler, EventId, EventName},
29    write_interval, write_stack,
30};
31use miden_utils_sync::RwLock;
32
33// EVENT NAMES
34// ================================================================================================
35
36/// Prints the entire operand stack.
37pub const PRINT_STACK_EVENT_NAME: EventName = EventName::new("miden::core::debug::print_stack");
38/// Prints memory in the range `[start, end)` of the current context.
39pub const PRINT_MEM_EVENT_NAME: EventName = EventName::new("miden::core::debug::print_mem");
40/// Prints the entire memory of the current context.
41pub const PRINT_MEM_ALL_EVENT_NAME: EventName = EventName::new("miden::core::debug::print_mem_all");
42/// Prints the advice stack in the range `[start, end)`.
43pub const PRINT_ADV_STACK_EVENT_NAME: EventName =
44    EventName::new("miden::core::debug::print_adv_stack");
45/// Prints the full advice map.
46pub const PRINT_ADV_MAP_EVENT_NAME: EventName = EventName::new("miden::core::debug::print_adv_map");
47/// Looks up a WORD key in the advice map and prints the associated values.
48pub const PRINT_ADV_MAP_ITEM_EVENT_NAME: EventName =
49    EventName::new("miden::core::debug::print_adv_map_item");
50
51/// Maximum number of addresses an explicit `print_mem` / `print_mem_addr` range may span.
52///
53/// Guards against a caller accidentally passing an enormous range. Use `print_mem_all` (which has
54/// its own event) to print the entire memory of the current context.
55const MAX_PRINT_MEM_RANGE: u64 = 1024;
56
57/// Returns the default `(EventName, handler)` pairs for print-style debugging.
58///
59/// The default set prints operand-stack and memory state to stdout. Advice-stack and advice-map
60/// printers are excluded because they may expose witness data.
61pub fn default_debug_handlers() -> Vec<(EventName, Arc<dyn EventHandler>)> {
62    let printer: Arc<dyn EventHandler> = Arc::new(DebugPrinter::default());
63    vec![
64        (PRINT_STACK_EVENT_NAME, printer.clone()),
65        (PRINT_MEM_EVENT_NAME, printer.clone()),
66        (PRINT_MEM_ALL_EVENT_NAME, printer),
67    ]
68}
69
70/// Returns no-op handlers for every `miden::core::debug` print event.
71///
72/// Privacy-sensitive hosts can use these to replace the default stdout handlers while still
73/// allowing programs that emit debug events to execute.
74pub fn noop_debug_handlers() -> Vec<(EventName, Arc<dyn EventHandler>)> {
75    let handler: Arc<dyn EventHandler> = Arc::new(NoopDebugHandler);
76    vec![
77        (PRINT_STACK_EVENT_NAME, handler.clone()),
78        (PRINT_MEM_EVENT_NAME, handler.clone()),
79        (PRINT_MEM_ALL_EVENT_NAME, handler.clone()),
80        (PRINT_ADV_STACK_EVENT_NAME, handler.clone()),
81        (PRINT_ADV_MAP_EVENT_NAME, handler.clone()),
82        (PRINT_ADV_MAP_ITEM_EVENT_NAME, handler),
83    ]
84}
85
86/// Returns the `(EventName, handler)` pairs that back the full `miden::core::debug` module.
87///
88/// All events share a single [`DebugPrinter`] instance writing to stdout.
89pub fn debug_handlers() -> Vec<(EventName, Arc<dyn EventHandler>)> {
90    let printer: Arc<dyn EventHandler> = Arc::new(DebugPrinter::default());
91    vec![
92        (PRINT_STACK_EVENT_NAME, printer.clone()),
93        (PRINT_MEM_EVENT_NAME, printer.clone()),
94        (PRINT_MEM_ALL_EVENT_NAME, printer.clone()),
95        (PRINT_ADV_STACK_EVENT_NAME, printer.clone()),
96        (PRINT_ADV_MAP_EVENT_NAME, printer.clone()),
97        (PRINT_ADV_MAP_ITEM_EVENT_NAME, printer),
98    ]
99}
100
101/// Returns the opt-in `(EventName, handler)` pairs for advice-backed debug events.
102///
103/// [`CoreLibrary::handlers`](crate::CoreLibrary::handlers) registers stack and memory debug
104/// handlers by default. This helper returns only the advice-stack and advice-map handlers so hosts
105/// can extend the core library handlers without duplicate event registrations.
106///
107/// All returned events share a single [`DebugPrinter`] instance writing to stdout.
108pub fn advice_debug_handlers() -> Vec<(EventName, Arc<dyn EventHandler>)> {
109    let printer: Arc<dyn EventHandler> = Arc::new(DebugPrinter::default());
110    vec![
111        (PRINT_ADV_STACK_EVENT_NAME, printer.clone()),
112        (PRINT_ADV_MAP_EVENT_NAME, printer.clone()),
113        (PRINT_ADV_MAP_ITEM_EVENT_NAME, printer),
114    ]
115}
116
117// DEBUG PRINTER
118// ================================================================================================
119
120/// Handles all `miden::core::debug::print_*` events by printing VM state to its writer.
121///
122/// The writer is guarded by an [`RwLock`] because [`EventHandler::on_event`] takes `&self`. The
123/// default writer prints to stdout (under the `std` feature); a custom writer (e.g. an in-memory
124/// buffer) can be supplied via [`DebugPrinter::new`] for testing.
125pub struct DebugPrinter<W: fmt::Write + Send + Sync = StdoutWriter> {
126    writer: RwLock<W>,
127}
128
129impl Default for DebugPrinter<StdoutWriter> {
130    fn default() -> Self {
131        Self { writer: RwLock::new(StdoutWriter) }
132    }
133}
134
135impl<W: fmt::Write + Send + Sync> DebugPrinter<W> {
136    /// Creates a new [`DebugPrinter`] writing to the provided writer.
137    pub fn new(writer: W) -> Self {
138        Self { writer: RwLock::new(writer) }
139    }
140}
141
142impl<W: fmt::Write + Send + Sync + 'static> EventHandler for DebugPrinter<W> {
143    fn on_event(&self, process: &ProcessorState) -> Result<Vec<AdviceMutation>, EventError> {
144        // The event id sits at the top of the stack (position 0); the procedure's arguments, if
145        // any, are immediately below it.
146        let id = EventId::from_felt(process.get_stack_item(0));
147        let mut writer = self.writer.write();
148        let w: &mut W = &mut writer;
149
150        if id == PRINT_STACK_EVENT_NAME.to_event_id() {
151            // Skip position 0 (the event id) so only the user's operand stack is shown. Print the
152            // entire stack (no cap).
153            let stack = process.get_stack_state();
154            let operand_stack = stack.get(1..).unwrap_or(&[]);
155            write_stack(w, operand_stack, None, "Stack", process.clock())?;
156        } else if id == PRINT_MEM_EVENT_NAME.to_event_id() {
157            let bounds = read_mem_print_range(process, 1, 2)?;
158            // Guard against an accidentally huge explicit range.
159            if let Some((first, last)) = bounds {
160                let len = u64::from(last - first) + 1;
161                if len > MAX_PRINT_MEM_RANGE {
162                    return Err(format!(
163                        "print_mem range length {len} exceeds maximum of {MAX_PRINT_MEM_RANGE}"
164                    )
165                    .into());
166                }
167            }
168            write_mem_range(w, process, bounds)?;
169        } else if id == PRINT_MEM_ALL_EVENT_NAME.to_event_id() {
170            write_mem_all(w, process)?;
171        } else if id == PRINT_ADV_STACK_EVENT_NAME.to_event_id() {
172            let start = stack_item_as_usize(process, 1);
173            let end = stack_item_as_usize(process, 2);
174            let adv_stack = process.advice_provider().stack();
175            let slice = slice_range(&adv_stack, start, end);
176            write_stack(w, slice, None, "Advice stack", process.clock())?;
177        } else if id == PRINT_ADV_MAP_EVENT_NAME.to_event_id() {
178            write_adv_map(w, process)?;
179        } else if id == PRINT_ADV_MAP_ITEM_EVENT_NAME.to_event_id() {
180            write_adv_map_entry(w, process)?;
181        }
182        // Unknown ids are ignored: the handler is only registered for the events above.
183
184        Ok(Vec::new())
185    }
186}
187
188struct NoopDebugHandler;
189
190impl EventHandler for NoopDebugHandler {
191    fn on_event(&self, _process: &ProcessorState) -> Result<Vec<AdviceMutation>, EventError> {
192        Ok(Vec::new())
193    }
194}
195
196// HELPERS
197// ================================================================================================
198
199/// Reads the element at `pos` on the operand stack as a `usize` (saturating).
200fn stack_item_as_usize(process: &ProcessorState, pos: usize) -> usize {
201    usize::try_from(process.get_stack_item(pos).as_canonical_u64()).unwrap_or(usize::MAX)
202}
203
204/// Returns `slice[start..end]`, clamped to the bounds of `slice` and to `start <= end`.
205fn slice_range(slice: &[Felt], start: usize, end: usize) -> &[Felt] {
206    let len = slice.len();
207    let start = start.min(len);
208    let end = end.clamp(start, len);
209    &slice[start..end]
210}
211
212/// Reads a half-open `[start, end)` memory range off the operand stack and returns it as inclusive
213/// `(first, last)` `u32` address bounds, or `None` if the range is empty (`start == end`).
214///
215/// Memory addresses are `u32`, so both bounds are valid `u32` values. The half-open end may be
216/// `2^32` (one past the last address) so the cell at `u32::MAX` stays reachable; it folds into an
217/// inclusive end of `u32::MAX`.
218fn read_mem_print_range(
219    process: &ProcessorState,
220    start_idx: usize,
221    end_idx: usize,
222) -> Result<Option<(u32, u32)>, MemoryError> {
223    let start_addr = process.get_stack_item(start_idx).as_canonical_u64();
224    let end_addr = process.get_stack_item(end_idx).as_canonical_u64();
225
226    if start_addr > u32::MAX as u64 {
227        return Err(MemoryError::AddressOutOfBounds { addr: start_addr });
228    }
229    // The exclusive end may be one past the last valid address (`2^32`).
230    if end_addr > u32::MAX as u64 + 1 {
231        return Err(MemoryError::AddressOutOfBounds { addr: end_addr });
232    }
233    if start_addr > end_addr {
234        return Err(MemoryError::InvalidMemoryRange { start_addr, end_addr });
235    }
236
237    if start_addr == end_addr {
238        Ok(None)
239    } else {
240        // Subtract in `u64` before the cast, since `end_addr` may be `2^32`; the result is in
241        // `[0, u32::MAX]`.
242        Ok(Some((start_addr as u32, (end_addr - 1) as u32)))
243    }
244}
245
246/// Prints every memory cell in the inclusive `[first, last]` bounds for the current context,
247/// showing uninitialized cells as `EMPTY`. `None` denotes an empty range.
248///
249/// The bounds are inclusive so the cell at `u32::MAX` can be printed without the end overflowing
250/// `u32`. The caller is responsible for capping the range length (see [`MAX_PRINT_MEM_RANGE`]).
251fn write_mem_range<W: fmt::Write>(
252    w: &mut W,
253    process: &ProcessorState,
254    bounds: Option<(u32, u32)>,
255) -> fmt::Result {
256    let (ctx, clk) = (process.ctx(), process.clock());
257    let Some((start, end)) = bounds else {
258        return writeln!(w, "Memory state before step {clk} for context {ctx}: range is empty.");
259    };
260    writeln!(
261        w,
262        "Memory state before step {clk} for context {ctx} in the range [{start}, {end}]:",
263    )?;
264    let items: Vec<_> = (start..=end)
265        .map(|addr| {
266            let value = process.get_mem_value(ctx, addr).map(|v| v.to_string());
267            (format!("{addr:#010x}"), value)
268        })
269        .collect();
270    write_interval(w, items, None)
271}
272
273/// Prints all initialized memory cells of the current context.
274fn write_mem_all<W: fmt::Write>(w: &mut W, process: &ProcessorState) -> fmt::Result {
275    let (ctx, clk) = (process.ctx(), process.clock());
276    writeln!(w, "Memory state before step {clk} for context {ctx}:")?;
277    let items: Vec<_> = process
278        .get_mem_state(ctx)
279        .into_iter()
280        .map(|(addr, value)| (format!("{addr:#010x}"), Some(value.to_string())))
281        .collect();
282    write_interval(w, items, None)
283}
284
285/// Prints the full advice map.
286fn write_adv_map<W: fmt::Write>(w: &mut W, process: &ProcessorState) -> fmt::Result {
287    let clk = process.clock();
288    let map = process.advice_provider().map();
289    if map.is_empty() {
290        return writeln!(w, "Advice map before step {clk}: empty.");
291    }
292
293    writeln!(w, "Advice map before step {clk}:")?;
294    let items: Vec<_> = map
295        .iter()
296        .map(|(key, values)| (format_word(key), Some(format_felt_slice(values))))
297        .collect();
298    write_interval(w, items, None)
299}
300
301/// Looks up the WORD key (at stack positions 1..5) in the advice map and prints its values.
302fn write_adv_map_entry<W: fmt::Write>(w: &mut W, process: &ProcessorState) -> fmt::Result {
303    let key = process.get_stack_word(1);
304    let key_str = format_word(&key);
305    let clk = process.clock();
306    match process.advice_provider().get_mapped_values(&key) {
307        Some(values) => {
308            writeln!(w, "Advice map entry for key {key_str} before step {clk}:")?;
309            let items: Vec<_> = values
310                .iter()
311                .enumerate()
312                .map(|(i, v)| (i.to_string(), Some(v.to_string())))
313                .collect();
314            write_interval(w, items, None)
315        },
316        None => writeln!(w, "No advice map entry for key {key_str} before step {clk}."),
317    }
318}
319
320fn format_word(word: &Word) -> String {
321    format!("[{}, {}, {}, {}]", word[0], word[1], word[2], word[3])
322}
323
324fn format_felt_slice(values: &[Felt]) -> String {
325    let mut out = String::from("[");
326    for (idx, value) in values.iter().enumerate() {
327        if idx > 0 {
328            out.push_str(", ");
329        }
330        out.push_str(&value.to_string());
331    }
332    out.push(']');
333    out
334}