Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve traceback() for dispatched methods #486

Merged
merged 9 commits into from
Nov 5, 2024
Merged
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
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# S7 (development version)

* The call context of a dispatched method (as visible in `sys.calls()` and
`traceback()`) no longer includes the inlined method and generic, resulting in
more compact and readable tracebacks. The dispatched method call now contains
only the method name, which serves as a hint for retrieving the method. For
example: `method(my_generic, class_double)`(x=10, ...). (#486)

* `new_class()` now automatically infers the package name when called from
within an R package (#459).

Expand Down
3 changes: 3 additions & 0 deletions R/generic.R
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ generic_add_method <- function(generic, signature, method) {
p_tbl <- generic@methods
chr_signature <- vcapply(signature, class_register)

if (is.null(attr(method, "name", TRUE)))
attr(method, "name") <- as.name(method_signature(generic, signature))

for (i in seq_along(chr_signature)) {
class_name <- chr_signature[[i]]
if (i != length(chr_signature)) {
Expand Down
4 changes: 4 additions & 0 deletions src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ SEXP sym_dot_setting_prop;
SEXP sym_obj_dispatch;
SEXP sym_dispatch_args;
SEXP sym_methods;
SEXP sym_S7_dispatch;
SEXP sym_name;

SEXP fn_base_quote;
SEXP fn_base_missing;
Expand Down Expand Up @@ -75,6 +77,8 @@ void R_init_S7(DllInfo *dll)
sym_obj_dispatch = Rf_install("obj_dispatch");
sym_dispatch_args = Rf_install("dispatch_args");
sym_methods = Rf_install("methods");
sym_S7_dispatch = Rf_install("S7_dispatch");
sym_name = Rf_install("name");

fn_base_quote = Rf_eval(Rf_install("quote"), R_BaseEnv);
fn_base_missing = Rf_eval(Rf_install("missing"), R_BaseEnv);
Expand Down
26 changes: 21 additions & 5 deletions src/method-dispatch.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
extern SEXP sym_obj_dispatch;
extern SEXP sym_dispatch_args;
extern SEXP sym_methods;
extern SEXP sym_S7_dispatch;
extern SEXP sym_name;

extern SEXP fn_base_quote;
extern SEXP fn_base_missing;

Expand Down Expand Up @@ -181,8 +184,8 @@
SEXP mcall_tail = mcall;

PROTECT_INDEX arg_pi, val_pi;
PROTECT_WITH_INDEX(R_NilValue, &arg_pi);
PROTECT_WITH_INDEX(R_NilValue, &val_pi);
PROTECT_WITH_INDEX(R_NilValue, &arg_pi); // unnecessary, for rchk only
PROTECT_WITH_INDEX(R_NilValue, &val_pi); // unnecessary, for rchk only

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

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

Expand Down Expand Up @@ -250,7 +253,20 @@

// Now that we have all the classes, we can look up what method to call
SEXP m = method_(generic, dispatch_classes, envir, R_TRUE);
SETCAR(mcall, m);
REPROTECT(m, val_pi); // unnecessary, for rchk only

/// Inlining the method closure in the call like `SETCAR(mcall, m);`
/// leads to extremely verbose (unreadable) traceback()s. So,
/// for nicer tracebacks, we set a SYMSXP at the head.
SEXP method_name = Rf_getAttrib(m, sym_name);
if (TYPEOF(method_name) != SYMSXP) {
// if name is missing, fallback to masking the `S7_dispatch` symbol.
// we could alternatively fallback to inlining m: SETCAR(mcall, m)
method_name = sym_S7_dispatch;

Check warning on line 265 in src/method-dispatch.c

View check run for this annotation

Codecov / codecov/patch

src/method-dispatch.c#L265

Added line #L265 was not covered by tests
}

Rf_defineVar(method_name, m, envir);
SETCAR(mcall, method_name);

SEXP out = Rf_eval(mcall, envir);
UNPROTECT(4);
Expand Down
31 changes: 31 additions & 0 deletions tests/testthat/_snaps/method-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,34 @@
Error in `foo_wrapper()`:
! argument "xx" is missing, with no default

# errors from dispatched methods have reasonable tracebacks

Code
my_generic(10)
Output
[[1]]
my_generic(10)

[[2]]
S7::S7_dispatch()

[[3]]
`method(my_generic, class_double)`(x = 10, ...)


---

Code
my_generic(3, 4)
Output
[[1]]
my_generic(3, 4)

[[2]]
S7::S7_dispatch()

[[3]]
`method(my_generic, list(class_double, class_double))`(x = 3,
y = 4, ...)


18 changes: 18 additions & 0 deletions tests/testthat/test-method-dispatch.R
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,23 @@ test_that("method dispatch works for class_missing", {
variant = if (getRversion() < "4.3") "R-lt-4-3",
foo_wrapper()
)
})

test_that("errors from dispatched methods have reasonable tracebacks", {

get_call_stack <- function(n = 3) {
x <- sys.calls()
x <- x[-length(x)] # remove get_call_stack()
x <- tail(x, n)
lapply(x, utils::removeSource)
}

my_generic <- new_generic("my_generic", "x")
t-kalinowski marked this conversation as resolved.
Show resolved Hide resolved
method(my_generic, class_numeric) <- function(x) get_call_stack()
expect_snapshot(my_generic(10))

my_generic <- new_generic("my_generic", c("x", "y"))
method(my_generic, list(class_numeric, class_numeric)) <-
function(x, y) get_call_stack()
expect_snapshot(my_generic(3, 4))
})
Loading