use super::Dispatched;
use crate::backend::{Backend, BreakId, BreakLoc, CanonicalOps, CanonicalReq};
pub fn dispatch_to(input: &str, backend: &dyn Backend) -> Dispatched {
let input = input.trim();
let (verb, rest) = split_verb(input);
if verb == "tool" {
return Dispatched::Immediate(render_tool_info(backend));
}
if verb == "raw" {
if rest.is_empty() {
return Dispatched::Immediate(
"usage: dbg raw <native-command> (sends the rest of the line verbatim)".into(),
);
}
return Dispatched::Native {
canonical_op: "raw",
native_cmd: rest.to_string(),
decorate: false,
structured: None,
};
}
let ops = match backend.canonical_ops() {
Some(o) => o,
None => return Dispatched::Fallthrough,
};
match verb {
"break" => dispatch_break(ops, rest),
"unbreak" => dispatch_unbreak(ops, rest),
"breaks" => one_arg_native(ops.op_breaks(), "breaks"),
"run" => dispatch_run(ops, rest),
"continue" => one_arg_native(ops.op_continue(), "continue"),
"step" | "step-into" | "step-in" | "stepi" => {
one_arg_native(ops.op_step(), "step")
}
"next" | "step-over" | "stepover" => {
one_arg_native(ops.op_next(), "next")
}
"finish" | "step-out" | "stepout" => {
one_arg_native(ops.op_finish(), "finish")
}
"pause" => one_arg_native(ops.op_pause(), "pause"),
"restart" => one_arg_native(ops.op_restart(), "restart"),
"stack" => dispatch_stack(ops, rest),
"frame" => dispatch_frame(ops, rest),
"locals" => dispatch_locals(ops),
"print" => dispatch_print(ops, rest),
"set" => dispatch_set(ops, rest),
"watch" => dispatch_watch(ops, rest),
"threads" => one_arg_native(ops.op_threads(), "threads"),
"thread" => dispatch_thread(ops, rest),
"list" => dispatch_list(ops, rest),
"catch" => dispatch_catch(ops, rest),
_ => Dispatched::Fallthrough,
}
}
fn split_verb(s: &str) -> (&str, &str) {
match s.find(|c: char| c.is_ascii_whitespace()) {
Some(i) => (&s[..i], s[i..].trim_start()),
None => (s, ""),
}
}
fn render_tool_info(backend: &dyn Backend) -> String {
match backend.canonical_ops() {
Some(ops) => {
let ver = ops
.tool_version()
.unwrap_or_else(|| "(version unavailable)".into());
format!(
"tool: {tool}\nversion: {ver}\nbackend: {name} — {desc}\nescape-hatch: `dbg raw <native-command>`",
tool = ops.tool_name(),
ver = ver,
name = backend.name(),
desc = backend.description(),
)
}
None => format!(
"tool: {name}\nversion: (not exposed via canonical ops)\nbackend: {name} — {desc}\ncanonical ops not available for this backend — all commands go through raw passthrough.",
name = backend.name(),
desc = backend.description(),
),
}
}
fn one_arg_native(
cmd: anyhow::Result<String>,
canonical_op: &'static str,
) -> Dispatched {
match cmd {
Ok(native_cmd) => Dispatched::Native {
canonical_op,
native_cmd,
decorate: true,
structured: None,
},
Err(e) => Dispatched::Immediate(format!("[error: {e}]")),
}
}
fn dispatch_break(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
if rest.is_empty() {
return Dispatched::Immediate(
"usage: dbg break <file:line | symbol | module!method> [if <cond>] [log <msg>]".into(),
);
}
let (head, log_msg) = match rest.find(" log ") {
Some(i) => (&rest[..i], &rest[i + 5..]),
None => (rest, ""),
};
let (loc_str, cond) = match head.find(" if ") {
Some(i) => (&head[..i], &head[i + 4..]),
None => (head, ""),
};
let loc = BreakLoc::parse(loc_str.trim());
let cond_trim = cond.trim();
let log_trim = log_msg.trim();
let result = if !log_trim.is_empty() {
ops.op_break_log(&loc, log_trim)
} else if cond_trim.is_empty() {
ops.op_break(&loc)
} else {
ops.op_break_conditional(&loc, cond_trim)
};
match result {
Ok(cmd) => Dispatched::Native {
canonical_op: "break",
native_cmd: cmd,
decorate: true,
structured: Some(CanonicalReq::Break {
loc,
cond: if cond_trim.is_empty() { None } else { Some(cond_trim.to_string()) },
log: if log_trim.is_empty() { None } else { Some(log_trim.to_string()) },
}),
},
Err(e) => Dispatched::Immediate(format!("[error: {e}]")),
}
}
fn dispatch_unbreak(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let id = match rest.parse::<u32>() {
Ok(n) => BreakId(n),
Err(_) => {
return Dispatched::Immediate(
"usage: dbg unbreak <id> (id comes from `dbg breaks`)".into(),
);
}
};
one_arg_native(ops.op_unbreak(id), "unbreak")
}
fn dispatch_run(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let args: Vec<String> = if rest.is_empty() {
vec![]
} else {
rest.split_whitespace().map(str::to_string).collect()
};
one_arg_native(ops.op_run(&args), "run")
}
fn dispatch_stack(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let n = rest.parse::<u32>().ok();
one_arg_native(ops.op_stack(n), "stack")
}
fn dispatch_frame(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let n = match rest.parse::<u32>() {
Ok(n) => n,
Err(_) => {
return Dispatched::Immediate("usage: dbg frame <index>".into());
}
};
one_arg_native(ops.op_frame(n), "frame")
}
fn dispatch_print(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
if rest.is_empty() {
return Dispatched::Immediate("usage: dbg print <expression>".into());
}
one_arg_native(ops.op_print(rest), "print")
}
fn dispatch_set(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let (lhs, rhs) = match rest.find('=') {
Some(i) => (rest[..i].trim(), rest[i + 1..].trim()),
None => return Dispatched::Immediate("usage: dbg set <lhs> = <expr>".into()),
};
if lhs.is_empty() || rhs.is_empty() {
return Dispatched::Immediate("usage: dbg set <lhs> = <expr>".into());
}
one_arg_native(ops.op_set(lhs, rhs), "set")
}
fn dispatch_locals(ops: &dyn CanonicalOps) -> Dispatched {
match ops.op_locals() {
Ok(native_cmd) => Dispatched::Native {
canonical_op: "locals",
native_cmd,
decorate: true,
structured: None,
},
Err(e) => {
let reason = e.to_string();
if reason.contains("not supported by") || reason.contains("unsupported") {
let payload = serde_json::json!({
"unsupported": true,
"op": "locals",
"reason": reason,
});
Dispatched::Immediate(payload.to_string())
} else {
Dispatched::Immediate(format!("[error: {reason}]"))
}
}
}
}
fn dispatch_watch(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
if rest.is_empty() {
return Dispatched::Immediate("usage: dbg watch <expression>".into());
}
one_arg_native(ops.op_watch(rest), "watch")
}
fn dispatch_thread(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let n = match rest.parse::<u32>() {
Ok(n) => n,
Err(_) => {
return Dispatched::Immediate("usage: dbg thread <index>".into());
}
};
one_arg_native(ops.op_thread(n), "thread")
}
fn dispatch_list(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let loc = if rest.is_empty() { None } else { Some(rest) };
one_arg_native(ops.op_list(loc), "list")
}
fn dispatch_catch(ops: &dyn CanonicalOps, rest: &str) -> Dispatched {
let filters: Vec<String> = if rest.is_empty() || rest.trim() == "off" {
vec![]
} else {
rest.split(|c: char| c.is_ascii_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
};
one_arg_native(ops.op_catch(&filters), "catch")
}
pub fn decorate_output(backend: &dyn Backend, output: &str) -> String {
decorate_output_for_op(backend, "", output)
}
pub fn decorate_output_for_op(
backend: &dyn Backend,
canonical_op: &str,
output: &str,
) -> String {
let Some(ops) = backend.canonical_ops() else {
return output.to_string();
};
let processed = ops.postprocess_output(canonical_op, output);
let name = ops.tool_name();
let header = match ops.tool_version() {
Some(v) => format!("[via {name} {v}]\n"),
None => format!("[via {name}]\n"),
};
format!("{header}{processed}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::lldb::LldbBackend;
use crate::backend::pdb::PdbBackend;
use crate::backend::perf::PerfBackend;
fn lldb() -> LldbBackend { LldbBackend }
fn native_of(d: Dispatched) -> (&'static str, String, bool) {
match d {
Dispatched::Native { canonical_op, native_cmd, decorate, .. } => {
(canonical_op, native_cmd, decorate)
}
other => panic!("expected Native, got {:?}", describe(&other)),
}
}
fn describe(d: &Dispatched) -> String {
match d {
Dispatched::Native { canonical_op, native_cmd, decorate, .. } => {
format!("Native({canonical_op}, {native_cmd:?}, decorate={decorate})")
}
Dispatched::Immediate(s) => format!("Immediate({s:?})"),
Dispatched::Query(q) => format!("Query({})", q.canonical_op()),
Dispatched::Lifecycle(l) => format!("Lifecycle({})", l.canonical_op()),
Dispatched::Fallthrough => "Fallthrough".into(),
}
}
fn structured_of(d: Dispatched) -> Option<crate::backend::CanonicalReq> {
match d {
Dispatched::Native { structured, .. } => structured,
other => panic!("expected Native, got {:?}", describe(&other)),
}
}
#[test]
fn break_carries_structured_req_plain() {
use crate::backend::{BreakLoc, CanonicalReq};
let d = dispatch_to("break app.go:10", &lldb());
match structured_of(d) {
Some(CanonicalReq::Break { loc, cond, log }) => {
assert_eq!(loc, BreakLoc::FileLine { file: "app.go".into(), line: 10 });
assert!(cond.is_none());
assert!(log.is_none());
}
other => panic!("expected Break, got {other:?}"),
}
}
#[test]
fn break_carries_structured_req_with_cond_and_log() {
use crate::backend::delve_proto::DelveProtoBackend;
use crate::backend::{BreakLoc, CanonicalReq};
let d = dispatch_to("break app.go:10 if x > 0", &DelveProtoBackend);
match structured_of(d) {
Some(CanonicalReq::Break { loc, cond, log }) => {
assert_eq!(loc, BreakLoc::FileLine { file: "app.go".into(), line: 10 });
assert_eq!(cond.as_deref(), Some("x > 0"));
assert!(log.is_none());
}
other => panic!("expected Break, got {other:?}"),
}
let d = dispatch_to("break app.go:10 log hit {x}", &DelveProtoBackend);
match structured_of(d) {
Some(CanonicalReq::Break { loc, cond, log }) => {
assert_eq!(loc, BreakLoc::FileLine { file: "app.go".into(), line: 10 });
assert!(cond.is_none());
assert_eq!(log.as_deref(), Some("hit {x}"));
}
other => panic!("expected Break, got {other:?}"),
}
}
#[test]
fn non_break_ops_have_no_structured_req() {
assert!(structured_of(dispatch_to("continue", &lldb())).is_none());
assert!(structured_of(dispatch_to("step", &lldb())).is_none());
}
#[test]
fn break_file_line_routes_to_lldb_syntax() {
let b = lldb();
let d = dispatch_to("break src/foo.rs:42", &b);
let (op, cmd, dec) = native_of(d);
assert_eq!(op, "break");
assert_eq!(cmd, "breakpoint set --file src/foo.rs --line 42");
assert!(dec);
}
#[test]
fn break_fqn_routes_by_name() {
let d = dispatch_to("break main", &lldb());
let (_, cmd, _) = native_of(d);
assert_eq!(cmd, "breakpoint set --name main");
}
#[test]
fn break_module_method_routes_via_shlib() {
let d = dispatch_to("break libfoo.so!bar", &lldb());
let (_, cmd, _) = native_of(d);
assert_eq!(cmd, "breakpoint set --shlib libfoo.so --name bar");
}
#[test]
fn exec_verbs_translate() {
let b = lldb();
for (verb, expected) in [
("continue", "process continue"),
("step", "thread step-in"),
("next", "thread step-over"),
("finish", "thread step-out"),
] {
let (op, cmd, _) = native_of(dispatch_to(verb, &b));
assert_eq!(op, verb);
assert_eq!(cmd, expected, "{verb}");
}
}
#[test]
fn ide_style_step_aliases_route_to_canonical() {
let b = lldb();
for (alias, canonical_op, expected_cmd) in [
("step-into", "step", "thread step-in"),
("step-in", "step", "thread step-in"),
("stepi", "step", "thread step-in"),
("step-over", "next", "thread step-over"),
("stepover", "next", "thread step-over"),
("step-out", "finish", "thread step-out"),
("stepout", "finish", "thread step-out"),
] {
let (op, cmd, _) = native_of(dispatch_to(alias, &b));
assert_eq!(op, canonical_op, "alias {alias} routed to wrong op");
assert_eq!(cmd, expected_cmd, "alias {alias} produced wrong native cmd");
}
}
#[test]
fn stack_with_count_passes_arg() {
let (_, cmd, _) = native_of(dispatch_to("stack 5", &lldb()));
assert_eq!(cmd, "thread backtrace --count 5");
}
#[test]
fn stack_without_count_plain() {
let (_, cmd, _) = native_of(dispatch_to("stack", &lldb()));
assert_eq!(cmd, "thread backtrace");
}
#[test]
fn print_forwards_expression_verbatim() {
let (_, cmd, _) = native_of(dispatch_to("print a + b * 2", &lldb()));
assert_eq!(cmd, "expression -- a + b * 2");
}
#[test]
fn unknown_verb_is_fallthrough() {
match dispatch_to("breakpoint list", &lldb()) {
Dispatched::Fallthrough => {}
other => panic!("expected Fallthrough, got {}", describe(&other)),
}
}
#[test]
fn raw_passthrough_no_decoration() {
let d = dispatch_to("raw breakpoint set --file main.c --line 1", &lldb());
let (op, cmd, dec) = native_of(d);
assert_eq!(op, "raw");
assert_eq!(cmd, "breakpoint set --file main.c --line 1");
assert!(!dec, "raw must not decorate");
}
#[test]
fn raw_without_payload_is_usage_hint() {
let d = dispatch_to("raw", &lldb());
match d {
Dispatched::Immediate(s) => assert!(s.contains("usage")),
other => panic!("expected Immediate, got {}", describe(&other)),
}
}
#[test]
fn watch_unsupported_on_pdb_is_immediate_error() {
let d = dispatch_to("watch x", &PdbBackend);
match d {
Dispatched::Immediate(s) => {
assert!(s.contains("pdb"));
assert!(s.contains("watchpoints"));
assert!(s.contains("dbg raw"));
}
other => panic!("expected Immediate error, got {}", describe(&other)),
}
}
#[test]
fn break_usage_hint_when_empty() {
match dispatch_to("break", &lldb()) {
Dispatched::Immediate(s) => assert!(s.contains("usage")),
other => panic!("expected Immediate, got {}", describe(&other)),
}
}
#[test]
fn unbreak_rejects_non_numeric() {
match dispatch_to("unbreak foo", &lldb()) {
Dispatched::Immediate(s) => assert!(s.contains("usage")),
other => panic!("expected Immediate, got {}", describe(&other)),
}
}
#[test]
fn frame_rejects_non_numeric() {
match dispatch_to("frame foo", &lldb()) {
Dispatched::Immediate(s) => assert!(s.contains("usage")),
other => panic!("expected Immediate, got {}", describe(&other)),
}
}
#[test]
fn tool_works_on_backend_with_canonical_ops() {
let d = dispatch_to("tool", &lldb());
match d {
Dispatched::Immediate(s) => {
assert!(s.starts_with("tool: lldb"));
assert!(s.contains("escape-hatch"));
assert!(s.contains("dbg raw"));
}
other => panic!("expected Immediate, got {}", describe(&other)),
}
}
#[test]
fn tool_works_even_without_canonical_ops() {
let d = dispatch_to("tool", &PerfBackend);
match d {
Dispatched::Immediate(s) => {
assert!(s.contains("perf"));
assert!(s.contains("canonical ops not available"));
}
other => panic!("expected Immediate, got {}", describe(&other)),
}
}
#[test]
fn fallthrough_when_backend_has_no_canonical_ops() {
match dispatch_to("continue", &PerfBackend) {
Dispatched::Fallthrough => {}
other => panic!("expected Fallthrough, got {}", describe(&other)),
}
}
#[test]
fn decorate_prepends_via_header_when_ops_available() {
let out = decorate_output(&lldb(), "hello\n");
assert!(out.starts_with("[via lldb"));
assert!(out.contains("\nhello\n"));
}
#[test]
fn decorate_passthrough_on_backend_without_ops() {
let out = decorate_output(&PerfBackend, "unchanged");
assert_eq!(out, "unchanged");
}
#[test]
fn pdb_break_routes_to_pdb_syntax() {
let (_, cmd, _) = native_of(dispatch_to("break app.py:10", &PdbBackend));
assert_eq!(cmd, "break app.py:10");
}
#[test]
fn pdb_exec_verbs_match_pdb_vocabulary() {
let b = PdbBackend;
for (verb, expected) in [
("continue", "continue"),
("step", "step"),
("next", "next"),
("finish", "return"),
] {
let (_, cmd, _) = native_of(dispatch_to(verb, &b));
assert_eq!(cmd, expected);
}
}
}