From ffa6ee9f0bec1ebe569bd8815e6a7d406e929e53 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Wed, 15 Feb 2023 23:32:12 +0100 Subject: [PATCH 01/32] evolution: add buffer (#252) --- Evolution/0009-buffer.md | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 Evolution/0009-buffer.md diff --git a/Evolution/0009-buffer.md b/Evolution/0009-buffer.md new file mode 100644 index 00000000..0e2b2fd4 --- /dev/null +++ b/Evolution/0009-buffer.md @@ -0,0 +1,114 @@ +# Buffer + +* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-buffer.md) +* Author(s): [Thibault Wittemberg](https://github.com/twittemb) +* Status: **Implemented** +* Implementation: [ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBuffer.swift) +] +* Decision Notes: +* Bugs: + +## Introduction + +Buffering is a technique that balances supply and demand by temporarily storing elements to even out fluctuations in production and consumption rates. `AsyncStream` facilitates this process by allowing you to control the size of the buffer and the strategy for handling elements that exceed that size. However, this approach may not be suitable for all situations, and it doesn't provide a way to adapt other `AsyncSequence` types to incorporate buffering. + +This proposal presents a new type that addresses these more advanced requirements and offers a comprehensive solution for buffering in asynchronous sequences. + +## Motivation + +As an `AsyncSequence` operates as a pull system, the production of elements is directly tied to the demand expressed by the consumer. The slower the consumer is in requesting elements, the slower the production of these elements will be. This can negatively impact the software that produces the elements, as demonstrated in the following example. + +Consider an `AsyncSequence` that reads and returns a line from a file every time a new element is requested. To ensure exclusive access to the file, a lock is maintained while reading. Ideally, the lock should be held for as short a duration as possible to allow other processes to access the file. However, if the consumer is slow in processing received lines or has a fluctuating pace, the lock will be maintained for an extended period, reducing the performance of the system. + +To mitigate this issue, a buffer operator can be employed to streamline the consumption of the `AsyncSequence`. This operator allows an internal iteration of the `AsyncSequence` independently from the consumer. Each element would then be made available for consumption by using a queuing mechanism. + +By applying the buffer operator to the previous example, the file can be read as efficiently as possible, allowing the lock to be released in a timely manner. This results in improved performance, as the consumer can consume elements at its own pace without negatively affecting the system. The buffer operator ensures that the production of elements is not limited by the pace of the consumer, allowing both the producer and consumer to operate at optimal levels. + +## Proposed Solution + +We propose to extend `AsyncSequence` with a `buffer()` operator. This operator will return an `AsyncBuffereSequence` that wraps the source `AsyncSequence` and handle the buffering mechanism. + +This operator will accept an `AsyncBufferSequencePolicy`. The policy will dictate the behaviour in case of a buffer overflow. + +As of now we propose 4 different behaviours: + +```swift +public struct AsyncBufferSequencePolicy: Sendable { + public static func bounded(_ limit: Int) + public static var unbounded + public static func bufferingLatest(_ limit: Int) + public static func bufferingOldest(_ limit: Int) +} +``` + +And the public API of `AsyncBuffereSequence` will be: + +```swift +extension AsyncSequence where Self: Sendable { + public func buffer( + policy: AsyncBufferSequencePolicy + ) -> AsyncBufferSequence { + AsyncBufferSequence(base: self, policy: policy) + } +} + +public struct AsyncBufferSequence: AsyncSequence { + public typealias Element = Base.Element + + public func makeAsyncIterator() -> Iterator + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } +} + +extension AsyncBufferSequence: Sendable where Base: Sendable { } +``` + +## Notes on Sendable + +Since all buffering means that the base asynchronous sequence must be iterated independently of the consumption (to resolve the production versus consumption issue) the base `AsyncSequence` needs to be able to be sent across task boundaries (the iterator does not need this requirement). + +## Detailed Design + +The choice of the buffering policy is made through an enumeration whose values are related to a storage type. To date, two types of storage are implemented: + +- `BoundedBufferStorage` backing the `AsyncBufferSequencePolicy.bounded(_:)` policy, +- `UnboundedBufferStorage` backing the `AsyncBufferSequencePolicy.unbounded`, `AsyncBufferSequencePolicy.bufferingNewest(_:)` and `AsyncBufferSequencePolicy.bufferingLatest(_:)` policies. + +Both storage types rely on a Mealy state machine stored in a `ManagedCriticalState`. They drive the mutations of the internal buffer, while ensuring that concurrent access to the state are safe. + +### BoundedBufferStorage + +`BoundedBufferStorage` is instantiated with the upstream `AsyncSequence` and a buffer maximum size. Upon the first call to the `next()` method, a task is spawned to support the iteration over this `AsyncSequence`. The iteration retrieves elements and adds them to an internal buffer as long as the buffer limit has not been reached. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case, the iteration is temporarily suspended until additional space becomes available. + +### UnboundedBufferStorage + +`UnboundedBufferStorage` is instantiated with the upstream `AsyncSequence` and a buffering policy. Upon the first call to the `next()` method, a task is spawned to support the iteration over this `AsyncSequence`. + +From there the behaviour will depend on the buffering policy. + +#### Unbounded +If the policy is `unbounded`, the iteration retrieves elements and adds them to an internal buffer until the upstream `AsyncSequence` finishes or fails. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. + +#### BufferingLatest +If the policy is `bufferingLatest(_:)`, the iteration retrieves elements and adds them to an internal buffer. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case the oldest buffered elements will be removed from the buffer and the latest ones will be added. + +#### BufferingOldest +If the policy is `bufferingOldest(_:)`, the iteration retrieves elements and adds them to an internal buffer. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case the latest element will be discarded and never added to the buffer. + +### Terminal events and cancellation + +Terminal events from the upstream `AsyncSequence`, such as normal completion or failure, are delayed, ensuring that the consumer will receive all the elements before receiving the termination. + +If the consuming `Task` is cancelled, so will be the `Task` supporting the iteration of the upstream `AsyncSequence`. + +## Alternatives Considered + +The buffering mechanism was originally thought to rely on an open implementation of an `AsyncBuffer` protocol, which would be constrained to an `Actor` conformance. It was meant for developers to be able to provide their own implementation of a buffering algorithm. This buffer was designed to control the behavior of elements when pushed and popped, in an isolated manner. + +A default implementation was provided to offers a queuing strategy for buffering elements, with options for unbounded, oldest-first, or newest-first buffering. + +This implementation was eventually discarded due to the potentially higher cost of calling isolated functions on an `Actor`, compared to using a low-level locking mechanism like the one employed in other operators through the use of the `ManagedCriticalState`. From 7e450054733d2335647d9ca7889540606b1b1166 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 28 Feb 2023 08:35:03 +0000 Subject: [PATCH 02/32] Switch from `group.waitForAll()` to `group.next()` (#254) # Motivation Swift 5.8 is including a change to how `group.waitForAll()` is working. It now properly waits for all tasks to finish even if one of the tasks throws. We have used `group.waitForAll()` in multiple places and need to change this code accordingly. # Modification Switch code from `group.waitForAll()` to `group.next()`. # Result This fixes a few stuck tests that we have seen when running against development snapshots. --- .../CombineLatestStateMachine.swift | 3 +- .../CombineLatest/CombineLatestStorage.swift | 54 ++++++++-------- .../Debounce/DebounceStateMachine.swift | 4 +- .../Debounce/DebounceStorage.swift | 64 ++++++++++--------- .../AsyncAlgorithms/Merge/MergeStorage.swift | 63 +++++++++--------- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 48 +++++++------- 6 files changed, 118 insertions(+), 118 deletions(-) diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 34f4dead..71d0507a 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -530,7 +530,8 @@ struct CombineLatestStateMachine< preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") case .upstreamsFinished: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + // We need to tolerate multiple upstreams failing + return .none case .waitingForDemand(let task, let upstreams, _): // An upstream threw. We can cancel everything now and transition to finished. diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index 31245579..bc32cee8 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -319,35 +319,33 @@ final class CombineLatestStorage< } } - do { - try await group.waitForAll() - } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - - switch action { - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let error, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .none: - break - } - } + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamThrew(error) + switch action { + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let error, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + downstreamContinuation.resume(returning: .failure(error)) + case .none: + break + } + } - group.cancelAll() + group.cancelAll() + } } } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index bd111ab1..98c23287 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -390,8 +390,8 @@ struct DebounceStateMachine { preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") case .upstreamFailure: - // The upstream already failed so it should never have throw again. - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + // We need to tolerate multiple upstreams failing + return .none case .waitingForDemand(let task, .none, let clockContinuation, .none): // We don't have any buffered element so we can just go ahead diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 21e1ddf6..cd1bb515 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -254,39 +254,41 @@ final class DebounceStorage: @unchecked Sendable } } - do { - try await group.waitForAll() - } catch { - // The upstream sequence threw an error - let action = self.stateMachine.withCriticalRegion { $0.upstreamThrew(error) } + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamThrew(error) + switch action { + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + let downstreamContinuation, + let error, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) - switch action { - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - let downstreamContinuation, - let error, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .cancelTaskAndClockContinuation( - let task, - let clockContinuation - ): - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - - case .none: - break - } + task.cancel() + + downstreamContinuation.resume(returning: .failure(error)) - group.cancelAll() + case .cancelTaskAndClockContinuation( + let task, + let clockContinuation + ): + clockContinuation?.resume(throwing: CancellationError()) + task.cancel() + case .none: + break + } + } + + group.cancelAll() + } } } } diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index de4c72b8..7a83ad8b 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -404,40 +404,39 @@ final class MergeStorage< } } } - - do { - try await group.waitForAll() - } catch { + + while !group.isEmpty { + do { + try await group.next() + } catch { // One of the upstream sequences threw an error - let action = self.lock.withLock { - self.stateMachine.upstreamThrew(error) - } - - switch action { - case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - error, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - - task.cancel() - - downstreamContinuation.resume(throwing: error) - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - - task.cancel() - - case .none: - break + let action = self.lock.withLock { + self.stateMachine.upstreamThrew(error) + } + switch action { + case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + error, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + + downstreamContinuation.resume(throwing: error) + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + case .none: + break + } + group.cancelAll() } - - group.cancelAll() } } } diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index d9fb3a82..f997e681 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -286,31 +286,31 @@ final class ZipStorage Date: Thu, 9 Mar 2023 10:40:27 +0000 Subject: [PATCH 03/32] Package.swift: make package name consistent (#255) Rest of SwiftPM packages provided by Apple follow a dash-case naming scheme with `swift-` prefix. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4f716a85..a1562d76 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( - name: "AsyncAlgorithms", + name: "swift-async-algorithms", platforms: [ .macOS("10.15"), .iOS("13.0"), From 9d83fff5b8cdf4dedaf006d0c72bb93595815126 Mon Sep 17 00:00:00 2001 From: elmetal Date: Wed, 10 May 2023 21:05:56 +0900 Subject: [PATCH 04/32] [Channel] Fix Source links in documentation (#264) --- .../AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md index 19eed41c..dc2c2e66 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md @@ -3,8 +3,8 @@ * Author(s): [Philippe Hausler](https://github.com/phausler) [ -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChannel.swift), -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift) | +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift), +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChannel.swift) ] From 0c00af2372d340a152d6cf5cd359d927daaaf64e Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 10 May 2023 16:10:31 +0400 Subject: [PATCH 05/32] Clean up old evolution proposals (#258) # Motivation Our old proposals had a couple of stale links and were not all aligned. This cleans them up. --- Evolution/0001-zip.md | 10 ++++------ Evolution/0002-merge.md | 10 ++++------ Evolution/0003-compacted.md | 4 ++-- Evolution/0004-joined.md | 2 +- Evolution/0005-adjacent-pairs.md | 2 +- Evolution/0006-combineLatest.md | 10 ++++------ Evolution/0007-chain.md | 2 +- Evolution/0008-bytes.md | 4 ++-- 0009-async.md => Evolution/0009-async.md | 2 +- Evolution/{0009-buffer.md => 0010-buffer.md} | 4 ++-- 10 files changed, 22 insertions(+), 28 deletions(-) rename 0009-async.md => Evolution/0009-async.md (94%) rename Evolution/{0009-buffer.md => 0010-buffer.md} (98%) diff --git a/Evolution/0001-zip.md b/Evolution/0001-zip.md index 1ba48205..2e1885c5 100644 --- a/Evolution/0001-zip.md +++ b/Evolution/0001-zip.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0001](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0001-zip.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestZip.swift)] * Decision Notes: * Bugs: @@ -45,8 +45,7 @@ public func zip: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -59,8 +58,7 @@ public struct AsyncZip2Sequence: Sen public struct AsyncZip3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0002-merge.md b/Evolution/0002-merge.md index ff15f23d..b3dacfc5 100644 --- a/Evolution/0002-merge.md +++ b/Evolution/0002-merge.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0002](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0002-merge.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Asyncmerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)] * Decision Notes: * Bugs: @@ -46,8 +46,7 @@ public struct AsyncMerge2Sequence: S where Base1.Element == Base2.Element, Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = Base1.Element public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncMerge3Sequence: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncCombineLatest2Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0007-chain.md b/Evolution/0007-chain.md index 8df4804f..df2aa7c1 100644 --- a/Evolution/0007-chain.md +++ b/Evolution/0007-chain.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0007](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0007-chain.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] diff --git a/Evolution/0008-bytes.md b/Evolution/0008-bytes.md index 76e2f3a0..50e2cf28 100644 --- a/Evolution/0008-bytes.md +++ b/Evolution/0008-bytes.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0008](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0008-bytes.md) * Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] @@ -33,7 +33,7 @@ struct AsyncBytes: AsyncSequence { ## Detailed Design ```swift -public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { +public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 public init( diff --git a/0009-async.md b/Evolution/0009-async.md similarity index 94% rename from 0009-async.md rename to Evolution/0009-async.md index 897e57b8..a29984ab 100644 --- a/0009-async.md +++ b/Evolution/0009-async.md @@ -1,6 +1,6 @@ # AsyncSyncSequence -* Proposal: [NNNN](NNNN-lazy.md) +* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-async.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** diff --git a/Evolution/0009-buffer.md b/Evolution/0010-buffer.md similarity index 98% rename from Evolution/0009-buffer.md rename to Evolution/0010-buffer.md index 0e2b2fd4..da56def7 100644 --- a/Evolution/0009-buffer.md +++ b/Evolution/0010-buffer.md @@ -1,8 +1,8 @@ # Buffer -* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-buffer.md) +* Proposal: [SAA-0010](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0010-buffer.md) * Author(s): [Thibault Wittemberg](https://github.com/twittemb) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [ [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBuffer.swift) From b3394663fd8ad9c701cbfcffcaedb64afc7e92be Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 10 May 2023 18:18:41 +0400 Subject: [PATCH 06/32] Add Interspersed proposal (#259) * Clean up old evolution proposals # Motivation Our old proposals had a couple of stale links and were not all aligned. This cleans them up. * Add Interspersed proposal # Motivation Add a new evolution proposal for the interspersed algorithm. --- Evolution/0011-interspersed.md | 135 ++++++++++++++++++ .../AsyncInterspersedSequence.swift | 22 ++- .../{ => Interspersed}/TestInterspersed.swift | 39 ++--- 3 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 Evolution/0011-interspersed.md rename Sources/AsyncAlgorithms/{ => Interspersed}/AsyncInterspersedSequence.swift (81%) rename Tests/AsyncAlgorithmsTests/{ => Interspersed}/TestInterspersed.swift (71%) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md new file mode 100644 index 00000000..79cc02f2 --- /dev/null +++ b/Evolution/0011-interspersed.md @@ -0,0 +1,135 @@ +# Feature name + +* Proposal: [SAA-0011](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0011-interspersed.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Review Manager: [Franz Busch](https://github.com/FranzBusch) +* Status: **Implemented** + +* Implementation: + [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift) + +## Motivation + +A common transformation that is applied to async sequences is to intersperse the elements with +a separator element. + +## Proposed solution + +We propose to add a new method on `AsyncSequence` that allows to intersperse +a separator between each emitted element. This proposed API looks like this + +```swift +extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameter separator: The value to insert in between each of this async + /// sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, separator: separator) + } +} +``` + +## Detailed design + +The bulk of the implementation of the new `interspersed` method is inside the new +`AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence +inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand +to the base iterator. +There is one special case that we have to call out. When the base async sequence throws +then `AsyncInterspersedSequence.Iterator.next()` will return the separator first and then rethrow the error. + +Below is the implementation of the `AsyncInterspersedSequence`. +```swift +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncInterspersedSequence { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Base.Element + + @usableFromInline + internal init(_ base: Base, separator: Base.Element) { + self.base = base + self.separator = separator + } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start + case element(Result) + case separator + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Base.Element + + @usableFromInline + internal var state = State.start + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { + self.iterator = iterator + self.separator = separator + } + + public mutating func next() async rethrows -> Base.Element? { + // After the start, the state flips between element and separator. Before + // returning a separator, a check is made for the next element as a + // separator is only returned between two elements. The next element is + // stored to allow it to be returned in the next iteration. However, if + // the checking the next element throws, the separator is emitted before + // rethrowing that error. + switch state { + case .start: + state = .separator + return try await iterator.next() + case .separator: + do { + guard let next = try await iterator.next() else { return nil } + state = .element(.success(next)) + } catch { + state = .element(.failure(error)) + } + return separator + case .element(let result): + state = .separator + return try result._rethrowGet() + } + } + } + + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.AsyncIterator { + AsyncIterator(base.makeAsyncIterator(), separator: separator) + } +} +``` diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift similarity index 81% rename from Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift rename to Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 43f8d974..7dd08c53 100644 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -10,10 +10,21 @@ //===----------------------------------------------------------------------===// extension AsyncSequence { - /// Returns an asynchronous sequence containing elements of this asynchronous sequence with - /// the given separator inserted in between each element. + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. /// - /// Any value of the asynchronous sequence's element type can be used as the separator. + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` /// /// - Parameter separator: The value to insert in between each of this async /// sequence’s elements. @@ -95,8 +106,11 @@ extension AsyncInterspersedSequence: AsyncSequence { @inlinable public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) + Iterator(base.makeAsyncIterator(), separator: separator) } } extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift similarity index 71% rename from Tests/AsyncAlgorithmsTests/TestInterspersed.swift rename to Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 79c93f73..2f351ab0 100644 --- a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -66,26 +66,33 @@ final class TestInterspersed: XCTestCase { func test_cancellation() async { let source = Indefinite(value: "test") let sequence = source.async.interspersed(with: "sep") - let finished = expectation(description: "finished") - let iterated = expectation(description: "iterated") - let task = Task { + let lockStepChannel = AsyncChannel() - var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() - iterated.fulfill() + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() - while let _ = await iterator.next() { } + // Information the parent task that we are consuming + await lockStepChannel.send(()) - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) + while let _ = await iterator.next() { } - finished.fulfill() + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + + // Information the parent task that we finished consuming + await lockStepChannel.send(()) + } + + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } + + // Now we cancel the child + group.cancelAll() + + // Waiting until the child task finished consuming + _ = await lockStepChannel.first { _ in true } } - // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) - // cancellation should ensure the loop finishes - // without regards to the remaining underlying sequence - task.cancel() - wait(for: [finished], timeout: 1.0) } } From ac9309b2ac88ccb6e96c8add8c4e943cf439cc48 Mon Sep 17 00:00:00 2001 From: Kazumasa Shimomura Date: Fri, 23 Jun 2023 04:54:24 +0900 Subject: [PATCH 07/32] Rename `downStream` to `downstream` (#263) --- .../CombineLatest/CombineLatestStorage.swift | 6 +++--- .../AsyncAlgorithms/Debounce/DebounceStateMachine.swift | 8 ++++---- Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift | 4 ++-- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index bc32cee8..d3b67404 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -58,7 +58,7 @@ final class CombineLatestStorage< base1: base1, base2: base2, base3: base3, - downStreamContinuation: continuation + downstreamContinuation: continuation ) case .resumeContinuation(let downstreamContinuation, let result): @@ -108,7 +108,7 @@ final class CombineLatestStorage< base1: Base1, base2: Base2, base3: Base3?, - downStreamContinuation: StateMachine.DownstreamContinuation + downstreamContinuation: StateMachine.DownstreamContinuation ) { // This creates a new `Task` that is iterating the upstream // sequences. We must store it to cancel it at the right times. @@ -350,6 +350,6 @@ final class CombineLatestStorage< } } - stateMachine.taskIsStarted(task: task, downstreamContinuation: downStreamContinuation) + stateMachine.taskIsStarted(task: task, downstreamContinuation: downstreamContinuation) } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index 98c23287..b75d2f3e 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -517,8 +517,8 @@ struct DebounceStateMachine { /// Actions returned by `clockSleepFinished()`. enum ClockSleepFinishedAction { /// Indicates that the downstream continuation should be resumed with the given element. - case resumeDownStreamContinuation( - downStreamContinuation: UnsafeContinuation, Never>, + case resumeDownstreamContinuation( + downstreamContinuation: UnsafeContinuation, Never>, element: Element ) } @@ -547,8 +547,8 @@ struct DebounceStateMachine { bufferedElement: nil ) - return .resumeDownStreamContinuation( - downStreamContinuation: downstreamContinuation, + return .resumeDownstreamContinuation( + downstreamContinuation: downstreamContinuation, element: currentElement.element ) } else { diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index cd1bb515..40c30634 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -238,8 +238,8 @@ final class DebounceStorage: @unchecked Sendable let action = self.stateMachine.withCriticalRegion { $0.clockSleepFinished() } switch action { - case .resumeDownStreamContinuation(let downStreamContinuation, let element): - downStreamContinuation.resume(returning: .success(element)) + case .resumeDownstreamContinuation(let downstreamContinuation, let element): + downstreamContinuation.resume(returning: .success(element)) case .none: break diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index f997e681..7d971a78 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -49,7 +49,7 @@ final class ZipStorage Date: Fri, 23 Jun 2023 06:40:09 -0700 Subject: [PATCH 08/32] Audit pass on inline and initialization (#271) --- .../AsyncInclusiveReductionsSequence.swift | 2 +- .../AsyncThrowingInclusiveReductionsSequence.swift | 2 +- Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift | 8 ++++---- .../CombineLatest/AsyncCombineLatest2Sequence.swift | 2 +- .../AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift | 2 +- .../Interspersed/AsyncInterspersedSequence.swift | 4 ++-- Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index 12611c14..0dab7cb0 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -58,7 +58,7 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { internal let transform: @Sendable (Base.Element, Base.Element) async -> Base.Element @inlinable - internal init( + init( _ iterator: Base.AsyncIterator, transform: @Sendable @escaping (Base.Element, Base.Element) async -> Base.Element ) { diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 36e88fb5..2a03304f 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -57,7 +57,7 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { internal let transform: @Sendable (Base.Element, Base.Element) async throws -> Base.Element @inlinable - internal init( + init( _ iterator: Base.AsyncIterator, transform: @Sendable @escaping (Base.Element, Base.Element) async throws -> Base.Element ) { diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index 8049a1a0..817615a4 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -70,7 +70,7 @@ public struct AsyncBufferSequencePolicy: Sendable { /// An `AsyncSequence` that buffers elements in regard to a policy. public struct AsyncBufferSequence: AsyncSequence { - enum StorageType { + enum StorageType { case transparent(Base.AsyncIterator) case bounded(storage: BoundedBufferStorage) case unbounded(storage: UnboundedBufferStorage) @@ -82,7 +82,7 @@ public struct AsyncBufferSequence: AsyncSequence let base: Base let policy: AsyncBufferSequencePolicy - public init( + init( base: Base, policy: AsyncBufferSequencePolicy ) { @@ -91,7 +91,7 @@ public struct AsyncBufferSequence: AsyncSequence } public func makeAsyncIterator() -> Iterator { - let storageType: StorageType + let storageType: StorageType switch self.policy.policy { case .bounded(...0), .bufferingNewest(...0), .bufferingOldest(...0): storageType = .transparent(self.base.makeAsyncIterator()) @@ -108,7 +108,7 @@ public struct AsyncBufferSequence: AsyncSequence } public struct Iterator: AsyncIteratorProtocol { - var storageType: StorageType + var storageType: StorageType public mutating func next() async rethrows -> Element? { switch self.storageType { diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index c07bd099..f8fd86cc 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -46,7 +46,7 @@ public struct AsyncCombineLatest2Sequence< let base1: Base1 let base2: Base2 - public init(_ base1: Base1, _ base2: Base2) { + init(_ base1: Base1, _ base2: Base2) { self.base1 = base1 self.base2 = base2 } diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index 5ae17f14..e2f8b7a9 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -41,7 +41,7 @@ public struct AsyncDebounceSequence: Sendable whe /// - interval: The interval to debounce. /// - tolerance: The tolerance of the clock. /// - clock: The clock. - public init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { + init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { self.base = base self.interval = interval self.tolerance = tolerance diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 7dd08c53..18398f9f 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -45,7 +45,7 @@ public struct AsyncInterspersedSequence { internal let separator: Base.Element @usableFromInline - internal init(_ base: Base, separator: Base.Element) { + init(_ base: Base, separator: Base.Element) { self.base = base self.separator = separator } @@ -73,7 +73,7 @@ extension AsyncInterspersedSequence: AsyncSequence { internal var state = State.start @usableFromInline - internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { + init(_ iterator: Base.AsyncIterator, separator: Base.Element) { self.iterator = iterator self.separator = separator } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 2723d112..c1a45ba3 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -40,7 +40,7 @@ public struct AsyncMerge2Sequence< /// - Parameters: /// - base1: The first upstream ``Swift/AsyncSequence``. /// - base2: The second upstream ``Swift/AsyncSequence``. - public init( + init( _ base1: Base1, _ base2: Base2 ) { diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index c2b54eb1..6f5abf13 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -49,7 +49,7 @@ public struct AsyncMerge3Sequence< /// - base1: The first upstream ``Swift/AsyncSequence``. /// - base2: The second upstream ``Swift/AsyncSequence``. /// - base3: The third upstream ``Swift/AsyncSequence``. - public init( + init( _ base1: Base1, _ base2: Base2, _ base3: Base3 From 936a68d01218f5ddeaacbef63fdb45af6c20c360 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 23 Jun 2023 14:47:25 +0100 Subject: [PATCH 09/32] [AsyncInterspersedSequence] Integrate review feedback (#267) * Integrate review feedback This integrates all of the feedback from the review thread. Here is a quick summary: - Change the trailing separator behaviour. We are no longer returning a separator before we are forwarding the error - Add a synchronous and asynchronous closure based `interspersed` method. - Support interspersing every n elements * Add AsyncThrowingInterspersedSequence * Update examples --- Evolution/0011-interspersed.md | 348 ++++++++++--- .../AsyncInterspersedSequence.swift | 484 +++++++++++++++--- .../Interspersed/TestInterspersed.swift | 226 +++++--- 3 files changed, 829 insertions(+), 229 deletions(-) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md index 79cc02f2..cfc99737 100644 --- a/Evolution/0011-interspersed.md +++ b/Evolution/0011-interspersed.md @@ -17,33 +17,134 @@ a separator element. ## Proposed solution We propose to add a new method on `AsyncSequence` that allows to intersperse -a separator between each emitted element. This proposed API looks like this +a separator between every n emitted element. This proposed API looks like this ```swift -extension AsyncSequence { - /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting - /// the given separator between each element. - /// - /// Any value of this asynchronous sequence's element type can be used as the separator. - /// - /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: - /// - /// ``` - /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") - /// for await element in interspersed { - /// print(element) - /// } - /// // Prints "A" "-" "B" "-" "C" - /// ``` - /// - /// - Parameter separator: The value to insert in between each of this async - /// sequence’s elements. - /// - Returns: The interspersed asynchronous sequence of elements. - @inlinable - public func interspersed(with separator: Element) -> AsyncInterspersedSequence { - AsyncInterspersedSequence(self, separator: separator) - } +public extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } } ``` @@ -53,83 +154,166 @@ The bulk of the implementation of the new `interspersed` method is inside the ne `AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand to the base iterator. -There is one special case that we have to call out. When the base async sequence throws -then `AsyncInterspersedSequence.Iterator.next()` will return the separator first and then rethrow the error. Below is the implementation of the `AsyncInterspersedSequence`. ```swift /// An asynchronous sequence that presents the elements of a base asynchronous sequence of /// elements with a separator between each of those elements. public struct AsyncInterspersedSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let separator: Base.Element - - @usableFromInline - internal init(_ base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } -} + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } -extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element + @usableFromInline + internal let base: Base - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline - internal enum State { - case start - case element(Result) - case separator - } + internal let separator: Separator @usableFromInline - internal var iterator: Base.AsyncIterator + internal let every: Int @usableFromInline - internal let separator: Base.Element + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } @usableFromInline - internal var state = State.start + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } @usableFromInline - internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) - public mutating func next() async rethrows -> Base.Element? { - // After the start, the state flips between element and separator. Before - // returning a separator, a check is made for the next element as a - // separator is only returned between two elements. The next element is - // stored to allow it to be returned in the next iteration. However, if - // the checking the next element throws, the separator is emitted before - // rethrowing that error. - switch state { - case .start: - state = .separator - return try await iterator.next() - case .separator: - do { - guard let next = try await iterator.next() else { return nil } - state = .element(.success(next)) - } catch { - state = .element(.failure(error)) - } - return separator - case .element(let result): - state = .separator - return try result._rethrowGet() - } + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async rethrows -> Base.Element? { + // After the start, the state flips between element and separator. Before + // returning a separator, a check is made for the next element as a + // separator is only returned between two elements. The next element is + // stored to allow it to be returned in the next iteration. However, if + // the checking the next element throws, the separator is emitted before + // rethrowing that error. + switch state { + case var .start(element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if every == 1 { + state = .separator + } else { + state = .element(1) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + state = .start(element) + switch separator { + case let .element(element): + return element + + case let .syncClosure(closure): + return closure() + + case let .asyncClosure(closure): + return await closure() + } + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case let .element(count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if every == newCount { + state = .separator + } else { + state = .element(newCount) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .finished: + return nil + } + } } - } - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.AsyncIterator { - AsyncIterator(base.makeAsyncIterator(), separator: separator) - } + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { + Iterator(base.makeAsyncIterator(), every: every, separator: separator) + } } ``` diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 18398f9f..9932e77e 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -10,107 +10,435 @@ //===----------------------------------------------------------------------===// extension AsyncSequence { - /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting - /// the given separator between each element. - /// - /// Any value of this asynchronous sequence's element type can be used as the separator. - /// - /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: - /// - /// ``` - /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") - /// for await element in interspersed { - /// print(element) - /// } - /// // Prints "A" "-" "B" "-" "C" - /// ``` - /// - /// - Parameter separator: The value to insert in between each of this async - /// sequence’s elements. - /// - Returns: The interspersed asynchronous sequence of elements. - @inlinable - public func interspersed(with separator: Element) -> AsyncInterspersedSequence { - AsyncInterspersedSequence(self, separator: separator) - } + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } } /// An asynchronous sequence that presents the elements of a base asynchronous sequence of /// elements with a separator between each of those elements. public struct AsyncInterspersedSequence { - @usableFromInline - internal let base: Base + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } + + @usableFromInline + internal let base: Base - @usableFromInline - internal let separator: Base.Element + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int - @usableFromInline - init(_ base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } + @usableFromInline + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } } extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async rethrows -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .element(let element): + return element - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct Iterator: AsyncIteratorProtocol { + case .syncClosure(let closure): + return closure() + + case .asyncClosure(let closure): + return await closure() + } + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncThrowingInterspersedSequence { @usableFromInline - internal enum State { - case start - case element(Result) - case separator + internal enum Separator { + case syncClosure(@Sendable () throws -> Element) + case asyncClosure(@Sendable () async throws -> Element) } @usableFromInline - internal var iterator: Base.AsyncIterator + internal let base: Base + + @usableFromInline + internal let separator: Separator @usableFromInline - internal let separator: Base.Element + internal let every: Int @usableFromInline - internal var state = State.start + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } @usableFromInline - init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator - } - - public mutating func next() async rethrows -> Base.Element? { - // After the start, the state flips between element and separator. Before - // returning a separator, a check is made for the next element as a - // separator is only returned between two elements. The next element is - // stored to allow it to be returned in the next iteration. However, if - // the checking the next element throws, the separator is emitted before - // rethrowing that error. - switch state { - case .start: - state = .separator - return try await iterator.next() - case .separator: - do { - guard let next = try await iterator.next() else { return nil } - state = .element(.success(next)) - } catch { - state = .element(.failure(error)) - } - return separator - case .element(let result): - state = .separator - return try result._rethrowGet() - } - } - } - - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) - } + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } } -extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncThrowingInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async throws -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .syncClosure(let closure): + return try closure() + + case .asyncClosure(let closure): + return try await closure() + } + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} + +extension AsyncThrowingInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncThrowingInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) extension AsyncInterspersedSequence.Iterator: Sendable {} +@available(*, unavailable) +extension AsyncThrowingInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 2f351ab0..6e09e84d 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -9,90 +9,178 @@ // //===----------------------------------------------------------------------===// -import XCTest import AsyncAlgorithms +import XCTest final class TestInterspersed: XCTestCase { - func test_interspersed() async { - let source = [1, 2, 3, 4, 5] - let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) + func test_interspersed() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_empty() async { - let source = [Int]() - let expected = [Int]() - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) + + func test_interspersed_every() async { + let source = [1, 2, 3, 4, 5, 6, 7, 8] + let expected = [1, 2, 3, 0, 4, 5, 6, 0, 7, 8] + let sequence = source.async.interspersed(every: 3, with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_with_throwing_upstream() async { - let source = [1, 2, 3, -1, 4, 5] - let expected = [1, 0, 2, 0, 3, 0] - var actual = [Int]() - let sequence = source.async.map { - try throwOn(-1, $0) - }.interspersed(with: 0) - - var iterator = sequence.makeAsyncIterator() - do { - while let item = try await iterator.next() { - actual.append(item) - } - XCTFail() - } catch { - XCTAssertEqual(Failure(), error as? Failure) + + func test_interspersed_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: { 0 }) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = try! await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = source.async.interspersed(with: "sep") - let lockStepChannel = AsyncChannel() - - await withTaskGroup(of: Void.self) { group in - group.addTask { + + func test_interspersed_async_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed { + try! await Task.sleep(nanoseconds: 1000) + return 0 + } + var actual = [Int]() var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } - // Information the parent task that we are consuming - await lockStepChannel.send(()) + func test_interspersed_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed(with: { throw Failure() }) - while let _ = await iterator.next() { } + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_async_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed { + try await Task.sleep(nanoseconds: 1000) + throw Failure() + } + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_empty() async { + let source = [Int]() + let expected = [Int]() + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } let pastEnd = await iterator.next() XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_with_throwing_upstream() async { + let source = [1, 2, 3, -1, 4, 5] + let expected = [1, 0, 2, 0, 3] + var actual = [Int]() + let sequence = source.async.map { + try throwOn(-1, $0) + }.interspersed(with: 0) + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_cancellation() async { + let source = Indefinite(value: "test") + let sequence = source.async.interspersed(with: "sep") + let lockStepChannel = AsyncChannel() + + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() + + // Information the parent task that we are consuming + await lockStepChannel.send(()) + + while let _ = await iterator.next() {} + + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - // Information the parent task that we finished consuming - await lockStepChannel.send(()) - } + // Information the parent task that we finished consuming + await lockStepChannel.send(()) + } - // Waiting until the child task started consuming - _ = await lockStepChannel.first { _ in true } + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } - // Now we cancel the child - group.cancelAll() + // Now we cancel the child + group.cancelAll() - // Waiting until the child task finished consuming - _ = await lockStepChannel.first { _ in true } + // Waiting until the child task finished consuming + _ = await lockStepChannel.first { _ in true } + } } - } } From 156bb4bf495789bf1f13b78cfa741d193a2f4700 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 23 Jun 2023 12:22:23 -0700 Subject: [PATCH 10/32] Reduce the overall warnings from tests (#274) * Migrate from semaphore style testing waits to async test fulfillment * Reduce warnings for missing hashable and comparable conformances per availability * Reduce warnings for executor (still one known warning) --- Sources/AsyncSequenceValidation/Clock.swift | 4 ++++ Sources/AsyncSequenceValidation/Test.swift | 6 ++++++ .../TestAdjacentPairs.swift | 5 +++-- Tests/AsyncAlgorithmsTests/TestBuffer.swift | 8 ++++---- .../TestBufferedByteIterator.swift | 4 ++-- Tests/AsyncAlgorithmsTests/TestChain.swift | 8 ++++---- Tests/AsyncAlgorithmsTests/TestChannel.swift | 2 +- .../TestCombineLatest.swift | 18 ++++++++--------- .../AsyncAlgorithmsTests/TestCompacted.swift | 4 ++-- Tests/AsyncAlgorithmsTests/TestJoin.swift | 8 ++++---- Tests/AsyncAlgorithmsTests/TestLazy.swift | 4 ++-- .../TestManualClock.swift | 6 +++--- Tests/AsyncAlgorithmsTests/TestMerge.swift | 8 ++++---- .../AsyncAlgorithmsTests/TestReductions.swift | 4 ++-- .../TestRemoveDuplicates.swift | 4 ++-- .../TestThrowingChannel.swift | 2 +- .../AsyncAlgorithmsTests/TestValidator.swift | 20 +++++++++---------- Tests/AsyncAlgorithmsTests/TestZip.swift | 8 ++++---- 18 files changed, 67 insertions(+), 56 deletions(-) diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index 6f33d15a..e76f5aa9 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -132,3 +132,7 @@ extension AsyncSequenceValidationDiagram.Clock: TestClock { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram.Clock: Clock { } + +// placeholders to avoid warnings +extension AsyncSequenceValidationDiagram.Clock.Instant: Hashable { } +extension AsyncSequenceValidationDiagram.Clock.Instant: Comparable { } diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index 35177cf2..0275cc11 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -68,6 +68,12 @@ extension AsyncSequenceValidationDiagram { struct Context { final class ClockExecutor: SerialExecutor { + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + func enqueue(_ job: __owned ExecutorJob) { + job.runSynchronously(on: asUnownedSerialExecutor()) + } + + @available(*, deprecated) // known deprecation warning func enqueue(_ job: UnownedJob) { job._runSynchronously(on: asUnownedSerialExecutor()) } diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 843624f6..4fea0ef5 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -88,10 +88,11 @@ final class TestAdjacentPairs: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index f8ef8a3c..7bb45f89 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -190,13 +190,13 @@ final class TestBuffer: XCTestCase { finished.fulfill() } // ensure the task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // When task.cancel() // Then - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_given_a_base_sequence_when_buffering_with_bounded_then_the_buffer_is_filled_in_and_suspends() async { @@ -310,13 +310,13 @@ final class TestBuffer: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // When task.cancel() // Then - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_given_a_base_sequence_when_bounded_with_limit_0_then_the_policy_is_transparent() async { diff --git a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift index ee6c5d12..2c8841d8 100644 --- a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift +++ b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift @@ -199,10 +199,10 @@ final class TestBufferedByteIterator: XCTestCase { } } } - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChain.swift b/Tests/AsyncAlgorithmsTests/TestChain.swift index b3be90a3..6bb2cb55 100644 --- a/Tests/AsyncAlgorithmsTests/TestChain.swift +++ b/Tests/AsyncAlgorithmsTests/TestChain.swift @@ -87,13 +87,13 @@ final class TestChain2: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } @@ -192,12 +192,12 @@ final class TestChain3: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index b181bddd..b5d28cd9 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -133,7 +133,7 @@ final class TestChannel: XCTestCase { task1.cancel() // Then: the first sending operation is resumed - wait(for: [send1IsResumed], timeout: 1.0) + await fulfillment(of: [send1IsResumed], timeout: 1.0) // When: collecting elements var iterator = sut.makeAsyncIterator() diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index f33258c9..cb0f7324 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -84,7 +84,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) } @@ -126,7 +126,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) } @@ -168,7 +168,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) } @@ -210,7 +210,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) } @@ -250,7 +250,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(validator.failure as? Failure, Failure()) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b")]) } @@ -290,7 +290,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(validator.failure as? Failure, Failure()) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a")]) } @@ -312,11 +312,11 @@ final class TestCombineLatest2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_combineLatest_when_cancelled() async { @@ -389,7 +389,7 @@ final class TestCombineLatest3: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)]) } diff --git a/Tests/AsyncAlgorithmsTests/TestCompacted.swift b/Tests/AsyncAlgorithmsTests/TestCompacted.swift index b82fe31e..3e2e5198 100644 --- a/Tests/AsyncAlgorithmsTests/TestCompacted.swift +++ b/Tests/AsyncAlgorithmsTests/TestCompacted.swift @@ -83,10 +83,10 @@ final class TestCompacted: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestJoin.swift b/Tests/AsyncAlgorithmsTests/TestJoin.swift index 91de1ef9..c60b71da 100644 --- a/Tests/AsyncAlgorithmsTests/TestJoin.swift +++ b/Tests/AsyncAlgorithmsTests/TestJoin.swift @@ -122,11 +122,11 @@ final class TestJoinedBySeparator: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } @@ -206,10 +206,10 @@ final class TestJoined: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestLazy.swift b/Tests/AsyncAlgorithmsTests/TestLazy.swift index 1ef69d29..3ff8c5c1 100644 --- a/Tests/AsyncAlgorithmsTests/TestLazy.swift +++ b/Tests/AsyncAlgorithmsTests/TestLazy.swift @@ -121,12 +121,12 @@ final class TestLazy: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestManualClock.swift b/Tests/AsyncAlgorithmsTests/TestManualClock.swift index 045ba2de..15cc97bb 100644 --- a/Tests/AsyncAlgorithmsTests/TestManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/TestManualClock.swift @@ -29,7 +29,7 @@ final class TestManualClock: XCTestCase { clock.advance() XCTAssertFalse(state.withCriticalRegion { $0 }) clock.advance() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } @@ -51,7 +51,7 @@ final class TestManualClock: XCTestCase { XCTAssertFalse(state.withCriticalRegion { $0 }) clock.advance() task.cancel() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } @@ -73,7 +73,7 @@ final class TestManualClock: XCTestCase { } XCTAssertFalse(state.withCriticalRegion { $0 }) task.cancel() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index 0bcb3479..c8d5e1ce 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -185,11 +185,11 @@ final class TestMerge2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_merge_when_cancelled() async { @@ -509,11 +509,11 @@ final class TestMerge3: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } // MARK: - IteratorInitialized diff --git a/Tests/AsyncAlgorithmsTests/TestReductions.swift b/Tests/AsyncAlgorithmsTests/TestReductions.swift index ad9cf5ac..24e7e4e4 100644 --- a/Tests/AsyncAlgorithmsTests/TestReductions.swift +++ b/Tests/AsyncAlgorithmsTests/TestReductions.swift @@ -217,10 +217,10 @@ final class TestReductions: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift index 01ddf3ff..932585b0 100644 --- a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift +++ b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift @@ -95,10 +95,10 @@ final class TestRemoveDuplicates: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift index 7dd60f18..6110c884 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift @@ -270,7 +270,7 @@ final class TestThrowingChannel: XCTestCase { task1.cancel() // Then: the first sending operation is resumed - wait(for: [send1IsResumed], timeout: 1.0) + await fulfillment(of: [send1IsResumed], timeout: 1.0) // When: collecting elements var iterator = sut.makeAsyncIterator() diff --git a/Tests/AsyncAlgorithmsTests/TestValidator.swift b/Tests/AsyncAlgorithmsTests/TestValidator.swift index 9c5ef9c2..67d2f37e 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidator.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidator.swift @@ -24,7 +24,7 @@ final class TestValidator: XCTestCase { } XCTAssertFalse(state.withCriticalRegion { $0 }) gate.open() - wait(for: [entered], timeout: 1.0) + await fulfillment(of: [entered], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } @@ -52,18 +52,18 @@ final class TestValidator: XCTestCase { } finished.fulfill() } - wait(for: [started], timeout: 1.0) + await fulfillment(of: [started], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, []) gated.advance() - wait(for: [expectations[0]], timeout: 1.0) + await fulfillment(of: [expectations[0]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) gated.advance() - wait(for: [expectations[1]], timeout: 1.0) + await fulfillment(of: [expectations[1]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2]) gated.advance() - wait(for: [expectations[2]], timeout: 1.0) + await fulfillment(of: [expectations[2]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2, 3]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_gatedSequence_throwing() async { @@ -93,14 +93,14 @@ final class TestValidator: XCTestCase { } finished.fulfill() } - wait(for: [started], timeout: 1.0) + await fulfillment(of: [started], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, []) gated.advance() - wait(for: [expectations[0]], timeout: 1.0) + await fulfillment(of: [expectations[0]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) gated.advance() XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) XCTAssertEqual(failure.withCriticalRegion { $0 as? Failure }, Failure()) } @@ -131,7 +131,7 @@ final class TestValidator: XCTestCase { XCTAssertEqual(value, [2, 3, 4]) a.advance() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [2, 3, 4]) } diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index 25bc9ec7..4c2fb229 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -152,11 +152,11 @@ final class TestZip2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_zip_when_cancelled() async { @@ -370,10 +370,10 @@ final class TestZip3: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } From a42b72ad84a963a15b88c69becfb569bf35f3d2d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 23 Jun 2023 14:21:34 -0700 Subject: [PATCH 11/32] Audit pass on Sendable conformances and requirements (#272) * Audit pass on Sendable conformances and requirements * A slightly cleaner mark for Sendability of OrderedSet * Remove flags for strict mode --- Package.swift | 2 +- .../Buffer/BoundedBufferStateMachine.swift | 5 ++++- .../Buffer/UnboundedBufferStateMachine.swift | 5 ++++- Sources/AsyncAlgorithms/Channels/AsyncChannel.swift | 2 +- .../Channels/AsyncThrowingChannel.swift | 2 +- .../Channels/ChannelStateMachine.swift | 11 +++++++---- Sources/AsyncAlgorithms/Channels/ChannelStorage.swift | 2 +- 7 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index a1562d76..56417e84 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3"))], + dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], targets: [ .target( name: "AsyncAlgorithms", diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index a5c50a61..b863bc50 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -16,7 +16,7 @@ struct BoundedBufferStateMachine { typealias SuspendedProducer = UnsafeContinuation typealias SuspendedConsumer = UnsafeContinuation?, Never> - private enum State { + fileprivate enum State { case initial(base: Base) case buffering( task: Task, @@ -308,3 +308,6 @@ struct BoundedBufferStateMachine { } } } + +extension BoundedBufferStateMachine: Sendable where Base: Sendable { } +extension BoundedBufferStateMachine.State: Sendable where Base: Sendable { } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index b163619a..a43c2023 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -21,7 +21,7 @@ struct UnboundedBufferStateMachine { case bufferingOldest(Int) } - private enum State { + fileprivate enum State { case initial(base: Base) case buffering( task: Task, @@ -248,3 +248,6 @@ struct UnboundedBufferStateMachine { } } } + +extension UnboundedBufferStateMachine: Sendable where Base: Sendable { } +extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index 75becf2d..8035d06a 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -19,7 +19,7 @@ /// on the `Iterator` is made, or when `finish()` is called from another Task. /// As `finish()` induces a terminal state, there is no more need for a back pressure management. /// This function does not suspend and will finish all the pending iterations. -public final class AsyncChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncChannel: AsyncSequence, @unchecked Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index 28de36ae..2fc48dfe 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -18,7 +18,7 @@ /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. /// Those functions do not suspend and will finish all the pending iterations. -public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index 2972c754..e823e5f7 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -10,8 +10,11 @@ //===----------------------------------------------------------------------===// @_implementationOnly import OrderedCollections -struct ChannelStateMachine: Sendable { - private struct SuspendedProducer: Hashable { +// NOTE: this is only marked as unchecked since the swift-collections tag is before auditing for Sendable +extension OrderedSet: @unchecked Sendable where Element: Sendable { } + +struct ChannelStateMachine: Sendable { + private struct SuspendedProducer: Hashable, Sendable { let id: UInt64 let continuation: UnsafeContinuation? let element: Element? @@ -29,7 +32,7 @@ struct ChannelStateMachine: Sendable { } } - private struct SuspendedConsumer: Hashable { + private struct SuspendedConsumer: Hashable, Sendable { let id: UInt64 let continuation: UnsafeContinuation? @@ -51,7 +54,7 @@ struct ChannelStateMachine: Sendable { case failed(Error) } - private enum State { + private enum State: Sendable { case channeling( suspendedProducers: OrderedSet, cancelledProducers: Set, diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index da398dbc..12b5ba72 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -8,7 +8,7 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -struct ChannelStorage: Sendable { +struct ChannelStorage: Sendable { private let stateMachine: ManagedCriticalState> private let ids = ManagedCriticalState(0) From 2ace7010e9c1a75f036a266131e3642192a62c35 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 23 Jun 2023 22:21:48 +0100 Subject: [PATCH 12/32] Add docker files for CI (#270) --- .swiftformat | 25 ++++++++++++++++++ docker/Dockerfile | 24 +++++++++++++++++ docker/docker-compose.2004.57.yaml | 22 ++++++++++++++++ docker/docker-compose.2004.58.yaml | 21 +++++++++++++++ docker/docker-compose.2204.main.yaml | 21 +++++++++++++++ docker/docker-compose.yaml | 39 ++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 .swiftformat create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.2004.57.yaml create mode 100644 docker/docker-compose.2004.58.yaml create mode 100644 docker/docker-compose.2204.main.yaml create mode 100644 docker/docker-compose.yaml diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..3eb557ea --- /dev/null +++ b/.swiftformat @@ -0,0 +1,25 @@ +# file options + +--swiftversion 5.7 +--exclude .build + +# format options + +--self insert +--patternlet inline +--ranges nospace +--stripunusedargs unnamed-only +--ifdef no-indent +--extensionacl on-declarations +--disable typeSugar # https://github.com/nicklockwood/SwiftFormat/issues/636 +--disable andOperator +--disable wrapMultilineStatementBraces +--disable enumNamespaces +--disable redundantExtensionACL +--disable redundantReturn +--disable preferKeyPath +--disable sortedSwitchCases +--disable hoistAwait +--disable hoistTry + +# rules diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..e592f92f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,24 @@ +ARG swift_version=5.7 +ARG ubuntu_version=focal +ARG base_image=swift:$swift_version-$ubuntu_version +FROM $base_image +# needed to do again after FROM due to docker limitation +ARG swift_version +ARG ubuntu_version + +# set as UTF-8 +RUN apt-get update && apt-get install -y locales locales-all +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +# tools +RUN mkdir -p $HOME/.tools +RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile + +# swiftformat (until part of the toolchain) + +ARG swiftformat_version=0.51.12 +RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format +RUN cd $HOME/.tools/swift-format && swift build -c release +RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/docker-compose.2004.57.yaml b/docker/docker-compose.2004.57.yaml new file mode 100644 index 00000000..19c52d83 --- /dev/null +++ b/docker/docker-compose.2004.57.yaml @@ -0,0 +1,22 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:20.04-5.7 + build: + args: + ubuntu_version: "focal" + swift_version: "5.7" + + build: + image: swift-async-algorithms:20.04-5.7 + + test: + image: swift-async-algorithms:20.04-5.7 + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:20.04-5.7 diff --git a/docker/docker-compose.2004.58.yaml b/docker/docker-compose.2004.58.yaml new file mode 100644 index 00000000..56d83dfc --- /dev/null +++ b/docker/docker-compose.2004.58.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:20.04-5.8 + build: + args: + base_image: "swiftlang/swift:nightly-5.8-focal" + + build: + image: swift-async-algorithms:20.04-5.8 + + test: + image: swift-async-algorithms:20.04-5.8 + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:20.04-5.8 diff --git a/docker/docker-compose.2204.main.yaml b/docker/docker-compose.2204.main.yaml new file mode 100644 index 00000000..f28e21d2 --- /dev/null +++ b/docker/docker-compose.2204.main.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:22.04-main + build: + args: + base_image: "swiftlang/swift:nightly-main-jammy" + + build: + image: swift-async-algorithms:22.04-main + + test: + image: swift-async-algorithms:22.04-main + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:22.04-main diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..8d1d9a33 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,39 @@ +# this file is not designed to be run directly +# instead, use the docker-compose.. files +# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.2004.56.yaml run test +version: "3" + +services: + runtime-setup: + image: swift-async-algorithms:default + build: + context: . + dockerfile: Dockerfile + + common: &common + image: swift-async-algorithms:default + depends_on: [runtime-setup] + volumes: + - ~/.ssh:/root/.ssh + - ..:/code:z + working_dir: /code + + soundness: + <<: *common + command: /bin/bash -xcl "swift -version && uname -a && ./scripts/soundness.sh" + + build: + <<: *common + environment: [] + command: /bin/bash -cl "swift build" + + test: + <<: *common + depends_on: [runtime-setup] + command: /bin/bash -xcl "swift $${SWIFT_TEST_VERB-test} $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}" + + # util + + shell: + <<: *common + entrypoint: /bin/bash From adb12bfcccaa040778c905c5a50da9d9367fd0db Mon Sep 17 00:00:00 2001 From: 0xpablo Date: Sat, 24 Jun 2023 00:54:35 +0200 Subject: [PATCH 13/32] Add WebAssembly support (#273) --- Sources/AsyncAlgorithms/Locking.swift | 2 ++ Sources/AsyncSequenceValidation/TaskDriver.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 4e8e246c..e1ed2626 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -24,6 +24,8 @@ internal struct Lock { typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK +#else + typealias Primitive = Int #endif typealias PlatformLock = UnsafeMutablePointer diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 9f45c1f6..69c8fe5c 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -50,8 +50,12 @@ final class TaskDriver { } func start() { +#if canImport(Darwin) || canImport(Glibc) pthread_create(&thread, nil, start_thread, Unmanaged.passRetained(self).toOpaque()) +#elseif canImport(WinSDK) +#error("TODO: Port TaskDriver threading to windows") +#endif } func run() { From 07a0c1ee08e90dd15b05d45a3ead10929c0b7ec5 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 11 Jul 2023 15:18:10 +0200 Subject: [PATCH 14/32] Remove copy and paste within MergeStorage (#275) --- .../AsyncAlgorithms/Merge/MergeStorage.swift | 348 +++++------------- 1 file changed, 94 insertions(+), 254 deletions(-) diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index 7a83ad8b..42712cae 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -147,262 +147,12 @@ final class MergeStorage< // sequences. We must store it to cancel it at the right times. let task = Task { await withThrowingTaskGroup(of: Void.self) { group in - // For each upstream sequence we are adding a child task that - // is consuming the upstream sequence - group.addTask { - var iterator1 = base1.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element1 = try await iterator1.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element1) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } - } - - // Copy from the above just using the base2 sequence - group.addTask { - var iterator2 = base2.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element2 = try await iterator2.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element2) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } - } + self.iterateAsyncSequence(base1, in: &group) + self.iterateAsyncSequence(base2, in: &group) // Copy from the above just using the base3 sequence if let base3 = base3 { - group.addTask { - var iterator3 = base3.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element3 = try await iterator3.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element3) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } - } + self.iterateAsyncSequence(base3, in: &group) } while !group.isEmpty { @@ -444,5 +194,95 @@ final class MergeStorage< // We need to inform our state machine that we started the Task stateMachine.taskStarted(task) } -} + private func iterateAsyncSequence( + _ base: AsyncSequence, + in taskGroup: inout ThrowingTaskGroup + ) where AsyncSequence.Element == Base1.Element { + // For each upstream sequence we are adding a child task that + // is consuming the upstream sequence + taskGroup.addTask { + var iterator = base.makeAsyncIterator() + + // This is our upstream consumption loop + loop: while true { + // We are creating a continuation before requesting the next + // element from upstream. This continuation is only resumed + // if the downstream consumer called `next` to signal his demand. + try await withUnsafeThrowingContinuation { continuation in + let action = self.lock.withLock { + self.stateMachine.childTaskSuspended(continuation) + } + + switch action { + case let .resumeContinuation(continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) + + case let .resumeContinuationWithError(continuation, error): + // This happens if another upstream already failed or if + // the task got cancelled. + continuation.resume(throwing: error) + + case .none: + break + } + } + + // We got signalled from the downstream that we have demand so let's + // request a new element from the upstream + if let element1 = try await iterator.next() { + let action = self.lock.withLock { + self.stateMachine.elementProduced(element1) + } + + switch action { + case let .resumeContinuation(continuation, element): + // We had an outstanding demand and where the first + // upstream to produce an element so we can forward it to + // the downstream + continuation.resume(returning: element) + + case .none: + break + } + + } else { + // The upstream returned `nil` which indicates that it finished + let action = self.lock.withLock { + self.stateMachine.upstreamFinished() + } + + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. + switch action { + case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: nil) + + break loop + + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + case .none: + + break loop + } + } + } + } + } +} From df46b4c235819d4d0bd421ec28ab4b6cc2243ef8 Mon Sep 17 00:00:00 2001 From: Freya Alminde <72786+freysie@users.noreply.github.com> Date: Mon, 24 Jul 2023 17:42:27 +0200 Subject: [PATCH 15/32] Fix typo in Guides/Chunked.md (#277) --- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md index 2ddacc5f..94389220 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md @@ -213,7 +213,7 @@ If both count and signal are specified, the chunking asynchronous sequence emits Like the example above, this code emits up to 1024-byte `Data` instances, but a chunk will also be emitted every second. ```swift -let packets = bytes.chunks(ofCount: 1024 or: .repeating(every: .seconds(1)), into: Data.self) +let packets = bytes.chunks(ofCount: 1024, or: .repeating(every: .seconds(1)), into: Data.self) for try await packet in packets { write(packet) } From f5d5fb6483e7ea664bd402b9c379179934622f58 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 27 Jul 2023 22:16:37 +0100 Subject: [PATCH 16/32] Mark all iterators as non-`Sendable` (#280) --- .../AsyncAdjacentPairsSequence.swift | 46 +++++++++--------- .../AsyncAlgorithms.docc/Guides/Timer.md | 2 +- .../AsyncAlgorithms/AsyncChain3Sequence.swift | 3 ++ .../AsyncChunkedByGroupSequence.swift | 4 +- .../AsyncChunkedOnProjectionSequence.swift | 4 +- .../AsyncChunksOfCountOrSignalSequence.swift | 3 ++ .../AsyncChunksOfCountSequence.swift | 3 ++ .../AsyncCompactedSequence.swift | 37 ++++++++------- .../AsyncExclusiveReductionsSequence.swift | 3 ++ .../AsyncInclusiveReductionsSequence.swift | 3 ++ .../AsyncJoinedBySeparatorSequence.swift | 3 ++ .../AsyncAlgorithms/AsyncJoinedSequence.swift | 3 ++ .../AsyncRemoveDuplicatesSequence.swift | 6 +++ .../AsyncAlgorithms/AsyncSyncSequence.swift | 3 ++ .../AsyncThrottleSequence.swift | 3 ++ ...cThrowingExclusiveReductionsSequence.swift | 3 ++ ...cThrowingInclusiveReductionsSequence.swift | 14 ++++++ .../AsyncAlgorithms/AsyncTimerSequence.swift | 3 ++ .../Channels/AsyncChannel.swift | 3 ++ .../Channels/AsyncThrowingChannel.swift | 3 ++ .../AsyncCombineLatest2Sequence.swift | 3 ++ .../AsyncCombineLatest3Sequence.swift | 3 ++ .../Debounce/AsyncDebounceSequence.swift | 9 ++-- .../Merge/AsyncMerge2Sequence.swift | 9 ++-- .../Merge/AsyncMerge3Sequence.swift | 9 ++-- .../AsyncAlgorithms/PartialIteration.swift | 47 ------------------- .../Zip/AsyncZip2Sequence.swift | 3 ++ .../Zip/AsyncZip3Sequence.swift | 3 ++ 28 files changed, 138 insertions(+), 100 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/PartialIteration.swift diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 2a37f918..b1a0a156 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -9,6 +9,29 @@ // //===----------------------------------------------------------------------===// +extension AsyncSequence { + /// An `AsyncSequence` that iterates over the adjacent pairs of the original + /// original `AsyncSequence`. + /// + /// ``` + /// for await (first, second) in (1...5).async.adjacentPairs() { + /// print("First: \(first), Second: \(second)") + /// } + /// + /// // First: 1, Second: 2 + /// // First: 2, Second: 3 + /// // First: 3, Second: 4 + /// // First: 4, Second: 5 + /// ``` + /// + /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements + /// or the original `AsyncSequence`. + @inlinable + public func adjacentPairs() -> AsyncAdjacentPairsSequence { + AsyncAdjacentPairsSequence(self) + } +} + /// An `AsyncSequence` that iterates over the adjacent pairs of the original /// `AsyncSequence`. @frozen @@ -60,29 +83,6 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { } } -extension AsyncSequence { - /// An `AsyncSequence` that iterates over the adjacent pairs of the original - /// original `AsyncSequence`. - /// - /// ``` - /// for await (first, second) in (1...5).async.adjacentPairs() { - /// print("First: \(first), Second: \(second)") - /// } - /// - /// // First: 1, Second: 2 - /// // First: 2, Second: 3 - /// // First: 3, Second: 4 - /// // First: 4, Second: 5 - /// ``` - /// - /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements - /// or the original `AsyncSequence`. - @inlinable - public func adjacentPairs() -> AsyncAdjacentPairsSequence { - AsyncAdjacentPairsSequence(self) - } -} - extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable { } @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md index 8228a4e2..1419d68b 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md @@ -42,7 +42,7 @@ extension AsyncTimerSequence: Sendable { } extension AsyncTimerSequence.Iterator: Sendable { } ``` -Since all the types comprising `AsyncTimerSequence` and it's `Iterator` are `Sendable` these types are also `Sendable`. +Since all the types comprising `AsyncTimerSequence` are `Sendable` these types are also `Sendable`. ## Credits/Inspiration diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index 1c275a2c..e88e3584 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -95,3 +95,6 @@ extension AsyncChain3Sequence: AsyncSequence { } extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } + +@available(*, unavailable) +extension AsyncChain3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index 26faafe5..0ce5d199 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -117,4 +117,6 @@ public struct AsyncChunkedByGroupSequence() -> AsyncCompactedSequence + where Element == Unwrapped? { + AsyncCompactedSequence(self) + } +} + /// An `AsyncSequence` that iterates over every non-nil element from the original /// `AsyncSequence`. @frozen @@ -50,22 +66,7 @@ public struct AsyncCompactedSequence: AsyncSequenc } } -extension AsyncSequence { - /// Returns a new `AsyncSequence` that iterates over every non-nil element from the - /// original `AsyncSequence`. - /// - /// Produces the same result as `c.compactMap { $0 }`. - /// - /// - Returns: An `AsyncSequence` where the element is the unwrapped original - /// element and iterates over every non-nil element from the original - /// `AsyncSequence`. - /// - /// Complexity: O(1) - @inlinable - public func compacted() -> AsyncCompactedSequence - where Element == Unwrapped? { - AsyncCompactedSequence(self) - } -} - extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncCompactedSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index 0d56c593..cef05359 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -112,3 +112,6 @@ extension AsyncExclusiveReductionsSequence: AsyncSequence { } extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncExclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index 0dab7cb0..ca907b80 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -85,3 +85,6 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { } extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncInclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index e401eea2..515d8a8e 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -142,3 +142,6 @@ public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base extension AsyncJoinedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable { } + +@available(*, unavailable) +extension AsyncJoinedSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index fd1d2192..0f45e21d 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -143,3 +143,9 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncRemoveDuplicatesSequence.Iterator: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingRemoveDuplicatesSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift index 6710df7c..70a6637b 100644 --- a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift @@ -67,3 +67,6 @@ public struct AsyncSyncSequence: AsyncSequence { } extension AsyncSyncSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncSyncSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index 4832b9ac..ae2b1db4 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -102,3 +102,6 @@ extension AsyncThrottleSequence: AsyncSequence { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncThrottleSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index 4b12c2c3..1cb49d8b 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -119,3 +119,6 @@ extension AsyncThrowingExclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingExclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 2a03304f..7779a842 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -1,3 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 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 +// +//===----------------------------------------------------------------------===// + extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. @@ -89,3 +100,6 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index fe3b58f6..f3a06fc0 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -89,3 +89,6 @@ extension AsyncTimerSequence where C == SuspendingClock { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence: Sendable { } + +@available(*, unavailable) +extension AsyncTimerSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index 8035d06a..f59b6b5f 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -58,3 +58,6 @@ public final class AsyncChannel: AsyncSequence, @unchecked Se } } } + +@available(*, unavailable) +extension AsyncChannel.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index 2fc48dfe..eaa55fcc 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -61,3 +61,6 @@ public final class AsyncThrowingChannel: Asyn } } } + +@available(*, unavailable) +extension AsyncThrowingChannel.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index f8fd86cc..fa68acf7 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -87,3 +87,6 @@ public struct AsyncCombineLatest2Sequence< } } } + +@available(*, unavailable) +extension AsyncCombineLatest2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift index a1c7e51a..4353c0b0 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -97,3 +97,6 @@ public struct AsyncCombineLatest3Sequence< } } } + +@available(*, unavailable) +extension AsyncCombineLatest3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index e2f8b7a9..888d4f42 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -53,20 +53,20 @@ public struct AsyncDebounceSequence: Sendable whe extension AsyncDebounceSequence: AsyncSequence { public typealias Element = Base.Element - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = DebounceStorage( base: self.base, interval: self.interval, tolerance: self.tolerance, clock: self.clock ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncDebounceSequence { - public struct AsyncIterator: AsyncIteratorProtocol { + public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -97,3 +97,6 @@ extension AsyncDebounceSequence { } } } + +@available(*, unavailable) +extension AsyncDebounceSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index c1a45ba3..2de482c8 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -50,18 +50,18 @@ public struct AsyncMerge2Sequence< } extension AsyncMerge2Sequence: AsyncSequence { - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, base2: base2, base3: nil ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } extension AsyncMerge2Sequence { - public struct AsyncIterator: AsyncIteratorProtocol { + public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -92,3 +92,6 @@ extension AsyncMerge2Sequence { } } } + +@available(*, unavailable) +extension AsyncMerge2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index 6f5abf13..8cafcd95 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -61,18 +61,18 @@ public struct AsyncMerge3Sequence< } extension AsyncMerge3Sequence: AsyncSequence { - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, base2: base2, base3: base3 ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } public extension AsyncMerge3Sequence { - struct AsyncIterator: AsyncIteratorProtocol { + struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -103,3 +103,6 @@ public extension AsyncMerge3Sequence { } } } + +@available(*, unavailable) +extension AsyncMerge3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/PartialIteration.swift b/Sources/AsyncAlgorithms/PartialIteration.swift deleted file mode 100644 index 1404ddc2..00000000 --- a/Sources/AsyncAlgorithms/PartialIteration.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 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 -// -//===----------------------------------------------------------------------===// - -enum PartialIteration: CustomStringConvertible { - case idle(Iterator) - case pending(Task) - case terminal - - var description: String { - switch self { - case .idle: return "idle" - case .pending: return "pending" - case .terminal: return "terminal" - } - } - - mutating func resolve(_ result: Result, _ iterator: Iterator) rethrows -> Iterator.Element? { - do { - guard let value = try result._rethrowGet() else { - self = .terminal - return nil - } - self = .idle(iterator) - return value - } catch { - self = .terminal - throw error - } - } - - mutating func cancel() { - if case .pending(let task) = self { - task.cancel() - } - self = .terminal - } -} - -extension PartialIteration: Sendable where Iterator: Sendable, Iterator.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index 6fb341ac..34e42913 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -69,3 +69,6 @@ public struct AsyncZip2Sequence: Asy } } } + +@available(*, unavailable) +extension AsyncZip2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 87474bae..513dc27a 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -74,3 +74,6 @@ public struct AsyncZip3Sequence Date: Tue, 1 Aug 2023 16:09:34 +0100 Subject: [PATCH 17/32] Fix more `Sendable` warnings and flaky test (#281) # Motivation We still had some `Sendable` warnings left under strict Concurrency checking. # Modification This PR fixes a bunch of `Sendable` warnings but we still have some left in the validation tests. Additionally, I fixed a flaky test. --- Package.swift | 25 +++++++-- Package@swift-5.7.swift | 51 +++++++++++++++++++ .../AsyncAlgorithms/Merge/MergeStorage.swift | 2 +- .../Interspersed/TestInterspersed.swift | 7 +-- .../Performance/ThroughputMeasurement.swift | 4 +- Tests/AsyncAlgorithmsTests/TestChunk.swift | 2 + 6 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 Package@swift-5.7.swift diff --git a/Package.swift b/Package.swift index 56417e84..36c4e078 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 5.8 import PackageDescription @@ -20,18 +20,33 @@ let package = Package( targets: [ .target( name: "AsyncAlgorithms", - dependencies: [.product(name: "Collections", package: "swift-collections")] + dependencies: [.product(name: "Collections", package: "swift-collections")], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] ), .target( name: "AsyncSequenceValidation", - dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .systemLibrary(name: "_CAsyncSequenceValidationSupport"), .target( name: "AsyncAlgorithms_XCTest", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), ] ) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift new file mode 100644 index 00000000..56417e84 --- /dev/null +++ b/Package@swift-5.7.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 5.6 + +import PackageDescription + +let package = Package( + name: "swift-async-algorithms", + platforms: [ + .macOS("10.15"), + .iOS("13.0"), + .tvOS("13.0"), + .watchOS("6.0") + ], + products: [ + .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), + .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), + .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), + .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), + ], + dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], + targets: [ + .target( + name: "AsyncAlgorithms", + dependencies: [.product(name: "Collections", package: "swift-collections")] + ), + .target( + name: "AsyncSequenceValidation", + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + .systemLibrary(name: "_CAsyncSequenceValidationSupport"), + .target( + name: "AsyncAlgorithms_XCTest", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + .testTarget( + name: "AsyncAlgorithmsTests", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + ] +) + +#if canImport(Darwin) +import Darwin +let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil +#elseif canImport(Glibc) +import Glibc +let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil +#else +let buildingDocs = false +#endif + +// Only require the docc plugin when building documentation +package.dependencies += buildingDocs ? [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), +] : [] diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index 42712cae..9dedee76 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -198,7 +198,7 @@ final class MergeStorage< private func iterateAsyncSequence( _ base: AsyncSequence, in taskGroup: inout ThrowingTaskGroup - ) where AsyncSequence.Element == Base1.Element { + ) where AsyncSequence.Element == Base1.Element, AsyncSequence: Sendable { // For each upstream sequence we are adding a child task that // is consuming the upstream sequence taskGroup.addTask { diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 6e09e84d..e51a4817 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -166,10 +166,6 @@ final class TestInterspersed: XCTestCase { while let _ = await iterator.next() {} - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - - // Information the parent task that we finished consuming await lockStepChannel.send(()) } @@ -179,8 +175,7 @@ final class TestInterspersed: XCTestCase { // Now we cancel the child group.cancelAll() - // Waiting until the child task finished consuming - _ = await lockStepChannel.first { _ in true } + await group.waitForAll() } } } diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index c8991bdd..db223f3e 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -56,7 +56,7 @@ final class _ThroughputMetric: NSObject, XCTMetric, @unchecked Sendable { } extension XCTestCase { - public func measureChannelThroughput(output: @escaping @autoclosure () -> Output) async { + public func measureChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 @@ -85,7 +85,7 @@ extension XCTestCase { } } - public func measureThrowingChannelThroughput(output: @escaping @autoclosure () -> Output) async { + public func measureThrowingChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 4970cdc0..eee61247 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -13,10 +13,12 @@ import XCTest import AsyncSequenceValidation import AsyncAlgorithms +@Sendable func sumCharacters(_ array: [String]) -> String { return "\(array.reduce(into: 0) { $0 = $0 + Int($1)! })" } +@Sendable func concatCharacters(_ array: [String]) -> String { return array.joined() } From e639e5c8896de0c1ded10d70e2571f165596b556 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 15 Aug 2023 15:40:57 -0700 Subject: [PATCH 18/32] Ensure tests work in deployments that host as swift 5.7 (#285) --- .../Performance/TestThroughput.swift | 5 ++- .../Support/Asserts.swift | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index d8003ca8..4ea06e06 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -90,11 +90,12 @@ final class TestThroughput: XCTestCase { zip($0, $1, $2) } } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func test_debounce() async { + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { await measureSequenceThroughput(source: (1...).async) { - $0.debounce(for: .zero, clock: ContinuousClock()) + $0.debounce(for: .zero, clock: ContinuousClock()) } + } } } #endif diff --git a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift index c9cdb968..d891cf91 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift @@ -165,3 +165,47 @@ internal func XCTAssertThrowsError( verify(error) } } + +class WaiterDelegate: NSObject, XCTWaiterDelegate { + let state: ManagedCriticalState?> = ManagedCriticalState(nil) + + init(_ continuation: UnsafeContinuation) { + state.withCriticalRegion { $0 = continuation } + } + + func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { + resume() + } + + func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { + resume() + } + + func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { + resume() + } + + func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { + + } + + func resume() { + let continuation = state.withCriticalRegion { continuation in + defer { continuation = nil } + return continuation + } + continuation?.resume() + } +} + +extension XCTestCase { + @_disfavoredOverload + func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async { + return await withUnsafeContinuation { continuation in + let delegate = WaiterDelegate(continuation) + let waiter = XCTWaiter(delegate: delegate) + waiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder) + delegate.resume() + } + } +} From b0ec4694d2046165d0365a64cee682bf4ca2d9ae Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 18 Aug 2023 19:19:58 +0100 Subject: [PATCH 19/32] Remove the validation packages from public products (#287) # Motivation The current validation testing library has a bunch of warnings and we haven't put it through a proper API review yet. Since we are preparing for a 1.0.0, I think it is best if we remove the products for the validation targets for now and take some time to make sure the interfaces are taken through an API review. # Modification Remove products for the validation targets. --- Package.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Package.swift b/Package.swift index 36c4e078..afc3bcaa 100644 --- a/Package.swift +++ b/Package.swift @@ -12,9 +12,6 @@ let package = Package( ], products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), - .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), - .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), - .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], targets: [ From 4a1fb99f0089a9d9db07859bcad55b4a77e3c3dd Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 22 Aug 2023 16:09:28 -0700 Subject: [PATCH 20/32] Rework availability for executor to avoid warnings (#288) --- Sources/AsyncSequenceValidation/Job.swift | 2 +- Sources/AsyncSequenceValidation/Test.swift | 47 +++++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncSequenceValidation/Job.swift b/Sources/AsyncSequenceValidation/Job.swift index 44dedbc8..461af50d 100644 --- a/Sources/AsyncSequenceValidation/Job.swift +++ b/Sources/AsyncSequenceValidation/Job.swift @@ -19,6 +19,6 @@ struct Job: Hashable, @unchecked Sendable { } func execute() { - _swiftJobRun(unsafeBitCast(job, to: UnownedJob.self), AsyncSequenceValidationDiagram.Context.executor.asUnownedSerialExecutor()) + _swiftJobRun(unsafeBitCast(job, to: UnownedJob.self), AsyncSequenceValidationDiagram.Context.unownedExecutor) } } diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index 0275cc11..8dc86832 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -67,25 +67,62 @@ extension AsyncSequenceValidationDiagram { } struct Context { +#if swift(<5.9) final class ClockExecutor: SerialExecutor { - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + func enqueue(_ job: UnownedJob) { + job._runSynchronously(on: self.asUnownedSerialExecutor()) + } + + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + private static let _executor = ClockExecutor() + + static var unownedExecutor: UnownedSerialExecutor { + _executor.asUnownedSerialExecutor() + } +#else + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + final class ClockExecutor_5_9: SerialExecutor { func enqueue(_ job: __owned ExecutorJob) { job.runSynchronously(on: asUnownedSerialExecutor()) } - @available(*, deprecated) // known deprecation warning + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + final class ClockExecutor_Pre5_9: SerialExecutor { + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(*, deprecated, message: "Implement 'enqueue(_: __owned ExecutorJob)' instead") func enqueue(_ job: UnownedJob) { - job._runSynchronously(on: asUnownedSerialExecutor()) + job._runSynchronously(on: self.asUnownedSerialExecutor()) } - + func asUnownedSerialExecutor() -> UnownedSerialExecutor { UnownedSerialExecutor(ordinary: self) } } + private static let _executor: AnyObject = { + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + return ClockExecutor_5_9() + } else { + return ClockExecutor_Pre5_9() + } + }() + + static var unownedExecutor: UnownedSerialExecutor { + (_executor as! any SerialExecutor).asUnownedSerialExecutor() + } +#endif + static var clock: Clock? - static let executor = ClockExecutor() + static var driver: TaskDriver? From 281e27c5d0c19bf31feb95aa31cc2ae146684f75 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 19 Sep 2023 17:23:38 +0200 Subject: [PATCH 21/32] Fix `chunks(countOf: 1)` (#293) --- Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift | 4 ++++ Tests/AsyncAlgorithmsTests/TestChunk.swift | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift index 9e1c4959..0ebafb4b 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift @@ -49,6 +49,10 @@ public struct AsyncChunksOfCountSequence String { } final class TestChunk: XCTestCase { + func test_count_one() { + validate { + "ABCDE|" + $0.inputs[0].chunks(ofCount: 1).map(concatCharacters) + "ABCDE|" + } + } + func test_signal_equalChunks() { validate { "ABC- DEF- GHI- |" From 6dfbfd5e49a33c024ea7963dd79c634720342518 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 21 Sep 2023 11:07:08 +0200 Subject: [PATCH 22/32] Remove background correction in `AsyncTimerSequence` (#289) # Motivation Currently, the `AsyncTimerSequence` is trying to correct for when an application becomes suspended and the timer might fire multiple times once the application gets foregrounded again. However, this is already handled by the `Clock` types themselves. The `SuspendingClock` is correcting for suspension of the app whereas the `ContinuousClock` is not. Additionally, this was not only hit by background an application but by just calling `Task.sleep` in the for-await loop that is consuming the sequence. # Modification This removes the part of the code in `AsyncTimerSequence` which corrected for suspension of the application. --- .../AsyncAlgorithms/AsyncTimerSequence.swift | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index f3a06fc0..dcfc878b 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -13,59 +13,49 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct AsyncTimerSequence: AsyncSequence { public typealias Element = C.Instant - + /// The iterator for an `AsyncTimerSequence` instance. public struct Iterator: AsyncIteratorProtocol { var clock: C? let interval: C.Instant.Duration let tolerance: C.Instant.Duration? var last: C.Instant? - + init(interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { self.clock = clock self.interval = interval self.tolerance = tolerance } - - func nextDeadline(_ clock: C) -> C.Instant { - let now = clock.now - let last = self.last ?? now - let next = last.advanced(by: interval) - if next < now { - return last.advanced(by: interval * Int(((next.duration(to: now)) / interval).rounded(.up))) - } else { - return next - } - } - + public mutating func next() async -> C.Instant? { - guard let clock = clock else { + guard let clock = self.clock else { return nil } - let next = nextDeadline(clock) + + let next = (self.last ?? clock.now).advanced(by: self.interval) do { - try await clock.sleep(until: next, tolerance: tolerance) + try await clock.sleep(until: next, tolerance: self.tolerance) } catch { self.clock = nil return nil } let now = clock.now - last = next + self.last = next return now } } - + let clock: C let interval: C.Instant.Duration let tolerance: C.Instant.Duration? - + /// Create an `AsyncTimerSequence` with a given repeating interval. public init(interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) { self.clock = clock self.interval = interval self.tolerance = tolerance } - + public func makeAsyncIterator() -> Iterator { Iterator(interval: interval, tolerance: tolerance, clock: clock) } From c889832c6499eeba5b31d142861f6478bec55308 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 21 Sep 2023 02:09:43 -0700 Subject: [PATCH 23/32] Add a proposal for AsyncChannel (#216) --- Evolution/NNNN-channel.md | 86 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 Evolution/NNNN-channel.md diff --git a/Evolution/NNNN-channel.md b/Evolution/NNNN-channel.md new file mode 100644 index 00000000..09256190 --- /dev/null +++ b/Evolution/NNNN-channel.md @@ -0,0 +1,86 @@ +# Channel + +* Author(s): [Philippe Hausler](https://github.com/phausler) + +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChannel.swift), +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChannel.swift) +] + +## Introduction + +`AsyncStream` introduced a mechanism to send buffered elements from a context that doesn't use Swift concurrency into one that does. That design only addressed a portion of the potential use cases; the missing portion was the back pressure excerpted across two concurrency domains. + +## Proposed Solution + +To achieve a system that supports back pressure and allows for the communication of more than one value from one task to another we are introducing a new type, the _channel_. The channel will be a reference-type asynchronous sequence with an asynchronous sending capability that awaits the consumption of iteration. Each value sent by the channel will await the consumption of that value by iteration. That awaiting behavior will allow for the affordance of back pressure applied from the consumption site to be transmitted to the production site. This means that the rate of production cannot exceed the rate of consumption, and that the rate of consumption cannot exceed the rate of production. Sending a terminal event to the channel will instantly resume all pending operations for every producers and consumers. + +## Detailed Design + +Similar to the `AsyncStream` and `AsyncThrowingStream` types, the type for sending elements via back pressure will come in two versions. These two versions will account for the throwing nature or non-throwing nature of the elements being produced. + +Each type will have functions to send elements and to send terminal events. + +```swift +public final class AsyncChannel: AsyncSequence, Sendable { + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async -> Element? + } + + public init(element elementType: Element.Type = Element.self) + + public func send(_ element: Element) async + public func finish() + + public func makeAsyncIterator() -> Iterator +} + +public final class AsyncThrowingChannel: AsyncSequence, Sendable { + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async throws -> Element? + } + + public init(element elementType: Element.Type = Element.self, failure failureType: Failure.Type = Failure.self) + + public func send(_ element: Element) async + public func fail(_ error: Error) where Failure == Error + public func finish() + + public func makeAsyncIterator() -> Iterator +} +``` + +Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` via the suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. This method suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` or `fail(_:)` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, or by throwing an error, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. The calls to `send(:)` and `next()` will immediately resume when their supporting task is cancelled, other operations from other tasks will remain active. + +```swift +let channel = AsyncChannel() +Task { + while let resultOfLongCalculation = doLongCalculations() { + await channel.send(resultOfLongCalculation) + } + channel.finish() +} + +for await calculationResult in channel { + print(calculationResult) +} +``` + +The example above uses a task to perform intense calculations; each of which are sent to the other task via the `send(_:)` method. That call to `send(_:)` returns when the next iteration of the channel is invoked. + +## Alternatives Considered + +The use of the name "subject" was considered, due to its heritage as a name for a sync-to-async adapter type. + +It was considered to make `AsyncChannel` and `AsyncThrowingChannel` actors, however due to the cancellation internals it would imply that these types would need to create new tasks to handle cancel events. The advantages of an actor in this particular case did not outweigh the impact of adjusting the implementations to be actors. + +## Future Directions + +`AsyncChannel` and `AsyncThrowingChannel` are just the prominent members of the channel-like behavior algorithms. It is reasonable to have as its own distinct type a buffering channel that provides more [tuned back pressure per a given buffer of elements](https://forums.swift.org/t/asyncchannel-should-we-allow-to-buffer/60876). These other members of the same category of algorithm should be considered on their own as distinct proposals. + +## Credits/Inspiration + +`AsyncChannel` and `AsyncThrowingChannel` was heavily inspired from `Subject` but with the key difference that it uses Swift concurrency to apply back pressure. + +https://developer.apple.com/documentation/combine/subject/ From 220f86f7925430f0a932925b5631fc822b4c25ec Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 21 Sep 2023 08:18:20 -0700 Subject: [PATCH 24/32] Ensure the last element of reduction in the throttle is emitted and use appropriate delay (#292) --- .../AsyncThrottleSequence.swift | 11 +++++++++- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 22 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index ae2b1db4..a8eec469 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -81,7 +81,16 @@ extension AsyncThrottleSequence: AsyncSequence { let start = last ?? clock.now repeat { guard let element = try await base.next() else { - return nil + if reduced != nil, let last { + // ensure the rate of elements never exceeds the given interval + let amount = interval - last.duration(to: clock.now) + if amount > .zero { + try? await clock.sleep(for: amount) + } + } + // the last value is unable to have any subsequent + // values so always return the last reduction + return reduced } let reduction = await reducing(reduced, element) let now = clock.now diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index 72c90f65..4e0d3898 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -72,7 +72,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) - "a--d--g--j-|" + "a--d--g--j--[k|]" } } @@ -81,7 +81,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) - "a--b--e--h-|" + "a--b--e--h--[k|]" } } @@ -138,4 +138,22 @@ final class TestThrottle: XCTestCase { "-a---c---e---g---i---k-|" } } + + func test_trailing_delay_without_latest() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "abcdefghijkl|" + $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) + "a--b--e--h--[k|]" + } + } + + func test_trailing_delay_with_latest() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "abcdefghijkl|" + $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: true) + "a--d--g--j--[l|]" + } + } } From f56556930fc677096331565242024158fd7235b7 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 21 Sep 2023 17:19:00 +0200 Subject: [PATCH 25/32] Remove majority of `@unchecked Sendable` usages (#295) # Motivation `@unchecked Sendable` is a great way to make a type `Sendable` while it is not really `Sendable` this is rarely useful and we should rather use conditional `@unchecked Sendable` annotations such as the one on `ManagedCriticalState` # Modification This PR removes all `@unchecked Sendable` in the main algorithm target except the one on `Merge` since we are doing manual locking there. # Result No more `@unchecked Sendable` usage. --- Sources/AsyncAlgorithms/Channels/AsyncChannel.swift | 2 +- Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift | 2 +- .../AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift | 6 +++--- Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift | 4 ++-- Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index f59b6b5f..026281de 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -19,7 +19,7 @@ /// on the `Iterator` is made, or when `finish()` is called from another Task. /// As `finish()` induces a terminal state, there is no more need for a back pressure management. /// This function does not suspend and will finish all the pending iterations. -public final class AsyncChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index eaa55fcc..63cbf50d 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -18,7 +18,7 @@ /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. /// Those functions do not suspend and will finish all the pending iterations. -public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncThrowingChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index 888d4f42..c57b2c42 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -13,14 +13,14 @@ extension AsyncSequence { /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed by using a specified Clock. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence where Self: Sendable { + public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { AsyncDebounceSequence(self, interval: interval, tolerance: tolerance, clock: clock) } /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence where Self: Sendable { + public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { self.debounce(for: interval, tolerance: tolerance, clock: .continuous) } } @@ -28,7 +28,7 @@ extension AsyncSequence { /// An `AsyncSequence` that emits the latest element after a given quiescence period /// has elapsed. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct AsyncDebounceSequence: Sendable where Base: Sendable { +public struct AsyncDebounceSequence: Sendable where Base.Element: Sendable { private let base: Base private let clock: C private let interval: C.Instant.Duration diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index b75d2f3e..5fb89451 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -10,10 +10,10 @@ //===----------------------------------------------------------------------===// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct DebounceStateMachine { +struct DebounceStateMachine: Sendable where Base.Element: Sendable { typealias Element = Base.Element - private enum State { + private enum State: Sendable { /// The initial state before a call to `next` happened. case initial(base: Base) diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 40c30634..1c223143 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class DebounceStorage: @unchecked Sendable where Base: Sendable { +final class DebounceStorage: Sendable where Base.Element: Sendable { typealias Element = Base.Element /// The state machine protected with a lock. From 260e19822105061bdf021ff01f401c104851143b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 21 Sep 2023 17:20:02 +0200 Subject: [PATCH 26/32] Add support for the Swift package index (#297) # Motivation We had some scripts around to generate docs for this package but all of our other packages are now providing documentation on the Swift package index. # Modification This removes the old script to generate documentation and adds an `.spi.yml` file. Furthermore, I removed the conditional dependency on the docc plugin which we are unconditionally depending on in most of our other packages like NIO. --- .spi.yml | 4 ++ Package.swift | 20 ++------ Package@swift-5.7.swift | 20 ++------ bin/update-gh-pages-documentation-site | 71 -------------------------- 4 files changed, 12 insertions(+), 103 deletions(-) create mode 100644 .spi.yml delete mode 100755 bin/update-gh-pages-documentation-site diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..2a779cf6 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [AsyncAlgorithms] diff --git a/Package.swift b/Package.swift index afc3bcaa..2932e199 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,10 @@ let package = Package( products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], targets: [ .target( name: "AsyncAlgorithms", @@ -46,18 +49,3 @@ let package = Package( ), ] ) - -#if canImport(Darwin) -import Darwin -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#elseif canImport(Glibc) -import Glibc -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#else -let buildingDocs = false -#endif - -// Only require the docc plugin when building documentation -package.dependencies += buildingDocs ? [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), -] : [] diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 56417e84..7c488af0 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -16,7 +16,10 @@ let package = Package( .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], targets: [ .target( name: "AsyncAlgorithms", @@ -34,18 +37,3 @@ let package = Package( dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), ] ) - -#if canImport(Darwin) -import Darwin -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#elseif canImport(Glibc) -import Glibc -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#else -let buildingDocs = false -#endif - -// Only require the docc plugin when building documentation -package.dependencies += buildingDocs ? [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), -] : [] diff --git a/bin/update-gh-pages-documentation-site b/bin/update-gh-pages-documentation-site deleted file mode 100755 index 0c1e59aa..00000000 --- a/bin/update-gh-pages-documentation-site +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -# -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2022 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 -# See https://swift.org/CONTRIBUTORS.txt for Swift project authors -# -# Updates the GitHub Pages documentation site thats published from the 'docs' -# subdirectory in the 'gh-pages' branch of this repository. -# -# This script should be run by someone with commit access to the 'gh-pages' branch -# at a regular frequency so that the documentation content on the GitHub Pages site -# is up-to-date with the content in this repo. -# - -export BUILDING_FOR_DOCUMENTATION_GENERATION=1 - -set -eu - -# A `realpath` alternative using the default C implementation. -filepath() { - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -SWIFT_ASYNC_ALGORITHMS_ROOT="$(dirname $(dirname $(filepath $0)))" - -ASYNC_ALGORITHMS_BUILD_DIR="$SWIFT_ASYNC_ALGORITHMS_ROOT"/.build/async-algorithms-gh-pages-build - -# Set current directory to the repository root -cd "$SWIFT_ASYNC_ALGORITHMS_ROOT" - -# Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory -git fetch -git worktree add --checkout gh-pages origin/gh-pages - -# Pretty print DocC JSON output so that it can be consistently diffed between commits -export DOCC_JSON_PRETTYPRINT="YES" - -# Generate documentation for the 'AsyncAlgorithms' target and output it -# to the /docs subdirectory in the gh-pages worktree directory. -swift package \ - --allow-writing-to-directory "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" \ - generate-documentation \ - --target AsyncAlgorithms \ - --disable-indexing \ - --transform-for-static-hosting \ - --hosting-base-path swift-async-algorithms \ - --output-path "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" - -# Save the current commit we've just built documentation from in a variable -CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` - -# Commit and push our changes to the gh-pages branch -cd gh-pages -git add docs - -if [ -n "$(git status --porcelain)" ]; then - echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin." - git commit -m "Update GitHub Pages documentation site to $CURRENT_COMMIT_HASH" - git push origin HEAD:gh-pages -else - # No changes found, nothing to commit. - echo "No documentation changes found." -fi - -# Delete the git worktree we created -cd .. -git worktree remove gh-pages From 0a3866daecd74737869f70fe43c4ac543fa89089 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 23 Sep 2023 18:16:32 +0200 Subject: [PATCH 27/32] Require 5.8 for SPI --- .spi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.spi.yml b/.spi.yml index 2a779cf6..2c794bc9 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,3 +2,4 @@ version: 1 builder: configs: - documentation_targets: [AsyncAlgorithms] + swift_version: 5.8 From 6360ca0344f058fb048c5597447f3d44f0329296 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 27 Sep 2023 16:19:00 +0100 Subject: [PATCH 28/32] Remove `@_implementationOnly` usage (#294) # Motivation We added `@_implementationOnly` a little while ago to hide the dependency on the `DequeModule`. However, with recent compilers this produces a warning since `@_implementationOnly` is only intended to be used in resilient libraries. # Modification This PR removes the usage of `@_implementationOnly` --- .../AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift | 2 +- .../Buffer/UnboundedBufferStateMachine.swift | 2 +- Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift | 2 +- .../CombineLatest/CombineLatestStateMachine.swift | 2 +- Sources/AsyncAlgorithms/Locking.swift | 6 +++--- Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift | 2 +- Sources/AsyncSequenceValidation/TaskDriver.swift | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index b863bc50..9ca7a993 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule struct BoundedBufferStateMachine { typealias Element = Base.Element diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index a43c2023..de5d37ae 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule struct UnboundedBufferStateMachine { typealias Element = Base.Element diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index e823e5f7..2c8b1b92 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -8,7 +8,7 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -@_implementationOnly import OrderedCollections +import OrderedCollections // NOTE: this is only marked as unchecked since the swift-collections tag is before auditing for Sendable extension OrderedSet: @unchecked Sendable where Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 71d0507a..5217e8de 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// State machine for combine latest struct CombineLatestStateMachine< diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index e1ed2626..952b13c8 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -10,11 +10,11 @@ //===----------------------------------------------------------------------===// #if canImport(Darwin) -@_implementationOnly import Darwin +import Darwin #elseif canImport(Glibc) -@_implementationOnly import Glibc +import Glibc #elseif canImport(WinSDK) -@_implementationOnly import WinSDK +import WinSDK #endif internal struct Lock { diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 2de482c8..9f82ed98 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index 8cafcd95..d5576694 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge< diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index e3bb59e4..bb832ada 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// The state machine for any of the `merge` operator. /// diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 69c8fe5c..ed128193 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -12,9 +12,9 @@ import _CAsyncSequenceValidationSupport #if canImport(Darwin) -@_implementationOnly import Darwin +import Darwin #elseif canImport(Glibc) -@_implementationOnly import Glibc +import Glibc #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") #endif From 8cfdf03b518ad8651044eb734615d557ac23359d Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 27 Sep 2023 16:26:01 +0100 Subject: [PATCH 29/32] Fix 5.7/5.8 build errors (#298) # Motivation Currently, this repo fails to build on Swift 5.7 and 5.8 since we were using clock APIs that were only available on 5.9. --- Sources/AsyncAlgorithms/AsyncThrottleSequence.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index a8eec469..f515fe6a 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -85,7 +85,7 @@ extension AsyncThrottleSequence: AsyncSequence { // ensure the rate of elements never exceeds the given interval let amount = interval - last.duration(to: clock.now) if amount > .zero { - try? await clock.sleep(for: amount) + try? await clock.sleep(until: clock.now.advanced(by: amount), tolerance: nil) } } // the last value is unable to have any subsequent From cb417003f962f9de3fc7852c1b735a1f1152a89a Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 27 Sep 2023 16:35:58 +0100 Subject: [PATCH 30/32] Make throttle underscored (#296) # Motivation During the discussion in https://github.com/apple/swift-async-algorithms/issues/248 it became clear that the semantics of `throttle` are not 100% figured out yet. # Modification This PR is making `throttle` an underscored API for the upcoming 1.0.0 release. This gives us more time to investigate what exact semantics we want to have. --- .../AsyncThrottleSequence.swift | 24 +++++++------- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 32 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index f515fe6a..4dbc1e48 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -12,20 +12,20 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> AsyncThrottleSequence { - AsyncThrottleSequence(self, interval: interval, clock: clock, reducing: reducing) + public func _throttle(for interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> _AsyncThrottleSequence { + _AsyncThrottleSequence(self, interval: interval, clock: clock, reducing: reducing) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: Duration, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> AsyncThrottleSequence { - throttle(for: interval, clock: .continuous, reducing: reducing) + public func _throttle(for interval: Duration, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: .continuous, reducing: reducing) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: C.Instant.Duration, clock: C, latest: Bool = true) -> AsyncThrottleSequence { - throttle(for: interval, clock: clock) { previous, element in + public func _throttle(for interval: C.Instant.Duration, clock: C, latest: Bool = true) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: clock) { previous, element in if latest { return element } else { @@ -36,14 +36,14 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: Duration, latest: Bool = true) -> AsyncThrottleSequence { - throttle(for: interval, clock: .continuous, latest: latest) + public func _throttle(for interval: Duration, latest: Bool = true) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: .continuous, latest: latest) } } /// A rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct AsyncThrottleSequence { +public struct _AsyncThrottleSequence { let base: Base let interval: C.Instant.Duration let clock: C @@ -58,7 +58,7 @@ public struct AsyncThrottleSequence { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence: AsyncSequence { +extension _AsyncThrottleSequence: AsyncSequence { public typealias Element = Reduced /// The iterator for an `AsyncThrottleSequence` instance. @@ -110,7 +110,7 @@ extension AsyncThrottleSequence: AsyncSequence { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } +extension _AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } @available(*, unavailable) -extension AsyncThrottleSequence.Iterator: Sendable { } +extension _AsyncThrottleSequence.Iterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index 4e0d3898..d7b60db3 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -17,7 +17,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(0), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(0), clock: $0.clock) "abcdefghijk|" } } @@ -26,7 +26,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(0), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(0), clock: $0.clock, latest: false) "abcdefghijk|" } } @@ -35,7 +35,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock) "abcdefghijk|" } } @@ -44,7 +44,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock, latest: false) "abcdefghijk|" } } @@ -53,7 +53,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "a-c-e-g-i-k|" } } @@ -62,7 +62,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock, latest: false) "a-b-d-f-h-j|" } } @@ -71,7 +71,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock) "a--d--g--j--[k|]" } } @@ -80,7 +80,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: false) "a--b--e--h--[k|]" } } @@ -89,7 +89,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdef^hijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "a-c-e-^" } } @@ -98,7 +98,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdef^hijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock, latest: false) "a-b-d-^" } } @@ -107,7 +107,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock) "-a-b-c-d-e-f-g-h-i-j-k-|" } } @@ -116,7 +116,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "-a-b-c-d-e-f-g-h-i-j-k-|" } } @@ -125,7 +125,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "--a--b--c--d--e--f--g|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "--a--b--c--d--e--f--g|" } } @@ -134,7 +134,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock) "-a---c---e---g---i---k-|" } } @@ -143,7 +143,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijkl|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: false) "a--b--e--h--[k|]" } } @@ -152,7 +152,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijkl|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: true) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: true) "a--d--g--j--[l|]" } } From 5bbdcc1d37990cca524852c5a3924c02fa2da983 Mon Sep 17 00:00:00 2001 From: Artem Gavrilik <73897254+GavrilikArt@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:42:24 +0200 Subject: [PATCH 31/32] Fix typo in BoundedBufferStateMachine (#301) --- .../Buffer/BoundedBufferStateMachine.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index 9ca7a993..5c99d3d7 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -64,7 +64,7 @@ struct BoundedBufferStateMachine { mutating func shouldSuspendProducer() -> Bool { switch state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(_, let buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -95,7 +95,7 @@ struct BoundedBufferStateMachine { mutating func producerSuspended(continuation: SuspendedProducer) -> ProducerSuspendedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, let buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -132,7 +132,7 @@ struct BoundedBufferStateMachine { mutating func elementProduced(element: Element) -> ElementProducedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, var buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -170,7 +170,7 @@ struct BoundedBufferStateMachine { mutating func finish(error: Error?) -> FinishAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(_, var buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -245,7 +245,7 @@ struct BoundedBufferStateMachine { mutating func nextSuspended(continuation: SuspendedConsumer) -> NextSuspendedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, let buffer, .none, .none) where buffer.isEmpty: // we are idle, we confirm the suspension of the consumer From da4e36f86544cdf733a40d59b3a2267e3a7bbf36 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 17 Nov 2023 16:20:14 +0000 Subject: [PATCH 32/32] Fix potential deadlocks when resuming a continuation while holding a lock (#303) * Fix potential deadlocks in zip * Fix debounce * Fix combineLatest * Fix Channel * Fix buffer --- .../Buffer/BoundedBufferStorage.swift | 129 ++++---- .../Buffer/UnboundedBufferStorage.swift | 101 +++--- .../Channels/ChannelStorage.swift | 145 +++++---- .../CombineLatest/CombineLatestStorage.swift | 223 +++++++------ .../Debounce/DebounceStorage.swift | 138 +++++---- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 293 +++++++++--------- 6 files changed, 553 insertions(+), 476 deletions(-) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index c00360e1..f83a37fa 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -18,34 +18,47 @@ final class BoundedBufferStorage: Sendable where Base: Send func next() async -> Result? { return await withTaskCancellationHandler { - let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action: BoundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next() switch action { case .startTask(let base): self.startTask(stateMachine: &stateMachine, base: base) - return (true, nil) + return nil + case .suspend: - return (true, nil) - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - return (false, result) + return action + case .returnResult: + return action } } - if !shouldSuspend { - return result + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .suspend: + break + + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + return result + + case .none: + break } return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended(continuation: continuation) - switch action { - case .none: - break - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended(continuation: continuation) + } + switch action { + case .none: + break + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + continuation.resume(returning: result) } } } onCancel: { @@ -68,15 +81,15 @@ final class BoundedBufferStorage: Sendable where Base: Send if shouldSuspend { await withUnsafeContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.producerSuspended(continuation: continuation) - - switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.producerSuspended(continuation: continuation) + } + + switch action { + case .none: + break + case .resumeProducer: + continuation.resume() } } } @@ -86,35 +99,35 @@ final class BoundedBufferStorage: Sendable where Base: Send break loop } - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced(element: element) - switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced(element: element) } - } - - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: nil) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } + + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: nil) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } catch { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) - switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -123,16 +136,16 @@ final class BoundedBufferStorage: Sendable where Base: Send } func interrupted() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.interrupted() - switch action { - case .none: - break - case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): - task.cancel() - producerContinuation?.resume() - consumerContinuation?.resume(returning: nil) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.interrupted() + } + switch action { + case .none: + break + case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): + task.cancel() + producerContinuation?.resume() + consumerContinuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift index 59b02810..b63b261f 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -19,32 +19,41 @@ final class UnboundedBufferStorage: Sendable where Base: Se func next() async -> Result? { return await withTaskCancellationHandler { - let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action: UnboundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next() switch action { case .startTask(let base): self.startTask(stateMachine: &stateMachine, base: base) - return (true, nil) + return nil case .suspend: - return (true, nil) - case .returnResult(let result): - return (false, result) + return action + case .returnResult: + return action } } - if !shouldSuspend { - return result + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + case .suspend: + break + case .returnResult(let result): + return result + case .none: + break } return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended(continuation: continuation) - switch action { - case .none: - break - case .resumeConsumer(let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended(continuation: continuation) + } + switch action { + case .none: + break + case .resumeConsumer(let result): + continuation.resume(returning: result) } } } onCancel: { @@ -59,35 +68,35 @@ final class UnboundedBufferStorage: Sendable where Base: Se let task = Task { do { for try await element in base { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced(element: element) - switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced(element: element) } - } - - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: nil) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } + + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: nil) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } catch { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) - switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -96,15 +105,15 @@ final class UnboundedBufferStorage: Sendable where Base: Se } func interrupted() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.interrupted() - switch action { - case .none: - break - case .resumeConsumer(let task, let continuation): - task.cancel() - continuation?.resume(returning: nil) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.interrupted() + } + switch action { + case .none: + break + case .resumeConsumer(let task, let continuation): + task.cancel() + continuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index 12b5ba72..0fb67818 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -25,21 +25,17 @@ struct ChannelStorage: Sendable { func send(element: Element) async { // check if a suspension is needed - let shouldExit = self.stateMachine.withCriticalRegion { stateMachine -> Bool in - let action = stateMachine.send() - - switch action { - case .suspend: - // the element has not been delivered because no consumer available, we must suspend - return false - case .resumeConsumer(let continuation): - continuation?.resume(returning: element) - return true - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.send() } - if shouldExit { - return + switch action { + case .suspend: + break + + case .resumeConsumer(let continuation): + continuation?.resume(returning: element) + return } let producerID = self.generateId() @@ -47,103 +43,100 @@ struct ChannelStorage: Sendable { await withTaskCancellationHandler { // a suspension is needed await withUnsafeContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) - - switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - case .resumeProducerAndConsumer(let consumerContinuation): - continuation.resume() - consumerContinuation?.resume(returning: element) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) } - } - } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.sendCancelled(producerID: producerID) switch action { case .none: break - case .resumeProducer(let continuation): - continuation?.resume() + case .resumeProducer: + continuation.resume() + case .resumeProducerAndConsumer(let consumerContinuation): + continuation.resume() + consumerContinuation?.resume(returning: element) } } - } - } - - func finish(error: Failure? = nil) { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) + } onCancel: { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.sendCancelled(producerID: producerID) + } switch action { case .none: break - case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): - producerContinuations.forEach { $0?.resume() } - if let error { - consumerContinuations.forEach { $0?.resume(throwing: error) } - } else { - consumerContinuations.forEach { $0?.resume(returning: nil) } - } + case .resumeProducer(let continuation): + continuation?.resume() } } } - func next() async throws -> Element? { - let (shouldExit, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in - let action = stateMachine.next() + func finish(error: Failure? = nil) { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } - switch action { - case .suspend: - return (false, nil) - case .resumeProducer(let producerContinuation, let result): - producerContinuation?.resume() - return (true, result) - } + switch action { + case .none: + break + case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): + producerContinuations.forEach { $0?.resume() } + if let error { + consumerContinuations.forEach { $0?.resume(throwing: error) } + } else { + consumerContinuations.forEach { $0?.resume(returning: nil) } + } } + } - if shouldExit { - return try result?._rethrowGet() + func next() async throws -> Element? { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.next() + } + + switch action { + case .suspend: + break + + case .resumeProducer(let producerContinuation, let result): + producerContinuation?.resume() + return try result._rethrowGet() } let consumerID = self.generateId() return try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended( + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended( continuation: continuation, consumerID: consumerID ) - - switch action { - case .none: - break - case .resumeConsumer(let element): - continuation.resume(returning: element) - case .resumeConsumerWithError(let error): - continuation.resume(throwing: error) - case .resumeProducerAndConsumer(let producerContinuation, let element): - producerContinuation?.resume() - continuation.resume(returning: element) - } } - } - } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextCancelled(consumerID: consumerID) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let element): + continuation.resume(returning: element) + case .resumeConsumerWithError(let error): + continuation.resume(throwing: error) + case .resumeProducerAndConsumer(let producerContinuation, let element): + producerContinuation?.resume() + continuation.resume(returning: element) } } + } onCancel: { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextCancelled(consumerID: consumerID) + } + + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } } } diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index d3b67404..0d97adea 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -48,7 +48,7 @@ final class CombineLatestStorage< func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? { try await withTaskCancellationHandler { let result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in + let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next(for: continuation) switch action { case .startTask(let base1, let base2, let base3): @@ -60,45 +60,65 @@ final class CombineLatestStorage< base3: base3, downstreamContinuation: continuation ) + return nil - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + case .resumeContinuation: + return action - case .resumeUpstreamContinuations(let upstreamContinuations): - // bases can be iterated over for 1 iteration so their next value can be retrieved - upstreamContinuations.forEach { $0.resume() } + case .resumeUpstreamContinuations: + return action - case .resumeDownstreamContinuationWithNil(let continuation): - // the async sequence is already finished, immediately resuming - continuation.resume(returning: .success(nil)) + case .resumeDownstreamContinuationWithNil: + return action } } + + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .resumeUpstreamContinuations(let upstreamContinuations): + // bases can be iterated over for 1 iteration so their next value can be retrieved + upstreamContinuations.forEach { $0.resume() } + + case .resumeDownstreamContinuationWithNil(let continuation): + // the async sequence is already finished, immediately resuming + continuation.resume(returning: .success(nil)) + + case .none: + break + } } return try result._rethrowGet() } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.cancelled() + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.cancelled() + } - switch action { - case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() - downstreamContinuation.resume(returning: .success(nil)) + downstreamContinuation.resume(returning: .success(nil)) - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() - case .none: - break - } + case .none: + break } } } @@ -124,33 +144,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element1 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((element1, nil, nil)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((element1, nil, nil)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -191,33 +211,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element2 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((nil, element2, nil)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((nil, element2, nil)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -259,33 +279,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element3 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((nil, nil, element3)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((nil, nil, element3)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -323,28 +343,29 @@ final class CombineLatestStorage< do { try await group.next() } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - switch action { - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let error, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - downstreamContinuation.resume(returning: .failure(error)) - case .none: - break - } - } + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) + } + + switch action { + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let error, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + downstreamContinuation.resume(returning: .failure(error)) + case .none: + break + } - group.cancelAll() + group.cancelAll() } } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 1c223143..1839e334 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -55,36 +55,59 @@ final class DebounceStorage: Sendable // We always suspend since we can never return an element right away let result: Result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { - let action = $0.next(for: continuation) - - switch action { - case .startTask(let base): - self.startTask( - stateMachine: &$0, - base: base, - downstreamContinuation: continuation - ) - - case .resumeUpstreamContinuation(let upstreamContinuation): - // This is signalling the upstream task that is consuming the upstream - // sequence to signal demand. - upstreamContinuation?.resume(returning: ()) - - case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): - // This is signalling the upstream task that is consuming the upstream - // sequence to signal demand and start the clock task. - upstreamContinuation?.resume(returning: ()) - clockContinuation?.resume(returning: deadline) - - case .resumeDownstreamContinuationWithNil(let continuation): - continuation.resume(returning: .success(nil)) - - case .resumeDownstreamContinuationWithError(let continuation, let error): - continuation.resume(returning: .failure(error)) - } + let action: DebounceStateMachine.NextAction? = self.stateMachine.withCriticalRegion { + let action = $0.next(for: continuation) + + switch action { + case .startTask(let base): + self.startTask( + stateMachine: &$0, + base: base, + downstreamContinuation: continuation + ) + return nil + + case .resumeUpstreamContinuation: + return action + + case .resumeUpstreamAndClockContinuation: + return action + + case .resumeDownstreamContinuationWithNil: + return action + + case .resumeDownstreamContinuationWithError: + return action } - } + } + + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .resumeUpstreamContinuation(let upstreamContinuation): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand. + upstreamContinuation?.resume(returning: ()) + + case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand and start the clock task. + upstreamContinuation?.resume(returning: ()) + clockContinuation?.resume(returning: deadline) + + case .resumeDownstreamContinuationWithNil(let continuation): + continuation.resume(returning: .success(nil)) + + case .resumeDownstreamContinuationWithError(let continuation, let error): + continuation.resume(returning: .failure(error)) + + case .none: + break + } + } return try result._rethrowGet() } onCancel: { @@ -258,37 +281,38 @@ final class DebounceStorage: Sendable do { try await group.next() } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - switch action { - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - let downstreamContinuation, - let error, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .cancelTaskAndClockContinuation( - let task, - let clockContinuation - ): - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - case .none: - break - } + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) } - group.cancelAll() + switch action { + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + let downstreamContinuation, + let error, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) + + task.cancel() + + downstreamContinuation.resume(returning: .failure(error)) + + case .cancelTaskAndClockContinuation( + let task, + let clockContinuation + ): + clockContinuation?.resume(throwing: CancellationError()) + task.cancel() + case .none: + break + } } + + group.cancelAll() } } } diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index 7d971a78..93a3466c 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -39,7 +39,7 @@ final class ZipStorage (Base1.Element, Base2.Element, Base3.Element?)? { try await withTaskCancellationHandler { let result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in + let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next(for: continuation) switch action { case .startTask(let base1, let base2, let base3): @@ -51,42 +51,59 @@ final class ZipStorage