Skip to content

Commit db014da

Browse files
authored
Implement debugging fallback when there are no srcrefs (#249)
* Implement debugging fallback when there are no srcrefs * Switch to explicit typing * Rename `sources` -> `fallback_sources` * Add comment around `handle_stdout()` * Respond with real errors in `handle_source()` * Switch to internal `debugger_stack_info()` and new tooling * One more refinement to use `RFunction` * `lines_flatten()` -> `lines_join()` * Switch to a `stop()` for this hopefully impossible case * Also works for `parse(text = <text>, keep.source = TRUE)` * `tryCatch()` the `parse()` call This at least lets the `contents` flow through, so the editor opens even though we don't know where we are * Add fallback behavior for known non parseable cases * Don't use strings as the replacement If both `<environment>` and a user defined string of `"<environment>"` happen to exist there, then replacing the `<environment>` with a string version of itself results in `""<environment>""`, i.e. two empty strings surrounding an unparseable `<environment>`. Instead we insert an identifier that will parse but looks intentionally weird.
1 parent 359bb20 commit db014da

File tree

11 files changed

+1143
-220
lines changed

11 files changed

+1143
-220
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ark/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ cfg-if = "1.0.0"
2020
chrono = "0.4.23"
2121
crossbeam = { version = "0.8.2", features = ["crossbeam-channel"] }
2222
ctor = "0.1.26"
23-
dap = { git = "https://github.com/lionel-/dap-rs", branch = "feature/all" }
23+
dap = { git = "https://github.com/sztomi/dap-rs", branch = "main" }
2424
dashmap = "5.4.0"
2525
ego-tree = "0.6.2"
2626
harp = { path = "../harp" }

crates/ark/src/dap/dap.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
//
66
//
77

8+
use std::collections::HashMap;
89
use std::sync::Arc;
910
use std::sync::Mutex;
1011

1112
use amalthea::comm::comm_channel::CommMsg;
1213
use amalthea::language::server_handler::ServerHandler;
1314
use crossbeam::channel::Sender;
14-
use harp::session::FrameInfo;
1515
use serde_json::json;
1616
use stdext::log_error;
1717
use stdext::spawn;
1818

19+
use crate::dap::dap_r_main::FrameInfo;
20+
use crate::dap::dap_r_main::FrameSource;
1921
use crate::dap::dap_server;
2022
use crate::request::RRequest;
2123

@@ -47,6 +49,13 @@ pub struct Dap {
4749
/// Current call stack
4850
pub stack: Option<Vec<FrameInfo>>,
4951

52+
/// Map of `source` -> `source_reference` used for frames that don't have
53+
/// associated files (i.e. no `srcref` attribute). The `source` is the key to
54+
/// ensure that we don't insert the same function multiple times, which would result
55+
/// in duplicate virtual editors being opened on the client side.
56+
pub fallback_sources: HashMap<String, i32>,
57+
current_source_reference: i32,
58+
5059
/// Channel for sending events to the comm frontend.
5160
comm_tx: Option<Sender<CommMsg>>,
5261

@@ -65,6 +74,8 @@ impl Dap {
6574
is_connected: false,
6675
backend_events_tx: None,
6776
stack: None,
77+
fallback_sources: HashMap::new(),
78+
current_source_reference: 1,
6879
comm_tx: None,
6980
r_request_tx,
7081
shared_self: None,
@@ -82,6 +93,24 @@ impl Dap {
8293
}
8394

8495
pub fn start_debug(&mut self, stack: Vec<FrameInfo>) {
96+
// Load `fallback_sources` with this stack's text sources
97+
for frame in stack.iter() {
98+
let source = &frame.source;
99+
100+
match source {
101+
FrameSource::File(_) => continue,
102+
FrameSource::Text(source) => {
103+
if self.fallback_sources.contains_key(source) {
104+
// Already in `fallback_sources`, associated with an existing `source_reference`
105+
continue;
106+
}
107+
self.fallback_sources
108+
.insert(source.clone(), self.current_source_reference);
109+
self.current_source_reference += 1;
110+
},
111+
}
112+
}
113+
85114
self.stack = Some(stack);
86115

87116
if self.is_debugging {
@@ -106,6 +135,8 @@ impl Dap {
106135
pub fn stop_debug(&mut self) {
107136
// Reset state
108137
self.stack = None;
138+
self.fallback_sources.clear();
139+
self.current_source_reference = 1;
109140
self.is_debugging = false;
110141

111142
if self.is_connected {

crates/ark/src/dap/dap_r_main.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
//
2+
// dap_r_main.rs
3+
//
4+
// Copyright (C) 2024 Posit Software, PBC. All rights reserved.
5+
//
6+
//
7+
8+
use std::sync::Arc;
9+
use std::sync::Mutex;
10+
11+
use anyhow::anyhow;
12+
use harp::exec::RFunction;
13+
use harp::exec::RFunctionExt;
14+
use harp::object::RObject;
15+
use harp::protect::RProtect;
16+
use harp::r_string;
17+
use harp::session::r_sys_calls;
18+
use harp::session::r_sys_functions;
19+
use libr::R_NilValue;
20+
use libr::R_Srcref;
21+
use libr::Rf_allocVector;
22+
use libr::Rf_xlength;
23+
use libr::INTSXP;
24+
use libr::SET_INTEGER_ELT;
25+
use libr::SEXP;
26+
use libr::VECTOR_ELT;
27+
use stdext::log_error;
28+
29+
use crate::dap::dap::DapBackendEvent;
30+
use crate::dap::Dap;
31+
use crate::modules::ARK_ENVS;
32+
33+
pub struct RMainDap {
34+
/// Underlying dap state
35+
dap: Arc<Mutex<Dap>>,
36+
37+
/// Whether or not we are currently in a debugging state.
38+
debugging: bool,
39+
40+
/// The current call emitted by R as `debug: <call-text>`.
41+
call_text: DebugCallText,
42+
43+
/// The last known `start_line` for the active context frame.
44+
last_start_line: Option<i64>,
45+
}
46+
47+
#[derive(Clone, Debug)]
48+
pub enum DebugCallText {
49+
None,
50+
Capturing(String),
51+
Finalized(String),
52+
}
53+
54+
#[derive(Clone, Debug)]
55+
pub struct FrameInfo {
56+
/// The name shown in the editor tab bar when this frame is viewed.
57+
pub source_name: String,
58+
/// The name shown in the stack frame UI when this frame is visible.
59+
pub frame_name: String,
60+
pub source: FrameSource,
61+
pub start_line: i64,
62+
pub start_column: i64,
63+
pub end_line: i64,
64+
pub end_column: i64,
65+
}
66+
67+
#[derive(Clone, Debug)]
68+
pub enum FrameSource {
69+
File(String),
70+
Text(String),
71+
}
72+
73+
impl RMainDap {
74+
pub fn new(dap: Arc<Mutex<Dap>>) -> Self {
75+
Self {
76+
dap,
77+
debugging: false,
78+
call_text: DebugCallText::None,
79+
last_start_line: None,
80+
}
81+
}
82+
83+
pub fn is_debugging(&self) -> bool {
84+
self.debugging
85+
}
86+
87+
pub fn start_debug(&mut self, stack: Vec<FrameInfo>) {
88+
self.debugging = true;
89+
let mut dap = self.dap.lock().unwrap();
90+
dap.start_debug(stack)
91+
}
92+
93+
pub fn stop_debug(&mut self) {
94+
let mut dap = self.dap.lock().unwrap();
95+
dap.stop_debug();
96+
self.debugging = false;
97+
}
98+
99+
pub fn handle_stdout(&mut self, content: &str) {
100+
if let DebugCallText::Capturing(ref mut call_text) = self.call_text {
101+
// Append to current expression if we are currently capturing stdout
102+
call_text.push_str(content);
103+
return;
104+
}
105+
106+
// `debug: ` is emitted by R (if no srcrefs are available!) right before it emits
107+
// the current expression we are debugging, so we use that as a signal to begin
108+
// capturing.
109+
if content == "debug: " {
110+
self.call_text = DebugCallText::Capturing(String::new());
111+
return;
112+
}
113+
114+
// Entering or exiting a closure, reset the debug start line state and call text
115+
if content == "debugging in: " || content == "exiting from: " {
116+
self.last_start_line = None;
117+
self.call_text = DebugCallText::None;
118+
return;
119+
}
120+
}
121+
122+
pub fn finalize_call_text(&mut self) {
123+
match &self.call_text {
124+
// If not debugging, nothing to do.
125+
DebugCallText::None => (),
126+
// If already finalized, keep what we have.
127+
DebugCallText::Finalized(_) => (),
128+
// If capturing, transition to finalized.
129+
DebugCallText::Capturing(call_text) => {
130+
self.call_text = DebugCallText::Finalized(call_text.clone())
131+
},
132+
}
133+
}
134+
135+
pub fn stack_info(&mut self) -> anyhow::Result<Vec<FrameInfo>> {
136+
// We leave finalized `call_text` in place rather than setting it to `None` here
137+
// in case the user executes an arbitrary expression in the debug R console, which
138+
// loops us back here without updating the `call_text` in any way, allowing us to
139+
// recreate the debugger state after their code execution.
140+
let call_text = match self.call_text.clone() {
141+
DebugCallText::None => None,
142+
DebugCallText::Capturing(call_text) => {
143+
log::error!(
144+
"Call text is in `Capturing` state, but should be `Finalized`: '{call_text}'."
145+
);
146+
None
147+
},
148+
DebugCallText::Finalized(call_text) => Some(call_text),
149+
};
150+
151+
let last_start_line = self.last_start_line.clone();
152+
153+
let frames = r_stack_info(call_text, last_start_line)?;
154+
155+
// If we have `frames`, update the `last_start_line` with the context
156+
// frame's start line
157+
if let Some(frame) = frames.get(0) {
158+
self.last_start_line = Some(frame.start_line);
159+
}
160+
161+
Ok(frames)
162+
}
163+
164+
pub fn send_dap(&self, event: DapBackendEvent) {
165+
let dap = self.dap.lock().unwrap();
166+
if let Some(tx) = &dap.backend_events_tx {
167+
log_error!(tx.send(event));
168+
}
169+
}
170+
}
171+
172+
fn r_stack_info(
173+
context_call_text: Option<String>,
174+
context_last_start_line: Option<i64>,
175+
) -> anyhow::Result<Vec<FrameInfo>> {
176+
unsafe {
177+
let mut protect = RProtect::new();
178+
179+
let context_srcref = libr::get(R_Srcref);
180+
protect.add(context_srcref);
181+
182+
let context_call_text = match context_call_text {
183+
Some(context_call_text) => r_string!(context_call_text, &mut protect),
184+
None => R_NilValue,
185+
};
186+
187+
let context_last_start_line = match context_last_start_line {
188+
Some(context_last_start_line) => {
189+
let x = Rf_allocVector(INTSXP, 1);
190+
protect.add(x);
191+
SET_INTEGER_ELT(x, 0, i32::try_from(context_last_start_line)?);
192+
x
193+
},
194+
None => R_NilValue,
195+
};
196+
197+
let functions = r_sys_functions()?;
198+
protect.add(functions);
199+
200+
let calls = r_sys_calls()?;
201+
protect.add(calls);
202+
203+
let info = RFunction::new("", "debugger_stack_info")
204+
.add(context_call_text)
205+
.add(context_last_start_line)
206+
.add(context_srcref)
207+
.add(functions)
208+
.add(calls)
209+
.call_in(ARK_ENVS.positron_ns)?;
210+
211+
let n: isize = Rf_xlength(info.sexp).try_into()?;
212+
213+
let mut out = Vec::with_capacity(n as usize);
214+
215+
// Reverse the order for DAP
216+
for i in (0..n).rev() {
217+
let frame = VECTOR_ELT(info.sexp, i);
218+
out.push(as_frame_info(frame)?);
219+
}
220+
221+
Ok(out)
222+
}
223+
}
224+
225+
fn as_frame_info(info: SEXP) -> anyhow::Result<FrameInfo> {
226+
unsafe {
227+
let mut i = 0;
228+
229+
let source_name = VECTOR_ELT(info, i);
230+
let source_name: String = RObject::view(source_name).try_into()?;
231+
232+
i += 1;
233+
let frame_name = VECTOR_ELT(info, i);
234+
let frame_name: String = RObject::view(frame_name).try_into()?;
235+
236+
let mut source = None;
237+
238+
i += 1;
239+
let file = VECTOR_ELT(info, i);
240+
if file != R_NilValue {
241+
let file: String = RObject::view(file).try_into()?;
242+
source = Some(FrameSource::File(file));
243+
}
244+
245+
i += 1;
246+
let text = VECTOR_ELT(info, i);
247+
if text != R_NilValue {
248+
let text: String = RObject::view(text).try_into()?;
249+
source = Some(FrameSource::Text(text));
250+
}
251+
252+
let Some(source) = source else {
253+
return Err(anyhow!(
254+
"Expected either `file` or `text` to be non-`NULL`."
255+
));
256+
};
257+
258+
i += 1;
259+
let start_line = VECTOR_ELT(info, i);
260+
let start_line: i32 = RObject::view(start_line).try_into()?;
261+
262+
i += 1;
263+
let start_column = VECTOR_ELT(info, i);
264+
let start_column: i32 = RObject::view(start_column).try_into()?;
265+
266+
i += 1;
267+
let end_line = VECTOR_ELT(info, i);
268+
let end_line: i32 = RObject::view(end_line).try_into()?;
269+
270+
// For `end_column`, the column range provided by R is inclusive `[,]`, but the
271+
// one used on the DAP / Positron side is exclusive `[,)` so we have to add 1.
272+
i += 1;
273+
let end_column = VECTOR_ELT(info, i);
274+
let end_column: i32 = RObject::view(end_column).try_into()?;
275+
let end_column = end_column + 1;
276+
277+
Ok(FrameInfo {
278+
source_name,
279+
frame_name,
280+
source,
281+
start_line: start_line.try_into()?,
282+
start_column: start_column.try_into()?,
283+
end_line: end_line.try_into()?,
284+
end_column: end_column.try_into()?,
285+
})
286+
}
287+
}

0 commit comments

Comments
 (0)