Skip to main content

miden_debug_engine/debug/
breakpoint.rs

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