1use std::fmt::Write;
2
3use clap::{ArgMatches, Command, FromArgMatches, Parser, Subcommand};
4use rustyline;
5use thiserror::Error;
6
7use crate::external_api::{breakpoint, global_properties, halt, next, run_ext_api};
8use crate::{define_data_plugin, trace, Context, HashMap, HashMapExt};
9
10#[derive(Error, Debug)]
11enum DebuggerError {
12 #[error("error splitting line")]
13 SplitLine,
14 #[error(transparent)]
15 Clap(#[from] clap::Error),
16 #[error("unknown command: {command}")]
17 UnknownCommand { command: String },
18}
19
20trait DebuggerCommand {
21 fn handle(
23 &self,
24 context: &mut Context,
25 matches: &ArgMatches,
26 ) -> Result<(bool, Option<String>), DebuggerError>;
27 fn extend(&self, command: Command) -> Command;
28}
29
30struct Debugger {
31 rl: rustyline::DefaultEditor,
32 cli: Command,
33 commands: HashMap<&'static str, Box<dyn DebuggerCommand>>,
34}
35define_data_plugin!(DebuggerPlugin, Option<Debugger>, |_context| {
36 trace!("initializing debugger");
38 let mut commands: HashMap<&'static str, Box<dyn DebuggerCommand>> = HashMap::new();
39 commands.insert("breakpoint", Box::new(BreakpointCommand));
40 commands.insert("continue", Box::new(ContinueCommand));
41 commands.insert("global", Box::new(GlobalPropertyCommand));
42 commands.insert("halt", Box::new(HaltCommand));
43 commands.insert("next", Box::new(NextCommand));
44
45 let mut cli = Command::new("repl")
46 .multicall(true)
47 .arg_required_else_help(true)
48 .subcommand_required(true)
49 .subcommand_value_name("DEBUGGER")
50 .subcommand_help_heading("IXA DEBUGGER")
51 .help_template("{all-args}");
52
53 for handler in commands.values() {
54 cli = handler.extend(cli);
55 }
56
57 Some(Debugger {
58 rl: rustyline::DefaultEditor::new().unwrap(),
59 cli,
60 commands,
61 })
62});
63
64impl Debugger {
65 fn get_command(&self, name: &str) -> Option<&dyn DebuggerCommand> {
66 self.commands.get(name).map(|command| &**command)
67 }
68
69 fn process_command(
70 &self,
71 l: &str,
72 context: &mut Context,
73 ) -> Result<(bool, Option<String>), DebuggerError> {
74 let args = shlex::split(l).ok_or(DebuggerError::SplitLine)?;
75 let matches = self
76 .cli
77 .clone() .try_get_matches_from(args)?;
79
80 if let Some((command, _)) = matches.subcommand() {
81 if let Some(handler) = self.get_command(command) {
84 return handler.handle(context, &matches);
85 }
86 return Err(DebuggerError::UnknownCommand {
88 command: command.to_string(),
89 });
90 }
91
92 unreachable!("subcommand required");
93 }
94}
95
96struct GlobalPropertyCommand;
97impl DebuggerCommand for GlobalPropertyCommand {
98 fn handle(
99 &self,
100 context: &mut Context,
101 matches: &ArgMatches,
102 ) -> Result<(bool, Option<String>), DebuggerError> {
103 let args = global_properties::Args::from_arg_matches(matches).unwrap();
104 let ret = run_ext_api::<global_properties::Api>(context, &args);
105 match ret {
106 Err(e) => Ok((false, Some(format!("error: {e}")))),
107 Ok(global_properties::Retval::List(properties)) => Ok((
108 false,
109 Some(format!(
110 "{} global properties registered:\n{}",
111 properties.len(),
112 properties.join("\n")
113 )),
114 )),
115 Ok(global_properties::Retval::Value(value)) => Ok((false, Some(value))),
116 }
117 }
118 fn extend(&self, command: Command) -> Command {
119 global_properties::Args::augment_subcommands(command)
120 }
121}
122
123struct HaltCommand;
125impl DebuggerCommand for HaltCommand {
126 fn handle(
127 &self,
128 context: &mut Context,
129 _matches: &ArgMatches,
130 ) -> Result<(bool, Option<String>), DebuggerError> {
131 context.shutdown();
132 Ok((true, None))
133 }
134 fn extend(&self, command: Command) -> Command {
135 halt::Args::augment_subcommands(command)
136 }
137}
138
139struct NextCommand;
141impl DebuggerCommand for NextCommand {
142 fn handle(
143 &self,
144 context: &mut Context,
145 _matches: &ArgMatches,
146 ) -> Result<(bool, Option<String>), DebuggerError> {
147 context.execute_single_step();
151 Ok((false, None))
152 }
153 fn extend(&self, command: Command) -> Command {
154 next::Args::augment_subcommands(command)
155 }
156}
157
158struct BreakpointCommand;
159impl DebuggerCommand for BreakpointCommand {
161 fn handle(
162 &self,
163 context: &mut Context,
164 matches: &ArgMatches,
165 ) -> Result<(bool, Option<String>), DebuggerError> {
166 let args = breakpoint::Args::from_arg_matches(matches).unwrap();
167 match run_ext_api::<breakpoint::Api>(context, &args) {
168 Err(e) => Ok((false, Some(format!("error: {e}")))),
169 Ok(return_value) => {
170 match return_value {
171 breakpoint::Retval::List(bp_list) => {
172 let mut msg = format!("Scheduled breakpoints: {}\n", bp_list.len());
173 for bp in bp_list {
174 _ = writeln!(&mut msg, "\t{bp}");
175 }
176 return Ok((false, Some(msg)));
177 }
178 breakpoint::Retval::Ok => { }
179 }
180
181 Ok((false, None))
182 }
183 }
184 }
185 fn extend(&self, command: Command) -> Command {
186 breakpoint::Args::augment_subcommands(command)
187 }
188}
189
190struct ContinueCommand;
191#[derive(Parser, Debug)]
192enum ContinueSubcommand {
193 Continue,
195}
196impl DebuggerCommand for ContinueCommand {
197 fn handle(
198 &self,
199 _context: &mut Context,
200 _matches: &ArgMatches,
201 ) -> Result<(bool, Option<String>), DebuggerError> {
202 Ok((true, None))
203 }
204 fn extend(&self, command: Command) -> Command {
205 ContinueSubcommand::augment_subcommands(command)
206 }
207}
208
209fn exit_debugger() -> ! {
210 println!("Got Ctrl-D, Exiting...");
211 std::process::exit(0);
212}
213
214#[allow(clippy::missing_panics_doc)]
216pub fn enter_debugger(context: &mut Context) {
217 let current_time = context.get_current_time();
218 context.cancel_debugger_request();
219
220 let mut debugger = context.get_data_mut(DebuggerPlugin).take().unwrap();
223
224 println!("Debugging simulation at t={current_time}");
225 loop {
226 let line = match debugger.rl.readline(&format!("t={current_time:.4} $ ")) {
227 Ok(line) => line,
228 Err(
229 rustyline::error::ReadlineError::Signal(_)
230 | rustyline::error::ReadlineError::Interrupted,
231 ) => continue,
232 Err(rustyline::error::ReadlineError::Eof) => exit_debugger(),
233 Err(err) => panic!("Read error: {err}"),
234 };
235 debugger
236 .rl
237 .add_history_entry(line.clone())
238 .expect("Should be able to add to input");
239 let line = line.trim();
240 if line.is_empty() {
241 continue;
242 }
243
244 match debugger.process_command(line, context) {
245 Ok((quit, message)) => {
246 if let Some(message) = message {
247 println!("{message}");
248 }
249 if quit {
250 break;
251 }
252 }
253 Err(err) => {
254 eprintln!("{err}");
255 }
256 }
257 }
258
259 let saved_debugger = context.get_data_mut(DebuggerPlugin);
261 *saved_debugger = Some(debugger);
262}
263
264#[cfg(test)]
265mod tests {
266 use assert_approx_eq::assert_approx_eq;
267
268 use super::{enter_debugger, DebuggerPlugin};
269 use crate::{define_global_property, Context, ContextGlobalPropertiesExt, ExecutionPhase};
270
271 fn process_line(line: &str, context: &mut Context) -> (bool, Option<String>) {
272 let data_container = context.get_data_mut(DebuggerPlugin);
275 let debugger = data_container.take().unwrap();
276
277 let res = debugger.process_command(line, context).unwrap();
278 let data_container = context.get_data_mut(DebuggerPlugin);
279 *data_container = Some(debugger);
280 res
281 }
282
283 define_global_property!(FooGlobal, String);
284 define_global_property!(BarGlobal, u32);
285
286 #[test]
287 fn test_cli_debugger_breakpoint_set() {
288 let context = &mut Context::new();
289 let (quits, _output) = process_line("breakpoint set 4.0\n", context);
290
291 assert!(!quits, "should not exit");
292
293 let list = context.list_breakpoints(0);
294 assert_eq!(list.len(), 1);
295 if let Some(schedule) = list.first() {
296 assert_eq!(schedule.priority, ExecutionPhase::First);
297 assert_eq!(schedule.plan_id, 0u64);
298 assert_approx_eq!(schedule.time, 4.0f64);
299 }
300 }
301
302 #[test]
303 fn test_cli_debugger_breakpoint_list() {
304 let context = &mut Context::new();
305
306 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
307 context.schedule_debugger(2.0, Some(ExecutionPhase::First), Box::new(enter_debugger));
308 context.schedule_debugger(3.0, Some(ExecutionPhase::Normal), Box::new(enter_debugger));
309 context.schedule_debugger(4.0, Some(ExecutionPhase::Last), Box::new(enter_debugger));
310
311 let expected = r"Scheduled breakpoints: 4
312 0: t=1 (First)
313 1: t=2 (First)
314 2: t=3 (Normal)
315 3: t=4 (Last)
316";
317
318 let (quits, output) = process_line("breakpoint list\n", context);
319
320 assert!(!quits, "should not exit");
321 assert!(output.is_some());
322 assert_eq!(output.unwrap(), expected);
323 }
324
325 #[test]
326 fn test_cli_debugger_breakpoint_delete_id() {
327 let context = &mut Context::new();
328
329 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
330 context.schedule_debugger(2.0, None, Box::new(enter_debugger));
331
332 let (quits, _output) = process_line("breakpoint delete 0\n", context);
333 assert!(!quits, "should not exit");
334 let list = context.list_breakpoints(0);
335
336 assert_eq!(list.len(), 1);
337 if let Some(schedule) = list.first() {
338 assert_eq!(schedule.priority, ExecutionPhase::First);
339 assert_eq!(schedule.plan_id, 1u64);
340 assert_approx_eq!(schedule.time, 2.0f64);
341 }
342 }
343
344 #[test]
345 fn test_cli_debugger_breakpoint_delete_all() {
346 let context = &mut Context::new();
347
348 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
349 context.schedule_debugger(2.0, None, Box::new(enter_debugger));
350
351 let (quits, _output) = process_line("breakpoint delete --all\n", context);
352 assert!(!quits, "should not exit");
353 let list = context.list_breakpoints(0);
354 assert_eq!(list.len(), 0);
355 }
356
357 #[test]
358 fn test_cli_debugger_breakpoint_disable_enable() {
359 let context = &mut Context::new();
360
361 let (quits, _output) = process_line("breakpoint disable\n", context);
362 assert!(!quits, "should not exit");
363 assert!(!context.breakpoints_are_enabled());
364
365 let (quits, _output) = process_line("breakpoint enable\n", context);
366 assert!(!quits, "should not exit");
367 assert!(context.breakpoints_are_enabled());
368 }
369
370 #[test]
371 fn test_cli_debugger_global_list() {
372 let context = &mut Context::new();
373 let (_quits, output) = process_line("global list\n", context);
374 let expected = format!(
375 "{} global properties registered:",
376 context.list_registered_global_properties().len()
377 );
378 assert!(output.unwrap().contains(&expected));
380 }
381
382 #[test]
383 fn test_cli_debugger_global_no_args() {
384 let input = "global get\n";
385 let context = &mut Context::new();
386
387 let debugger = context.get_data_mut(DebuggerPlugin).take().unwrap();
390 let result = debugger.process_command(input, context);
391 let data_container = context.get_data_mut(DebuggerPlugin);
392 *data_container = Some(debugger);
393
394 assert!(result.is_err());
395 assert!(result
396 .unwrap_err()
397 .to_string()
398 .contains("required arguments were not provided"));
399 }
400
401 #[test]
402 fn test_cli_debugger_global_get_unregistered_prop() {
403 let context = &mut Context::new();
404 let (_quits, output) = process_line("global get NotExist\n", context);
405 assert_eq!(output.unwrap(), "error: no global property: NotExist");
406 }
407
408 #[test]
409 fn test_cli_debugger_global_get_registered_prop() {
410 let context = &mut Context::new();
411 context
412 .set_global_property_value(FooGlobal, "hello".to_string())
413 .unwrap();
414 let (_quits, output) = process_line("global get ixa.FooGlobal\n", context);
415 assert_eq!(output.unwrap(), "\"hello\"");
416 }
417
418 #[test]
419 fn test_cli_debugger_global_get_empty_prop() {
420 define_global_property!(EmptyGlobal, String);
421 let context = &mut Context::new();
422 let (_quits, output) = process_line("global get ixa.EmptyGlobal\n", context);
423 assert_eq!(
424 output.unwrap(),
425 "error: property ixa.EmptyGlobal is not set"
426 );
427 }
428
429 #[test]
430 fn test_cli_continue() {
431 let context = &mut Context::new();
432 let (quits, _) = process_line("continue\n", context);
433 assert!(quits, "should exit");
434 }
435
436 #[test]
437 fn test_cli_next() {
438 let context = &mut Context::new();
439 assert_eq!(context.remaining_plan_count(), 0);
440 let (quits, _) = process_line("next\n", context);
441 assert!(!quits, "should not exit");
442 }
443}