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

Conversation

t-kalinowski
Copy link
Member

@t-kalinowski t-kalinowski commented Nov 4, 2024

Currently, printing traceback() on an error signaled from a generic dispatched method is extremely verbose, bordering on unreadable. This is because the generic method closure is inlined into the constructed call, which is then deparsed in the traceback.

With this PR, we now construct the method call with a symbol at the head. The name is intentionally non-syntatic to avoid potential clashes with the generic's arguments.

Example evaluating the following at the console:

library(S7)
my_generic <- new_generic("my_generic", "x")
method(my_generic, class_numeric) <- function(x) stop("hi")
my_generic(10)
traceback()

After this PR, it prints:

> library(S7)
my_generic <- new_generic("my_generic", "x")
method(my_generic, class_numeric) <- function(x) stop("hi")
my_generic(10)
Error in `my_generic@methods$double`(x = 10, ...) : hi
> traceback()
4: stop("hi") at #1
3: `my_generic@methods$double`(x = 10, ...) at method-dispatch.R#23
2: S7::S7_dispatch()
1: my_generic(10)

Presently prints:

> library(S7)
my_generic <- new_generic("my_generic", "x")
method(my_generic, class_numeric) <- function(x) stop("hi")
my_generic(10)
Error in (function (x)  : hi

> traceback()
4: stop("hi") at #1
3: (structure(function (x) 
   stop("hi"), class = c("S7_method", "function", "S7_object"), S7_class = structure(function (.data = function() NULL, 
       generic = (function (.data = function() NULL, name = character(0), 
           methods = new.env(parent = emptyenv()), dispatch_args = character(0)) 
       new_object(fun(.data = .data), name = name, methods = methods, 
           dispatch_args = dispatch_args))(), signature = list()) 
   new_object(fun(.data = .data), generic = generic, signature = signature), name = "S7_method", parent = structure(list(
       class = "function", constructor_name = "fun", constructor = function (.data = function() NULL) 
       .data, validator = function (object) 
       {
           if (base_class(object) != name) {
               sprintf("Underlying data must be <%s> not <%s>", 
                   name, base_class(object))
           }
       }), class = "S7_base_class"), properties = list(generic = structure(list(
       name = "generic", class = structure(function (.data = function() NULL, 
           name = character(0), methods = new.env(parent = emptyenv()), 
           dispatch_args = character(0)) 
       new_object(fun(.data = .data), name = name, methods = methods, 
           dispatch_args = dispatch_args), name = "S7_generic", parent = structure(list(
           class = "function", constructor_name = "fun", constructor = function (.data = function() NULL) 
           .data, validator = function (object) 
           {
               if (base_class(object) != name) {
                   sprintf("Underlying data must be <%s> not <%s>", 
                     name, base_class(object))
               }
           }), class = "S7_base_class"), properties = list(name = structure(list(
           name = "name", class = structure(list(class = "character", 
               constructor_name = "character", constructor = function (.data = character(0)) 
               .data, validator = function (object) 
               {
                   if (base_class(object) != name) {
                     sprintf("Underlying data must be <%s> not <%s>", 
                       name, base_class(object))
                   }
               }), class = "S7_base_class"), getter = NULL, setter = NULL, 
           validator = NULL, default = NULL), class = "S7_property"), 
           methods = structure(list(name = "methods", class = structure(list(
               class = "environment", constructor_name = "environment", 
               constructor = function (.data = new.env(parent = emptyenv())) 
               .data, validator = function (object) 
               {
                   if (base_class(object) != name) {
                     sprintf("Underlying data must be <%s> not <%s>", 
                       name, base_class(object))
                   }
               }), class = "S7_base_class"), getter = NULL, setter = NULL, 
               validator = NULL, default = NULL), class = "S7_property"), 
           dispatch_args = structure(list(name = "dispatch_args", 
               class = structure(list(class = "character", constructor_name = "character", 
                   constructor = function (.data = character(0)) 
                   .data, validator = function (object) 
                   {
                     if (base_class(object) != name) {
                       sprintf("Underlying data must be <%s> not <%s>", 
                         name, base_class(object))
                     }
                   }), class = "S7_base_class"), getter = NULL, 
               setter = NULL, validator = NULL, default = NULL), class = "S7_property")), abstract = FALSE, constructor = function (.data = function() NULL, 
           name = character(0), methods = new.env(parent = emptyenv()), 
           dispatch_args = character(0)) 
       new_object(fun(.data = .data), name = name, methods = methods, 
           dispatch_args = dispatch_args), class = c("S7_class", 
       "S7_object")), getter = NULL, setter = NULL, validator = NULL, 
       default = NULL), class = "S7_property"), signature = structure(list(
       name = "signature", class = structure(list(class = "list", 
           constructor_name = "list", constructor = function (.data = list()) 
           .data, validator = function (object) 
           {
               if (base_class(object) != name) {
                   sprintf("Underlying data must be <%s> not <%s>", 
                     name, base_class(object))
               }
           }), class = "S7_base_class"), getter = NULL, setter = NULL, 
       validator = NULL, default = NULL), class = "S7_property")), abstract = FALSE, constructor = function (.data = function() NULL, 
       generic = (function (.data = function() NULL, name = character(0), 
           methods = new.env(parent = emptyenv()), dispatch_args = character(0)) 
       new_object(fun(.data = .data), name = name, methods = methods, 
           dispatch_args = dispatch_args))(), signature = list()) 
   new_object(fun(.data = .data), generic = generic, signature = signature), class = c("S7_class", 
   "S7_object")), generic = structure(function (x, ...) 
   S7::S7_dispatch(), class = c("S7_generic", "function", "S7_object"
   ), S7_class = structure(function (.data = function() NULL, name = character(0), 
       methods = new.env(parent = emptyenv()), dispatch_args = character(0)) 
   new_object(fun(.data = .data), name = name, methods = methods, 
       dispatch_args = dispatch_args), name = "S7_generic", parent = structure(list(
       class = "function", constructor_name = "fun", constructor = function (.data = function() NULL) 
       .data, validator = function (object) 
       {
           if (base_class(object) != name) {
               sprintf("Underlying data must be <%s> not <%s>", 
                   name, base_class(object))
           }
       }), class = "S7_base_class"), properties = list(name = structure(list(
       name = "name", class = structure(list(class = "character", 
           constructor_name = "character", constructor = function (.data = character(0)) 
           .data, validator = function (object) 
           {
               if (base_class(object) != name) {
                   sprintf("Underlying data must be <%s> not <%s>", 
                     name, base_class(object))
               }
           }), class = "S7_base_class"), getter = NULL, setter = NULL, 
       validator = NULL, default = NULL), class = "S7_property"), 
       methods = structure(list(name = "methods", class = structure(list(
           class = "environment", constructor_name = "environment", 
           constructor = function (.data = new.env(parent = emptyenv())) 
           .data, validator = function (object) 
           {
               if (base_class(object) != name) {
                   sprintf("Underlying data must be <%s> not <%s>", 
                     name, base_class(object))
               }
           }), class = "S7_base_class"), getter = NULL, setter = NULL, 
           validator = NULL, default = NULL), class = "S7_property"), 
       dispatch_args = structure(list(name = "dispatch_args", class = structure(list(
           class = "character", constructor_name = "character", 
           constructor = function (.data = character(0)) 
           .data, validator = function (object) 
           {
               if (base_class(object) != name) {
                   sprintf("Underlying data must be <%s> not <%s>", 
                     name, base_class(object))
               }
           }), class = "S7_base_class"), getter = NULL, setter = NULL, 
           validator = NULL, default = NULL), class = "S7_property")), abstract = FALSE, constructor = function (.data = function() NULL, 
       name = character(0), methods = new.env(parent = emptyenv()), 
       dispatch_args = character(0)) 
   new_object(fun(.data = .data), name = name, methods = methods, 
       dispatch_args = dispatch_args), class = c("S7_class", "S7_object"
   )), name = "my_generic", methods = <environment>, dispatch_args = "x"), signature = list(
       structure(list(class = "double", constructor_name = "double", 
           constructor = function (.data = numeric(0)) 
           .data, validator = function (object) 
           {
               if (base_class(object) != name) {
                   sprintf("Underlying data must be <%s> not <%s>", 
                     name, base_class(object))
               }
           }), class = "S7_base_class"))))(x = 10, ...) at method-dispatch.R#23
2: S7::S7_dispatch()
1: my_generic(10)

@lawremi
Copy link
Collaborator

lawremi commented Nov 4, 2024

Nice improvement. The verbose traces were bothering me, so I was setting the option to truncate them. This is much more readable though.

R/generic.R Outdated Show resolved Hide resolved
tests/testthat/test-method-dispatch.R Show resolved Hide resolved
@hadley
Copy link
Member

hadley commented Nov 4, 2024

I think this fixes #132, at least to the level we want to worry about for now.

...

Actually I think #483 did most of the rest. The call stack is now pretty minimal:

    ▆
 1. └─global my_generic(10)
 2.   ├─S7::S7_dispatch()
 3.   └─global `my_generic@methods$double`(x = `<dbl>`, ...)

cf. CRAN S7:

    ▆
 1. └─global my_generic(10)
 2.   ├─S7::S7_dispatch()
 3.   │ └─base::eval(S7_dispatched_call, envir = sys.frame(-1))
 4.   │   └─base::eval(S7_dispatched_call, envir = sys.frame(-1))
 5.   └─global `<S7_methd>`(`<dbl>`, ... = ...)

R/generic.R Outdated Show resolved Hide resolved
@t-kalinowski
Copy link
Member Author

t-kalinowski commented Nov 4, 2024

The snapshot test is failing, but I can't reproduce the failure locally. On CI, the call stack includes the .External2(method_call_, ...) call, but when I run it locally, it does not. I could work around this by adding a variant or capturing less in the snapshot, but I’m curious—does anyone know why .External2 only appears sometimes?

It was because the call had a srcref sometimes. The srcref printed as .External2(...), even though the actual call constructed and passed to Rf_eval() from .External2() was different.

@hadley
Copy link
Member

hadley commented Nov 4, 2024

This is worth of a news bullet IMO.

@t-kalinowski t-kalinowski merged commit bcedb64 into main Nov 5, 2024
13 checks passed
@t-kalinowski t-kalinowski deleted the better-tracebacks-for-dispatched-methods branch November 5, 2024 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants