Skip to content

Commit 14df4d1

Browse files
committed
Compatibility for Optional -> AnyHashable casts
As part of making casting more consistent, the behavior of Optional -> AnyHashable casts was changed in some cases. This PR provides a hook for re-enabling the old behavior in certain contexts. Background: Most of the time, casts from String? to AnyHashable get optimized to just injects the String? into the AnyHashable, so the following has long been true and remains true in Swift 5.4: ``` let s = "abc" let o: String? = s // Next test is true because s is promoted to Optional<String> print(s == o) // Next test is false: Optional<String> and String are different types print(s as AnyHashable == o as AnyHashable) ``` But when casts ended up going through the runtime, Swift 5.3 behaved differently, as you could see by casting a dictionary with `String?` keys (in the generic array code, key and value casts always use the runtime logic). In the following code, both print statements behave differently in Swift 5.4 than before: ``` let a: [String?:String] = ["Foo":"Bar"] let b = a as [AnyHashable:Any] print(b["Foo"] == "Bar") // Works before Swift 5.4 print(b["Foo" as String?] == "Bar") // Works in Swift 5.4 and later ``` Old behavior: The `String?` keys would get unwrapped to `String` before being injected into AnyHashable. This allows the first to work but strangely breaks the second. New behavior: The `String?` keys do not get unwrapped. This breaks the first but makes the second work. TODO: The long-term goal, of course, is for `AnyHashable("Foo" as String?)` to test equal to `AnyHashable("Foo")` (and hash the same, of course). In that case, all of the tests above will succeed. Resolves rdar://73301155
1 parent ac56f03 commit 14df4d1

File tree

2 files changed

+85
-14
lines changed

2 files changed

+85
-14
lines changed

stdlib/public/runtime/DynamicCast.cpp

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,14 @@ struct ObjCBridgeMemo {
712712
};
713713
#endif
714714

715+
static bool avoidOptionalsInAnyHashable() {
716+
// Unexpected nulls are fatal in Swift 5.4 and not before
717+
// AnyHashable avoids optionals before Swift 5.4
718+
// TODO: This is admittedly awkward, but I need to get this changed quickly
719+
// I'll come back and clean this up later.
720+
return !unexpectedNullIsFatal();
721+
}
722+
715723
static DynamicCastResult
716724
tryCastToAnyHashable(
717725
OpaqueValue *destLocation, const Metadata *destType,
@@ -731,26 +739,47 @@ tryCastToAnyHashable(
731739
// TODO: Implement a fast path for NSString->AnyHashable casts.
732740
// These are incredibly common because an NSDictionary with
733741
// NSString keys is bridged by default to [AnyHashable:Any].
734-
// Until this is implemented, fall through to the default case
735-
SWIFT_FALLTHROUGH;
742+
// Until this is implemented, fall through to the general case
743+
break;
736744
#else
737-
// If no Obj-C interop, just fall through to the default case.
738-
SWIFT_FALLTHROUGH;
745+
// If no Obj-C interop, just fall through to the general case.
746+
break;
739747
#endif
740748
}
741-
default: {
742-
auto hashableConformance = reinterpret_cast<const HashableWitnessTable *>(
743-
swift_conformsToProtocol(srcType, &HashableProtocolDescriptor));
744-
if (hashableConformance) {
745-
_swift_convertToAnyHashableIndirect(srcValue, destLocation,
746-
srcType, hashableConformance);
747-
return DynamicCastResult::SuccessViaCopy;
748-
} else {
749-
return DynamicCastResult::Failure;
749+
case MetadataKind::Optional: {
750+
if (avoidOptionalsInAnyHashable()) {
751+
// Mimic the Swift 5.3 behavior when running in older contexts
752+
// This behavior is consistent with Swift 5.3 runtime, but inconsistent
753+
// with how other casting paths handle this case (which is why it's being
754+
// changed in Swift 5.4).
755+
// Old behavior: "Foo" as! String? as! AnyHashable => AnyHashable("Foo")
756+
// New behavior: "Foo" as! String? as! AnyHashable => AnyHashable(Optional("Foo"))
757+
auto srcInnerType = cast<EnumMetadata>(srcType)->getGenericArgs()[0];
758+
unsigned sourceEnumCase = srcInnerType->vw_getEnumTagSinglePayload(
759+
srcValue, /*emptyCases=*/1);
760+
auto nonNil = (sourceEnumCase == 0);
761+
if (nonNil) {
762+
return DynamicCast::Failure; // Our caller will unwrap the optional and try again
763+
}
764+
// If it is nil, fall through to the general case to just wrap the nil
750765
}
766+
break;
751767
}
768+
default:
769+
break;
770+
}
771+
772+
773+
// General case: If it conforms to Hashable, we cast it
774+
auto hashableConformance = reinterpret_cast<const HashableWitnessTable *>(
775+
swift_conformsToProtocol(srcType, &HashableProtocolDescriptor));
776+
if (hashableConformance) {
777+
_swift_convertToAnyHashableIndirect(srcValue, destLocation,
778+
srcType, hashableConformance);
779+
return DynamicCastResult::SuccessViaCopy;
780+
} else {
781+
return DynamicCastResult::Failure;
752782
}
753-
return DynamicCastResult::Failure;
754783
}
755784

756785
static DynamicCastResult

test/Casting/Casts.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,4 +881,46 @@ CastsTests.test("NSDictionary -> Dictionary casting [SR-12025]") {
881881
}
882882
#endif
883883

884+
// Casting optionals to AnyHashable is a little peculiar
885+
// TODO: It would be nice if AnyHashable(Optional("Foo")) == AnyHashable("Foo")
886+
// (including as dictionary keys). That would make this a lot less confusing.
887+
CastsTests.test("Optional cast to AnyHashable") {
888+
let d: [String?: String] = ["FooKey": "FooValue", nil: "NilValue"]
889+
// In Swift 5.3, this cast DOES unwrap the non-nil key
890+
// In Swift 5.4, the cast does NOT unwrap the non-nil key
891+
let d2 = d as [AnyHashable: String]
892+
let d3 = d2["FooKey" as String? as AnyHashable]
893+
expectNotNil(d3)
894+
let d4 = d2["FooKey" as String?]
895+
expectNotNil(d4)
896+
// In Swift 5.4, AnyHashable(String?) is never equal to AnyHashable(String)
897+
// This is expected to change...
898+
let d5 = d2["FooKey"]
899+
expectNil(d5)
900+
let d6 = d2["FooKey" as AnyHashable]
901+
expectNil(d6)
902+
903+
// In both Swift 5.3 and 5.4, the nil key should be preserved and still function
904+
let d7 = d2[nil]
905+
expectNotNil(d7)
906+
907+
// Direct casts via the runtime unwrap the optional in 5.3 but not 5.4.
908+
let a: String = "Foo"
909+
let ah: AnyHashable = a
910+
let b: String? = a
911+
let bh = runtimeCast(b, to: AnyHashable.self)
912+
// bh is an AnyHashable(Optional("Foo")) in Swift 5.4 but not earlier
913+
// ah is an AnyHashable("Foo")
914+
expectNotEqual(bh, ah)
915+
916+
// In both Swift 5.3 and 5.4, direct casts that don't go through the runtime don't unwrap the optional
917+
let x: String = "Baz"
918+
let xh = x as AnyHashable
919+
let y: String? = x
920+
let yh = y as AnyHashable // Doesn't unwrap the optional
921+
// xh is AnyHashable("Baz")
922+
// yh is AnyHashable(Optional("Baz")) in Swift 5.4 and earlier
923+
expectNotEqual(xh, yh)
924+
}
925+
884926
runAllTests()

0 commit comments

Comments
 (0)