From 98d782ff47b72538c9207ffe06fae6ce28b3db0b Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 6 Mar 2025 08:45:53 -0800 Subject: [PATCH 01/10] [Observation] Initial implementation of Observed for transactional tracked values over time --- .../Sources/Observation/CMakeLists.txt | 1 + .../Sources/Observation/Observed.swift | 226 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 stdlib/public/Observation/Sources/Observation/Observed.swift diff --git a/stdlib/public/Observation/Sources/Observation/CMakeLists.txt b/stdlib/public/Observation/Sources/Observation/CMakeLists.txt index 7872598bd64d9..c416e791ff001 100644 --- a/stdlib/public/Observation/Sources/Observation/CMakeLists.txt +++ b/stdlib/public/Observation/Sources/Observation/CMakeLists.txt @@ -18,6 +18,7 @@ add_swift_target_library(swiftObservation ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS Observable.swift ObservationRegistrar.swift ObservationTracking.swift + Observed.swift ThreadLocal.cpp ThreadLocal.swift diff --git a/stdlib/public/Observation/Sources/Observation/Observed.swift b/stdlib/public/Observation/Sources/Observation/Observed.swift new file mode 100644 index 0000000000000..04c8aedfa3939 --- /dev/null +++ b/stdlib/public/Observation/Sources/Observation/Observed.swift @@ -0,0 +1,226 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +#if STANDALONE +import Observation +#endif +import Synchronization + +/// An asychronous sequence generated from a closure that tracks the transactional changes of `@Observable` types. +/// +/// `Observed` conforms to `AsyncSequence`, providing a intutive and safe mechanism to track changes to +/// types that are marked as `@Observable` by using Swift Concurrency to indicate transactional boundaries +/// starting from the willSet of the first mutation to the next suspension point of the safe access. +#if !STANDALONE +@available(SwiftStdlib 9999, *) +#endif +public struct Observed: AsyncSequence, Sendable { + final class SharedState: Sendable { + let criticalRegion: Mutex + + init(_ state: consuming sending State) { + criticalRegion = Mutex(state) + } + + internal func withCriticalRegion(body: (inout sending State) throws(F) -> sending R) throws(F) -> R { + try criticalRegion.withLock(body) + } + } + + struct State { + var id = 0 + var tracking = false + var continuations: [Int: UnsafeContinuation] = [:] + + // create a generation id for the unique identification of the continuations + // this allows the shared awaiting of the willSets. + // Most likely, there wont be more than a handful of active iterations + // so this only needs to be unique for those active iterations + // that are in the process of calling next. + static func generation(_ state: SharedState) -> Int { + state.withCriticalRegion { state in + defer { state.id &+= 1 } + return state.id + } + } + + // the cancellation of awaiting on willSet only ferries in resuming early + // it is the responsability of the caller to check if the task is actually + // cancelled after awaiting the willSet to act accordingly. + static func cancel(_ state: SharedState, id: Int) { + state.withCriticalRegion { state in + return state.continuations.removeValue(forKey: id) + }?.resume() + } + + // this atomically transitions the observation from a not yet tracked state + // to a tracked state. No backwards transitions exist. + static func startTracking(_ state: SharedState) -> Bool { + state.withCriticalRegion { state in + if !state.tracking { + state.tracking = true + return true + } else { + return false + } + } + } + + // fire off ALL awaiting willChange continuations such that they are no + // longer pending. + static func emitWillChange(_ state: SharedState) { + let continuations = state.withCriticalRegion { state in + defer { + state.continuations.removeAll() + } + return state.continuations.values + } + for continuation in continuations { + continuation.resume() + } + } + + // install a willChange continuation into the set of continuations + // this must take a locally unique id (to the active calls of next) + static func willChange(_ state: SharedState, id: Int) async { + await withUnsafeContinuation { continuation in + state.withCriticalRegion { state in + state.continuations[id] = continuation + } + } + } + } + + let isolation: (any Actor)? + let state: SharedState + let work: @Sendable () throws(Failure) -> Element? + + /// Constructs an asynchronous sequence for a given closure by tracking changes of `@Observable` types. + /// + /// The emit closure is responsible for extracting a value out of a single or many `@Observable` types. When `nil` is + /// returned the sequence terminates and no more values will be produced. Likewise when an error is thrown, iterations will + /// catch that error and the sequence will be in a terminal state, no longer producing values. + /// + /// When constructing an `Observed` asynchronous sequence it is strongly suggested that the isolation parameter is left + /// to the default value. Other values specified are only for advanced and unique use cases and may result in unexpected + /// behavior. + /// + /// - Parameters: + /// - isolation: The concurrency isolation domain of the caller. + /// - emit: A closure to generate an element for the sequence. + /// + public init( + isolation: isolated (any Actor)? = #isolation, + @_inheritActorContext _ emit: @Sendable @escaping () throws(Failure) -> Element? + ) { + self.isolation = isolation + self.work = { () throws(Failure) -> Element? in + if let isolation { + do { + return try isolation.assumeIsolated { _ in + try emit() + } + } catch { + throw error as! Failure + } + } else { + return try emit() + } + } + self.state = SharedState(State()) + } + + public struct Iterator: AsyncIteratorProtocol { + let isolation: (any Actor)? + // the state ivar serves two purposes: + // 1) to store a critical region of state of the mutations + // 2) to idenitify the termination of _this_ sequence + var state: SharedState? + let work: @Sendable () throws(Failure) -> Element? + + // this is the primary implementation of the tracking + // it is bound to be called on the specified isolation of the construction + fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: SharedState, work: @escaping @Sendable () throws(Failure) -> Element?) throws(Failure) -> Element? { + // this ferries in an intermediate form with Result to skip over `withObservationTracking` not handling errors being thrown + // particularly this case is that the error is also an iteration state transition data point (it terminates the sequence) + // so we need to hold that to get a chance to catch and clean-up + let result = withObservationTracking { + Result(catching: work) + } onChange: { [state] in + // resume all cases where the awaiting continuations are awaiting a willSet + State.emitWillChange(state) + } + return try result.get() + } + + fileprivate mutating func terminate(throwing failure: Failure? = nil) throws(Failure) -> Element? { + // flag the sequence as terminal by nil'ing out the state + state = nil + if let failure { + throw failure + } else { + return nil + } + } + + public mutating func next(isolation iterationIsolation: isolated (any Actor)? = #isolation) async throws(Failure) -> Element? { + // early exit if the sequence is terminal already + guard let state else { return nil } + do { + // there are two versions; + // either the tracking has never yet started at all and we need to prime the pump + // or the tracking has already started and we are going to await a change + if State.startTracking(state) { + guard !Task.isCancelled else { return nil } + // start by directly tracking the emission via a withObservation tracking on the isolation specified fro mthe init + guard let element = try await Iterator.trackEmission(isolation: isolation, state: state, work: work) else { + // the user returned a nil from the closure so terminate the sequence + return try terminate() + } + return element + } else { + // set up an id for this generation + let id = State.generation(state) + // wait for the willChange (and NOT the value itself) + // since this is going to be on the isolation of the object (e.g. the isolation specified in the initialization) + // this will mean our next await for the emission will ensure the suspension return of the willChange context + // back to the trailing edges of the mutations. In short, this enables the transactionality bounded by the + // isolation of the mutation. + await withTaskCancellationHandler { + await State.willChange(state, id: id) + } onCancel: { + // ensure to clean out our continuation uon cancellation + State.cancel(state, id: id) + } + + guard !Task.isCancelled else { + // the task was cancelled while awaiting a willChange so ensure a proper termination + return try terminate() + } + + // re-prime the pump for the observation tracking + guard let element = try await Iterator.trackEmission(isolation: isolation, state: state, work: work) else { + // again ensure the user can terminate by returning nil + return try terminate() + } + return element + } + } catch { + // the user threw a failure in the closure so propigate that outwards and terminate the sequence + return try terminate(throwing: error) + } + } + } + + public func makeAsyncIterator() -> Iterator { + Iterator(isolation: isolation, state: state, work: work) + } +} From d13c7aa822563cb0b8df57ad682dd8e3eb52bc43 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Mon, 17 Mar 2025 16:51:02 -0700 Subject: [PATCH 02/10] Update the initializer for Observed to use @isolated(any) --- .../Sources/Observation/Observed.swift | 65 +++++++------------ 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/stdlib/public/Observation/Sources/Observation/Observed.swift b/stdlib/public/Observation/Sources/Observation/Observed.swift index 04c8aedfa3939..f8ca5636f1446 100644 --- a/stdlib/public/Observation/Sources/Observation/Observed.swift +++ b/stdlib/public/Observation/Sources/Observation/Observed.swift @@ -13,6 +13,7 @@ import Observation #endif import Synchronization +import _Concurrency /// An asychronous sequence generated from a closure that tracks the transactional changes of `@Observable` types. /// @@ -99,9 +100,8 @@ public struct Observed: AsyncSequence, Sendab } } - let isolation: (any Actor)? let state: SharedState - let work: @Sendable () throws(Failure) -> Element? + let emit: @isolated(any) @Sendable () throws(Failure) -> Element? /// Constructs an asynchronous sequence for a given closure by tracking changes of `@Observable` types. /// @@ -118,42 +118,27 @@ public struct Observed: AsyncSequence, Sendab /// - emit: A closure to generate an element for the sequence. /// public init( - isolation: isolated (any Actor)? = #isolation, - @_inheritActorContext _ emit: @Sendable @escaping () throws(Failure) -> Element? + @_inheritActorContext _ emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element? ) { - self.isolation = isolation - self.work = { () throws(Failure) -> Element? in - if let isolation { - do { - return try isolation.assumeIsolated { _ in - try emit() - } - } catch { - throw error as! Failure - } - } else { - return try emit() - } - } + self.emit = emit self.state = SharedState(State()) } public struct Iterator: AsyncIteratorProtocol { - let isolation: (any Actor)? // the state ivar serves two purposes: // 1) to store a critical region of state of the mutations // 2) to idenitify the termination of _this_ sequence var state: SharedState? - let work: @Sendable () throws(Failure) -> Element? + let emit: @isolated(any) @Sendable () throws(Failure) -> Element? // this is the primary implementation of the tracking // it is bound to be called on the specified isolation of the construction - fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: SharedState, work: @escaping @Sendable () throws(Failure) -> Element?) throws(Failure) -> Element? { + fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: SharedState, emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element?) throws(Failure) -> Element? { // this ferries in an intermediate form with Result to skip over `withObservationTracking` not handling errors being thrown // particularly this case is that the error is also an iteration state transition data point (it terminates the sequence) // so we need to hold that to get a chance to catch and clean-up let result = withObservationTracking { - Result(catching: work) + Result(catching: emit) } onChange: { [state] in // resume all cases where the awaiting continuations are awaiting a willSet State.emitWillChange(state) @@ -171,6 +156,19 @@ public struct Observed: AsyncSequence, Sendab } } + fileprivate mutating func trackEmission(isolation iterationIsolation: isolated (any Actor)?, state: SharedState) async throws(Failure) -> Element? { + guard !Task.isCancelled else { + // the task was cancelled while awaiting a willChange so ensure a proper termination + return try terminate() + } + // start by directly tracking the emission via a withObservation tracking on the isolation specified fro mthe init + guard let element = try await Iterator.trackEmission(isolation: emit.isolation, state: state, emit: emit) else { + // the user returned a nil from the closure so terminate the sequence + return try terminate() + } + return element + } + public mutating func next(isolation iterationIsolation: isolated (any Actor)? = #isolation) async throws(Failure) -> Element? { // early exit if the sequence is terminal already guard let state else { return nil } @@ -179,13 +177,7 @@ public struct Observed: AsyncSequence, Sendab // either the tracking has never yet started at all and we need to prime the pump // or the tracking has already started and we are going to await a change if State.startTracking(state) { - guard !Task.isCancelled else { return nil } - // start by directly tracking the emission via a withObservation tracking on the isolation specified fro mthe init - guard let element = try await Iterator.trackEmission(isolation: isolation, state: state, work: work) else { - // the user returned a nil from the closure so terminate the sequence - return try terminate() - } - return element + return try await trackEmission(isolation: iterationIsolation, state: state) } else { // set up an id for this generation let id = State.generation(state) @@ -200,18 +192,7 @@ public struct Observed: AsyncSequence, Sendab // ensure to clean out our continuation uon cancellation State.cancel(state, id: id) } - - guard !Task.isCancelled else { - // the task was cancelled while awaiting a willChange so ensure a proper termination - return try terminate() - } - - // re-prime the pump for the observation tracking - guard let element = try await Iterator.trackEmission(isolation: isolation, state: state, work: work) else { - // again ensure the user can terminate by returning nil - return try terminate() - } - return element + return try await trackEmission(isolation: iterationIsolation, state: state) } } catch { // the user threw a failure in the closure so propigate that outwards and terminate the sequence @@ -221,6 +202,6 @@ public struct Observed: AsyncSequence, Sendab } public func makeAsyncIterator() -> Iterator { - Iterator(isolation: isolation, state: state, work: work) + Iterator(state: state, emit: emit) } } From b39352e607eac35eeef6f4f77cb8754e619a9b2c Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 20 Mar 2025 09:05:24 -0700 Subject: [PATCH 03/10] Add a pre-continuation cancellation check to ensure cancelled emissions dont hang --- .../Sources/Observation/Observed.swift | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/stdlib/public/Observation/Sources/Observation/Observed.swift b/stdlib/public/Observation/Sources/Observation/Observed.swift index f8ca5636f1446..58241af8a578f 100644 --- a/stdlib/public/Observation/Sources/Observation/Observed.swift +++ b/stdlib/public/Observation/Sources/Observation/Observed.swift @@ -37,9 +37,19 @@ public struct Observed: AsyncSequence, Sendab } struct State { + enum Continuation { + case cancelled + case active(UnsafeContinuation) + func resume() { + switch self { + case .cancelled: break + case .active(let continuation): continuation.resume() + } + } + } var id = 0 var tracking = false - var continuations: [Int: UnsafeContinuation] = [:] + var continuations: [Int: Continuation] = [:] // create a generation id for the unique identification of the continuations // this allows the shared awaiting of the willSets. @@ -58,7 +68,14 @@ public struct Observed: AsyncSequence, Sendab // cancelled after awaiting the willSet to act accordingly. static func cancel(_ state: SharedState, id: Int) { state.withCriticalRegion { state in - return state.continuations.removeValue(forKey: id) + guard let continuation = state.continuations.removeValue(forKey: id) else { + // if there was no continuation yet active (e.g. it was cancelled at + // the start of the invocation, then put a tombstone in to gate that + // resuming later + state.continuations[id] = .cancelled + return nil as Continuation? + } + return continuation }?.resume() } @@ -92,10 +109,19 @@ public struct Observed: AsyncSequence, Sendab // install a willChange continuation into the set of continuations // this must take a locally unique id (to the active calls of next) static func willChange(_ state: SharedState, id: Int) async { - await withUnsafeContinuation { continuation in + return await withUnsafeContinuation { continuation in state.withCriticalRegion { state in - state.continuations[id] = continuation - } + // first check if a cancelled tombstone exists, remove it, + // and then return the freshly minted continuation to + // be immediately resumed + if case .cancelled = state.continuations[id] { + state.continuations[id] = nil + return continuation as UnsafeContinuation? + } else { + state.continuations[id] = .active(continuation) + return nil as UnsafeContinuation? + } + }?.resume() } } } From 52b36e7359d463783fef1ed2a88db39bdec48a3c Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 20 Mar 2025 09:27:12 -0700 Subject: [PATCH 04/10] Plumb the id through to ensure the id is defensively removed to prevent table growth in edge cases --- .../Sources/Observation/Observed.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/stdlib/public/Observation/Sources/Observation/Observed.swift b/stdlib/public/Observation/Sources/Observation/Observed.swift index 58241af8a578f..87d60748c9b2c 100644 --- a/stdlib/public/Observation/Sources/Observation/Observed.swift +++ b/stdlib/public/Observation/Sources/Observation/Observed.swift @@ -172,7 +172,11 @@ public struct Observed: AsyncSequence, Sendab return try result.get() } - fileprivate mutating func terminate(throwing failure: Failure? = nil) throws(Failure) -> Element? { + fileprivate mutating func terminate(throwing failure: Failure? = nil, id: Int) throws(Failure) -> Element? { + // this is purely defensive to any leaking out of iteration generation ids + state?.withCriticalRegion { state in + state.continuations.removeValue(forKey: id) + }?.resume() // flag the sequence as terminal by nil'ing out the state state = nil if let failure { @@ -182,15 +186,15 @@ public struct Observed: AsyncSequence, Sendab } } - fileprivate mutating func trackEmission(isolation iterationIsolation: isolated (any Actor)?, state: SharedState) async throws(Failure) -> Element? { + fileprivate mutating func trackEmission(isolation iterationIsolation: isolated (any Actor)?, state: SharedState, id: Int) async throws(Failure) -> Element? { guard !Task.isCancelled else { // the task was cancelled while awaiting a willChange so ensure a proper termination - return try terminate() + return try terminate(id: id) } // start by directly tracking the emission via a withObservation tracking on the isolation specified fro mthe init guard let element = try await Iterator.trackEmission(isolation: emit.isolation, state: state, emit: emit) else { // the user returned a nil from the closure so terminate the sequence - return try terminate() + return try terminate(id: id) } return element } @@ -198,15 +202,16 @@ public struct Observed: AsyncSequence, Sendab public mutating func next(isolation iterationIsolation: isolated (any Actor)? = #isolation) async throws(Failure) -> Element? { // early exit if the sequence is terminal already guard let state else { return nil } + // set up an id for this generation + let id = State.generation(state) do { // there are two versions; // either the tracking has never yet started at all and we need to prime the pump // or the tracking has already started and we are going to await a change if State.startTracking(state) { - return try await trackEmission(isolation: iterationIsolation, state: state) + return try await trackEmission(isolation: iterationIsolation, state: state, id: id) } else { - // set up an id for this generation - let id = State.generation(state) + // wait for the willChange (and NOT the value itself) // since this is going to be on the isolation of the object (e.g. the isolation specified in the initialization) // this will mean our next await for the emission will ensure the suspension return of the willChange context @@ -218,11 +223,11 @@ public struct Observed: AsyncSequence, Sendab // ensure to clean out our continuation uon cancellation State.cancel(state, id: id) } - return try await trackEmission(isolation: iterationIsolation, state: state) + return try await trackEmission(isolation: iterationIsolation, state: state, id: id) } } catch { // the user threw a failure in the closure so propigate that outwards and terminate the sequence - return try terminate(throwing: error) + return try terminate(throwing: error, id: id) } } } From 855dfa85b0af0c00e569571198f02a50e1359acb Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 20 Mar 2025 15:08:20 -0700 Subject: [PATCH 05/10] Add ABI assertions for observed --- test/abi/Inputs/macOS/arm64/observation/baseline | 10 ++++++++++ .../Inputs/macOS/arm64/observation/baseline-asserts | 10 ++++++++++ test/abi/Inputs/macOS/x86_64/observation/baseline | 10 ++++++++++ .../Inputs/macOS/x86_64/observation/baseline-asserts | 10 ++++++++++ 4 files changed, 40 insertions(+) diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline b/test/abi/Inputs/macOS/arm64/observation/baseline index 567b0e3345cf7..a1484f6f4069e 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline +++ b/test/abi/Inputs/macOS/arm64/observation/baseline @@ -31,3 +31,13 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu +_$s11Observation8ObservedV8IteratorVMa +_$s11Observation8ObservedV8IteratorVMn +_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedVMa +_$s11Observation8ObservedVMn +_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts index 567b0e3345cf7..a1484f6f4069e 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts @@ -31,3 +31,13 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu +_$s11Observation8ObservedV8IteratorVMa +_$s11Observation8ObservedV8IteratorVMn +_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedVMa +_$s11Observation8ObservedVMn +_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline b/test/abi/Inputs/macOS/x86_64/observation/baseline index 567b0e3345cf7..a1484f6f4069e 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline @@ -31,3 +31,13 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu +_$s11Observation8ObservedV8IteratorVMa +_$s11Observation8ObservedV8IteratorVMn +_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedVMa +_$s11Observation8ObservedVMn +_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts index 567b0e3345cf7..a1484f6f4069e 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts @@ -31,3 +31,13 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF +_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu +_$s11Observation8ObservedV8IteratorVMa +_$s11Observation8ObservedV8IteratorVMn +_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedVMa +_$s11Observation8ObservedVMn +_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file From c1b5278b1e12d948afa11a8c6532f6996a6b98d4 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 21 Mar 2025 12:58:44 -0700 Subject: [PATCH 06/10] Stop using synchronization since that isnt supported on windows and use _ManagedCriticalState instead --- .../Sources/Observation/Observed.swift | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/stdlib/public/Observation/Sources/Observation/Observed.swift b/stdlib/public/Observation/Sources/Observation/Observed.swift index 87d60748c9b2c..f4e0f2e84760d 100644 --- a/stdlib/public/Observation/Sources/Observation/Observed.swift +++ b/stdlib/public/Observation/Sources/Observation/Observed.swift @@ -9,10 +9,7 @@ // //===----------------------------------------------------------------------===// -#if STANDALONE import Observation -#endif -import Synchronization import _Concurrency /// An asychronous sequence generated from a closure that tracks the transactional changes of `@Observable` types. @@ -20,22 +17,8 @@ import _Concurrency /// `Observed` conforms to `AsyncSequence`, providing a intutive and safe mechanism to track changes to /// types that are marked as `@Observable` by using Swift Concurrency to indicate transactional boundaries /// starting from the willSet of the first mutation to the next suspension point of the safe access. -#if !STANDALONE @available(SwiftStdlib 9999, *) -#endif public struct Observed: AsyncSequence, Sendable { - final class SharedState: Sendable { - let criticalRegion: Mutex - - init(_ state: consuming sending State) { - criticalRegion = Mutex(state) - } - - internal func withCriticalRegion(body: (inout sending State) throws(F) -> sending R) throws(F) -> R { - try criticalRegion.withLock(body) - } - } - struct State { enum Continuation { case cancelled @@ -56,7 +39,7 @@ public struct Observed: AsyncSequence, Sendab // Most likely, there wont be more than a handful of active iterations // so this only needs to be unique for those active iterations // that are in the process of calling next. - static func generation(_ state: SharedState) -> Int { + static func generation(_ state: _ManagedCriticalState) -> Int { state.withCriticalRegion { state in defer { state.id &+= 1 } return state.id @@ -66,7 +49,7 @@ public struct Observed: AsyncSequence, Sendab // the cancellation of awaiting on willSet only ferries in resuming early // it is the responsability of the caller to check if the task is actually // cancelled after awaiting the willSet to act accordingly. - static func cancel(_ state: SharedState, id: Int) { + static func cancel(_ state: _ManagedCriticalState, id: Int) { state.withCriticalRegion { state in guard let continuation = state.continuations.removeValue(forKey: id) else { // if there was no continuation yet active (e.g. it was cancelled at @@ -81,7 +64,7 @@ public struct Observed: AsyncSequence, Sendab // this atomically transitions the observation from a not yet tracked state // to a tracked state. No backwards transitions exist. - static func startTracking(_ state: SharedState) -> Bool { + static func startTracking(_ state: _ManagedCriticalState) -> Bool { state.withCriticalRegion { state in if !state.tracking { state.tracking = true @@ -94,7 +77,7 @@ public struct Observed: AsyncSequence, Sendab // fire off ALL awaiting willChange continuations such that they are no // longer pending. - static func emitWillChange(_ state: SharedState) { + static func emitWillChange(_ state: _ManagedCriticalState) { let continuations = state.withCriticalRegion { state in defer { state.continuations.removeAll() @@ -108,7 +91,7 @@ public struct Observed: AsyncSequence, Sendab // install a willChange continuation into the set of continuations // this must take a locally unique id (to the active calls of next) - static func willChange(_ state: SharedState, id: Int) async { + static func willChange(_ state: _ManagedCriticalState, id: Int) async { return await withUnsafeContinuation { continuation in state.withCriticalRegion { state in // first check if a cancelled tombstone exists, remove it, @@ -126,7 +109,7 @@ public struct Observed: AsyncSequence, Sendab } } - let state: SharedState + let state: _ManagedCriticalState let emit: @isolated(any) @Sendable () throws(Failure) -> Element? /// Constructs an asynchronous sequence for a given closure by tracking changes of `@Observable` types. @@ -147,19 +130,19 @@ public struct Observed: AsyncSequence, Sendab @_inheritActorContext _ emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element? ) { self.emit = emit - self.state = SharedState(State()) + self.state = _ManagedCriticalState(State()) } public struct Iterator: AsyncIteratorProtocol { // the state ivar serves two purposes: // 1) to store a critical region of state of the mutations // 2) to idenitify the termination of _this_ sequence - var state: SharedState? + var state: _ManagedCriticalState? let emit: @isolated(any) @Sendable () throws(Failure) -> Element? // this is the primary implementation of the tracking // it is bound to be called on the specified isolation of the construction - fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: SharedState, emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element?) throws(Failure) -> Element? { + fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: _ManagedCriticalState, emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element?) throws(Failure) -> Element? { // this ferries in an intermediate form with Result to skip over `withObservationTracking` not handling errors being thrown // particularly this case is that the error is also an iteration state transition data point (it terminates the sequence) // so we need to hold that to get a chance to catch and clean-up @@ -186,7 +169,7 @@ public struct Observed: AsyncSequence, Sendab } } - fileprivate mutating func trackEmission(isolation iterationIsolation: isolated (any Actor)?, state: SharedState, id: Int) async throws(Failure) -> Element? { + fileprivate mutating func trackEmission(isolation iterationIsolation: isolated (any Actor)?, state: _ManagedCriticalState, id: Int) async throws(Failure) -> Element? { guard !Task.isCancelled else { // the task was cancelled while awaiting a willChange so ensure a proper termination return try terminate(id: id) From 6438ff4d23b80aa63029c101c0280af1cb2d4506 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 26 Mar 2025 13:44:34 -0700 Subject: [PATCH 07/10] Correct abi assertions trailing new lines --- test/abi/Inputs/macOS/arm64/observation/baseline | 2 +- test/abi/Inputs/macOS/arm64/observation/baseline-asserts | 2 +- test/abi/Inputs/macOS/x86_64/observation/baseline | 2 +- test/abi/Inputs/macOS/x86_64/observation/baseline-asserts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline b/test/abi/Inputs/macOS/arm64/observation/baseline index a1484f6f4069e..aac839e573f8c 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline +++ b/test/abi/Inputs/macOS/arm64/observation/baseline @@ -40,4 +40,4 @@ _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn _$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file +_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts index a1484f6f4069e..aac839e573f8c 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts @@ -40,4 +40,4 @@ _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn _$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file +_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline b/test/abi/Inputs/macOS/x86_64/observation/baseline index a1484f6f4069e..aac839e573f8c 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline @@ -40,4 +40,4 @@ _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn _$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file +_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts index a1484f6f4069e..aac839e573f8c 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts @@ -40,4 +40,4 @@ _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn _$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc \ No newline at end of file +_$s11Observation8ObservedVyxq_GSciAAMc From 534fb0f0b0c1dc4888655e7ca7fcae7e709cc0a5 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 10 Apr 2025 10:04:57 -0700 Subject: [PATCH 08/10] Integrate feedback for making the initializer non-nullable and split off an `untilFinished` version that allows developers to specify when the iteration should terminate (including Optional elements) --- .../Sources/Observation/Observed.swift | 89 +++++++++++++++---- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/stdlib/public/Observation/Sources/Observation/Observed.swift b/stdlib/public/Observation/Sources/Observation/Observed.swift index f4e0f2e84760d..569d595b9f5e8 100644 --- a/stdlib/public/Observation/Sources/Observation/Observed.swift +++ b/stdlib/public/Observation/Sources/Observation/Observed.swift @@ -19,6 +19,16 @@ import _Concurrency /// starting from the willSet of the first mutation to the next suspension point of the safe access. @available(SwiftStdlib 9999, *) public struct Observed: AsyncSequence, Sendable { + /// An indication type for specifying if an element is the next element or a finishing of iteration + /// + /// This is used in conjunction with `Observed.untilFinished` to emit values until a `.finish` is + /// returned. All other elements in the observation emit closure for `untilFinished` should return + /// `.next(element)` to indicate a properly formed next element from the observation closure. + public enum Iteration: Sendable { + case next(Element) + case finish + } + struct State { enum Continuation { case cancelled @@ -109,28 +119,67 @@ public struct Observed: AsyncSequence, Sendab } } + // @isolated(any) closures cannot be composed and retain or forward their isolation + // this basically would be replaced with `{ .next(elementProducer()) }` if that + // were to become possible. + enum Emit { + case iteration(@isolated(any) @Sendable () throws(Failure) -> Iteration) + case element(@isolated(any) @Sendable () throws(Failure) -> Element) + + var isolation: (any Actor)? { + switch self { + case .iteration(let closure): closure.isolation + case .element(let closure): closure.isolation + } + } + } + let state: _ManagedCriticalState - let emit: @isolated(any) @Sendable () throws(Failure) -> Element? + let emit: Emit + + // internal funnel method for initialziation + internal init(emit: Emit) { + self.emit = emit + self.state = _ManagedCriticalState(State()) + } /// Constructs an asynchronous sequence for a given closure by tracking changes of `@Observable` types. /// - /// The emit closure is responsible for extracting a value out of a single or many `@Observable` types. When `nil` is - /// returned the sequence terminates and no more values will be produced. Likewise when an error is thrown, iterations will - /// catch that error and the sequence will be in a terminal state, no longer producing values. + /// The emit closure is responsible for extracting a value out of a single or many `@Observable` types. When + /// this initializer is invoked the closure inherits the current actor isolation and subseqent calls made + /// internally by `Observed` re-invoke the closure on that isolation if present. This means that if the + /// `Observed` is constructed on the `@MainActor`, all following calls to the emit closure will also be + /// isolated to the `@MainActor` and likewise for other isolations. /// - /// When constructing an `Observed` asynchronous sequence it is strongly suggested that the isolation parameter is left - /// to the default value. Other values specified are only for advanced and unique use cases and may result in unexpected - /// behavior. + /// In the case that this method is used in a `nonisolated` context it then means that the usage point + /// must maintain rules pertaining to the `Sendable` nature of captured types. This method and other + /// parts of `Observed` do not add additional concurrency protection for these cases; so types must + /// be safe to maintain the safe construction and usage of `Observed` when called in an explicitly + /// `nonisolated` isolation domain. /// /// - Parameters: /// - isolation: The concurrency isolation domain of the caller. /// - emit: A closure to generate an element for the sequence. - /// public init( - @_inheritActorContext _ emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element? + @_inheritActorContext _ emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element ) { - self.emit = emit - self.state = _ManagedCriticalState(State()) + self.init(emit: .element(emit)) + } + + /// Constructs an asynchronous sequence for a given closure by tracking changes of `@Observable` types. + /// + /// The emit closure is responsible for extracting a value out of a single or many `@Observable` types. This method + /// continues to be invoked until the .finished option is returned or an error is thrown. Additionally the emit + /// closure follows the same isolation rules as the initializer form; where isolation is preserved from the + /// initial invocation and restored if present to ensure the closure is always isolated to the initial construction + /// isolation domain. + /// + /// - Parameters: + /// - emit: A closure to generate an element for the sequence. + public static func untilFinished( + @_inheritActorContext _ emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Iteration + ) -> Observed { + .init(emit: .iteration(emit)) } public struct Iterator: AsyncIteratorProtocol { @@ -138,16 +187,21 @@ public struct Observed: AsyncSequence, Sendab // 1) to store a critical region of state of the mutations // 2) to idenitify the termination of _this_ sequence var state: _ManagedCriticalState? - let emit: @isolated(any) @Sendable () throws(Failure) -> Element? + let emit: Emit // this is the primary implementation of the tracking // it is bound to be called on the specified isolation of the construction - fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: _ManagedCriticalState, emit: @escaping @isolated(any) @Sendable () throws(Failure) -> Element?) throws(Failure) -> Element? { + fileprivate static func trackEmission(isolation trackingIsolation: isolated (any Actor)?, state: _ManagedCriticalState, emit: Emit) throws(Failure) -> Iteration { // this ferries in an intermediate form with Result to skip over `withObservationTracking` not handling errors being thrown // particularly this case is that the error is also an iteration state transition data point (it terminates the sequence) // so we need to hold that to get a chance to catch and clean-up let result = withObservationTracking { - Result(catching: emit) + switch emit { + case .element(let element): + Result(catching: element).map { Iteration.next($0) } + case .iteration(let iteration): + Result(catching: iteration) + } } onChange: { [state] in // resume all cases where the awaiting continuations are awaiting a willSet State.emitWillChange(state) @@ -175,11 +229,10 @@ public struct Observed: AsyncSequence, Sendab return try terminate(id: id) } // start by directly tracking the emission via a withObservation tracking on the isolation specified fro mthe init - guard let element = try await Iterator.trackEmission(isolation: emit.isolation, state: state, emit: emit) else { - // the user returned a nil from the closure so terminate the sequence - return try terminate(id: id) + switch try await Iterator.trackEmission(isolation: emit.isolation, state: state, emit: emit) { + case .finish: return try terminate(id: id) + case .next(let element): return element } - return element } public mutating func next(isolation iterationIsolation: isolated (any Actor)? = #isolation) async throws(Failure) -> Element? { From 508aedc25b30dd5433bd7d9d7d6e8aa6a48a825e Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 22 Apr 2025 16:31:01 -0700 Subject: [PATCH 09/10] Update ABI assertions for the latest revision of the interfaces --- test/abi/Inputs/macOS/arm64/observation/baseline | 7 ++++++- test/abi/Inputs/macOS/arm64/observation/baseline-asserts | 7 ++++++- test/abi/Inputs/macOS/x86_64/observation/baseline | 7 ++++++- test/abi/Inputs/macOS/x86_64/observation/baseline-asserts | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline b/test/abi/Inputs/macOS/arm64/observation/baseline index aac839e573f8c..73c2d1058b3ca 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline +++ b/test/abi/Inputs/macOS/arm64/observation/baseline @@ -31,13 +31,18 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ _$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu _$s11Observation8ObservedV8IteratorVMa _$s11Observation8ObservedV8IteratorVMn _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationOMa +_$s11Observation8ObservedV9IterationOMn _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC _$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts index aac839e573f8c..73c2d1058b3ca 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts @@ -31,13 +31,18 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ _$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu _$s11Observation8ObservedV8IteratorVMa _$s11Observation8ObservedV8IteratorVMn _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationOMa +_$s11Observation8ObservedV9IterationOMn _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC _$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline b/test/abi/Inputs/macOS/x86_64/observation/baseline index aac839e573f8c..73c2d1058b3ca 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline @@ -31,13 +31,18 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ _$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu _$s11Observation8ObservedV8IteratorVMa _$s11Observation8ObservedV8IteratorVMn _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationOMa +_$s11Observation8ObservedV9IterationOMn _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC _$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts index aac839e573f8c..73c2d1058b3ca 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts @@ -31,13 +31,18 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp +_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ _$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu _$s11Observation8ObservedV8IteratorVMa _$s11Observation8ObservedV8IteratorVMn _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc +_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC +_$s11Observation8ObservedV9IterationOMa +_$s11Observation8ObservedV9IterationOMn _$s11Observation8ObservedVMa _$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxSgyYbq_YKYAccfC +_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC _$s11Observation8ObservedVyxq_GSciAAMc From 39df5db3c3f7acbeb110d2d712e90d33ff692f55 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 22 Apr 2025 16:40:29 -0700 Subject: [PATCH 10/10] Correct the abi assertions to be the tests instead of altering the baselines --- .../Inputs/macOS/arm64/observation/baseline | 15 ------- .../macOS/arm64/observation/baseline-asserts | 15 ------- .../Inputs/macOS/x86_64/observation/baseline | 15 ------- .../macOS/x86_64/observation/baseline-asserts | 15 ------- test/abi/macOS/arm64/observation.swift | 45 +++++++++++++++++++ test/abi/macOS/x86_64/observation.swift | 45 +++++++++++++++++++ 6 files changed, 90 insertions(+), 60 deletions(-) diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline b/test/abi/Inputs/macOS/arm64/observation/baseline index 73c2d1058b3ca..567b0e3345cf7 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline +++ b/test/abi/Inputs/macOS/arm64/observation/baseline @@ -31,18 +31,3 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp -_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ -_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu -_$s11Observation8ObservedV8IteratorVMa -_$s11Observation8ObservedV8IteratorVMn -_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc -_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationOMa -_$s11Observation8ObservedV9IterationOMn -_$s11Observation8ObservedVMa -_$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts index 73c2d1058b3ca..567b0e3345cf7 100644 --- a/test/abi/Inputs/macOS/arm64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/arm64/observation/baseline-asserts @@ -31,18 +31,3 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp -_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ -_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu -_$s11Observation8ObservedV8IteratorVMa -_$s11Observation8ObservedV8IteratorVMn -_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc -_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationOMa -_$s11Observation8ObservedV9IterationOMn -_$s11Observation8ObservedVMa -_$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline b/test/abi/Inputs/macOS/x86_64/observation/baseline index 73c2d1058b3ca..567b0e3345cf7 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline @@ -31,18 +31,3 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp -_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ -_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu -_$s11Observation8ObservedV8IteratorVMa -_$s11Observation8ObservedV8IteratorVMn -_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc -_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationOMa -_$s11Observation8ObservedV9IterationOMn -_$s11Observation8ObservedVMa -_$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts index 73c2d1058b3ca..567b0e3345cf7 100644 --- a/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts +++ b/test/abi/Inputs/macOS/x86_64/observation/baseline-asserts @@ -31,18 +31,3 @@ _$s11Observation0A9RegistrarVSHAAMc _$s11Observation0A9RegistrarVSQAAMc _$s11Observation0A9RegistrarVSeAAMc _$s11Observation10ObservableMp -_$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ -_$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF -_$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu -_$s11Observation8ObservedV8IteratorVMa -_$s11Observation8ObservedV8IteratorVMn -_$s11Observation8ObservedV8IteratorVyxq__GScIAAMc -_$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC -_$s11Observation8ObservedV9IterationOMa -_$s11Observation8ObservedV9IterationOMn -_$s11Observation8ObservedVMa -_$s11Observation8ObservedVMn -_$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC -_$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/macOS/arm64/observation.swift b/test/abi/macOS/arm64/observation.swift index cc193b65629fe..6df1ad678a4fd 100644 --- a/test/abi/macOS/arm64/observation.swift +++ b/test/abi/macOS/arm64/observation.swift @@ -42,3 +42,48 @@ Added: _$s11Observation0A8TrackingV7changeds10AnyKeyPathCSgvg // property descriptor for Observation.ObservationTracking.changed : Swift.AnyKeyPath? Added: _$s11Observation0A8TrackingV7changeds10AnyKeyPathCSgvpMV + +// static Observation.Observed.untilFinished(@isolated(any) @Sendable () throws(B) -> Observation.Observed.Iteration) -> Observation.Observed +Added: _$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ + +// Observation.Observed.makeAsyncIterator() -> Observation.Observed.Iterator +Added: _$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF + +// Observation.Observed.Iterator.next(isolation: isolated Swift.Actor?) async throws(B) -> A? +Added: _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF + +// async function pointer to Observation.Observed.Iterator.next(isolation: isolated Swift.Actor?) async throws(B) -> A? +Added: _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu + +// type metadata accessor for Observation.Observed.Iterator +Added: _$s11Observation8ObservedV8IteratorVMa + +// nominal type descriptor for Observation.Observed.Iterator +Added: _$s11Observation8ObservedV8IteratorVMn + +// protocol conformance descriptor for Observation.Observed.Iterator : Swift.AsyncIteratorProtocol in Observation +Added: _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc + +// enum case for Observation.Observed.Iteration.next(Observation.Observed.Iteration.Type) -> (A) -> Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC + +// enum case for Observation.Observed.Iteration.finish(Observation.Observed.Iteration.Type) -> Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC + +// type metadata accessor for Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationOMa + +// nominal type descriptor for Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationOMn + +// type metadata accessor for Observation.Observed +Added: _$s11Observation8ObservedVMa + +// nominal type descriptor for Observation.Observed +Added: _$s11Observation8ObservedVMn + +// Observation.Observed.init(@isolated(any) @Sendable () throws(B) -> A) -> Observation.Observed +Added: _$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC + +// protocol conformance descriptor for Observation.Observed : Swift.AsyncSequence in Observation +Added: _$s11Observation8ObservedVyxq_GSciAAMc diff --git a/test/abi/macOS/x86_64/observation.swift b/test/abi/macOS/x86_64/observation.swift index 0c80ebce005a5..e6fa48f6195ff 100644 --- a/test/abi/macOS/x86_64/observation.swift +++ b/test/abi/macOS/x86_64/observation.swift @@ -41,3 +41,48 @@ Added: _$s11Observation0A8TrackingV7changeds10AnyKeyPathCSgvg // property descriptor for Observation.ObservationTracking.changed : Swift.AnyKeyPath? Added: _$s11Observation0A8TrackingV7changeds10AnyKeyPathCSgvpMV + +// static Observation.Observed.untilFinished(@isolated(any) @Sendable () throws(B) -> Observation.Observed.Iteration) -> Observation.Observed +Added: _$s11Observation8ObservedV13untilFinishedyACyxq_GAC9IterationOyxq__GyYbq_YKYAcFZ + +// Observation.Observed.makeAsyncIterator() -> Observation.Observed.Iterator +Added: _$s11Observation8ObservedV17makeAsyncIteratorAC0E0Vyxq__GyF + +// Observation.Observed.Iterator.next(isolation: isolated Swift.Actor?) async throws(B) -> A? +Added: _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKF + +// async function pointer to Observation.Observed.Iterator.next(isolation: isolated Swift.Actor?) async throws(B) -> A? +Added: _$s11Observation8ObservedV8IteratorV4next9isolationxSgScA_pSgYi_tYaq_YKFTu + +// type metadata accessor for Observation.Observed.Iterator +Added: _$s11Observation8ObservedV8IteratorVMa + +// nominal type descriptor for Observation.Observed.Iterator +Added: _$s11Observation8ObservedV8IteratorVMn + +// protocol conformance descriptor for Observation.Observed.Iterator : Swift.AsyncIteratorProtocol in Observation +Added: _$s11Observation8ObservedV8IteratorVyxq__GScIAAMc + +// enum case for Observation.Observed.Iteration.next(Observation.Observed.Iteration.Type) -> (A) -> Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationO4nextyAEyxq__GxcAGms8SendableRzs5ErrorR_r0_lFWC + +// enum case for Observation.Observed.Iteration.finish(Observation.Observed.Iteration.Type) -> Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationO6finishyAEyxq__GAGms8SendableRzs5ErrorR_r0_lFWC + +// type metadata accessor for Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationOMa + +// nominal type descriptor for Observation.Observed.Iteration +Added: _$s11Observation8ObservedV9IterationOMn + +// type metadata accessor for Observation.Observed +Added: _$s11Observation8ObservedVMa + +// nominal type descriptor for Observation.Observed +Added: _$s11Observation8ObservedVMn + +// Observation.Observed.init(@isolated(any) @Sendable () throws(B) -> A) -> Observation.Observed +Added: _$s11Observation8ObservedVyACyxq_GxyYbq_YKYAccfC + +// protocol conformance descriptor for Observation.Observed : Swift.AsyncSequence in Observation +Added: _$s11Observation8ObservedVyxq_GSciAAMc