Skip to content

Add doesCastPreserveOwnershipForType() to SIL/Utils/DynamicCasts.cpp #41952

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 3 commits into from
Apr 6, 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
4 changes: 4 additions & 0 deletions include/swift/AST/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,10 @@ class alignas(1 << TypeAlignInBits) TypeBase
/// Whether this is the AnyObject type.
bool isAnyObject();

/// Return true if this type is potentially an AnyObject existential after
/// substitution.
bool isPotentiallyAnyObject();

/// Whether this is an existential composition containing
/// Error.
bool isExistentialWithError();
Expand Down
22 changes: 17 additions & 5 deletions include/swift/SIL/DynamicCasts.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class SILType;
enum class CastConsumptionKind : uint8_t;
struct SILDynamicCastInst;

/// Returns true if the ownership of all references in this type are preserved
/// (without unbalanced retains or releases) during dynamic casting.
bool doesCastPreserveOwnershipForTypes(SILModule &module, CanType sourceType,
CanType targetType);

enum class DynamicCastFeasibility {
/// The cast will always succeed.
WillSucceed,
Expand Down Expand Up @@ -82,10 +87,17 @@ bool emitSuccessfulIndirectUnconditionalCast(
bool emitSuccessfulIndirectUnconditionalCast(SILBuilder &B, SILLocation loc,
SILDynamicCastInst dynamicCast);

/// Can the given cast be performed by the scalar checked-cast instructions in
/// the current SIL stage, or do we need to use the indirect instructions?
bool canSILUseScalarCheckedCastInstructions(SILModule &M,
CanType sourceType,
CanType targetType);

/// Can the given cast be performed by the scalar checked-cast
/// instructions, or does we need to use the indirect instructions?
bool canUseScalarCheckedCastInstructions(SILModule &M,
CanType sourceType,CanType targetType);
/// instructions at IRGen, or do we need to use the indirect instructions?
bool canIRGenUseScalarCheckedCastInstructions(SILModule &M,
CanType sourceType,
CanType targetType);

/// Carry out the operations required for an indirect conditional cast
/// using a scalar cast operation.
Expand Down Expand Up @@ -435,8 +447,8 @@ struct SILDynamicCastInst {
llvm_unreachable("covered switch");
}

bool canUseScalarCheckedCastInstructions() const {
return swift::canUseScalarCheckedCastInstructions(
bool canSILUseScalarCheckedCastInstructions() const {
return swift::canSILUseScalarCheckedCastInstructions(
getModule(), getSourceFormalType(), getTargetFormalType());
}
};
Expand Down
4 changes: 2 additions & 2 deletions include/swift/SIL/TypeSubstCloner.h
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ class TypeSubstCloner : public SILClonerWithScopes<ImplClass> {
auto FalseCount = inst->getFalseBBCount();

// Try to use the scalar cast instruction.
if (canUseScalarCheckedCastInstructions(B.getModule(),
sourceType, targetType)) {
if (canSILUseScalarCheckedCastInstructions(B.getModule(),
sourceType, targetType)) {
emitIndirectConditionalCastWithScalar(
B, SwiftMod, loc, inst->getConsumptionKind(), src, sourceType, dest,
targetType, succBB, failBB, TrueCount, FalseCount);
Expand Down
14 changes: 14 additions & 0 deletions lib/AST/Type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,20 @@ bool TypeBase::isAnyObject() {
return canTy.getExistentialLayout().isAnyObject();
}

// Distinguish between class-bound types that might be AnyObject vs other
// class-bound types. Only types that are potentially AnyObject might have a
// transparent runtime type wrapper like __SwiftValue. This must look through
// all optional types because dynamic casting sees through them.
bool TypeBase::isPotentiallyAnyObject() {
Type unwrappedTy = lookThroughAllOptionalTypes();
if (auto archetype = unwrappedTy->getAs<ArchetypeType>()) {
// Does archetype have any requirements that contradict AnyObject?
// 'T : AnyObject' requires a layout constraint, not a conformance.
return archetype->getConformsTo().empty() && !archetype->getSuperclass();
}
return unwrappedTy->isAnyObject();
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want this to be true for Any as well as AnyObject?

Copy link
Contributor

Choose a reason for hiding this comment

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

AnyObject is very different from Any as regards casting behavior, so I suspect Andy wants this to only be true for AnyObject.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@slavapestov Good question. If we cared about Any, then the API would need to be called isAnyOrAnyObject(). I don't think it's possible for an Any to be AnyObject. That would imply that an existential contains another existential. I noticed the type system code does an Any-or-AnyObject checks in some places, but this API is specifically for potentially AnyObject.

The client of this API cares about cases like this. What's important to the client is that only AnyObject can hold a __SwiftValue. Any is never a __SwiftValue:

class C: Hashable, Equatable, P {
  static func == (lhs: C, rhs: C) -> Bool {
    return true
  }

  func hash(into hasher: inout Hasher) {}
}

let val: AnyHashable = C()
let anyObject = val as AnyObject // metatype == "class __SwiftValue"
let any = val as Any // metatype == "struct AnyHashable"

}

bool ExistentialLayout::isErrorExistential() const {
auto protocols = getProtocols();
return (!hasExplicitAnyObject &&
Expand Down
163 changes: 126 additions & 37 deletions lib/SIL/Utils/DynamicCasts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,103 @@ static CanType getHashableExistentialType(ModuleDecl *M) {
return hashable->getDeclaredInterfaceType()->getCanonicalType();
}

static bool canBeExistential(CanType ty) {
// If ty is an archetype, conservatively assume it's an existential.
return ty.isAnyExistentialType() || ty->is<ArchetypeType>();
}
// Returns true if casting \p sourceFormalType to \p targetFormalType preserves
// ownership.
//
// Casting preserves ownership when all references from the source value are
// forwarded into the result value (without unbalanced retains or releases).
//
// When both the source and target types of checked-cast preserve ownership,
// then the cast is compatible with guaranteed ownership. A guaranteed
// compatible cast cannot release any references within its operand's value
// and cannot retain any references owned by its result.
//
// A type's ownership might not be preserved by a dynamic cast if it is either
// (A) a potentially bridged value
// or
// (B) potentially wrapped in a transparent type, which is equivalent to
// isPotentiallyAnyObject()
//
// Given:
// let source: sourceType
// let dest = source as! targetType
//
// Ownership conversion happens when
//
// (A) one type is a bridged value and the other is an object:
//
// (A1) Boxing: <trivial> as! Object instantiates references in Object
// Presumably, Object's type must be class-bound, but this is not
// currently checked.
//
// (A2) Unboxing: Object as! <trivial> destroys references in Object
// Object may be any type that can hold an object, including
// non-class-bound archetypes and existentials.
//
// (B) one type is transparently wrapped in __SwiftValue, while the other is
// unwrapped. Given:
//
// class C : Hashable {}
// let a = AnyHashable(C())
//
// (B1) When the substituted source type is AnyHashable and the
// substituted destination type is AnyObject, the cast
// instantiates an owned __SwiftValue:
//
// // instantiates __SwiftValue
// let b = a as! AnyObject
// or
// let b = a as! T where T.self == AnyObject.self
//
// (B2) When the substituted source type is Any or AnyObject, and the
// substituted destination type is not Any or AnyObject, the cast
// releases the owned __SwiftValue:
//
// let c = b as! C // releases __SwiftValue
//
// After unwrapping Optional, the type may fall into one one of
// the following categories that are relevant for cast ownership:
//
// Class-bound types (hasReferenceSemantics() && !isPotentiallyAnyObject())
// - includes classes, class-bound existentials other than AnyObject,
// class-bound archetypes with a superclass or protocol constraint,
// objc types, blocks, Builtin.NativeObject, etc.
// - excludes any type that are potentially AnyObject after substitution
// - the value is a single reference
// - the single reference is "known unwrapped". It never transparently wraps the
// underlying dynamically typed value in another type, such as __SwiftValue
// - casting directly forwards the reference
//
// Potentially bridged values:
// - includes struct, enum, non-class archetype, non-class existential,
// and non-objc-metatype
// - these types are potentially trivial after subsitution. If so, then they
// convert to a reference when casting to AnyObject or certain classes
//
// Any and AnyObject existentials:
// - although called existentials, their type is a protocol composition
// - these do not include existentials with constraints
// - these are very special types, unlike normal existentials...
// - the immediately erased value may itself be an existential
// (an AnyObject existential can be wrapped within an Any existential!)
// - the underlying dynamically typed value may be transparently wrapped in
// __SwiftValue
//
// These type categories are disjoint, except that a non-class archetype is both
// potentially bridged and potentially Any or AnyObject after substitution.
//
// TODO: In the future, when the runtime stops wrapping nontrivial types inside
// __SwiftValue, cases (B1) and (B2) above will no longer apply. At that time,
// expand ownership preserving cast types to AnyObject. Then remove the
// isPotentiallyAnyObject() check.
bool swift::doesCastPreserveOwnershipForTypes(SILModule &module,
CanType sourceType,
CanType targetType) {
if (!canIRGenUseScalarCheckedCastInstructions(module, sourceType, targetType))
return false;

static bool canBeClass(CanType ty) {
// If ty is an archetype, conservatively assume it's an existential.
return ty.getClassOrBoundGenericClass() || ty->is<ArchetypeType>();
return !sourceType->isPotentiallyAnyObject()
&& !targetType->isPotentiallyAnyObject();
}

bool SILDynamicCastInst::isRCIdentityPreserving() const {
Expand All @@ -247,26 +336,12 @@ bool SILDynamicCastInst::isRCIdentityPreserving() const {
// would get confused and might eliminate a retain of such an object
// completely.
SILFunction &f = *getFunction();
if (getSourceLoweredType().isTrivial(f) != getTargetLoweredType().isTrivial(f))
return false;

CanType source = getSourceFormalType();
CanType target = getTargetFormalType();

// An existential may be holding a reference to a bridgeable struct.
// In this case, ARC on the existential affects the refcount of the container
// holding the struct, not the class to which the struct is bridged.
// Therefore, don't assume RC identity when casting between existentials and
// classes (and also between two existentials).
if (canBeExistential(source) &&
(canBeClass(target) || canBeExistential(target)))
return false;

// And vice versa.
if (canBeClass(source) && canBeExistential(target))
return false;

return true;
if (getSourceLoweredType().isTrivial(f)
&& getTargetLoweredType().isTrivial(f)) {
return true;
}
return doesCastPreserveOwnershipForTypes(f.getModule(), getSourceFormalType(),
getTargetFormalType());
}

/// Check if a given type conforms to _BridgedToObjectiveC protocol.
Expand Down Expand Up @@ -1204,15 +1279,28 @@ bool swift::emitSuccessfulIndirectUnconditionalCast(
}

/// Can the given cast be performed by the scalar checked-cast
/// instructions?
/// instructions at the current SIL stage?
///
/// TODO: in OSSA-with-opaque-values SIL, all casts could be modeled using
/// scalar casts by setting 'OwnershipForwardingMixin::directlyForwards =
/// false'. This would simplify SIL analysis. Temporaries would be emitted
/// during address lowering.
bool swift::canUseScalarCheckedCastInstructions(SILModule &M,
CanType sourceFormalType,
CanType targetFormalType) {
/// Always returns true for !useLoweredAddresses. Scalar casts are always
/// valid for owned values. If the operand is +1, the case will always destroy
/// or forward it. The result is always either +1 or trivial. The cast never
/// hides a copy. doesCastPreserveOwnershipForTypes determines whether the
/// scalar cast is also compatible with guaranteed values.
bool swift::canSILUseScalarCheckedCastInstructions(SILModule &M,
CanType sourceFormalType,
CanType targetFormalType) {
if (!M.useLoweredAddresses())
return true;

return canIRGenUseScalarCheckedCastInstructions(M, sourceFormalType,
targetFormalType);
}

/// Can the given cast be performed by the scalar checked-cast
/// instructions?
bool swift::canIRGenUseScalarCheckedCastInstructions(SILModule &M,
CanType sourceFormalType,
CanType targetFormalType) {
// Look through one level of optionality on the source.
auto objectType = sourceFormalType;
if (auto type = objectType.getOptionalObjectType())
Expand Down Expand Up @@ -1278,8 +1366,9 @@ void swift::emitIndirectConditionalCastWithScalar(
SILValue destAddr, CanType targetFormalType,
SILBasicBlock *indirectSuccBB, SILBasicBlock *indirectFailBB,
ProfileCounter TrueCount, ProfileCounter FalseCount) {
assert(canUseScalarCheckedCastInstructions(B.getModule(),
sourceFormalType, targetFormalType));
assert(canSILUseScalarCheckedCastInstructions(B.getModule(),
sourceFormalType,
targetFormalType));

// Create our successor and fail blocks.
SILBasicBlock *scalarFailBB = B.splitBlockForFallthrough();
Expand Down
7 changes: 3 additions & 4 deletions lib/SIL/Verifier/SILVerifier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3994,17 +3994,16 @@ class SILVerifier : public SILVerifierBase<SILVerifier> {
}
}

void verifyCheckedCast(bool isExact, SILType fromTy, SILType toTy,
bool isOpaque = false) {
void verifyCheckedCast(bool isExact, SILType fromTy, SILType toTy) {
// Verify common invariants.
require(fromTy.isObject() && toTy.isObject(),
"value checked cast src and dest must be objects");

auto fromCanTy = fromTy.getASTType();
auto toCanTy = toTy.getASTType();

require(isOpaque || canUseScalarCheckedCastInstructions(F.getModule(),
fromCanTy, toCanTy),
require(canSILUseScalarCheckedCastInstructions(F.getModule(),
fromCanTy, toCanTy),
"invalid value checked cast src or dest types");

// Peel off metatypes. If two types are checked-cast-able, so are their
Expand Down
6 changes: 3 additions & 3 deletions lib/SILGen/SILGenDynamicCast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ namespace {

private:
CastStrategy computeStrategy() const {
if (canUseScalarCheckedCastInstructions(SGF.SGM.M, SourceType,
TargetType))
if (canSILUseScalarCheckedCastInstructions(SGF.SGM.M, SourceType,
TargetType))
return CastStrategy::Scalar;
return CastStrategy::Address;
}
Expand Down Expand Up @@ -361,7 +361,7 @@ adjustForConditionalCheckedCastOperand(SILLocation loc, ManagedValue src,

// Figure out if we need the value to be in a temporary.
bool requiresAddress =
!canUseScalarCheckedCastInstructions(SGF.SGM.M, sourceType, targetType);
!canSILUseScalarCheckedCastInstructions(SGF.SGM.M, sourceType, targetType);

AbstractionPattern abstraction = SGF.SGM.M.Types.getMostGeneralAbstraction();
auto &srcAbstractTL = SGF.getTypeLowering(abstraction, sourceType);
Expand Down
4 changes: 2 additions & 2 deletions lib/SILGen/SILGenPattern.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1638,8 +1638,8 @@ emitCastOperand(SILGenFunction &SGF, SILLocation loc,
// temporary if necessary.

// Figure out if we need the value to be in a temporary.
bool requiresAddress = !canUseScalarCheckedCastInstructions(SGF.SGM.M,
sourceType, targetType);
bool requiresAddress =
!canSILUseScalarCheckedCastInstructions(SGF.SGM.M, sourceType, targetType);

AbstractionPattern abstraction = SGF.SGM.M.Types.getMostGeneralAbstraction();
auto &srcAbstractTL = SGF.getTypeLowering(abstraction, sourceType);
Expand Down
4 changes: 2 additions & 2 deletions lib/SILOptimizer/Utils/CastOptimizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,7 @@ CastOptimizer::simplifyCheckedCastBranchInst(CheckedCastBranchInst *Inst) {
// The unconditional_cast can be skipped, if the result of a cast
// is not used afterwards.
if (!ResultNotUsed) {
if (!dynamicCast.canUseScalarCheckedCastInstructions())
if (!dynamicCast.canSILUseScalarCheckedCastInstructions())
return nullptr;

CastedValue =
Expand Down Expand Up @@ -1143,7 +1143,7 @@ SILInstruction *CastOptimizer::optimizeCheckedCastAddrBranchInst(

if (MI) {
if (SuccessBB->getSinglePredecessorBlock() &&
canUseScalarCheckedCastInstructions(
canSILUseScalarCheckedCastInstructions(
Inst->getModule(), MI->getType().getASTType(),
Inst->getTargetFormalType())) {
SILBuilderWithScope B(Inst, builderContext);
Expand Down
5 changes: 3 additions & 2 deletions test/SILOptimizer/rcidentity.sil
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Builtin

typealias AnyObject = Builtin.AnyObject

protocol FakeAnyObject : class {}
protocol FakeAnyObject : AnyObject {}

class C : FakeAnyObject {
init()
Expand Down Expand Up @@ -58,7 +58,8 @@ enum E1 {
// Tests //
///////////

// Make sure that we see all the way through the chain of casts that %9 has an RCIdentity of %0 and that %12 is really the partial apply.
// Make sure that we see all the way through the chain of casts that %9 has
// an RCIdentity of %0 and that %12 is really the partial apply.
// CHECK-LABEL: @test_rcid_preserving_casts@
// CHECK: RESULT #9: 9 = 0
// CHECK: RESULT #12: 12 = 11
Expand Down
Loading