Skip to content

SE-0309: Covariant erasure for dependent member types #39492

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

Merged
merged 13 commits into from
Feb 2, 2022

Conversation

AnthonyLatsis
Copy link
Collaborator

@AnthonyLatsis AnthonyLatsis commented Sep 28, 2021

This change enables us to access existential members when their type signatures, as seen in the context of the base type, reference Self-rooted associated types only in covariant positions, by type-erasing these references to their upper bounds. This implies that you can use a protocol value to call a method that returns an associated type, or a method that accepts one if the associated type is bound to a fully concrete type via generic requirements on the protocol. Accessing these members becomes possible because their types are adjusted so as to become well-defined from outside their protocol context, which is key to type-safe member accesses on a type-erased value.

protocol P {
  associatedtype A: P
  func method1() -> A
  func method2(_: A) 
}
protocol PBool: P where A == Bool {}

func test(p: P, pBool: PBool) {
  let _: P = p.method1() // ok, p.method1 has type () -> P
  
  pBool.method2(true) // ok, pBool.method2 has type (Bool) -> Void
}

lib/AST/Type.cpp Outdated
continue;
}
inputInfo |= ::findExistentialSelfReferences(
sig, param.getParameterType(), TypePosition::Contravariant);
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't quite right. In ((T) -> U) -> V, the input (T) -> U is contravariant, so (T) is covariant and U is contravariant in those positions. In general, recurring under the input side of an arrow flips variance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This function is the entry point for a member ValueDecl. We do flip variance in the recursive findExistentialSelfReferences that takes a type and a relative variance position.

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

Comment on lines 5201 to +5341
auto gather = ExpansionGatherer{fn};
Type transformedPat = expand->getPatternType().transformRec(gather);
Type transformedPat =
expand->getPatternType().transformWithPosition(pos, gather);
Copy link
Collaborator Author

@AnthonyLatsis AnthonyLatsis Dec 21, 2021

Choose a reason for hiding this comment

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

@CodaFi I am intuitively treating packs as invariant (because they seem like they cannot appear within another type), and expansions as covariant here; let me know if anything sounds fishy about that.

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test macOS

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please clean smoke test macOS

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please test source compatibility

@AnthonyLatsis AnthonyLatsis force-pushed the se-309-2 branch 3 times, most recently from 9f24e75 to 891689f Compare January 21, 2022 08:07
@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please clean smoke test Linux

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please test source compatibility

@AnthonyLatsis
Copy link
Collaborator Author

@slavapestov This is finally ready for another review.

@DougGregor
Copy link
Member

@swift-ci please smoke test

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

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

I haven't reviewed earlier drafts of this pull request, but in it's current state it's looking fantastic. I have just a few review comments, but nothing that would prevent us from merging this.

@@ -616,7 +616,7 @@ class alignas(1 << TypeAlignInBits) TypeBase

/// Determine whether the type involves the given opened existential
/// archetype.
bool hasOpenedExistential(OpenedArchetypeType *opened);
bool hasOpenedExistentialWithRoot(const OpenedArchetypeType *root) const;
Copy link
Member

Choose a reason for hiding this comment

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

I was looking at this recently, and wondering whether the GenericEnvironment itself is a better discriminator than the root archetype for the long term. Right now, any opened existential always has a single generic parameter at the top level, but it's not inconceivable that we would want to allow matching existentials to structural types with type parameters, e.g., x as <T, U>(T, U) to open something as a 2-tuple. In such cases, the root no longer uniquely identifies the opened existential. We don't have to make this change to use GenericEnvironment---your change here makes sure clients are properly reasoning about the roots so they handle nested archetypes properly, which is a huge step forward.

return mapTypeIntoContext(type, conformanceLookupFn);

if (type->hasTypeParameter()) {
return mapTypeIntoContext(type, conformanceLookupFn);
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why we need this check for type parameters here. Are we tripping the assertion in mapTypeIntoContext in some way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, we trip it when we encounter an archetype from somewhere else. Consider the following:

class Class<A>: P {}
protocol P {
  associatedtype A
}
protocol Q: P {
  func method(_: A)
}
extension Q {
  func test(arg: Q & Class<Self>) {
    arg.method(self) // OK
  }
}

The opened archetype signature for Q & Class<Self> is <Self where Self : Q, Self : Class<archetype>>.

Copy link
Contributor

Choose a reason for hiding this comment

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

We were discussing this internally and we concluded the best approach is to model the opened existential's environment as being derived from the outer generic signature. So in your example, it becomes

<A, Self where Self : Q, Self : Class<A>>

However this is a big refactoring and outside of the scope of this PR.

// building a root OpenedArchetypeType, and the latter when building
// nested archetypes.
// For compatibility, continue using the existential layout's version when
// the interface type is a generic parameter. We should align these at
Copy link
Member

Choose a reason for hiding this comment

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

Good choice; we might as well get the better answer for nested types.

@@ -1727,6 +1727,103 @@ static bool isMainDispatchQueueMember(ConstraintLocator *locator) {
return true;
}

/// Type-erase occurences of covariant 'Self'-rooted type parameters to their
Copy link
Member

Choose a reason for hiding this comment

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

Typo "occurences"

return None;
});

// Then, type-erase occurences of covariant 'Self'-rooted type parameters.
Copy link
Member

Choose a reason for hiding this comment

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

Typo occurences. This three-step transform makes a ton of sense. getTypeOfMemberReference is getting huge, so at some point I'd like us to factor the transform out into a separate function.

@@ -5686,6 +5687,12 @@ class OpenedArchetypeType final : public ArchetypeType,
/// Retrieve the opened existential type
Type getOpenedExistentialType() const;
Copy link
Member

Choose a reason for hiding this comment

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

This function is a bit of a footgun, because even on a nested opened archetype it still returns the top-level existential type, rather than the erased type for this particular archetype. We probably want to do a quick audit of clients to ensure that they are only using this on the root archetype, and consider renaming the API (getOpenedExistentialTypeAtRoot or similar) or removing it entirely (clients can ask for the opened existential type on the generic environment, which is obviously shared amongst all archetypes in that opened existential).

Copy link
Collaborator Author

@AnthonyLatsis AnthonyLatsis Feb 1, 2022

Choose a reason for hiding this comment

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

Noticed this too; I will explore your suggestions in a follow-up.

@DougGregor
Copy link
Member

Ah, looks like some minor bit-rot due to the change in the signature of transformRec. @AnthonyLatsis would you like to fix things up to get this merged, or would you like some help?

@AnthonyLatsis
Copy link
Collaborator Author

AnthonyLatsis commented Feb 1, 2022

@DougGregor already at it!

While we're here, also tidy up the documentation for the type transform APIs
@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

@AnthonyLatsis
Copy link
Collaborator Author

@swift-ci please smoke test

@AnthonyLatsis
Copy link
Collaborator Author

AnthonyLatsis commented Feb 2, 2022

Thanks to all for reviewing!

@AnthonyLatsis AnthonyLatsis merged commit c7fd60f into swiftlang:main Feb 2, 2022
@AnthonyLatsis AnthonyLatsis deleted the se-309-2 branch February 2, 2022 04:07
@AnthonyLatsis AnthonyLatsis changed the title SE-309: Covariant erasure for dependent member types SE-0309: Covariant erasure for dependent member types Apr 29, 2022
@AnthonyLatsis AnthonyLatsis added the swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process label Apr 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants