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

Compatibility for Optional -> AnyHashable casts #35650

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 37 additions & 14 deletions stdlib/public/runtime/DynamicCast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -731,26 +731,49 @@ tryCastToAnyHashable(
// TODO: Implement a fast path for NSString->AnyHashable casts.
// These are incredibly common because an NSDictionary with
// NSString keys is bridged by default to [AnyHashable:Any].
// Until this is implemented, fall through to the default case
SWIFT_FALLTHROUGH;
// Until this is implemented, fall through to the general case
break;
#else
// If no Obj-C interop, just fall through to the default case.
SWIFT_FALLTHROUGH;
// If no Obj-C interop, just fall through to the general case.
break;
#endif
}
default: {
auto hashableConformance = reinterpret_cast<const HashableWitnessTable *>(
swift_conformsToProtocol(srcType, &HashableProtocolDescriptor));
if (hashableConformance) {
_swift_convertToAnyHashableIndirect(srcValue, destLocation,
srcType, hashableConformance);
return DynamicCastResult::SuccessViaCopy;
} else {
return DynamicCastResult::Failure;
case MetadataKind::Optional: {
// Until SR-9047 fixes the interactions between AnyHashable and Optional, we
// avoid directly injecting Optionals. In particular, this allows
// casts from [String?:String] to [AnyHashable:Any] to work the way people
// expect. Otherwise, without SR-9047, the resulting dictionary can only be
// indexed with an explicit Optional<String>, not a plain String.
// After SR-9047, we can consider dropping this special case entirely.

// !!!! This breaks compatibility with compiler-optimized casts
// (which just inject) and violates the Casting Spec. It just preserves
// the behavior of the older casting code until we can clean things up.
auto srcInnerType = cast<EnumMetadata>(srcType)->getGenericArgs()[0];
unsigned sourceEnumCase = srcInnerType->vw_getEnumTagSinglePayload(
srcValue, /*emptyCases=*/1);
auto nonNil = (sourceEnumCase == 0);
if (nonNil) {
return DynamicCastResult::Failure; // Our caller will unwrap the optional and try again
}
// Else Optional is nil -- the general case below will inject it
break;
}
default:
break;
}


// General case: If it conforms to Hashable, we cast it
auto hashableConformance = reinterpret_cast<const HashableWitnessTable *>(
swift_conformsToProtocol(srcType, &HashableProtocolDescriptor));
if (hashableConformance) {
_swift_convertToAnyHashableIndirect(srcValue, destLocation,
srcType, hashableConformance);
return DynamicCastResult::SuccessViaCopy;
} else {
return DynamicCastResult::Failure;
}
return DynamicCastResult::Failure;
}

static DynamicCastResult
Expand Down
43 changes: 43 additions & 0 deletions test/Casting/Casts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -881,4 +881,47 @@ CastsTests.test("NSDictionary -> Dictionary casting [SR-12025]") {
}
#endif

// Casting optionals to AnyHashable is a little peculiar
// TODO: It would be nice if AnyHashable(Optional("Foo")) == AnyHashable("Foo")
// (including as dictionary keys). That would make this a lot less confusing.
CastsTests.test("Optional cast to AnyHashable") {
let d: [String?: String] = ["FooKey": "FooValue", nil: "NilValue"]
// In Swift 5.3, this cast DOES unwrap the non-nil key
// We've deliberately tried to preserve that behavior in Swift 5.4
let d2 = d as [AnyHashable: String]

// After SR-9047, all four of the following should work:
let d3 = d2["FooKey" as String? as AnyHashable]
expectNil(d3)
let d4 = d2["FooKey" as String?]
expectNil(d4)
let d5 = d2["FooKey"]
expectNotNil(d5)
let d6 = d2["FooKey" as AnyHashable]
expectNotNil(d6)

// The nil key should be preserved and still function
let d7 = d2[String?.none as AnyHashable]
expectNotNil(d7)

// Direct casts via the runtime unwrap the optional
let a: String = "Foo"
let ah: AnyHashable = a
let b: String? = a
let bh = runtimeCast(b, to: AnyHashable.self)
expectEqual(bh, ah)

// Direct casts that don't go through the runtime don't unwrap the optional
// This is inconsistent with the runtime cast behavior above. We should
// probably change the runtime behavior above to work the same as this,
// but that should wait until SR-9047 lands.
let x: String = "Baz"
let xh = x as AnyHashable
let y: String? = x
let yh = y as AnyHashable // Doesn't unwrap the optional
// xh is AnyHashable("Baz")
// yh is AnyHashable(Optional("Baz"))
expectNotEqual(xh, yh)
}

runAllTests()