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        let procedure = miden_debug_types::Uri::from(format!("file://{procedure}"));
105        match self {
106            Self::Called(pattern) => pattern.is_match(&procedure),
107            _ => false,
108        }
109    }
110
111    /// Return true if this breakpoint indicates we should break at `loc`
112    pub fn should_break_at(&self, loc: &ResolvedLocation) -> bool {
113        let uri = loc.source_file.uri();
114        match self {
115            Self::File(pattern) => pattern.is_match(uri),
116            Self::Line { pattern, line } if line == &loc.line => pattern.is_match(uri),
117            _ => false,
118        }
119    }
120
121    /// Returns true if this breakpoint is internal to the debugger (i.e. not creatable via :b)
122    pub fn is_internal(&self) -> bool {
123        matches!(
124            self,
125            BreakpointType::Next
126                | BreakpointType::NextLine
127                | BreakpointType::Step
128                | BreakpointType::Finish
129        )
130    }
131
132    /// Returns true if this breakpoint is removed upon being hit
133    pub fn is_one_shot(&self) -> bool {
134        matches!(
135            self,
136            BreakpointType::Next
137                | BreakpointType::NextLine
138                | BreakpointType::Finish
139                | BreakpointType::Step
140                | BreakpointType::StepN(_)
141                | BreakpointType::StepTo(_)
142        )
143    }
144}
145
146impl FromStr for BreakpointType {
147    type Err = String;
148
149    fn from_str(s: &str) -> Result<Self, Self::Err> {
150        let s = s.trim();
151
152        // b next
153        // b finish
154        // b after {n}
155        // b for {opcode}
156        // b at {cycle}
157        // b in {procedure}
158        // b {file}[:{line}]
159        if s == "next" {
160            return Ok(BreakpointType::Next);
161        }
162        if s == "finish" {
163            return Ok(BreakpointType::Finish);
164        }
165        if let Some(n) = s.strip_prefix("after ") {
166            let n = n.trim().parse::<usize>().map_err(|err| {
167                format!("invalid breakpoint expression: could not parse cycle count: {err}")
168            })?;
169            return Ok(BreakpointType::StepN(n));
170        }
171        if let Some(opcode) = s.strip_prefix("for ") {
172            return Ok(BreakpointType::Opcode(opcode.parse::<OperationMatcher>()?));
173        }
174        if let Some(cycle) = s.strip_prefix("at ") {
175            let cycle = cycle.trim().parse::<usize>().map_err(|err| {
176                format!("invalid breakpoint expression: could not parse cycle value: {err}")
177            })?;
178            return Ok(BreakpointType::StepTo(cycle));
179        }
180        if let Some(procedure) = s.strip_prefix("in ") {
181            return Ok(BreakpointType::Called(procedure_pattern(procedure)?));
182        }
183        match s.split_once(':') {
184            Some((file, line)) => {
185                let pattern = file_pattern(file)?;
186                let line = line.trim().parse::<u32>().map_err(|err| {
187                    format!("invalid breakpoint expression: could not parse line: {err}")
188                })?;
189                Ok(BreakpointType::Line { pattern, line })
190            }
191            None => Ok(BreakpointType::File(file_pattern(s)?)),
192        }
193    }
194}
195
196/// Compile a user-provided procedure spec into a glob pattern.
197///
198/// Procedures are matched against their fully-qualified name, including the
199/// package qualification (e.g. `::"root_ns:root@1.0.0"::fibonacci::entrypoint`),
200/// which users rarely know or type. Anchor unqualified specs with a leading
201/// `*::` so they match by trailing path components: `entrypoint` and
202/// `fibonacci::entrypoint` both match the example above, while a partial
203/// component like `point` does not.
204fn procedure_pattern(spec: &str) -> Result<GlobMatcher, String> {
205    let spec = spec.trim();
206    let anchored;
207    let spec = if spec.starts_with("::") || spec.starts_with('*') {
208        spec
209    } else {
210        anchored = format!("*::{spec}");
211        &anchored
212    };
213    crate::glob::GlobBuilder::new(spec)
214        .build()
215        .map(|glob| glob.compile_matcher())
216        .map_err(|err| format!("invalid breakpoint pattern: {err}"))
217}
218
219/// Compile a user-provided file spec into a glob pattern.
220///
221/// Source locations in debug info are stored as absolute paths, so a relative
222/// spec like `src/lib.rs` would never match as-is. Anchor relative specs with
223/// a leading `**/` so they match by path suffix, like gdb's `break FILE:LINE`.
224fn file_pattern(spec: &str) -> Result<GlobMatcher, String> {
225    let spec = spec.trim();
226    let anchored;
227    let spec = if spec.starts_with('/') || spec.starts_with('*') {
228        spec
229    } else {
230        anchored = format!("**/{spec}");
231        &anchored
232    };
233    crate::glob::GlobBuilder::new(spec)
234        .build()
235        .map(|glob| glob.compile_matcher())
236        .map_err(|err| format!("invalid breakpoint pattern: {err}"))
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240#[non_exhaustive]
241pub enum OperationMatcher {
242    Asm(String),
243    Exact(miden_core::operations::Operation),
244    Assert,
245    Push,
246    Dup,
247    SwapW,
248    Movup,
249    Movdn,
250}
251
252impl OperationMatcher {
253    pub fn should_break_for(&self, op: &miden_core::operations::Operation) -> bool {
254        use miden_core::operations::Operation;
255        match self {
256            Self::Asm(_) => false,
257            Self::Exact(expected) => op == expected,
258            Self::Assert => matches!(op, Operation::Assert(_)),
259            Self::Push => matches!(op, Operation::Push(_)),
260            Self::Dup => matches!(
261                op,
262                Operation::Dup0
263                    | Operation::Dup1
264                    | Operation::Dup2
265                    | Operation::Dup3
266                    | Operation::Dup4
267                    | Operation::Dup5
268                    | Operation::Dup6
269                    | Operation::Dup7
270                    | Operation::Dup9
271                    | Operation::Dup11
272                    | Operation::Dup13
273                    | Operation::Dup15
274            ),
275            Self::SwapW => matches!(op, Operation::SwapW | Operation::SwapW2 | Operation::SwapW3),
276            Self::Movup => matches!(
277                op,
278                Operation::MovUp2
279                    | Operation::MovUp3
280                    | Operation::MovUp4
281                    | Operation::MovUp5
282                    | Operation::MovUp6
283                    | Operation::MovUp7
284                    | Operation::MovUp8
285            ),
286            Self::Movdn => matches!(
287                op,
288                Operation::MovDn2
289                    | Operation::MovDn3
290                    | Operation::MovDn4
291                    | Operation::MovDn5
292                    | Operation::MovDn6
293                    | Operation::MovDn7
294                    | Operation::MovDn8
295            ),
296        }
297    }
298}
299
300impl core::fmt::Display for OperationMatcher {
301    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
302        match self {
303            Self::Asm(op) => f.write_str(op),
304            Self::Exact(op) => core::fmt::Display::fmt(op, f),
305            Self::Assert => f.write_str("assert.*"),
306            Self::Push => f.write_str("push.*"),
307            Self::Dup => f.write_str("dup"),
308            Self::SwapW => f.write_str("swapw"),
309            Self::Movup => f.write_str("movup"),
310            Self::Movdn => f.write_str("movdn"),
311        }
312    }
313}
314
315impl FromStr for OperationMatcher {
316    type Err = String;
317
318    fn from_str(name: &str) -> Result<Self, Self::Err> {
319        use miden_core::operations::Operation::*;
320        let opcode_parts = name
321            .split_once('.')
322            .map(|(name, rest)| (name, Some(rest)))
323            .unwrap_or((name, None));
324        let opcode = match opcode_parts {
325            ("nop" | "noop", _) => Noop,
326            ("assert", Some("*")) => return Ok(OperationMatcher::Assert),
327            ("assert", Some(code)) => Assert(
328                code.parse::<u32>()
329                    .map(miden_core::Felt::from_u32)
330                    .map_err(|err| err.to_string())?,
331            ),
332            ("assert", None) => Assert(miden_core::Felt::from_u32(0)),
333            ("sdepth", None) => SDepth,
334            ("caller", None) => Caller,
335            ("clk", None) => Clk,
336            ("emit", None) => Emit,
337            ("add", None) => Add,
338            ("neg", None) => Neg,
339            ("mul", None) => Mul,
340            ("inv", None) => Inv,
341            ("incr", None) => Incr,
342            ("and", None) => And,
343            ("or", None) => Or,
344            ("not", None) => Not,
345            ("eq", None) => Eq,
346            ("eqz", None) => Eqz,
347            ("expacc", None) => Expacc,
348            ("ext2mul", None) => Ext2Mul,
349            ("u32split", None) => U32split,
350            ("u32add", None) => U32add,
351            ("u32assert2", Some(code)) => U32assert2(
352                code.parse::<u32>()
353                    .map(miden_core::Felt::from_u32)
354                    .map_err(|err| err.to_string())?,
355            ),
356            ("u32assert2", None) => U32assert2(miden_core::Felt::from_u32(0)),
357            ("u32add3", None) => U32add3,
358            ("u32sub", None) => U32sub,
359            ("u32mul", None) => U32mul,
360            ("u32madd", None) => U32madd,
361            ("u32div", None) => U32div,
362            ("u32and", None) => U32and,
363            ("u32xor", None) => U32xor,
364            ("pad", None) => Pad,
365            ("drop", None) => Drop,
366            ("dup", _) => return Ok(OperationMatcher::Dup),
367            ("swap", _) => Swap,
368            ("swapw", _) => return Ok(OperationMatcher::SwapW),
369            ("swapdw", _) => SwapDW,
370            ("movup", _) => return Ok(OperationMatcher::Movup),
371            ("movdn", _) => return Ok(OperationMatcher::Movdn),
372            ("cswap", _) => CSwap,
373            ("cswapw", _) => CSwapW,
374            ("push", _) => return Ok(OperationMatcher::Push),
375            ("advpop", _) => AdvPop,
376            ("advpopw", _) => AdvPopW,
377            ("mloadw", _) => MLoadW,
378            ("mstorew", _) => MStoreW,
379            ("mload", _) => MLoad,
380            ("mstore", _) => MStore,
381            ("mstream", _) => MStream,
382            ("pipe", _) => Pipe,
383            ("crypto_stream", _) => CryptoStream,
384            ("hperm", _) => HPerm,
385            ("mpverify", Some(code)) => MpVerify(
386                code.parse::<u32>()
387                    .map(miden_core::Felt::from_u32)
388                    .map_err(|err| err.to_string())?,
389            ),
390            ("mpverify", None) => MpVerify(miden_core::Felt::from_u32(0)),
391            ("mrupdate", None) => MrUpdate,
392            ("frie2f4", None) => FriE2F4,
393            ("horner_base", None) => HornerBase,
394            ("horner_ext", None) => HornerExt,
395            ("eval_circuit", None) => EvalCircuit,
396            ("log_deferred" | "log_precompile", None) => LogDeferred,
397            _ => return Ok(OperationMatcher::Asm(name.to_string())),
398        };
399
400        Ok(OperationMatcher::Exact(opcode))
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use alloc::string::ToString;
407
408    use super::OperationMatcher;
409
410    #[test]
411    fn log_deferred_breakpoint_accepts_legacy_name_and_displays_canonically() {
412        let canonical = "log_deferred".parse::<OperationMatcher>().unwrap();
413        let legacy = "log_precompile".parse::<OperationMatcher>().unwrap();
414
415        assert_eq!(canonical, legacy);
416        assert_eq!(canonical.to_string(), "log_deferred");
417    }
418}