Skip to content

Commit bcedb64

Browse files
t-kalinowskihadley
andauthored
Improve traceback() for dispatched methods (RConsortium#486)
* Improve `traceback()` for dispatched methods * fix typo, malformed test * Update R/generic.R Co-authored-by: Hadley Wickham <[email protected]> * better snapshot tests * delete unused helper * whitespace * `zap_srcref` before printing callstack in snapshot. * use `utils::removeSource` instead of `rlang::zap_srcref` * Add NEWS --------- Co-authored-by: Hadley Wickham <[email protected]>
1 parent 89ff0c7 commit bcedb64

File tree

6 files changed

+83
-5
lines changed

6 files changed

+83
-5
lines changed

NEWS.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# S7 (development version)
22

3+
* The call context of a dispatched method (as visible in `sys.calls()` and
4+
`traceback()`) no longer includes the inlined method and generic, resulting in
5+
more compact and readable tracebacks. The dispatched method call now contains
6+
only the method name, which serves as a hint for retrieving the method. For
7+
example: `method(my_generic, class_double)`(x=10, ...). (#486)
8+
39
* `new_class()` now automatically infers the package name when called from
410
within an R package (#459).
511

R/generic.R

+3
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ generic_add_method <- function(generic, signature, method) {
193193
p_tbl <- generic@methods
194194
chr_signature <- vcapply(signature, class_register)
195195

196+
if (is.null(attr(method, "name", TRUE)))
197+
attr(method, "name") <- as.name(method_signature(generic, signature))
198+
196199
for (i in seq_along(chr_signature)) {
197200
class_name <- chr_signature[[i]]
198201
if (i != length(chr_signature)) {

src/init.c

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ SEXP sym_dot_setting_prop;
4545
SEXP sym_obj_dispatch;
4646
SEXP sym_dispatch_args;
4747
SEXP sym_methods;
48+
SEXP sym_S7_dispatch;
49+
SEXP sym_name;
4850

4951
SEXP fn_base_quote;
5052
SEXP fn_base_missing;
@@ -75,6 +77,8 @@ void R_init_S7(DllInfo *dll)
7577
sym_obj_dispatch = Rf_install("obj_dispatch");
7678
sym_dispatch_args = Rf_install("dispatch_args");
7779
sym_methods = Rf_install("methods");
80+
sym_S7_dispatch = Rf_install("S7_dispatch");
81+
sym_name = Rf_install("name");
7882

7983
fn_base_quote = Rf_eval(Rf_install("quote"), R_BaseEnv);
8084
fn_base_missing = Rf_eval(Rf_install("missing"), R_BaseEnv);

src/method-dispatch.c

+21-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ extern SEXP ns_S7;
88
extern SEXP sym_obj_dispatch;
99
extern SEXP sym_dispatch_args;
1010
extern SEXP sym_methods;
11+
extern SEXP sym_S7_dispatch;
12+
extern SEXP sym_name;
13+
1114
extern SEXP fn_base_quote;
1215
extern SEXP fn_base_missing;
1316

@@ -181,8 +184,8 @@ SEXP method_call_(SEXP call_, SEXP op_, SEXP args_, SEXP env_) {
181184
SEXP mcall_tail = mcall;
182185

183186
PROTECT_INDEX arg_pi, val_pi;
184-
PROTECT_WITH_INDEX(R_NilValue, &arg_pi);
185-
PROTECT_WITH_INDEX(R_NilValue, &val_pi);
187+
PROTECT_WITH_INDEX(R_NilValue, &arg_pi); // unnecessary, for rchk only
188+
PROTECT_WITH_INDEX(R_NilValue, &val_pi); // unnecessary, for rchk only
186189

187190
// For each of the arguments to the generic
188191
for (R_xlen_t i = 0; i < n_args; ++i) {
@@ -205,9 +208,9 @@ SEXP method_call_(SEXP call_, SEXP op_, SEXP args_, SEXP env_) {
205208
// Instead of Rf_eval(arg, R_EmptyEnv), we do Rf_eval(name, envir), so that
206209
// - if TYPEOF(arg) == LANGSXP or SYMSXP, arg doesn't need to be enquoted and
207210
// - if TYPEOF(arg) == PROMSXP, arg is updated in place.
208-
REPROTECT(arg, arg_pi); // not really necessary, but rchk flags spuriously
211+
REPROTECT(arg, arg_pi); // unnecessary, for rchk only
209212
SEXP val = Rf_eval(name, envir);
210-
REPROTECT(val, val_pi);
213+
REPROTECT(val, val_pi); // unnecessary, for rchk only
211214

212215
if (Rf_inherits(val, "S7_super")) {
213216

@@ -250,7 +253,20 @@ SEXP method_call_(SEXP call_, SEXP op_, SEXP args_, SEXP env_) {
250253

251254
// Now that we have all the classes, we can look up what method to call
252255
SEXP m = method_(generic, dispatch_classes, envir, R_TRUE);
253-
SETCAR(mcall, m);
256+
REPROTECT(m, val_pi); // unnecessary, for rchk only
257+
258+
/// Inlining the method closure in the call like `SETCAR(mcall, m);`
259+
/// leads to extremely verbose (unreadable) traceback()s. So,
260+
/// for nicer tracebacks, we set a SYMSXP at the head.
261+
SEXP method_name = Rf_getAttrib(m, sym_name);
262+
if (TYPEOF(method_name) != SYMSXP) {
263+
// if name is missing, fallback to masking the `S7_dispatch` symbol.
264+
// we could alternatively fallback to inlining m: SETCAR(mcall, m)
265+
method_name = sym_S7_dispatch;
266+
}
267+
268+
Rf_defineVar(method_name, m, envir);
269+
SETCAR(mcall, method_name);
254270

255271
SEXP out = Rf_eval(mcall, envir);
256272
UNPROTECT(4);

tests/testthat/_snaps/method-dispatch.md

+31
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,34 @@
5757
Error in `foo_wrapper()`:
5858
! argument "xx" is missing, with no default
5959

60+
# errors from dispatched methods have reasonable tracebacks
61+
62+
Code
63+
my_generic(10)
64+
Output
65+
[[1]]
66+
my_generic(10)
67+
68+
[[2]]
69+
S7::S7_dispatch()
70+
71+
[[3]]
72+
`method(my_generic, class_double)`(x = 10, ...)
73+
74+
75+
---
76+
77+
Code
78+
my_generic(3, 4)
79+
Output
80+
[[1]]
81+
my_generic(3, 4)
82+
83+
[[2]]
84+
S7::S7_dispatch()
85+
86+
[[3]]
87+
`method(my_generic, list(class_double, class_double))`(x = 3,
88+
y = 4, ...)
89+
90+

tests/testthat/test-method-dispatch.R

+18
Original file line numberDiff line numberDiff line change
@@ -225,5 +225,23 @@ test_that("method dispatch works for class_missing", {
225225
variant = if (getRversion() < "4.3") "R-lt-4-3",
226226
foo_wrapper()
227227
)
228+
})
229+
230+
test_that("errors from dispatched methods have reasonable tracebacks", {
231+
232+
get_call_stack <- function(n = 3) {
233+
x <- sys.calls()
234+
x <- x[-length(x)] # remove get_call_stack()
235+
x <- tail(x, n)
236+
lapply(x, utils::removeSource)
237+
}
238+
239+
my_generic <- new_generic("my_generic", "x")
240+
method(my_generic, class_numeric) <- function(x) get_call_stack()
241+
expect_snapshot(my_generic(10))
228242

243+
my_generic <- new_generic("my_generic", c("x", "y"))
244+
method(my_generic, list(class_numeric, class_numeric)) <-
245+
function(x, y) get_call_stack()
246+
expect_snapshot(my_generic(3, 4))
229247
})

0 commit comments

Comments
 (0)