diff --git a/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift b/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift index 81461ae0aead6..47a092e558ee9 100644 --- a/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift +++ b/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift @@ -51,17 +51,22 @@ public struct ObservableMacro: MemberMacro, MemberAttributeMacro, ConformanceMac let _registrar = ObservationRegistrar<\(parentName)>() """ - let transactions: DeclSyntax = + let changes: DeclSyntax = """ - public nonisolated func transactions(for properties: TrackedProperties<\(parentName)>, isolation: Delivery) -> ObservedTransactions<\(parentName), Delivery> where Delivery: Actor { - _registrar.transactions(for: properties, isolation: isolation) + public nonisolated func changes( + for properties: TrackedProperties<\(parentName)>, + isolatedTo isolation: Isolation + ) -> ObservedChanges<\(parentName), Isolation> { + _registrar.changes(for: properties, isolatedTo: isolation) } """ - let changes: DeclSyntax = + let values: DeclSyntax = """ - public nonisolated func changes(for keyPath: KeyPath<\(parentName), Member>) -> ObservedChanges<\(parentName), Member> where Member: Sendable { - _registrar.changes(for: keyPath) + public nonisolated func values( + for keyPath: KeyPath<\(parentName), Member> + ) -> ObservedValues<\(parentName), Member> { + _registrar.values(for: keyPath) } """ @@ -85,8 +90,8 @@ public struct ObservableMacro: MemberMacro, MemberAttributeMacro, ConformanceMac return [ registrar, - transactions, changes, + values, storageStruct, storage, ] diff --git a/stdlib/public/Observation/Sources/Observation/CMakeLists.txt b/stdlib/public/Observation/Sources/Observation/CMakeLists.txt index dc17b06f16f74..6fb3f8b11131b 100644 --- a/stdlib/public/Observation/Sources/Observation/CMakeLists.txt +++ b/stdlib/public/Observation/Sources/Observation/CMakeLists.txt @@ -12,15 +12,16 @@ list(APPEND swift_runtime_library_compile_flags -I${SWIFT_SOURCE_DIR}/stdlib/include -I${SWIFT_SOURCE_DIR}/include) -add_swift_target_library(swiftObservation ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS_STDLIB +add_swift_target_library(swift_Observation ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS_STDLIB Locking.cpp Locking.swift Macros.swift Observable.swift ObservationRegistrar.swift + ObservationRegistrarStateMachine.swift ObservationTracking.swift ObservedChanges.swift - ObservedTransactions.swift + ObservedValues.swift ThreadLocal.cpp ThreadLocal.swift TrackedProperties.swift diff --git a/stdlib/public/Observation/Sources/Observation/Locking.cpp b/stdlib/public/Observation/Sources/Observation/Locking.cpp index 3e3f6af686750..520fc07d17e92 100644 --- a/stdlib/public/Observation/Sources/Observation/Locking.cpp +++ b/stdlib/public/Observation/Sources/Observation/Locking.cpp @@ -15,7 +15,7 @@ using namespace swift; -extern "C" SWIFT_CC(swift) +extern "C" SWIFT_CC(swift) __attribute__((visibility("hidden"))) size_t _swift_observation_lock_size() { size_t bytes = sizeof(Mutex); @@ -26,17 +26,17 @@ size_t _swift_observation_lock_size() { return bytes; } -extern "C" SWIFT_CC(swift) +extern "C" SWIFT_CC(swift) __attribute__((visibility("hidden"))) void _swift_observation_lock_init(Mutex &lock) { new (&lock) Mutex(); } -extern "C" SWIFT_CC(swift) +extern "C" SWIFT_CC(swift) __attribute__((visibility("hidden"))) void _swift_observation_lock_lock(Mutex &lock) { lock.lock(); } -extern "C" SWIFT_CC(swift) +extern "C" SWIFT_CC(swift) __attribute__((visibility("hidden"))) void _swift_observation_lock_unlock(Mutex &lock) { lock.unlock(); } diff --git a/stdlib/public/Observation/Sources/Observation/Locking.swift b/stdlib/public/Observation/Sources/Observation/Locking.swift index 998a8315326b8..89d87299e06fc 100644 --- a/stdlib/public/Observation/Sources/Observation/Locking.swift +++ b/stdlib/public/Observation/Sources/Observation/Locking.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Copyright (c) 2023 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information diff --git a/stdlib/public/Observation/Sources/Observation/Macros.swift b/stdlib/public/Observation/Sources/Observation/Macros.swift index 40c1f2fc5e422..3d5ffbb54c6d8 100644 --- a/stdlib/public/Observation/Sources/Observation/Macros.swift +++ b/stdlib/public/Observation/Sources/Observation/Macros.swift @@ -12,7 +12,7 @@ #if $Macros && hasAttribute(attached) @available(SwiftStdlib 5.9, *) -@attached(member, names: named(_registrar), named(transactions), named(changes), named(_Storage), named(_storage)) +@attached(member, names: named(_registrar), named(changes), named(values), named(_Storage), named(_storage)) @attached(memberAttribute) @attached(conformance) public macro Observable() = diff --git a/stdlib/public/Observation/Sources/Observation/Observable.swift b/stdlib/public/Observation/Sources/Observation/Observable.swift index cca8714aef875..99dcc49b844fa 100644 --- a/stdlib/public/Observation/Sources/Observation/Observable.swift +++ b/stdlib/public/Observation/Sources/Observation/Observable.swift @@ -13,14 +13,14 @@ import _Concurrency @available(SwiftStdlib 5.9, *) public protocol Observable { - nonisolated func transactions( + nonisolated func changes( for properties: TrackedProperties, - isolation: Delivery - ) -> ObservedTransactions + isolatedTo isolation: Isolation + ) -> ObservedChanges - nonisolated func changes( + nonisolated func values( for keyPath: KeyPath - ) -> ObservedChanges + ) -> ObservedValues nonisolated static func dependencies( of keyPath: PartialKeyPath @@ -30,23 +30,23 @@ public protocol Observable { @available(SwiftStdlib 5.9, *) extension Observable { - public nonisolated func transactions( + public nonisolated func changes( for keyPath: KeyPath, - isolation: Delivery - ) -> ObservedTransactions { - transactions(for: [keyPath], isolation: isolation) + isolatedTo isolation: Isolation + ) -> ObservedChanges { + changes(for: [keyPath], isolatedTo: isolation) } - public nonisolated func transactions( + public nonisolated func changes( for properties: TrackedProperties - ) -> ObservedTransactions { - transactions(for: properties, isolation: MainActor.shared) + ) -> ObservedChanges { + changes(for: properties, isolatedTo: MainActor.shared) } - public nonisolated func transactions( + public nonisolated func changes( for keyPath: KeyPath - ) -> ObservedTransactions { - transactions(for: [keyPath], isolation: MainActor.shared) + ) -> ObservedChanges { + changes(for: [keyPath], isolatedTo: MainActor.shared) } public nonisolated static func dependencies( diff --git a/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift b/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift index 0f1a617df0249..028b01ce421b2 100644 --- a/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift +++ b/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift @@ -13,18 +13,11 @@ import _Concurrency @available(SwiftStdlib 5.9, *) public struct ObservationRegistrar: Sendable { - fileprivate let context = Context() - private let lifetime: Lifetime - - public init() { - lifetime = Lifetime(state: context.state) - } - internal struct Context: Identifiable { fileprivate let state = _ManagedCriticalState(managing: State()) internal var id: ObjectIdentifier { state.id } - + fileprivate init() { } } @@ -39,46 +32,17 @@ public struct ObservationRegistrar: Sendable { state.withCriticalRegion { $0.deinitialize() } } } - - fileprivate enum Phase { - case willSet - case didSet - case complete - } - - fileprivate enum ResumeAction { - case resumeAndRemove(UnsafeContinuation) - case informAndRemove(@Sendable () -> Void) - } - - fileprivate struct Next { - enum Kind { - case transaction(TrackedProperties) - case pendingTransaction(TrackedProperties, UnsafeContinuation?, Never>) - case change(TrackedProperties, UnsafeContinuation) - case tracking(TrackedProperties, @Sendable () -> Void) - case cancelled - } - - fileprivate var kind: Kind - fileprivate var collected: TrackedProperties? - } - - fileprivate struct State: @unchecked Sendable { - fileprivate var generation = 0 - fileprivate var nexts = [Int: Next]() - fileprivate var lookups = [PartialKeyPath: Set]() - fileprivate var terminal = false - fileprivate init() { } + fileprivate let context = Context() + private let lifetime: Lifetime + + public init() { + lifetime = Lifetime(state: context.state) } -} -@available(SwiftStdlib 5.9, *) -extension ObservationRegistrar { public func access( - _ subject: Subject, - keyPath: KeyPath + _ subject: Subject, + keyPath: KeyPath ) { if let trackingPtr = _ThreadLocal.value? .assumingMemoryBound(to: ObservationTracking._AccessList?.self) { @@ -90,29 +54,40 @@ extension ObservationRegistrar { } public func willSet( - _ subject: Subject, - keyPath: KeyPath + _ subject: Subject, + keyPath: KeyPath ) { - let observers = context.state.withCriticalRegion { state in + let action = context.state.withCriticalRegion { state in state.willSet(subject, keyPath: keyPath) } - for observer in observers { - observer() + if let action { + switch action { + case .tracking(let observers): + for observer in observers { + observer() + } + } } } public func didSet( - _ subject: Subject, - keyPath: KeyPath + _ subject: Subject, + keyPath: KeyPath ) { - context.state.withCriticalRegion { state in + let actions = context.state.withCriticalRegion { state in state.didSet(subject, keyPath: keyPath) } + for action in actions { + switch action { + case .resumeWithMember(let continuation): + continuation.resume(returning: subject[keyPath: keyPath]) + } + } } public func withMutation( - of subject: Subject, - keyPath: KeyPath, + of subject: Subject, + keyPath: KeyPath, _ mutation: () throws -> T ) rethrows -> T { willSet(subject, keyPath: keyPath) @@ -120,361 +95,404 @@ extension ObservationRegistrar { return try mutation() } - public func transactions( - for properties: TrackedProperties, - isolation: Delivery - ) -> ObservedTransactions { - ObservedTransactions(context, properties: properties, isolation: isolation) + public func changes( + for properties: TrackedProperties, + isolatedTo isolation: Isolation + ) -> ObservedChanges { + ObservedChanges(context, properties: properties, isolation: isolation) } - public func changes( + public func values( for keyPath: KeyPath - ) -> ObservedChanges { - ObservedChanges(context, keyPath: keyPath) + ) -> ObservedValues { + ObservedValues(context, keyPath: keyPath) } } @available(SwiftStdlib 5.9, *) -extension ObservationRegistrar.Context { - internal func schedule( - _ generation: Int, - isolation: isolated Delivery - ) async -> TrackedProperties? { - return await withUnsafeContinuation { continuation in - state.withCriticalRegion { state in - state.complete(generation, continuation: continuation) - } - } - } - - internal func nextTransaction( - for properties: TrackedProperties, - isolation: Delivery - ) async -> TrackedProperties? { - let generation = state.withCriticalRegion { state in - let generation = state.nextGeneration() - state.insert(transaction: properties, generation: generation) - return generation - } - return await withTaskCancellationHandler { - return await schedule(generation, isolation: isolation) - } onCancel: { - state.withCriticalRegion { $0.cancel(generation) } - } - } - - internal func nextChange( - to keyPath: KeyPath, - properties: TrackedProperties - ) async -> Member? { - let generation = state.withCriticalRegion { $0.nextGeneration() } - let subject: Subject? = await withTaskCancellationHandler { - await withUnsafeContinuation { continuation in - state.withCriticalRegion { state in - state.insert(change: properties, - continuation: continuation, generation: generation) - } - } - } onCancel: { - state.withCriticalRegion { $0.cancel(generation) } - } - return subject.map { $0[keyPath: keyPath] } - } - - internal func nextTracking( - for properties: TrackedProperties, - _ observer: @Sendable @escaping () -> Void - ) -> Int { - return state.withCriticalRegion { state in - let generation = state.nextGeneration() - state.insert(properties: properties, - tracking: observer, generation: generation) - return generation - } - } - - internal func cancel(_ generation: Int) { - state.withCriticalRegion { $0.cancel(generation) } - } -} - -@available(SwiftStdlib 5.9, *) -extension ObservationRegistrar.Next { - fileprivate mutating func resume( - keyPath: PartialKeyPath, - phase: ObservationRegistrar.Phase - ) -> ObservationRegistrar.ResumeAction? { - kind.resume(keyPath: keyPath, phase: phase, collected: &collected) - } - - fileprivate func remove( - from lookup: inout [PartialKeyPath: Set], - generation: Int - ) { - kind.remove(from: &lookup, generation: generation) - } - - fileprivate func deinitialize() { - kind.deinitialize() +extension ObservationRegistrar { + enum ObservationKind { + case transactions + case changes } } @available(SwiftStdlib 5.9, *) -extension ObservationRegistrar.Next.Kind { - fileprivate mutating func resume( - keyPath: PartialKeyPath, - phase: ObservationRegistrar.Phase, - collected: inout TrackedProperties? - ) -> ObservationRegistrar.ResumeAction? { - switch (self, phase) { - case (.transaction(let observedTrackedProperties), .willSet): - if observedTrackedProperties.contains(keyPath) { - if var properties = collected { - properties.insert(keyPath) - collected = properties - } else { - var properties = TrackedProperties() - properties.insert(keyPath) - collected = properties - } +extension ObservationRegistrar { + struct State: @unchecked Sendable { + enum Observation { + case idleChanges(IdleChangesState) + case pendingChange(PendingChangeState) + case activeChange(ActiveChangeState) + case idleValues(IdleValuesState) + case pendingValue(PendingValueState) + case activeValue(ActiveValueState) + case tracking(TrackingState) + case cancelled + } + + var id: Int = 0 + var observations = [Int: Observation]() + var lookups = [PartialKeyPath: Set]() + + mutating func insert( + _ kind: ObservationRegistrar.ObservationKind, + properties: TrackedProperties + ) -> Int { + let id = self.id + self.id = id + 1 + switch kind { + case .transactions: + observations[id] = Observation(idleChanges: properties) + case .changes: + observations[id] = Observation(idleValues: properties) } - return nil - case (.pendingTransaction(let observedTrackedProperties, let continuation), .willSet): - if observedTrackedProperties.contains(keyPath) { - if var properties = collected { - properties.insert(keyPath) - continuation.resume(returning: properties) - } else { - var properties = TrackedProperties() - properties.insert(keyPath) - continuation.resume(returning: properties) + insert(for: properties, id: id) + return id + } + + mutating func removeLookups( + _ properties: TrackedProperties, + id: Int + ) { + for keyPath in properties.raw { + if var observationIds = lookups.removeValue(forKey: keyPath) { + observationIds.remove(id) + if observationIds.count > 0 { + lookups[keyPath] = observationIds + } } - self = .transaction(observedTrackedProperties) } - - return nil - case (.change(let observedTrackedProperties, let continuation), .didSet): - if observedTrackedProperties.contains(keyPath) { - if var properties = collected { - properties.insert(keyPath) - collected = properties - } else { - var properties = TrackedProperties() - properties.insert(keyPath) - collected = properties + } + + mutating func remove(_ id: Int) { + if let observation = observations.removeValue(forKey: id) { + switch observation { + case .idleChanges(let observation): + removeLookups(observation.properties, id: id) + case .pendingChange(let observation): + removeLookups(observation.properties, id: id) + case .activeChange(let observation): + removeLookups(observation.properties, id: id) + observation.continuation.resume(returning: nil) + case .idleValues: + break + case .pendingValue: + break + case .activeValue(let observation): + removeLookups(observation.properties, id: id) + func terminate(_ valueType: Member.Type) { + unsafeBitCast( + observation.rawContinuation, + to: UnsafeContinuation.self + ).resume(returning: nil) + } + _openExistential(type(of: observation.keyPath).valueType, do: terminate) + case .tracking(let observation): + removeLookups(observation.properties, id: id) + case .cancelled: + break } - return .resumeAndRemove(continuation) } - return nil - case (.tracking(let observedTrackedProperties, let observer), .willSet): - if observedTrackedProperties.contains(keyPath) { - if var properties = collected { - properties.insert(keyPath) - collected = properties - } else { - var properties = TrackedProperties() - properties.insert(keyPath) - collected = properties + } + + enum WillSetAction { + case tracking([@Sendable () -> Void]) + } + + mutating func willSet( + _ subject: Subject, + keyPath: KeyPath + ) -> WillSetAction? { + var result = [@Sendable () -> Void]() + if let ids = lookups[keyPath] { + for id in ids { + switch observations[id] { + // the only participant of willSet is the tracking + case .tracking(let observation): + result.append(observation.observer) + remove(id) + default: + break + } } - return .informAndRemove(observer) + } + if result.count > 0 { + return .tracking(result) } else { return nil } - default: - return nil } - } - - fileprivate func invalidate( - properties: TrackedProperties, - from lookup: inout [PartialKeyPath: Set], - generation: Int - ) { - for raw in properties.raw { - if var members = lookup[raw] { - members.remove(generation) - if members.isEmpty { - lookup.removeValue(forKey: raw) - } else { - lookup[raw] = members + + enum DidSetAction { + case resumeWithMember(UnsafeContinuation) + } + + mutating func didSet( + _ subject: Subject, + keyPath: KeyPath + ) -> [DidSetAction] { + var actions = [DidSetAction]() + if let ids = lookups[keyPath] { + for id in ids { + switch observations[id] { + case .idleChanges(let observation): + let change = + ObservedChange(subject: subject, properties: [keyPath]) + observations[id] = observation.transitionToPending(change) + break + case .none: + fatalError("Internal inconsistency") + case .pendingChange(let observation): + observations[id] = observation.inserting(subject, keyPath) + case .activeChange(let observation): + let change = ObservedChange(subject: subject, properties: [keyPath]) + observation.continuation.resume(returning: change) + observations[id] = observation.transitionToIdle() + case .idleValues(let observation): + observations[id] = observation.transitionToPending(subject) + case .pendingValue: + // the placeholder already has the subject of + // observation so no need to update + break + case .activeValue(let observation): + observations[id] = observation.transitionToIdle() + actions.append( + .resumeWithMember( + unsafeBitCast( + observation.rawContinuation, + to: UnsafeContinuation.self + ) + ) + ) + case .tracking: + // Tracking does not interplay with didSet + break + case .cancelled: + // cancelled does not interplay with didSet + break + } } } + return actions } - } - - fileprivate func remove( - from lookup: inout [PartialKeyPath: Set], - generation: Int - ) { - switch self { - case .transaction(let properties): - invalidate(properties: properties, from: &lookup, generation: generation) - case .pendingTransaction(let properties, _): - invalidate(properties: properties, from: &lookup, generation: generation) - case .change(let properties, _): - invalidate(properties: properties, from: &lookup, generation: generation) - case .tracking(let properties, _): - invalidate(properties: properties, from: &lookup, generation: generation) - default: - break - } - } - - fileprivate func deinitialize() { - switch self { - case .pendingTransaction(_, let continuation): - continuation.resume(returning: nil) - case .change(_, let continuation): - continuation.resume(returning: nil) - default: - break + + mutating func beginTransaction( + for properties: TrackedProperties, + id: Int + ) { + switch observations[id] { + case .idleChanges(let observation): + observations[id] = observation.transitionToPending() + case .pendingChange: + // beginning more than once just no-ops past the first + break + case .activeChange: fallthrough + case .idleValues: fallthrough + case .pendingValue: fallthrough + case .activeValue: fallthrough + case .tracking: fallthrough + case .none: + fatalError("Internal inconsistency") + case .cancelled: + // cancellation can happen at any time + break + } } - } -} - -@available(SwiftStdlib 5.9, *) -extension ObservationRegistrar.State { - fileprivate mutating func nextGeneration() -> Int { - defer { generation &+= 1 } - return generation - } - - fileprivate mutating func cancel(_ generation: Int) { - if let existing = nexts.removeValue(forKey: generation) { - existing.remove(from: &lookups, generation: generation) - existing.deinitialize() - } else { - nexts[generation] = ObservationRegistrar.Next(kind: .cancelled) + + mutating func insert( + for properties: TrackedProperties, + id: Int + ) { + for keyPath in properties.raw { + lookups[keyPath, default: []].insert(id) + } } - } - - fileprivate mutating func insert( - transaction properties: TrackedProperties, - generation: Int - ) { - if let existing = nexts.removeValue(forKey: generation) { - switch existing.kind { + + mutating func insertNextChange( + for properties: TrackedProperties, + continuation: UnsafeContinuation?, Never>, + id: Int + ) { + switch observations[id] { + case .idleChanges(let observation): + observations[id] = observation.transitionToActive(continuation) + case .pendingChange(let observation): + if let change = observation.change { + observations[id] = observation.transitionToIdle() + continuation.resume(returning: change) + } else { + observations[id] = observation.transitionToActive(continuation) + } + case .idleValues: fallthrough + case .pendingValue: fallthrough + case .activeValue: fallthrough + case .tracking: fallthrough + case .none: + fatalError("Internal inconsistency") + case .activeChange: + fatalError("attempting to await more than once on a non-sendable iterator") case .cancelled: - return - default: - existing.remove(from: &lookups, generation: generation) + remove(id) + continuation.resume(returning: nil) } } - nexts[generation] = ObservationRegistrar.Next(kind: .transaction(properties)) - for raw in properties.raw { - lookups[raw, default: []].insert(generation) - } - } - - fileprivate mutating func insert( - change properties: TrackedProperties, - continuation: UnsafeContinuation, - generation: Int - ) { - guard !terminal else { - continuation.resume(returning: nil) - return + + enum InsertChangeAction { + case resumeWithMember(Subject) } - if let existing = nexts.removeValue(forKey: generation) { - switch existing.kind { + + mutating func insertNextValue( + for keyPath: KeyPath, + properties: TrackedProperties, + continuation: UnsafeContinuation, + id: Int + ) -> InsertChangeAction? { + switch observations[id] { + case .idleValues(let observation): + observations[id] = + observation.transitionToActive(keyPath, properties, continuation) + case .pendingValue(let observation): + observations[id] = observation.transitionToIdle() + return .resumeWithMember(observation.subject) + case .activeValue: + fatalError("attempting to await more than once on a non-sendable iterator") + case .idleChanges: fallthrough + case .pendingChange: fallthrough + case .activeChange: fallthrough + case .tracking: fallthrough + case .none: + fatalError("Internal inconsistency") case .cancelled: + remove(id) continuation.resume(returning: nil) - return - default: - existing.remove(from: &lookups, generation: generation) + } + return nil + } + + mutating func insertNextTracking( + for properties: TrackedProperties, + _ observer: @Sendable @escaping () -> Void + ) -> Int { + let id = self.id + self.id = id + 1 + observations[id] = Observation(tracking: properties, observer) + insert(for: properties, id: id) + return id + } + + mutating func cancel(_ id: Int) { + switch observations[id] { + // Any observation state before active must identify it as cancelled + // just in case an active state comes along later + case .idleChanges: fallthrough + case .pendingChange: fallthrough + case .idleValues: fallthrough + case .pendingValue: + observations[id] = .cancelled + case .activeChange: fallthrough + case .activeValue: + remove(id) + case .tracking: fallthrough + case .cancelled: fallthrough + case .none: + break } } - nexts[generation] = - ObservationRegistrar.Next(kind: .change(properties, continuation)) - for raw in properties.raw { - lookups[raw, default: []].insert(generation) + } +} + +@available(SwiftStdlib 5.9, *) +extension ObservationRegistrar.State: _Deinitializable { + mutating func deinitialize() { + for id in observations.keys { + remove(id) } } +} + +@available(SwiftStdlib 5.9, *) +extension ObservationRegistrar.Context { + func register( + _ kind: ObservationRegistrar.ObservationKind, + properties: TrackedProperties + ) -> Int { + state.withCriticalRegion { $0.insert(kind, properties: properties) } + } - fileprivate mutating func insert( - properties: TrackedProperties, - tracking: @Sendable @escaping () -> Void, - generation: Int - ) { - nexts[generation] = - ObservationRegistrar.Next(kind: .tracking(properties, tracking)) - for raw in properties.raw { - lookups[raw, default: []].insert(generation) - } + func unregister(_ id: Int) { + state.withCriticalRegion { $0.remove(id) } } - fileprivate mutating func complete( - _ generation: Int, - continuation: UnsafeContinuation?, Never> - ){ - if let existing = nexts.removeValue(forKey: generation) { - switch existing.kind { - case .transaction(let properties): - if let collected = existing.collected { - continuation.resume(returning: collected) - } else { - nexts[generation] = ObservationRegistrar.Next(kind: .pendingTransaction(properties, continuation)) - } - default: - continuation.resume(returning: nil) + private func scheduleNextChange( + for properties: TrackedProperties, + isolation: isolated Isolation, + id: Int + ) async -> ObservedChange? { + return await withUnsafeContinuation { continuation in + state.withCriticalRegion { state in + state.insertNextChange( + for: properties, + continuation: continuation, + id: id + ) } } } - fileprivate mutating func willSet( - _ subject: Subject, - keyPath: KeyPath - ) -> [() -> Void] { - let raw = keyPath - var observers = [() -> Void]() - for generation in lookups[raw] ?? [] { - if let resume = nexts[generation]?.resume(keyPath: raw, phase: .willSet) { - switch resume { - case .resumeAndRemove(let continuation): - continuation.resume(returning: subject) - case .informAndRemove(let observer): - observers.append(observer) - } - if let existing = nexts.removeValue(forKey: generation) { - existing.remove(from: &lookups, generation: generation) - } - } + internal func nextChange( + for properties: TrackedProperties, + isolation: Isolation, + id: Int + ) async -> ObservedChange? { + await withTaskCancellationHandler { + return await scheduleNextChange( + for: properties, + isolation: isolation, + id: id + ) + } onCancel: { + state.withCriticalRegion { $0.cancel(id) } } - return observers } - fileprivate mutating func didSet( - _ subject: Subject, - keyPath: KeyPath - ) { - let raw = keyPath - guard let generations = lookups[raw] else { - return - } - for generation in generations { - if let resume = nexts[generation]?.resume(keyPath: raw, phase: .didSet) { - switch resume { - case .resumeAndRemove(let continuation): - continuation.resume(returning: subject) - default: - break + internal func nextValue( + for keyPath: KeyPath, + properties: TrackedProperties, + id: Int + ) async -> Member? { + await withTaskCancellationHandler { + await withUnsafeContinuation { continuation in + let action = state.withCriticalRegion { state in + state.insertNextValue( + for: keyPath, + properties: properties, + continuation: continuation, + id: id + ) } - if let existing = nexts.removeValue(forKey: generation) { - existing.remove(from: &lookups, generation: generation) + if let action { + switch action { + case .resumeWithMember(let subject): + continuation.resume(returning: subject[keyPath: keyPath]) + } } } + } onCancel: { + state.withCriticalRegion { $0.cancel(id) } } } -} - -@available(SwiftStdlib 5.9, *) -extension ObservationRegistrar.State: _Deinitializable { - fileprivate mutating func deinitialize() { - terminal = true - for next in nexts.values { - next.deinitialize() + + internal func nextTracking( + for properties: TrackedProperties, + _ observer: @Sendable @escaping () -> Void + ) -> Int { + state.withCriticalRegion { state in + state.insertNextTracking(for: properties, observer) } - nexts.removeAll() - lookups.removeAll() + } + + internal func cancel(_ id: Int) { + state.withCriticalRegion { $0.cancel(id) } } } diff --git a/stdlib/public/Observation/Sources/Observation/ObservationRegistrarStateMachine.swift b/stdlib/public/Observation/Sources/Observation/ObservationRegistrarStateMachine.swift new file mode 100644 index 0000000000000..9bee379b06bbd --- /dev/null +++ b/stdlib/public/Observation/Sources/Observation/ObservationRegistrarStateMachine.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _Concurrency + +@available(SwiftStdlib 5.9, *) +extension ObservationRegistrar { + struct IdleChangesState { + let properties: TrackedProperties + + fileprivate init(properties: TrackedProperties) { + self.properties = properties + } + + func transitionToPending( + _ change: ObservedChange? = nil + ) -> State.Observation { + return .pendingChange( + PendingChangeState(properties: properties, change: change)) + } + + func transitionToActive( + _ continuation: UnsafeContinuation?, Never> + ) -> State.Observation { + return .activeChange( + ActiveChangeState(properties: properties, continuation: continuation)) + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } + + struct PendingChangeState { + let properties: TrackedProperties + let change: ObservedChange? + + fileprivate init( + properties: TrackedProperties, + change: ObservedChange? + ) { + self.properties = properties + self.change = change + } + + func inserting( + _ subject: Subject, + _ keyPath: PartialKeyPath + ) -> State.Observation { + if var change { + change.insert(keyPath) + return .pendingChange( + PendingChangeState(properties: properties, change: change)) + } else { + let change = ObservedChange(subject: subject, properties: [keyPath]) + return .pendingChange( + PendingChangeState(properties: properties, change: change)) + } + } + + func transitionToIdle() -> State.Observation { + return .idleChanges(IdleChangesState(properties: properties)) + } + + func transitionToActive( + _ continuation: UnsafeContinuation?, Never> + ) -> State.Observation { + return .activeChange( + ActiveChangeState(properties: properties, continuation: continuation)) + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } + + struct ActiveChangeState { + let properties: TrackedProperties + let continuation: UnsafeContinuation?, Never> + + fileprivate init( + properties: TrackedProperties, + continuation: UnsafeContinuation?, Never> + ) { + self.properties = properties + self.continuation = continuation + } + + func transitionToIdle() -> State.Observation { + return .idleChanges(IdleChangesState(properties: properties)) + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } + + struct IdleValuesState { + fileprivate init() { } + + func transitionToPending(_ subject: Subject) -> State.Observation { + return .pendingValue(PendingValueState(subject: subject)) + } + + func transitionToActive( + _ keyPath: KeyPath, + _ properties: TrackedProperties, + _ continuation: UnsafeContinuation + ) -> State.Observation { + return .activeValue( + ActiveValueState( + properties: properties, + keyPath: keyPath, + continuation: continuation + ) + ) + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } + + struct PendingValueState { + let subject: Subject + + fileprivate init(subject: Subject) { + self.subject = subject + } + + func transitionToIdle() -> State.Observation { + return .idleValues(IdleValuesState()) + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } + + struct ActiveValueState { + let properties: TrackedProperties + let keyPath: PartialKeyPath + let rawContinuation: UnsafeRawPointer + + fileprivate init( + properties: TrackedProperties, + keyPath: KeyPath, + continuation: UnsafeContinuation + ) { + self.properties = properties + self.keyPath = keyPath + self.rawContinuation = + unsafeBitCast(continuation, to: UnsafeRawPointer.self) + } + + func transitionToIdle() -> State.Observation { + return .idleValues(IdleValuesState()) + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } + + struct TrackingState { + let properties: TrackedProperties + let observer: @Sendable () -> Void + + fileprivate init( + properties: TrackedProperties, + observer: @Sendable @escaping () -> Void + ) { + self.properties = properties + self.observer = observer + } + + func transitionToCancelled() -> State.Observation { + return .cancelled + } + } +} + +@available(SwiftStdlib 5.9, *) +extension ObservationRegistrar.State.Observation { + init(idleChanges properties: TrackedProperties) { + self = .idleChanges( + ObservationRegistrar.IdleChangesState(properties: properties)) + } + + init(idleValues properties: TrackedProperties) { + self = .idleValues(ObservationRegistrar.IdleValuesState()) + } + + init( + tracking properties: TrackedProperties, + _ observer: @Sendable @escaping () -> Void + ) { + self = .tracking( + ObservationRegistrar.TrackingState(properties: properties, observer: observer)) + } +} diff --git a/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift b/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift index 29b01981c84ea..6147b250f6357 100644 --- a/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift +++ b/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift @@ -45,7 +45,7 @@ public struct ObservationTracking { internal init() { } internal mutating func addAccess( - keyPath: PartialKeyPath, + keyPath: PartialKeyPath, context: ObservationRegistrar.Context ) { entries[context.id, default: Entry(context)].insert(keyPath) @@ -53,7 +53,7 @@ public struct ObservationTracking { } public static func withTracking( - _ apply: () -> T, + _ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void ) -> T { var _AccessList: _AccessList? @@ -79,7 +79,7 @@ public struct ObservationTracking { @_spi(SwiftUI) public static func _installTracking( - _ list: _AccessList, + _ list: _AccessList, onChange: @escaping @Sendable () -> Void ) { let state = _ManagedCriticalState([ObjectIdentifier: Int]()) diff --git a/stdlib/public/Observation/Sources/Observation/ObservedChanges.swift b/stdlib/public/Observation/Sources/Observation/ObservedChanges.swift index 930238bf52d6e..7a816054a9f8e 100644 --- a/stdlib/public/Observation/Sources/Observation/ObservedChanges.swift +++ b/stdlib/public/Observation/Sources/Observation/ObservedChanges.swift @@ -12,47 +12,99 @@ import _Concurrency @available(SwiftStdlib 5.9, *) -public struct ObservedChanges { +public struct ObservedChange: @unchecked Sendable { + private let _subject: Subject + private var properties: TrackedProperties + + init(subject: Subject, properties: TrackedProperties) { + self._subject = subject + self.properties = properties + } + + internal mutating func insert(_ keyPath: PartialKeyPath) { + properties.insert(keyPath) + } + + public func contains(_ member: PartialKeyPath) -> Bool { + properties.contains(member) + } +} + +@available(SwiftStdlib 5.9, *) +extension ObservedChange where Subject: Sendable { + public var subject: Subject { _subject } +} + +@available(SwiftStdlib 5.9, *) +public struct ObservedChanges { let context: ObservationRegistrar.Context - let keyPath: KeyPath + let properties: TrackedProperties + let isolation: Isolation init( - _ context: ObservationRegistrar.Context, - keyPath: KeyPath + _ context: ObservationRegistrar.Context, + properties: TrackedProperties, + isolation: Isolation ) { self.context = context - self.keyPath = keyPath + self.properties = properties + self.isolation = isolation } } @available(SwiftStdlib 5.9, *) extension ObservedChanges: AsyncSequence { + public typealias Element = ObservedChange + public struct Iterator: AsyncIteratorProtocol { - let context: ObservationRegistrar.Context - let keyPath: KeyPath + final class Iteration { + let context: ObservationRegistrar.Context + let id: Int + + init( + _ context: ObservationRegistrar.Context, + properties: TrackedProperties + ) { + self.context = context + self.id = context.register(.transactions, properties: properties) + } + + deinit { + context.unregister(id) + } + } + + let iteration: Iteration let properties: TrackedProperties + let isolation: Isolation init( - _ context: ObservationRegistrar.Context, - keyPath: KeyPath + _ context: ObservationRegistrar.Context, + properties: TrackedProperties, + isolation: Isolation ) { - self.context = context - self.keyPath = keyPath - properties = Subject.dependencies(of: keyPath) + self.iteration = Iteration(context, properties: properties) + self.properties = properties + self.isolation = isolation + } public mutating func next() async -> Element? { - await context.nextChange(to: keyPath, properties: properties) + await iteration.context.nextChange( + for: properties, + isolation: isolation, + id: iteration.id + ) } } public func makeAsyncIterator() -> Iterator { - Iterator(context, keyPath: keyPath) + Iterator(context, properties: properties, isolation: isolation) } } @available(SwiftStdlib 5.9, *) -extension ObservedChanges: @unchecked Sendable where Subject: Sendable { } +extension ObservedChanges: @unchecked Sendable { } @available(*, unavailable) extension ObservedChanges.Iterator: Sendable { } diff --git a/stdlib/public/Observation/Sources/Observation/ObservedTransactions.swift b/stdlib/public/Observation/Sources/Observation/ObservedTransactions.swift deleted file mode 100644 index 1f3dca38ed5ef..0000000000000 --- a/stdlib/public/Observation/Sources/Observation/ObservedTransactions.swift +++ /dev/null @@ -1,64 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -import _Concurrency - -@available(SwiftStdlib 5.9, *) -public struct ObservedTransactions { - let context: ObservationRegistrar.Context - let properties: TrackedProperties - let isolation: Delivery - - init( - _ context: ObservationRegistrar.Context, - properties: TrackedProperties, - isolation: Delivery - ) { - self.context = context - self.properties = properties - self.isolation = isolation - } -} - -@available(SwiftStdlib 5.9, *) -extension ObservedTransactions: AsyncSequence { - public typealias Element = TrackedProperties - - public struct Iterator: AsyncIteratorProtocol { - let context: ObservationRegistrar.Context - let properties: TrackedProperties - let isolation: Delivery - - init( - _ context: ObservationRegistrar.Context, - properties: TrackedProperties, - isolation: Delivery - ) { - self.context = context - self.properties = properties - self.isolation = isolation - } - - public mutating func next() async -> Element? { - await context.nextTransaction(for: properties, isolation: isolation) - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(context, properties: properties, isolation: isolation) - } -} - -@available(SwiftStdlib 5.9, *) -extension ObservedTransactions: @unchecked Sendable { } - -@available(*, unavailable) -extension ObservedTransactions.Iterator: Sendable { } diff --git a/stdlib/public/Observation/Sources/Observation/ObservedValues.swift b/stdlib/public/Observation/Sources/Observation/ObservedValues.swift new file mode 100644 index 0000000000000..5da2f54d89efd --- /dev/null +++ b/stdlib/public/Observation/Sources/Observation/ObservedValues.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import _Concurrency + +@available(SwiftStdlib 5.9, *) +public struct ObservedValues { + let context: ObservationRegistrar.Context + let keyPath: KeyPath + + init( + _ context: ObservationRegistrar.Context, + keyPath: KeyPath + ) { + self.context = context + self.keyPath = keyPath + } +} + +@available(SwiftStdlib 5.9, *) +extension ObservedValues: AsyncSequence { + public struct Iterator: AsyncIteratorProtocol { + final class Iteration { + let context: ObservationRegistrar.Context + let id: Int + + init( + _ context: ObservationRegistrar.Context, + properties: TrackedProperties + ) { + self.context = context + self.id = context.register(.changes, properties: properties) + } + + deinit { + context.unregister(id) + } + } + let iteration: Iteration + let keyPath: KeyPath + let properties: TrackedProperties + + + init( + _ context: ObservationRegistrar.Context, + keyPath: KeyPath + ) { + let properties = Subject.dependencies(of: keyPath) + self.iteration = Iteration(context, properties: properties) + self.keyPath = keyPath + self.properties = properties + } + + public mutating func next() async -> Element? { + await iteration.context.nextValue( + for: keyPath, + properties: properties, + id: iteration.id + ) + } + } + + public func makeAsyncIterator() -> Iterator { + Iterator(context, keyPath: keyPath) + } +} + +@available(SwiftStdlib 5.9, *) +extension ObservedValues: @unchecked Sendable where Subject: Sendable { } + +@available(*, unavailable) +extension ObservedValues.Iterator: Sendable { } diff --git a/stdlib/public/Observation/Sources/Observation/ThreadLocal.cpp b/stdlib/public/Observation/Sources/Observation/ThreadLocal.cpp index db8dd0d366bfe..281e4e0afe1cf 100644 --- a/stdlib/public/Observation/Sources/Observation/ThreadLocal.cpp +++ b/stdlib/public/Observation/Sources/Observation/ThreadLocal.cpp @@ -16,12 +16,12 @@ static SWIFT_THREAD_LOCAL_TYPE(void *, swift::tls_key::observation_transaction) Value; -extern "C" SWIFT_CC(swift) +extern "C" SWIFT_CC(swift) __attribute__((visibility("hidden"))) void *_swift_observation_tls_get() { return Value.get(); } -extern "C" SWIFT_CC(swift) +extern "C" SWIFT_CC(swift) __attribute__((visibility("hidden"))) void _swift_observation_tls_set(void *value) { Value.set(value); } diff --git a/test/stdlib/Observation/Observable.swift b/test/stdlib/Observation/Observable.swift index 014732b8e217b..39bff6d54deb2 100644 --- a/test/stdlib/Observation/Observable.swift +++ b/test/stdlib/Observation/Observable.swift @@ -7,7 +7,7 @@ // REQUIRES: executable_test import StdlibUnittest -import Observation +import _Observation import _Concurrency @usableFromInline @@ -26,17 +26,17 @@ final class UnsafeBox: @unchecked Sendable { final class TestWithoutMacro: Observable { let _registrar = ObservationRegistrar() - public nonisolated func transactions( + nonisolated func changes( for properties: TrackedProperties, - isolation: Delivery - ) -> ObservedTransactions where Delivery: Actor { - _registrar.transactions(for: properties, isolation: isolation) + isolatedTo isolation: Isolation + ) -> ObservedChanges where Isolation: Actor { + _registrar.changes(for: properties, isolatedTo: isolation) } - public nonisolated func changes( + nonisolated func values( for keyPath: KeyPath - ) -> ObservedChanges where Member: Sendable { - _registrar.changes(for: keyPath) + ) -> ObservedValues where Member : Sendable { + _registrar.values(for: keyPath) } private struct _Storage { @@ -149,7 +149,7 @@ extension TriggerSequence: AsyncSequence { subject.field3 = i } } -#if false // disabled for now + suite.test("unobserved value changes (nonmacro)") { let subject = TestWithoutMacro() for i in 0..<100 { @@ -164,9 +164,9 @@ extension TriggerSequence: AsyncSequence { t = Task { @MainActor in // Note: this must be fully established // so we must await the trigger to fire - let changes = subject.changes(for: \.field1) + let values = subject.values(for: \.field1) .triggerIteration(continuation) - for await value in changes { + for await value in values { return value } return nil @@ -184,9 +184,9 @@ extension TriggerSequence: AsyncSequence { t = Task { @MainActor in // Note: this must be fully established // so we must await the trigger to fire - let changes = subject.changes(for: \.field1) + let values = subject.values(for: \.field1) .triggerIteration(continuation) - for await value in changes { + for await value in values { return value } return nil @@ -214,16 +214,16 @@ extension TriggerSequence: AsyncSequence { expectEqual(finished, true) } - suite.test("transactions emit values (macro)") { @MainActor in + suite.test("emit values (macro)") { @MainActor in let subject = TestWithMacro() - var t: Task?, Never>? + var t: Task? await withUnsafeContinuation { continuation in t = Task { @MainActor in // Note: this must be fully established // so we must await the trigger to fire - let transactions = subject.transactions(for: \.field1) + let values = subject.values(for: \.field1) .triggerIteration(continuation) - for await value in transactions { + for await value in values { return value } return nil @@ -231,19 +231,19 @@ extension TriggerSequence: AsyncSequence { } subject.field1 = "a" let value = await t!.value - expectEqual(value?.contains(\.field1), true) + expectEqual(value, "a") } - suite.test("transactions emit values (nonmacro)") { @MainActor in + suite.test("emit values (nonmacro)") { @MainActor in let subject = TestWithoutMacro() - var t: Task?, Never>? + var t: Task? await withUnsafeContinuation { continuation in t = Task { @MainActor in // Note: this must be fully established // so we must await the trigger to fire - let transactions = subject.transactions(for: \.field1) + let values = subject.values(for: \.field1) .triggerIteration(continuation) - for await value in transactions { + for await value in values { return value } return nil @@ -251,7 +251,7 @@ extension TriggerSequence: AsyncSequence { } subject.field1 = "a" let value = await t!.value - expectEqual(value?.contains(\.field1), true) + expectEqual(value, "a") } suite.test("tracking") { @MainActor in @@ -268,7 +268,7 @@ extension TriggerSequence: AsyncSequence { subject.field1 = "asdf" expectEqual(changed.contents, true) } -#endif + await runAllTestsAsync() } }