Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions crates/mq-dap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ mq-dbg query.mq input.md
Once connected to a DAP client:

1. **Set Breakpoints**: Click in the gutter or use your editor's breakpoint command
2. **Start Debugging**: Launch the debugger with your query file
3. **Step Through Code**: Use step over, step in, and step out commands
4. **Inspect Variables**: Hover over variables or view them in the variables pane
5. **View Call Stack**: See the current execution stack in the call stack pane
2. **Conditional Breakpoints**: Only stop when an mq expression evaluates truthy, e.g. `x > 3`
3. **Hit Count Breakpoints**: Only stop once the hit count condition is met. A bare number, e.g. `3`, is shorthand for `hit_count >= 3`; otherwise it's evaluated as an mq expression with `hit_count` bound to the current hit count, e.g. `hit_count >= 3 && x == 1`
4. **Logpoints**: Log a message instead of stopping; uses mq's own `${expr}` string interpolation syntax, e.g. `x is ${x}`. `${self}` yields the current pipeline value and `${$VAR}` reads the environment variable `VAR`
5. **Start Debugging**: Launch the debugger with your query file
6. **Step Through Code**: Use step over, step in, and step out commands
7. **Inspect Variables**: Hover over variables or view them in the variables pane
8. **Watch Expressions**: Add mq expressions to your editor's watch pane to re-evaluate them against the current scope every time execution stops
9. **View Call Stack**: See the current execution stack in the call stack pane

### Example Debug Session

Expand Down
121 changes: 119 additions & 2 deletions crates/mq-dap/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crossbeam_channel::{Receiver, Sender};
use dap::prelude::*;
use dap::responses::{
ContinueResponse, EvaluateResponse, ScopesResponse, SetBreakpointsResponse, SetExceptionBreakpointsResponse,
SetVariableResponse, StackTraceResponse, ThreadsResponse, VariablesResponse,
SetExpressionResponse, SetVariableResponse, StackTraceResponse, ThreadsResponse, VariablesResponse,
};
use dap::types::Breakpoint;
use mq_lang::Shared;
Expand Down Expand Up @@ -205,6 +205,10 @@ impl MqAdapter {

server.send_event(event)?;
}
DebuggerMessage::LogPoint { message } => {
debug!(message = %message, "Sending output event for logpoint");
self.send_log_output(&message, server)?;
}
DebuggerMessage::Terminated => {
debug!("Sending terminated event");

Expand Down Expand Up @@ -346,10 +350,13 @@ impl MqAdapter {
self.engine.debugger().write().unwrap().remove_breakpoints(&source);

for breakpoint in &breakpoints_vec {
let id = self.engine.debugger().write().unwrap().add_breakpoint(
let id = self.engine.debugger().write().unwrap().add_breakpoint_with_options(
breakpoint.line as usize,
breakpoint.column.map(|bp| bp as usize),
source.clone(),
breakpoint.condition.clone(),
breakpoint.hit_condition.clone(),
breakpoint.log_message.clone(),
);
breakpoints_response.push(Breakpoint {
verified: true,
Expand Down Expand Up @@ -480,6 +487,21 @@ impl MqAdapter {
}));
server.respond(rsp)?;
}
Command::SetExpression(args) => {
debug!(?args, "Received SetExpression request");
self.eval(format!("let {} = {}", args.expression, args.value).as_str())?;

let value = args.value.clone();
let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse {
value,
type_field: None,
presentation_hint: None,
variables_reference: None,
named_variables: None,
indexed_variables: None,
}));
server.respond(rsp)?;
}
Command::Continue(_) => {
debug!("Received Continue request");
self.send_debugger_command(DapCommand::Continue)?;
Expand Down Expand Up @@ -718,6 +740,7 @@ mod tests {
column: None,
enabled: true,
source: None,
..Default::default()
};

let message = DebuggerMessage::BreakpointHit {
Expand All @@ -732,6 +755,23 @@ mod tests {
assert!(adapter.current_debug_context.is_some());
}

#[test]
fn test_handle_debugger_message_log_point_does_not_stop_execution() {
let mut adapter = MqAdapter::new();
let input = BufReader::new(Cursor::new(Vec::new()));
let output = BufWriter::new(Cursor::new(Vec::new()));
let mut server = Server::new(input, output);

let message = DebuggerMessage::LogPoint {
message: "x is 3".to_string(),
};

let result = adapter.handle_debugger_message(message, &mut server);
assert!(result.is_ok());
// A logpoint must not be treated as a stop: no debug context should be recorded.
assert!(adapter.current_debug_context.is_none());
}

#[test]
fn test_handle_debugger_message_step_completed() {
let mut adapter = MqAdapter::new();
Expand Down Expand Up @@ -1038,6 +1078,83 @@ mod tests {
assert!(result.is_ok());
}

#[test]
fn test_handle_request_set_breakpoints_forwards_condition_hit_condition_and_log_message() {
let mut adapter = MqAdapter::new();
adapter.query_file = Some("/path/to/test.mq".to_string());
let input = BufReader::new(Cursor::new(Vec::new()));
let output = BufWriter::new(Cursor::new(Vec::new()));
let mut server = Server::new(input, output);

let source = dap::types::Source {
name: Some("test.mq".to_string()),
path: Some("/path/to/test.mq".to_string()),
adapter_data: None,
source_reference: None,
presentation_hint: None,
origin: None,
checksums: None,
sources: None,
};

let breakpoints = vec![dap::types::SourceBreakpoint {
line: 10,
column: None,
condition: Some("x > 1".to_string()),
hit_condition: Some(">= 2".to_string()),
log_message: Some("x is {x}".to_string()),
}];

#[allow(deprecated)]
let req = Request {
seq: 1,
command: Command::SetBreakpoints(dap::requests::SetBreakpointsArguments {
source,
breakpoints: Some(breakpoints),
lines: None,
source_modified: None,
}),
};

let result = adapter.handle_request(req, &mut server);
assert!(result.is_ok());

let debugger = adapter.engine.debugger();
let debugger = debugger.read().unwrap();
let stored = debugger.list_breakpoints();
assert_eq!(stored.len(), 1);
assert_eq!(stored[0].condition.as_deref(), Some("x > 1"));
assert_eq!(stored[0].hit_condition.as_deref(), Some(">= 2"));
assert_eq!(stored[0].log_message.as_deref(), Some("x is {x}"));
}

#[test]
fn test_handle_request_set_expression() {
let mut adapter = MqAdapter::new();
let input = BufReader::new(Cursor::new(Vec::new()));
let output = BufWriter::new(Cursor::new(Vec::new()));
let mut server = Server::new(input, output);

let req = Request {
seq: 1,
command: Command::SetExpression(dap::requests::SetExpressionArguments {
expression: "test_var".to_string(),
value: "42".to_string(),
frame_id: None,
format: None,
}),
};

let result = adapter.handle_request(req, &mut server);
// No active debug context yet, so evaluation fails, but the command must be handled
// (not fall through to UnhandledCommand) now that supportsSetExpression is advertised.
assert!(result.is_err());
assert!(!matches!(
result.unwrap_err().downcast_ref::<MqAdapterError>(),
Some(MqAdapterError::UnhandledCommand(_))
));
}

#[test]
fn test_handle_request_unhandled_command() {
let mut adapter = MqAdapter::new();
Expand Down
44 changes: 44 additions & 0 deletions crates/mq-dap/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ impl mq_lang::DebuggerHandler for DapHandlerWrapper {
}
}

fn on_log_point(&self, breakpoint: &mq_lang::Breakpoint, message: &str, _context: &mq_lang::DebugContext) {
debug!(line = breakpoint.line, message = %message, "Logpoint hit");

if let Err(e) = self.handler.message_tx.send(DebuggerMessage::LogPoint {
message: message.to_string(),
}) {
error!(error = %e, "Failed to send logpoint message to DAP server");
}
}

fn on_step(&self, context: &mq_lang::DebugContext) -> mq_lang::DebuggerAction {
debug!(line = context.token.range.start.line + 1, "Step event");

Expand Down Expand Up @@ -174,6 +184,34 @@ mod tests {
assert!(debug_str.contains("DapHandlerWrapper"));
}

#[test]
fn test_on_log_point_sends_message_without_blocking() {
let (message_tx, message_rx) = unbounded::<DebuggerMessage>();
let (_command_tx, command_rx) = unbounded::<DapCommand>();

let handler = DapDebuggerHandler::new(message_tx);
let wrapper = DapHandlerWrapper::new(handler, command_rx);

let breakpoint = mq_lang::Breakpoint {
id: 1,
line: 10,
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

// Should return immediately without waiting on command_rx (nothing was sent to it).
wrapper.on_log_point(&breakpoint, "x is 3", &context);

let received_message = message_rx.try_recv().unwrap();
match received_message {
DebuggerMessage::LogPoint { message } => assert_eq!(message, "x is 3"),
_ => panic!("Expected LogPoint message"),
}
}

#[test]
fn test_on_breakpoint_hit_continue() {
let (message_tx, message_rx) = unbounded::<DebuggerMessage>();
Expand All @@ -188,6 +226,7 @@ mod tests {
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

Expand Down Expand Up @@ -224,6 +263,7 @@ mod tests {
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

Expand All @@ -247,6 +287,7 @@ mod tests {
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

Expand All @@ -270,6 +311,7 @@ mod tests {
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

Expand All @@ -293,6 +335,7 @@ mod tests {
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

Expand All @@ -316,6 +359,7 @@ mod tests {
column: Some(5),
enabled: true,
source: None,
..Default::default()
};
let context = mq_lang::DebugContext::default();

Expand Down
4 changes: 2 additions & 2 deletions crates/mq-dap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
//! # Features
//!
//! - Full DAP protocol support for mq debugging
//! - Breakpoint management
//! - Breakpoint management, including conditional breakpoints, hit count breakpoints, and logpoints
//! - Step-through execution (step in, step out, step over)
//! - Variable inspection
//! - Variable inspection and watch expressions
//! - Stack trace visualization
//! - Expression evaluation in debug context
//!
Expand Down
2 changes: 2 additions & 0 deletions crates/mq-dap/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub enum DebuggerMessage {
line: usize,
context: mq_lang::DebugContext,
},
/// A logpoint breakpoint fired; should send an output event without stopping execution.
LogPoint { message: String },
/// Program has terminated
Terminated,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/mq-dap/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub fn start() -> DynResult<()> {
supports_evaluate_for_hovers: Some(true),
supports_exception_options: Some(false),
supports_exception_filter_options: Some(false),
supports_conditional_breakpoints: Some(true),
supports_hit_conditional_breakpoints: Some(true),
supports_log_points: Some(true),
..Default::default()
};
let rsp = req.success(ResponseBody::Initialize(capabilities));
Expand Down
Loading