Skip to content

Commit e08b219

Browse files
authored
Merge pull request #23683 from lorentey/casting-improvements
[stdlib] Fix Set/Dictionary casting issues
2 parents 2b5a691 + ea6ae67 commit e08b219

File tree

7 files changed

+301
-33
lines changed

7 files changed

+301
-33
lines changed

stdlib/public/core/DictionaryCasting.swift

+61-14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,30 @@
1212

1313
//===--- Compiler conversion/casting entry points for Dictionary<K, V> ----===//
1414

15+
extension Dictionary {
16+
@_alwaysEmitIntoClient @inlinable // Introduced in 5.1
17+
@inline(__always)
18+
internal init?<C: Collection>(
19+
_mapping source: C,
20+
allowingDuplicates: Bool,
21+
transform: (C.Element) -> (key: Key, value: Value)?
22+
) {
23+
var target = _NativeDictionary<Key, Value>(capacity: source.count)
24+
if allowingDuplicates {
25+
for member in source {
26+
guard let (key, value) = transform(member) else { return nil }
27+
target._unsafeUpdate(key: key, value: value)
28+
}
29+
} else {
30+
for member in source {
31+
guard let (key, value) = transform(member) else { return nil }
32+
target._unsafeInsertNew(key: key, value: value)
33+
}
34+
}
35+
self.init(_native: target)
36+
}
37+
}
38+
1539
/// Perform a non-bridged upcast that always succeeds.
1640
///
1741
/// - Precondition: `BaseKey` and `BaseValue` are base classes or base `@objc`
@@ -21,12 +45,15 @@
2145
public func _dictionaryUpCast<DerivedKey, DerivedValue, BaseKey, BaseValue>(
2246
_ source: Dictionary<DerivedKey, DerivedValue>
2347
) -> Dictionary<BaseKey, BaseValue> {
24-
var builder = _DictionaryBuilder<BaseKey, BaseValue>(count: source.count)
25-
26-
for (k, v) in source {
27-
builder.add(key:k as! BaseKey, value: v as! BaseValue)
28-
}
29-
return builder.take()
48+
return Dictionary(
49+
_mapping: source,
50+
// String and NSString have different concepts of equality, so
51+
// NSString-keyed Dictionaries may generate key collisions when "upcasted"
52+
// to String. See rdar://problem/35995647
53+
allowingDuplicates: (BaseKey.self == String.self)
54+
) { k, v in
55+
(k as! BaseKey, v as! BaseValue)
56+
}!
3057
}
3158

3259
/// Called by the casting machinery.
@@ -66,7 +93,20 @@ public func _dictionaryDownCast<BaseKey, BaseValue, DerivedKey, DerivedValue>(
6693
_immutableCocoaDictionary: source._variant.asNative.bridged())
6794
}
6895
#endif
69-
return _dictionaryDownCastConditional(source)!
96+
97+
// Note: We can't delegate this call to _dictionaryDownCastConditional,
98+
// because we rely on as! to generate nice runtime errors when the downcast
99+
// fails.
100+
101+
return Dictionary(
102+
_mapping: source,
103+
// String and NSString have different concepts of equality, so
104+
// NSString-keyed Dictionaries may generate key collisions when downcasted
105+
// to String. See rdar://problem/35995647
106+
allowingDuplicates: (DerivedKey.self == String.self)
107+
) { k, v in
108+
(k as! DerivedKey, v as! DerivedValue)
109+
}!
70110
}
71111

72112
/// Called by the casting machinery.
@@ -97,12 +137,19 @@ public func _dictionaryDownCastConditional<
97137
>(
98138
_ source: Dictionary<BaseKey, BaseValue>
99139
) -> Dictionary<DerivedKey, DerivedValue>? {
100-
101-
var builder = _DictionaryBuilder<DerivedKey, DerivedValue>(count: source.count)
102-
for (k, v) in source {
103-
guard let k1 = k as? DerivedKey, let v1 = v as? DerivedValue
104-
else { return nil }
105-
builder.add(key: k1, value: v1)
140+
return Dictionary(
141+
_mapping: source,
142+
// String and NSString have different concepts of equality, so
143+
// NSString-keyed Dictionaries may generate key collisions when downcasted
144+
// to String. See rdar://problem/35995647
145+
allowingDuplicates: (DerivedKey.self == String.self)
146+
) { k, v in
147+
guard
148+
let key = k as? DerivedKey,
149+
let value = v as? DerivedValue
150+
else {
151+
return nil
152+
}
153+
return (key, value)
106154
}
107-
return builder.take()
108155
}

stdlib/public/core/NativeDictionary.swift

+22
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,28 @@ extension _NativeDictionary { // Insertions
485485
_storage._count &+= 1
486486
}
487487

488+
/// Insert a new element into uniquely held storage, replacing an existing
489+
/// value (if any). Storage must be uniquely referenced with adequate
490+
/// capacity.
491+
@_alwaysEmitIntoClient @inlinable // Introduced in 5.1
492+
internal mutating func _unsafeUpdate(
493+
key: __owned Key,
494+
value: __owned Value
495+
) {
496+
let (bucket, found) = find(key)
497+
if found {
498+
// Note that we also update the key here. This method is used to handle
499+
// collisions arising from equality transitions during bridging, and in
500+
// that case it is desirable to keep values paired with their original
501+
// keys. This is not how `updateValue(_:, forKey:)` works.
502+
(_keys + bucket.offset).pointee = key
503+
(_values + bucket.offset).pointee = value
504+
} else {
505+
_precondition(count < capacity)
506+
_insert(at: bucket, key: key, value: value)
507+
}
508+
}
509+
488510
/// Insert a new entry into uniquely held storage.
489511
/// Storage must be uniquely referenced.
490512
/// The `key` must not be already present in the Dictionary.

stdlib/public/core/NativeSet.swift

+27-1
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,21 @@ extension _NativeSet { // Low-level unchecked operations
117117
@inline(__always)
118118
internal func uncheckedInitialize(
119119
at bucket: Bucket,
120-
to element: __owned Element) {
120+
to element: __owned Element
121+
) {
121122
_internalInvariant(hashTable.isValid(bucket))
122123
(_elements + bucket.offset).initialize(to: element)
123124
}
125+
126+
@_alwaysEmitIntoClient @inlinable // Introduced in 5.1
127+
@inline(__always)
128+
internal func uncheckedAssign(
129+
at bucket: Bucket,
130+
to element: __owned Element
131+
) {
132+
_internalInvariant(hashTable.isOccupied(bucket))
133+
(_elements + bucket.offset).pointee = element
134+
}
124135
}
125136

126137
extension _NativeSet { // Low-level lookup operations
@@ -430,6 +441,21 @@ extension _NativeSet { // Insertions
430441
_unsafeInsertNew(element, at: bucket)
431442
return nil
432443
}
444+
445+
/// Insert an element into uniquely held storage, replacing an existing value
446+
/// (if any). Storage must be uniquely referenced with adequate capacity.
447+
@_alwaysEmitIntoClient @inlinable // Introduced in 5.1
448+
internal mutating func _unsafeUpdate(
449+
with element: __owned Element
450+
) {
451+
let (bucket, found) = find(element)
452+
if found {
453+
uncheckedAssign(at: bucket, to: element)
454+
} else {
455+
_precondition(count < capacity)
456+
_unsafeInsertNew(element, at: bucket)
457+
}
458+
}
433459
}
434460

435461
extension _NativeSet {

stdlib/public/core/SetCasting.swift

+56-16
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,47 @@
1212

1313
//===--- Compiler conversion/casting entry points for Set<Element> --------===//
1414

15+
extension Set {
16+
@_alwaysEmitIntoClient @inlinable // Introduced in 5.1
17+
@inline(__always)
18+
internal init?<C: Collection>(
19+
_mapping source: C,
20+
allowingDuplicates: Bool,
21+
transform: (C.Element) -> Element?
22+
) {
23+
var target = _NativeSet<Element>(capacity: source.count)
24+
if allowingDuplicates {
25+
for m in source {
26+
guard let member = transform(m) else { return nil }
27+
target._unsafeUpdate(with: member)
28+
}
29+
} else {
30+
for m in source {
31+
guard let member = transform(m) else { return nil }
32+
target._unsafeInsertNew(member)
33+
}
34+
}
35+
self.init(_native: target)
36+
}
37+
}
38+
1539
/// Perform a non-bridged upcast that always succeeds.
1640
///
1741
/// - Precondition: `BaseValue` is a base class or base `@objc`
1842
/// protocol (such as `AnyObject`) of `DerivedValue`.
1943
@inlinable
20-
public func _setUpCast<DerivedValue, BaseValue>(_ source: Set<DerivedValue>)
21-
-> Set<BaseValue> {
22-
var builder = _SetBuilder<BaseValue>(count: source.count)
23-
for x in source {
24-
builder.add(member: x as! BaseValue)
25-
}
26-
return builder.take()
44+
public func _setUpCast<DerivedValue, BaseValue>(
45+
_ source: Set<DerivedValue>
46+
) -> Set<BaseValue> {
47+
return Set(
48+
_mapping: source,
49+
// String and NSString have different concepts of equality, so Set<NSString>
50+
// may generate key collisions when "upcasted" to Set<String>.
51+
// See rdar://problem/35995647
52+
allowingDuplicates: (BaseValue.self == String.self)
53+
) { member in
54+
(member as! BaseValue)
55+
}!
2756
}
2857

2958
/// Called by the casting machinery.
@@ -54,7 +83,18 @@ public func _setDownCast<BaseValue, DerivedValue>(_ source: Set<BaseValue>)
5483
return Set(_immutableCocoaSet: source._variant.asNative.bridged())
5584
}
5685
#endif
57-
return _setDownCastConditional(source)!
86+
// We can't just delegate to _setDownCastConditional here because we rely on
87+
// `as!` to generate nice runtime errors when the downcast fails.
88+
89+
return Set(
90+
_mapping: source,
91+
// String and NSString have different concepts of equality, so
92+
// NSString-keyed Sets may generate key collisions when downcasted
93+
// to String. See rdar://problem/35995647
94+
allowingDuplicates: (DerivedValue.self == String.self)
95+
) { member in
96+
(member as! DerivedValue)
97+
}!
5898
}
5999

60100
/// Called by the casting machinery.
@@ -81,13 +121,13 @@ internal func _setDownCastConditionalIndirect<SourceValue, TargetValue>(
81121
public func _setDownCastConditional<BaseValue, DerivedValue>(
82122
_ source: Set<BaseValue>
83123
) -> Set<DerivedValue>? {
84-
var result = Set<DerivedValue>(minimumCapacity: source.count)
85-
for member in source {
86-
if let derivedMember = member as? DerivedValue {
87-
result.insert(derivedMember)
88-
continue
89-
}
90-
return nil
124+
return Set(
125+
_mapping: source,
126+
// String and NSString have different concepts of equality, so
127+
// NSString-keyed Sets may generate key collisions when downcasted
128+
// to String. See rdar://problem/35995647
129+
allowingDuplicates: (DerivedValue.self == String.self)
130+
) { member in
131+
member as? DerivedValue
91132
}
92-
return result
93133
}

validation-test/stdlib/Dictionary.swift

+15
Original file line numberDiff line numberDiff line change
@@ -3473,6 +3473,21 @@ DictionaryTestSuite.test("BridgedFromObjC.Nonverbatim.StringEqualityMismatch") {
34733473
expectTrue(v == 42 || v == 23)
34743474
}
34753475

3476+
DictionaryTestSuite.test("Upcast.StringEqualityMismatch") {
3477+
// Upcasting from NSString to String keys changes their concept of equality,
3478+
// resulting in two equal keys, one of which should be discarded by the
3479+
// downcast. (Along with its associated value.)
3480+
// rdar://problem/35995647
3481+
let d: Dictionary<NSString, NSObject> = [
3482+
"cafe\u{301}": 1 as NSNumber,
3483+
"café": 2 as NSNumber,
3484+
]
3485+
expectEqual(d.count, 2)
3486+
let d2 = d as Dictionary<String, NSObject>
3487+
expectEqual(d2.count, 1)
3488+
}
3489+
3490+
34763491
DictionaryTestSuite.test("BridgedFromObjC.Verbatim.OptionalDowncastFailure") {
34773492
let nsd = NSDictionary(
34783493
objects: [1 as NSNumber, 2 as NSNumber, 3 as NSNumber],

validation-test/stdlib/DictionaryTrapsObjC.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,12 @@ DictionaryTraps.test("ForcedVerbatimBridge.Value")
284284
}
285285
}
286286

287-
DictionaryTraps.test("Downcast1") {
287+
DictionaryTraps.test("Downcast.Verbatim")
288+
.skip(.custom(
289+
{ _isFastAssertConfiguration() },
290+
reason: "this trap is not guaranteed to happen in -Ounchecked"))
291+
.crashOutputMatches("Could not cast value of type")
292+
.code {
288293
let d: Dictionary<NSObject, NSObject> = [ TestObjCKeyTy(10): NSObject(),
289294
NSObject() : NSObject() ]
290295
let d2: Dictionary<TestObjCKeyTy, NSObject> = _dictionaryDownCast(d)
@@ -296,10 +301,11 @@ DictionaryTraps.test("Downcast1") {
296301
for (_, _) in d2 { }
297302
}
298303

299-
DictionaryTraps.test("Downcast2")
304+
DictionaryTraps.test("Downcast.NonVerbatimBridged")
300305
.skip(.custom(
301306
{ _isFastAssertConfiguration() },
302307
reason: "this trap is not guaranteed to happen in -Ounchecked"))
308+
.crashOutputMatches("Could not cast value of type")
303309
.code {
304310
let d: Dictionary<NSObject, NSObject> = [ TestObjCKeyTy(10): NSObject(),
305311
NSObject() : NSObject() ]

0 commit comments

Comments
 (0)