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