Skip to content

Commit c6a2437

Browse files
authored
[Observation] Exclude generic class types from pre-caching their keypaths (#78790)
* [Observation] Exclude generic class types from pre-caching their keypaths * Account for multiple nesting levels of generics for observation cached keypaths
1 parent 8d3b0d0 commit c6a2437

File tree

3 files changed

+138
-41
lines changed

3 files changed

+138
-41
lines changed

lib/Macros/Sources/ObservationMacros/Extensions.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ import SwiftSyntaxMacros
1414
import SwiftDiagnostics
1515
import SwiftSyntaxBuilder
1616

17+
extension Syntax {
18+
var isNonGeneric: Bool {
19+
if let classDecl = self.as(ClassDeclSyntax.self) {
20+
if classDecl.genericParameterClause == nil { return true }
21+
} else if let structDecl = self.as(StructDeclSyntax.self) {
22+
if structDecl.genericParameterClause == nil { return true }
23+
} else if let enumDecl = self.as(EnumDeclSyntax.self) {
24+
if enumDecl.genericParameterClause == nil { return true }
25+
} else if let actorDecl = self.as(ActorDeclSyntax.self) {
26+
if actorDecl.genericParameterClause == nil { return true }
27+
}
28+
return false
29+
}
30+
}
31+
1732
extension VariableDeclSyntax {
1833
var identifierPattern: IdentifierPatternSyntax? {
1934
bindings.first?.pattern.as(IdentifierPatternSyntax.self)

lib/Macros/Sources/ObservationMacros/ObservableMacro.swift

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ public struct ObservableMacro {
101101
"""
102102
}
103103

104+
static func canCacheKeyPaths(_ lexicalContext: [Syntax]) -> Bool {
105+
lexicalContext.allSatisfy { $0.isNonGeneric }
106+
}
107+
104108
static var ignoredAttribute: AttributeSyntax {
105109
AttributeSyntax(
106110
leadingTrivia: .space,
@@ -353,47 +357,88 @@ public struct ObservationTrackedMacro: AccessorMacro {
353357
_\(identifier) = initialValue
354358
}
355359
"""
360+
if ObservableMacro.canCacheKeyPaths(context.lexicalContext) {
361+
let getAccessor: AccessorDeclSyntax =
362+
"""
363+
get {
364+
access(keyPath: \(container.trimmed.name)._cachedKeypath_\(identifier))
365+
return _\(identifier)
366+
}
367+
"""
356368

357-
let getAccessor: AccessorDeclSyntax =
358-
"""
359-
get {
360-
access(keyPath: \(container.trimmed.name)._cachedKeypath_\(identifier))
361-
return _\(identifier)
362-
}
363-
"""
369+
let setAccessor: AccessorDeclSyntax =
370+
"""
371+
set {
372+
guard shouldNotifyObservers(_\(identifier), newValue) else {
373+
return
374+
}
375+
withMutation(keyPath: \(container.trimmed.name)._cachedKeypath_\(identifier)) {
376+
_\(identifier) = newValue
377+
}
378+
}
379+
"""
380+
381+
// Note: this accessor cannot test the equality since it would incur
382+
// additional CoW's on structural types. Most mutations in-place do
383+
// not leave the value equal so this is "fine"-ish.
384+
// Warning to future maintence: adding equality checks here can make
385+
// container mutation O(N) instead of O(1).
386+
// e.g. observable.array.append(element) should just emit a change
387+
// to the new array, and NOT cause a copy of each element of the
388+
// array to an entirely new array.
389+
let modifyAccessor: AccessorDeclSyntax =
390+
"""
391+
_modify {
392+
let keyPath = \(container.trimmed.name)._cachedKeypath_\(identifier)
393+
access(keyPath: keyPath)
394+
\(raw: ObservableMacro.registrarVariableName).willSet(self, keyPath: keyPath)
395+
defer { \(raw: ObservableMacro.registrarVariableName).didSet(self, keyPath: keyPath) }
396+
yield &_\(identifier)
397+
}
398+
"""
364399

365-
let setAccessor: AccessorDeclSyntax =
366-
"""
367-
set {
368-
guard shouldNotifyObservers(_\(identifier), newValue) else {
369-
return
400+
return [initAccessor, getAccessor, setAccessor, modifyAccessor]
401+
} else {
402+
let getAccessor: AccessorDeclSyntax =
403+
"""
404+
get {
405+
access(keyPath: \\.\(identifier))
406+
return _\(identifier)
407+
}
408+
"""
409+
410+
let setAccessor: AccessorDeclSyntax =
411+
"""
412+
set {
413+
guard shouldNotifyObservers(_\(identifier), newValue) else {
414+
return
415+
}
416+
withMutation(keyPath: \\.\(identifier)) {
417+
_\(identifier) = newValue
418+
}
370419
}
371-
withMutation(keyPath: \(container.trimmed.name)._cachedKeypath_\(identifier)) {
372-
_\(identifier) = newValue
420+
"""
421+
422+
// Note: this accessor cannot test the equality since it would incur
423+
// additional CoW's on structural types. Most mutations in-place do
424+
// not leave the value equal so this is "fine"-ish.
425+
// Warning to future maintence: adding equality checks here can make
426+
// container mutation O(N) instead of O(1).
427+
// e.g. observable.array.append(element) should just emit a change
428+
// to the new array, and NOT cause a copy of each element of the
429+
// array to an entirely new array.
430+
let modifyAccessor: AccessorDeclSyntax =
431+
"""
432+
_modify {
433+
access(keyPath: \\.\(identifier))
434+
\(raw: ObservableMacro.registrarVariableName).willSet(self, keyPath: \\.\(identifier))
435+
defer { \(raw: ObservableMacro.registrarVariableName).didSet(self, keyPath: \\.\(identifier)) }
436+
yield &_\(identifier)
373437
}
374-
}
375-
"""
376-
377-
// Note: this accessor cannot test the equality since it would incur
378-
// additional CoW's on structural types. Most mutations in-place do
379-
// not leave the value equal so this is "fine"-ish.
380-
// Warning to future maintence: adding equality checks here can make
381-
// container mutation O(N) instead of O(1).
382-
// e.g. observable.array.append(element) should just emit a change
383-
// to the new array, and NOT cause a copy of each element of the
384-
// array to an entirely new array.
385-
let modifyAccessor: AccessorDeclSyntax =
386-
"""
387-
_modify {
388-
let keyPath = \(container.trimmed.name)._cachedKeypath_\(identifier)
389-
access(keyPath: keyPath)
390-
\(raw: ObservableMacro.registrarVariableName).willSet(self, keyPath: keyPath)
391-
defer { \(raw: ObservableMacro.registrarVariableName).didSet(self, keyPath: keyPath) }
392-
yield &_\(identifier)
393-
}
394-
"""
438+
"""
395439

396-
return [initAccessor, getAccessor, setAccessor, modifyAccessor]
440+
return [initAccessor, getAccessor, setAccessor, modifyAccessor]
441+
}
397442
}
398443
}
399444

@@ -422,11 +467,15 @@ extension ObservationTrackedMacro: PeerMacro {
422467
}
423468

424469
let storage = DeclSyntax(property.privatePrefixed("_", addingAttribute: ObservableMacro.ignoredAttribute))
425-
let cachedKeypath: DeclSyntax =
426-
"""
427-
private static let _cachedKeypath_\(identifier) = \\\(container.name).\(identifier)
428-
"""
429-
return [storage, cachedKeypath]
470+
if ObservableMacro.canCacheKeyPaths(context.lexicalContext) {
471+
let cachedKeypath: DeclSyntax =
472+
"""
473+
private static let _cachedKeypath_\(identifier) = \\\(container.name).\(identifier)
474+
"""
475+
return [storage, cachedKeypath]
476+
} else {
477+
return [storage]
478+
}
430479
}
431480
}
432481

test/stdlib/Observation/Observable.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,39 @@ class RecursiveInner {
185185
var value = "prefix"
186186
}
187187

188+
@Observable
189+
class GenericClass<T> {
190+
var value = 3
191+
}
192+
193+
struct StructParent {
194+
@Observable
195+
class NestedGenericClass<T> {
196+
var value = 3
197+
}
198+
}
199+
200+
struct GenericStructParent<T> {
201+
@Observable
202+
class NestedClass {
203+
var value = 3
204+
}
205+
}
206+
207+
class ClassParent {
208+
@Observable
209+
class NestedGenericClass<T> {
210+
var value = 3
211+
}
212+
}
213+
214+
class GenericClassParent<T> {
215+
@Observable
216+
class NestedClass {
217+
var value = 3
218+
}
219+
}
220+
188221
@Observable
189222
class RecursiveOuter {
190223
var inner = RecursiveInner()

0 commit comments

Comments
 (0)