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
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
140 changes: 49 additions & 91 deletions include/swift/AST/Decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,45 @@ class PoundDiagnosticDecl : public Decl {

class OpaqueTypeDecl;

/// Describes the least favorable positions at which a requirement refers
/// to 'Self' in terms of variance, for use in the is-inheritable and
/// is-available-existential checks.
class SelfReferenceInfo final {
using OptionalTypePosition = OptionalEnum<decltype(TypePosition::Covariant)>;

public:
bool hasCovariantSelfResult;

OptionalTypePosition selfRef;
OptionalTypePosition assocTypeRef;

/// A reference to 'Self'.
static SelfReferenceInfo forSelfRef(TypePosition position) {
return SelfReferenceInfo(false, position, llvm::None);
}

/// A reference to 'Self' through an associated type.
static SelfReferenceInfo forAssocTypeRef(TypePosition position) {
return SelfReferenceInfo(false, llvm::None, position);
}

SelfReferenceInfo &operator|=(const SelfReferenceInfo &other);

explicit operator bool() const {
return hasCovariantSelfResult || selfRef || assocTypeRef;
}

SelfReferenceInfo()
: hasCovariantSelfResult(false), selfRef(llvm::None),
assocTypeRef(llvm::None) {}

private:
SelfReferenceInfo(bool hasCovariantSelfResult, OptionalTypePosition selfRef,
OptionalTypePosition assocTypeRef)
: hasCovariantSelfResult(hasCovariantSelfResult), selfRef(selfRef),
assocTypeRef(assocTypeRef) {}
};

/// ValueDecl - All named decls that are values in the language. These can
/// have a type, etc.
class ValueDecl : public Decl {
Expand Down Expand Up @@ -2634,6 +2673,16 @@ class ValueDecl : public Decl {
/// @_dynamicReplacement(for: ...), compute the original declaration
/// that this declaration dynamically replaces.
ValueDecl *getDynamicallyReplacedDecl() const;

/// Report 'Self' references within the type of this declaration as a
/// member of the given existential base type.
///
/// \param treatNonResultCovariantSelfAsInvariant If true, 'Self' or 'Self?'
/// is considered covariant only when it appears as the immediate type of a
/// property, or the uncurried result type of a method/subscript, e.g.
/// '() -> () -> Self'.
SelfReferenceInfo findExistentialSelfReferences(
Type baseTy, bool treatNonResultCovariantSelfAsInvariant) const;
};

/// This is a common base class for declarations which declare a type.
Expand Down Expand Up @@ -4212,82 +4261,6 @@ class ClassDecl final : public NominalTypeDecl {
bool isForeignReferenceType();
};

/// A convenience wrapper around the \c SelfReferencePosition::Kind enum.
struct SelfReferencePosition final {
enum Kind : uint8_t { None, Covariant, Contravariant, Invariant };

private:
Kind kind;

public:
SelfReferencePosition(Kind kind) : kind(kind) {}

SelfReferencePosition flipped() const {
switch (kind) {
case None:
case Invariant:
return *this;
case Covariant:
return Contravariant;
case Contravariant:
return Covariant;
}
llvm_unreachable("unhandled self reference position!");
}

explicit operator bool() const { return kind > None; }

operator Kind() const { return kind; }
};

/// Describes the least favorable positions at which a requirement refers
/// to 'Self' in terms of variance, for use in the is-inheritable and
/// is-available-existential checks.
struct SelfReferenceInfo final {
using Position = SelfReferencePosition;

bool hasCovariantSelfResult;
Position selfRef;
Position assocTypeRef;

/// A reference to 'Self'.
static SelfReferenceInfo forSelfRef(Position position) {
assert(position);
return SelfReferenceInfo(false, position, Position::None);
}

/// A reference to 'Self' through an associated type.
static SelfReferenceInfo forAssocTypeRef(Position position) {
assert(position);
return SelfReferenceInfo(false, Position::None, position);
}

SelfReferenceInfo operator|=(const SelfReferenceInfo &pos) {
hasCovariantSelfResult |= pos.hasCovariantSelfResult;
if (pos.selfRef > selfRef) {
selfRef = pos.selfRef;
}
if (pos.assocTypeRef > assocTypeRef) {
assocTypeRef = pos.assocTypeRef;
}
return *this;
}

explicit operator bool() const {
return hasCovariantSelfResult || selfRef || assocTypeRef;
}

SelfReferenceInfo()
: hasCovariantSelfResult(false), selfRef(Position::None),
assocTypeRef(Position::None) {}

private:
SelfReferenceInfo(bool hasCovariantSelfResult, Position selfRef,
Position assocTypeRef)
: hasCovariantSelfResult(hasCovariantSelfResult), selfRef(selfRef),
assocTypeRef(assocTypeRef) {}
};

/// The set of known protocols for which derived conformances are supported.
enum class KnownDerivableProtocolKind : uint8_t {
RawRepresentable,
Expand Down Expand Up @@ -4481,21 +4454,6 @@ class ProtocolDecl final : public NominalTypeDecl {
/// Does this protocol require a self-conformance witness table?
bool requiresSelfConformanceWitnessTable() const;

/// Find direct Self references within the given requirement.
///
/// \param treatNonResultCovariantSelfAsInvariant If true, 'Self' is only
/// assumed to be covariant in a top-level non-function type, or in the
/// eventual result type of a top-level function type.
SelfReferenceInfo
findProtocolSelfReferences(const ValueDecl *decl,
bool treatNonResultCovariantSelfAsInvariant) const;

/// Determine whether we are allowed to refer to an existential type
/// conforming to this protocol. This is only permitted if the type of
/// the member does not contain any associated types, and does not
/// contain 'Self' in 'parameter' or 'other' position.
bool isAvailableInExistential(const ValueDecl *decl) const;

/// Determine whether an existential type must be explicitly prefixed
/// with \c any. \c any is required if any of the members contain
/// an associated type, or if \c Self appears in non-covariant position.
Expand Down
9 changes: 9 additions & 0 deletions include/swift/AST/GenericSignature.h
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,8 @@ class alignas(1 << TypeAlignInBits) GenericSignatureImpl final

bool isCanonicalTypeInContext(Type type) const;

/// Determine whether the given type parameter is defined under this generic
/// signature.
bool isValidTypeInContext(Type type) const;

/// Retrieve the conformance access path used to extract the conformance of
Expand Down Expand Up @@ -428,6 +430,13 @@ class alignas(1 << TypeAlignInBits) GenericSignatureImpl final
/// generic parameter types by their sugared form.
Type getSugaredType(Type type) const;

/// Given a type parameter, compute the most specific supertype (upper bound)
/// that is not dependent on other type parameters.
///
/// \note If the upper bound is a protocol or protocol composition,
/// will return an instance of \c ExistentialType.
Type getNonDependentUpperBounds(Type type) const;

static void Profile(llvm::FoldingSetNodeID &ID,
TypeArrayView<GenericTypeParamType> genericParams,
ArrayRef<Requirement> requirements);
Expand Down
86 changes: 57 additions & 29 deletions include/swift/AST/Type.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,34 @@ enum class ForeignRepresentableKind : uint8_t {
StaticBridged,
};

/// An enum wrapper used to describe the variance position of a type within
/// another type. For example, a function type is covariant in its result type;
/// therefore, the result type is in covariant position relative to the function
/// type.
struct TypePosition final {
enum : uint8_t { Covariant, Contravariant, Invariant };

private:
decltype(Covariant) kind;

public:
TypePosition(decltype(kind) kind) : kind(kind) {}

TypePosition flipped() const {
switch (kind) {
case Invariant:
return *this;
case Covariant:
return Contravariant;
case Contravariant:
return Covariant;
}
llvm_unreachable("Unhandled type position!");
}

operator decltype(kind)() const { return kind; }
};

/// Type - This is a simple value object that contains a pointer to a type
/// class. This is potentially sugared. We use this throughout the codebase
/// instead of a raw "TypeBase*" to disable equality comparison, which is unsafe
Expand Down Expand Up @@ -241,47 +269,47 @@ class Type {
/// its children.
bool findIf(llvm::function_ref<bool(Type)> pred) const;

/// Transform the given type by applying the user-provided function to
/// each type.
/// Transform the given type by recursively applying the user-provided
/// function to each node.
///
/// This routine applies the given function to transform one type into
/// another. If the function leaves the type unchanged, recurse into the
/// child type nodes and transform those. If any child type node changes,
/// the parent type node will be rebuilt.
///
/// If at any time the function returns a null type, the null will be
/// propagated out.
///
/// \param fn A function object with the signature \c Type(Type), which
/// accepts a type and returns either a transformed type or a null type.
/// \param fn A function object with the signature \c Type(Type) , which
/// accepts a type and returns either a transformed type or a null type
/// (which will propagate out the null type).
///
/// \returns the result of transforming the type.
Type transform(llvm::function_ref<Type(Type)> fn) const;

/// Transform the given type by applying the user-provided function to
/// each type.
///
/// This routine applies the given function to transform one type into
/// another. If the function leaves the type unchanged, recurse into the
/// child type nodes and transform those. If any child type node changes,
/// the parent type node will be rebuilt.
///
/// If at any time the function returns a null type, the null will be
/// propagated out.
/// Transform the given type by recursively applying the user-provided
/// function to each node.
///
/// If the function returns \c None, the transform operation will
///
/// \param fn A function object with the signature
/// \c Optional<Type>(TypeBase *), which accepts a type pointer and returns a
/// transformed type, a null type (which will propagate the null type to the
/// outermost \c transform() call), or None (to indicate that the transform
/// operation should recursively transform the subtypes). The function object
/// should use \c dyn_cast rather \c getAs, because the transform itself
/// handles desugaring.
/// \param fn A function object which accepts a type pointer and returns a
/// transformed type, a null type (which will propagate out the null type),
/// or None (to indicate that the transform operation should recursively
/// transform the children). The function object should use \c dyn_cast rather
/// than \c getAs when the transform is intended to preserve sugar
///
/// \returns the result of transforming the type.
Type transformRec(llvm::function_ref<Optional<Type>(TypeBase *)> fn) const;

/// Transform the given type by recursively applying the user-provided
/// function to each node.
///
/// \param pos The variance position of the receiver.
///
/// \param fn A function object which accepts a type pointer along with its
/// variance position and returns either a transformed type, a null type
/// (which will propagate out the null type), or \c None (to indicate that the
/// transform operation should recursively transform the children).
/// The function object should use \c dyn_cast rather than \c getAs when the
/// transform is intended to preserve sugar.
///
/// \returns the result of transforming the type.
Type transformWithPosition(
TypePosition pos,
llvm::function_ref<Optional<Type>(TypeBase *, TypePosition)> fn) const;

/// Look through the given type and its children and apply fn to them.
void visit(llvm::function_ref<void (Type)> fn) const {
findIf([&fn](Type t) -> bool {
Expand Down
31 changes: 19 additions & 12 deletions include/swift/AST/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -513,13 +513,13 @@ class alignas(1 << TypeAlignInBits) TypeBase
TypeBase *getWithoutSyntaxSugar();

/// getASTContext - Return the ASTContext that this type belongs to.
ASTContext &getASTContext();
ASTContext &getASTContext() const;

/// isEqual - Return true if these two types are equal, ignoring sugar.
///
/// To compare sugar, check for pointer equality of the underlying TypeBase *
/// values, obtained by calling getPointer().
bool isEqual(Type Other);
bool isEqual(Type Other) const;

/// getDesugaredType - If this type is a sugared type, remove all levels of
/// sugar until we get down to a non-sugar type.
Expand Down Expand Up @@ -621,7 +621,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.


/// Determine whether the type involves an opaque type.
bool hasOpaqueArchetype() const {
Expand All @@ -637,13 +637,13 @@ class alignas(1 << TypeAlignInBits) TypeBase
/// Determine whether the type is an opened existential type with Error inside
bool isOpenedExistentialWithError();

/// Retrieve the set of opened existential archetypes that occur
/// within this type.
void getOpenedExistentials(SmallVectorImpl<OpenedArchetypeType *> &opened);
/// Retrieve the set of root opened archetypes that occur within this type.
void getRootOpenedExistentials(
SmallVectorImpl<OpenedArchetypeType *> &rootOpenedArchetypes) const;

/// Erase the given opened existential type by replacing it with its
/// existential type throughout the given type.
Type eraseOpenedExistential(OpenedArchetypeType *opened);
/// Replace opened archetypes with the given root with their most
/// specific non-dependent upper bounds throughout this type.
Type typeEraseOpenedArchetypesWithRoot(const OpenedArchetypeType *root) const;

/// Given a declaration context, returns a function type with the 'self'
/// type curried as the input if the declaration context describes a type.
Expand Down Expand Up @@ -5536,7 +5536,8 @@ class ArchetypeType : public SubstitutableType,
/// primary archetype.
ArchetypeType *getParent() const;

/// Return the root archetype parent of this archetype.
/// Return the archetype that represents the root generic parameter of its
/// interface type.
ArchetypeType *getRoot() const;

/// Determine whether this is a root archetype within the environment.
Expand Down Expand Up @@ -5773,6 +5774,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.


/// Return the archetype that represents the root generic parameter of its
/// interface type.
OpenedArchetypeType *getRoot() const {
return cast<OpenedArchetypeType>(ArchetypeType::getRoot());
}

static bool classof(const TypeBase *T) {
return T->getKind() == TypeKind::OpenedArchetype;
}
Expand Down Expand Up @@ -6287,7 +6294,7 @@ BEGIN_CAN_TYPE_WRAPPER(PackExpansionType, Type)
END_CAN_TYPE_WRAPPER(PackExpansionType, Type)

/// getASTContext - Return the ASTContext that this type belongs to.
inline ASTContext &TypeBase::getASTContext() {
inline ASTContext &TypeBase::getASTContext() const {
// If this type is canonical, it has the ASTContext in it.
if (isCanonical())
return *const_cast<ASTContext*>(Context);
Expand Down
Loading