Skip to content

high priorty S4 compatibility fixes#721

Merged
lawremi merged 9 commits into
mainfrom
s4-fixes
Jul 8, 2026
Merged

high priorty S4 compatibility fixes#721
lawremi merged 9 commits into
mainfrom
s4-fixes

Conversation

@lawremi

@lawremi lawremi commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

This PR addresses several high-priority compatibility issues found in #720:

  • Registers S4 methods in the caller/package environment when S7 methods are registered for S4-derived classes on base/internal generics, fixing cases like unlist().
  • Allows generated constructors for S4 parents to route parent slot arguments through methods::initialize(). Parent slots were being dropped during construction.
    • Side fix: new_object() correctly detects its evaluation frame even when lazily evaluated as part of another call.
  • Makes the initialize() method on S4-derived classes delegate to any non-default parent initialize() method, so as not to assume that the custom method has the same interface and semantics as the default method. When S4 is not extending S7, we could just not register our initialize() method when the default is overridden by a parent, but this seems like the simplest solution.
  • Rejects S7 properties with custom getters or setters when they are overriding an S4 slot, since the slot layer will violate any encapsulation.
  • Teaches class_extends() to correctly handle S4/S7 inheritance cases.

@lawremi lawremi requested a review from hadley July 7, 2026 16:28

@hadley hadley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a bunch of minor style suggestions; feel free to take as many or as few as you like.

Comment thread R/class-spec.R
Comment thread R/class.R
#' @export
new_object <- function(`_parent`, ...) {
class <- sys.function(-1)
class <- sys.function(sys.parent())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed/what it does?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case that exposed this problem is initialize(new_object()), which causes new_object() to be evaluated when forced by initialize(). As you know, this results in a confusing stack trace, where new_object() is nested within initialize(). That makes sys.function(-1) see initialize(), while sys.parent() returns the actual index of the frame in which new_object() is evaluated.

To quote the admittedly vague man page:

The parent frame of a function evaluation is the environment in
which the function was called.  It is not necessarily numbered one
less than the frame number of the current evaluation,

I guess this is partly why rlang was created :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, makes sense!

Comment thread R/class.R Outdated
overridden <- intersect(names(child_props), names(parent_props))
parent_S4 <- if (is_S4_class(parent)) parent else S4_ancestor(parent)
if (!is.null(parent_S4)) {
check_S4_slot_overrides(child_props, parent_S4, call = call)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that parent_S4 is only used by check_S4_slot_overrides(), maybe pass parent and then early return if it's an S4 class?

Comment thread R/constructor.R
} else {
quote(S7::S7_object())
}
new_object_call <-

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be worth refactoring this if statement to make it clear that there are now 3 cases?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except that the third case depends on the first two cases being resolved, since new_object_call is being wrapped.

Comment thread R/method-register-S3.R Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be clearer to rename this envir instead of creating a S4_envir?

Comment thread R/property.R

prop_is_dynamic <- function(prop) is.function(prop$getter)

prop_is_encapsulated <- function(prop) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


S4PropertyS7Parent := new_class(package = NULL)
S4_register(S4PropertyS7Parent)
setClass(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use local_S7_class() here and elsewhere?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make a separate PR addressing the S4 class clean up in tests.

Comment thread tests/testthat/test-S4.R
parent = getClass("S4regAccessorParent"),
package = NULL
)
expect_error(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to have at least one snapshot test of this error so you can see the whole thing in one place?

lawremi and others added 3 commits July 7, 2026 16:34
Co-authored-by: Hadley Wickham <h.wickham@gmail.com>
@lawremi lawremi merged commit a0a4b34 into main Jul 8, 2026
13 checks passed
@lawremi lawremi deleted the s4-fixes branch July 8, 2026 15:05
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.

2 participants