Skip to main content

miden_debug/script/
api.rs

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