Skip to content

Commit 2a46d70

Browse files
committed
Update
1 parent 8e9c94b commit 2a46d70

File tree

6 files changed

+208
-30
lines changed

6 files changed

+208
-30
lines changed

Sources/StructTransaction/Source.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class Hoge {
8989

9090
var stored_2: Int
9191

92+
var name = ""
93+
9294
init(stored: Int) {
9395
self.stored = stored
9496
self.stored_2 = stored

Sources/StructTransaction/Tracking.swift

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import Foundation
33
public struct Storage: Equatable {
44

55
public struct Identifier: Hashable {
6-
public let name: String
6+
public let keyPath: AnyKeyPath
77

8-
public init(name: String) {
9-
self.name = name
8+
public init(keyPath: AnyKeyPath) {
9+
self.keyPath = keyPath
1010
}
1111
}
1212

@@ -22,23 +22,106 @@ public struct Storage: Equatable {
2222
}
2323
}
2424

25-
public func _tracking_modifyStorage(_ modifier: (inout Storage) -> Void) {
26-
guard Thread.current.threadDictionary["tracking"] != nil else {
27-
return
25+
private enum ThreadDictionaryKey {
26+
case tracking
27+
case currentKeyPathStack
28+
}
29+
30+
extension NSMutableDictionary {
31+
fileprivate var currentKeyPathStack: Tracking.KeyPathStack? {
32+
get {
33+
self[ThreadDictionaryKey.currentKeyPathStack] as? Tracking.KeyPathStack
34+
}
35+
set {
36+
self[ThreadDictionaryKey.currentKeyPathStack] = newValue
37+
}
38+
}
39+
40+
fileprivate var tracking: Storage? {
41+
get {
42+
self[ThreadDictionaryKey.tracking] as? Storage
43+
}
44+
set {
45+
self[ThreadDictionaryKey.tracking] = newValue
46+
}
47+
}
48+
}
49+
50+
public enum Tracking {
51+
52+
public static func _pushKeyPath(_ keyPath: AnyKeyPath) {
53+
if Thread.current.threadDictionary.currentKeyPathStack == nil {
54+
Thread.current.threadDictionary.currentKeyPathStack = KeyPathStack()
55+
}
56+
57+
Thread.current.threadDictionary.currentKeyPathStack?.push(
58+
keyPath
59+
)
60+
}
61+
62+
public static func _currentKeyPath(_ keyPath: AnyKeyPath) -> AnyKeyPath? {
63+
guard let current = Thread.current.threadDictionary.currentKeyPathStack else {
64+
return keyPath
65+
}
66+
return current.currentKeyPath()?.appending(path: keyPath)
67+
}
68+
69+
public static func _popKeyPath() {
70+
Thread.current.threadDictionary.currentKeyPathStack?.pop()
71+
}
72+
73+
fileprivate struct KeyPathStack: Equatable {
74+
75+
var stack: [AnyKeyPath]
76+
77+
init() {
78+
stack = []
79+
}
80+
81+
mutating func push(_ keyPath: AnyKeyPath) {
82+
stack.append(keyPath)
83+
}
84+
85+
mutating func pop() {
86+
stack.removeLast()
87+
}
88+
89+
func currentKeyPath() -> AnyKeyPath? {
90+
91+
guard var keyPath = stack.first else {
92+
return nil
93+
}
94+
95+
for component in stack.dropFirst() {
96+
guard let appended = keyPath.appending(path: component) else {
97+
return nil
98+
}
99+
100+
keyPath = appended
101+
}
102+
103+
return keyPath
104+
}
105+
106+
107+
}
108+
109+
public static func _tracking_modifyStorage(_ modifier: (inout Storage) -> Void) {
110+
guard Thread.current.threadDictionary.tracking != nil else {
111+
return
112+
}
113+
modifier(&Thread.current.threadDictionary.tracking!)
28114
}
29-
var storage = Thread.current.threadDictionary["tracking"] as! Storage
30-
modifier(&storage)
31-
Thread.current.threadDictionary["tracking"] = storage
32115
}
33116

34117
public func withTracking(_ perform: () -> Void) -> Storage {
35-
let current = Thread.current.threadDictionary["tracking"] as? Storage
118+
let current = Thread.current.threadDictionary.tracking
36119
defer {
37-
Thread.current.threadDictionary["tracking"] = current
120+
Thread.current.threadDictionary.tracking = current
38121
}
39122

40-
Thread.current.threadDictionary["tracking"] = Storage()
123+
Thread.current.threadDictionary.tracking = Storage()
41124
perform()
42-
let result = Thread.current.threadDictionary["tracking"] as! Storage
125+
let result = Thread.current.threadDictionary.tracking!
43126
return result
44127
}

Sources/StructTransactionMacros/COWTrackingPropertyMacro.swift

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import SwiftCompilerPlugin
22
import SwiftSyntax
33
import SwiftSyntaxBuilder
44
import SwiftSyntaxMacros
5+
import SwiftSyntaxMacroExpansion
56

67
public struct COWTrackingPropertyMacro {
78

8-
public enum Error: Swift.Error {
9-
10-
}
119
}
1210

1311
extension COWTrackingPropertyMacro: PeerMacro {
@@ -21,6 +19,13 @@ extension COWTrackingPropertyMacro: PeerMacro {
2119
return []
2220
}
2321

22+
// for binding in variableDecl.bindings {
23+
// guard binding.typeAnnotation != nil else {
24+
// context.diagnose(.init(node: node, message: MacroExpansionErrorMessage("Property needs type annotation")))
25+
// return []
26+
// }
27+
// }
28+
2429
var newMembers: [DeclSyntax] = []
2530

2631
let ignoreMacroAttached = variableDecl.attributes.contains {
@@ -53,7 +58,7 @@ extension COWTrackingPropertyMacro: PeerMacro {
5358
return "_Backing_COW_Storage<\(type.trimmed)>"
5459
})
5560
.modifyingInit({ initializer in
56-
return .init(value: ".init(\(initializer.value))" as ExprSyntax)
61+
return .init(value: "_Backing_COW_Storage.init(\(initializer.value))" as ExprSyntax)
5762
})
5863

5964
_variableDecl.leadingTrivia = .spaces(2)
@@ -92,12 +97,35 @@ extension COWTrackingPropertyMacro: AccessorMacro {
9297
}
9398
"""
9499
)
100+
101+
let readAccessor = AccessorDeclSyntax(
102+
"""
103+
_read {
104+
let keyPath = \\Self.\(raw: propertyName)
105+
let currentKeyPath = Tracking._currentKeyPath(keyPath) ?? keyPath
106+
Tracking._pushKeyPath(keyPath)
107+
108+
Tracking._tracking_modifyStorage {
109+
$0.read(identifier: .init(keyPath: currentKeyPath))
110+
}
111+
yield \(raw: backingName).value
112+
113+
Tracking._popKeyPath()
114+
}
115+
"""
116+
)
95117

96118
let getAccessor = AccessorDeclSyntax(
97119
"""
98120
get {
99-
_tracking_modifyStorage {
100-
$0.read(identifier: .init(name: "\(raw: propertyName)"))
121+
let keyPath = \\Self.\(raw: propertyName)
122+
let currentKeyPath = Tracking._currentKeyPath(keyPath) ?? keyPath
123+
Tracking._pushKeyPath(keyPath)
124+
defer {
125+
Tracking._popKeyPath()
126+
}
127+
Tracking._tracking_modifyStorage {
128+
$0.read(identifier: .init(keyPath: currentKeyPath))
101129
}
102130
return \(raw: backingName).value
103131
}
@@ -107,8 +135,14 @@ extension COWTrackingPropertyMacro: AccessorMacro {
107135
let setAccessor = AccessorDeclSyntax(
108136
"""
109137
set {
110-
_tracking_modifyStorage {
111-
$0.write(identifier: .init(name: "\(raw: propertyName)"))
138+
let keyPath = \\Self.\(raw: propertyName)
139+
let currentKeyPath = Tracking._currentKeyPath(keyPath) ?? keyPath
140+
Tracking._pushKeyPath(keyPath)
141+
defer {
142+
Tracking._popKeyPath()
143+
}
144+
Tracking._tracking_modifyStorage {
145+
$0.write(identifier: .init(keyPath: currentKeyPath))
112146
}
113147
if !isKnownUniquelyReferenced(&\(raw: backingName)) {
114148
\(raw: backingName) = .init(newValue)
@@ -122,8 +156,14 @@ extension COWTrackingPropertyMacro: AccessorMacro {
122156
let modifyAccessor = AccessorDeclSyntax(
123157
"""
124158
_modify {
125-
_tracking_modifyStorage {
126-
$0.write(identifier: .init(name: "\(raw: propertyName)"))
159+
let keyPath = \\Self.\(raw: propertyName)
160+
let currentKeyPath = Tracking._currentKeyPath(keyPath) ?? keyPath
161+
Tracking._pushKeyPath(keyPath)
162+
defer {
163+
Tracking._popKeyPath()
164+
}
165+
Tracking._tracking_modifyStorage {
166+
$0.write(identifier: .init(keyPath: currentKeyPath))
127167
}
128168
if !isKnownUniquelyReferenced(&\(raw: backingName)) {
129169
\(raw: backingName) = .init(\(raw: backingName).value)
@@ -136,13 +176,15 @@ extension COWTrackingPropertyMacro: AccessorMacro {
136176
if binding.initializer == nil {
137177
return [
138178
initAccessor,
139-
getAccessor,
179+
readAccessor,
180+
// getAccessor,
140181
setAccessor,
141182
modifyAccessor,
142183
]
143184
} else {
144185
return [
145-
getAccessor,
186+
readAccessor,
187+
// getAccessor,
146188
setAccessor,
147189
modifyAccessor,
148190
]

Tests/StructTransactionMacroTests/COWTrackingProperyMacroTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ final class COWTrackingProperyMacroTests: XCTestCase {
88

99
override func invokeTest() {
1010
withMacroTesting(
11-
isRecording: false,
11+
isRecording: true,
1212
macros: [
1313
"COWTrackingProperty": COWTrackingPropertyMacro.self,
1414
"TrackingIgnored": TrackingIgnoredMacro.self,
@@ -48,7 +48,7 @@ final class COWTrackingProperyMacroTests: XCTestCase {
4848
$0.write(identifier: .init(name: "stored_0"))
4949
}
5050
if !isKnownUniquelyReferenced(&_backing_stored_0) {
51-
_backing_stored_0 = .init(_backing_stored_0.value)
51+
_backing_stored_0 = .init(newValue)
5252
} else {
5353
_backing_stored_0.value = newValue
5454
}
@@ -63,7 +63,7 @@ final class COWTrackingProperyMacroTests: XCTestCase {
6363
yield &_backing_stored_0.value
6464
}
6565
}
66-
private var _backing_stored_0: _Backing_COW_Storage<Int> = .init(18)
66+
private var _backing_stored_0: _Backing_COW_Storage<Int> = _Backing_COW_Storage.init(18)
6767
6868
func compute() {
6969
}

Tests/StructTransactionTests/MyState.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ struct MyState {
3737
var nested: Nested = .init(name: "hello")
3838
var nestedAttached: NestedAttached = .init(name: "")
3939

40+
@Tracking
4041
struct Nested {
42+
43+
init(name: String) {
44+
self.name = name
45+
}
46+
4147
var name = ""
4248
}
4349

Tests/StructTransactionTests/Tests.swift

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,54 @@ struct Tests {
1313
original.height = 100
1414
}
1515

16-
#expect(result.writeIdentifiers.contains(.init(name: "height")))
16+
#expect(result.writeIdentifiers.contains(.init(keyPath: \MyState.height)))
1717

1818
}
19+
20+
@Test
21+
func tracking_write_nested_stored_property() {
22+
23+
var original = MyState.init()
24+
25+
let result = withTracking {
26+
original.nested.name = "AAA"
27+
}
28+
29+
#expect(result.writeIdentifiers.contains(.init(keyPath: \MyState.nested)))
30+
#expect(result.writeIdentifiers.contains(.init(keyPath: \MyState.nested.name)))
31+
32+
}
33+
34+
@Test
35+
func tracking_read_nested_stored_property() {
36+
37+
let original = MyState.init()
38+
39+
let result = withTracking {
40+
_ = original.nested.name
41+
}
42+
43+
#expect(result.readIdentifiers.contains(.init(keyPath: \MyState.nested)))
44+
#expect(result.readIdentifiers.contains(.init(keyPath: \MyState.nested.name)))
45+
46+
}
47+
48+
@Test
49+
func modify_endpoint() {
50+
51+
var original = MyState.init()
52+
53+
func update(_ value: inout String) {
54+
value = "AAA"
55+
}
56+
57+
let result = withTracking {
58+
update(&original.nested.name)
59+
}
60+
61+
#expect(result.writeIdentifiers.contains(.init(keyPath: \MyState.nested)))
62+
#expect(result.writeIdentifiers.contains(.init(keyPath: \MyState.nested.name)))
63+
}
1964

2065
@Test
2166
func tracking_computed_property() {
@@ -26,7 +71,7 @@ struct Tests {
2671
let _ = original.computedName
2772
}
2873

29-
#expect(result.readIdentifiers.contains(.init(name: "name")))
74+
#expect(result.readIdentifiers.contains(.init(keyPath: \MyState.name)))
3075

3176
}
3277

0 commit comments

Comments
 (0)