use std::sync::Mutex;
use itertools::Itertools;
use once_cell::sync::Lazy;
use starlark_derive::starlark_module;
use thiserror::Error;
use crate as starlark;
use crate::environment::GlobalsBuilder;
use crate::eval::Evaluator;
use crate::read_line::ReadLine;
use crate::syntax::AstModule;
use crate::syntax::Dialect;
use crate::values::none::NoneType;
static BREAKPOINT_MUTEX: Lazy<Mutex<State>> = Lazy::new(|| Mutex::new(State::Allow));
pub(crate) trait BreakpointConsole {
fn read_line(&mut self) -> anyhow::Result<Option<String>>;
fn println(&mut self, line: &str);
}
pub(crate) struct RealBreakpointConsole {
read_line: ReadLine,
}
impl BreakpointConsole for RealBreakpointConsole {
fn read_line(&mut self) -> anyhow::Result<Option<String>> {
self.read_line.read_line("$> ")
}
fn println(&mut self, line: &str) {
eprintln!("{}", line);
}
}
impl RealBreakpointConsole {
pub(crate) fn factory() -> Box<dyn Fn() -> anyhow::Result<Box<dyn BreakpointConsole>>> {
Box::new(|| {
Ok(Box::new(RealBreakpointConsole {
read_line: ReadLine::new("STARLARK_RUST_DEBUGGER_HISTFILE")?,
}))
})
}
}
#[derive(PartialEq, Eq)]
enum State {
Allow, Stop, }
enum Next {
Again, Resume, Fail, }
fn cmd_help(_eval: &mut Evaluator, rl: &mut dyn BreakpointConsole) -> anyhow::Result<Next> {
for (name, msg, _) in COMMANDS {
rl.println(&format!("* :{}, {}", name[0], msg))
}
Ok(Next::Again)
}
fn cmd_variables(eval: &mut Evaluator, rl: &mut dyn BreakpointConsole) -> anyhow::Result<Next> {
fn truncate(mut s: String, n: usize) -> String {
if s.len() > n {
s.truncate(n);
s.push_str("...");
}
s
}
for (name, value) in eval.local_variables() {
rl.println(&format!("* {} = {}", name, truncate(value.to_string(), 80)))
}
Ok(Next::Again)
}
fn cmd_stack(eval: &mut Evaluator, rl: &mut dyn BreakpointConsole) -> anyhow::Result<Next> {
for line in eval.call_stack().to_string().lines() {
rl.println(line)
}
Ok(Next::Again)
}
fn cmd_resume(_eval: &mut Evaluator, _rl: &mut dyn BreakpointConsole) -> anyhow::Result<Next> {
Ok(Next::Resume)
}
fn cmd_fail(_eval: &mut Evaluator, _rl: &mut dyn BreakpointConsole) -> anyhow::Result<Next> {
Ok(Next::Fail)
}
const COMMANDS: &[(
&[&str], &str, fn(eval: &mut Evaluator, &mut dyn BreakpointConsole) -> anyhow::Result<Next>,
)] = &[
(&["help", "?"], "Show this help message", cmd_help),
(&["vars"], "Show all local variables", cmd_variables),
(&["stack"], "Show the stack trace", cmd_stack),
(&["resume", "quit", "exit"], "Resume execution", cmd_resume),
(&["fail"], "Abort with a failure message", cmd_fail),
];
fn pick_command(
x: &str,
rl: &mut dyn BreakpointConsole,
) -> Option<fn(eval: &mut Evaluator, &mut dyn BreakpointConsole) -> anyhow::Result<Next>> {
let mut poss = Vec::new();
for (names, _, cmd) in COMMANDS {
for n in *names {
if *n == x {
return Some(*cmd);
}
if n.starts_with(x) {
poss.push((n, cmd));
break;
}
}
}
match poss.as_slice() {
[] => rl.println("Unrecognised command, type :help for all commands"),
[x] => return Some(*x.1),
xs => rl.println(&format!(
"Ambiguous command, could have been any of: {}",
xs.iter().map(|x| x.0).join(" ")
)),
}
None
}
fn breakpoint_loop(
eval: &mut Evaluator,
mut rl: Box<dyn BreakpointConsole>,
) -> anyhow::Result<State> {
loop {
let readline = rl.read_line()?;
match readline {
Some(line) => {
if let Some(line) = line.strip_prefix(':') {
if let Some(cmd) = pick_command(line.trim_end(), &mut *rl) {
match cmd(eval, &mut *rl)? {
Next::Again => {}
Next::Resume => return Ok(State::Allow),
Next::Fail => {
return Err(anyhow::anyhow!("Selected :fail at breakpoint()"));
}
}
}
} else {
let ast = AstModule::parse("interactive", line, &Dialect::AllOptionsInternal);
let res = ast.and_then(|ast| eval.eval_statements(ast));
match res {
Err(e) => {
rl.println(&format!("{:#}", e));
}
Ok(v) => {
if !v.is_none() {
rl.println(&v.to_string())
}
}
}
}
}
None => return Ok(State::Stop),
}
}
}
#[derive(Error, Debug)]
enum BreakpointError {
#[error("Breakpoint handler is not enabled for current Evaluator")]
NoHandler,
}
const BREAKPOINT_HIT_MESSAGE: &str = "BREAKPOINT HIT! :resume to continue, :help for all options";
#[starlark_module]
pub fn global(builder: &mut GlobalsBuilder) {
fn breakpoint(eval: &mut Evaluator) -> anyhow::Result<NoneType> {
{
let mut guard = BREAKPOINT_MUTEX.lock().unwrap();
if *guard == State::Allow {
let mut rl = match &mut eval.breakpoint_handler {
Some(rl) => rl()?,
None => return Err(BreakpointError::NoHandler.into()),
};
rl.println(BREAKPOINT_HIT_MESSAGE);
*guard = breakpoint_loop(eval, rl)?;
}
}
Ok(NoneType)
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::env;
use std::rc::Rc;
use dupe::Dupe;
use super::*;
use crate::assert::Assert;
static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn reset_global_state() {
*BREAKPOINT_MUTEX.lock().unwrap() = State::Allow;
}
#[test]
fn test_breakpoint_real() {
let _g = TEST_MUTEX.lock();
reset_global_state();
if env::var("BREAKPOINT") != Ok("1".to_owned()) {
return;
}
let mut a = Assert::new();
a.setup_eval(|e| e.enable_terminal_breakpoint_console());
a.globals_add(global);
a.pass("x = [1,2,3]; breakpoint(); print(x)");
}
#[test]
fn test_breakpoint_mock() {
let _g = TEST_MUTEX.lock();
reset_global_state();
let printed_lines = Rc::new(RefCell::new(Vec::new()));
let printed_lines_copy = printed_lines.dupe();
let mut a = Assert::new();
a.globals_add(global);
a.setup_eval(move |eval| {
let printed_lines = printed_lines.dupe();
eval.breakpoint_handler = Some(Box::new(move || {
printed_lines.borrow_mut().clear();
struct Handler {
printed_lines: Rc<RefCell<Vec<String>>>,
called: bool,
}
impl BreakpointConsole for Handler {
fn read_line(&mut self) -> anyhow::Result<Option<String>> {
let called = self.called;
self.called = true;
if !called {
Ok(Some("x".to_owned()))
} else {
Ok(None)
}
}
fn println(&mut self, line: &str) {
self.printed_lines.borrow_mut().push(line.to_owned());
}
}
Ok(Box::new(Handler {
printed_lines: printed_lines.dupe(),
called: false,
}))
}));
});
a.pass("x = [1,2,3]; breakpoint()");
assert_eq!(
vec![BREAKPOINT_HIT_MESSAGE, "[1, 2, 3]"],
*printed_lines_copy.borrow()
);
}
#[test]
fn test_breakpoint_disabled() {
let _g = TEST_MUTEX.lock();
reset_global_state();
let mut a = Assert::new();
a.globals_add(global);
a.fail(
"x = [1,2,3]; breakpoint()",
"Breakpoint handler is not enabled",
);
}
}