Skip to content

Commit ed2612f

Browse files
authored
Merge pull request #9516 from jckarter/objc-keypath-id
SILGen/IRGen/KeyPaths: Components for ObjC properties need to be identified by selector.
2 parents 6285cc5 + 9830f39 commit ed2612f

File tree

13 files changed

+230
-26
lines changed

13 files changed

+230
-26
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@ CHANGELOG
2121
Swift 4.0
2222
---------
2323

24+
* [SE-0161][] is partially implemented. Swift now natively supports key path
25+
objects for properties. Similar to KVC key path strings in Cocoa, key path
26+
objects allow a property to be referenced independently of accessing it
27+
from a value:
28+
29+
```swift
30+
struct Point {
31+
var x, y: Double
32+
}
33+
let x = \Point.x
34+
let y = \Point.y
35+
36+
let p = Point(x: 3, y: 4)
37+
p[keyPath: x] // gives 3
38+
p[keyPath: y] // gives 4
39+
```
40+
2441
* Core Foundation types implicitly conform to Hashable (and Equatable), using
2542
CFHash and CFEqual as the implementation. This change applies even to "Swift
2643
3 mode", so if you were previously adding this conformance yourself, use

include/swift/ABI/KeyPath.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,18 @@ class KeyPathComponentHeader {
164164
};
165165

166166
enum ComputedPropertyIDKind {
167-
Getter,
167+
Pointer,
168168
StoredPropertyOffset,
169169
VTableOffset,
170170
};
171171

172+
constexpr static uint32_t
173+
getResolutionStrategy(ComputedPropertyIDKind idKind) {
174+
return idKind == Pointer ? _SwiftKeyPathComponentHeader_ComputedIDUnresolvedIndirectPointer
175+
: idKind == StoredPropertyOffset ? _SwiftKeyPathComponentHeader_ComputedIDUnresolvedFieldOffset
176+
: (assert("no resolution strategy implemented" && false), 0);
177+
}
178+
172179
constexpr static KeyPathComponentHeader
173180
forComputedProperty(ComputedPropertyKind kind,
174181
ComputedPropertyIDKind idKind,
@@ -186,7 +193,8 @@ class KeyPathComponentHeader {
186193
| (idKind == VTableOffset
187194
? _SwiftKeyPathComponentHeader_ComputedIDByVTableOffsetFlag : 0)
188195
| (hasArguments ? _SwiftKeyPathComponentHeader_ComputedHasArgumentsFlag : 0)
189-
| (resolvedID ? 0 : _SwiftKeyPathComponentHeader_ComputedUnresolvedIDFlag));
196+
| (resolvedID ? _SwiftKeyPathComponentHeader_ComputedIDResolved
197+
: getResolutionStrategy(idKind)));
190198
}
191199

192200
constexpr uint32_t getData() const { return Data; }

lib/IRGen/GenKeyPath.cpp

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -271,26 +271,37 @@ IRGenModule::getAddrOfKeyPathPattern(KeyPathPattern *pattern,
271271
bool idResolved;
272272
switch (id.getKind()) {
273273
case KeyPathPatternComponent::ComputedPropertyId::Function:
274-
idKind = KeyPathComponentHeader::Getter;
274+
idKind = KeyPathComponentHeader::Pointer;
275275
idValue = getAddrOfSILFunction(id.getFunction(), NotForDefinition);
276276
idResolved = true;
277277
break;
278278
case KeyPathPatternComponent::ComputedPropertyId::DeclRef: {
279-
idKind = KeyPathComponentHeader::VTableOffset;
280279
auto declRef = id.getDeclRef();
281-
auto dc = declRef.getDecl()->getDeclContext();
282-
if (isa<ClassDecl>(dc)) {
283-
auto index = getVirtualMethodIndex(*this, declRef);
284-
idValue = llvm::ConstantInt::get(SizeTy, index);
285-
idResolved = true;
286-
} else if (auto methodProto = dyn_cast<ProtocolDecl>(dc)) {
287-
auto &protoInfo = getProtocolInfo(methodProto);
288-
auto index = protoInfo.getFunctionIndex(
289-
cast<AbstractFunctionDecl>(declRef.getDecl()));
290-
idValue = llvm::ConstantInt::get(SizeTy, -index.getValue());
291-
idResolved = true;
280+
281+
// Foreign method refs identify using a selector
282+
// reference, which is doubly-indirected and filled in with a unique
283+
// pointer by dyld.
284+
if (declRef.isForeign) {
285+
assert(ObjCInterop && "foreign keypath component w/o objc interop?!");
286+
idKind = KeyPathComponentHeader::Pointer;
287+
idValue = getAddrOfObjCSelectorRef(declRef);
288+
idResolved = false;
292289
} else {
293-
llvm_unreachable("neither a class nor protocol dynamic method?");
290+
idKind = KeyPathComponentHeader::VTableOffset;
291+
auto dc = declRef.getDecl()->getDeclContext();
292+
if (isa<ClassDecl>(dc)) {
293+
auto index = getVirtualMethodIndex(*this, declRef);
294+
idValue = llvm::ConstantInt::get(SizeTy, index);
295+
idResolved = true;
296+
} else if (auto methodProto = dyn_cast<ProtocolDecl>(dc)) {
297+
auto &protoInfo = getProtocolInfo(methodProto);
298+
auto index = protoInfo.getFunctionIndex(
299+
cast<AbstractFunctionDecl>(declRef.getDecl()));
300+
idValue = llvm::ConstantInt::get(SizeTy, -index.getValue());
301+
idResolved = true;
302+
} else {
303+
llvm_unreachable("neither a class nor protocol dynamic method?");
304+
}
294305
}
295306
break;
296307
}

lib/IRGen/GenObjC.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,11 @@ namespace {
581581
};
582582
} // end anonymous namespace
583583

584+
llvm::Constant *IRGenModule::getAddrOfObjCSelectorRef(SILDeclRef method) {
585+
assert(method.isForeign);
586+
return getAddrOfObjCSelectorRef(Selector(method).str());
587+
}
588+
584589
static void emitSuperArgument(IRGenFunction &IGF, bool isInstanceMethod,
585590
llvm::Value *selfValue,
586591
Explosion &selfValues,

lib/IRGen/IRGenModule.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ class IRGenModule {
662662
llvm::Constant *getAddrOfGlobalConstantString(StringRef utf8);
663663
llvm::Constant *getAddrOfGlobalUTF16ConstantString(StringRef utf8);
664664
llvm::Constant *getAddrOfObjCSelectorRef(StringRef selector);
665+
llvm::Constant *getAddrOfObjCSelectorRef(SILDeclRef method);
665666
llvm::Constant *getAddrOfObjCMethodName(StringRef methodName);
666667
llvm::Constant *getAddrOfObjCProtocolRecord(ProtocolDecl *proto,
667668
ForDefinition_t forDefinition);

lib/SILGen/SILGenExpr.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2694,7 +2694,12 @@ getIdForKeyPathComponentComputedProperty(SILGenFunction &SGF,
26942694
}
26952695
case AccessStrategy::DispatchToAccessor: {
26962696
// Identify the property by its vtable or wtable slot.
2697-
return SILDeclRef(property->getGetter(), SILDeclRef::Kind::Func);
2697+
// Use the foreign selector if the decl is ObjC-imported, dynamic, or
2698+
// otherwise requires objc_msgSend for its ABI.
2699+
return SILDeclRef(property->getGetter(), SILDeclRef::Kind::Func,
2700+
ResilienceExpansion::Minimal,
2701+
/*curried*/ false,
2702+
/*foreign*/ property->requiresForeignGetterAndSetter());
26982703
}
26992704
case AccessStrategy::BehaviorStorage:
27002705
llvm_unreachable("unpossible");

stdlib/public/SwiftShims/KeyPath.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,14 @@ static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedIDByVTableOff
8383
= 0x02000000U;
8484
static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedHasArgumentsFlag
8585
= 0x01000000U;
86-
static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedUnresolvedIDFlag
86+
static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedIDResolutionMask
87+
= 0x0000000FU;
88+
static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedIDResolved
89+
= 0x00000000U;
90+
static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedIDUnresolvedFieldOffset
8791
= 0x00000001U;
92+
static const __swift_uint32_t _SwiftKeyPathComponentHeader_ComputedIDUnresolvedIndirectPointer
93+
= 0x00000002U;
8894

8995

9096
#ifdef __cplusplus

stdlib/public/core/KeyPath.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,18 @@ internal struct RawKeyPathComponent {
623623
static var computedHasArgumentsFlag: UInt32 {
624624
return _SwiftKeyPathComponentHeader_ComputedHasArgumentsFlag
625625
}
626-
static var computedUnresolvedIDFlag: UInt32 {
627-
return _SwiftKeyPathComponentHeader_ComputedUnresolvedIDFlag
626+
627+
static var computedIDResolutionMask: UInt32 {
628+
return _SwiftKeyPathComponentHeader_ComputedIDResolutionMask
629+
}
630+
static var computedIDResolved: UInt32 {
631+
return _SwiftKeyPathComponentHeader_ComputedIDResolved
632+
}
633+
static var computedIDUnresolvedFieldOffset: UInt32 {
634+
return _SwiftKeyPathComponentHeader_ComputedIDUnresolvedFieldOffset
635+
}
636+
static var computedIDUnresolvedIndirectPointer: UInt32 {
637+
return _SwiftKeyPathComponentHeader_ComputedIDUnresolvedIndirectPointer
628638
}
629639

630640
var _value: UInt32
@@ -1733,16 +1743,29 @@ internal func _instantiateKeyPathBuffer(
17331743
// property.
17341744
var newHeader = header
17351745
var id = patternBuffer.pop(Int.self)
1736-
if header.payload
1737-
& RawKeyPathComponent.Header.computedUnresolvedIDFlag != 0 {
1746+
switch header.payload
1747+
& RawKeyPathComponent.Header.computedIDResolutionMask {
1748+
case RawKeyPathComponent.Header.computedIDResolved:
1749+
// Nothing to do.
1750+
break
1751+
case RawKeyPathComponent.Header.computedIDUnresolvedFieldOffset:
1752+
// The value in the pattern is an offset into the type metadata that
1753+
// points to the field offset for the stored property identifying the
1754+
// component.
17381755
_sanityCheck(header.payload
17391756
& RawKeyPathComponent.Header.computedIDByStoredPropertyFlag != 0,
1740-
"only stored property IDs should need resolution")
1741-
newHeader.payload &=
1742-
~RawKeyPathComponent.Header.computedUnresolvedIDFlag
1757+
"only stored property IDs should need offset resolution")
17431758
let metadataPtr = unsafeBitCast(base, to: UnsafeRawPointer.self)
17441759
id = metadataPtr.load(fromByteOffset: id, as: Int.self)
1760+
case RawKeyPathComponent.Header.computedIDUnresolvedIndirectPointer:
1761+
// The value in the pattern is a pointer to the actual unique word-sized
1762+
// value in memory.
1763+
let idPtr = UnsafeRawPointer(bitPattern: id).unsafelyUnwrapped
1764+
id = idPtr.load(as: Int.self)
1765+
default:
1766+
_sanityCheckFailure("unpossible")
17451767
}
1768+
newHeader.payload &= ~RawKeyPathComponent.Header.computedIDResolutionMask
17461769
pushDest(newHeader)
17471770
pushDest(id)
17481771
// Carry over the accessors.

test/IRGen/keypaths_objc.sil

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) -emit-ir %s | %FileCheck %s --check-prefix=CHECK --check-prefix=CHECK-%target-ptrsize
2+
// REQUIRES: objc_interop
3+
4+
import Swift
5+
import Foundation
6+
7+
class C: NSObject {
8+
dynamic var x: NSString { get }
9+
override init()
10+
}
11+
12+
sil_vtable C {}
13+
14+
sil @x_get : $@convention(thin) (@in C) -> @out NSString
15+
16+
// CHECK: [[KEYPATH_A:@keypath.*]] = private global
17+
// -- 0x2000_0002: computed, get-only, indirect identifier
18+
// CHECK-SAME: i32 536870914
19+
// CHECK-SAME: i8** @"\01L_selector(x)"
20+
21+
// CHECK-LABEL: define swiftcc void @objc_only_property()
22+
sil @objc_only_property : $@convention(thin) () -> () {
23+
entry:
24+
// CHECK: call %swift.refcounted* @swift_getKeyPath({{.*}} [[KEYPATH_A]]
25+
%a = keypath $KeyPath<C, NSString>, (objc "x"; root $C; gettable_property $NSString, id #C.x!getter.1.foreign, getter @x_get : $@convention(thin) (@in C) -> @out NSString)
26+
unreachable
27+
}
28+
29+
sil hidden @_T013keypaths_objc1CC1xSo8NSStringCfgTo : $@convention(objc_method) (@guaranteed C) -> NSString {
30+
entry(%0 : $C):
31+
unreachable
32+
}
33+
34+
sil hidden @_T013keypaths_objc1CCACycfcTo : $@convention(objc_method) (@objc_metatype C.Type) -> @owned C {
35+
entry(%0 : $@objc_metatype C.Type):
36+
unreachable
37+
}

test/SILGen/Inputs/keypaths_objc.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import Foundation;
2+
3+
@interface ObjCFoo
4+
5+
@property(readonly) NSString *_Nonnull objcProp;
6+
7+
@end

test/SILGen/keypaths_objc.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) -enable-experimental-keypaths -emit-silgen %s | %FileCheck %s
1+
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) -enable-experimental-keypaths -emit-silgen -import-objc-header %S/Inputs/keypaths_objc.h %s | %FileCheck %s
22
// REQUIRES: objc_interop
33

44
import Foundation
@@ -16,6 +16,8 @@ class Foo: NSObject {
1616

1717
@objc subscript(x: Int) -> Foo { return self }
1818
@objc subscript(x: Bar) -> Foo { return self }
19+
20+
dynamic var dyn: String { fatalError() }
1921
}
2022

2123
class Bar: NSObject {
@@ -43,3 +45,13 @@ func objcKeypaths() {
4345
// CHECK: keypath $KeyPath<Foo, Bar>, (objc "thisIsADifferentName"
4446
_ = \Foo.differentName
4547
}
48+
49+
// CHECK-LABEL: sil hidden @_T013keypaths_objc0B18KeypathIdentifiersyyF
50+
func objcKeypathIdentifiers() {
51+
// CHECK: keypath $KeyPath<ObjCFoo, String>, (objc "objcProp"; {{.*}} id #ObjCFoo.objcProp!getter.1.foreign
52+
_ = \ObjCFoo.objcProp
53+
// CHECK: keypath $KeyPath<Foo, String>, (objc "dyn"; {{.*}} id #Foo.dyn!getter.1.foreign
54+
_ = \Foo.dyn
55+
// CHECK: keypath $KeyPath<Foo, Int>, (objc "int"; {{.*}} id #Foo.int!getter.1 :
56+
_ = \Foo.int
57+
}

test/stdlib/KeyPath.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,66 @@ keyPath.test("key path generic instantiation") {
192192
expectEqual(s_c_x_lt, s_c_x_lt2)
193193
}
194194

195+
struct TestComputed {
196+
static var numNonmutatingSets = 0
197+
static var numMutatingSets = 0
198+
199+
static func resetCounts() {
200+
numNonmutatingSets = 0
201+
numMutatingSets = 0
202+
}
203+
204+
var canary = LifetimeTracked(0)
205+
206+
var readonly: LifetimeTracked {
207+
return LifetimeTracked(1)
208+
}
209+
var nonmutating: LifetimeTracked {
210+
get {
211+
return LifetimeTracked(2)
212+
}
213+
nonmutating set { TestComputed.numNonmutatingSets += 1 }
214+
}
215+
var mutating: LifetimeTracked {
216+
get {
217+
return LifetimeTracked(3)
218+
}
219+
set {
220+
canary = newValue
221+
}
222+
}
223+
}
224+
225+
keyPath.test("computed properties") {
226+
var test = TestComputed()
227+
228+
do {
229+
let tc_readonly = \TestComputed.readonly
230+
expectTrue(test[keyPath: tc_readonly] !== test[keyPath: tc_readonly])
231+
expectEqual(test[keyPath: tc_readonly].value,
232+
test[keyPath: tc_readonly].value)
233+
}
234+
235+
do {
236+
let tc_nonmutating = \TestComputed.nonmutating
237+
expectTrue(test[keyPath: tc_nonmutating] !== test[keyPath: tc_nonmutating])
238+
expectEqual(test[keyPath: tc_nonmutating].value,
239+
test[keyPath: tc_nonmutating].value)
240+
TestComputed.resetCounts()
241+
test[keyPath: tc_nonmutating] = LifetimeTracked(4)
242+
expectEqual(TestComputed.numNonmutatingSets, 1)
243+
}
244+
245+
do {
246+
let tc_mutating = \TestComputed.mutating
247+
TestComputed.resetCounts()
248+
expectTrue(test[keyPath: tc_mutating] !== test[keyPath: tc_mutating])
249+
expectEqual(test[keyPath: tc_mutating].value,
250+
test[keyPath: tc_mutating].value)
251+
let newObject = LifetimeTracked(5)
252+
test[keyPath: tc_mutating] = newObject
253+
expectTrue(test.canary === newObject)
254+
}
255+
}
256+
195257
runAllTests()

test/stdlib/KeyPathObjC.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class Foo: NSObject {
2020

2121
@objc subscript(x: Int) -> Foo { return self }
2222
@objc subscript(x: Bar) -> Foo { return self }
23+
24+
dynamic var dynamic: Bar { fatalError() }
2325
}
2426

2527
class Bar: NSObject {
@@ -70,4 +72,12 @@ testKVCStrings.test("KVC strings") {
7072
}
7173
}
7274

75+
testKVCStrings.test("identification by selector") {
76+
let foo_dynamic = \Foo.dynamic
77+
let bar_foo = \Bar.foo
78+
let foo_dynamic_foo = \Foo.dynamic.foo
79+
80+
expectEqual(foo_dynamic.appending(path: bar_foo), foo_dynamic_foo)
81+
}
82+
7383
runAllTests()

0 commit comments

Comments
 (0)