Skip to content

Commit 77f2e98

Browse files
committed
Record backtraces in RFunction calls
1 parent 0c1edd3 commit 77f2e98

File tree

5 files changed

+119
-36
lines changed

5 files changed

+119
-36
lines changed

crates/harp/src/exec.rs

Lines changed: 45 additions & 26 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;
@@ -84,35 +86,36 @@ impl RFunction {
8486

8587
pub fn call_in(&mut self, env: SEXP) -> Result<RObject> {
8688
unsafe {
87-
let call = self.call.build();
89+
let user_call = self.call.build();
90+
let eval_call = RCall::new(r_symbol!("safe_evalq"))
91+
.add(user_call.sexp)
92+
.add(env)
93+
.build();
8894

89-
let mut protect = RProtect::new();
95+
let result = RObject::new(Rf_eval(eval_call.sexp, HARP_ENV.unwrap()));
96+
97+
// Invariant of return value: List of length 2 [output, error].
98+
// These are exclusive.
99+
let out = r_list_get(result.sexp, 0);
100+
let err = r_list_get(result.sexp, 1);
101+
102+
if err != r_null() {
103+
let code = r_stringify(user_call.sexp, "\n")?;
90104

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 });
105+
// Invariant of error slot: Character vector of length 2 [message, trace],
106+
// with `trace` possibly an empty string.
107+
let err = CharacterVector::new(err)?;
108+
109+
let message = err.get_value(0)?;
110+
let trace = err.get_value(1)?;
111+
112+
return Err(Error::EvaluationError {
113+
code,
114+
message: message + "\nTrace:\n" + &trace,
115+
});
113116
}
114117

115-
return Ok(RObject::new(result));
118+
return Ok(RObject::new(out));
116119
}
117120
}
118121
}
@@ -681,6 +684,22 @@ mod tests {
681684
}
682685
}
683686

687+
#[test]
688+
fn test_basic_function_error() {
689+
r_test! {
690+
let result = RFunction::from("+")
691+
.add(1)
692+
.add("")
693+
.call();
694+
695+
assert_match!(result, Err(err) => {
696+
let msg = format!("{err}");
697+
let re = regex::Regex::new("Trace:\n(.|\n)*1L [+] \"\"").unwrap();
698+
assert!(re.is_match(&msg));
699+
});
700+
}
701+
}
702+
684703
#[test]
685704
fn test_utf8_strings() {
686705
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)*) => {{

crates/harp/src/modules.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use anyhow::anyhow;
22
use libr::R_PreserveObject;
3+
use libr::Rf_eval;
34
use libr::SEXP;
45
use rust_embed::RustEmbed;
56

7+
use crate::call::RCall;
68
use crate::environment::R_ENVS;
9+
use crate::exec::r_parse_exprs;
710
use crate::exec::r_source_str_in;
8-
use crate::exec::RFunction;
9-
use crate::exec::RFunctionExt;
11+
use crate::exec::r_top_level_exec;
12+
use crate::r_symbol;
1013

1114
pub static mut HARP_ENV: Option<SEXP> = None;
1215

@@ -27,19 +30,32 @@ where
2730
}
2831

2932
pub fn init_modules() -> anyhow::Result<()> {
30-
let namespace = RFunction::new("base", "new.env")
31-
.param("parent", R_ENVS.base)
32-
.call()?;
33+
let namespace = unsafe {
34+
let new_env_call = RCall::new(r_symbol!("new.env")).build();
35+
Rf_eval(new_env_call.sexp, R_ENVS.base)
36+
};
3337

3438
unsafe {
35-
R_PreserveObject(namespace.sexp);
36-
HARP_ENV = Some(namespace.sexp);
39+
R_PreserveObject(namespace);
40+
HARP_ENV = Some(namespace);
3741
}
3842

43+
// We don't have `safe_eval()` yet so source the init file manually
44+
with_asset::<HarpModuleAsset, _>("init.R", |source| {
45+
let exprs = r_parse_exprs(source)?;
46+
unsafe {
47+
let source_call = RCall::new(r_symbol!("source"))
48+
.param("exprs", exprs)
49+
.param("local", namespace)
50+
.build();
51+
r_top_level_exec(|| Rf_eval(source_call.sexp, R_ENVS.base))?;
52+
}
53+
Ok(())
54+
})?;
55+
56+
// It's alright to source the init file twice
3957
for file in HarpModuleAsset::iter() {
40-
with_asset::<HarpModuleAsset, _>(&file, |source| {
41-
Ok(r_source_str_in(source, namespace.sexp)?)
42-
})?;
58+
with_asset::<HarpModuleAsset, _>(&file, |source| Ok(r_source_str_in(source, namespace)?))?;
4359
}
4460

4561
Ok(())

crates/harp/src/modules/init.R

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Invariants:
2+
#
3+
# - Return value: List of length 2 [output, error]. These are exclusive.
4+
#
5+
# - Error slot: Character vector of length 2 [message, trace], with `trace`
6+
# possibly an empty string.
7+
#
8+
# TODO: Prevent this function from jumping with on.exit
9+
safe_evalq <- function(expr, env) {
10+
# Create a promise to make call stack leaner
11+
do.call(delayedAssign, list("out", substitute(expr), env))
12+
13+
# Prepare non-local exit with error value
14+
err <- NULL
15+
delayedAssign("bail", return(list(NULL, err)))
16+
17+
handler <- function(cnd) {
18+
# Save backtrace in error value
19+
calls <- sys.calls()
20+
trace <- paste(format(calls), collapse = '\n')
21+
22+
message <- conditionMessage(cnd)
23+
24+
# A character vector is easier to destructure from Rust.
25+
err <<- c(message, trace)
26+
27+
# Trigger non-local return
28+
force(bail)
29+
}
30+
31+
withCallingHandlers(
32+
list(out, NULL),
33+
interrupt = handler,
34+
error = handler
35+
)
36+
}

crates/harp/src/vector/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ pub trait Vector {
6565
Ok(self.get_unchecked(index))
6666
}
6767

68+
// Better name?
69+
fn get_value(&self, index: isize) -> Result<Self::Type> {
70+
let value = self
71+
.get(index)?
72+
.ok_or(crate::error::Error::MissingValueError)?;
73+
Ok(value)
74+
}
75+
6876
unsafe fn new(object: impl Into<SEXP>) -> Result<Self>
6977
where
7078
Self: Sized,

0 commit comments

Comments
 (0)