Skip to content

Commit fc4d11f

Browse files
authored
Escaping track (#32)
1 parent f8c7eb7 commit fc4d11f

17 files changed

+877
-363
lines changed

Sources/StateStruct/PropertyNode.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public struct PropertyNode: Equatable, CustomDebugStringConvertible {
1+
public struct PropertyNode: Sendable, Equatable, CustomDebugStringConvertible {
22

33
public struct Status: OptionSet, Sendable {
44
public let rawValue: Int8
@@ -258,7 +258,7 @@ extension PropertyNode {
258258

259259
modify(&nodes)
260260
}
261-
261+
262262
}
263263

264264
extension PropertyNode {

Sources/StateStruct/Source.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,20 @@ public macro COWTrackingProperty() =
3333
)
3434
@attached(peer, names: prefixed(`_backing_`))
3535
public macro WeakTrackingProperty() =
36-
#externalMacro(module: "StateStructMacros", type: "WeakTrackingPropertyMacro")
36+
#externalMacro(module: "StateStructMacros", type: "PrimitiveTrackingPropertyMacro")
37+
38+
@attached(
39+
accessor,
40+
names: named(init), named(_read), named(set), named(_modify)
41+
)
42+
@attached(peer, names: prefixed(`_backing_`))
43+
public macro PrimitiveTrackingProperty() =
44+
#externalMacro(module: "StateStructMacros", type: "PrimitiveTrackingPropertyMacro")
45+
3746

3847
#if DEBUG
3948

49+
4050
@Tracking
4151
struct OptinalPropertyState {
4252

Sources/StateStruct/Tracking.swift

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,2 @@
11
import Foundation
22
import os.lock
3-
4-
private enum ThreadDictionaryKey {
5-
case tracking
6-
case currentKeyPathStack
7-
}
8-
9-
extension NSMutableDictionary {
10-
11-
var tracking: TrackingResult? {
12-
get {
13-
self[ThreadDictionaryKey.tracking] as? TrackingResult
14-
}
15-
set {
16-
self[ThreadDictionaryKey.tracking] = newValue
17-
}
18-
}
19-
}
20-
21-
public enum _Tracking {
22-
23-
public static func _tracking_modifyStorage(_ modifier: (inout TrackingResult) -> Void) {
24-
guard Thread.current.threadDictionary.tracking != nil else {
25-
return
26-
}
27-
modifier(&Thread.current.threadDictionary.tracking!)
28-
}
29-
}

Sources/StateStruct/TrackingObject.swift

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,30 @@ import Foundation.NSThread
33
/// A type that represents an object that can be tracked for changes.
44
/// This protocol is automatically implemented by types marked with `@Tracking` macro.
55
public protocol TrackingObject {
6-
var _tracking_context: _TrackingContext { get }
6+
var _tracking_context: _TrackingContext { get set }
77
}
88

99
extension TrackingObject {
10+
11+
public var trackingResult: TrackingResult? {
12+
_tracking_context.trackingResultRef?.result
13+
}
1014

11-
public func tracking(
12-
using graph: consuming PropertyNode? = nil,
13-
_ applier: () throws -> Void
14-
) rethrows -> TrackingResult {
15-
let current = Thread.current.threadDictionary.tracking
16-
startTracking()
17-
defer {
18-
Thread.current.threadDictionary.tracking = current
19-
endTracking()
20-
}
21-
22-
Thread.current.threadDictionary.tracking = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self))))
23-
try applier()
24-
let result = Thread.current.threadDictionary.tracking!
25-
return result
15+
public consuming func tracked(using graph: consuming PropertyNode? = nil) -> Self {
16+
startNewTracking(using: graph)
17+
return self
2618
}
2719

28-
private func startTracking() {
20+
public mutating func startNewTracking(using graph: consuming PropertyNode? = nil) {
21+
22+
let newResult = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self))))
23+
24+
_tracking_context = .init(trackingResultRef: .init(result: newResult))
2925
_tracking_context.path = .root(of: Self.self)
3026
}
3127

32-
private func endTracking() {
28+
public func endTracking() {
29+
_tracking_context.trackingResultRef = nil
3330
}
34-
31+
3532
}

Sources/StateStruct/TrackingResult.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
1+
import os.lock
12

2-
public final class TrackingResultRef {
3+
public final class TrackingResultRef: Sendable {
34

4-
public var result: TrackingResult
5+
public let resultBox: OSAllocatedUnfairLock<TrackingResult>
56

7+
public var result: TrackingResult {
8+
resultBox.withLock {
9+
$0
10+
}
11+
}
12+
613
public init(result: TrackingResult) {
7-
self.result = result
14+
self.resultBox = .init(initialState: result)
15+
}
16+
17+
public func modify(_ closure: (inout TrackingResult) -> Void) {
18+
resultBox.withLockUnchecked(closure)
19+
}
20+
public func accessorRead(path: PropertyPath?) {
21+
resultBox.withLockUnchecked { result in
22+
result.accessorRead(path: path)
23+
}
824
}
925

26+
public func accessorSet(path: PropertyPath?) {
27+
resultBox.withLockUnchecked { result in
28+
result.accessorSet(path: path)
29+
}
30+
}
31+
32+
public func accessorModify(path: PropertyPath?) {
33+
resultBox.withLockUnchecked { result in
34+
result.accessorModify(path: path)
35+
}
36+
}
1037
}
1138

12-
public struct TrackingResult: Equatable {
39+
public struct TrackingResult: Equatable, Sendable {
1340

1441
public var graph: PropertyNode
1542

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os.lock
22
import Foundation.NSThread
33

4-
public final class _TrackingContext: Sendable, Hashable {
4+
public struct _TrackingContext: Sendable, Hashable {
55

66
public static func == (lhs: _TrackingContext, rhs: _TrackingContext) -> Bool {
77
// ``_TrackingContext`` is used only for embedding into the struct.
@@ -13,52 +13,79 @@ public final class _TrackingContext: Sendable, Hashable {
1313
public func hash(into hasher: inout Hasher) {
1414
0.hash(into: &hasher)
1515
}
16+
17+
public struct Info {
18+
19+
public var path: PropertyPath?
20+
21+
public var identifier: AnyHashable?
22+
23+
public var currentResultRef: TrackingResultRef?
1624

17-
@inlinable
25+
init(
26+
path: PropertyPath? = nil,
27+
identifier: AnyHashable? = nil,
28+
currentResultRef: TrackingResultRef?
29+
) {
30+
self.path = path
31+
self.identifier = identifier
32+
self.currentResultRef = currentResultRef
33+
}
34+
35+
}
36+
37+
private let infoBox: OSAllocatedUnfairLock<Info>
38+
1839
public var path: PropertyPath? {
1940
get {
2041
infoBox.withLockUnchecked {
21-
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.path
42+
$0.path
2243
}
2344
}
24-
set {
45+
nonmutating set {
2546
infoBox.withLockUnchecked {
26-
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].path = newValue
47+
$0.path = newValue
2748
}
2849
}
2950
}
3051

3152
public var identifier: AnyHashable? {
3253
get {
3354
infoBox.withLockUnchecked {
34-
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.identifier
55+
$0.identifier
3556
}
3657
}
37-
set {
58+
nonmutating set {
3859
infoBox.withLockUnchecked {
39-
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].identifier = newValue
60+
$0.identifier = newValue
4061
}
4162
}
4263
}
43-
44-
@usableFromInline
45-
struct Info {
46-
@usableFromInline
47-
var path: PropertyPath?
48-
49-
@usableFromInline
50-
var identifier: AnyHashable?
51-
52-
@inlinable
53-
init() {
64+
65+
public var trackingResultRef: TrackingResultRef? {
66+
get {
67+
infoBox.withLock { $0.currentResultRef }
68+
}
69+
nonmutating set {
70+
infoBox.withLock {
71+
$0.currentResultRef = newValue
72+
}
5473
}
5574
}
56-
57-
@usableFromInline
58-
let infoBox: OSAllocatedUnfairLock<[UnsafeMutableRawPointer: Info]> = .init(
59-
uncheckedState: [:]
60-
)
61-
75+
76+
public init(trackingResultRef: TrackingResultRef) {
77+
infoBox = .init(
78+
uncheckedState: .init(
79+
currentResultRef: trackingResultRef
80+
)
81+
)
82+
}
83+
6284
public init() {
85+
infoBox = .init(
86+
uncheckedState: .init(
87+
currentResultRef: nil
88+
)
89+
)
6390
}
6491
}

Sources/StateStructMacros/COWTrackingPropertyMacro.swift

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ extension COWTrackingPropertyMacro: AccessorMacro {
143143

144144
let isConstant = variableDecl.bindingSpecifier.tokenKind == .keyword(.let)
145145
let propertyName = identifierPattern.identifier.text
146+
let typeName = variableDecl.typeSyntax!.trimmed
146147
let backingName = "_backing_" + propertyName
147148
let hasWillSet = variableDecl.willSetBlock != nil
148149

@@ -157,12 +158,23 @@ extension COWTrackingPropertyMacro: AccessorMacro {
157158

158159
let readAccessor = AccessorDeclSyntax(
159160
"""
160-
_read {
161-
(\(raw: backingName).value as? TrackingObject)?._tracking_context.path = _tracking_context.path?.pushed(.init("\(raw: propertyName)"))
162-
_Tracking._tracking_modifyStorage {
163-
$0.accessorRead(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
161+
get {
162+
163+
let component = PropertyPath.Component.init("\(raw: propertyName)")
164+
_tracking_context.trackingResultRef?.accessorRead(path: _tracking_context.path?.pushed(component))
165+
166+
if var value = \(raw: backingName).value as? TrackingObject, let ref = _tracking_context.trackingResultRef {
167+
168+
if value._tracking_context.trackingResultRef !== ref {
169+
value._tracking_context = _TrackingContext(trackingResultRef: ref)
170+
}
171+
172+
value._tracking_context.path = _tracking_context.path?.pushed(component)
173+
174+
return value as! \(typeName)
164175
}
165-
yield \(raw: backingName).value
176+
177+
return \(raw: backingName).value
166178
}
167179
"""
168180
)
@@ -174,10 +186,10 @@ extension COWTrackingPropertyMacro: AccessorMacro {
174186
// willset
175187
\(variableDecl.makeWillSetDoBlock())
176188
177-
(\(raw: backingName).value as? TrackingObject)?._tracking_context.path = _tracking_context.path?.pushed(.init("\(raw: propertyName)"))
178-
_Tracking._tracking_modifyStorage {
179-
$0.accessorSet(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
189+
if let ref = _tracking_context.trackingResultRef {
190+
ref.accessorSet(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
180191
}
192+
181193
if !isKnownUniquelyReferenced(&\(raw: backingName)) {
182194
\(raw: backingName) = .init(newValue)
183195
} else {
@@ -193,15 +205,34 @@ extension COWTrackingPropertyMacro: AccessorMacro {
193205
let modifyAccessor = AccessorDeclSyntax(
194206
"""
195207
_modify {
196-
(\(raw: backingName).value as? TrackingObject)?._tracking_context.path = _tracking_context.path?.pushed(.init("\(raw: propertyName)"))
197-
_Tracking._tracking_modifyStorage {
198-
$0.accessorModify(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
208+
209+
if let ref = _tracking_context.trackingResultRef {
210+
ref.accessorModify(path: _tracking_context.path?.pushed(.init("\(raw: propertyName)")))
199211
}
200-
if !isKnownUniquelyReferenced(&\(raw: backingName)) {
212+
213+
if !isKnownUniquelyReferenced(&\(raw: backingName)) {
201214
\(raw: backingName) = .init(\(raw: backingName).value)
202215
}
203-
yield &\(raw: backingName).value
204-
216+
217+
let oldValue = \(raw: backingName).value
218+
219+
if var value = \(raw: backingName).value as? TrackingObject,
220+
let ref = _tracking_context.trackingResultRef {
221+
222+
let component = PropertyPath.Component.init("\(raw: propertyName)")
223+
224+
if value._tracking_context.trackingResultRef !== ref {
225+
value._tracking_context = _TrackingContext(trackingResultRef: ref)
226+
}
227+
value._tracking_context.path = _tracking_context.path?.pushed(component)
228+
229+
\(raw: backingName).value = value as! \(typeName)
230+
231+
yield &\(raw: backingName).value
232+
} else {
233+
yield &\(raw: backingName).value
234+
}
235+
205236
// didSet
206237
\(variableDecl.makeDidSetDoBlock())
207238
}

0 commit comments

Comments
 (0)