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#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ScriptSourceLocation {
22 pub path: String,
23 pub line: u32,
24 pub column: u32,
25}
26
27#[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#[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#[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#[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#[derive(Clone)]
65pub struct ScriptDebugger {
66 engine: Rc<RefCell<ReplEngine>>,
67}
68
69impl ScriptDebugger {
70 pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
72 Self::from_config(config)
73 }
74
75 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 #[cfg(feature = "repl")]
84 pub(crate) fn make_prompt(&self, color: bool) -> String {
85 self.engine.borrow().make_prompt(color)
86 }
87
88 #[cfg(feature = "repl")]
90 pub(crate) fn print_location(&self, out: &mut dyn Write) {
91 self.engine.borrow().print_location(out);
92 }
93
94 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 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 pub fn cycle(&self) -> usize {
116 self.engine.borrow().state().executor().cycle
117 }
118
119 pub fn stopped(&self) -> bool {
121 self.engine.borrow().state().stopped
122 }
123
124 pub fn terminated(&self) -> bool {
126 self.engine.borrow().state().executor().stopped
127 }
128
129 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 pub fn result(&self) -> Result<Option<String>, String> {
143 self.engine.borrow().state().typed_result()
144 }
145
146 pub fn source_path_prefixes(&self) -> Vec<String> {
148 self.engine.borrow().state().source_path_prefixes()
149 }
150
151 pub fn frame(&self) -> ScriptFrame {
153 self.frame_with_variables(false)
154 }
155
156 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 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 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 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 pub fn clear_hit_breakpoints(&self) {
225 self.engine.borrow_mut().state_mut().breakpoints_hit.clear();
226 }
227
228 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 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 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 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 pub fn next(&self) -> Result<String, String> {
277 self.handle_command("next")
278 }
279
280 pub fn next_line(&self) -> Result<String, String> {
282 self.handle_command("next-line")
283 }
284
285 pub fn continue_(&self) -> Result<String, String> {
287 self.handle_command("continue")
288 }
289
290 pub fn finish(&self) -> Result<String, String> {
292 self.handle_command("finish")
293 }
294
295 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}