ixa/
debugger.rs

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    /// Handle the command and any inputs; returning true will exit the debugger
22    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    // Build the debugger context.
37    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() // cli can only be used once.
78            .try_get_matches_from(args)?;
79
80        if let Some((command, _)) = matches.subcommand() {
81            // If the provided command is known, run its handler
82
83            if let Some(handler) = self.get_command(command) {
84                return handler.handle(context, &matches);
85            }
86            // Unexpected command: print an error
87            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
123/// Exits the debugger and ends the simulation.
124struct 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
139/// Adds a new debugger breakpoint at t
140struct NextCommand;
141impl DebuggerCommand for NextCommand {
142    fn handle(
143        &self,
144        context: &mut Context,
145        _matches: &ArgMatches,
146    ) -> Result<(bool, Option<String>), DebuggerError> {
147        // We execute directly instead of setting `Context::break_requested` so as not to interfere
148        // with anything else that might be requesting a break, or in case debugger sessions become
149        // stateful.
150        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;
159/// Adds a new debugger breakpoint at t
160impl 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 => { /* pass */ }
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    /// Exits the debugger and continues the simulation
194    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/// Starts a debugging REPL session, interrupting the normal simulation event loop.
215#[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    // We temporarily swap out the debugger so we can have simultaneous mutable access to
221    // it and to `context`. We swap it back in at the end of the function.
222    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    // Restore the debugger
260    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        // Temporarily take the data container out of context so that
273        // we can operate on context.
274        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        // Note: the global property names are also listed as part of the output
379        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        // We can't use process_line here because we expect an error to be
388        // returned rather than string output
389        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}