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 procedure = miden_debug_types::Uri::from(format!("file://{procedure}"));
221    pattern.is_match(&procedure)
222}
223
224/// Compile a user-provided file spec into a glob pattern.
225///
226/// Source locations in debug info are stored as absolute paths, so a relative
227/// spec like `src/lib.rs` would never match as-is. Anchor relative specs with
228/// a leading `**/` so they match by path suffix, like gdb's `break FILE:LINE`.
229fn file_pattern(spec: &str) -> Result<GlobMatcher, String> {
230    let spec = spec.trim();
231    let anchored;
232    let spec = if spec.starts_with('/') || spec.starts_with('*') {
233        spec
234    } else {
235        anchored = format!("**/{spec}");
236        &anchored
237    };
238    crate::glob::GlobBuilder::new(spec)
239        .build()
240        .map(|glob| glob.compile_matcher())
241        .map_err(|err| format!("invalid breakpoint pattern: {err}"))
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245#[non_exhaustive]
246pub enum OperationMatcher {
247    Asm(String),
248    Exact(miden_core::operations::Operation),
249    Assert,
250    Push,
251    Dup,
252    SwapW,
253    Movup,
254    Movdn,
255}
256
257impl OperationMatcher {
258    pub fn should_break_for(&self, op: &miden_core::operations::Operation) -> bool {
259        use miden_core::operations::Operation;
260        match self {
261            Self::Asm(_) => false,
262            Self::Exact(expected) => op == expected,
263            Self::Assert => matches!(op, Operation::Assert(_)),
264            Self::Push => matches!(op, Operation::Push(_)),
265            Self::Dup => matches!(
266                op,
267                Operation::Dup0
268                    | Operation::Dup1
269                    | Operation::Dup2
270                    | Operation::Dup3
271                    | Operation::Dup4
272                    | Operation::Dup5
273                    | Operation::Dup6
274                    | Operation::Dup7
275                    | Operation::Dup9
276                    | Operation::Dup11
277                    | Operation::Dup13
278                    | Operation::Dup15
279            ),
280            Self::SwapW => matches!(op, Operation::SwapW | Operation::SwapW2 | Operation::SwapW3),
281            Self::Movup => matches!(
282                op,
283                Operation::MovUp2
284                    | Operation::MovUp3
285                    | Operation::MovUp4
286                    | Operation::MovUp5
287                    | Operation::MovUp6
288                    | Operation::MovUp7
289                    | Operation::MovUp8
290            ),
291            Self::Movdn => matches!(
292                op,
293                Operation::MovDn2
294                    | Operation::MovDn3
295                    | Operation::MovDn4
296                    | Operation::MovDn5
297                    | Operation::MovDn6
298                    | Operation::MovDn7
299                    | Operation::MovDn8
300            ),
301        }
302    }
303}
304
305impl core::fmt::Display for OperationMatcher {
306    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307        match self {
308            Self::Asm(op) => f.write_str(op),
309            Self::Exact(op) => core::fmt::Display::fmt(op, f),
310            Self::Assert => f.write_str("assert.*"),
311            Self::Push => f.write_str("push.*"),
312            Self::Dup => f.write_str("dup"),
313            Self::SwapW => f.write_str("swapw"),
314            Self::Movup => f.write_str("movup"),
315            Self::Movdn => f.write_str("movdn"),
316        }
317    }
318}
319
320impl FromStr for OperationMatcher {
321    type Err = String;
322
323    fn from_str(name: &str) -> Result<Self, Self::Err> {
324        use miden_core::operations::Operation::*;
325        let opcode_parts = name
326            .split_once('.')
327            .map(|(name, rest)| (name, Some(rest)))
328            .unwrap_or((name, None));
329        let opcode = match opcode_parts {
330            ("nop" | "noop", _) => Noop,
331            ("assert", Some("*")) => return Ok(OperationMatcher::Assert),
332            ("assert", Some(code)) => Assert(
333                code.parse::<u32>()
334                    .map(miden_core::Felt::from_u32)
335                    .map_err(|err| err.to_string())?,
336            ),
337            ("assert", None) => Assert(miden_core::Felt::from_u32(0)),
338            ("sdepth", None) => SDepth,
339            ("caller", None) => Caller,
340            ("clk", None) => Clk,
341            ("emit", None) => Emit,
342            ("add", None) => Add,
343            ("neg", None) => Neg,
344            ("mul", None) => Mul,
345            ("inv", None) => Inv,
346            ("incr", None) => Incr,
347            ("and", None) => And,
348            ("or", None) => Or,
349            ("not", None) => Not,
350            ("eq", None) => Eq,
351            ("eqz", None) => Eqz,
352            ("expacc", None) => Expacc,
353            ("ext2mul", None) => Ext2Mul,
354            ("u32split", None) => U32split,
355            ("u32add", None) => U32add,
356            ("u32assert2", Some(code)) => U32assert2(
357                code.parse::<u32>()
358                    .map(miden_core::Felt::from_u32)
359                    .map_err(|err| err.to_string())?,
360            ),
361            ("u32assert2", None) => U32assert2(miden_core::Felt::from_u32(0)),
362            ("u32add3", None) => U32add3,
363            ("u32sub", None) => U32sub,
364            ("u32mul", None) => U32mul,
365            ("u32madd", None) => U32madd,
366            ("u32div", None) => U32div,
367            ("u32and", None) => U32and,
368            ("u32xor", None) => U32xor,
369            ("pad", None) => Pad,
370            ("drop", None) => Drop,
371            ("dup", _) => return Ok(OperationMatcher::Dup),
372            ("swap", _) => Swap,
373            ("swapw", _) => return Ok(OperationMatcher::SwapW),
374            ("swapdw", _) => SwapDW,
375            ("movup", _) => return Ok(OperationMatcher::Movup),
376            ("movdn", _) => return Ok(OperationMatcher::Movdn),
377            ("cswap", _) => CSwap,
378            ("cswapw", _) => CSwapW,
379            ("push", _) => return Ok(OperationMatcher::Push),
380            ("advpop", _) => AdvPop,
381            ("advpopw", _) => AdvPopW,
382            ("mloadw", _) => MLoadW,
383            ("mstorew", _) => MStoreW,
384            ("mload", _) => MLoad,
385            ("mstore", _) => MStore,
386            ("mstream", _) => MStream,
387            ("pipe", _) => Pipe,
388            ("crypto_stream", _) => CryptoStream,
389            ("hperm", _) => HPerm,
390            ("mpverify", Some(code)) => MpVerify(
391                code.parse::<u32>()
392                    .map(miden_core::Felt::from_u32)
393                    .map_err(|err| err.to_string())?,
394            ),
395            ("mpverify", None) => MpVerify(miden_core::Felt::from_u32(0)),
396            ("mrupdate", None) => MrUpdate,
397            ("frie2f4", None) => FriE2F4,
398            ("horner_base", None) => HornerBase,
399            ("horner_ext", None) => HornerExt,
400            ("eval_circuit", None) => EvalCircuit,
401            ("log_deferred" | "log_precompile", None) => LogDeferred,
402            _ => return Ok(OperationMatcher::Asm(name.to_string())),
403        };
404
405        Ok(OperationMatcher::Exact(opcode))
406    }
407}
408
409#[cfg(test)]
410mod tests;