use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BreakLoc {
FileLine { file: String, line: u32 },
Fqn(String),
ModuleMethod { module: String, method: String },
}
impl BreakLoc {
pub fn parse(spec: &str) -> Self {
if let Some((m, meth)) = spec.split_once('!') {
if !m.is_empty() && !meth.is_empty() {
return BreakLoc::ModuleMethod {
module: m.to_string(),
method: meth.to_string(),
};
}
}
if let Some((f, l)) = spec.rsplit_once(':') {
if !l.is_empty() && l.chars().all(|c| c.is_ascii_digit()) {
if let Ok(line) = l.parse::<u32>() {
return BreakLoc::FileLine {
file: f.to_string(),
line,
};
}
}
}
BreakLoc::Fqn(spec.to_string())
}
pub fn location_key(&self) -> String {
match self {
BreakLoc::FileLine { file, line } => format!("{file}:{line}"),
BreakLoc::Fqn(s) => s.clone(),
BreakLoc::ModuleMethod { module, method } => format!("{module}!{method}"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BreakId(pub u32);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CanonicalReq {
Break {
loc: BreakLoc,
cond: Option<String>,
log: Option<String>,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HitEvent {
pub location_key: String,
pub thread: Option<String>,
pub frame_symbol: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
}
pub trait CanonicalOps: Send + Sync {
fn tool_name(&self) -> &'static str;
fn tool_version(&self) -> Option<String> {
None
}
fn op_break(&self, loc: &BreakLoc) -> anyhow::Result<String> {
Ok(match loc {
BreakLoc::FileLine { file, line } => format!("break {file}:{line}"),
BreakLoc::Fqn(name) => format!("bfn {name}"),
BreakLoc::ModuleMethod { module, method } => format!("bfn {module}::{method}"),
})
}
fn op_break_conditional(&self, loc: &BreakLoc, cond: &str) -> anyhow::Result<String> {
if cond.is_empty() {
self.op_break(loc)
} else {
Err(unsupported(self.tool_name(), "conditional breakpoints"))
}
}
fn op_break_log(&self, _loc: &BreakLoc, _msg: &str) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "logpoints"))
}
fn op_unbreak(&self, id: BreakId) -> anyhow::Result<String> {
Ok(format!("breakpoint delete {}", id.0))
}
fn op_breaks(&self) -> anyhow::Result<String> {
Ok("breakpoint list".into())
}
fn op_run(&self, _args: &[String]) -> anyhow::Result<String> {
Ok("continue".into())
}
fn op_continue(&self) -> anyhow::Result<String> {
Ok("continue".into())
}
fn op_step(&self) -> anyhow::Result<String> {
Ok("step".into())
}
fn op_next(&self) -> anyhow::Result<String> {
Ok("next".into())
}
fn op_finish(&self) -> anyhow::Result<String> {
Ok("out".into())
}
fn op_pause(&self) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "pause"))
}
fn op_restart(&self) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "restart"))
}
fn op_stack(&self, _n: Option<u32>) -> anyhow::Result<String> {
Ok("backtrace".into())
}
fn postprocess_output(&self, _canonical_op: &str, out: &str) -> String {
out.to_string()
}
fn op_frame(&self, n: u32) -> anyhow::Result<String> {
Ok(format!("frame {n}"))
}
fn op_locals(&self) -> anyhow::Result<String> {
Ok("locals".into())
}
fn op_print(&self, expr: &str) -> anyhow::Result<String> {
Ok(format!("print {expr}"))
}
fn op_set(&self, _lhs: &str, _rhs: &str) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "variable assignment"))
}
fn op_watch(&self, _expr: &str) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "watchpoints"))
}
fn op_threads(&self) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "thread listing"))
}
fn op_thread(&self, _n: u32) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "thread switching"))
}
fn op_list(&self, _loc: Option<&str>) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "source listing"))
}
fn op_catch(&self, _filters: &[String]) -> anyhow::Result<String> {
Err(unsupported(self.tool_name(), "exception breakpoints"))
}
fn parse_hit(&self, _output: &str) -> Option<HitEvent> {
None
}
fn parse_locals(&self, _output: &str) -> Option<Value> {
None
}
fn auto_capture_locals(&self) -> bool {
true
}
}
pub fn unsupported(tool: &'static str, what: &str) -> anyhow::Error {
anyhow::anyhow!(
"{what} not supported by {tool}: use `dbg raw <native-command>` to send backend-specific commands"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_file_line() {
assert_eq!(
BreakLoc::parse("src/main.rs:42"),
BreakLoc::FileLine { file: "src/main.rs".into(), line: 42 }
);
}
#[test]
fn parse_fqn() {
assert_eq!(
BreakLoc::parse("foo::bar::baz"),
BreakLoc::Fqn("foo::bar::baz".into())
);
}
#[test]
fn parse_module_method() {
assert_eq!(
BreakLoc::parse("libfoo.so!bar"),
BreakLoc::ModuleMethod {
module: "libfoo.so".into(),
method: "bar".into(),
}
);
}
#[test]
fn parse_colon_in_fqn_not_mistaken_for_line() {
assert_eq!(
BreakLoc::parse("foo::bar"),
BreakLoc::Fqn("foo::bar".into())
);
}
#[test]
fn parse_empty_module_falls_through_to_filename() {
assert_eq!(
BreakLoc::parse("!foo"),
BreakLoc::Fqn("!foo".into())
);
}
#[test]
fn location_key_stable_across_forms() {
let fl = BreakLoc::FileLine { file: "m.c".into(), line: 1 };
assert_eq!(fl.location_key(), "m.c:1");
assert_eq!(BreakLoc::Fqn("main".into()).location_key(), "main");
assert_eq!(
BreakLoc::ModuleMethod { module: "m".into(), method: "f".into() }.location_key(),
"m!f"
);
}
#[test]
fn unsupported_mentions_raw_escape() {
let e = unsupported("pdb", "watchpoints").to_string();
assert!(e.contains("pdb"));
assert!(e.contains("watchpoints"));
assert!(e.contains("dbg raw"));
}
}