Skip to main content

miden_debug/script/
api.rs

1use std::{cell::RefCell, io::Write, rc::Rc, str::FromStr};
2
3use miden_assembly_syntax::diagnostics::Report;
4
5use crate::{
6    DebuggerConfig,
7    debug::{Breakpoint, BreakpointType, ReadMemoryExpr},
8    repl::engine::{Outcome, ReplEngine, format_bp_type},
9};
10
11/// Source location snapshot exposed to scripting frontends.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ScriptSourceLocation {
14    pub path: String,
15    pub line: u32,
16    pub column: u32,
17}
18
19/// Variable snapshot exposed to scripting frontends.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ScriptValue {
22    pub name: String,
23    pub value: Option<u64>,
24    pub location: String,
25    pub source: Option<ScriptSourceLocation>,
26}
27
28/// Current frame snapshot exposed to scripting frontends.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ScriptFrame {
31    pub function_name: Option<String>,
32    pub source_location: Option<ScriptSourceLocation>,
33    pub variables: Vec<ScriptValue>,
34}
35
36/// Breakpoint snapshot exposed to scripting frontends.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ScriptBreakpoint {
39    pub id: u8,
40    pub spec: String,
41    pub internal: bool,
42    pub one_shot: bool,
43}
44
45/// Execution context snapshot exposed to scripting callbacks.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ScriptExecutionContext {
48    pub cycle: usize,
49    pub stopped: bool,
50    pub terminated: bool,
51    pub frame: ScriptFrame,
52}
53
54/// Stable facade used by debugger scripting integrations.
55#[derive(Clone)]
56pub struct ScriptDebugger {
57    engine: Rc<RefCell<ReplEngine>>,
58}
59
60impl ScriptDebugger {
61    /// Create a script debugger from the same configuration used by the REPL.
62    pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
63        Self::from_config(config)
64    }
65
66    /// Create a script debugger from a debugger configuration.
67    pub fn from_config(config: Box<DebuggerConfig>) -> Result<Self, Report> {
68        Ok(Self {
69            engine: Rc::new(RefCell::new(ReplEngine::from_config(config)?)),
70        })
71    }
72
73    /// Create a script debugger from inline MASM source.
74    ///
75    /// This is useful for tests and small programmatic debugging harnesses.
76    pub fn from_masm_source(
77        source: &str,
78        args: Vec<crate::processor::Felt>,
79    ) -> Result<Self, Report> {
80        let state = crate::ui::state::State::from_masm_source(source, args)?;
81        Ok(Self {
82            engine: Rc::new(RefCell::new(ReplEngine::from_state(state))),
83        })
84    }
85
86    /// Render the normal debugger prompt.
87    pub(crate) fn make_prompt(&self, color: bool) -> String {
88        self.engine.borrow().make_prompt(color)
89    }
90
91    /// Print the current source location / procedure.
92    pub(crate) fn print_location(&self, out: &mut dyn Write) {
93        self.engine.borrow().print_location(out);
94    }
95
96    /// Execute a REPL command line while preserving the raw REPL outcome.
97    pub(crate) fn execute_repl_line(
98        &self,
99        line: &str,
100        out: &mut dyn Write,
101    ) -> Result<Outcome, String> {
102        self.engine.borrow_mut().execute_line(line, out)
103    }
104
105    /// Execute a debugger command and capture its textual output.
106    pub fn handle_command(&self, command: &str) -> Result<String, String> {
107        let mut output = Vec::new();
108        match self.engine.borrow_mut().execute_line(command, &mut output)? {
109            Outcome::Continue => {}
110            Outcome::Quit => return Err("quit requested".into()),
111        }
112
113        String::from_utf8(output).map_err(|err| format!("command output was not UTF-8: {err}"))
114    }
115
116    /// Current VM cycle.
117    pub fn cycle(&self) -> usize {
118        self.engine.borrow().state().executor().cycle
119    }
120
121    /// Whether execution is currently stopped at a debugger stop point.
122    pub fn stopped(&self) -> bool {
123        self.engine.borrow().state().stopped
124    }
125
126    /// Whether the debuggee has terminated.
127    pub fn terminated(&self) -> bool {
128        self.engine.borrow().state().executor().stopped
129    }
130
131    /// Current operand stack snapshot, in debugger display order.
132    pub fn stack(&self) -> Vec<u64> {
133        self.engine
134            .borrow()
135            .state()
136            .executor()
137            .current_stack
138            .iter()
139            .map(|felt| felt.as_canonical_u64())
140            .collect()
141    }
142
143    /// Source path prefix mappings currently configured for this debugger.
144    pub fn source_path_prefixes(&self) -> Vec<String> {
145        self.engine.borrow().state().source_path_prefixes()
146    }
147
148    /// Current frame snapshot.
149    pub fn frame(&self) -> ScriptFrame {
150        self.frame_with_variables(false)
151    }
152
153    /// Current frame snapshot with either source-visible or all debug variables.
154    pub fn frame_with_variables(&self, show_all: bool) -> ScriptFrame {
155        let engine = self.engine.borrow();
156        let state = engine.state();
157        let source_location = state.current_display_location().map(|loc| ScriptSourceLocation {
158            path: loc.source_file.uri().as_str().to_string(),
159            line: loc.line,
160            column: loc.col,
161        });
162        let function_name = state.current_procedure().map(|name| name.to_string());
163        let variables = state
164            .current_variables(show_all)
165            .into_iter()
166            .map(|variable| ScriptValue {
167                name: variable.name,
168                value: variable.value.map(|felt| felt.as_canonical_u64()),
169                location: variable.location,
170                source: variable.source.map(|source| ScriptSourceLocation {
171                    path: source.path,
172                    line: source.line,
173                    column: source.column,
174                }),
175            })
176            .collect();
177
178        ScriptFrame {
179            function_name,
180            source_location,
181            variables,
182        }
183    }
184
185    /// Current execution context snapshot.
186    pub fn execution_context(&self) -> ScriptExecutionContext {
187        ScriptExecutionContext {
188            cycle: self.cycle(),
189            stopped: self.stopped(),
190            terminated: self.terminated(),
191            frame: self.frame(),
192        }
193    }
194
195    /// Current user-visible breakpoints.
196    pub fn breakpoints(&self) -> Vec<ScriptBreakpoint> {
197        self.engine
198            .borrow()
199            .state()
200            .breakpoints
201            .iter()
202            .filter(|bp| !bp.is_internal())
203            .map(script_breakpoint_from)
204            .collect()
205    }
206
207    /// Breakpoints hit at the current stop.
208    pub fn hit_breakpoints(&self) -> Vec<ScriptBreakpoint> {
209        self.engine
210            .borrow()
211            .state()
212            .breakpoints_hit
213            .iter()
214            .filter(|bp| !bp.is_internal())
215            .map(script_breakpoint_from)
216            .collect()
217    }
218
219    /// Clear the current hit-breakpoint list.
220    pub fn clear_hit_breakpoints(&self) {
221        self.engine.borrow_mut().state_mut().breakpoints_hit.clear();
222    }
223
224    /// Set a breakpoint from the normal debugger breakpoint grammar.
225    pub fn set_breakpoint(&self, spec: &str) -> Result<ScriptBreakpoint, String> {
226        let ty = BreakpointType::from_str(spec)?;
227        let mut engine = self.engine.borrow_mut();
228        engine.state_mut().create_breakpoint(ty);
229        let bp = engine
230            .state()
231            .breakpoints
232            .last()
233            .ok_or_else(|| "breakpoint was not created".to_string())?;
234        Ok(script_breakpoint_from(bp))
235    }
236
237    /// Delete one breakpoint by id, or all user breakpoints if `id` is `None`.
238    pub fn delete_breakpoint(&self, id: Option<u8>) -> Result<(), String> {
239        let mut engine = self.engine.borrow_mut();
240        let state = engine.state_mut();
241        match id {
242            Some(id) => {
243                let before = state.breakpoints.len();
244                state.breakpoints.retain(|bp| bp.id != id);
245                if state.breakpoints.len() == before {
246                    return Err(format!("no breakpoint with id {id}"));
247                }
248            }
249            None => {
250                state.breakpoints.retain(|bp| bp.is_internal());
251            }
252        }
253        Ok(())
254    }
255
256    /// Read memory using the debugger memory expression grammar.
257    pub fn read_memory(&self, expression: &str) -> Result<String, String> {
258        let expression = expression.parse::<ReadMemoryExpr>()?;
259        self.engine.borrow_mut().state_mut().read_memory(&expression)
260    }
261
262    /// Step one or more VM cycles.
263    pub fn step(&self, count: usize) -> Result<String, String> {
264        if count <= 1 {
265            self.handle_command("step")
266        } else {
267            self.handle_command(&format!("step {count}"))
268        }
269    }
270
271    /// Step to the next instruction boundary.
272    pub fn next(&self) -> Result<String, String> {
273        self.handle_command("next")
274    }
275
276    /// Step to the next source line.
277    pub fn next_line(&self) -> Result<String, String> {
278        self.handle_command("next-line")
279    }
280
281    /// Continue execution until the next breakpoint or termination.
282    pub fn continue_(&self) -> Result<String, String> {
283        self.handle_command("continue")
284    }
285
286    /// Continue execution until the current frame returns.
287    pub fn finish(&self) -> Result<String, String> {
288        self.handle_command("finish")
289    }
290
291    /// Reload the debuggee.
292    pub fn reload(&self) -> Result<String, String> {
293        self.handle_command("reload")
294    }
295}
296
297fn script_breakpoint_from(bp: &Breakpoint) -> ScriptBreakpoint {
298    ScriptBreakpoint {
299        id: bp.id,
300        spec: format_bp_type(&bp.ty),
301        internal: bp.is_internal(),
302        one_shot: bp.is_one_shot(),
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use miden_core::Felt;
309
310    use super::*;
311
312    #[test]
313    fn script_debugger_executes_commands_and_exposes_state() {
314        let debugger = ScriptDebugger::from_masm_source(
315            r#"
316begin
317    push.3
318    push.4
319    add
320end
321"#,
322            Vec::<Felt>::new(),
323        )
324        .unwrap();
325
326        assert_eq!(debugger.cycle(), 0);
327
328        let output = debugger.handle_command("step").unwrap();
329        assert!(output.contains("in") || output.is_empty(), "unexpected output: {output}");
330        assert_eq!(debugger.cycle(), 1);
331
332        let stack_output = debugger.handle_command("stack").unwrap();
333        assert!(stack_output.contains("Operand Stack"));
334    }
335
336    #[test]
337    fn script_debugger_can_manage_breakpoints() {
338        let debugger = ScriptDebugger::from_masm_source(
339            r#"
340begin
341    push.3
342end
343"#,
344            Vec::<Felt>::new(),
345        )
346        .unwrap();
347
348        let bp = debugger.set_breakpoint("after 1").unwrap();
349        assert_eq!(bp.id, 0);
350        assert_eq!(debugger.breakpoints().len(), 1);
351
352        debugger.delete_breakpoint(Some(bp.id)).unwrap();
353        assert!(debugger.breakpoints().is_empty());
354    }
355}