Skip to content

Commit 2616a47

Browse files
authored
Merge pull request #257 from posit-dev/feature/r-trace
Include R-level backtrace in failing `RFunction` calls
2 parents 2aaa7c5 + 0219d45 commit 2616a47

File tree

13 files changed

+307
-158
lines changed

13 files changed

+307
-158
lines changed

crates/ark/src/errors.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use harp::object::RObject;
99
use harp::r_symbol;
10+
use harp::session::r_format_traceback;
1011
use libr::R_GlobalEnv;
1112
use libr::R_NilValue;
1213
use libr::Rf_eval;
@@ -43,6 +44,11 @@ unsafe extern "C" fn ps_record_error(evalue: SEXP, traceback: SEXP) -> anyhow::R
4344
Ok(R_NilValue)
4445
}
4546

47+
#[harp::register]
48+
unsafe extern "C" fn ps_format_traceback(calls: SEXP) -> anyhow::Result<SEXP> {
49+
Ok(r_format_traceback(calls.into())?.sexp)
50+
}
51+
4652
pub unsafe fn initialize() {
4753
// Must be called after the public Positron function environment is set up
4854
info!("Initializing global error handler");

crates/ark/src/interface.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,17 @@ pub fn start_r(
184184
libraries.initialize_post_setup_r();
185185

186186
unsafe {
187-
// Optionally run a user specified R startup script
188-
if let Some(file) = &startup_file {
189-
r_source(file).or_log_error(&format!("Failed to source startup file '{file}' due to"));
190-
}
191-
192187
// Register embedded routines
193188
r_register_routines();
194189

195190
// Initialize harp (after routine registration)
196191
harp::initialize();
197192

193+
// Optionally run a user specified R startup script (after harp init)
194+
if let Some(file) = &startup_file {
195+
r_source(file).or_log_error(&format!("Failed to source startup file '{file}' due to"));
196+
}
197+
198198
// Initialize support functions (after routine registration)
199199
if let Err(err) = modules::initialize(false) {
200200
log::error!("Can't load R modules: {err:?}");

crates/ark/src/modules/positron/errors.R

Lines changed: 2 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -137,99 +137,8 @@ handle_error_base <- function(cnd) {
137137
}
138138

139139
#' @param traceback A list of calls.
140-
format_traceback <- function(traceback = list()) {
141-
n <- length(traceback)
142-
143-
# TODO: This implementation prints the traceback in the same ordering
144-
# as rlang, i.e. with call 1 on the stack being the first thing you
145-
# see. `traceback()` prints in the reverse order, so users may want a
146-
# way to reverse both our display ordering and rlang's (requiring a
147-
# new `rlang::format()` argument).
148-
149-
# Collect source location, if there is any
150-
srcrefs <- lapply(traceback, function(call) attr(call, "srcref"))
151-
srcrefs <- vapply(srcrefs, src_loc, FUN.VALUE = character(1))
152-
has_srcref <- nchar(srcrefs) != 0L
153-
srcrefs[has_srcref] <- vec_paste0(" at ", srcrefs[has_srcref])
154-
155-
# Converts to a list of quoted calls to a list of deparsd calls.
156-
# Respects global options `"traceback.max.lines"` and `"deparse.max.lines"`!
157-
traceback <- .traceback(traceback)
158-
159-
# Prepend the stack number to each deparsed call, padding multiline calls as needed,
160-
# and then collapse multiline calls into one line
161-
prefixes <- vec_paste0(seq_len(n), ". ")
162-
prefixes <- format(prefixes, justify = "right")
163-
164-
traceback <- mapply(prepend_prefix, traceback, prefixes, SIMPLIFY = FALSE)
165-
traceback <- lapply(traceback, function(lines) paste0(lines, collapse = "\n"))
166-
traceback <- as.character(traceback)
167-
168-
paste0(traceback, srcrefs)
169-
}
170-
171-
prepend_prefix <- function(lines, prefix) {
172-
n_lines <- length(lines)
173-
174-
if (n_lines == 0L) {
175-
return(lines)
176-
}
177-
178-
# First line gets the prefix
179-
line <- lines[[1L]]
180-
line <- vec_paste0(prefix, line)
181-
182-
# Other lines are padded with whitespace as needed
183-
padding <- strrep(" ", times = nchar(prefix))
184-
185-
lines <- lines[-1L]
186-
lines <- vec_paste0(padding, lines)
187-
188-
lines <- c(line, lines)
189-
190-
lines
191-
}
192-
193-
src_loc <- function(srcref) {
194-
# Adapted from `rlang:::src_loc()`
195-
if (is.null(srcref)) {
196-
return("")
197-
}
198-
199-
srcfile <- attr(srcref, "srcfile")
200-
if (is.null(srcfile)) {
201-
return("")
202-
}
203-
204-
# May be:
205-
# - An actual file path
206-
# - `""` for user defined functions in the console
207-
# - `"<text>"` for `parse()`d functions
208-
# We only try and display the source location for file paths
209-
file <- srcfile$filename
210-
if (identical(file, "") || identical(file, "<text>")) {
211-
return("")
212-
}
213-
214-
file_trimmed <- path_trim_prefix(file, 3L)
215-
216-
first_line <- srcref[[1L]]
217-
first_column <- srcref[[5L]]
218-
219-
# TODO: We could generate file hyperlinks here like `rlang:::src_loc()`
220-
paste0(file_trimmed, ":", first_line, ":", first_column)
221-
}
222-
223-
path_trim_prefix <- function(path, n) {
224-
# `rlang:::path_trim_prefix()`
225-
split <- strsplit(path, "/")[[1]]
226-
n_split <- length(split)
227-
228-
if (n_split <= n) {
229-
path
230-
} else {
231-
paste(split[seq(n_split - n + 1, n_split)], collapse = "/")
232-
}
140+
format_traceback <- function(calls = list()) {
141+
.ps.Call("ps_format_traceback", calls)
233142
}
234143

235144
handle_error_rlang <- function(cnd) {

crates/harp/src/error.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub enum Error {
2525
EvaluationError {
2626
code: String,
2727
message: String,
28+
trace: String,
2829
},
2930
UnsafeEvaluationError(String),
3031
UnexpectedLength(usize, usize),
@@ -89,8 +90,18 @@ impl fmt::Display for Error {
8990
write!(f, "Error parsing {}: {}", code, message)
9091
},
9192

92-
Error::EvaluationError { code, message } => {
93-
write!(f, "Error evaluating {}: {}", code, message)
93+
Error::EvaluationError {
94+
code,
95+
message,
96+
trace,
97+
} => {
98+
let mut msg = format!("Error evaluating {code}: {message}");
99+
100+
if !trace.is_empty() {
101+
msg = format!("{msg}\nR backtrace:\n{trace}");
102+
}
103+
104+
write!(f, "{msg}")
94105
},
95106

96107
Error::UnsafeEvaluationError(code) => {

crates/harp/src/eval.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub fn r_parse_eval(code: &str, options: RParseEvalOptions) -> Result<RObject> {
5959
return Err(Error::EvaluationError {
6060
code: code.to_string(),
6161
message: geterrmessage(),
62+
trace: String::from(""),
6263
});
6364
}
6465
}

crates/harp/src/exec.rs

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ use crate::error::Result;
1919
use crate::interrupts::RInterruptsSuspendedScope;
2020
use crate::line_ending::convert_line_endings;
2121
use crate::line_ending::LineEnding;
22+
use crate::modules::HARP_ENV;
23+
use crate::object::r_list_get;
2224
use crate::object::RObject;
2325
use crate::polled_events::RPolledEventsSuspendedScope;
2426
use crate::protect::RProtect;
27+
use crate::r_null;
2528
use crate::r_string;
2629
use crate::r_symbol;
27-
use crate::utils::r_inherits;
2830
use crate::utils::r_stringify;
2931
use crate::vector::CharacterVector;
3032
use crate::vector::Vector;
@@ -83,38 +85,48 @@ impl RFunction {
8385
}
8486

8587
pub fn call_in(&mut self, env: SEXP) -> Result<RObject> {
86-
unsafe {
87-
let call = self.call.build();
88-
89-
let mut protect = RProtect::new();
90-
91-
// now, wrap call in tryCatch, so that errors don't longjmp
92-
let try_catch = protect.add(Rf_lang3(
93-
r_symbol!("::"),
94-
r_symbol!("base"),
95-
r_symbol!("tryCatch"),
96-
));
97-
let call = protect.add(Rf_lang4(
98-
try_catch,
99-
call.sexp,
100-
r_symbol!("identity"),
101-
r_symbol!("identity"),
102-
));
103-
SET_TAG(call, R_NilValue);
104-
SET_TAG(CDDR(call), r_symbol!("error"));
105-
SET_TAG(CDDDR(call), r_symbol!("interrupt"));
106-
107-
let result = protect.add(Rf_eval(call, env));
108-
109-
if r_inherits(result, "error") {
110-
let code = r_stringify(call, "\n")?;
111-
let message = geterrmessage();
112-
return Err(Error::EvaluationError { code, message });
113-
}
88+
let user_call = self.call.build();
89+
r_safe_eval(user_call, env.into())
90+
}
91+
}
11492

115-
return Ok(RObject::new(result));
93+
pub fn r_safe_eval(expr: RObject, env: RObject) -> crate::Result<RObject> {
94+
// We could detect and cancel early exits from the r side, but we'd be at
95+
// risk of very unlucky interrupts occurring between `rf_eval()` and our
96+
// `on.exit()` handling. So stay on the safe side by wrapping in
97+
// top-level-exec. This also insulates us from user handlers.
98+
r_top_level_exec(|| unsafe {
99+
let eval_call = RCall::new(r_symbol!("safe_evalq"))
100+
.add(expr.sexp)
101+
.add(env)
102+
.build();
103+
104+
let result = RObject::new(Rf_eval(eval_call.sexp, HARP_ENV.unwrap()));
105+
106+
// Invariant of return value: List of length 2 [output, error].
107+
// These are exclusive.
108+
let out = r_list_get(result.sexp, 0);
109+
let err = r_list_get(result.sexp, 1);
110+
111+
if err != r_null() {
112+
let code = r_stringify(expr.sexp, "\n")?;
113+
114+
// Invariant of error slot: Character vector of length 2 [message, trace],
115+
// with `trace` possibly an empty string.
116+
let err = CharacterVector::new(err)?;
117+
118+
let message = err.get_value(0)?;
119+
let trace = err.get_value(1)?;
120+
121+
return Err(Error::EvaluationError {
122+
code,
123+
message,
124+
trace,
125+
});
116126
}
117-
}
127+
128+
Ok(RObject::new(out))
129+
})?
118130
}
119131

120132
impl From<&str> for RFunction {
@@ -177,6 +189,8 @@ pub fn geterrmessage() -> String {
177189
}
178190
}
179191

192+
// TODO: Reimplement around `r_safe_eval()` to gain backtraces
193+
180194
/// Wrappers around R_tryCatch()
181195
///
182196
/// Takes a single closure that returns either a SEXP or `()`. If an R error is
@@ -320,25 +334,10 @@ pub unsafe fn r_try_catch<F, R>(fun: F) -> Result<RObject>
320334
where
321335
F: FnMut() -> R,
322336
RObject: From<R>,
323-
{
324-
let out = r_try_catch_any(fun);
325-
out.map(|x| RObject::from(x))
326-
}
327-
328-
pub unsafe fn r_try_catch_any<F, R>(fun: F) -> Result<R>
329-
where
330-
F: FnMut() -> R,
331337
{
332338
let vector = CharacterVector::create(["error"]);
333-
r_try_catch_finally(fun, vector, || {})
334-
}
335-
336-
pub unsafe fn r_try_catch_classes<F, R, S>(fun: F, classes: S) -> Result<R>
337-
where
338-
F: FnMut() -> R,
339-
S: Into<CharacterVector>,
340-
{
341-
r_try_catch_finally(fun, classes, || {})
339+
let out = r_try_catch_finally(fun, vector, || {});
340+
out.map(|x| RObject::from(x))
342341
}
343342

344343
/// Run closure inside top-level context
@@ -681,6 +680,22 @@ mod tests {
681680
}
682681
}
683682

683+
#[test]
684+
fn test_basic_function_error() {
685+
r_test! {
686+
let result = RFunction::from("+")
687+
.add(1)
688+
.add("")
689+
.call();
690+
691+
assert_match!(result, Err(err) => {
692+
let msg = format!("{err}");
693+
let re = regex::Regex::new("R backtrace:\n(.|\n)*1L [+] \"\"").unwrap();
694+
assert!(re.is_match(&msg));
695+
});
696+
}
697+
}
698+
684699
#[test]
685700
fn test_utf8_strings() {
686701
r_test! {

crates/harp/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ pub type Result<T> = std::result::Result<T, error::Error>;
6262
// doesn't need synchronisation.
6363
pub static mut R_MAIN_THREAD_ID: Option<std::thread::ThreadId> = None;
6464

65+
pub fn r_null() -> libr::SEXP {
66+
unsafe { libr::R_NilValue }
67+
}
68+
6569
#[macro_export]
6670
macro_rules! with_vector_impl {
6771
($x:expr, $class:ident, $variable:ident, $($code:tt)*) => {{

0 commit comments

Comments
 (0)