1use std::fmt::Write;
2
3use clap::{ArgMatches, Command, FromArgMatches, Parser, Subcommand};
4use rustyline;
5
6use crate::external_api::{
7 breakpoint, global_properties, halt, next, people, population, run_ext_api, EmptyArgs,
8};
9use crate::{define_data_plugin, trace, Context, HashMap, HashMapExt, IxaError};
10
11trait DebuggerCommand {
12 fn handle(
14 &self,
15 context: &mut Context,
16 matches: &ArgMatches,
17 ) -> Result<(bool, Option<String>), String>;
18 fn extend(&self, command: Command) -> Command;
19}
20
21struct Debugger {
22 rl: rustyline::DefaultEditor,
23 cli: Command,
24 commands: HashMap<&'static str, Box<dyn DebuggerCommand>>,
25}
26define_data_plugin!(DebuggerPlugin, Option<Debugger>, |_context| {
27 trace!("initializing debugger");
29 let mut commands: HashMap<&'static str, Box<dyn DebuggerCommand>> = HashMap::new();
30 commands.insert("breakpoint", Box::new(BreakpointCommand));
31 commands.insert("continue", Box::new(ContinueCommand));
32 commands.insert("global", Box::new(GlobalPropertyCommand));
33 commands.insert("halt", Box::new(HaltCommand));
34 commands.insert("next", Box::new(NextCommand));
35 commands.insert("people", Box::new(PeopleCommand));
36 commands.insert("population", Box::new(PopulationCommand));
37
38 let mut cli = Command::new("repl")
39 .multicall(true)
40 .arg_required_else_help(true)
41 .subcommand_required(true)
42 .subcommand_value_name("DEBUGGER")
43 .subcommand_help_heading("IXA DEBUGGER")
44 .help_template("{all-args}");
45
46 for handler in commands.values() {
47 cli = handler.extend(cli);
48 }
49
50 Some(Debugger {
51 rl: rustyline::DefaultEditor::new().unwrap(),
52 cli,
53 commands,
54 })
55});
56
57impl Debugger {
58 fn get_command(&self, name: &str) -> Option<&dyn DebuggerCommand> {
59 self.commands.get(name).map(|command| &**command)
60 }
61
62 fn process_command(
63 &self,
64 l: &str,
65 context: &mut Context,
66 ) -> Result<(bool, Option<String>), String> {
67 let args = shlex::split(l).ok_or("Error splitting lines")?;
68 let matches = self
69 .cli
70 .clone() .try_get_matches_from(args)
72 .map_err(|e| e.to_string())?;
73
74 if let Some((command, _)) = matches.subcommand() {
75 if let Some(handler) = self.get_command(command) {
78 return handler.handle(context, &matches);
79 }
80 return Err(format!("error: Unknown command: {command}"));
82 }
83
84 unreachable!("subcommand required");
85 }
86}
87
88struct PopulationCommand;
89impl DebuggerCommand for PopulationCommand {
90 fn handle(
91 &self,
92 context: &mut Context,
93 _matches: &ArgMatches,
94 ) -> Result<(bool, Option<String>), String> {
95 let output = format!(
96 "{}",
97 run_ext_api::<population::Api>(context, &EmptyArgs {})
98 .unwrap()
99 .population
100 );
101 Ok((false, Some(output)))
102 }
103 fn extend(&self, command: Command) -> Command {
104 population::Args::augment_subcommands(command)
105 }
106}
107
108struct PeopleCommand;
109impl DebuggerCommand for PeopleCommand {
110 fn extend(&self, command: Command) -> Command {
111 people::Args::augment_subcommands(command)
112 }
113 fn handle(
114 &self,
115 context: &mut Context,
116 matches: &ArgMatches,
117 ) -> Result<(bool, Option<String>), String> {
118 let args = people::Args::from_arg_matches(matches).unwrap();
119 match run_ext_api::<people::Api>(context, &args) {
120 Ok(people::Retval::Properties(props)) => Ok((
121 false,
122 Some(
123 props
124 .into_iter()
125 .map(|(k, v)| format!("{k}: {v}"))
126 .collect::<Vec<_>>()
127 .join("\n"),
128 ),
129 )),
130 Ok(people::Retval::PropertyNames(names)) => Ok((
131 false,
132 Some(format!("Available properties:\n{}", names.join("\n"))),
133 )),
134 Ok(people::Retval::Tabulated(rows)) => Ok((
135 false,
136 Some(
137 rows.into_iter()
138 .map(|(props, count)| {
139 format!(
140 "{}: {}",
141 count,
142 props
143 .into_iter()
144 .map(|(k, v)| format!("{k}={v}"))
145 .collect::<Vec<_>>()
146 .join(", ")
147 )
148 })
149 .collect::<Vec<_>>()
150 .join("\n"),
151 ),
152 )),
153 Err(e) => Ok((false, Some(format!("error: {e}")))),
154 }
155 }
156}
157
158struct GlobalPropertyCommand;
159impl DebuggerCommand for GlobalPropertyCommand {
160 fn extend(&self, command: Command) -> Command {
161 global_properties::Args::augment_subcommands(command)
162 }
163 fn handle(
164 &self,
165 context: &mut Context,
166 matches: &ArgMatches,
167 ) -> Result<(bool, Option<String>), String> {
168 let args = global_properties::Args::from_arg_matches(matches).unwrap();
169 let ret = run_ext_api::<global_properties::Api>(context, &args);
170 match ret {
171 Err(IxaError::IxaError(e)) => Ok((false, Some(format!("error: {e}")))),
172 Err(e) => Ok((false, Some(format!("error: {e}")))),
173 Ok(global_properties::Retval::List(properties)) => Ok((
174 false,
175 Some(format!(
176 "{} global properties registered:\n{}",
177 properties.len(),
178 properties.join("\n")
179 )),
180 )),
181 Ok(global_properties::Retval::Value(value)) => Ok((false, Some(value))),
182 }
183 }
184}
185
186struct HaltCommand;
188impl DebuggerCommand for HaltCommand {
189 fn handle(
190 &self,
191 context: &mut Context,
192 _matches: &ArgMatches,
193 ) -> Result<(bool, Option<String>), String> {
194 context.shutdown();
195 Ok((true, None))
196 }
197 fn extend(&self, command: Command) -> Command {
198 halt::Args::augment_subcommands(command)
199 }
200}
201
202struct NextCommand;
204impl DebuggerCommand for NextCommand {
205 fn handle(
206 &self,
207 context: &mut Context,
208 _matches: &ArgMatches,
209 ) -> Result<(bool, Option<String>), String> {
210 context.execute_single_step();
214 Ok((false, None))
215 }
216 fn extend(&self, command: Command) -> Command {
217 next::Args::augment_subcommands(command)
218 }
219}
220
221struct BreakpointCommand;
222impl DebuggerCommand for BreakpointCommand {
224 fn handle(
225 &self,
226 context: &mut Context,
227 matches: &ArgMatches,
228 ) -> Result<(bool, Option<String>), String> {
229 let args = breakpoint::Args::from_arg_matches(matches).unwrap();
230 match run_ext_api::<breakpoint::Api>(context, &args) {
231 Err(IxaError::IxaError(e)) => Ok((false, Some(format!("error: {e}")))),
232 Ok(return_value) => {
233 match return_value {
234 breakpoint::Retval::List(bp_list) => {
235 let mut msg = format!("Scheduled breakpoints: {}\n", bp_list.len());
236 for bp in bp_list {
237 _ = writeln!(&mut msg, "\t{bp}");
238 }
239 return Ok((false, Some(msg)));
240 }
241 breakpoint::Retval::Ok => { }
242 }
243
244 Ok((false, None))
245 }
246 _ => unimplemented!(),
247 }
248 }
249 fn extend(&self, command: Command) -> Command {
250 breakpoint::Args::augment_subcommands(command)
251 }
252}
253
254struct ContinueCommand;
255#[derive(Parser, Debug)]
256enum ContinueSubcommand {
257 Continue,
259}
260impl DebuggerCommand for ContinueCommand {
261 fn handle(
262 &self,
263 _context: &mut Context,
264 _matches: &ArgMatches,
265 ) -> Result<(bool, Option<String>), String> {
266 Ok((true, None))
267 }
268 fn extend(&self, command: Command) -> Command {
269 ContinueSubcommand::augment_subcommands(command)
270 }
271}
272
273fn exit_debugger() -> ! {
274 println!("Got Ctrl-D, Exiting...");
275 std::process::exit(0);
276}
277
278#[allow(clippy::missing_panics_doc)]
280pub fn enter_debugger(context: &mut Context) {
281 let current_time = context.get_current_time();
282 context.cancel_debugger_request();
283
284 let mut debugger = context.get_data_mut(DebuggerPlugin).take().unwrap();
287
288 println!("Debugging simulation at t={current_time}");
289 loop {
290 let line = match debugger.rl.readline(&format!("t={current_time:.4} $ ")) {
291 Ok(line) => line,
292 Err(
293 rustyline::error::ReadlineError::Signal(_)
294 | rustyline::error::ReadlineError::Interrupted,
295 ) => continue,
296 Err(rustyline::error::ReadlineError::Eof) => exit_debugger(),
297 Err(err) => panic!("Read error: {err}"),
298 };
299 debugger
300 .rl
301 .add_history_entry(line.clone())
302 .expect("Should be able to add to input");
303 let line = line.trim();
304 if line.is_empty() {
305 continue;
306 }
307
308 match debugger.process_command(line, context) {
309 Ok((quit, message)) => {
310 if let Some(message) = message {
311 println!("{message}");
312 }
313 if quit {
314 break;
315 }
316 }
317 Err(err) => {
318 eprintln!("{err}");
319 }
320 }
321 }
322
323 let saved_debugger = context.get_data_mut(DebuggerPlugin);
325 *saved_debugger = Some(debugger);
326}
327
328#[cfg(test)]
329mod tests {
330 use assert_approx_eq::assert_approx_eq;
331
332 use super::{enter_debugger, DebuggerPlugin};
333 use crate::{
334 define_global_property, define_person_property, Context, ContextGlobalPropertiesExt,
335 ContextPeopleExt, ExecutionPhase,
336 };
337
338 fn process_line(line: &str, context: &mut Context) -> (bool, Option<String>) {
339 let data_container = context.get_data_mut(DebuggerPlugin);
342 let debugger = data_container.take().unwrap();
343
344 let res = debugger.process_command(line, context).unwrap();
345 let data_container = context.get_data_mut(DebuggerPlugin);
346 *data_container = Some(debugger);
347 res
348 }
349
350 define_global_property!(FooGlobal, String);
351 define_global_property!(BarGlobal, u32);
352 define_person_property!(Age, u8);
353 define_person_property!(Smile, u32);
354
355 #[test]
356 fn test_cli_debugger_breakpoint_set() {
357 let context = &mut Context::new();
358 let (quits, _output) = process_line("breakpoint set 4.0\n", context);
359
360 assert!(!quits, "should not exit");
361
362 let list = context.list_breakpoints(0);
363 assert_eq!(list.len(), 1);
364 if let Some(schedule) = list.first() {
365 assert_eq!(schedule.priority, ExecutionPhase::First);
366 assert_eq!(schedule.plan_id, 0u64);
367 assert_approx_eq!(schedule.time, 4.0f64);
368 }
369 }
370
371 #[test]
372 fn test_cli_debugger_breakpoint_list() {
373 let context = &mut Context::new();
374
375 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
376 context.schedule_debugger(2.0, Some(ExecutionPhase::First), Box::new(enter_debugger));
377 context.schedule_debugger(3.0, Some(ExecutionPhase::Normal), Box::new(enter_debugger));
378 context.schedule_debugger(4.0, Some(ExecutionPhase::Last), Box::new(enter_debugger));
379
380 let expected = r"Scheduled breakpoints: 4
381 0: t=1 (First)
382 1: t=2 (First)
383 2: t=3 (Normal)
384 3: t=4 (Last)
385";
386
387 let (quits, output) = process_line("breakpoint list\n", context);
388
389 assert!(!quits, "should not exit");
390 assert!(output.is_some());
391 assert_eq!(output.unwrap(), expected);
392 }
393
394 #[test]
395 fn test_cli_debugger_breakpoint_delete_id() {
396 let context = &mut Context::new();
397
398 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
399 context.schedule_debugger(2.0, None, Box::new(enter_debugger));
400
401 let (quits, _output) = process_line("breakpoint delete 0\n", context);
402 assert!(!quits, "should not exit");
403 let list = context.list_breakpoints(0);
404
405 assert_eq!(list.len(), 1);
406 if let Some(schedule) = list.first() {
407 assert_eq!(schedule.priority, ExecutionPhase::First);
408 assert_eq!(schedule.plan_id, 1u64);
409 assert_approx_eq!(schedule.time, 2.0f64);
410 }
411 }
412
413 #[test]
414 fn test_cli_debugger_breakpoint_delete_all() {
415 let context = &mut Context::new();
416
417 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
418 context.schedule_debugger(2.0, None, Box::new(enter_debugger));
419
420 let (quits, _output) = process_line("breakpoint delete --all\n", context);
421 assert!(!quits, "should not exit");
422 let list = context.list_breakpoints(0);
423 assert_eq!(list.len(), 0);
424 }
425
426 #[test]
427 fn test_cli_debugger_breakpoint_disable_enable() {
428 let context = &mut Context::new();
429
430 let (quits, _output) = process_line("breakpoint disable\n", context);
431 assert!(!quits, "should not exit");
432 assert!(!context.breakpoints_are_enabled());
433
434 let (quits, _output) = process_line("breakpoint enable\n", context);
435 assert!(!quits, "should not exit");
436 assert!(context.breakpoints_are_enabled());
437 }
438
439 #[test]
440 fn test_cli_debugger_population() {
441 let context = &mut Context::new();
442 context.add_person(()).unwrap();
444 context.add_person(()).unwrap();
445
446 let (quits, output) = process_line("population\n", context);
447
448 assert!(!quits, "should not exit");
449 assert!(output.unwrap().contains('2'));
450 }
451
452 #[test]
453 fn test_cli_debugger_people_get() {
454 let context = &mut Context::new();
455 context.add_person((Age, 10)).unwrap();
457 context.add_person((Age, 5)).unwrap();
458 assert_eq!(context.remaining_plan_count(), 0);
459 let (_, output) = process_line("people get 0 Age", context);
460 assert_eq!(output.unwrap(), "Age: 10");
461 let (_, output) = process_line("people get 1 Age", context);
462 assert_eq!(output.unwrap(), "Age: 5");
463 }
464
465 #[test]
466 fn test_cli_debugger_people_properties() {
467 let context = &mut Context::new();
468 context.add_person(((Age, 10), (Smile, 50))).unwrap();
470 context.add_person(((Age, 5), (Smile, 60))).unwrap();
471 let (_, output) = process_line("people get 0 Smile", context);
472 assert_eq!(output.unwrap(), "Smile: 50");
473 let (_, output) = process_line("people properties", context);
474 let properties = output.unwrap();
475 assert!(properties.contains("Smile"));
476 assert!(properties.contains("Age"));
477 }
478
479 #[test]
480 fn test_cli_debugger_people_tabulate() {
481 let context = &mut Context::new();
482 context.add_person(((Age, 10), (Smile, 50))).unwrap();
484 context.add_person(((Age, 10), (Smile, 60))).unwrap();
485 context.add_person(((Age, 10), (Smile, 60))).unwrap();
486 let (_, output) = process_line("people tabulate Age", context);
487 assert_eq!(output.unwrap(), "3: Age=10");
488 let (_, output) = process_line("people tabulate Smile", context);
489 let results = output.unwrap();
490 assert!(results.contains("1: Smile=50"));
491 assert!(results.contains("2: Smile=60"));
492 }
493
494 #[test]
495 fn test_cli_debugger_global_list() {
496 let context = &mut Context::new();
497 let (_quits, output) = process_line("global list\n", context);
498 let expected = format!(
499 "{} global properties registered:",
500 context.list_registered_global_properties().len()
501 );
502 assert!(output.unwrap().contains(&expected));
504 }
505
506 #[test]
507 fn test_cli_debugger_global_no_args() {
508 let input = "global get\n";
509 let context = &mut Context::new();
510
511 let debugger = context.get_data_mut(DebuggerPlugin).take().unwrap();
514 let result = debugger.process_command(input, context);
515 let data_container = context.get_data_mut(DebuggerPlugin);
516 *data_container = Some(debugger);
517
518 assert!(result.is_err());
519 assert!(result
520 .unwrap_err()
521 .contains("required arguments were not provided"));
522 }
523
524 #[test]
525 fn test_cli_debugger_global_get_unregistered_prop() {
526 let context = &mut Context::new();
527 let (_quits, output) = process_line("global get NotExist\n", context);
528 assert_eq!(output.unwrap(), "error: No global property: NotExist");
529 }
530
531 #[test]
532 fn test_cli_debugger_global_get_registered_prop() {
533 let context = &mut Context::new();
534 context
535 .set_global_property_value(FooGlobal, "hello".to_string())
536 .unwrap();
537 let (_quits, output) = process_line("global get ixa.FooGlobal\n", context);
538 assert_eq!(output.unwrap(), "\"hello\"");
539 }
540
541 #[test]
542 fn test_cli_debugger_global_get_empty_prop() {
543 define_global_property!(EmptyGlobal, String);
544 let context = &mut Context::new();
545 let (_quits, output) = process_line("global get ixa.EmptyGlobal\n", context);
546 assert_eq!(
547 output.unwrap(),
548 "error: Property ixa.EmptyGlobal is not set"
549 );
550 }
551
552 #[test]
553 fn test_cli_continue() {
554 let context = &mut Context::new();
555 let (quits, _) = process_line("continue\n", context);
556 assert!(quits, "should exit");
557 }
558
559 #[test]
560 fn test_cli_next() {
561 let context = &mut Context::new();
562 assert_eq!(context.remaining_plan_count(), 0);
563 let (quits, _) = process_line("next\n", context);
564 assert!(!quits, "should not exit");
565 }
566}