Skip to main content

miden_debug_engine/debug/
breakpoint.rs

1use alloc::string::{String, ToString};
2use core::{ops::Deref, str::FromStr};
3
4use miden_processor::ProcessorState;
5
6use super::ResolvedLocation;
7use crate::{Event, glob::GlobMatcher};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Breakpoint {
11    pub id: u8,
12    pub creation_cycle: usize,
13    pub ty: BreakpointType,
14}
15
16impl Default for Breakpoint {
17    fn default() -> Self {
18        Self {
19            id: 0,
20            creation_cycle: 0,
21            ty: BreakpointType::Step,
22        }
23    }
24}
25
26impl Breakpoint {
27    /// Create a new default `Breakpoint` of the given type
28    pub fn new(ty: BreakpointType) -> Self {
29        Self {
30            ty,
31            ..Default::default()
32        }
33    }
34
35    /// Return the number of cycles remaining, as of `current_cycle`, before this breakpoint
36    /// should trigger (zero means it should trigger now), or `None` if the breakpoint is
37    /// triggered by something other than cycle count, or its target cycle has already passed.
38    pub fn cycles_to_skip(&self, current_cycle: usize) -> Option<usize> {
39        let cycles_passed = current_cycle - self.creation_cycle;
40        match &self.ty {
41            BreakpointType::Step => Some(1usize.saturating_sub(cycles_passed)),
42            BreakpointType::StepN(n) => Some(n.saturating_sub(cycles_passed)),
43            BreakpointType::StepTo(to) if to >= &current_cycle => Some(to - current_cycle),
44            _ => None,
45        }
46    }
47}
48impl Deref for Breakpoint {
49    type Target = BreakpointType;
50
51    #[inline]
52    fn deref(&self) -> &Self::Target {
53        &self.ty
54    }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum BreakpointType {
59    /// Break at next cycle
60    Step,
61    /// Skip N cycles
62    StepN(usize),
63    /// Break at a given cycle
64    StepTo(usize),
65    /// Break at the first cycle of the next instruction
66    Next,
67    /// Break at the next source line, or the next instruction if no source location is available.
68    NextLine,
69    /// Break when we exit the current call frame
70    Finish,
71    /// Break when any cycle corresponds to a source location whose file matches PATTERN
72    File(GlobMatcher),
73    /// Break when any cycle corresponds to a source location whose file matches PATTERN and occurs
74    /// on LINE
75    Line { pattern: GlobMatcher, line: u32 },
76    /// Break anytime the given operation occurs
77    Opcode(OperationMatcher),
78    /// Break when any cycle causes us to push a frame for PROCEDURE on the call stack
79    Called(GlobMatcher),
80    /// Break when the given event is emitted
81    Event(Event),
82}
83
84impl BreakpointType {
85    /// Return true if this breakpoint indicates we should break for `current_op`
86    pub fn should_break_for(
87        &self,
88        current_op: &miden_core::operations::Operation,
89        state: &ProcessorState<'_>,
90    ) -> bool {
91        use miden_core::operations::Operation;
92
93        match self {
94            Self::Opcode(matcher) => matcher.should_break_for(current_op),
95            Self::Event(event) if matches!(current_op, Operation::Emit) => {
96                state.get_stack_item(0) == event.as_event_id().as_felt()
97            }
98            _ => false,
99        }
100    }
101
102    /// Return true if this breakpoint indicates we should break on entry to `procedure`
103    pub fn should_break_in(&self, procedure: &str) -> bool {
104        match self {
105            Self::Called(pattern) => procedure_matches(pattern, procedure),
106            _ => false,
107        }
108    }
109
110    /// Return true if this breakpoint indicates we should break at `loc`
111    pub fn should_break_at(&self, loc: &ResolvedLocation) -> bool {
112        let uri = loc.source_file.uri();
113        match self {
114            Self::File(pattern) => pattern.is_match(uri),
115            Self::Line { pattern, line } if line == &loc.line => pattern.is_match(uri),
116            _ => false,
117        }
118    }
119
120    /// Returns true if this breakpoint is internal to the debugger (i.e. not creatable via :b)
121    pub fn is_internal(&self) -> bool {
122        matches!(
123            self,
124            BreakpointType::Next
125                | BreakpointType::NextLine
126                | BreakpointType::Step
127                | BreakpointType::Finish
128        )
129    }
130
131    /// Returns true if this breakpoint is removed upon being hit
132    pub fn is_one_shot(&self) -> bool {
133        matches!(
134            self,
135            BreakpointType::Next
136                | BreakpointType::NextLine
137                | BreakpointType::Finish
138                | BreakpointType::Step
139                | BreakpointType::StepN(_)
140                | BreakpointType::StepTo(_)
141        )
142    }
143}
144
145impl FromStr for BreakpointType {
146    type Err = String;
147
148    fn from_str(s: &str) -> Result<Self, Self::Err> {
149        let s = s.trim();
150
151        // b next
152        // b finish
153        // b after {n}
154        // b for {opcode}
155        // b at {cycle}
156        // b in {procedure}
157        // b {file}[:{line}]
158        if s == "next" {
159            return Ok(BreakpointType::Next);
160        }
161        if s == "finish" {
162            return Ok(BreakpointType::Finish);
163        }
164        if let Some(n) = s.strip_prefix("after ") {
165            let n = n.trim().parse::<usize>().map_err(|err| {
166                format!("invalid breakpoint expression: could not parse cycle count: {err}")
167            })?;
168            return Ok(BreakpointType::StepN(n));
169        }
170        if let Some(opcode) = s.strip_prefix("for ") {
171            return Ok(BreakpointType::Opcode(opcode.parse::<OperationMatcher>()?));
172        }
173        if let Some(cycle) = s.strip_prefix("at ") {
174            let cycle = cycle.trim().parse::<usize>().map_err(|err| {
175                format!("invalid breakpoint expression: could not parse cycle value: {err}")
176            })?;
177            return Ok(BreakpointType::StepTo(cycle));
178        }
179        if let Some(procedure) = s.strip_prefix("in ") {
180            return Ok(BreakpointType::Called(procedure_pattern(procedure)?));
181        }
182        match s.split_once(':') {
183            Some((file, line)) => {
184                let pattern = file_pattern(file)?;
185                let line = line.trim().parse::<u32>().map_err(|err| {
186                    format!("invalid breakpoint expression: could not parse line: {err}")
187                })?;
188                Ok(BreakpointType::Line { pattern, line })
189            }
190            None => Ok(BreakpointType::File(file_pattern(s)?)),
191        }
192    }
193}
194
195/// Compile a user-provided procedure spec into a glob pattern.
196///
197/// Procedures are matched against their fully-qualified name, including the
198/// package qualification (e.g. `::"root_ns:root@1.0.0"::fibonacci::entrypoint`),
199/// which users rarely know or type. Anchor unqualified specs with a leading
200/// `*::` so they match by trailing path components: `entrypoint` and
201/// `fibonacci::entrypoint` both match the example above, while a partial
202/// component like `point` does not.
203pub(crate) fn procedure_pattern(spec: &str) -> Result<GlobMatcher, String> {
204    let spec = spec.trim();
205    let anchored;
206    let spec = if spec.starts_with("::") || spec.starts_with('*') {
207        spec
208    } else {
209        anchored = format!("*::{spec}");
210        &anchored
211    };
212    crate::glob::GlobBuilder::new(spec)
213        .build()
214        .map(|glob| glob.compile_matcher())
215        .map_err(|err| format!("invalid breakpoint pattern: {err}"))
216}
217
218/// Match a procedure pattern against a fully-qualified procedure name.
219pub(crate) fn procedure_matches(pattern: &GlobMatcher, procedure: &str) -> bool {
220    let raw = miden_debug_types::Uri::from(format!("file://{procedure}"));
221    if pattern.is_match(&raw) {
222        return true;
223    }
224    let demangled = super::stacktrace::demangle(procedure);
225    demangled != procedure
226        && pattern.is_match(&miden_debug_types::Uri::from(format!("file://{demangled}")))
227}
228
229/// Compile a user-provided file spec into a glob pattern.
230///
231/// Source locations in debug info are stored as absolute paths, so a relative
232/// spec like `src/lib.rs` would never match as-is. Anchor relative specs with
233/// a leading `**/` so they match by path suffix, like gdb's `break FILE:LINE`.
234fn file_pattern(spec: &str) -> Result<GlobMatcher, String> {
235    let spec = spec.trim();
236    let anchored;
237    let spec = if spec.starts_with('/') || spec.starts_with('*') {
238        spec
239    } else {
240        anchored = format!("**/{spec}");
241        &anchored
242    };
243    crate::glob::GlobBuilder::new(spec)
244        .build()
245        .map(|glob| glob.compile_matcher())
246        .map_err(|err| format!("invalid breakpoint pattern: {err}"))
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250#[non_exhaustive]
251pub enum OperationMatcher {
252    Asm(String),
253    Exact(miden_core::operations::Operation),
254    Assert,
255    Push,
256    Dup,
257    SwapW,
258    Movup,
259    Movdn,
260}
261
262impl OperationMatcher {
263    pub fn should_break_for(&self, op: &miden_core::operations::Operation) -> bool {
264        use miden_core::operations::Operation;
265        match self {
266            Self::Asm(_) => false,
267            Self::Exact(expected) => op == expected,
268            Self::Assert => matches!(op, Operation::Assert(_)),
269            Self::Push => matches!(op, Operation::Push(_)),
270            Self::Dup => matches!(
271                op,
272                Operation::Dup0
273                    | Operation::Dup1
274                    | Operation::Dup2
275                    | Operation::Dup3
276                    | Operation::Dup4
277                    | Operation::Dup5
278                    | Operation::Dup6
279                    | Operation::Dup7
280                    | Operation::Dup9
281                    | Operation::Dup11
282                    | Operation::Dup13
283                    | Operation::Dup15
284            ),
285            Self::SwapW => matches!(op, Operation::SwapW | Operation::SwapW2 | Operation::SwapW3),
286            Self::Movup => matches!(
287                op,
288                Operation::MovUp2
289                    | Operation::MovUp3
290                    | Operation::MovUp4
291                    | Operation::MovUp5
292                    | Operation::MovUp6
293                    | Operation::MovUp7
294                    | Operation::MovUp8
295            ),
296            Self::Movdn => matches!(
297                op,
298                Operation::MovDn2
299                    | Operation::MovDn3
300                    | Operation::MovDn4
301                    | Operation::MovDn5
302                    | Operation::MovDn6
303                    | Operation::MovDn7
304                    | Operation::MovDn8
305            ),
306        }
307    }
308}
309
310impl core::fmt::Display for OperationMatcher {
311    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312        match self {
313            Self::Asm(op) => f.write_str(op),
314            Self::Exact(op) => core::fmt::Display::fmt(op, f),
315            Self::Assert => f.write_str("assert.*"),
316            Self::Push => f.write_str("push.*"),
317            Self::Dup => f.write_str("dup"),
318            Self::SwapW => f.write_str("swapw"),
319            Self::Movup => f.write_str("movup"),
320            Self::Movdn => f.write_str("movdn"),
321        }
322    }
323}
324
325impl FromStr for OperationMatcher {
326    type Err = String;
327
328    fn from_str(name: &str) -> Result<Self, Self::Err> {
329        use miden_core::operations::Operation::*;
330        let opcode_parts = name
331            .split_once('.')
332            .map(|(name, rest)| (name, Some(rest)))
333            .unwrap_or((name, None));
334        let opcode = match opcode_parts {
335            ("nop" | "noop", _) => Noop,
336            ("assert", Some("*")) => return Ok(OperationMatcher::Assert),
337            ("assert", Some(code)) => Assert(
338                code.parse::<u32>()
339                    .map(miden_core::Felt::from_u32)
340                    .map_err(|err| err.to_string())?,
341            ),
342            ("assert", None) => Assert(miden_core::Felt::from_u32(0)),
343            ("sdepth", None) => SDepth,
344            ("caller", None) => Caller,
345            ("clk", None) => Clk,
346            ("emit", None) => Emit,
347            ("add", None) => Add,
348            ("neg", None) => Neg,
349            ("mul", None) => Mul,
350            ("inv", None) => Inv,
351            ("incr", None) => Incr,
352            ("and", None) => And,
353            ("or", None) => Or,
354            ("not", None) => Not,
355            ("eq", None) => Eq,
356            ("eqz", None) => Eqz,
357            ("expacc", None) => Expacc,
358            ("ext2mul", None) => Ext2Mul,
359            ("u32split", None) => U32split,
360            ("u32add", None) => U32add,
361            ("u32assert2", Some(code)) => U32assert2(
362                code.parse::<u32>()
363                    .map(miden_core::Felt::from_u32)
364                    .map_err(|err| err.to_string())?,
365            ),
366            ("u32assert2", None) => U32assert2(miden_core::Felt::from_u32(0)),
367            ("u32add3", None) => U32add3,
368            ("u32sub", None) => U32sub,
369            ("u32mul", None) => U32mul,
370            ("u32madd", None) => U32madd,
371            ("u32div", None) => U32div,
372            ("u32and", None) => U32and,
373            ("u32xor", None) => U32xor,
374            ("pad", None) => Pad,
375            ("drop", None) => Drop,
376            ("dup", _) => return Ok(OperationMatcher::Dup),
377            ("swap", _) => Swap,
378            ("swapw", _) => return Ok(OperationMatcher::SwapW),
379            ("swapdw", _) => SwapDW,
380            ("movup", _) => return Ok(OperationMatcher::Movup),
381            ("movdn", _) => return Ok(OperationMatcher::Movdn),
382            ("cswap", _) => CSwap,
383            ("cswapw", _) => CSwapW,
384            ("push", _) => return Ok(OperationMatcher::Push),
385            ("advpop", _) => AdvPop,
386            ("advpopw", _) => AdvPopW,
387            ("mloadw", _) => MLoadW,
388            ("mstorew", _) => MStoreW,
389            ("mload", _) => MLoad,
390            ("mstore", _) => MStore,
391            ("mstream", _) => MStream,
392            ("pipe", _) => Pipe,
393            ("crypto_stream", _) => CryptoStream,
394            ("hperm", _) => HPerm,
395            ("mpverify", Some(code)) => MpVerify(
396                code.parse::<u32>()
397                    .map(miden_core::Felt::from_u32)
398                    .map_err(|err| err.to_string())?,
399            ),
400            ("mpverify", None) => MpVerify(miden_core::Felt::from_u32(0)),
401            ("mrupdate", None) => MrUpdate,
402            ("frie2f4", None) => FriE2F4,
403            ("horner_base", None) => HornerBase,
404            ("horner_ext", None) => HornerExt,
405            ("eval_circuit", None) => EvalCircuit,
406            ("log_deferred" | "log_precompile", None) => LogDeferred,
407            _ => return Ok(OperationMatcher::Asm(name.to_string())),
408        };
409
410        Ok(OperationMatcher::Exact(opcode))
411    }
412}
413
414#[cfg(test)]
415mod tests;