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#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ScriptSourceLocation {
14 pub path: String,
15 pub line: u32,
16 pub column: u32,
17}
18
19#[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#[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#[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#[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#[derive(Clone)]
56pub struct ScriptDebugger {
57 engine: Rc<RefCell<ReplEngine>>,
58}
59
60impl ScriptDebugger {
61 pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
63 Self::from_config(config)
64 }
65
66 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 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 pub(crate) fn make_prompt(&self, color: bool) -> String {
88 self.engine.borrow().make_prompt(color)
89 }
90
91 pub(crate) fn print_location(&self, out: &mut dyn Write) {
93 self.engine.borrow().print_location(out);
94 }
95
96 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 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 pub fn cycle(&self) -> usize {
118 self.engine.borrow().state().executor().cycle
119 }
120
121 pub fn stopped(&self) -> bool {
123 self.engine.borrow().state().stopped
124 }
125
126 pub fn terminated(&self) -> bool {
128 self.engine.borrow().state().executor().stopped
129 }
130
131 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 pub fn source_path_prefixes(&self) -> Vec<String> {
145 self.engine.borrow().state().source_path_prefixes()
146 }
147
148 pub fn frame(&self) -> ScriptFrame {
150 self.frame_with_variables(false)
151 }
152
153 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 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 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 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 pub fn clear_hit_breakpoints(&self) {
221 self.engine.borrow_mut().state_mut().breakpoints_hit.clear();
222 }
223
224 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 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 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 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 pub fn next(&self) -> Result<String, String> {
273 self.handle_command("next")
274 }
275
276 pub fn next_line(&self) -> Result<String, String> {
278 self.handle_command("next-line")
279 }
280
281 pub fn continue_(&self) -> Result<String, String> {
283 self.handle_command("continue")
284 }
285
286 pub fn finish(&self) -> Result<String, String> {
288 self.handle_command("finish")
289 }
290
291 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}