diff --git a/.license_header_template b/.license_header_template new file mode 100644 index 00000000..888a0c9c --- /dev/null +++ b/.license_header_template @@ -0,0 +1,10 @@ +@@===----------------------------------------------------------------------===@@ +@@ +@@ This source file is part of the Swift Async Algorithms open source project +@@ +@@ Copyright (c) YEARS Apple Inc. and the Swift project authors +@@ Licensed under Apache License v2.0 with Runtime Library Exception +@@ +@@ See https://swift.org/LICENSE.txt for license information +@@ +@@===----------------------------------------------------------------------===@@ diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 00000000..4c6b2382 --- /dev/null +++ b/.licenseignore @@ -0,0 +1,40 @@ +.gitignore +**/.gitignore +.licenseignore +.gitattributes +.mailfilter +.mailmap +.spi.yml +.swift-format +.editorconfig +.github/* +*.md +*.txt +*.yml +*.yaml +*.json +Package.swift +**/Package.swift +Package@-*.swift +Package@swift-*.swift +**/Package@-*.swift +Package.resolved +**/Package.resolved +Makefile +*.modulemap +**/*.modulemap +**/*.docc/* +*.xcprivacy +**/*.xcprivacy +*.symlink +**/*.symlink +Dockerfile +**/Dockerfile +Snippets/* +dev/git.commit.template +*.crt +**/*.crt +*.pem +**/*.pem +*.der +**/*.der diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..7eda7043 --- /dev/null +++ b/.swift-format @@ -0,0 +1,58 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : true, + "lineLength" : 120, + "maximumBlankLines" : 1, + "prioritizeKeepingFunctionOutputTogether" : true, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : false, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : false, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : true, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 4, + "version" : 1 +} \ No newline at end of file diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 3eb557ea..00000000 --- a/.swiftformat +++ /dev/null @@ -1,25 +0,0 @@ -# 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/.yamllint.yml b/.yamllint.yml new file mode 100644 index 00000000..52a1770f --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,7 @@ +extends: default + +rules: + line-length: false + document-start: false + truthy: + check-keys: false # Otherwise we get a false positive on GitHub action's `on` key diff --git a/Evolution/0000-swift-async-algorithms-template.md b/Evolution/0000-swift-async-algorithms-template.md index e5b35999..721c70ba 100644 --- a/Evolution/0000-swift-async-algorithms-template.md +++ b/Evolution/0000-swift-async-algorithms-template.md @@ -51,7 +51,7 @@ would become part of a public API? If so, what kinds of changes can be made without breaking ABI? Can this feature be added/removed without breaking ABI? For more information about the resilience model, see the [library evolution -document](https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst) +document](https://github.com/apple/swift/blob/main/docs/LibraryEvolution.rst) in the Swift repository. ## Alternatives considered diff --git a/Package.swift b/Package.swift index 4132319a..1177d22d 100644 --- a/Package.swift +++ b/Package.swift @@ -8,27 +8,27 @@ let package = Package( .macOS("10.15"), .iOS("13.0"), .tvOS("13.0"), - .watchOS("6.0") + .watchOS("6.0"), ], products: [ - .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), + .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]) ], targets: [ .target( name: "AsyncAlgorithms", dependencies: [ - .product(name: "OrderedCollections", package: "swift-collections"), - .product(name: "DequeModule", package: "swift-collections"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections"), ], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableExperimentalFeature("StrictConcurrency=complete") ] ), .target( name: "AsyncSequenceValidation", dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableExperimentalFeature("StrictConcurrency=complete") ] ), .systemLibrary(name: "_CAsyncSequenceValidationSupport"), @@ -36,14 +36,14 @@ let package = Package( name: "AsyncAlgorithms_XCTest", dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableExperimentalFeature("StrictConcurrency=complete") ] ), .testTarget( name: "AsyncAlgorithmsTests", dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableExperimentalFeature("StrictConcurrency=complete") ] ), ] @@ -56,6 +56,6 @@ if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { ] } else { package.dependencies += [ - .package(path: "../swift-collections"), + .package(path: "../swift-collections") ] } diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 7c488af0..88c8f069 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -8,7 +8,7 @@ let package = Package( .macOS("10.15"), .iOS("13.0"), .tvOS("13.0"), - .watchOS("6.0") + .watchOS("6.0"), ], products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), @@ -27,13 +27,16 @@ let package = Package( ), .target( name: "AsyncSequenceValidation", - dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"] + ), .systemLibrary(name: "_CAsyncSequenceValidationSupport"), .target( name: "AsyncAlgorithms_XCTest", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"] + ), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"] + ), ] ) diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index b1a0a156..0ba5e90d 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -83,7 +83,7 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { } } -extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) -extension AsyncAdjacentPairsSequence.Iterator: Sendable { } +extension AsyncAdjacentPairsSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md index af576312..308fbc83 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md @@ -1,10 +1,10 @@ # AsyncBufferedByteIterator +Provides a highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. + [[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)] -Provides a highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. - This type provides infrastructure for creating `AsyncSequence` types with an `Element` of `UInt8` backed by file descriptors or similar read sources. ```swift diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md index 190e6c3e..2db97f1d 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md @@ -1,5 +1,7 @@ # Chain +Chains two or more asynchronous sequences together sequentially. + [[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/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md index b481eaae..f1e36550 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md @@ -1,10 +1,10 @@ # Combine Latest +Combines the latest values produced from two or more asynchronous sequences into an asynchronous sequence of tuples. + [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift)] -Combines the latest values produced from two or more asynchronous sequences into an asynchronous sequence of tuples. - ```swift let appleFeed = URL("http://www.example.com/ticker?symbol=AAPL").lines let nasdaqFeed = URL("http://www.example.com/ticker?symbol=^IXIC").lines diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md index 82830bb6..3d965151 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md @@ -1,3 +1,7 @@ +# Effects + +Lists the effects of all async algorithms. + | Type | Throws | Sendability | |-----------------------------------------------------|--------------|-------------| | `AsyncAdjacentPairsSequence` | rethrows | Conditional | diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md index 71d827bb..fbe775d1 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md @@ -1,10 +1,10 @@ # Intersperse +Places a given value in between each element of the asynchronous sequence. + [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestInterspersed.swift)] -Places a given value in between each element of the asynchronous sequence. - ```swift let numbers = [1, 2, 3].async.interspersed(with: 0) for await number in numbers { diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md index fe5dda9d..48f8cfbf 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md @@ -1,12 +1,10 @@ # AsyncSyncSequence +This operation is available for all `Sequence` types. + [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncSyncSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift)] -Converts a non-asynchronous sequence into an asynchronous one. - -This operation is available for all `Sequence` types. - ```swift let numbers = [1, 2, 3, 4].async let characters = "abcde".async diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md index edc1842b..9b4328c5 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md @@ -1,10 +1,10 @@ # Merge +Merges two or more asynchronous sequences sharing the same element type into one singular asynchronous sequence. + [[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)] -Merges two or more asynchronous sequences sharing the same element type into one singular asynchronous sequence. - ```swift let appleFeed = URL(string: "http://www.example.com/ticker?symbol=AAPL")!.lines.map { "AAPL: " + $0 } let nasdaqFeed = URL(string:"http://www.example.com/ticker?symbol=^IXIC")!.lines.map { "^IXIC: " + $0 } diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md index e73e1fde..2c34b4ab 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md @@ -1,10 +1,10 @@ # Zip +Combines the latest values produced from two or more asynchronous sequences into an asynchronous sequence of tuples. + [[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)] -Combines the latest values produced from two or more asynchronous sequences into an asynchronous sequence of tuples. - ```swift let appleFeed = URL(string: "http://www.example.com/ticker?symbol=AAPL")!.lines let nasdaqFeed = URL(string: "http://www.example.com/ticker?symbol=^IXIC")!.lines diff --git a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift index 9016b7af..4d696e26 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift @@ -42,7 +42,7 @@ public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 @usableFromInline var buffer: _AsyncBytesBuffer - + /// Creates an asynchronous buffered byte iterator with a specified capacity and read function. /// /// - Parameters: @@ -55,7 +55,7 @@ public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { ) { buffer = _AsyncBytesBuffer(capacity: capacity, readFunction: readFunction) } - + /// Reads a byte out of the buffer if available. When no bytes are available, this will trigger /// the read function to reload the buffer and then return the next byte from that buffer. @inlinable @inline(__always) @@ -65,14 +65,14 @@ public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { } @available(*, unavailable) -extension AsyncBufferedByteIterator: Sendable { } +extension AsyncBufferedByteIterator: Sendable {} @frozen @usableFromInline internal struct _AsyncBytesBuffer { @usableFromInline final class Storage { fileprivate let buffer: UnsafeMutableRawBufferPointer - + init( capacity: Int ) { @@ -82,19 +82,19 @@ internal struct _AsyncBytesBuffer { alignment: MemoryLayout.alignment ) } - + deinit { buffer.deallocate() } } - + @usableFromInline internal let storage: Storage @usableFromInline internal var nextPointer: UnsafeRawPointer @usableFromInline internal var endPointer: UnsafeRawPointer - + internal let readFunction: @Sendable (UnsafeMutableRawBufferPointer) async throws -> Int internal var finished = false - + @usableFromInline init( capacity: Int, readFunction: @Sendable @escaping (UnsafeMutableRawBufferPointer) async throws -> Int @@ -105,7 +105,7 @@ internal struct _AsyncBytesBuffer { nextPointer = UnsafeRawPointer(s.buffer.baseAddress!) endPointer = nextPointer } - + @inline(never) @usableFromInline internal mutating func reloadBufferAndNext() async throws -> UInt8? { if finished { @@ -128,7 +128,7 @@ internal struct _AsyncBytesBuffer { } return try await next() } - + @inlinable @inline(__always) internal mutating func next() async throws -> UInt8? { if _fastPath(nextPointer != endPointer) { diff --git a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift index 3e9b4c4f..d0e70250 100644 --- a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift @@ -18,7 +18,10 @@ /// - Returns: An asynchronous sequence that iterates first over the elements of `s1`, and /// then over the elements of `s2`. @inlinable -public func chain(_ s1: Base1, _ s2: Base2) -> AsyncChain2Sequence where Base1.Element == Base2.Element { +public func chain( + _ s1: Base1, + _ s2: Base2 +) -> AsyncChain2Sequence where Base1.Element == Base2.Element { AsyncChain2Sequence(s1, s2) } @@ -27,10 +30,10 @@ public func chain(_ s1: Base1, _ s2: public struct AsyncChain2Sequence where Base1.Element == Base2.Element { @usableFromInline let base1: Base1 - + @usableFromInline let base2: Base2 - + @usableFromInline init(_ base1: Base1, _ base2: Base2) { self.base1 = base1 @@ -40,22 +43,22 @@ public struct AsyncChain2Sequence wh extension AsyncChain2Sequence: AsyncSequence { public typealias Element = Base1.Element - + /// The iterator for a `AsyncChain2Sequence` instance. @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline var base1: Base1.AsyncIterator? - + @usableFromInline var base2: Base2.AsyncIterator? - + @usableFromInline init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator) { self.base1 = base1 self.base2 = base2 } - + @inlinable public mutating func next() async rethrows -> Element? { do { @@ -72,14 +75,14 @@ extension AsyncChain2Sequence: AsyncSequence { } } } - + @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator()) } } -extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable { } +extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable {} @available(*, unavailable) -extension AsyncChain2Sequence.Iterator: Sendable { } +extension AsyncChain2Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index e88e3584..ec6d68ae 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -19,22 +19,27 @@ /// - Returns: An asynchronous sequence that iterates first over the elements of `s1`, and /// then over the elements of `s2`, and then over the elements of `s3` @inlinable -public func chain(_ s1: Base1, _ s2: Base2, _ s3: Base3) -> AsyncChain3Sequence { +public func chain( + _ s1: Base1, + _ s2: Base2, + _ s3: Base3 +) -> AsyncChain3Sequence { AsyncChain3Sequence(s1, s2, s3) } /// A concatenation of three asynchronous sequences with the same element type. @frozen -public struct AsyncChain3Sequence where Base1.Element == Base2.Element, Base1.Element == Base3.Element { +public struct AsyncChain3Sequence +where Base1.Element == Base2.Element, Base1.Element == Base3.Element { @usableFromInline let base1: Base1 - + @usableFromInline let base2: Base2 - + @usableFromInline let base3: Base3 - + @usableFromInline init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { self.base1 = base1 @@ -45,26 +50,26 @@ public struct AsyncChain3Sequence Element? { do { @@ -87,14 +92,14 @@ extension AsyncChain3Sequence: AsyncSequence { } } } - + @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator(), base3.makeAsyncIterator()) } } -extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } +extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable {} @available(*, unavailable) -extension AsyncChain3Sequence.Iterator: Sendable { } +extension AsyncChain3Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index 0ce5d199..a0e8b446 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -13,13 +13,18 @@ extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` /// type by testing if elements belong in the same group. @inlinable - public func chunked(into: Collected.Type, by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool) -> AsyncChunkedByGroupSequence where Collected.Element == Element { + public func chunked( + into: Collected.Type, + by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool + ) -> AsyncChunkedByGroupSequence where Collected.Element == Element { AsyncChunkedByGroupSequence(self, grouping: belongInSameGroup) } /// Creates an asynchronous sequence that creates chunks by testing if elements belong in the same group. @inlinable - public func chunked(by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool) -> AsyncChunkedByGroupSequence { + public func chunked( + by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool + ) -> AsyncChunkedByGroupSequence { chunked(into: [Element].self, by: belongInSameGroup) } } @@ -46,7 +51,8 @@ extension AsyncSequence { /// // [10, 20, 30] /// // [10, 40, 40] /// // [10, 20] -public struct AsyncChunkedByGroupSequence: AsyncSequence where Collected.Element == Base.Element { +public struct AsyncChunkedByGroupSequence: AsyncSequence +where Collected.Element == Base.Element { public typealias Element = Collected /// The iterator for a `AsyncChunkedByGroupSequence` instance. @@ -76,7 +82,7 @@ public struct AsyncChunkedByGroupSequence Bool + let grouping: @Sendable (Base.Element, Base.Element) -> Bool @usableFromInline init(_ base: Base, grouping: @escaping @Sendable (Base.Element, Base.Element) -> Bool) { @@ -116,7 +121,7 @@ public struct AsyncChunkedByGroupSequence(into: Collected.Type, on projection: @escaping @Sendable (Element) -> Subject) -> AsyncChunkedOnProjectionSequence { + public func chunked( + into: Collected.Type, + on projection: @escaping @Sendable (Element) -> Subject + ) -> AsyncChunkedOnProjectionSequence { AsyncChunkedOnProjectionSequence(self, projection: projection) } /// Creates an asynchronous sequence that creates chunks on the uniqueness of a given subject. @inlinable - public func chunked(on projection: @escaping @Sendable (Element) -> Subject) -> AsyncChunkedOnProjectionSequence { + public func chunked( + on projection: @escaping @Sendable (Element) -> Subject + ) -> AsyncChunkedOnProjectionSequence { chunked(into: [Element].self, on: projection) } } /// An `AsyncSequence` that chunks on a subject when it differs from the last element. -public struct AsyncChunkedOnProjectionSequence: AsyncSequence where Collected.Element == Base.Element { +public struct AsyncChunkedOnProjectionSequence< + Base: AsyncSequence, + Subject: Equatable, + Collected: RangeReplaceableCollection +>: AsyncSequence where Collected.Element == Base.Element { public typealias Element = (Subject, Collected) /// The iterator for a `AsyncChunkedOnProjectionSequence` instance. @@ -67,22 +76,21 @@ public struct AsyncChunkedOnProjectionSequence Subject + let projection: @Sendable (Base.Element) -> Subject @usableFromInline init(_ base: Base, projection: @escaping @Sendable (Base.Element) -> Subject) { @@ -96,7 +104,7 @@ public struct AsyncChunkedOnProjectionSequence(ofCount count: Int, or signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunks( + ofCount count: Int, + or signal: Signal, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: signal) } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. - public func chunks(ofCount count: Int, or signal: Signal) -> AsyncChunksOfCountOrSignalSequence { + public func chunks( + ofCount count: Int, + or signal: Signal + ) -> AsyncChunksOfCountOrSignalSequence { chunks(ofCount: count, or: signal, into: [Element].self) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. - public func chunked(by signal: Signal, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { + public func chunked( + by signal: Signal, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: signal) } @@ -32,31 +42,54 @@ extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunks( + ofCount count: Int, + or timer: AsyncTimerSequence, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunks(ofCount count: Int, or timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { + public func chunks( + ofCount count: Int, + or timer: AsyncTimerSequence + ) -> AsyncChunksOfCountOrSignalSequence> { chunks(ofCount: count, or: timer, into: [Element].self) } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence, into: Collected.Type) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { + public func chunked( + by timer: AsyncTimerSequence, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence> where Collected.Element == Element { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func chunked(by timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { + public func chunked( + by timer: AsyncTimerSequence + ) -> AsyncChunksOfCountOrSignalSequence> { chunked(by: timer, into: [Element].self) } } /// An `AsyncSequence` that chunks elements into collected `RangeReplaceableCollection` instances by either count or a signal from another `AsyncSequence`. -public struct AsyncChunksOfCountOrSignalSequence: AsyncSequence, Sendable where Collected.Element == Base.Element, Base: Sendable, Signal: Sendable, Base.Element: Sendable, Signal.Element: Sendable { +public struct AsyncChunksOfCountOrSignalSequence< + Base: AsyncSequence, + Collected: RangeReplaceableCollection, + Signal: AsyncSequence +>: AsyncSequence, Sendable +where + Collected.Element == Base.Element, + Base: Sendable, + Signal: Sendable, + Base.Element: Sendable, + Signal.Element: Sendable +{ public typealias Element = Collected @@ -65,23 +98,23 @@ public struct AsyncChunksOfCountOrSignalSequence typealias EitherMappedSignal = AsyncMapSequence typealias ChainedBase = AsyncChain2Sequence> typealias Merged = AsyncMerge2Sequence - + let count: Int? var iterator: Merged.AsyncIterator var terminated = false - + init(iterator: Merged.AsyncIterator, count: Int?) { self.count = count self.iterator = iterator } - + public mutating func next() async rethrows -> Collected? { guard !terminated else { return nil @@ -124,10 +157,16 @@ public struct AsyncChunksOfCountOrSignalSequence Iterator { - - return Iterator(iterator: merge(chain(base.map { Either.element($0) }, [.terminal].async), signal.map { _ in Either.signal }).makeAsyncIterator(), count: count) + + return Iterator( + iterator: merge( + chain(base.map { Either.element($0) }, [.terminal].async), + signal.map { _ in Either.signal } + ).makeAsyncIterator(), + count: count + ) } } @available(*, unavailable) -extension AsyncChunksOfCountOrSignalSequence.Iterator: Sendable { } +extension AsyncChunksOfCountOrSignalSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift index 0ebafb4b..70e429ff 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift @@ -12,7 +12,10 @@ extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` of a given count. @inlinable - public func chunks(ofCount count: Int, into: Collected.Type) -> AsyncChunksOfCountSequence where Collected.Element == Element { + public func chunks( + ofCount count: Int, + into: Collected.Type + ) -> AsyncChunksOfCountSequence where Collected.Element == Element { AsyncChunksOfCountSequence(self, count: count) } @@ -24,7 +27,8 @@ extension AsyncSequence { } /// An `AsyncSequence` that chunks elements into `RangeReplaceableCollection` instances of at least a given count. -public struct AsyncChunksOfCountSequence: AsyncSequence where Collected.Element == Base.Element { +public struct AsyncChunksOfCountSequence: AsyncSequence +where Collected.Element == Base.Element { public typealias Element = Collected /// The iterator for a `AsyncChunksOfCountSequence` instance. @@ -67,10 +71,10 @@ public struct AsyncChunksOfCountSequence() -> AsyncCompactedSequence - where Element == Unwrapped? { + where Element == Unwrapped? { AsyncCompactedSequence(self) } } @@ -29,11 +29,11 @@ extension AsyncSequence { /// `AsyncSequence`. @frozen public struct AsyncCompactedSequence: AsyncSequence - where Base.Element == Element? { +where Base.Element == Element? { @usableFromInline let base: Base - + @inlinable init(_ base: Base) { self.base = base @@ -44,12 +44,12 @@ public struct AsyncCompactedSequence: AsyncSequenc public struct Iterator: AsyncIteratorProtocol { @usableFromInline var base: Base.AsyncIterator - + @inlinable init(_ base: Base.AsyncIterator) { self.base = base } - + @inlinable public mutating func next() async rethrows -> Element? { while let wrapped = try await base.next() { @@ -66,7 +66,7 @@ public struct AsyncCompactedSequence: AsyncSequenc } } -extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) -extension AsyncCompactedSequence.Iterator: Sendable { } +extension AsyncCompactedSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index cef05359..a6de1f25 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -23,12 +23,15 @@ extension AsyncSequence { /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. @inlinable - public func reductions(_ initial: Result, _ transform: @Sendable @escaping (Result, Element) async -> Result) -> AsyncExclusiveReductionsSequence { + public func reductions( + _ initial: Result, + _ transform: @Sendable @escaping (Result, Element) async -> Result + ) -> AsyncExclusiveReductionsSequence { reductions(into: initial) { result, element in result = await transform(result, element) } } - + /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given closure. /// @@ -43,7 +46,10 @@ extension AsyncSequence { /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. @inlinable - public func reductions(into initial: Result, _ transform: @Sendable @escaping (inout Result, Element) async -> Void) -> AsyncExclusiveReductionsSequence { + public func reductions( + into initial: Result, + _ transform: @Sendable @escaping (inout Result, Element) async -> Void + ) -> AsyncExclusiveReductionsSequence { AsyncExclusiveReductionsSequence(self, initial: initial, transform: transform) } } @@ -54,13 +60,13 @@ extension AsyncSequence { public struct AsyncExclusiveReductionsSequence { @usableFromInline let base: Base - + @usableFromInline let initial: Element - + @usableFromInline let transform: @Sendable (inout Element, Base.Element) async -> Void - + @inlinable init(_ base: Base, initial: Element, transform: @Sendable @escaping (inout Element, Base.Element) async -> Void) { self.base = base @@ -75,43 +81,45 @@ extension AsyncExclusiveReductionsSequence: AsyncSequence { public struct Iterator: AsyncIteratorProtocol { @usableFromInline var iterator: Base.AsyncIterator - + @usableFromInline var current: Element? - + @usableFromInline let transform: @Sendable (inout Element, Base.Element) async -> Void - + @inlinable - init(_ iterator: Base.AsyncIterator, initial: Element, transform: @Sendable @escaping (inout Element, Base.Element) async -> Void) { + init( + _ iterator: Base.AsyncIterator, + initial: Element, + transform: @Sendable @escaping (inout Element, Base.Element) async -> Void + ) { self.iterator = iterator self.current = initial self.transform = transform } - + @inlinable public mutating func next() async rethrows -> Element? { - guard let result = current else { return nil } + guard var result = current else { return nil } let value = try await iterator.next() - if let value = value { - var result = result - await transform(&result, value) - current = result - return result - } else { + guard let value = value else { current = nil return nil } + await transform(&result, value) + current = result + return result } } - + @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeAsyncIterator(), initial: initial, transform: transform) } } -extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } +extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable {} @available(*, unavailable) -extension AsyncExclusiveReductionsSequence.Iterator: Sendable { } +extension AsyncExclusiveReductionsSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index ca907b80..377918e9 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.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 closure. @@ -31,10 +42,10 @@ extension AsyncSequence { public struct AsyncInclusiveReductionsSequence { @usableFromInline let base: Base - + @usableFromInline let transform: @Sendable (Base.Element, Base.Element) async -> Base.Element - + @inlinable init(_ base: Base, transform: @Sendable @escaping (Base.Element, Base.Element) async -> Base.Element) { self.base = base @@ -44,7 +55,7 @@ public struct AsyncInclusiveReductionsSequence { extension AsyncInclusiveReductionsSequence: AsyncSequence { public typealias Element = Base.Element - + /// The iterator for an `AsyncInclusiveReductionsSequence` instance. @frozen public struct Iterator: AsyncIteratorProtocol { @@ -84,7 +95,7 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { } } -extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable { } +extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable {} @available(*, unavailable) -extension AsyncInclusiveReductionsSequence.Iterator: Sendable { } +extension AsyncInclusiveReductionsSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index 515d8a8e..05e78c3f 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -12,13 +12,16 @@ extension AsyncSequence where Element: AsyncSequence { /// Concatenate an `AsyncSequence` of `AsyncSequence` elements with a separator. @inlinable - public func joined(separator: Separator) -> AsyncJoinedBySeparatorSequence { + public func joined( + separator: Separator + ) -> AsyncJoinedBySeparatorSequence { return AsyncJoinedBySeparatorSequence(self, separator: separator) } } /// An `AsyncSequence` that concatenates `AsyncSequence` elements with a separator. -public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base.Element: AsyncSequence, Separator.Element == Base.Element.Element { +public struct AsyncJoinedBySeparatorSequence: AsyncSequence +where Base.Element: AsyncSequence, Separator.Element == Base.Element.Element { public typealias Element = Base.Element.Element public typealias AsyncIterator = Iterator @@ -36,31 +39,31 @@ public struct AsyncJoinedBySeparatorSequence SeparatorState { switch self { - case .initial(let separatorSequence): - return .partialAsync(separatorSequence.makeAsyncIterator(), []) - case .cached(let array): - return .partialCached(array.makeIterator(), array) - default: - fatalError("Invalid separator sequence state") + case .initial(let separatorSequence): + return .partialAsync(separatorSequence.makeAsyncIterator(), []) + case .cached(let array): + return .partialCached(array.makeIterator(), array) + default: + fatalError("Invalid separator sequence state") } } @usableFromInline func next() async rethrows -> (Element?, SeparatorState) { switch self { - case .partialAsync(var separatorIterator, var cache): - guard let next = try await separatorIterator.next() else { - return (nil, .cached(cache)) - } - cache.append(next) - return (next, .partialAsync(separatorIterator, cache)) - case .partialCached(var cacheIterator, let cache): - guard let next = cacheIterator.next() else { - return (nil, .cached(cache)) - } - return (next, .partialCached(cacheIterator, cache)) - default: - fatalError("Invalid separator sequence state") + case .partialAsync(var separatorIterator, var cache): + guard let next = try await separatorIterator.next() else { + return (nil, .cached(cache)) + } + cache.append(next) + return (next, .partialAsync(separatorIterator, cache)) + case .partialCached(var cacheIterator, let cache): + guard let next = cacheIterator.next() else { + return (nil, .cached(cache)) + } + return (next, .partialCached(cacheIterator, cache)) + default: + fatalError("Invalid separator sequence state") } } } @@ -83,37 +86,37 @@ public struct AsyncJoinedBySeparatorSequence Base.Element.Element? { do { switch state { - case .terminal: + case .terminal: + return nil + case .initial(var outerIterator, let separator): + guard let innerSequence = try await outerIterator.next() else { + state = .terminal return nil - case .initial(var outerIterator, let separator): - guard let innerSequence = try await outerIterator.next() else { - state = .terminal - return nil - } - let innerIterator = innerSequence.makeAsyncIterator() - state = .sequence(outerIterator, innerIterator, .initial(separator)) - return try await next() - case .sequence(var outerIterator, var innerIterator, let separatorState): - if let item = try await innerIterator.next() { - state = .sequence(outerIterator, innerIterator, separatorState) - return item - } + } + let innerIterator = innerSequence.makeAsyncIterator() + state = .sequence(outerIterator, innerIterator, .initial(separator)) + return try await next() + case .sequence(var outerIterator, var innerIterator, let separatorState): + if let item = try await innerIterator.next() { + state = .sequence(outerIterator, innerIterator, separatorState) + return item + } - guard let nextInner = try await outerIterator.next() else { - state = .terminal - return nil - } + guard let nextInner = try await outerIterator.next() else { + state = .terminal + return nil + } - state = .separator(outerIterator, separatorState.startSeparator(), nextInner) + state = .separator(outerIterator, separatorState.startSeparator(), nextInner) + return try await next() + case .separator(let iterator, let separatorState, let nextBase): + let (itemOpt, newSepState) = try await separatorState.next() + guard let item = itemOpt else { + state = .sequence(iterator, nextBase.makeAsyncIterator(), newSepState) return try await next() - case .separator(let iterator, let separatorState, let nextBase): - let (itemOpt, newSepState) = try await separatorState.next() - guard let item = itemOpt else { - state = .sequence(iterator, nextBase.makeAsyncIterator(), newSepState) - return try await next() - } - state = .separator(iterator, newSepState, nextBase) - return item + } + state = .separator(iterator, newSepState, nextBase) + return item } } catch { state = .terminal @@ -141,7 +144,7 @@ public struct AsyncJoinedBySeparatorSequence AsyncJoinedSequence { return AsyncJoinedSequence(self) @@ -32,42 +32,42 @@ public struct AsyncJoinedSequence: AsyncSequence where Base case sequence(Base.AsyncIterator, Base.Element.AsyncIterator) case terminal } - + @usableFromInline var state: State - + @inlinable init(_ iterator: Base.AsyncIterator) { state = .initial(iterator) } - + @inlinable public mutating func next() async rethrows -> Base.Element.Element? { do { switch state { - case .terminal: + case .terminal: + return nil + case .initial(var outerIterator): + guard let innerSequence = try await outerIterator.next() else { + state = .terminal return nil - case .initial(var outerIterator): - guard let innerSequence = try await outerIterator.next() else { - state = .terminal - return nil - } - let innerIterator = innerSequence.makeAsyncIterator() + } + let innerIterator = innerSequence.makeAsyncIterator() + state = .sequence(outerIterator, innerIterator) + return try await next() + case .sequence(var outerIterator, var innerIterator): + if let item = try await innerIterator.next() { state = .sequence(outerIterator, innerIterator) - return try await next() - case .sequence(var outerIterator, var innerIterator): - if let item = try await innerIterator.next() { - state = .sequence(outerIterator, innerIterator) - return item - } - - guard let nextInner = try await outerIterator.next() else { - state = .terminal - return nil - } + return item + } + + guard let nextInner = try await outerIterator.next() else { + state = .terminal + return nil + } - state = .sequence(outerIterator, nextInner.makeAsyncIterator()) - return try await next() + state = .sequence(outerIterator, nextInner.makeAsyncIterator()) + return try await next() } } catch { state = .terminal @@ -75,15 +75,15 @@ public struct AsyncJoinedSequence: AsyncSequence where Base } } } - + @usableFromInline let base: Base - + @usableFromInline init(_ base: Base) { self.base = base } - + @inlinable public func makeAsyncIterator() -> Iterator { return Iterator(base.makeAsyncIterator()) @@ -91,7 +91,7 @@ public struct AsyncJoinedSequence: AsyncSequence where Base } extension AsyncJoinedSequence: Sendable -where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable { } +where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable {} @available(*, unavailable) -extension AsyncJoinedSequence.Iterator: Sendable { } +extension AsyncJoinedSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index 0f45e21d..3b63c64d 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -20,12 +20,16 @@ extension AsyncSequence where Element: Equatable { extension AsyncSequence { /// Creates an asynchronous sequence that omits repeated elements by testing them with a predicate. - public func removeDuplicates(by predicate: @escaping @Sendable (Element, Element) async -> Bool) -> AsyncRemoveDuplicatesSequence { + public func removeDuplicates( + by predicate: @escaping @Sendable (Element, Element) async -> Bool + ) -> AsyncRemoveDuplicatesSequence { return AsyncRemoveDuplicatesSequence(self, predicate: predicate) } - + /// Creates an asynchronous sequence that omits repeated elements by testing them with an error-throwing predicate. - public func removeDuplicates(by predicate: @escaping @Sendable (Element, Element) async throws -> Bool) -> AsyncThrowingRemoveDuplicatesSequence { + public func removeDuplicates( + by predicate: @escaping @Sendable (Element, Element) async throws -> Bool + ) -> AsyncThrowingRemoveDuplicatesSequence { return AsyncThrowingRemoveDuplicatesSequence(self, predicate: predicate) } } @@ -73,7 +77,7 @@ public struct AsyncRemoveDuplicatesSequence: AsyncSequence @usableFromInline let predicate: @Sendable (Element, Element) async -> Bool - + init(_ base: Base, predicate: @escaping @Sendable (Element, Element) async -> Bool) { self.base = base self.predicate = predicate @@ -88,7 +92,7 @@ public struct AsyncRemoveDuplicatesSequence: AsyncSequence /// An asynchronous sequence that omits repeated elements by testing them with an error-throwing predicate. public struct AsyncThrowingRemoveDuplicatesSequence: AsyncSequence { public typealias Element = Base.Element - + /// The iterator for an `AsyncThrowingRemoveDuplicatesSequence` instance. public struct Iterator: AsyncIteratorProtocol { @@ -128,7 +132,7 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS @usableFromInline let predicate: @Sendable (Element, Element) async throws -> Bool - + init(_ base: Base, predicate: @escaping @Sendable (Element, Element) async throws -> Bool) { self.base = base self.predicate = predicate @@ -140,12 +144,11 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS } } - -extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } -extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) -extension AsyncRemoveDuplicatesSequence.Iterator: Sendable { } +extension AsyncRemoveDuplicatesSequence.Iterator: Sendable {} @available(*, unavailable) -extension AsyncThrowingRemoveDuplicatesSequence.Iterator: Sendable { } +extension AsyncThrowingRemoveDuplicatesSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift index 70a6637b..49cfac7a 100644 --- a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift @@ -30,43 +30,42 @@ extension Sequence { @frozen public struct AsyncSyncSequence: AsyncSequence { public typealias Element = Base.Element - + @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline var iterator: Base.Iterator? - + @usableFromInline init(_ iterator: Base.Iterator) { self.iterator = iterator } - + @inlinable public mutating func next() async -> Base.Element? { - if !Task.isCancelled, let value = iterator?.next() { - return value - } else { + guard !Task.isCancelled, let value = iterator?.next() else { iterator = nil return nil } + return value } } - + @usableFromInline let base: Base - + @usableFromInline init(_ base: Base) { self.base = base } - + @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeIterator()) } } -extension AsyncSyncSequence: Sendable where Base: Sendable { } +extension AsyncSyncSequence: Sendable where Base: Sendable {} @available(*, unavailable) -extension AsyncSyncSequence.Iterator: Sendable { } +extension AsyncSyncSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index 4dbc1e48..6b5e617d 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -12,32 +12,45 @@ 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 - if latest { - return element - } else { + public func _throttle( + for interval: C.Instant.Duration, + clock: C, + latest: Bool = true + ) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: clock) { previous, element in + guard latest else { return previous ?? element } + return element } } - + /// 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) } } @@ -48,8 +61,13 @@ public struct _AsyncThrottleSequence { let interval: C.Instant.Duration let clock: C let reducing: @Sendable (Reduced?, Base.Element) async -> Reduced - - init(_ base: Base, interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Base.Element) async -> Reduced) { + + init( + _ base: Base, + interval: C.Instant.Duration, + clock: C, + reducing: @Sendable @escaping (Reduced?, Base.Element) async -> Reduced + ) { self.base = base self.interval = interval self.clock = clock @@ -60,7 +78,7 @@ public struct _AsyncThrottleSequence { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension _AsyncThrottleSequence: AsyncSequence { public typealias Element = Reduced - + /// The iterator for an `AsyncThrottleSequence` instance. public struct Iterator: AsyncIteratorProtocol { var base: Base.AsyncIterator @@ -68,14 +86,19 @@ extension _AsyncThrottleSequence: AsyncSequence { let interval: C.Instant.Duration let clock: C let reducing: @Sendable (Reduced?, Base.Element) async -> Reduced - - init(_ base: Base.AsyncIterator, interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Base.Element) async -> Reduced) { + + init( + _ base: Base.AsyncIterator, + interval: C.Instant.Duration, + clock: C, + reducing: @Sendable @escaping (Reduced?, Base.Element) async -> Reduced + ) { self.base = base self.interval = interval self.clock = clock self.reducing = reducing } - + public mutating func next() async rethrows -> Reduced? { var reduced: Reduced? let start = last ?? clock.now @@ -103,14 +126,14 @@ extension _AsyncThrottleSequence: AsyncSequence { } while true } } - + public func makeAsyncIterator() -> Iterator { Iterator(base.makeAsyncIterator(), interval: interval, clock: clock, reducing: reducing) } } @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/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index 1cb49d8b..cb22708b 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -24,12 +24,15 @@ extension AsyncSequence { /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. @inlinable - public func reductions(_ initial: Result, _ transform: @Sendable @escaping (Result, Element) async throws -> Result) -> AsyncThrowingExclusiveReductionsSequence { + public func reductions( + _ initial: Result, + _ transform: @Sendable @escaping (Result, Element) async throws -> Result + ) -> AsyncThrowingExclusiveReductionsSequence { reductions(into: initial) { result, element in result = try await transform(result, element) } } - + /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. /// @@ -45,7 +48,10 @@ extension AsyncSequence { /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. @inlinable - public func reductions(into initial: Result, _ transform: @Sendable @escaping (inout Result, Element) async throws -> Void) -> AsyncThrowingExclusiveReductionsSequence { + public func reductions( + into initial: Result, + _ transform: @Sendable @escaping (inout Result, Element) async throws -> Void + ) -> AsyncThrowingExclusiveReductionsSequence { AsyncThrowingExclusiveReductionsSequence(self, initial: initial, transform: transform) } } @@ -56,15 +62,19 @@ extension AsyncSequence { public struct AsyncThrowingExclusiveReductionsSequence { @usableFromInline let base: Base - + @usableFromInline let initial: Element - + @usableFromInline let transform: @Sendable (inout Element, Base.Element) async throws -> Void - + @inlinable - init(_ base: Base, initial: Element, transform: @Sendable @escaping (inout Element, Base.Element) async throws -> Void) { + init( + _ base: Base, + initial: Element, + transform: @Sendable @escaping (inout Element, Base.Element) async throws -> Void + ) { self.base = base self.initial = initial self.transform = transform @@ -77,48 +87,50 @@ extension AsyncThrowingExclusiveReductionsSequence: AsyncSequence { public struct Iterator: AsyncIteratorProtocol { @usableFromInline var iterator: Base.AsyncIterator - + @usableFromInline var current: Element? - + @usableFromInline let transform: @Sendable (inout Element, Base.Element) async throws -> Void - + @inlinable - init(_ iterator: Base.AsyncIterator, initial: Element, transform: @Sendable @escaping (inout Element, Base.Element) async throws -> Void) { + init( + _ iterator: Base.AsyncIterator, + initial: Element, + transform: @Sendable @escaping (inout Element, Base.Element) async throws -> Void + ) { self.iterator = iterator self.current = initial self.transform = transform } - + @inlinable public mutating func next() async throws -> Element? { - guard let result = current else { return nil } + guard var result = current else { return nil } let value = try await iterator.next() - if let value = value { - var result = result - do { - try await transform(&result, value) - current = result - return result - } catch { - current = nil - throw error - } - } else { + guard let value = value else { current = nil return nil } + do { + try await transform(&result, value) + current = result + return result + } catch { + current = nil + throw error + } } } - + @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeAsyncIterator(), initial: initial, transform: transform) } } -extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } +extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable {} @available(*, unavailable) -extension AsyncThrowingExclusiveReductionsSequence.Iterator: Sendable { } +extension AsyncThrowingExclusiveReductionsSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 7779a842..4ba2d81f 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -41,10 +41,10 @@ extension AsyncSequence { public struct AsyncThrowingInclusiveReductionsSequence { @usableFromInline let base: Base - + @usableFromInline let transform: @Sendable (Base.Element, Base.Element) async throws -> Base.Element - + @inlinable init(_ base: Base, transform: @Sendable @escaping (Base.Element, Base.Element) async throws -> Base.Element) { self.base = base @@ -54,7 +54,7 @@ public struct AsyncThrowingInclusiveReductionsSequence { extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { public typealias Element = Base.Element - + /// The iterator for an `AsyncThrowingInclusiveReductionsSequence` instance. @frozen public struct Iterator: AsyncIteratorProtocol { @@ -99,7 +99,7 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { } } -extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable { } +extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable {} @available(*, unavailable) -extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable { } +extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index dcfc878b..25420da7 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -64,7 +64,11 @@ public struct AsyncTimerSequence: AsyncSequence { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence { /// Create an `AsyncTimerSequence` with a given repeating interval. - public static func repeating(every interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncTimerSequence { + public static func repeating( + every interval: C.Instant.Duration, + tolerance: C.Instant.Duration? = nil, + clock: C + ) -> AsyncTimerSequence { return AsyncTimerSequence(interval: interval, tolerance: tolerance, clock: clock) } } @@ -72,13 +76,16 @@ extension AsyncTimerSequence { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence where C == SuspendingClock { /// Create an `AsyncTimerSequence` with a given repeating interval. - public static func repeating(every interval: Duration, tolerance: Duration? = nil) -> AsyncTimerSequence { + public static func repeating( + every interval: Duration, + tolerance: Duration? = nil + ) -> AsyncTimerSequence { return AsyncTimerSequence(interval: interval, tolerance: tolerance, clock: SuspendingClock()) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncTimerSequence: Sendable { } +extension AsyncTimerSequence: Sendable {} @available(*, unavailable) -extension AsyncTimerSequence.Iterator: Sendable { } +extension AsyncTimerSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index 817615a4..5361a233 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -93,16 +93,16 @@ public struct AsyncBufferSequence: AsyncSequence public func makeAsyncIterator() -> Iterator { let storageType: StorageType switch self.policy.policy { - case .bounded(...0), .bufferingNewest(...0), .bufferingOldest(...0): - storageType = .transparent(self.base.makeAsyncIterator()) - case .bounded(let limit): - storageType = .bounded(storage: BoundedBufferStorage(base: self.base, limit: limit)) - case .unbounded: - storageType = .unbounded(storage: UnboundedBufferStorage(base: self.base, policy: .unlimited)) - case .bufferingNewest(let limit): - storageType = .unbounded(storage: UnboundedBufferStorage(base: self.base, policy: .bufferingNewest(limit))) - case .bufferingOldest(let limit): - storageType = .unbounded(storage: UnboundedBufferStorage(base: self.base, policy: .bufferingOldest(limit))) + case .bounded(...0), .bufferingNewest(...0), .bufferingOldest(...0): + storageType = .transparent(self.base.makeAsyncIterator()) + case .bounded(let limit): + storageType = .bounded(storage: BoundedBufferStorage(base: self.base, limit: limit)) + case .unbounded: + storageType = .unbounded(storage: UnboundedBufferStorage(base: self.base, policy: .unlimited)) + case .bufferingNewest(let limit): + storageType = .unbounded(storage: UnboundedBufferStorage(base: self.base, policy: .bufferingNewest(limit))) + case .bufferingOldest(let limit): + storageType = .unbounded(storage: UnboundedBufferStorage(base: self.base, policy: .bufferingOldest(limit))) } return Iterator(storageType: storageType) } @@ -112,20 +112,20 @@ public struct AsyncBufferSequence: AsyncSequence public mutating func next() async rethrows -> Element? { switch self.storageType { - case .transparent(var iterator): - let element = try await iterator.next() - self.storageType = .transparent(iterator) - return element - case .bounded(let storage): - return try await storage.next()?._rethrowGet() - case .unbounded(let storage): - return try await storage.next()?._rethrowGet() + case .transparent(var iterator): + let element = try await iterator.next() + self.storageType = .transparent(iterator) + return element + case .bounded(let storage): + return try await storage.next()?._rethrowGet() + case .unbounded(let storage): + return try await storage.next()?._rethrowGet() } } } } -extension AsyncBufferSequence: Sendable where Base: Sendable { } +extension AsyncBufferSequence: Sendable where Base: Sendable {} @available(*, unavailable) -extension AsyncBufferSequence.Iterator: Sendable { } +extension AsyncBufferSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index d6008ad9..e6a1f324 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -41,52 +41,52 @@ struct BoundedBufferStateMachine { var task: Task? { switch self.state { - case .buffering(let task, _, _, _): - return task - default: - return nil + case .buffering(let task, _, _, _): + return task + default: + return nil } } mutating func taskStarted(task: Task) { switch self.state { - case .initial: - self.state = .buffering(task: task, buffer: [], suspendedProducer: nil, suspendedConsumer: nil) - - case .buffering: - preconditionFailure("Invalid state.") + case .initial: + self.state = .buffering(task: task, buffer: [], suspendedProducer: nil, suspendedConsumer: nil) - case .modifying: - preconditionFailure("Invalid state.") + case .buffering: + preconditionFailure("Invalid state.") - case .finished: - preconditionFailure("Invalid state.") + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + preconditionFailure("Invalid state.") } } mutating func shouldSuspendProducer() -> Bool { switch state { - case .initial: - preconditionFailure("Invalid state. The task should already be started.") + case .initial: + 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) - // if there are free slots, we should directly request the next element - return buffer.count >= self.limit + case .buffering(_, let buffer, .none, .none): + // we are either idle or the buffer is already in use (no awaiting consumer) + // if there are free slots, we should directly request the next element + return buffer.count >= self.limit - case .buffering(_, _, .none, .some): - // we have an awaiting consumer, we should not suspended the producer, we should - // directly request the next element - return false + case .buffering(_, _, .none, .some): + // we have an awaiting consumer, we should not suspended the producer, we should + // directly request the next element + return false - case .buffering(_, _, .some, _): - preconditionFailure("Invalid state. There is already a suspended producer.") + case .buffering(_, _, .some, _): + preconditionFailure("Invalid state. There is already a suspended producer.") - case .modifying: - preconditionFailure("Invalid state.") + case .modifying: + preconditionFailure("Invalid state.") - case .finished: - return false + case .finished: + return false } } @@ -97,33 +97,40 @@ struct BoundedBufferStateMachine { mutating func producerSuspended(continuation: SuspendedProducer) -> ProducerSuspendedAction { switch self.state { - case .initial: - 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) - // if the buffer is available we resume the producer so it can we can request the next element - // otherwise we confirm the suspension - if buffer.count < limit { - return .resumeProducer - } else { - self.state = .buffering(task: task, buffer: buffer, suspendedProducer: continuation, suspendedConsumer: nil) - return .none - } - - case .buffering(_, let buffer, .none, .some): - // we have an awaiting consumer, we can resume the producer so the next element can be requested - precondition(buffer.isEmpty, "Invalid state. The buffer should be empty as we have an awaiting consumer already.") - return .resumeProducer - - case .buffering(_, _, .some, _): - preconditionFailure("Invalid state. There is already a suspended producer.") - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - return .resumeProducer + case .initial: + 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) + // if the buffer is available we resume the producer so it can we can request the next element + // otherwise we confirm the suspension + guard buffer.count < limit else { + self.state = .buffering( + task: task, + buffer: buffer, + suspendedProducer: continuation, + suspendedConsumer: nil + ) + return .none + } + return .resumeProducer + + case .buffering(_, let buffer, .none, .some): + // we have an awaiting consumer, we can resume the producer so the next element can be requested + precondition( + buffer.isEmpty, + "Invalid state. The buffer should be empty as we have an awaiting consumer already." + ) + return .resumeProducer + + case .buffering(_, _, .some, _): + preconditionFailure("Invalid state. There is already a suspended producer.") + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + return .resumeProducer } } @@ -134,32 +141,35 @@ struct BoundedBufferStateMachine { mutating func elementProduced(element: Element) -> ElementProducedAction { switch self.state { - case .initial: - 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) - // we have to stack the new element or suspend the producer if the buffer is full - precondition(buffer.count < limit, "Invalid state. The buffer should be available for stacking a new element.") - self.state = .modifying - buffer.append(.success(.init(element))) - self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) - return .none - - case .buffering(let task, let buffer, .none, .some(let suspendedConsumer)): - // we have an awaiting consumer, we can resume it with the element and exit - precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") - self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) - return .resumeConsumer(continuation: suspendedConsumer, result: .success(element)) - - case .buffering(_, _, .some, _): - preconditionFailure("Invalid state. There should not be a suspended producer.") - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - return .none + case .initial: + 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) + // we have to stack the new element or suspend the producer if the buffer is full + precondition( + buffer.count < limit, + "Invalid state. The buffer should be available for stacking a new element." + ) + self.state = .modifying + buffer.append(.success(.init(element))) + self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) + return .none + + case .buffering(let task, let buffer, .none, .some(let suspendedConsumer)): + // we have an awaiting consumer, we can resume it with the element and exit + precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") + self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) + return .resumeConsumer(continuation: suspendedConsumer, result: .success(element)) + + case .buffering(_, _, .some, _): + preconditionFailure("Invalid state. There should not be a suspended producer.") + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + return .none } } @@ -172,32 +182,32 @@ struct BoundedBufferStateMachine { mutating func finish(error: Error?) -> FinishAction { switch self.state { - case .initial: - 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) - // if we have an error we stack it in the buffer so it can be consumed later - if let error { - buffer.append(.failure(error)) - } - self.state = .finished(buffer: buffer) - return .none - - case .buffering(_, let buffer, .none, .some(let suspendedConsumer)): - // we have an awaiting consumer, we can resume it - precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") - self.state = .finished(buffer: []) - return .resumeConsumer(continuation: suspendedConsumer) - - case .buffering(_, _, .some, _): - preconditionFailure("Invalid state. There should not be a suspended producer.") - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - return .none + case .initial: + 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) + // if we have an error we stack it in the buffer so it can be consumed later + if let error { + buffer.append(.failure(error)) + } + self.state = .finished(buffer: buffer) + return .none + + case .buffering(_, let buffer, .none, .some(let suspendedConsumer)): + // we have an awaiting consumer, we can resume it + precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") + self.state = .finished(buffer: []) + return .resumeConsumer(continuation: suspendedConsumer) + + case .buffering(_, _, .some, _): + preconditionFailure("Invalid state. There should not be a suspended producer.") + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + return .none } } @@ -209,34 +219,34 @@ struct BoundedBufferStateMachine { mutating func next() -> NextAction { switch state { - case .initial(let base): - return .startTask(base: base) - - case .buffering(_, let buffer, .none, .none) where buffer.isEmpty: - // we are idle, we must suspend the consumer - return .suspend - - case .buffering(let task, var buffer, let suspendedProducer, .none): - // we have values in the buffer, we unstack the oldest one and resume a potential suspended producer - self.state = .modifying - let result = buffer.popFirst()! - self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) - return .returnResult(producerContinuation: suspendedProducer, result: result.map { $0.wrapped }) - - case .buffering(_, _, _, .some): - preconditionFailure("Invalid states. There is already a suspended consumer.") - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished(let buffer) where buffer.isEmpty: - return .returnResult(producerContinuation: nil, result: nil) - - case .finished(var buffer): - self.state = .modifying - let result = buffer.popFirst()! - self.state = .finished(buffer: buffer) - return .returnResult(producerContinuation: nil, result: result.map { $0.wrapped }) + case .initial(let base): + return .startTask(base: base) + + case .buffering(_, let buffer, .none, .none) where buffer.isEmpty: + // we are idle, we must suspend the consumer + return .suspend + + case .buffering(let task, var buffer, let suspendedProducer, .none): + // we have values in the buffer, we unstack the oldest one and resume a potential suspended producer + self.state = .modifying + let result = buffer.popFirst()! + self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) + return .returnResult(producerContinuation: suspendedProducer, result: result.map { $0.wrapped }) + + case .buffering(_, _, _, .some): + preconditionFailure("Invalid states. There is already a suspended consumer.") + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished(let buffer) where buffer.isEmpty: + return .returnResult(producerContinuation: nil, result: nil) + + case .finished(var buffer): + self.state = .modifying + let result = buffer.popFirst()! + self.state = .finished(buffer: buffer) + return .returnResult(producerContinuation: nil, result: result.map { $0.wrapped }) } } @@ -247,35 +257,35 @@ struct BoundedBufferStateMachine { mutating func nextSuspended(continuation: SuspendedConsumer) -> NextSuspendedAction { switch self.state { - case .initial: - 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 - self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: continuation) - return .none - - case .buffering(let task, var buffer, let suspendedProducer, .none): - // we have values in the buffer, we unstack the oldest one and resume a potential suspended producer - self.state = .modifying - let result = buffer.popFirst()! - self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) - return .returnResult(producerContinuation: suspendedProducer, result: result.map { $0.wrapped }) - - case .buffering(_, _, _, .some): - preconditionFailure("Invalid states. There is already a suspended consumer.") - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished(let buffer) where buffer.isEmpty: - return .returnResult(producerContinuation: nil, result: nil) - - case .finished(var buffer): - self.state = .modifying - let result = buffer.popFirst()! - self.state = .finished(buffer: buffer) - return .returnResult(producerContinuation: nil, result: result.map { $0.wrapped }) + case .initial: + 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 + self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: continuation) + return .none + + case .buffering(let task, var buffer, let suspendedProducer, .none): + // we have values in the buffer, we unstack the oldest one and resume a potential suspended producer + self.state = .modifying + let result = buffer.popFirst()! + self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) + return .returnResult(producerContinuation: suspendedProducer, result: result.map { $0.wrapped }) + + case .buffering(_, _, _, .some): + preconditionFailure("Invalid states. There is already a suspended consumer.") + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished(let buffer) where buffer.isEmpty: + return .returnResult(producerContinuation: nil, result: nil) + + case .finished(var buffer): + self.state = .modifying + let result = buffer.popFirst()! + self.state = .finished(buffer: buffer) + return .returnResult(producerContinuation: nil, result: result.map { $0.wrapped }) } } @@ -290,27 +300,27 @@ struct BoundedBufferStateMachine { mutating func interrupted() -> InterruptedAction { switch self.state { - case .initial: - self.state = .finished(buffer: []) - return .none - - case .buffering(let task, _, let suspendedProducer, let suspendedConsumer): - self.state = .finished(buffer: []) - return .resumeProducerAndConsumer( - task: task, - producerContinuation: suspendedProducer, - consumerContinuation: suspendedConsumer - ) - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - self.state = .finished(buffer: []) - return .none + case .initial: + self.state = .finished(buffer: []) + return .none + + case .buffering(let task, _, let suspendedProducer, let suspendedConsumer): + self.state = .finished(buffer: []) + return .resumeProducerAndConsumer( + task: task, + producerContinuation: suspendedProducer, + consumerContinuation: suspendedConsumer + ) + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + self.state = .finished(buffer: []) + return .none } } } -extension BoundedBufferStateMachine: Sendable where Base: Sendable { } -extension BoundedBufferStateMachine.State: Sendable where Base: Sendable { } +extension BoundedBufferStateMachine: Sendable where Base: Sendable {} +extension BoundedBufferStateMachine.State: Sendable where Base: Sendable {} diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index f83a37fa..4ccc1928 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -18,47 +18,49 @@ final class BoundedBufferStorage: Sendable where Base: Send func next() async -> Result? { return await withTaskCancellationHandler { - let action: BoundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine 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 nil - - case .suspend: - return action - case .returnResult: - return action + case .startTask(let base): + self.startTask(stateMachine: &stateMachine, base: base) + return nil + + case .suspend: + return action + case .returnResult: + 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 .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 .suspend: + break - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - return result + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + return result case .none: break } - return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in + return await withUnsafeContinuation { + (continuation: UnsafeContinuation?, Never>) in 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) + case .none: + break + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + continuation.resume(returning: result) } } } onCancel: { @@ -86,10 +88,10 @@ final class BoundedBufferStorage: Sendable where Base: Send } switch action { - case .none: - break - case .resumeProducer: - continuation.resume() + case .none: + break + case .resumeProducer: + continuation.resume() } } } @@ -103,10 +105,10 @@ final class BoundedBufferStorage: Sendable where Base: Send stateMachine.elementProduced(element: element) } switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) + case .none: + break + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } @@ -114,20 +116,20 @@ final class BoundedBufferStorage: Sendable where Base: Send stateMachine.finish(error: nil) } switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) } } catch { 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)) + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -140,12 +142,12 @@ final class BoundedBufferStorage: Sendable where Base: Send stateMachine.interrupted() } switch action { - case .none: - break - case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): - task.cancel() - producerContinuation?.resume() - consumerContinuation?.resume(returning: nil) + 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/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index be19b58b..2ba5b45b 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -45,26 +45,26 @@ struct UnboundedBufferStateMachine { var task: Task? { switch self.state { - case .buffering(let task, _, _): - return task - default: - return nil + case .buffering(let task, _, _): + return task + default: + return nil } } mutating func taskStarted(task: Task) { switch self.state { - case .initial: - self.state = .buffering(task: task, buffer: [], suspendedConsumer: nil) + case .initial: + self.state = .buffering(task: task, buffer: [], suspendedConsumer: nil) - case .buffering: - preconditionFailure("Invalid state.") + case .buffering: + preconditionFailure("Invalid state.") - case .modifying: - preconditionFailure("Invalid state.") + case .modifying: + preconditionFailure("Invalid state.") - case .finished: - preconditionFailure("Invalid state.") + case .finished: + preconditionFailure("Invalid state.") } } @@ -78,43 +78,43 @@ struct UnboundedBufferStateMachine { mutating func elementProduced(element: Element) -> ElementProducedAction { switch self.state { - case .initial: - preconditionFailure("Invalid state. The task should already by started.") - - case .buffering(let task, var buffer, .none): - // we are either idle or the buffer is already in use (no awaiting consumer) - // we have to apply the policy when stacking the new element - self.state = .modifying - switch self.policy { - case .unlimited: - buffer.append(.success(.init(element))) - case .bufferingNewest(let limit): - if buffer.count >= limit { - _ = buffer.popFirst() - } - buffer.append(.success(.init(element))) - case .bufferingOldest(let limit): - if buffer.count < limit { - buffer.append(.success(.init(element))) - } + case .initial: + preconditionFailure("Invalid state. The task should already by started.") + + case .buffering(let task, var buffer, .none): + // we are either idle or the buffer is already in use (no awaiting consumer) + // we have to apply the policy when stacking the new element + self.state = .modifying + switch self.policy { + case .unlimited: + buffer.append(.success(.init(element))) + case .bufferingNewest(let limit): + if buffer.count >= limit { + _ = buffer.popFirst() } - self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) - return .none - - case .buffering(let task, let buffer, .some(let suspendedConsumer)): - // we have an awaiting consumer, we can resume it with the element - precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") - self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) - return .resumeConsumer( - continuation: suspendedConsumer, - result: .success(element) - ) - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - return .none + buffer.append(.success(.init(element))) + case .bufferingOldest(let limit): + if buffer.count < limit { + buffer.append(.success(.init(element))) + } + } + self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) + return .none + + case .buffering(let task, let buffer, .some(let suspendedConsumer)): + // we have an awaiting consumer, we can resume it with the element + precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") + self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) + return .resumeConsumer( + continuation: suspendedConsumer, + result: .success(element) + ) + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + return .none } } @@ -125,29 +125,29 @@ struct UnboundedBufferStateMachine { mutating func finish(error: Error?) -> FinishAction { switch self.state { - case .initial: - preconditionFailure("Invalid state. The task should already by started.") - - case .buffering(_, var buffer, .none): - // we are either idle or the buffer is already in use (no awaiting consumer) - // if we have an error we stack it in the buffer so it can be consumed later - if let error { - buffer.append(.failure(error)) - } - self.state = .finished(buffer: buffer) - return .none - - case .buffering(_, let buffer, let suspendedConsumer): - // we have an awaiting consumer, we can resume it with nil or the error - precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") - self.state = .finished(buffer: []) - return .resumeConsumer(continuation: suspendedConsumer) - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - return .none + case .initial: + preconditionFailure("Invalid state. The task should already by started.") + + case .buffering(_, var buffer, .none): + // we are either idle or the buffer is already in use (no awaiting consumer) + // if we have an error we stack it in the buffer so it can be consumed later + if let error { + buffer.append(.failure(error)) + } + self.state = .finished(buffer: buffer) + return .none + + case .buffering(_, let buffer, let suspendedConsumer): + // we have an awaiting consumer, we can resume it with nil or the error + precondition(buffer.isEmpty, "Invalid state. The buffer should be empty.") + self.state = .finished(buffer: []) + return .resumeConsumer(continuation: suspendedConsumer) + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + return .none } } @@ -159,33 +159,33 @@ struct UnboundedBufferStateMachine { mutating func next() -> NextAction { switch self.state { - case .initial(let base): - return .startTask(base: base) - - case .buffering(_, let buffer, let suspendedConsumer) where buffer.isEmpty: - // we are idle, we have to suspend the consumer - precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") - return .suspend - - case .buffering(let task, var buffer, let suspendedConsumer): - // the buffer is already in use, we can unstack a value and directly resume the consumer - precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") - self.state = .modifying - let result = buffer.popFirst()! - self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) - return .returnResult(result.map { $0.wrapped }) - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished(let buffer) where buffer.isEmpty: - return .returnResult(nil) - - case .finished(var buffer): - self.state = .modifying - let result = buffer.popFirst()! - self.state = .finished(buffer: buffer) - return .returnResult(result.map { $0.wrapped }) + case .initial(let base): + return .startTask(base: base) + + case .buffering(_, let buffer, let suspendedConsumer) where buffer.isEmpty: + // we are idle, we have to suspend the consumer + precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") + return .suspend + + case .buffering(let task, var buffer, let suspendedConsumer): + // the buffer is already in use, we can unstack a value and directly resume the consumer + precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") + self.state = .modifying + let result = buffer.popFirst()! + self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) + return .returnResult(result.map { $0.wrapped }) + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished(let buffer) where buffer.isEmpty: + return .returnResult(nil) + + case .finished(var buffer): + self.state = .modifying + let result = buffer.popFirst()! + self.state = .finished(buffer: buffer) + return .returnResult(result.map { $0.wrapped }) } } @@ -196,34 +196,34 @@ struct UnboundedBufferStateMachine { mutating func nextSuspended(continuation: SuspendedConsumer) -> NextSuspendedAction { switch self.state { - case .initial: - preconditionFailure("Invalid state. The task should already by started.") - - case .buffering(let task, let buffer, let suspendedConsumer) where buffer.isEmpty: - // we are idle, we confirm the suspension of the consumer - precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") - self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: continuation) - return .none - - case .buffering(let task, var buffer, let suspendedConsumer): - // the buffer is already in use, we can unstack a value and directly resume the consumer - precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") - self.state = .modifying - let result = buffer.popFirst()! - self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) - return .resumeConsumer(result.map { $0.wrapped }) - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished(let buffer) where buffer.isEmpty: - return .resumeConsumer(nil) - - case .finished(var buffer): - self.state = .modifying - let result = buffer.popFirst()! - self.state = .finished(buffer: buffer) - return .resumeConsumer(result.map { $0.wrapped }) + case .initial: + preconditionFailure("Invalid state. The task should already by started.") + + case .buffering(let task, let buffer, let suspendedConsumer) where buffer.isEmpty: + // we are idle, we confirm the suspension of the consumer + precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") + self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: continuation) + return .none + + case .buffering(let task, var buffer, let suspendedConsumer): + // the buffer is already in use, we can unstack a value and directly resume the consumer + precondition(suspendedConsumer == nil, "Invalid states. There is already a suspended consumer.") + self.state = .modifying + let result = buffer.popFirst()! + self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) + return .resumeConsumer(result.map { $0.wrapped }) + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished(let buffer) where buffer.isEmpty: + return .resumeConsumer(nil) + + case .finished(var buffer): + self.state = .modifying + let result = buffer.popFirst()! + self.state = .finished(buffer: buffer) + return .resumeConsumer(result.map { $0.wrapped }) } } @@ -234,25 +234,23 @@ struct UnboundedBufferStateMachine { mutating func interrupted() -> InterruptedAction { switch self.state { - case .initial: - state = .finished(buffer: []) - return .none - - case .buffering(let task, _, let suspendedConsumer): - self.state = .finished(buffer: []) - return .resumeConsumer(task: task, continuation: suspendedConsumer) - - case .modifying: - preconditionFailure("Invalid state.") - - case .finished: - self.state = .finished(buffer: []) - return .none + case .initial: + state = .finished(buffer: []) + return .none + + case .buffering(let task, _, let suspendedConsumer): + self.state = .finished(buffer: []) + return .resumeConsumer(task: task, continuation: suspendedConsumer) + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + self.state = .finished(buffer: []) + return .none } } } -extension UnboundedBufferStateMachine: Sendable where Base: Sendable { } -extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable { } - - +extension UnboundedBufferStateMachine: Sendable where Base: Sendable {} +extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable {} diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift index b63b261f..b8a6ac24 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -19,41 +19,43 @@ final class UnboundedBufferStorage: Sendable where Base: Se func next() async -> Result? { return await withTaskCancellationHandler { - let action: UnboundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine 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 nil - case .suspend: - return action - case .returnResult: - return action + case .startTask(let base): + self.startTask(stateMachine: &stateMachine, base: base) + return nil + case .suspend: + return action + case .returnResult: + 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 .suspend: - break - case .returnResult(let result): - return result - case .none: - break + 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 + return await withUnsafeContinuation { + (continuation: UnsafeContinuation?, Never>) in let action = self.stateMachine.withCriticalRegion { stateMachine in stateMachine.nextSuspended(continuation: continuation) } switch action { - case .none: - break - case .resumeConsumer(let result): - continuation.resume(returning: result) + case .none: + break + case .resumeConsumer(let result): + continuation.resume(returning: result) } } } onCancel: { @@ -72,10 +74,10 @@ final class UnboundedBufferStorage: Sendable where Base: Se stateMachine.elementProduced(element: element) } switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) + case .none: + break + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } @@ -83,20 +85,20 @@ final class UnboundedBufferStorage: Sendable where Base: Se stateMachine.finish(error: nil) } switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) } } catch { 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)) + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -109,11 +111,11 @@ final class UnboundedBufferStorage: Sendable where Base: Se stateMachine.interrupted() } switch action { - case .none: - break - case .resumeConsumer(let task, let continuation): - task.cancel() - continuation?.resume(returning: nil) + case .none: + break + case .resumeConsumer(let task, let continuation): + task.cancel() + continuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index 026281de..a7c5d384 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -60,4 +60,4 @@ public final class AsyncChannel: AsyncSequence, Sendable { } @available(*, unavailable) -extension AsyncChannel.Iterator: Sendable { } +extension AsyncChannel.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index 63cbf50d..e84a94c5 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -63,4 +63,4 @@ public final class AsyncThrowingChannel: Asyn } @available(*, unavailable) -extension AsyncThrowingChannel.Iterator: Sendable { } +extension AsyncThrowingChannel.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index 920f6056..dad46297 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -61,7 +61,12 @@ struct ChannelStateMachine: Sendable { case terminated(Termination) } - private var state: State = .channeling(suspendedProducers: [], cancelledProducers: [], suspendedConsumers: [], cancelledConsumers: []) + private var state: State = .channeling( + suspendedProducers: [], + cancelledProducers: [], + suspendedConsumers: [], + cancelledConsumers: [] + ) enum SendAction { case resumeConsumer(continuation: UnsafeContinuation?) @@ -70,23 +75,23 @@ struct ChannelStateMachine: Sendable { mutating func send() -> SendAction { switch self.state { - case .channeling(_, _, let suspendedConsumers, _) where suspendedConsumers.isEmpty: - // we are idle or waiting for consumers, we have to suspend the producer - return .suspend - - case .channeling(let suspendedProducers, let cancelledProducers, var suspendedConsumers, let cancelledConsumers): - // we are waiting for producers, we can resume the first available consumer - let suspendedConsumer = suspendedConsumers.removeFirst() - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeConsumer(continuation: suspendedConsumer.continuation) - - case .terminated: - return .resumeConsumer(continuation: nil) + case .channeling(_, _, let suspendedConsumers, _) where suspendedConsumers.isEmpty: + // we are idle or waiting for consumers, we have to suspend the producer + return .suspend + + case .channeling(let suspendedProducers, let cancelledProducers, var suspendedConsumers, let cancelledConsumers): + // we are waiting for producers, we can resume the first available consumer + let suspendedConsumer = suspendedConsumers.removeFirst() + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .resumeConsumer(continuation: suspendedConsumer.continuation) + + case .terminated: + return .resumeConsumer(continuation: nil) } } @@ -101,45 +106,44 @@ struct ChannelStateMachine: Sendable { producerID: UInt64 ) -> SendSuspendedAction? { switch self.state { - case .channeling(var suspendedProducers, var cancelledProducers, var suspendedConsumers, let cancelledConsumers): - let suspendedProducer = SuspendedProducer(id: producerID, continuation: continuation, element: element) - if let _ = cancelledProducers.remove(suspendedProducer) { - // the producer was already cancelled, we resume it - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeProducer - } - - if suspendedConsumers.isEmpty { - // we are idle or waiting for consumers - // we stack the incoming producer in a suspended state - suspendedProducers.append(suspendedProducer) - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .none - } else { - // we are waiting for producers - // we resume the first consumer - let suspendedConsumer = suspendedConsumers.removeFirst() - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeProducerAndConsumer(continuation: suspendedConsumer.continuation) - } - - case .terminated: + case .channeling(var suspendedProducers, var cancelledProducers, var suspendedConsumers, let cancelledConsumers): + let suspendedProducer = SuspendedProducer(id: producerID, continuation: continuation, element: element) + if let _ = cancelledProducers.remove(suspendedProducer) { + // the producer was already cancelled, we resume it + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) return .resumeProducer + } + + guard suspendedConsumers.isEmpty else { + // we are waiting for producers + // we resume the first consumer + let suspendedConsumer = suspendedConsumers.removeFirst() + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .resumeProducerAndConsumer(continuation: suspendedConsumer.continuation) + } + // we are idle or waiting for consumers + // we stack the incoming producer in a suspended state + suspendedProducers.append(suspendedProducer) + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .none + + case .terminated: + return .resumeProducer } } @@ -150,33 +154,33 @@ struct ChannelStateMachine: Sendable { mutating func sendCancelled(producerID: UInt64) -> SendCancelledAction { switch self.state { - case .channeling(var suspendedProducers, var cancelledProducers, let suspendedConsumers, let cancelledConsumers): - // the cancelled producer might be part of the waiting list - let placeHolder = SuspendedProducer.placeHolder(id: producerID) - - if let removed = suspendedProducers.remove(placeHolder) { - // the producer was cancelled after being added to the suspended ones, we resume it - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeProducer(continuation: removed.continuation) - } + case .channeling(var suspendedProducers, var cancelledProducers, let suspendedConsumers, let cancelledConsumers): + // the cancelled producer might be part of the waiting list + let placeHolder = SuspendedProducer.placeHolder(id: producerID) - // the producer was cancelled before being added to the suspended ones - cancelledProducers.update(with: placeHolder) + if let removed = suspendedProducers.remove(placeHolder) { + // the producer was cancelled after being added to the suspended ones, we resume it self.state = .channeling( suspendedProducers: suspendedProducers, cancelledProducers: cancelledProducers, suspendedConsumers: suspendedConsumers, cancelledConsumers: cancelledConsumers ) - return .none - - case .terminated: - return .none + return .resumeProducer(continuation: removed.continuation) + } + + // the producer was cancelled before being added to the suspended ones + cancelledProducers.update(with: placeHolder) + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .none + + case .terminated: + return .none } } @@ -190,24 +194,24 @@ struct ChannelStateMachine: Sendable { mutating func finish(error: Failure?) -> FinishAction { switch self.state { - case .channeling(let suspendedProducers, _, let suspendedConsumers, _): - // no matter if we are idle, waiting for producers or waiting for consumers, we resume every thing that is suspended - if let error { - if suspendedConsumers.isEmpty { - self.state = .terminated(.failed(error)) - } else { - self.state = .terminated(.finished) - } + case .channeling(let suspendedProducers, _, let suspendedConsumers, _): + // no matter if we are idle, waiting for producers or waiting for consumers, we resume every thing that is suspended + if let error { + if suspendedConsumers.isEmpty { + self.state = .terminated(.failed(error)) } else { self.state = .terminated(.finished) } - return .resumeProducersAndConsumers( - producerSontinuations: suspendedProducers.map { $0.continuation }, - consumerContinuations: suspendedConsumers.map { $0.continuation } - ) - - case .terminated: - return .none + } else { + self.state = .terminated(.finished) + } + return .resumeProducersAndConsumers( + producerSontinuations: suspendedProducers.map { $0.continuation }, + consumerContinuations: suspendedConsumers.map { $0.continuation } + ) + + case .terminated: + return .none } } @@ -218,30 +222,30 @@ struct ChannelStateMachine: Sendable { mutating func next() -> NextAction { switch self.state { - case .channeling(let suspendedProducers, _, _, _) where suspendedProducers.isEmpty: - // we are idle or waiting for producers, we must suspend - return .suspend - - case .channeling(var suspendedProducers, let cancelledProducers, let suspendedConsumers, let cancelledConsumers): - // we are waiting for consumers, we can resume the first awaiting producer - let suspendedProducer = suspendedProducers.removeFirst() - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeProducer( - continuation: suspendedProducer.continuation, - result: .success(suspendedProducer.element) - ) - - case .terminated(.failed(let error)): - self.state = .terminated(.finished) - return .resumeProducer(continuation: nil, result: .failure(error)) - - case .terminated: - return .resumeProducer(continuation: nil, result: .success(nil)) + case .channeling(let suspendedProducers, _, _, _) where suspendedProducers.isEmpty: + // we are idle or waiting for producers, we must suspend + return .suspend + + case .channeling(var suspendedProducers, let cancelledProducers, let suspendedConsumers, let cancelledConsumers): + // we are waiting for consumers, we can resume the first awaiting producer + let suspendedProducer = suspendedProducers.removeFirst() + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .resumeProducer( + continuation: suspendedProducer.continuation, + result: .success(suspendedProducer.element) + ) + + case .terminated(.failed(let error)): + self.state = .terminated(.finished) + return .resumeProducer(continuation: nil, result: .failure(error)) + + case .terminated: + return .resumeProducer(continuation: nil, result: .success(nil)) } } @@ -256,52 +260,51 @@ struct ChannelStateMachine: Sendable { consumerID: UInt64 ) -> NextSuspendedAction? { switch self.state { - case .channeling(var suspendedProducers, let cancelledProducers, var suspendedConsumers, var cancelledConsumers): - let suspendedConsumer = SuspendedConsumer(id: consumerID, continuation: continuation) - if let _ = cancelledConsumers.remove(suspendedConsumer) { - // the consumer was already cancelled, we resume it - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeConsumer(element: nil) - } - - if suspendedProducers.isEmpty { - // we are idle or waiting for producers - // we stack the incoming consumer in a suspended state - suspendedConsumers.append(suspendedConsumer) - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .none - } else { - // we are waiting for consumers - // we resume the first producer - let suspendedProducer = suspendedProducers.removeFirst() - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeProducerAndConsumer( - continuation: suspendedProducer.continuation, - element: suspendedProducer.element - ) - } - - case .terminated(.finished): + case .channeling(var suspendedProducers, let cancelledProducers, var suspendedConsumers, var cancelledConsumers): + let suspendedConsumer = SuspendedConsumer(id: consumerID, continuation: continuation) + if let _ = cancelledConsumers.remove(suspendedConsumer) { + // the consumer was already cancelled, we resume it + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) return .resumeConsumer(element: nil) + } - case .terminated(.failed(let error)): - self.state = .terminated(.finished) - return .resumeConsumerWithError(error: error) + guard suspendedProducers.isEmpty else { + // we are waiting for consumers + // we resume the first producer + let suspendedProducer = suspendedProducers.removeFirst() + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .resumeProducerAndConsumer( + continuation: suspendedProducer.continuation, + element: suspendedProducer.element + ) + } + // we are idle or waiting for producers + // we stack the incoming consumer in a suspended state + suspendedConsumers.append(suspendedConsumer) + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .none + + case .terminated(.finished): + return .resumeConsumer(element: nil) + + case .terminated(.failed(let error)): + self.state = .terminated(.finished) + return .resumeConsumerWithError(error: error) } } @@ -312,33 +315,33 @@ struct ChannelStateMachine: Sendable { mutating func nextCancelled(consumerID: UInt64) -> NextCancelledAction { switch self.state { - case .channeling(let suspendedProducers, let cancelledProducers, var suspendedConsumers, var cancelledConsumers): - // the cancelled consumer might be part of the suspended ones - let placeHolder = SuspendedConsumer.placeHolder(id: consumerID) - - if let removed = suspendedConsumers.remove(placeHolder) { - // the consumer was cancelled after being added to the suspended ones, we resume it - self.state = .channeling( - suspendedProducers: suspendedProducers, - cancelledProducers: cancelledProducers, - suspendedConsumers: suspendedConsumers, - cancelledConsumers: cancelledConsumers - ) - return .resumeConsumer(continuation: removed.continuation) - } + case .channeling(let suspendedProducers, let cancelledProducers, var suspendedConsumers, var cancelledConsumers): + // the cancelled consumer might be part of the suspended ones + let placeHolder = SuspendedConsumer.placeHolder(id: consumerID) - // the consumer was cancelled before being added to the suspended ones - cancelledConsumers.update(with: placeHolder) + if let removed = suspendedConsumers.remove(placeHolder) { + // the consumer was cancelled after being added to the suspended ones, we resume it self.state = .channeling( suspendedProducers: suspendedProducers, cancelledProducers: cancelledProducers, suspendedConsumers: suspendedConsumers, cancelledConsumers: cancelledConsumers ) - return .none - - case .terminated: - return .none + return .resumeConsumer(continuation: removed.continuation) + } + + // the consumer was cancelled before being added to the suspended ones + cancelledConsumers.update(with: placeHolder) + self.state = .channeling( + suspendedProducers: suspendedProducers, + cancelledProducers: cancelledProducers, + suspendedConsumers: suspendedConsumers, + cancelledConsumers: cancelledConsumers + ) + return .none + + case .terminated: + return .none } } } diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index 0fb67818..585d9c5f 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -30,12 +30,12 @@ struct ChannelStorage: Sendable { } switch action { - case .suspend: + case .suspend: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: element) - return + case .resumeConsumer(let continuation): + continuation?.resume(returning: element) + return } let producerID = self.generateId() @@ -48,13 +48,13 @@ struct ChannelStorage: Sendable { } switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - case .resumeProducerAndConsumer(let consumerContinuation): - continuation.resume() - consumerContinuation?.resume(returning: element) + case .none: + break + case .resumeProducer: + continuation.resume() + case .resumeProducerAndConsumer(let consumerContinuation): + continuation.resume() + consumerContinuation?.resume(returning: element) } } } onCancel: { @@ -63,10 +63,10 @@ struct ChannelStorage: Sendable { } switch action { - case .none: - break - case .resumeProducer(let continuation): - continuation?.resume() + case .none: + break + case .resumeProducer(let continuation): + continuation?.resume() } } } @@ -77,15 +77,15 @@ struct ChannelStorage: Sendable { } 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 .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) } + } } } @@ -95,12 +95,12 @@ struct ChannelStorage: Sendable { } switch action { - case .suspend: - break + case .suspend: + break - case .resumeProducer(let producerContinuation, let result): - producerContinuation?.resume() - return try result._rethrowGet() + case .resumeProducer(let producerContinuation, let result): + producerContinuation?.resume() + return try result._rethrowGet() } let consumerID = self.generateId() @@ -115,15 +115,15 @@ struct ChannelStorage: Sendable { } 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) + 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: { @@ -132,10 +132,10 @@ struct ChannelStorage: Sendable { } switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) } } } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index fa68acf7..fab5772e 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -23,11 +23,13 @@ public func combineLatest< Base1: AsyncSequence, Base2: AsyncSequence ->(_ base1: Base1, _ base2: Base2) -> AsyncCombineLatest2Sequence where +>(_ base1: Base1, _ base2: Base2) -> AsyncCombineLatest2Sequence +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, - Base2.Element: Sendable { + Base2.Element: Sendable +{ AsyncCombineLatest2Sequence(base1, base2) } @@ -35,11 +37,13 @@ public func combineLatest< public struct AsyncCombineLatest2Sequence< Base1: AsyncSequence, Base2: AsyncSequence ->: AsyncSequence, Sendable where +>: AsyncSequence, Sendable +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, - Base2.Element: Sendable { + Base2.Element: Sendable +{ public typealias Element = (Base1.Element, Base2.Element) public typealias AsyncIterator = Iterator @@ -89,4 +93,4 @@ public struct AsyncCombineLatest2Sequence< } @available(*, unavailable) -extension AsyncCombineLatest2Sequence.Iterator: Sendable { } +extension AsyncCombineLatest2Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift index 4353c0b0..3152827f 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -24,13 +24,15 @@ public func combineLatest< Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence ->(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncCombineLatest3Sequence where +>(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncCombineLatest3Sequence +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, - Base3.Element: Sendable { + Base3.Element: Sendable +{ AsyncCombineLatest3Sequence(base1, base2, base3) } @@ -39,13 +41,15 @@ public struct AsyncCombineLatest3Sequence< Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence ->: AsyncSequence, Sendable where +>: AsyncSequence, Sendable +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, - Base3.Element: Sendable { + Base3.Element: Sendable +{ public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public typealias AsyncIterator = Iterator @@ -60,7 +64,8 @@ public struct AsyncCombineLatest3Sequence< } public func makeAsyncIterator() -> AsyncIterator { - Iterator(storage: .init(self.base1, self.base2, self.base3) + Iterator( + storage: .init(self.base1, self.base2, self.base3) ) } @@ -99,4 +104,4 @@ public struct AsyncCombineLatest3Sequence< } @available(*, unavailable) -extension AsyncCombineLatest3Sequence.Iterator: Sendable { } +extension AsyncCombineLatest3Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 5217e8de..aae12b87 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -16,18 +16,24 @@ struct CombineLatestStateMachine< Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence ->: Sendable where +>: Sendable +where Base1: Sendable, Base2: Sendable, Base3: Sendable, Base1.Element: Sendable, Base2.Element: Sendable, - Base3.Element: Sendable { - typealias DownstreamContinuation = UnsafeContinuation, Never> + Base3.Element: Sendable +{ + typealias DownstreamContinuation = UnsafeContinuation< + Result< + ( + Base1.Element, + Base2.Element, + Base3.Element? + )?, Error + >, Never + > private enum State: Sendable { /// Small wrapper for the state of an upstream sequence. @@ -115,7 +121,9 @@ struct CombineLatestStateMachine< case .combining: // An iterator was deinitialized while we have a suspended continuation. - preconditionFailure("Internal inconsistency current state \(self.state) and received iteratorDeinitialized()") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received iteratorDeinitialized()" + ) case .waitingForDemand(let task, let upstreams, _): // The iterator was dropped which signals that the consumer is finished. @@ -124,7 +132,8 @@ struct CombineLatestStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .upstreamThrew, .upstreamsFinished: @@ -180,7 +189,10 @@ struct CombineLatestStateMachine< ) } - mutating func childTaskSuspended(baseIndex: Int, continuation: UnsafeContinuation) -> ChildTaskSuspendedAction? { + mutating func childTaskSuspended( + baseIndex: Int, + continuation: UnsafeContinuation + ) -> ChildTaskSuspendedAction? { switch self.state { case .initial: // Child tasks are only created after we transitioned to `zipping` @@ -203,7 +215,9 @@ struct CombineLatestStateMachine< upstreams.2.continuation = continuation default: - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)" + ) } self.state = .waitingForDemand( @@ -283,7 +297,10 @@ struct CombineLatestStateMachine< return .none case .combining(let task, var upstreams, let downstreamContinuation, let buffer): - precondition(buffer.isEmpty, "Internal inconsistency current state \(self.state) and the buffer is not empty") + precondition( + buffer.isEmpty, + "Internal inconsistency current state \(self.state) and the buffer is not empty" + ) self.state = .modifying switch result { @@ -302,8 +319,9 @@ struct CombineLatestStateMachine< // Implementing this for the two arities without variadic generics is a bit awkward sadly. if let first = upstreams.0.element, - let second = upstreams.1.element, - let third = upstreams.2.element { + let second = upstreams.1.element, + let third = upstreams.2.element + { // We got an element from each upstream so we can resume the downstream now self.state = .waitingForDemand( task: task, @@ -317,8 +335,9 @@ struct CombineLatestStateMachine< ) } else if let first = upstreams.0.element, - let second = upstreams.1.element, - self.numberOfUpstreamSequences == 2 { + let second = upstreams.1.element, + self.numberOfUpstreamSequences == 2 + { // We got an element from each upstream so we can resume the downstream now self.state = .waitingForDemand( task: task, @@ -335,9 +354,21 @@ struct CombineLatestStateMachine< self.state = .combining( task: task, upstreams: ( - .init(continuation: upstreams.0.continuation, element: upstreams.0.element, isFinished: upstreams.0.isFinished), - .init(continuation: upstreams.1.continuation, element: upstreams.1.element, isFinished: upstreams.1.isFinished), - .init(continuation: upstreams.2.continuation, element: upstreams.2.element, isFinished: upstreams.2.isFinished) + .init( + continuation: upstreams.0.continuation, + element: upstreams.0.element, + isFinished: upstreams.0.isFinished + ), + .init( + continuation: upstreams.1.continuation, + element: upstreams.1.element, + isFinished: upstreams.1.isFinished + ), + .init( + continuation: upstreams.2.continuation, + element: upstreams.2.element, + isFinished: upstreams.2.isFinished + ) ), downstreamContinuation: downstreamContinuation, buffer: buffer @@ -397,7 +428,9 @@ struct CombineLatestStateMachine< upstreams.2.isFinished = true default: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished() with base index \(baseIndex)") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received upstreamFinished() with base index \(baseIndex)" + ) } if upstreams.0.isFinished && upstreams.1.isFinished && upstreams.2.isFinished { @@ -410,7 +443,9 @@ struct CombineLatestStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [ + upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation, + ].compactMap { $0 } ) } else if upstreams.0.isFinished && upstreams.1.isFinished && self.numberOfUpstreamSequences == 2 { // All upstreams finished we can transition to either finished or upstreamsFinished now @@ -422,7 +457,9 @@ struct CombineLatestStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [ + upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation, + ].compactMap { $0 } ) } else { self.state = .waitingForDemand( @@ -455,7 +492,9 @@ struct CombineLatestStateMachine< emptyUpstreamFinished = upstreams.2.element == nil default: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished() with base index \(baseIndex)") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received upstreamFinished() with base index \(baseIndex)" + ) } // Implementing this for the two arities without variadic generics is a bit awkward sadly. @@ -466,7 +505,9 @@ struct CombineLatestStateMachine< return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation: downstreamContinuation, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [ + upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation, + ].compactMap { $0 } ) } else if upstreams.0.isFinished && upstreams.1.isFinished && upstreams.2.isFinished { @@ -476,7 +517,9 @@ struct CombineLatestStateMachine< return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation: downstreamContinuation, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [ + upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation, + ].compactMap { $0 } ) } else if upstreams.0.isFinished && upstreams.1.isFinished && self.numberOfUpstreamSequences == 2 { @@ -486,7 +529,9 @@ struct CombineLatestStateMachine< return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation: downstreamContinuation, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [ + upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation, + ].compactMap { $0 } ) } else { self.state = .combining( @@ -542,7 +587,8 @@ struct CombineLatestStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .combining(let task, let upstreams, let downstreamContinuation, _): @@ -555,7 +601,8 @@ struct CombineLatestStateMachine< downstreamContinuation: downstreamContinuation, error: error, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .upstreamThrew, .finished: @@ -597,7 +644,8 @@ struct CombineLatestStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .combining(let task, let upstreams, let downstreamContinuation, _): @@ -608,7 +656,8 @@ struct CombineLatestStateMachine< return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation: downstreamContinuation, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .upstreamsFinished: @@ -661,19 +710,10 @@ struct CombineLatestStateMachine< // If not we have to transition to combining and need to resume all upstream continuations now self.state = .modifying - if let element = buffer.popFirst() { - self.state = .waitingForDemand( - task: task, - upstreams: upstreams, - buffer: buffer - ) - - return .resumeContinuation( - downstreamContinuation: continuation, - result: .success(element) - ) - } else { - let upstreamContinuations = [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + guard let element = buffer.popFirst() else { + let upstreamContinuations = [ + upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation, + ].compactMap { $0 } upstreams.0.continuation = nil upstreams.1.continuation = nil upstreams.2.continuation = nil @@ -689,22 +729,31 @@ struct CombineLatestStateMachine< upstreamContinuation: upstreamContinuations ) } + self.state = .waitingForDemand( + task: task, + upstreams: upstreams, + buffer: buffer + ) + + return .resumeContinuation( + downstreamContinuation: continuation, + result: .success(element) + ) case .upstreamsFinished(var buffer): self.state = .modifying - if let element = buffer.popFirst() { - self.state = .upstreamsFinished(buffer: buffer) - - return .resumeContinuation( - downstreamContinuation: continuation, - result: .success(element) - ) - } else { + guard let element = buffer.popFirst() else { self.state = .finished return .resumeDownstreamContinuationWithNil(continuation) } + self.state = .upstreamsFinished(buffer: buffer) + + return .resumeContinuation( + downstreamContinuation: continuation, + result: .success(element) + ) case .upstreamThrew(let error): // One of the upstreams threw and we have to return this error now. diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index 0d97adea..18012832 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -13,13 +13,15 @@ final class CombineLatestStorage< Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence ->: Sendable where +>: Sendable +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, - Base3.Element: Sendable { + Base3.Element: Sendable +{ typealias StateMachine = CombineLatestStateMachine private let stateMachine: ManagedCriticalState @@ -340,33 +342,33 @@ final class CombineLatestStorage< } while !group.isEmpty { - do { - try await group.next() - } catch { - // 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 - } + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) + } - group.cancelAll() + 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() + } } } } diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index c57b2c42..286b7aa2 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -10,93 +10,100 @@ //===----------------------------------------------------------------------===// 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, 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 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, 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, Self.Element: Sendable { - self.debounce(for: interval, tolerance: tolerance, clock: .continuous) - } + /// 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, Self.Element: Sendable { + self.debounce(for: interval, tolerance: tolerance, clock: .continuous) + } } /// 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.Element: Sendable { - private let base: Base - private let clock: C - private let interval: C.Instant.Duration - private let tolerance: C.Instant.Duration? + private let base: Base + private let clock: C + private let interval: C.Instant.Duration + private let tolerance: C.Instant.Duration? - /// Initializes a new ``AsyncDebounceSequence``. - /// - /// - Parameters: - /// - base: The base sequence. - /// - interval: The interval to debounce. - /// - tolerance: The tolerance of the clock. - /// - clock: The clock. - init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { - self.base = base - self.interval = interval - self.tolerance = tolerance - self.clock = clock - } + /// Initializes a new ``AsyncDebounceSequence``. + /// + /// - Parameters: + /// - base: The base sequence. + /// - interval: The interval to debounce. + /// - tolerance: The tolerance of the clock. + /// - clock: The clock. + init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { + self.base = base + self.interval = interval + self.tolerance = tolerance + self.clock = clock + } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncDebounceSequence: AsyncSequence { - public typealias Element = Base.Element + public typealias Element = Base.Element - public func makeAsyncIterator() -> Iterator { - let storage = DebounceStorage( - base: self.base, - interval: self.interval, - tolerance: self.tolerance, - clock: self.clock - ) - return Iterator(storage: storage) - } + public func makeAsyncIterator() -> Iterator { + let storage = DebounceStorage( + base: self.base, + interval: self.interval, + tolerance: self.tolerance, + clock: self.clock + ) + return Iterator(storage: storage) + } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncDebounceSequence { - 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. - final class InternalClass: Sendable { - private let storage: DebounceStorage + 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. + final class InternalClass: Sendable { + private let storage: DebounceStorage - fileprivate init(storage: DebounceStorage) { - self.storage = storage - } + fileprivate init(storage: DebounceStorage) { + self.storage = storage + } - deinit { - self.storage.iteratorDeinitialized() - } + deinit { + self.storage.iteratorDeinitialized() + } - func next() async rethrows -> Element? { - try await self.storage.next() - } - } + func next() async rethrows -> Element? { + try await self.storage.next() + } + } - let internalClass: InternalClass + let internalClass: InternalClass - fileprivate init(storage: DebounceStorage) { - self.internalClass = InternalClass(storage: storage) - } + fileprivate init(storage: DebounceStorage) { + self.internalClass = InternalClass(storage: storage) + } - public mutating func next() async rethrows -> Element? { - try await self.internalClass.next() - } + public mutating func next() async rethrows -> Element? { + try await self.internalClass.next() } + } } @available(*, unavailable) -extension AsyncDebounceSequence.Iterator: Sendable { } +extension AsyncDebounceSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index 5fb89451..d9948392 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -11,696 +11,699 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct DebounceStateMachine: Sendable where Base.Element: Sendable { - typealias Element = Base.Element - - private enum State: Sendable { - /// The initial state before a call to `next` happened. - case initial(base: Base) - - /// The state while we are waiting for downstream demand. - case waitingForDemand( - task: Task, - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation?, - bufferedElement: (element: Element, deadline: C.Instant)? - ) - - /// The state once the downstream signalled demand but before we received - /// the first element from the upstream. - case demandSignalled( - task: Task, - clockContinuation: UnsafeContinuation?, - downstreamContinuation: UnsafeContinuation, Never> - ) - - /// The state while we are consuming the upstream and waiting for the Clock.sleep to finish. - case debouncing( - task: Task, - upstreamContinuation: UnsafeContinuation?, - downstreamContinuation: UnsafeContinuation, Never>, - currentElement: (element: Element, deadline: C.Instant) - ) - - /// The state once any of the upstream sequences threw an `Error`. - case upstreamFailure( - error: Error - ) - - /// The state once all upstream sequences finished or the downstream consumer stopped, i.e. by dropping all references - /// or by getting their `Task` cancelled. - case finished + typealias Element = Base.Element + + private enum State: Sendable { + /// The initial state before a call to `next` happened. + case initial(base: Base) + + /// The state while we are waiting for downstream demand. + case waitingForDemand( + task: Task, + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation?, + bufferedElement: (element: Element, deadline: C.Instant)? + ) + + /// The state once the downstream signalled demand but before we received + /// the first element from the upstream. + case demandSignalled( + task: Task, + clockContinuation: UnsafeContinuation?, + downstreamContinuation: UnsafeContinuation, Never> + ) + + /// The state while we are consuming the upstream and waiting for the Clock.sleep to finish. + case debouncing( + task: Task, + upstreamContinuation: UnsafeContinuation?, + downstreamContinuation: UnsafeContinuation, Never>, + currentElement: (element: Element, deadline: C.Instant) + ) + + /// The state once any of the upstream sequences threw an `Error`. + case upstreamFailure( + error: Error + ) + + /// The state once all upstream sequences finished or the downstream consumer stopped, i.e. by dropping all references + /// or by getting their `Task` cancelled. + case finished + } + + /// The state machine's current state. + private var state: State + /// The interval to debounce. + private let interval: C.Instant.Duration + /// The clock. + private let clock: C + + init(base: Base, clock: C, interval: C.Instant.Duration) { + self.state = .initial(base: base) + self.clock = clock + self.interval = interval + } + + /// Actions returned by `iteratorDeinitialized()`. + enum IteratorDeinitializedAction { + /// Indicates that the `Task` needs to be cancelled and + /// the upstream and clock continuation need to be resumed with a `CancellationError`. + case cancelTaskAndUpstreamAndClockContinuations( + task: Task, + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation? + ) + } + + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch self.state { + case .initial: + // Nothing to do here. No demand was signalled until now + return .none + + case .debouncing, .demandSignalled: + // An iterator was deinitialized while we have a suspended continuation. + preconditionFailure( + "Internal inconsistency current state \(self.state) and received iteratorDeinitialized()" + ) + + case .waitingForDemand(let task, let upstreamContinuation, let clockContinuation, _): + // The iterator was dropped which signals that the consumer is finished. + // We can transition to finished now and need to clean everything up. + self.state = .finished + + return .cancelTaskAndUpstreamAndClockContinuations( + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: clockContinuation + ) + + case .upstreamFailure: + // The iterator was dropped which signals that the consumer is finished. + // We can transition to finished now. The cleanup already happened when we + // transitioned to `upstreamFailure`. + self.state = .finished + + return .none + + case .finished: + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + return .none } - - /// The state machine's current state. - private var state: State - /// The interval to debounce. - private let interval: C.Instant.Duration - /// The clock. - private let clock: C - - init(base: Base, clock: C, interval: C.Instant.Duration) { - self.state = .initial(base: base) - self.clock = clock - self.interval = interval + } + + mutating func taskStarted( + _ task: Task, + downstreamContinuation: UnsafeContinuation, Never> + ) { + switch self.state { + case .initial: + // The user called `next` and we are starting the `Task` + // to consume the upstream sequence + self.state = .demandSignalled( + task: task, + clockContinuation: nil, + downstreamContinuation: downstreamContinuation + ) + + case .debouncing, .demandSignalled, .waitingForDemand, .upstreamFailure, .finished: + // We only a single iterator to be created so this must never happen. + preconditionFailure("Internal inconsistency current state \(self.state) and received taskStarted()") } - - /// Actions returned by `iteratorDeinitialized()`. - enum IteratorDeinitializedAction { - /// Indicates that the `Task` needs to be cancelled and - /// the upstream and clock continuation need to be resumed with a `CancellationError`. - case cancelTaskAndUpstreamAndClockContinuations( - task: Task, - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation? - ) + } + + /// Actions returned by `upstreamTaskSuspended()`. + enum UpstreamTaskSuspendedAction { + /// Indicates that the continuation should be resumed which will lead to calling `next` on the upstream. + case resumeContinuation( + upstreamContinuation: UnsafeContinuation + ) + /// Indicates that the continuation should be resumed with an Error because another upstream sequence threw. + case resumeContinuationWithError( + upstreamContinuation: UnsafeContinuation, + error: Error + ) + } + + mutating func upstreamTaskSuspended(_ continuation: UnsafeContinuation) -> UpstreamTaskSuspendedAction? { + switch self.state { + case .initial: + // Child tasks are only created after we transitioned to `merging` + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .waitingForDemand(_, .some, _, _), .debouncing(_, .some, _, _): + // We already have an upstream continuation so we can never get a second one + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .upstreamFailure: + // The upstream already failed so it should never suspend again since the child task + // should have exited + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .waitingForDemand(let task, .none, let clockContinuation, let bufferedElement): + // The upstream task is ready to consume the next element + // we are just waiting to get demand + self.state = .waitingForDemand( + task: task, + upstreamContinuation: continuation, + clockContinuation: clockContinuation, + bufferedElement: bufferedElement + ) + + return .none + + case .demandSignalled: + // It can happen that the demand got signalled before our upstream suspended for the first time + // We need to resume it right away to demand the first element from the upstream + return .resumeContinuation(upstreamContinuation: continuation) + + case .debouncing(_, .none, _, _): + // We are currently debouncing and the upstream task suspended again + // We need to resume the continuation right away so that it continues to + // consume new elements from the upstream + + return .resumeContinuation(upstreamContinuation: continuation) + + case .finished: + // Since cancellation is cooperative it might be that child tasks are still getting + // suspended even though we already cancelled them. We must tolerate this and just resume + // the continuation with an error. + return .resumeContinuationWithError( + upstreamContinuation: continuation, + error: CancellationError() + ) } - - mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { - switch self.state { - case .initial: - // Nothing to do here. No demand was signalled until now - return .none - - case .debouncing, .demandSignalled: - // An iterator was deinitialized while we have a suspended continuation. - preconditionFailure("Internal inconsistency current state \(self.state) and received iteratorDeinitialized()") - - case .waitingForDemand(let task, let upstreamContinuation, let clockContinuation, _): - // The iterator was dropped which signals that the consumer is finished. - // We can transition to finished now and need to clean everything up. - self.state = .finished - - return .cancelTaskAndUpstreamAndClockContinuations( - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: clockContinuation - ) - - case .upstreamFailure: - // The iterator was dropped which signals that the consumer is finished. - // We can transition to finished now. The cleanup already happened when we - // transitioned to `upstreamFailure`. - self.state = .finished - - return .none - - case .finished: - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - return .none - } + } + + /// Actions returned by `elementProduced()`. + enum ElementProducedAction { + /// Indicates that the clock continuation should be resumed to start the `Clock.sleep`. + case resumeClockContinuation( + clockContinuation: UnsafeContinuation?, + deadline: C.Instant + ) + } + + mutating func elementProduced(_ element: Element, deadline: C.Instant) -> ElementProducedAction? { + switch self.state { + case .initial: + // Child tasks that are producing elements are only created after we transitioned to `merging` + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + + case .waitingForDemand(_, _, _, .some): + // We can only ever buffer one element because of the race of both child tasks + // After that element got buffered we are not resuming the upstream continuation + // and should never get another element until we get downstream demand signalled + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + + case .upstreamFailure: + // The upstream already failed so it should never have produced another element + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .waitingForDemand(let task, let upstreamContinuation, let clockContinuation, .none): + // We got an element even though we don't have an outstanding demand + // this can happen because we race the upstream and Clock child tasks + // and the upstream might finish after the Clock. We just need + // to buffer the element for the next demand. + self.state = .waitingForDemand( + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: clockContinuation, + bufferedElement: (element, deadline) + ) + + return .none + + case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): + // This is the first element that got produced after we got demand signalled + // We can now transition to debouncing and start the Clock.sleep + self.state = .debouncing( + task: task, + upstreamContinuation: nil, + downstreamContinuation: downstreamContinuation, + currentElement: (element, deadline) + ) + + let deadline = self.clock.now.advanced(by: self.interval) + return .resumeClockContinuation( + clockContinuation: clockContinuation, + deadline: deadline + ) + + case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, _): + // We just got another element and the Clock hasn't finished sleeping yet + // We just need to store the new element + self.state = .debouncing( + task: task, + upstreamContinuation: upstreamContinuation, + downstreamContinuation: downstreamContinuation, + currentElement: (element, deadline) + ) + + return .none + + case .finished: + // Since cancellation is cooperative it might be that child tasks + // are still producing elements after we finished. + // We are just going to drop them since there is nothing we can do + return .none } - - mutating func taskStarted(_ task: Task, downstreamContinuation: UnsafeContinuation, Never>) { - switch self.state { - case .initial: - // The user called `next` and we are starting the `Task` - // to consume the upstream sequence - self.state = .demandSignalled( - task: task, - clockContinuation: nil, - downstreamContinuation: downstreamContinuation - ) - - case .debouncing, .demandSignalled, .waitingForDemand, .upstreamFailure, .finished: - // We only a single iterator to be created so this must never happen. - preconditionFailure("Internal inconsistency current state \(self.state) and received taskStarted()") - } + } + + /// Actions returned by `upstreamFinished()`. + enum UpstreamFinishedAction { + /// Indicates that the task and the clock continuation should be cancelled. + case cancelTaskAndClockContinuation( + task: Task, + clockContinuation: UnsafeContinuation? + ) + /// Indicates that the downstream continuation should be resumed with `nil` and + /// the task and the upstream continuation should be cancelled. + case resumeContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: UnsafeContinuation, Never>, + task: Task, + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation? + ) + /// Indicates that the downstream continuation should be resumed with `nil` and + /// the task and the upstream continuation should be cancelled. + case resumeContinuationWithElementAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: UnsafeContinuation, Never>, + element: Element, + task: Task, + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation? + ) + } + + mutating func upstreamFinished() -> UpstreamFinishedAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .waitingForDemand(_, .some, _, _): + // We will never receive an upstream finished and have an outstanding continuation + // since we only receive finish after resuming the upstream continuation + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .waitingForDemand(_, .none, _, .some): + // We will never receive an upstream finished while we have a buffered element + // To get there we would need to have received the buffered element and then + // received upstream finished all while waiting for demand; however, we should have + // never demanded the next element from upstream in the first place + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .upstreamFailure: + // The upstream already failed so it should never have finished again + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .waitingForDemand(let task, .none, let clockContinuation, .none): + // We don't have any buffered element so we can just go ahead + // and transition to finished and cancel everything + self.state = .finished + + return .cancelTaskAndClockContinuation( + task: task, + clockContinuation: clockContinuation + ) + + case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): + // We demanded the next element from the upstream after we got signalled demand + // and the upstream finished. This means we need to resume the downstream with nil + self.state = .finished + + return .resumeContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuation: nil, + clockContinuation: clockContinuation + ) + + case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, let currentElement): + // We are debouncing and the upstream finished. At this point + // we can just resume the downstream continuation with element and cancel everything else + self.state = .finished + + return .resumeContinuationWithElementAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: downstreamContinuation, + element: currentElement.element, + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: nil + ) + + case .finished: + // This is just everything finishing up, nothing to do here + return .none } - - /// Actions returned by `upstreamTaskSuspended()`. - enum UpstreamTaskSuspendedAction { - /// Indicates that the continuation should be resumed which will lead to calling `next` on the upstream. - case resumeContinuation( - upstreamContinuation: UnsafeContinuation - ) - /// Indicates that the continuation should be resumed with an Error because another upstream sequence threw. - case resumeContinuationWithError( - upstreamContinuation: UnsafeContinuation, - error: Error - ) + } + + /// Actions returned by `upstreamThrew()`. + enum UpstreamThrewAction { + /// Indicates that the task and the clock continuation should be cancelled. + case cancelTaskAndClockContinuation( + task: Task, + clockContinuation: UnsafeContinuation? + ) + /// Indicates that the downstream continuation should be resumed with the `error` and + /// the task and the upstream continuation should be cancelled. + case resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + downstreamContinuation: UnsafeContinuation, Never>, + error: Error, + task: Task, + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation? + ) + } + + mutating func upstreamThrew(_ error: Error) -> UpstreamThrewAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .waitingForDemand(_, .some, _, _): + // We will never receive an upstream threw and have an outstanding continuation + // since we only receive threw after resuming the upstream continuation + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .waitingForDemand(_, .none, _, .some): + // We will never receive an upstream threw while we have a buffered element + // To get there we would need to have received the buffered element and then + // received upstream threw all while waiting for demand; however, we should have + // never demanded the next element from upstream in the first place + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .upstreamFailure: + // 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 + // and transition to finished and cancel everything + self.state = .finished + + return .cancelTaskAndClockContinuation( + task: task, + clockContinuation: clockContinuation + ) + + case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): + // We demanded the next element from the upstream after we got signalled demand + // and the upstream threw. This means we need to resume the downstream with the error + self.state = .finished + + return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + downstreamContinuation: downstreamContinuation, + error: error, + task: task, + upstreamContinuation: nil, + clockContinuation: clockContinuation + ) + + case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, _): + // We are debouncing and the upstream threw. At this point + // we can just resume the downstream continuation with error and cancel everything else + self.state = .finished + + return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + downstreamContinuation: downstreamContinuation, + error: error, + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: nil + ) + + case .finished: + // This is just everything finishing up, nothing to do here + return .none } - - mutating func upstreamTaskSuspended(_ continuation: UnsafeContinuation) -> UpstreamTaskSuspendedAction? { - switch self.state { - case .initial: - // Child tasks are only created after we transitioned to `merging` - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") - - case .waitingForDemand(_, .some, _, _), .debouncing(_, .some, _, _): - // We already have an upstream continuation so we can never get a second one - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") - - case .upstreamFailure: - // The upstream already failed so it should never suspend again since the child task - // should have exited - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") - - case .waitingForDemand(let task, .none, let clockContinuation, let bufferedElement): - // The upstream task is ready to consume the next element - // we are just waiting to get demand - self.state = .waitingForDemand( - task: task, - upstreamContinuation: continuation, - clockContinuation: clockContinuation, - bufferedElement: bufferedElement - ) - - return .none - - case .demandSignalled: - // It can happen that the demand got signalled before our upstream suspended for the first time - // We need to resume it right away to demand the first element from the upstream - return .resumeContinuation(upstreamContinuation: continuation) - - case .debouncing(_, .none, _, _): - // We are currently debouncing and the upstream task suspended again - // We need to resume the continuation right away so that it continues to - // consume new elements from the upstream - - return .resumeContinuation(upstreamContinuation: continuation) - - case .finished: - // Since cancellation is cooperative it might be that child tasks are still getting - // suspended even though we already cancelled them. We must tolerate this and just resume - // the continuation with an error. - return .resumeContinuationWithError( - upstreamContinuation: continuation, - error: CancellationError() - ) - } + } + + /// Actions returned by `clockTaskSuspended()`. + enum ClockTaskSuspendedAction { + /// Indicates that the continuation should be resumed which will lead to calling `sleep` on the Clock. + case resumeContinuation( + clockContinuation: UnsafeContinuation, + deadline: C.Instant + ) + /// Indicates that the continuation should be resumed with an Error because another upstream sequence threw. + case resumeContinuationWithError( + clockContinuation: UnsafeContinuation, + error: Error + ) + } + + mutating func clockTaskSuspended(_ continuation: UnsafeContinuation) -> ClockTaskSuspendedAction? { + switch self.state { + case .initial: + // Child tasks are only created after we transitioned to `merging` + preconditionFailure("Internal inconsistency current state \(self.state) and received clockTaskSuspended()") + + case .waitingForDemand(_, _, .some, _): + // We already have a clock continuation so we can never get a second one + preconditionFailure("Internal inconsistency current state \(self.state) and received clockTaskSuspended()") + + case .demandSignalled(_, .some, _): + // We already have a clock continuation so we can never get a second one + preconditionFailure("Internal inconsistency current state \(self.state) and received clockTaskSuspended()") + + case .waitingForDemand(let task, let upstreamContinuation, .none, let bufferedElement): + // The clock child task suspended and we just need to store the continuation until + // demand is signalled + + self.state = .waitingForDemand( + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: continuation, + bufferedElement: bufferedElement + ) + + return .none + + case .demandSignalled(let task, .none, let downstreamContinuation): + // The demand was signalled but we haven't gotten the first element from the upstream yet + // so we need to stay in this state and do nothing + self.state = .demandSignalled( + task: task, + clockContinuation: continuation, + downstreamContinuation: downstreamContinuation + ) + + return .none + + case .debouncing(_, _, _, let currentElement): + // We are currently debouncing and the Clock task suspended + // We need to resume the continuation right away. + return .resumeContinuation( + clockContinuation: continuation, + deadline: currentElement.deadline + ) + + case .upstreamFailure: + // The upstream failed while we were waiting to suspend the clock task again + // The task should have already been cancelled and we just need to cancel the continuation + return .resumeContinuationWithError( + clockContinuation: continuation, + error: CancellationError() + ) + + case .finished: + // Since cancellation is cooperative it might be that child tasks are still getting + // suspended even though we already cancelled them. We must tolerate this and just resume + // the continuation with an error. + return .resumeContinuationWithError( + clockContinuation: continuation, + error: CancellationError() + ) } - - /// Actions returned by `elementProduced()`. - enum ElementProducedAction { - /// Indicates that the clock continuation should be resumed to start the `Clock.sleep`. - case resumeClockContinuation( - clockContinuation: UnsafeContinuation?, - deadline: C.Instant - ) + } + + /// Actions returned by `clockSleepFinished()`. + enum ClockSleepFinishedAction { + /// Indicates that the downstream continuation should be resumed with the given element. + case resumeDownstreamContinuation( + downstreamContinuation: UnsafeContinuation, Never>, + element: Element + ) + } + + mutating func clockSleepFinished() -> ClockSleepFinishedAction? { + switch self.state { + case .initial: + // Child tasks are only created after we transitioned to `merging` + preconditionFailure("Internal inconsistency current state \(self.state) and received clockSleepFinished()") + + case .waitingForDemand: + // This can never happen since we kicked-off the Clock.sleep because we got signalled demand. + preconditionFailure("Internal inconsistency current state \(self.state) and received clockSleepFinished()") + + case .demandSignalled: + // This can never happen since we are still waiting for the first element until we resume the Clock sleep. + preconditionFailure("Internal inconsistency current state \(self.state) and received clockSleepFinished()") + + case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, let currentElement): + guard currentElement.deadline <= self.clock.now else { + // The deadline is still in the future so we need to sleep again + return .none + } + // The deadline for the last produced element expired and we can forward it to the downstream + self.state = .waitingForDemand( + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: nil, + bufferedElement: nil + ) + + return .resumeDownstreamContinuation( + downstreamContinuation: downstreamContinuation, + element: currentElement.element + ) + + case .upstreamFailure: + // The upstream failed before the Clock.sleep finished + // We already cleaned everything up so nothing left to do here. + return .none + + case .finished: + // The upstream failed before the Clock.sleep finished + // We already cleaned everything up so nothing left to do here. + return .none } - - mutating func elementProduced(_ element: Element, deadline: C.Instant) -> ElementProducedAction? { - switch self.state { - case .initial: - // Child tasks that are producing elements are only created after we transitioned to `merging` - preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") - - case .waitingForDemand(_, _, _, .some): - // We can only ever buffer one element because of the race of both child tasks - // After that element got buffered we are not resuming the upstream continuation - // and should never get another element until we get downstream demand signalled - preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") - - case .upstreamFailure: - // The upstream already failed so it should never have produced another element - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") - - case .waitingForDemand(let task, let upstreamContinuation, let clockContinuation, .none): - // We got an element even though we don't have an outstanding demand - // this can happen because we race the upstream and Clock child tasks - // and the upstream might finish after the Clock. We just need - // to buffer the element for the next demand. - self.state = .waitingForDemand( - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: clockContinuation, - bufferedElement: (element, deadline) - ) - - return .none - - case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): - // This is the first element that got produced after we got demand signalled - // We can now transition to debouncing and start the Clock.sleep - self.state = .debouncing( - task: task, - upstreamContinuation: nil, - downstreamContinuation: downstreamContinuation, - currentElement: (element, deadline) - ) - - let deadline = self.clock.now.advanced(by: self.interval) - return .resumeClockContinuation( - clockContinuation: clockContinuation, - deadline: deadline - ) - - case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, _): - // We just got another element and the Clock hasn't finished sleeping yet - // We just need to store the new element - self.state = .debouncing( - task: task, - upstreamContinuation: upstreamContinuation, - downstreamContinuation: downstreamContinuation, - currentElement: (element, deadline) - ) - - return .none - - case .finished: - // Since cancellation is cooperative it might be that child tasks - // are still producing elements after we finished. - // We are just going to drop them since there is nothing we can do - return .none - } + } + + /// Actions returned by `cancelled()`. + enum CancelledAction { + /// Indicates that the downstream continuation needs to be resumed and + /// task and the upstream continuations should be cancelled. + case resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: UnsafeContinuation, Never>, + task: Task, + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation? + ) + } + + mutating func cancelled() -> CancelledAction? { + switch self.state { + case .initial: + state = .finished + return .none + + case .waitingForDemand: + // We got cancelled before we event got any demand. This can happen if a cancelled task + // calls next and the onCancel handler runs first. We can transition to finished right away. + self.state = .finished + + return .none + + case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): + // We got cancelled while we were waiting for the first upstream element + // We can cancel everything at this point and return nil + self.state = .finished + + return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuation: nil, + clockContinuation: clockContinuation + ) + + case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, _): + // We got cancelled while debouncing. + // We can cancel everything at this point and return nil + self.state = .finished + + return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuation: upstreamContinuation, + clockContinuation: nil + ) + + case .upstreamFailure: + // An upstream already threw and we cancelled everything already. + // We should stay in the upstream failure state until the error is consumed + return .none + + case .finished: + // We are already finished so nothing to do here: + self.state = .finished + + return .none } - - /// Actions returned by `upstreamFinished()`. - enum UpstreamFinishedAction { - /// Indicates that the task and the clock continuation should be cancelled. - case cancelTaskAndClockContinuation( - task: Task, - clockContinuation: UnsafeContinuation? + } + + /// Actions returned by `next()`. + enum NextAction { + /// Indicates that a new `Task` should be created that consumes the sequence. + case startTask(Base) + case resumeUpstreamContinuation( + upstreamContinuation: UnsafeContinuation? + ) + case resumeUpstreamAndClockContinuation( + upstreamContinuation: UnsafeContinuation?, + clockContinuation: UnsafeContinuation?, + deadline: C.Instant + ) + /// Indicates that the downstream continuation should be resumed with `nil`. + case resumeDownstreamContinuationWithNil(UnsafeContinuation, Never>) + /// Indicates that the downstream continuation should be resumed with the error. + case resumeDownstreamContinuationWithError( + UnsafeContinuation, Never>, + Error + ) + } + + mutating func next(for continuation: UnsafeContinuation, Never>) -> NextAction { + switch self.state { + case .initial(let base): + // This is the first time we get demand singalled so we have to start the task + // The transition to the next state is done in the taskStarted method + return .startTask(base) + + case .demandSignalled, .debouncing: + // We already got demand signalled and have suspended the downstream task + // Getting a second next calls means the iterator was transferred across Tasks which is not allowed + preconditionFailure("Internal inconsistency current state \(self.state) and received next()") + + case .waitingForDemand(let task, let upstreamContinuation, let clockContinuation, let bufferedElement): + guard let bufferedElement = bufferedElement else { + // We don't have a buffered element so have to resume the upstream continuation + // to get the first one and transition to demandSignalled + self.state = .demandSignalled( + task: task, + clockContinuation: clockContinuation, + downstreamContinuation: continuation ) - /// Indicates that the downstream continuation should be resumed with `nil` and - /// the task and the upstream continuation should be cancelled. - case resumeContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: UnsafeContinuation, Never>, - task: Task, - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation? - ) - /// Indicates that the downstream continuation should be resumed with `nil` and - /// the task and the upstream continuation should be cancelled. - case resumeContinuationWithElementAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: UnsafeContinuation, Never>, - element: Element, - task: Task, - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation? - ) - } - - mutating func upstreamFinished() -> UpstreamFinishedAction? { - switch self.state { - case .initial: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") - - case .waitingForDemand(_, .some, _, _): - // We will never receive an upstream finished and have an outstanding continuation - // since we only receive finish after resuming the upstream continuation - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") - - case .waitingForDemand(_, .none, _, .some): - // We will never receive an upstream finished while we have a buffered element - // To get there we would need to have received the buffered element and then - // received upstream finished all while waiting for demand; however, we should have - // never demanded the next element from upstream in the first place - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") - - case .upstreamFailure: - // The upstream already failed so it should never have finished again - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") - - case .waitingForDemand(let task, .none, let clockContinuation, .none): - // We don't have any buffered element so we can just go ahead - // and transition to finished and cancel everything - self.state = .finished - - return .cancelTaskAndClockContinuation( - task: task, - clockContinuation: clockContinuation - ) - - case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): - // We demanded the next element from the upstream after we got signalled demand - // and the upstream finished. This means we need to resume the downstream with nil - self.state = .finished - - return .resumeContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: downstreamContinuation, - task: task, - upstreamContinuation: nil, - clockContinuation: clockContinuation - ) - - case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, let currentElement): - // We are debouncing and the upstream finished. At this point - // we can just resume the downstream continuation with element and cancel everything else - self.state = .finished - - return .resumeContinuationWithElementAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: downstreamContinuation, - element: currentElement.element, - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: nil - ) - - case .finished: - // This is just everything finishing up, nothing to do here - return .none - } - } - - /// Actions returned by `upstreamThrew()`. - enum UpstreamThrewAction { - /// Indicates that the task and the clock continuation should be cancelled. - case cancelTaskAndClockContinuation( - task: Task, - clockContinuation: UnsafeContinuation? - ) - /// Indicates that the downstream continuation should be resumed with the `error` and - /// the task and the upstream continuation should be cancelled. - case resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - downstreamContinuation: UnsafeContinuation, Never>, - error: Error, - task: Task, - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation? - ) - } - - mutating func upstreamThrew(_ error: Error) -> UpstreamThrewAction? { - switch self.state { - case .initial: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") - - case .waitingForDemand(_, .some, _, _): - // We will never receive an upstream threw and have an outstanding continuation - // since we only receive threw after resuming the upstream continuation - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") - - case .waitingForDemand(_, .none, _, .some): - // We will never receive an upstream threw while we have a buffered element - // To get there we would need to have received the buffered element and then - // received upstream threw all while waiting for demand; however, we should have - // never demanded the next element from upstream in the first place - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") - - case .upstreamFailure: - // 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 - // and transition to finished and cancel everything - self.state = .finished - - return .cancelTaskAndClockContinuation( - task: task, - clockContinuation: clockContinuation - ) - - case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): - // We demanded the next element from the upstream after we got signalled demand - // and the upstream threw. This means we need to resume the downstream with the error - self.state = .finished - - return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - downstreamContinuation: downstreamContinuation, - error: error, - task: task, - upstreamContinuation: nil, - clockContinuation: clockContinuation - ) - - case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, _): - // We are debouncing and the upstream threw. At this point - // we can just resume the downstream continuation with error and cancel everything else - self.state = .finished - - return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - downstreamContinuation: downstreamContinuation, - error: error, - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: nil - ) - - case .finished: - // This is just everything finishing up, nothing to do here - return .none - } - } - - /// Actions returned by `clockTaskSuspended()`. - enum ClockTaskSuspendedAction { - /// Indicates that the continuation should be resumed which will lead to calling `sleep` on the Clock. - case resumeContinuation( - clockContinuation: UnsafeContinuation, - deadline: C.Instant - ) - /// Indicates that the continuation should be resumed with an Error because another upstream sequence threw. - case resumeContinuationWithError( - clockContinuation: UnsafeContinuation, - error: Error - ) - } - - mutating func clockTaskSuspended(_ continuation: UnsafeContinuation) -> ClockTaskSuspendedAction? { - switch self.state { - case .initial: - // Child tasks are only created after we transitioned to `merging` - preconditionFailure("Internal inconsistency current state \(self.state) and received clockTaskSuspended()") - - case .waitingForDemand(_, _, .some, _): - // We already have a clock continuation so we can never get a second one - preconditionFailure("Internal inconsistency current state \(self.state) and received clockTaskSuspended()") - - case .demandSignalled(_, .some, _): - // We already have a clock continuation so we can never get a second one - preconditionFailure("Internal inconsistency current state \(self.state) and received clockTaskSuspended()") - - case .waitingForDemand(let task, let upstreamContinuation, .none, let bufferedElement): - // The clock child task suspended and we just need to store the continuation until - // demand is signalled - - self.state = .waitingForDemand( - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: continuation, - bufferedElement: bufferedElement - ) - - return .none - - case .demandSignalled(let task, .none, let downstreamContinuation): - // The demand was signalled but we haven't gotten the first element from the upstream yet - // so we need to stay in this state and do nothing - self.state = .demandSignalled( - task: task, - clockContinuation: continuation, - downstreamContinuation: downstreamContinuation - ) - - return .none - - case .debouncing(_, _, _, let currentElement): - // We are currently debouncing and the Clock task suspended - // We need to resume the continuation right away. - return .resumeContinuation( - clockContinuation: continuation, - deadline: currentElement.deadline - ) - - case .upstreamFailure: - // The upstream failed while we were waiting to suspend the clock task again - // The task should have already been cancelled and we just need to cancel the continuation - return .resumeContinuationWithError( - clockContinuation: continuation, - error: CancellationError() - ) - - case .finished: - // Since cancellation is cooperative it might be that child tasks are still getting - // suspended even though we already cancelled them. We must tolerate this and just resume - // the continuation with an error. - return .resumeContinuationWithError( - clockContinuation: continuation, - error: CancellationError() - ) - } - } - - /// Actions returned by `clockSleepFinished()`. - enum ClockSleepFinishedAction { - /// Indicates that the downstream continuation should be resumed with the given element. - case resumeDownstreamContinuation( - downstreamContinuation: UnsafeContinuation, Never>, - element: Element - ) - } - - mutating func clockSleepFinished() -> ClockSleepFinishedAction? { - switch self.state { - case .initial: - // Child tasks are only created after we transitioned to `merging` - preconditionFailure("Internal inconsistency current state \(self.state) and received clockSleepFinished()") - - case .waitingForDemand: - // This can never happen since we kicked-off the Clock.sleep because we got signalled demand. - preconditionFailure("Internal inconsistency current state \(self.state) and received clockSleepFinished()") - - case .demandSignalled: - // This can never happen since we are still waiting for the first element until we resume the Clock sleep. - preconditionFailure("Internal inconsistency current state \(self.state) and received clockSleepFinished()") - - case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, let currentElement): - if currentElement.deadline <= self.clock.now { - // The deadline for the last produced element expired and we can forward it to the downstream - self.state = .waitingForDemand( - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: nil, - bufferedElement: nil - ) - - return .resumeDownstreamContinuation( - downstreamContinuation: downstreamContinuation, - element: currentElement.element - ) - } else { - // The deadline is still in the future so we need to sleep again - return .none - } - - case .upstreamFailure: - // The upstream failed before the Clock.sleep finished - // We already cleaned everything up so nothing left to do here. - return .none - - case .finished: - // The upstream failed before the Clock.sleep finished - // We already cleaned everything up so nothing left to do here. - return .none - } - } - - /// Actions returned by `cancelled()`. - enum CancelledAction { - /// Indicates that the downstream continuation needs to be resumed and - /// task and the upstream continuations should be cancelled. - case resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: UnsafeContinuation, Never>, - task: Task, - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation? - ) - } - - mutating func cancelled() -> CancelledAction? { - switch self.state { - case .initial: - state = .finished - return .none - - case .waitingForDemand: - // We got cancelled before we event got any demand. This can happen if a cancelled task - // calls next and the onCancel handler runs first. We can transition to finished right away. - self.state = .finished - - return .none - - case .demandSignalled(let task, let clockContinuation, let downstreamContinuation): - // We got cancelled while we were waiting for the first upstream element - // We can cancel everything at this point and return nil - self.state = .finished - - return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: downstreamContinuation, - task: task, - upstreamContinuation: nil, - clockContinuation: clockContinuation - ) - - case .debouncing(let task, let upstreamContinuation, let downstreamContinuation, _): - // We got cancelled while debouncing. - // We can cancel everything at this point and return nil - self.state = .finished - - return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( - downstreamContinuation: downstreamContinuation, - task: task, - upstreamContinuation: upstreamContinuation, - clockContinuation: nil - ) - - case .upstreamFailure: - // An upstream already threw and we cancelled everything already. - // We should stay in the upstream failure state until the error is consumed - return .none - - case .finished: - // We are already finished so nothing to do here: - self.state = .finished - - return .none - } - } - - /// Actions returned by `next()`. - enum NextAction { - /// Indicates that a new `Task` should be created that consumes the sequence. - case startTask(Base) - case resumeUpstreamContinuation( - upstreamContinuation: UnsafeContinuation? - ) - case resumeUpstreamAndClockContinuation( - upstreamContinuation: UnsafeContinuation?, - clockContinuation: UnsafeContinuation?, - deadline: C.Instant - ) - /// Indicates that the downstream continuation should be resumed with `nil`. - case resumeDownstreamContinuationWithNil(UnsafeContinuation, Never>) - /// Indicates that the downstream continuation should be resumed with the error. - case resumeDownstreamContinuationWithError( - UnsafeContinuation, Never>, - Error - ) - } - mutating func next(for continuation: UnsafeContinuation, Never>) -> NextAction { - switch self.state { - case .initial(let base): - // This is the first time we get demand singalled so we have to start the task - // The transition to the next state is done in the taskStarted method - return .startTask(base) - - case .demandSignalled, .debouncing: - // We already got demand signalled and have suspended the downstream task - // Getting a second next calls means the iterator was transferred across Tasks which is not allowed - preconditionFailure("Internal inconsistency current state \(self.state) and received next()") - - case .waitingForDemand(let task, let upstreamContinuation, let clockContinuation, let bufferedElement): - if let bufferedElement = bufferedElement { - // We already got an element from the last buffered one - // We can kick of the clock and upstream consumption right away and transition to debouncing - self.state = .debouncing( - task: task, - upstreamContinuation: nil, - downstreamContinuation: continuation, - currentElement: bufferedElement - ) - - return .resumeUpstreamAndClockContinuation( - upstreamContinuation: upstreamContinuation, - clockContinuation: clockContinuation, - deadline: bufferedElement.deadline - ) - } else { - // We don't have a buffered element so have to resume the upstream continuation - // to get the first one and transition to demandSignalled - self.state = .demandSignalled( - task: task, - clockContinuation: clockContinuation, - downstreamContinuation: continuation - ) - - return .resumeUpstreamContinuation(upstreamContinuation: upstreamContinuation) - } - - case .upstreamFailure(let error): - // The upstream threw and haven't delivered the error yet - // Let's deliver it and transition to finished - self.state = .finished - - return .resumeDownstreamContinuationWithError(continuation, error) - - case .finished: - // We are already finished so we are just returning `nil` - return .resumeDownstreamContinuationWithNil(continuation) - } + return .resumeUpstreamContinuation(upstreamContinuation: upstreamContinuation) + } + // We already got an element from the last buffered one + // We can kick of the clock and upstream consumption right away and transition to debouncing + self.state = .debouncing( + task: task, + upstreamContinuation: nil, + downstreamContinuation: continuation, + currentElement: bufferedElement + ) + + return .resumeUpstreamAndClockContinuation( + upstreamContinuation: upstreamContinuation, + clockContinuation: clockContinuation, + deadline: bufferedElement.deadline + ) + + case .upstreamFailure(let error): + // The upstream threw and haven't delivered the error yet + // Let's deliver it and transition to finished + self.state = .finished + + return .resumeDownstreamContinuationWithError(continuation, error) + + case .finished: + // We are already finished so we are just returning `nil` + return .resumeDownstreamContinuationWithNil(continuation) } + } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 1839e334..6f69fa4c 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -11,312 +11,317 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class DebounceStorage: Sendable where Base.Element: Sendable { - typealias Element = Base.Element - - /// The state machine protected with a lock. - private let stateMachine: ManagedCriticalState> - /// The interval to debounce. - private let interval: C.Instant.Duration - /// The tolerance for the clock. - private let tolerance: C.Instant.Duration? - /// The clock. - private let clock: C - - init(base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { - self.stateMachine = .init(.init(base: base, clock: clock, interval: interval)) - self.interval = interval - self.tolerance = tolerance - self.clock = clock + typealias Element = Base.Element + + /// The state machine protected with a lock. + private let stateMachine: ManagedCriticalState> + /// The interval to debounce. + private let interval: C.Instant.Duration + /// The tolerance for the clock. + private let tolerance: C.Instant.Duration? + /// The clock. + private let clock: C + + init(base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { + self.stateMachine = .init(.init(base: base, clock: clock, interval: interval)) + self.interval = interval + self.tolerance = tolerance + self.clock = clock + } + + func iteratorDeinitialized() { + let action = self.stateMachine.withCriticalRegion { $0.iteratorDeinitialized() } + + switch action { + case .cancelTaskAndUpstreamAndClockContinuations( + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) + + task.cancel() + + case .none: + break } - - func iteratorDeinitialized() { - let action = self.stateMachine.withCriticalRegion { $0.iteratorDeinitialized() } + } + + func next() async rethrows -> Element? { + // We need to handle cancellation here because we are creating a continuation + // and because we need to cancel the `Task` we created to consume the upstream + return try await withTaskCancellationHandler { + // We always suspend since we can never return an element right away + + let result: Result = await withUnsafeContinuation { continuation in + 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 .cancelTaskAndUpstreamAndClockContinuations( - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) + 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) - task.cancel() + case .resumeDownstreamContinuationWithNil(let continuation): + continuation.resume(returning: .success(nil)) + + case .resumeDownstreamContinuationWithError(let continuation, let error): + continuation.resume(returning: .failure(error)) case .none: - break + break } - } + } - func next() async rethrows -> Element? { - // We need to handle cancellation here because we are creating a continuation - // and because we need to cancel the `Task` we created to consume the upstream - return try await withTaskCancellationHandler { - // We always suspend since we can never return an element right away + return try result._rethrowGet() + } onCancel: { + let action = self.stateMachine.withCriticalRegion { $0.cancelled() } - let result: Result = await withUnsafeContinuation { continuation in - let action: DebounceStateMachine.NextAction? = self.stateMachine.withCriticalRegion { - let action = $0.next(for: continuation) + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + let downstreamContinuation, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) - switch action { - case .startTask(let base): - self.startTask( - stateMachine: &$0, - base: base, - downstreamContinuation: continuation - ) - return nil + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) - case .resumeUpstreamContinuation: - return action + case .none: + break + } + } + } + + private func startTask( + stateMachine: inout DebounceStateMachine, + base: Base, + downstreamContinuation: UnsafeContinuation, Never> + ) { + let task = Task { + await withThrowingTaskGroup(of: Void.self) { group in + // The task that consumes the upstream sequence + group.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 + // and until the Clock sleep finished. + try await withUnsafeThrowingContinuation { continuation in + let action = self.stateMachine.withCriticalRegion { $0.upstreamTaskSuspended(continuation) } - case .resumeUpstreamAndClockContinuation: - return action + switch action { + case .resumeContinuation(let continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) - case .resumeDownstreamContinuationWithNil: - return action + case .resumeContinuationWithError(let continuation, let error): + // This happens if the task got cancelled. + continuation.resume(throwing: error) - case .resumeDownstreamContinuationWithError: - return action - } + 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 element = try await iterator.next() { + let action = self.stateMachine.withCriticalRegion { + let deadline = self.clock.now.advanced(by: self.interval) + return $0.elementProduced(element, deadline: deadline) } 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: ()) + case .resumeClockContinuation(let clockContinuation, let deadline): 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 } - } + } else { + // The upstream returned `nil` which indicates that it finished + let action = self.stateMachine.withCriticalRegion { $0.upstreamFinished() } - return try result._rethrowGet() - } onCancel: { - let action = self.stateMachine.withCriticalRegion { $0.cancelled() } + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. + switch action { + case .cancelTaskAndClockContinuation(let task, let clockContinuation): + task.cancel() + clockContinuation?.resume(throwing: CancellationError()) - switch action { - case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + break loop + case .resumeContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( let downstreamContinuation, let task, let upstreamContinuation, let clockContinuation - ): + ): upstreamContinuation?.resume(throwing: CancellationError()) clockContinuation?.resume(throwing: CancellationError()) - task.cancel() downstreamContinuation.resume(returning: .success(nil)) - case .none: - break + break loop + + case .resumeContinuationWithElementAndCancelTaskAndUpstreamAndClockContinuation( + let downstreamContinuation, + let element, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) + task.cancel() + + downstreamContinuation.resume(returning: .success(element)) + + break loop + + case .none: + + break loop + } } + } } - } - private func startTask( - stateMachine: inout DebounceStateMachine, - base: Base, - downstreamContinuation: UnsafeContinuation, Never> - ) { - let task = Task { - await withThrowingTaskGroup(of: Void.self) { group in - // The task that consumes the upstream sequence - group.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 - // and until the Clock sleep finished. - try await withUnsafeThrowingContinuation { continuation in - let action = self.stateMachine.withCriticalRegion { $0.upstreamTaskSuspended(continuation) } - - switch action { - case .resumeContinuation(let continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case .resumeContinuationWithError(let continuation, let error): - // This happens 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 element = try await iterator.next() { - let action = self.stateMachine.withCriticalRegion { - let deadline = self.clock.now.advanced(by: self.interval) - return $0.elementProduced(element, deadline: deadline) - } - - switch action { - case .resumeClockContinuation(let clockContinuation, let deadline): - clockContinuation?.resume(returning: deadline) - - case .none: - break - } - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.stateMachine.withCriticalRegion { $0.upstreamFinished() } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case .cancelTaskAndClockContinuation(let task, let clockContinuation): - task.cancel() - clockContinuation?.resume(throwing: CancellationError()) - - break loop - case .resumeContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( - let downstreamContinuation, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - - downstreamContinuation.resume(returning: .success(nil)) - - break loop - - case .resumeContinuationWithElementAndCancelTaskAndUpstreamAndClockContinuation( - let downstreamContinuation, - let element, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - - downstreamContinuation.resume(returning: .success(element)) - - break loop - - case .none: - - break loop - } - } - } + group.addTask { + // This is our clock scheduling loop + loop: while true { + do { + // We are creating a continuation sleeping on the Clock. + // This continuation is only resumed if the downstream consumer called `next`. + let deadline: C.Instant = try await withUnsafeThrowingContinuation { continuation in + let action = self.stateMachine.withCriticalRegion { + $0.clockTaskSuspended(continuation) } - group.addTask { - // This is our clock scheduling loop - loop: while true { - do { - // We are creating a continuation sleeping on the Clock. - // This continuation is only resumed if the downstream consumer called `next`. - let deadline: C.Instant = try await withUnsafeThrowingContinuation { continuation in - let action = self.stateMachine.withCriticalRegion { $0.clockTaskSuspended(continuation) } - - switch action { - case .resumeContinuation(let continuation, let deadline): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: deadline) - - case .resumeContinuationWithError(let continuation, let error): - // This happens if the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - try await self.clock.sleep(until: deadline, tolerance: self.tolerance) - - let action = self.stateMachine.withCriticalRegion { $0.clockSleepFinished() } - - switch action { - case .resumeDownstreamContinuation(let downstreamContinuation, let element): - downstreamContinuation.resume(returning: .success(element)) - - case .none: - break - } - } catch { - // The only error that we expect is the `CancellationError` - // thrown from the Clock.sleep or from the withUnsafeContinuation. - // This happens if we are cleaning everything up. We can just drop that error and break our loop - precondition(error is CancellationError, "Received unexpected error \(error) in the Clock loop") - break loop - } - } - } + switch action { + case .resumeContinuation(let continuation, let deadline): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: deadline) + + case .resumeContinuationWithError(let continuation, let error): + // This happens if the task got cancelled. + continuation.resume(throwing: error) - while !group.isEmpty { - do { - try await group.next() - } catch { - // One of the upstream sequences threw an error - let action = self.stateMachine.withCriticalRegion { stateMachine in - 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 - } - } - - group.cancelAll() + case .none: + break } + } + + try await self.clock.sleep(until: deadline, tolerance: self.tolerance) + + let action = self.stateMachine.withCriticalRegion { $0.clockSleepFinished() } + + switch action { + case .resumeDownstreamContinuation(let downstreamContinuation, let element): + downstreamContinuation.resume(returning: .success(element)) + + case .none: + break + } + } catch { + // The only error that we expect is the `CancellationError` + // thrown from the Clock.sleep or from the withUnsafeContinuation. + // This happens if we are cleaning everything up. We can just drop that error and break our loop + precondition( + error is CancellationError, + "Received unexpected error \(error) in the Clock loop" + ) + break loop } + } } - stateMachine.taskStarted(task, downstreamContinuation: downstreamContinuation) + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + 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 + } + } + + group.cancelAll() + } + } } + + stateMachine.taskStarted(task, downstreamContinuation: downstreamContinuation) + } } diff --git a/Sources/AsyncAlgorithms/Dictionary.swift b/Sources/AsyncAlgorithms/Dictionary.swift index 78437b1a..629a7712 100644 --- a/Sources/AsyncAlgorithms/Dictionary.swift +++ b/Sources/AsyncAlgorithms/Dictionary.swift @@ -20,14 +20,13 @@ extension Dictionary { /// /// - Parameter keysAndValues: An asynchronous sequence of key-value pairs to use for /// the new dictionary. Every key in `keysAndValues` must be unique. - /// - Returns: A new dictionary initialized with the elements of - /// `keysAndValues`. /// - Precondition: The sequence must not have duplicate keys. @inlinable - public init(uniqueKeysWithValues keysAndValues: S) async rethrows where S.Element == (Key, Value) { + public init(uniqueKeysWithValues keysAndValues: S) async rethrows + where S.Element == (Key, Value) { self.init(uniqueKeysWithValues: try await Array(keysAndValues)) } - + /// Creates a new dictionary from the key-value pairs in the given asynchronous sequence, /// using a combining closure to determine the value for any duplicate keys. /// @@ -47,7 +46,10 @@ extension Dictionary { /// the final dictionary, or throws an error if building the dictionary /// can't proceed. @inlinable - public init(_ keysAndValues: S, uniquingKeysWith combine: (Value, Value) async throws -> Value) async rethrows where S.Element == (Key, Value) { + public init( + _ keysAndValues: S, + uniquingKeysWith combine: (Value, Value) async throws -> Value + ) async rethrows where S.Element == (Key, Value) { self.init() for try await (key, value) in keysAndValues { if let existing = self[key] { @@ -57,7 +59,7 @@ extension Dictionary { } } } - + /// Creates a new dictionary whose keys are the groupings returned by the /// given closure and whose values are arrays of the elements that returned /// each key. @@ -71,7 +73,8 @@ extension Dictionary { /// - keyForValue: A closure that returns a key for each element in /// `values`. @inlinable - public init(grouping values: S, by keyForValue: (S.Element) async throws -> Key) async rethrows where Value == [S.Element] { + public init(grouping values: S, by keyForValue: (S.Element) async throws -> Key) async rethrows + where Value == [S.Element] { self.init() for try await value in values { let key = try await keyForValue(value) diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 78ef20d3..b755950a 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -10,144 +10,203 @@ //===----------------------------------------------------------------------===// 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 - 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) - } + /// 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 enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } + + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed every 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 every 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 every 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 Separator { - case element(Element) - case syncClosure(@Sendable () -> Element) - case asyncClosure(@Sendable () async -> Element) + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished } @usableFromInline - internal let base: Base + internal var iterator: Base.AsyncIterator @usableFromInline internal let separator: Separator @@ -156,151 +215,140 @@ public struct AsyncInterspersedSequence { internal let every: Int @usableFromInline - internal init(_ base: Base, every: Int, separator: Element) { - precondition(every > 0, "Separators can only be interspersed every 1+ elements") - self.base = base - self.separator = .element(separator) - self.every = every - } + internal var state = State.start(nil) @usableFromInline - internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { - precondition(every > 0, "Separators can only be interspersed every 1+ elements") - self.base = base - self.separator = .syncClosure(separator) - self.every = every + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every } - @usableFromInline - internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { - precondition(every > 0, "Separators can only be interspersed every 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 + 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() + } + + guard let element = element else { + self.state = .finished + return nil + } + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } catch { + self.state = .finished + throw error } - @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 + case .separator: + do { + guard let element = try await iterator.next() else { + self.state = .finished + return nil + } + self.state = .start(element) + switch self.separator { + case .element(let element): + return element + + case .syncClosure(let closure): + return closure() + + case .asyncClosure(let closure): + return await closure() + } + } catch { + self.state = .finished + throw error } - 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 - - 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 - } + case .element(let count): + do { + guard let element = try await iterator.next() else { + self.state = .finished + return nil + } + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } catch { + self.state = .finished + throw error } - } - @inlinable - public func makeAsyncIterator() -> Iterator { - Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + 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 Separator { + case syncClosure(@Sendable () throws -> Element) + case asyncClosure(@Sendable () async throws -> Element) + } + + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { + precondition(every > 0, "Separators can only be interspersed every 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { + precondition(every > 0, "Separators can only be interspersed every 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } +} + +extension AsyncThrowingInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { @usableFromInline - internal enum Separator { - case syncClosure(@Sendable () throws -> Element) - case asyncClosure(@Sendable () async throws -> Element) + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished } @usableFromInline - internal let base: Base + internal var iterator: Base.AsyncIterator @usableFromInline internal let separator: Separator @@ -309,127 +357,85 @@ public struct AsyncThrowingInterspersedSequence { internal let every: Int @usableFromInline - internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { - precondition(every > 0, "Separators can only be interspersed every 1+ elements") - self.base = base - self.separator = .syncClosure(separator) - self.every = every - } + internal var state = State.start(nil) @usableFromInline - internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { - precondition(every > 0, "Separators can only be interspersed every 1+ elements") - self.base = base - self.separator = .asyncClosure(separator) - self.every = every + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every } -} -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 + 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() + } + + guard let element = element else { + self.state = .finished + return nil + } + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } catch { + self.state = .finished + throw error } - @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 + case .separator: + do { + guard let element = try await iterator.next() else { + self.state = .finished + return nil + } + self.state = .start(element) + switch self.separator { + case .syncClosure(let closure): + return try closure() + + case .asyncClosure(let closure): + return try await closure() + } + } catch { + self.state = .finished + throw error } - 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 - } + case .element(let count): + do { + guard let element = try await iterator.next() else { + self.state = .finished + return nil + } + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } catch { + self.state = .finished + throw error } - } - @inlinable - public func makeAsyncIterator() -> Iterator { - Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + 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 {} diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 4265bdfd..d87ef76d 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -24,67 +24,67 @@ import Bionic #endif internal struct Lock { -#if canImport(Darwin) + #if canImport(Darwin) typealias Primitive = os_unfair_lock -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) typealias Primitive = pthread_mutex_t -#elseif canImport(WinSDK) + #elseif canImport(WinSDK) typealias Primitive = SRWLOCK -#else + #else #error("Unsupported platform") -#endif - + #endif + typealias PlatformLock = UnsafeMutablePointer let platformLock: PlatformLock private init(_ platformLock: PlatformLock) { self.platformLock = platformLock } - + fileprivate static func initialize(_ platformLock: PlatformLock) { -#if canImport(Darwin) + #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_init(platformLock, nil) precondition(result == 0, "pthread_mutex_init failed") -#elseif canImport(WinSDK) + #elseif canImport(WinSDK) InitializeSRWLock(platformLock) -#else + #else #error("Unsupported platform") -#endif + #endif } - + fileprivate static func deinitialize(_ platformLock: PlatformLock) { -#if canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #if canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_destroy(platformLock) precondition(result == 0, "pthread_mutex_destroy failed") -#endif + #endif platformLock.deinitialize(count: 1) } - + fileprivate static func lock(_ platformLock: PlatformLock) { -#if canImport(Darwin) + #if canImport(Darwin) os_unfair_lock_lock(platformLock) -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_mutex_lock(platformLock) -#elseif canImport(WinSDK) + #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) -#else + #else #error("Unsupported platform") -#endif + #endif } - + fileprivate static func unlock(_ platformLock: PlatformLock) { -#if canImport(Darwin) + #if canImport(Darwin) os_unfair_lock_unlock(platformLock) -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_unlock(platformLock) precondition(result == 0, "pthread_mutex_unlock failed") -#elseif canImport(WinSDK) + #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) -#else + #else #error("Unsupported platform") -#endif + #endif } static func allocate() -> Lock { @@ -106,26 +106,26 @@ internal struct Lock { Lock.unlock(platformLock) } - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() - } - return try body() + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() } + return try body() + } - // specialise Void return (for performance) - func withLockVoid(_ body: () throws -> Void) rethrows -> Void { - try self.withLock(body) - } + // specialise Void return (for performance) + func withLockVoid(_ body: () throws -> Void) rethrows { + try self.withLock(body) + } } struct ManagedCriticalState { @@ -134,16 +134,16 @@ struct ManagedCriticalState { withUnsafeMutablePointerToElements { Lock.deinitialize($0) } } } - + private let buffer: ManagedBuffer - + init(_ initial: State) { buffer = LockedBuffer.create(minimumCapacity: 1) { buffer in buffer.withUnsafeMutablePointerToElements { Lock.initialize($0) } return initial } } - + func withCriticalRegion(_ critical: (inout State) throws -> R) rethrows -> R { try buffer.withUnsafeMutablePointers { header, lock in Lock.lock(lock) @@ -153,4 +153,4 @@ struct ManagedCriticalState { } } -extension ManagedCriticalState: @unchecked Sendable where State: Sendable { } +extension ManagedCriticalState: @unchecked Sendable where State: Sendable {} diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 9f82ed98..a705c4a9 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -12,86 +12,92 @@ import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences -public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence - where - Base1.Element == Base2.Element, - Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable +public func merge( + _ base1: Base1, + _ base2: Base2 +) -> AsyncMerge2Sequence +where + Base1.Element == Base2.Element, + Base1: Sendable, + Base2: Sendable, + Base1.Element: Sendable { - return AsyncMerge2Sequence(base1, base2) + return AsyncMerge2Sequence(base1, base2) } -/// An ``Swift/AsyncSequence`` that takes two upstream ``Swift/AsyncSequence``s and combines their elements. +/// An `AsyncSequence` that takes two upstream `AsyncSequence`s and combines their elements. public struct AsyncMerge2Sequence< - Base1: AsyncSequence, - Base2: AsyncSequence ->: Sendable where - Base1.Element == Base2.Element, - Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable + Base1: AsyncSequence, + Base2: AsyncSequence +>: Sendable +where + Base1.Element == Base2.Element, + Base1: Sendable, + Base2: Sendable, + Base1.Element: Sendable { - public typealias Element = Base1.Element + public typealias Element = Base1.Element - private let base1: Base1 - private let base2: Base2 + private let base1: Base1 + private let base2: Base2 - /// Initializes a new ``AsyncMerge2Sequence``. - /// - /// - Parameters: - /// - base1: The first upstream ``Swift/AsyncSequence``. - /// - base2: The second upstream ``Swift/AsyncSequence``. - init( - _ base1: Base1, - _ base2: Base2 - ) { - self.base1 = base1 - self.base2 = base2 - } + /// Initializes a new ``AsyncMerge2Sequence``. + /// + /// - Parameters: + /// - base1: The first upstream ``Swift/AsyncSequence``. + /// - base2: The second upstream ``Swift/AsyncSequence``. + init( + _ base1: Base1, + _ base2: Base2 + ) { + self.base1 = base1 + self.base2 = base2 + } } extension AsyncMerge2Sequence: AsyncSequence { - public func makeAsyncIterator() -> Iterator { - let storage = MergeStorage( - base1: base1, - base2: base2, - base3: nil - ) - return Iterator(storage: storage) - } + public func makeAsyncIterator() -> Iterator { + let storage = MergeStorage( + base1: base1, + base2: base2, + base3: nil + ) + return Iterator(storage: storage) + } } extension AsyncMerge2Sequence { - 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. - final class InternalClass: Sendable { - private let storage: MergeStorage + 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. + final class InternalClass: Sendable { + private let storage: MergeStorage - fileprivate init(storage: MergeStorage) { - self.storage = storage - } + fileprivate init(storage: MergeStorage) { + self.storage = storage + } - deinit { - self.storage.iteratorDeinitialized() - } + deinit { + self.storage.iteratorDeinitialized() + } - func next() async rethrows -> Element? { - try await storage.next() - } - } + func next() async rethrows -> Element? { + try await storage.next() + } + } - let internalClass: InternalClass + let internalClass: InternalClass - fileprivate init(storage: MergeStorage) { - internalClass = InternalClass(storage: storage) - } + fileprivate init(storage: MergeStorage) { + internalClass = InternalClass(storage: storage) + } - public mutating func next() async rethrows -> Element? { - try await internalClass.next() - } + public mutating func next() async rethrows -> Element? { + try await internalClass.next() } + } } @available(*, unavailable) -extension AsyncMerge2Sequence.Iterator: Sendable { } +extension AsyncMerge2Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index d5576694..f4a15edf 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -13,96 +13,101 @@ import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge< - Base1: AsyncSequence, - Base2: AsyncSequence, - Base3: AsyncSequence + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence >(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncMerge3Sequence - where - Base1.Element == Base2.Element, - Base1.Element == Base3.Element, - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable +where + Base1.Element == Base2.Element, + Base1.Element == Base3.Element, + Base1: Sendable, + Base2: Sendable, + Base3: Sendable, + Base1.Element: Sendable { - return AsyncMerge3Sequence(base1, base2, base3) + return AsyncMerge3Sequence(base1, base2, base3) } -/// An ``Swift/AsyncSequence`` that takes three upstream ``Swift/AsyncSequence``s and combines their elements. +/// An `AsyncSequence` that takes three upstream `AsyncSequence`s and combines their elements. public struct AsyncMerge3Sequence< - Base1: AsyncSequence, - Base2: AsyncSequence, - Base3: AsyncSequence ->: Sendable where - Base1.Element == Base2.Element, - Base1.Element == Base3.Element, - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>: Sendable +where + Base1.Element == Base2.Element, + Base1.Element == Base3.Element, + Base1: Sendable, + Base2: Sendable, + Base3: Sendable, + Base1.Element: Sendable { - public typealias Element = Base1.Element + public typealias Element = Base1.Element - private let base1: Base1 - private let base2: Base2 - private let base3: Base3 + private let base1: Base1 + private let base2: Base2 + private let base3: Base3 - /// Initializes a new ``AsyncMerge2Sequence``. - /// - /// - Parameters: - /// - base1: The first upstream ``Swift/AsyncSequence``. - /// - base2: The second upstream ``Swift/AsyncSequence``. - /// - base3: The third upstream ``Swift/AsyncSequence``. - init( - _ base1: Base1, - _ base2: Base2, - _ base3: Base3 - ) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } + /// Initializes a new ``AsyncMerge2Sequence``. + /// + /// - Parameters: + /// - base1: The first upstream ``Swift/AsyncSequence``. + /// - base2: The second upstream ``Swift/AsyncSequence``. + /// - base3: The third upstream ``Swift/AsyncSequence``. + init( + _ base1: Base1, + _ base2: Base2, + _ base3: Base3 + ) { + self.base1 = base1 + self.base2 = base2 + self.base3 = base3 + } } extension AsyncMerge3Sequence: AsyncSequence { - public func makeAsyncIterator() -> Iterator { - let storage = MergeStorage( - base1: base1, - base2: base2, - base3: base3 - ) - return Iterator(storage: storage) - } + public func makeAsyncIterator() -> Iterator { + let storage = MergeStorage( + base1: base1, + base2: base2, + base3: base3 + ) + return Iterator(storage: storage) + } } -public extension AsyncMerge3Sequence { - 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. - final class InternalClass: Sendable { - private let storage: MergeStorage +extension AsyncMerge3Sequence { + 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. + final class InternalClass: Sendable { + private let storage: MergeStorage - fileprivate init(storage: MergeStorage) { - self.storage = storage - } + fileprivate init(storage: MergeStorage) { + self.storage = storage + } - deinit { - self.storage.iteratorDeinitialized() - } + deinit { + self.storage.iteratorDeinitialized() + } - func next() async rethrows -> Element? { - try await storage.next() - } - } + func next() async rethrows -> Element? { + try await storage.next() + } + } - let internalClass: InternalClass + let internalClass: InternalClass - fileprivate init(storage: MergeStorage) { - internalClass = InternalClass(storage: storage) - } + fileprivate init(storage: MergeStorage) { + internalClass = InternalClass(storage: storage) + } - public mutating func next() async rethrows -> Element? { - try await internalClass.next() - } + public mutating func next() async rethrows -> Element? { + try await internalClass.next() } + } } @available(*, unavailable) -extension AsyncMerge3Sequence.Iterator: Sendable { } +extension AsyncMerge3Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index bb832ada..24b574ec 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -16,614 +16,621 @@ import DequeModule /// Right now this state machine supports 3 upstream `AsyncSequences`; however, this can easily be extended. /// Once variadic generic land we should migrate this to use them instead. struct MergeStateMachine< - Base1: AsyncSequence, - Base2: AsyncSequence, - Base3: AsyncSequence -> where - Base1.Element == Base2.Element, - Base1.Element == Base3.Element, - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +> +where + Base1.Element == Base2.Element, + Base1.Element == Base3.Element, + Base1: Sendable, + Base2: Sendable, + Base3: Sendable, + Base1.Element: Sendable { - typealias Element = Base1.Element - - private enum State { - /// The initial state before a call to `makeAsyncIterator` happened. - case initial( - base1: Base1, - base2: Base2, - base3: Base3? - ) - - /// The state after `makeAsyncIterator` was called and we created our `Task` to consume the upstream. - case merging( - task: Task, - buffer: Deque, - upstreamContinuations: [UnsafeContinuation], - upstreamsFinished: Int, - downstreamContinuation: UnsafeContinuation? - ) - - /// The state once any of the upstream sequences threw an `Error`. - case upstreamFailure( - buffer: Deque, - error: Error - ) - - /// The state once all upstream sequences finished or the downstream consumer stopped, i.e. by dropping all references - /// or by getting their `Task` cancelled. - case finished - - /// Internal state to avoid CoW. - case modifying + typealias Element = Base1.Element + + private enum State { + /// The initial state before a call to `makeAsyncIterator` happened. + case initial( + base1: Base1, + base2: Base2, + base3: Base3? + ) + + /// The state after `makeAsyncIterator` was called and we created our `Task` to consume the upstream. + case merging( + task: Task, + buffer: Deque, + upstreamContinuations: [UnsafeContinuation], + upstreamsFinished: Int, + downstreamContinuation: UnsafeContinuation? + ) + + /// The state once any of the upstream sequences threw an `Error`. + case upstreamFailure( + buffer: Deque, + error: Error + ) + + /// The state once all upstream sequences finished or the downstream consumer stopped, i.e. by dropping all references + /// or by getting their `Task` cancelled. + case finished + + /// Internal state to avoid CoW. + case modifying + } + + /// The state machine's current state. + private var state: State + + private let numberOfUpstreamSequences: Int + + /// Initializes a new `StateMachine`. + init( + base1: Base1, + base2: Base2, + base3: Base3? + ) { + state = .initial( + base1: base1, + base2: base2, + base3: base3 + ) + + if base3 == nil { + self.numberOfUpstreamSequences = 2 + } else { + self.numberOfUpstreamSequences = 3 } - - /// The state machine's current state. - private var state: State - - private let numberOfUpstreamSequences: Int - - /// Initializes a new `StateMachine`. - init( - base1: Base1, - base2: Base2, - base3: Base3? - ) { - state = .initial( - base1: base1, - base2: base2, - base3: base3 - ) - - if base3 == nil { - self.numberOfUpstreamSequences = 2 - } else { - self.numberOfUpstreamSequences = 3 - } + } + + /// Actions returned by `iteratorDeinitialized()`. + enum IteratorDeinitializedAction { + /// Indicates that the `Task` needs to be cancelled and + /// all upstream continuations need to be resumed with a `CancellationError`. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that nothing should be done. + case none + } + + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction { + switch state { + case .initial: + // Nothing to do here. No demand was signalled until now + return .none + + case .merging(_, _, _, _, .some): + // An iterator was deinitialized while we have a suspended continuation. + preconditionFailure( + "Internal inconsistency current state \(self.state) and received iteratorDeinitialized()" + ) + + case let .merging(task, _, upstreamContinuations, _, .none): + // The iterator was dropped which signals that the consumer is finished. + // We can transition to finished now and need to clean everything up. + state = .finished + + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: upstreamContinuations + ) + + case .upstreamFailure: + // The iterator was dropped which signals that the consumer is finished. + // We can transition to finished now. The cleanup already happened when we + // transitioned to `upstreamFailure`. + state = .finished + + return .none + + case .finished: + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + return .none + + case .modifying: + preconditionFailure("Invalid state") } - - /// Actions returned by `iteratorDeinitialized()`. - enum IteratorDeinitializedAction { - /// Indicates that the `Task` needs to be cancelled and - /// all upstream continuations need to be resumed with a `CancellationError`. - case cancelTaskAndUpstreamContinuations( - task: Task, - upstreamContinuations: [UnsafeContinuation] - ) - /// Indicates that nothing should be done. - case none + } + + mutating func taskStarted(_ task: Task) { + switch state { + case .initial: + // The user called `makeAsyncIterator` and we are starting the `Task` + // to consume the upstream sequences + state = .merging( + task: task, + buffer: .init(), + upstreamContinuations: [], // This should reserve capacity in the variadic generics case + upstreamsFinished: 0, + downstreamContinuation: nil + ) + + case .merging, .upstreamFailure, .finished: + // We only a single iterator to be created so this must never happen. + preconditionFailure("Internal inconsistency current state \(self.state) and received taskStarted()") + + case .modifying: + preconditionFailure("Invalid state") } - - mutating func iteratorDeinitialized() -> IteratorDeinitializedAction { - switch state { - case .initial: - // Nothing to do here. No demand was signalled until now - return .none - - case .merging(_, _, _, _, .some): - // An iterator was deinitialized while we have a suspended continuation. - preconditionFailure("Internal inconsistency current state \(self.state) and received iteratorDeinitialized()") - - case let .merging(task, _, upstreamContinuations, _, .none): - // The iterator was dropped which signals that the consumer is finished. - // We can transition to finished now and need to clean everything up. - state = .finished - - return .cancelTaskAndUpstreamContinuations( - task: task, - upstreamContinuations: upstreamContinuations - ) - - case .upstreamFailure: - // The iterator was dropped which signals that the consumer is finished. - // We can transition to finished now. The cleanup already happened when we - // transitioned to `upstreamFailure`. - state = .finished - - return .none - - case .finished: - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - return .none - - case .modifying: - preconditionFailure("Invalid state") - } + } + + /// Actions returned by `childTaskSuspended()`. + enum ChildTaskSuspendedAction { + /// Indicates that the continuation should be resumed which will lead to calling `next` on the upstream. + case resumeContinuation( + upstreamContinuation: UnsafeContinuation + ) + /// Indicates that the continuation should be resumed with an Error because another upstream sequence threw. + case resumeContinuationWithError( + upstreamContinuation: UnsafeContinuation, + error: Error + ) + /// Indicates that nothing should be done. + case none + } + + mutating func childTaskSuspended(_ continuation: UnsafeContinuation) -> ChildTaskSuspendedAction { + switch state { + case .initial: + // Child tasks are only created after we transitioned to `merging` + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .merging(_, _, _, _, .some): + // We have outstanding demand so request the next element + return .resumeContinuation(upstreamContinuation: continuation) + + case .merging(let task, let buffer, var upstreamContinuations, let upstreamsFinished, .none): + // There is no outstanding demand from the downstream + // so we are storing the continuation and resume it once there is demand. + state = .modifying + + upstreamContinuations.append(continuation) + + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: nil + ) + + return .none + + case .upstreamFailure: + // Another upstream already threw so we just need to throw from this continuation + // which will end the consumption of the upstream. + + return .resumeContinuationWithError( + upstreamContinuation: continuation, + error: CancellationError() + ) + + case .finished: + // Since cancellation is cooperative it might be that child tasks are still getting + // suspended even though we already cancelled them. We must tolerate this and just resume + // the continuation with an error. + return .resumeContinuationWithError( + upstreamContinuation: continuation, + error: CancellationError() + ) + + case .modifying: + preconditionFailure("Invalid state") } - - mutating func taskStarted(_ task: Task) { - switch state { - case .initial: - // The user called `makeAsyncIterator` and we are starting the `Task` - // to consume the upstream sequences - state = .merging( - task: task, - buffer: .init(), - upstreamContinuations: [], // This should reserve capacity in the variadic generics case - upstreamsFinished: 0, - downstreamContinuation: nil - ) - - case .merging, .upstreamFailure, .finished: - // We only a single iterator to be created so this must never happen. - preconditionFailure("Internal inconsistency current state \(self.state) and received taskStarted()") - - case .modifying: - preconditionFailure("Invalid state") - } + } + + /// Actions returned by `elementProduced()`. + enum ElementProducedAction { + /// Indicates that the downstream continuation should be resumed with the element. + case resumeContinuation( + downstreamContinuation: UnsafeContinuation, + element: Element + ) + /// Indicates that nothing should be done. + case none + } + + mutating func elementProduced(_ element: Element) -> ElementProducedAction { + switch state { + case .initial: + // Child tasks that are producing elements are only created after we transitioned to `merging` + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + + case let .merging(task, buffer, upstreamContinuations, upstreamsFinished, .some(downstreamContinuation)): + // We produced an element and have an outstanding downstream continuation + // this means we can go right ahead and resume the continuation with that element + precondition(buffer.isEmpty, "We are holding a continuation so the buffer must be empty") + + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: nil + ) + + return .resumeContinuation( + downstreamContinuation: downstreamContinuation, + element: element + ) + + case .merging(let task, var buffer, let upstreamContinuations, let upstreamsFinished, .none): + // There is not outstanding downstream continuation so we must buffer the element + // This happens if we race our upstream sequences to produce elements + // and the _losers_ are signalling their produced element + state = .modifying + + buffer.append(element) + + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: nil + ) + + return .none + + case .upstreamFailure: + // Another upstream already produced an error so we just drop the new element + return .none + + case .finished: + // Since cancellation is cooperative it might be that child tasks + // are still producing elements after we finished. + // We are just going to drop them since there is nothing we can do + return .none + + case .modifying: + preconditionFailure("Invalid state") } - - /// Actions returned by `childTaskSuspended()`. - enum ChildTaskSuspendedAction { - /// Indicates that the continuation should be resumed which will lead to calling `next` on the upstream. - case resumeContinuation( - upstreamContinuation: UnsafeContinuation - ) - /// Indicates that the continuation should be resumed with an Error because another upstream sequence threw. - case resumeContinuationWithError( - upstreamContinuation: UnsafeContinuation, - error: Error + } + + /// Actions returned by `upstreamFinished()`. + enum UpstreamFinishedAction { + /// Indicates that the task and the upstream continuations should be cancelled. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that the downstream continuation should be resumed with `nil` and + /// the task and the upstream continuations should be cancelled. + case resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: UnsafeContinuation, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that nothing should be done. + case none + } + + mutating func upstreamFinished() -> UpstreamFinishedAction { + switch state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .merging( + let task, + let buffer, + let upstreamContinuations, + var upstreamsFinished, + let .some(downstreamContinuation) + ): + // One of the upstreams finished + precondition(buffer.isEmpty, "We are holding a continuation so the buffer must be empty") + + // First we increment our counter of finished upstreams + upstreamsFinished += 1 + + guard upstreamsFinished == self.numberOfUpstreamSequences else { + // There are still upstreams that haven't finished so we are just storing our new + // counter of finished upstreams + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: downstreamContinuation ) - /// Indicates that nothing should be done. - case none - } - mutating func childTaskSuspended(_ continuation: UnsafeContinuation) -> ChildTaskSuspendedAction { - switch state { - case .initial: - // Child tasks are only created after we transitioned to `merging` - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") - - case .merging(_, _, _, _, .some): - // We have outstanding demand so request the next element - return .resumeContinuation(upstreamContinuation: continuation) - - case .merging(let task, let buffer, var upstreamContinuations, let upstreamsFinished, .none): - // There is no outstanding demand from the downstream - // so we are storing the continuation and resume it once there is demand. - state = .modifying - - upstreamContinuations.append(continuation) - - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: nil - ) - - return .none - - case .upstreamFailure: - // Another upstream already threw so we just need to throw from this continuation - // which will end the consumption of the upstream. - - return .resumeContinuationWithError( - upstreamContinuation: continuation, - error: CancellationError() - ) - - case .finished: - // Since cancellation is cooperative it might be that child tasks are still getting - // suspended even though we already cancelled them. We must tolerate this and just resume - // the continuation with an error. - return .resumeContinuationWithError( - upstreamContinuation: continuation, - error: CancellationError() - ) - - case .modifying: - preconditionFailure("Invalid state") - } + return .none + } + // All of our upstreams have finished and we can transition to finished now + // We also need to cancel the tasks and any outstanding continuations + state = .finished + + return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: upstreamContinuations + ) + + case .merging(let task, let buffer, let upstreamContinuations, var upstreamsFinished, .none): + // First we increment our counter of finished upstreams + upstreamsFinished += 1 + + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: nil + ) + + guard upstreamsFinished == self.numberOfUpstreamSequences else { + // There are still upstreams that haven't finished. + return .none + } + // All of our upstreams have finished; however, we are only transitioning to + // finished once our downstream calls `next` again. + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: upstreamContinuations + ) + + case .upstreamFailure: + // Another upstream threw already so we can just ignore this finish + return .none + + case .finished: + // This is just everything finishing up, nothing to do here + return .none + + case .modifying: + preconditionFailure("Invalid state") } - - /// Actions returned by `elementProduced()`. - enum ElementProducedAction { - /// Indicates that the downstream continuation should be resumed with the element. - case resumeContinuation( - downstreamContinuation: UnsafeContinuation, - element: Element - ) - /// Indicates that nothing should be done. - case none + } + + /// Actions returned by `upstreamThrew()`. + enum UpstreamThrewAction { + /// Indicates that the task and the upstream continuations should be cancelled. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that the downstream continuation should be resumed with the `error` and + /// the task and the upstream continuations should be cancelled. + case resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: UnsafeContinuation, + error: Error, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that nothing should be done. + case none + } + + mutating func upstreamThrew(_ error: Error) -> UpstreamThrewAction { + switch state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case let .merging(task, buffer, upstreamContinuations, _, .some(downstreamContinuation)): + // An upstream threw an error and we have a downstream continuation. + // We just need to resume the downstream continuation with the error and cancel everything + precondition(buffer.isEmpty, "We are holding a continuation so the buffer must be empty") + + // We can transition to finished right away because we are returning the error + state = .finished + + return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + error: error, + task: task, + upstreamContinuations: upstreamContinuations + ) + + case let .merging(task, buffer, upstreamContinuations, _, .none): + // An upstream threw an error and we don't have a downstream continuation. + // We need to store the error and wait for the downstream to consume the + // rest of the buffer and the error. However, we can already cancel the task + // and the other upstream continuations since we won't need any more elements. + state = .upstreamFailure( + buffer: buffer, + error: error + ) + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: upstreamContinuations + ) + + case .upstreamFailure: + // Another upstream threw already so we can just ignore this error + return .none + + case .finished: + // This is just everything finishing up, nothing to do here + return .none + + case .modifying: + preconditionFailure("Invalid state") } - - mutating func elementProduced(_ element: Element) -> ElementProducedAction { - switch state { - case .initial: - // Child tasks that are producing elements are only created after we transitioned to `merging` - preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") - - case let .merging(task, buffer, upstreamContinuations, upstreamsFinished, .some(downstreamContinuation)): - // We produced an element and have an outstanding downstream continuation - // this means we can go right ahead and resume the continuation with that element - precondition(buffer.isEmpty, "We are holding a continuation so the buffer must be empty") - - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: nil - ) - - return .resumeContinuation( - downstreamContinuation: downstreamContinuation, - element: element - ) - - case .merging(let task, var buffer, let upstreamContinuations, let upstreamsFinished, .none): - // There is not outstanding downstream continuation so we must buffer the element - // This happens if we race our upstream sequences to produce elements - // and the _losers_ are signalling their produced element - state = .modifying - - buffer.append(element) - - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: nil - ) - - return .none - - case .upstreamFailure: - // Another upstream already produced an error so we just drop the new element - return .none - - case .finished: - // Since cancellation is cooperative it might be that child tasks - // are still producing elements after we finished. - // We are just going to drop them since there is nothing we can do - return .none - - case .modifying: - preconditionFailure("Invalid state") - } + } + + /// Actions returned by `cancelled()`. + enum CancelledAction { + /// Indicates that the downstream continuation needs to be resumed and + /// task and the upstream continuations should be cancelled. + case resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: UnsafeContinuation, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that the task and the upstream continuations should be cancelled. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that nothing should be done. + case none + } + + mutating func cancelled() -> CancelledAction { + switch state { + case .initial: + // Since we are only transitioning to merging when the task is started we + // can be cancelled already. + state = .finished + + return .none + + case let .merging(task, _, upstreamContinuations, _, .some(downstreamContinuation)): + // The downstream Task got cancelled so we need to cancel our upstream Task + // and resume all continuations. We can also transition to finished. + state = .finished + + return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: upstreamContinuations + ) + + case let .merging(task, _, upstreamContinuations, _, .none): + // The downstream Task got cancelled so we need to cancel our upstream Task + // and resume all continuations. We can also transition to finished. + state = .finished + + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: upstreamContinuations + ) + + case .upstreamFailure: + // An upstream already threw and we cancelled everything already. + // We can just transition to finished now + state = .finished + + return .none + + case .finished: + // We are already finished so nothing to do here: + state = .finished + + return .none + + case .modifying: + preconditionFailure("Invalid state") } - - /// Actions returned by `upstreamFinished()`. - enum UpstreamFinishedAction { - /// Indicates that the task and the upstream continuations should be cancelled. - case cancelTaskAndUpstreamContinuations( - task: Task, - upstreamContinuations: [UnsafeContinuation] + } + + /// Actions returned by `next()`. + enum NextAction { + /// Indicates that a new `Task` should be created that consumes the sequence and the downstream must be supsended + case startTaskAndSuspendDownstreamTask(Base1, Base2, Base3?) + /// Indicates that the `element` should be returned. + case returnElement(Result) + /// Indicates that `nil` should be returned. + case returnNil + /// Indicates that the `error` should be thrown. + case throwError(Error) + /// Indicates that the downstream task should be suspended. + case suspendDownstreamTask + } + + mutating func next() -> NextAction { + switch state { + case .initial(let base1, let base2, let base3): + // This is the first time we got demand signalled. We need to start the task now + // We are transitioning to merging in the taskStarted method. + return .startTaskAndSuspendDownstreamTask(base1, base2, base3) + + case .merging(_, _, _, _, .some): + // We have multiple AsyncIterators iterating the sequence + preconditionFailure("Internal inconsistency current state \(self.state) and received next()") + + case .merging(let task, var buffer, let upstreamContinuations, let upstreamsFinished, .none): + state = .modifying + + guard let element = buffer.popFirst() else { + // There was nothing in the buffer so we have to suspend the downstream task + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: nil ) - /// Indicates that the downstream continuation should be resumed with `nil` and - /// the task and the upstream continuations should be cancelled. - case resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation: UnsafeContinuation, - task: Task, - upstreamContinuations: [UnsafeContinuation] - ) - /// Indicates that nothing should be done. - case none - } - - mutating func upstreamFinished() -> UpstreamFinishedAction { - switch state { - case .initial: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") - - case .merging(let task, let buffer, let upstreamContinuations, var upstreamsFinished, let .some(downstreamContinuation)): - // One of the upstreams finished - precondition(buffer.isEmpty, "We are holding a continuation so the buffer must be empty") - - // First we increment our counter of finished upstreams - upstreamsFinished += 1 - - if upstreamsFinished == self.numberOfUpstreamSequences { - // All of our upstreams have finished and we can transition to finished now - // We also need to cancel the tasks and any outstanding continuations - state = .finished - - return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation: downstreamContinuation, - task: task, - upstreamContinuations: upstreamContinuations - ) - } else { - // There are still upstreams that haven't finished so we are just storing our new - // counter of finished upstreams - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: downstreamContinuation - ) - - return .none - } - - case .merging(let task, let buffer, let upstreamContinuations, var upstreamsFinished, .none): - // First we increment our counter of finished upstreams - upstreamsFinished += 1 - - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: nil - ) - - if upstreamsFinished == self.numberOfUpstreamSequences { - // All of our upstreams have finished; however, we are only transitioning to - // finished once our downstream calls `next` again. - return .cancelTaskAndUpstreamContinuations( - task: task, - upstreamContinuations: upstreamContinuations - ) - } else { - // There are still upstreams that haven't finished. - return .none - } - - case .upstreamFailure: - // Another upstream threw already so we can just ignore this finish - return .none - - case .finished: - // This is just everything finishing up, nothing to do here - return .none - - case .modifying: - preconditionFailure("Invalid state") - } - } - /// Actions returned by `upstreamThrew()`. - enum UpstreamThrewAction { - /// Indicates that the task and the upstream continuations should be cancelled. - case cancelTaskAndUpstreamContinuations( - task: Task, - upstreamContinuations: [UnsafeContinuation] - ) - /// Indicates that the downstream continuation should be resumed with the `error` and - /// the task and the upstream continuations should be cancelled. - case resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - downstreamContinuation: UnsafeContinuation, - error: Error, - task: Task, - upstreamContinuations: [UnsafeContinuation] - ) - /// Indicates that nothing should be done. - case none + return .suspendDownstreamTask + } + // We have an element buffered already so we can just return that. + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: upstreamContinuations, + upstreamsFinished: upstreamsFinished, + downstreamContinuation: nil + ) + + return .returnElement(.success(element)) + + case .upstreamFailure(var buffer, let error): + state = .modifying + + guard let element = buffer.popFirst() else { + // The buffer is empty and we can now throw the error + // that an upstream produced + state = .finished + + return .throwError(error) + } + // There was still a left over element that we need to return + state = .upstreamFailure( + buffer: buffer, + error: error + ) + + return .returnElement(.success(element)) + + case .finished: + // We are already finished so we are just returning `nil` + return .returnNil + + case .modifying: + preconditionFailure("Invalid state") } - - mutating func upstreamThrew(_ error: Error) -> UpstreamThrewAction { - switch state { - case .initial: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") - - case let .merging(task, buffer, upstreamContinuations, _, .some(downstreamContinuation)): - // An upstream threw an error and we have a downstream continuation. - // We just need to resume the downstream continuation with the error and cancel everything - precondition(buffer.isEmpty, "We are holding a continuation so the buffer must be empty") - - // We can transition to finished right away because we are returning the error - state = .finished - - return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - downstreamContinuation: downstreamContinuation, - error: error, - task: task, - upstreamContinuations: upstreamContinuations - ) - - case let .merging(task, buffer, upstreamContinuations, _, .none): - // An upstream threw an error and we don't have a downstream continuation. - // We need to store the error and wait for the downstream to consume the - // rest of the buffer and the error. However, we can already cancel the task - // and the other upstream continuations since we won't need any more elements. - state = .upstreamFailure( - buffer: buffer, - error: error - ) - return .cancelTaskAndUpstreamContinuations( - task: task, - upstreamContinuations: upstreamContinuations - ) - - case .upstreamFailure: - // Another upstream threw already so we can just ignore this error - return .none - - case .finished: - // This is just everything finishing up, nothing to do here - return .none - - case .modifying: - preconditionFailure("Invalid state") - } - } - - /// Actions returned by `cancelled()`. - enum CancelledAction { - /// Indicates that the downstream continuation needs to be resumed and - /// task and the upstream continuations should be cancelled. - case resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation: UnsafeContinuation, - task: Task, - upstreamContinuations: [UnsafeContinuation] - ) - /// Indicates that the task and the upstream continuations should be cancelled. - case cancelTaskAndUpstreamContinuations( - task: Task, - upstreamContinuations: [UnsafeContinuation] - ) - /// Indicates that nothing should be done. - case none - } - - mutating func cancelled() -> CancelledAction { - switch state { - case .initial: - // Since we are only transitioning to merging when the task is started we - // can be cancelled already. - state = .finished - - return .none - - case let .merging(task, _, upstreamContinuations, _, .some(downstreamContinuation)): - // The downstream Task got cancelled so we need to cancel our upstream Task - // and resume all continuations. We can also transition to finished. - state = .finished - - return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation: downstreamContinuation, - task: task, - upstreamContinuations: upstreamContinuations - ) - - case let .merging(task, _, upstreamContinuations, _, .none): - // The downstream Task got cancelled so we need to cancel our upstream Task - // and resume all continuations. We can also transition to finished. - state = .finished - - return .cancelTaskAndUpstreamContinuations( - task: task, - upstreamContinuations: upstreamContinuations - ) - - case .upstreamFailure: - // An upstream already threw and we cancelled everything already. - // We can just transition to finished now - state = .finished - - return .none - - case .finished: - // We are already finished so nothing to do here: - state = .finished - - return .none - - case .modifying: - preconditionFailure("Invalid state") - } - } - - /// Actions returned by `next()`. - enum NextAction { - /// Indicates that a new `Task` should be created that consumes the sequence and the downstream must be supsended - case startTaskAndSuspendDownstreamTask(Base1, Base2, Base3?) - /// Indicates that the `element` should be returned. - case returnElement(Result) - /// Indicates that `nil` should be returned. - case returnNil - /// Indicates that the `error` should be thrown. - case throwError(Error) - /// Indicates that the downstream task should be suspended. - case suspendDownstreamTask - } - - mutating func next() -> NextAction { - switch state { - case .initial(let base1, let base2, let base3): - // This is the first time we got demand signalled. We need to start the task now - // We are transitioning to merging in the taskStarted method. - return .startTaskAndSuspendDownstreamTask(base1, base2, base3) - - case .merging(_, _, _, _, .some): - // We have multiple AsyncIterators iterating the sequence - preconditionFailure("Internal inconsistency current state \(self.state) and received next()") - - case .merging(let task, var buffer, let upstreamContinuations, let upstreamsFinished, .none): - state = .modifying - - if let element = buffer.popFirst() { - // We have an element buffered already so we can just return that. - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: nil - ) - - return .returnElement(.success(element)) - } else { - // There was nothing in the buffer so we have to suspend the downstream task - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: upstreamContinuations, - upstreamsFinished: upstreamsFinished, - downstreamContinuation: nil - ) - - return .suspendDownstreamTask - } - - case .upstreamFailure(var buffer, let error): - state = .modifying - - if let element = buffer.popFirst() { - // There was still a left over element that we need to return - state = .upstreamFailure( - buffer: buffer, - error: error - ) - - return .returnElement(.success(element)) - } else { - // The buffer is empty and we can now throw the error - // that an upstream produced - state = .finished - - return .throwError(error) - } - - case .finished: - // We are already finished so we are just returning `nil` - return .returnNil - - case .modifying: - preconditionFailure("Invalid state") - } - } - - /// Actions returned by `next(for)`. - enum NextForAction { - /// Indicates that the upstream continuations should be resumed to demand new elements. - case resumeUpstreamContinuations( - upstreamContinuations: [UnsafeContinuation] - ) - } - - mutating func next(for continuation: UnsafeContinuation) -> NextForAction { - switch state { - case .initial, - .merging(_, _, _, _, .some), - .upstreamFailure, - .finished: - // All other states are handled by `next` already so we should never get in here with - // any of those - preconditionFailure("Internal inconsistency current state \(self.state) and received next(for:)") - - case let .merging(task, buffer, upstreamContinuations, upstreamsFinished, .none): - // We suspended the task and need signal the upstreams - state = .merging( - task: task, - buffer: buffer, - upstreamContinuations: [], // TODO: don't alloc new array here - upstreamsFinished: upstreamsFinished, - downstreamContinuation: continuation - ) - - return .resumeUpstreamContinuations( - upstreamContinuations: upstreamContinuations - ) - - case .modifying: - preconditionFailure("Invalid state") - } + } + + /// Actions returned by `next(for)`. + enum NextForAction { + /// Indicates that the upstream continuations should be resumed to demand new elements. + case resumeUpstreamContinuations( + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func next(for continuation: UnsafeContinuation) -> NextForAction { + switch state { + case .initial, + .merging(_, _, _, _, .some), + .upstreamFailure, + .finished: + // All other states are handled by `next` already so we should never get in here with + // any of those + preconditionFailure("Internal inconsistency current state \(self.state) and received next(for:)") + + case let .merging(task, buffer, upstreamContinuations, upstreamsFinished, .none): + // We suspended the task and need signal the upstreams + state = .merging( + task: task, + buffer: buffer, + upstreamContinuations: [], // TODO: don't alloc new array here + upstreamsFinished: upstreamsFinished, + downstreamContinuation: continuation + ) + + return .resumeUpstreamContinuations( + upstreamContinuations: upstreamContinuations + ) + + case .modifying: + preconditionFailure("Invalid state") } + } } diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index 9dedee76..c7332dda 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -10,279 +10,286 @@ //===----------------------------------------------------------------------===// final class MergeStorage< - Base1: AsyncSequence, - Base2: AsyncSequence, - Base3: AsyncSequence ->: @unchecked Sendable where - Base1.Element == Base2.Element, - Base1.Element == Base3.Element, - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>: @unchecked Sendable +where + Base1.Element == Base2.Element, + Base1.Element == Base3.Element, + Base1: Sendable, + Base2: Sendable, + Base3: Sendable, + Base1.Element: Sendable { - typealias Element = Base1.Element - - /// The lock that protects our state. - private let lock = Lock.allocate() - /// The state machine. - private var stateMachine: MergeStateMachine - - init( - base1: Base1, - base2: Base2, - base3: Base3? - ) { - stateMachine = .init(base1: base1, base2: base2, base3: base3) + typealias Element = Base1.Element + + /// The lock that protects our state. + private let lock = Lock.allocate() + /// The state machine. + private var stateMachine: MergeStateMachine + + init( + base1: Base1, + base2: Base2, + base3: Base3? + ) { + stateMachine = .init(base1: base1, base2: base2, base3: base3) + } + + deinit { + self.lock.deinitialize() + } + + func iteratorDeinitialized() { + let action = lock.withLock { self.stateMachine.iteratorDeinitialized() } + + switch action { + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + + case .none: + break } + } + + func next() async rethrows -> Element? { + // We need to handle cancellation here because we are creating a continuation + // and because we need to cancel the `Task` we created to consume the upstream + try await withTaskCancellationHandler { + self.lock.lock() + let action = self.stateMachine.next() + + switch action { + case .startTaskAndSuspendDownstreamTask(let base1, let base2, let base3): + self.startTask( + stateMachine: &self.stateMachine, + base1: base1, + base2: base2, + base3: base3 + ) + // It is safe to hold the lock across this method + // since the closure is guaranteed to be run straight away + return try await withUnsafeThrowingContinuation { continuation in + let action = self.stateMachine.next(for: continuation) + self.lock.unlock() + + switch action { + case let .resumeUpstreamContinuations(upstreamContinuations): + // This is signalling the child tasks that are consuming the upstream + // sequences to signal demand. + upstreamContinuations.forEach { $0.resume(returning: ()) } + } + } - deinit { - self.lock.deinitialize() - } + case let .returnElement(element): + self.lock.unlock() - func iteratorDeinitialized() { - let action = lock.withLock { self.stateMachine.iteratorDeinitialized() } + return try element._rethrowGet() - switch action { - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + case .returnNil: + self.lock.unlock() + return nil - task.cancel() + case let .throwError(error): + self.lock.unlock() + throw error - case .none: - break + case .suspendDownstreamTask: + // It is safe to hold the lock across this method + // since the closure is guaranteed to be run straight away + return try await withUnsafeThrowingContinuation { continuation in + let action = self.stateMachine.next(for: continuation) + self.lock.unlock() + + switch action { + case let .resumeUpstreamContinuations(upstreamContinuations): + // This is signalling the child tasks that are consuming the upstream + // sequences to signal demand. + upstreamContinuations.forEach { $0.resume(returning: ()) } + } } - } + } + } onCancel: { + let action = self.lock.withLock { self.stateMachine.cancelled() } - func next() async rethrows -> Element? { - // We need to handle cancellation here because we are creating a continuation - // and because we need to cancel the `Task` we created to consume the upstream - try await withTaskCancellationHandler { - self.lock.lock() - let action = self.stateMachine.next() + switch action { + case let .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - switch action { - case .startTaskAndSuspendDownstreamTask(let base1, let base2, let base3): - self.startTask( - stateMachine: &self.stateMachine, - base1: base1, - base2: base2, - base3: base3 - ) - // It is safe to hold the lock across this method - // since the closure is guaranteed to be run straight away - return try await withUnsafeThrowingContinuation { continuation in - let action = self.stateMachine.next(for: continuation) - self.lock.unlock() - - switch action { - case let .resumeUpstreamContinuations(upstreamContinuations): - // This is signalling the child tasks that are consuming the upstream - // sequences to signal demand. - upstreamContinuations.forEach { $0.resume(returning: ()) } - } - } - - - case let .returnElement(element): - self.lock.unlock() - - return try element._rethrowGet() - - case .returnNil: - self.lock.unlock() - return nil - - case let .throwError(error): - self.lock.unlock() - throw error - - case .suspendDownstreamTask: - // It is safe to hold the lock across this method - // since the closure is guaranteed to be run straight away - return try await withUnsafeThrowingContinuation { continuation in - let action = self.stateMachine.next(for: continuation) - self.lock.unlock() - - switch action { - case let .resumeUpstreamContinuations(upstreamContinuations): - // This is signalling the child tasks that are consuming the upstream - // sequences to signal demand. - upstreamContinuations.forEach { $0.resume(returning: ()) } - } - } - } - } onCancel: { - let action = self.lock.withLock { self.stateMachine.cancelled() } + task.cancel() + + downstreamContinuation.resume(returning: nil) + + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + case .none: + break + } + } + } + + private func startTask( + stateMachine: inout MergeStateMachine, + base1: Base1, + base2: Base2, + base3: Base3? + ) { + // This creates a new `Task` that is iterating the upstream + // sequences. We must store it to cancel it at the right times. + let task = Task { + await withThrowingTaskGroup(of: Void.self) { group in + self.iterateAsyncSequence(base1, in: &group) + self.iterateAsyncSequence(base2, in: &group) + + // Copy from the above just using the base3 sequence + if let base3 = base3 { + self.iterateAsyncSequence(base3, in: &group) + } + + 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 .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations + case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + error, + task, + upstreamContinuations ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - - task.cancel() + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - downstreamContinuation.resume(returning: nil) + task.cancel() + downstreamContinuation.resume(throwing: error) case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations + task, + upstreamContinuations ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - - task.cancel() + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() case .none: - break + break } + group.cancelAll() + } } + } } - private func startTask(stateMachine: inout MergeStateMachine, base1: Base1, base2: Base2, base3: Base3?) { - // This creates a new `Task` that is iterating the upstream - // sequences. We must store it to cancel it at the right times. - let task = Task { - await withThrowingTaskGroup(of: Void.self) { group in - self.iterateAsyncSequence(base1, in: &group) - self.iterateAsyncSequence(base2, in: &group) - - // Copy from the above just using the base3 sequence - if let base3 = base3 { - self.iterateAsyncSequence(base3, in: &group) - } - - 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 - } - group.cancelAll() - } - } - } + // 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, AsyncSequence: Sendable { + // 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 need to inform our state machine that we started the Task - stateMachine.taskStarted(task) - } + // 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() - private func iterateAsyncSequence( - _ base: AsyncSequence, - in taskGroup: inout ThrowingTaskGroup - ) 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 { - 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 - } - } - } + 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 + } } + } } + } } diff --git a/Sources/AsyncAlgorithms/Rethrow.swift b/Sources/AsyncAlgorithms/Rethrow.swift index 6edf4a41..56ecbc77 100644 --- a/Sources/AsyncAlgorithms/Rethrow.swift +++ b/Sources/AsyncAlgorithms/Rethrow.swift @@ -25,11 +25,10 @@ extension _ErrorMechanism { _ = try _rethrowGet() fatalError("materialized error without being in a throwing context") } - + internal func _rethrowGet() rethrows -> Output { return try get() } } -extension Result: _ErrorMechanism { } - +extension Result: _ErrorMechanism {} diff --git a/Sources/AsyncAlgorithms/SetAlgebra.swift b/Sources/AsyncAlgorithms/SetAlgebra.swift index a88e5dde..14f885db 100644 --- a/Sources/AsyncAlgorithms/SetAlgebra.swift +++ b/Sources/AsyncAlgorithms/SetAlgebra.swift @@ -13,7 +13,7 @@ extension SetAlgebra { /// Creates a new set from an asynchronous sequence of items. /// /// Use this initializer to create a new set from an asynchronous sequence - /// + /// /// - Parameter source: The elements to use as members of the new set. @inlinable public init(_ source: Source) async rethrows where Source.Element == Element { diff --git a/Sources/AsyncAlgorithms/UnsafeTransfer.swift b/Sources/AsyncAlgorithms/UnsafeTransfer.swift index c8bfca12..7d2e1980 100644 --- a/Sources/AsyncAlgorithms/UnsafeTransfer.swift +++ b/Sources/AsyncAlgorithms/UnsafeTransfer.swift @@ -11,9 +11,9 @@ /// A wrapper struct to unconditionally to transfer an non-Sendable value. struct UnsafeTransfer: @unchecked Sendable { - let wrapped: Element + let wrapped: Element - init(_ wrapped: Element) { - self.wrapped = wrapped - } + init(_ wrapped: Element) { + self.wrapped = wrapped + } } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index 34e42913..fb24e88b 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -21,7 +21,7 @@ public func zip( /// An asynchronous sequence that concurrently awaits values from two `AsyncSequence` types /// and emits a tuple of the values. public struct AsyncZip2Sequence: AsyncSequence, Sendable - where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public typealias AsyncIterator = Iterator @@ -71,4 +71,4 @@ public struct AsyncZip2Sequence: Asy } @available(*, unavailable) -extension AsyncZip2Sequence.Iterator: Sendable { } +extension AsyncZip2Sequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 513dc27a..68c261a2 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -21,8 +21,16 @@ public func zip: AsyncSequence, Sendable - where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { +public struct AsyncZip3Sequence: AsyncSequence, + Sendable +where + Base1: Sendable, + Base1.Element: Sendable, + Base2: Sendable, + Base2.Element: Sendable, + Base3: Sendable, + Base3.Element: Sendable +{ public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public typealias AsyncIterator = Iterator @@ -37,7 +45,8 @@ public struct AsyncZip3Sequence AsyncIterator { - Iterator(storage: .init(self.base1, self.base2, self.base3) + Iterator( + storage: .init(self.base1, self.base2, self.base3) ) } @@ -76,4 +85,4 @@ public struct AsyncZip3Sequence: Sendable where +>: Sendable +where Base1: Sendable, Base2: Sendable, Base3: Sendable, Base1.Element: Sendable, Base2.Element: Sendable, - Base3.Element: Sendable { - typealias DownstreamContinuation = UnsafeContinuation, Never> + Base3.Element: Sendable +{ + typealias DownstreamContinuation = UnsafeContinuation< + Result< + ( + Base1.Element, + Base2.Element, + Base3.Element? + )?, Error + >, Never + > private enum State: Sendable { /// Small wrapper for the state of an upstream sequence. @@ -101,7 +107,9 @@ struct ZipStateMachine< case .zipping: // An iterator was deinitialized while we have a suspended continuation. - preconditionFailure("Internal inconsistency current state \(self.state) and received iteratorDeinitialized()") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received iteratorDeinitialized()" + ) case .waitingForDemand(let task, let upstreams): // The iterator was dropped which signals that the consumer is finished. @@ -110,7 +118,8 @@ struct ZipStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .finished: @@ -159,7 +168,10 @@ struct ZipStateMachine< ) } - mutating func childTaskSuspended(baseIndex: Int, continuation: UnsafeContinuation) -> ChildTaskSuspendedAction? { + mutating func childTaskSuspended( + baseIndex: Int, + continuation: UnsafeContinuation + ) -> ChildTaskSuspendedAction? { switch self.state { case .initial: // Child tasks are only created after we transitioned to `zipping` @@ -179,7 +191,9 @@ struct ZipStateMachine< upstreams.2.continuation = continuation default: - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)" + ) } self.state = .waitingForDemand( @@ -194,9 +208,7 @@ struct ZipStateMachine< // already then we store the continuation otherwise we just go ahead and resume it switch baseIndex { case 0: - if upstreams.0.element == nil { - return .resumeContinuation(upstreamContinuation: continuation) - } else { + guard upstreams.0.element == nil else { self.state = .modifying upstreams.0.continuation = continuation self.state = .zipping( @@ -206,11 +218,10 @@ struct ZipStateMachine< ) return .none } + return .resumeContinuation(upstreamContinuation: continuation) case 1: - if upstreams.1.element == nil { - return .resumeContinuation(upstreamContinuation: continuation) - } else { + guard upstreams.1.element == nil else { self.state = .modifying upstreams.1.continuation = continuation self.state = .zipping( @@ -220,11 +231,10 @@ struct ZipStateMachine< ) return .none } + return .resumeContinuation(upstreamContinuation: continuation) case 2: - if upstreams.2.element == nil { - return .resumeContinuation(upstreamContinuation: continuation) - } else { + guard upstreams.2.element == nil else { self.state = .modifying upstreams.2.continuation = continuation self.state = .zipping( @@ -234,9 +244,12 @@ struct ZipStateMachine< ) return .none } + return .resumeContinuation(upstreamContinuation: continuation) default: - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)") + preconditionFailure( + "Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)" + ) } case .finished: @@ -295,8 +308,9 @@ struct ZipStateMachine< // Implementing this for the two arities without variadic generics is a bit awkward sadly. if let first = upstreams.0.element, - let second = upstreams.1.element, - let third = upstreams.2.element { + let second = upstreams.1.element, + let third = upstreams.2.element + { // We got an element from each upstream so we can resume the downstream now self.state = .waitingForDemand( task: task, @@ -313,8 +327,9 @@ struct ZipStateMachine< ) } else if let first = upstreams.0.element, - let second = upstreams.1.element, - self.numberOfUpstreamSequences == 2 { + let second = upstreams.1.element, + self.numberOfUpstreamSequences == 2 + { // We got an element from each upstream so we can resume the downstream now self.state = .waitingForDemand( task: task, @@ -385,7 +400,8 @@ struct ZipStateMachine< return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation: downstreamContinuation, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .finished: @@ -429,7 +445,8 @@ struct ZipStateMachine< downstreamContinuation: downstreamContinuation, error: error, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .finished: @@ -460,9 +477,9 @@ struct ZipStateMachine< mutating func cancelled() -> CancelledAction? { switch self.state { case .initial: - state = .finished + state = .finished - return .none + return .none case .waitingForDemand(let task, let upstreams): // The downstream task got cancelled so we need to cancel our upstream Task @@ -471,7 +488,8 @@ struct ZipStateMachine< return .cancelTaskAndUpstreamContinuations( task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .zipping(let task, let upstreams, let downstreamContinuation): @@ -482,7 +500,8 @@ struct ZipStateMachine< return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation: downstreamContinuation, task: task, - upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } ) case .finished: @@ -524,7 +543,8 @@ struct ZipStateMachine< // We also need to resume all upstream continuations now self.state = .modifying - let upstreamContinuations = [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + let upstreamContinuations = [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation] + .compactMap { $0 } upstreams.0.continuation = nil upstreams.1.continuation = nil upstreams.2.continuation = nil diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index 93a3466c..551d4ada 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -10,7 +10,14 @@ //===----------------------------------------------------------------------===// final class ZipStorage: Sendable - where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { +where + Base1: Sendable, + Base1.Element: Sendable, + Base2: Sendable, + Base2.Element: Sendable, + Base3: Sendable, + Base3.Element: Sendable +{ typealias StateMachine = ZipStateMachine private let stateMachine: ManagedCriticalState @@ -63,9 +70,9 @@ final class ZipStorage(theme: Theme, expectedFailures: Set, @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, line: UInt = #line) { + + func validate( + theme: Theme, + expectedFailures: Set, + @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, + file: StaticString = #file, + line: UInt = #line + ) { var expectations = expectedFailures var result: AsyncSequenceValidationDiagram.ExpectationResult? var failures = [AsyncSequenceValidationDiagram.ExpectationFailure]() @@ -61,16 +76,30 @@ extension XCTestCase { XCTFail("Expected failure: \(expectation) did not occur.", file: file, line: line) } } - - func validate(expectedFailures: Set, @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, line: UInt = #line) { + + func validate( + expectedFailures: Set, + @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, + file: StaticString = #file, + line: UInt = #line + ) { validate(theme: .ascii, expectedFailures: expectedFailures, build, file: file, line: line) } - - public func validate(theme: Theme, @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, line: UInt = #line) { + + public func validate( + theme: Theme, + @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, + file: StaticString = #file, + line: UInt = #line + ) { validate(theme: theme, expectedFailures: [], build, file: file, line: line) } - - public func validate(@AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, line: UInt = #line) { + + public func validate( + @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, + file: StaticString = #file, + line: UInt = #line + ) { validate(theme: .ascii, expectedFailures: [], build, file: file, line: line) } } diff --git a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift index b7ee6547..88d74045 100644 --- a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift +++ b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift @@ -12,71 +12,114 @@ import _CAsyncSequenceValidationSupport @resultBuilder -public struct AsyncSequenceValidationDiagram : Sendable { +public struct AsyncSequenceValidationDiagram: Sendable { public struct Component { var component: T var location: SourceLocation } - + public struct AccumulatedInputs { var inputs: [Specification] = [] } - + public struct AccumulatedInputsWithOperation where Operation.Element == String { var inputs: [Specification] var operation: Operation } - - public static func buildExpression(_ expr: String, file: StaticString = #file, line: UInt = #line) -> Component { + + public static func buildExpression( + _ expr: String, + file: StaticString = #file, + line: UInt = #line + ) -> Component { Component(component: expr, location: SourceLocation(file: file, line: line)) } - - public static func buildExpression(_ expr: S, file: StaticString = #file, line: UInt = #line) -> Component { + + public static func buildExpression( + _ expr: S, + file: StaticString = #file, + line: UInt = #line + ) -> Component { Component(component: expr, location: SourceLocation(file: file, line: line)) } - + public static func buildPartialBlock(first input: Component) -> AccumulatedInputs { return AccumulatedInputs(inputs: [Specification(specification: input.component, location: input.location)]) } - - public static func buildPartialBlock(first operation: Component) -> AccumulatedInputsWithOperation where Operation.Element == String { + + public static func buildPartialBlock( + first operation: Component + ) -> AccumulatedInputsWithOperation where Operation.Element == String { return AccumulatedInputsWithOperation(inputs: [], operation: operation.component) } - - public static func buildPartialBlock(accumulated: AccumulatedInputs, next input: Component) -> AccumulatedInputs { - return AccumulatedInputs(inputs: accumulated.inputs + [Specification(specification: input.component, location: input.location)]) + + public static func buildPartialBlock( + accumulated: AccumulatedInputs, + next input: Component + ) -> AccumulatedInputs { + return AccumulatedInputs( + inputs: accumulated.inputs + [Specification(specification: input.component, location: input.location)] + ) } - - public static func buildPartialBlock(accumulated: AccumulatedInputs, next operation: Component) -> AccumulatedInputsWithOperation { + + public static func buildPartialBlock( + accumulated: AccumulatedInputs, + next operation: Component + ) -> AccumulatedInputsWithOperation { return AccumulatedInputsWithOperation(inputs: accumulated.inputs, operation: operation.component) } - - public static func buildPartialBlock(accumulated: AccumulatedInputsWithOperation, next output: Component) -> some AsyncSequenceValidationTest { - return Test(inputs: accumulated.inputs, sequence: accumulated.operation, output: Specification(specification: output.component, location: output.location)) + + public static func buildPartialBlock( + accumulated: AccumulatedInputsWithOperation, + next output: Component + ) -> some AsyncSequenceValidationTest { + return Test( + inputs: accumulated.inputs, + sequence: accumulated.operation, + output: Specification(specification: output.component, location: output.location) + ) } - - public static func buildBlock(_ sequence: Component, _ output: Component) -> some AsyncSequenceValidationTest where Operation.Element == String { + + public static func buildBlock( + _ sequence: Component, + _ output: Component + ) -> some AsyncSequenceValidationTest where Operation.Element == String { let part1 = buildPartialBlock(first: sequence) let part2 = buildPartialBlock(accumulated: part1, next: output) return part2 } - - public static func buildBlock(_ input1: Component, _ sequence: Component, _ output: Component) -> some AsyncSequenceValidationTest where Operation.Element == String { + + public static func buildBlock( + _ input1: Component, + _ sequence: Component, + _ output: Component + ) -> some AsyncSequenceValidationTest where Operation.Element == String { let part1 = buildPartialBlock(first: input1) let part2 = buildPartialBlock(accumulated: part1, next: sequence) let part3 = buildPartialBlock(accumulated: part2, next: output) return part3 } - - public static func buildBlock(_ input1: Component, _ input2: Component, _ sequence: Component, _ output: Component) -> some AsyncSequenceValidationTest where Operation.Element == String { + + public static func buildBlock( + _ input1: Component, + _ input2: Component, + _ sequence: Component, + _ output: Component + ) -> some AsyncSequenceValidationTest where Operation.Element == String { let part1 = buildPartialBlock(first: input1) let part2 = buildPartialBlock(accumulated: part1, next: input2) let part3 = buildPartialBlock(accumulated: part2, next: sequence) let part4 = buildPartialBlock(accumulated: part3, next: output) return part4 } - - public static func buildBlock(_ input1: Component, _ input2: Component, _ input3: Component, _ sequence: Component, _ output: Component) -> some AsyncSequenceValidationTest where Operation.Element == String { + + public static func buildBlock( + _ input1: Component, + _ input2: Component, + _ input3: Component, + _ sequence: Component, + _ output: Component + ) -> some AsyncSequenceValidationTest where Operation.Element == String { let part1 = buildPartialBlock(first: input1) let part2 = buildPartialBlock(accumulated: part1, next: input2) let part3 = buildPartialBlock(accumulated: part2, next: input3) @@ -84,8 +127,15 @@ public struct AsyncSequenceValidationDiagram : Sendable { let part5 = buildPartialBlock(accumulated: part4, next: output) return part5 } - - public static func buildBlock(_ input1: Component, _ input2: Component, _ input3: Component, _ input4: Component, _ sequence: Component, _ output: Component) -> some AsyncSequenceValidationTest where Operation.Element == String { + + public static func buildBlock( + _ input1: Component, + _ input2: Component, + _ input3: Component, + _ input4: Component, + _ sequence: Component, + _ output: Component + ) -> some AsyncSequenceValidationTest where Operation.Element == String { let part1 = buildPartialBlock(first: input1) let part2 = buildPartialBlock(accumulated: part1, next: input2) let part3 = buildPartialBlock(accumulated: part2, next: input3) @@ -94,17 +144,17 @@ public struct AsyncSequenceValidationDiagram : Sendable { let part6 = buildPartialBlock(accumulated: part5, next: output) return part6 } - + let queue: WorkQueue let _clock: Clock - + public var inputs: InputList - + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public var clock: Clock { _clock } - + internal init() { let queue = WorkQueue() self.queue = queue @@ -112,4 +162,3 @@ public struct AsyncSequenceValidationDiagram : Sendable { self._clock = Clock(queue: queue) } } - diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index e76f5aa9..d9ebab3d 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -14,19 +14,18 @@ import AsyncAlgorithms extension AsyncSequenceValidationDiagram { public struct Clock { let queue: WorkQueue - + init(queue: WorkQueue) { self.queue = queue } } } - public protocol TestClock: Sendable { associatedtype Instant: TestInstant - + var now: Instant { get } - + func sleep(until deadline: Self.Instant, tolerance: Self.Instant.Duration?) async throws } @@ -37,77 +36,77 @@ public protocol TestInstant: Equatable { extension AsyncSequenceValidationDiagram.Clock { public struct Step: DurationProtocol, Hashable, CustomStringConvertible { internal var rawValue: Int - + internal init(_ rawValue: Int) { self.rawValue = rawValue } - + public static func + (lhs: Step, rhs: Step) -> Step { return .init(lhs.rawValue + rhs.rawValue) } - + public static func - (lhs: Step, rhs: Step) -> Step { .init(lhs.rawValue - rhs.rawValue) } - + public static func / (lhs: Step, rhs: Int) -> Step { .init(lhs.rawValue / rhs) } - + public static func * (lhs: Step, rhs: Int) -> Step { .init(lhs.rawValue * rhs) } - + public static func / (lhs: Step, rhs: Step) -> Double { Double(lhs.rawValue) / Double(rhs.rawValue) } - + public static func < (lhs: Step, rhs: Step) -> Bool { lhs.rawValue < rhs.rawValue } - + public static var zero: Step { .init(0) } - + public static func steps(_ amount: Int) -> Step { return Step(amount) } - + public var description: String { return "step \(rawValue)" } } - + public struct Instant: CustomStringConvertible { public typealias Duration = Step - + let when: Step - + public func advanced(by duration: Step) -> Instant { Instant(when: when + duration) } - + public func duration(to other: Instant) -> Step { other.when - when } - + public static func < (lhs: Instant, rhs: Instant) -> Bool { lhs.when < rhs.when } - + public var description: String { // the raw value is 1 indexed in execution but we should report it as 0 indexed return "tick \(when.rawValue - 1)" } } - + public var now: Instant { queue.now } - + public var minimumResolution: Step { .steps(1) } - + public func sleep( until deadline: Instant, tolerance: Step? = nil @@ -115,7 +114,12 @@ extension AsyncSequenceValidationDiagram.Clock { let token = queue.prepare() try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { continuation in - queue.enqueue(AsyncSequenceValidationDiagram.Context.currentJob, deadline: deadline, continuation: continuation, token: token) + queue.enqueue( + AsyncSequenceValidationDiagram.Context.currentJob, + deadline: deadline, + continuation: continuation, + token: token + ) } } onCancel: { queue.cancel(token) @@ -123,16 +127,16 @@ extension AsyncSequenceValidationDiagram.Clock { } } -extension AsyncSequenceValidationDiagram.Clock.Instant: TestInstant { } +extension AsyncSequenceValidationDiagram.Clock.Instant: TestInstant {} @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncSequenceValidationDiagram.Clock.Instant: InstantProtocol { } +extension AsyncSequenceValidationDiagram.Clock.Instant: InstantProtocol {} -extension AsyncSequenceValidationDiagram.Clock: TestClock { } +extension AsyncSequenceValidationDiagram.Clock: TestClock {} @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncSequenceValidationDiagram.Clock: Clock { } +extension AsyncSequenceValidationDiagram.Clock: Clock {} // placeholders to avoid warnings -extension AsyncSequenceValidationDiagram.Clock.Instant: Hashable { } -extension AsyncSequenceValidationDiagram.Clock.Instant: Comparable { } +extension AsyncSequenceValidationDiagram.Clock.Instant: Hashable {} +extension AsyncSequenceValidationDiagram.Clock.Instant: Comparable {} diff --git a/Sources/AsyncSequenceValidation/Event.swift b/Sources/AsyncSequenceValidation/Event.swift index 70101475..a0fe887a 100644 --- a/Sources/AsyncSequenceValidation/Event.swift +++ b/Sources/AsyncSequenceValidation/Event.swift @@ -10,13 +10,13 @@ //===----------------------------------------------------------------------===// extension AsyncSequenceValidationDiagram { - struct Failure: Error, Equatable { } - + struct Failure: Error, Equatable {} + enum ParseFailure: Error, CustomStringConvertible, SourceFailure { case stepInGroup(String, String.Index, SourceLocation) case nestedGroup(String, String.Index, SourceLocation) case unbalancedNesting(String, String.Index, SourceLocation) - + var location: SourceLocation { switch self { case .stepInGroup(_, _, let location): return location @@ -24,7 +24,7 @@ extension AsyncSequenceValidationDiagram { case .unbalancedNesting(_, _, let location): return location } } - + var description: String { switch self { case .stepInGroup: @@ -36,14 +36,14 @@ extension AsyncSequenceValidationDiagram { } } } - + enum Event { case value(String, String.Index) case failure(Error, String.Index) case finish(String.Index) case delayNext(String.Index) case cancel(String.Index) - + var results: [Result] { switch self { case .value(let value, _): return [.success(value)] @@ -53,7 +53,7 @@ extension AsyncSequenceValidationDiagram { case .cancel: return [] } } - + var index: String.Index { switch self { case .value(_, let index): return index @@ -63,23 +63,26 @@ extension AsyncSequenceValidationDiagram { case .cancel(let index): return index } } - - static func parse(_ dsl: String, theme: Theme, location: SourceLocation) throws -> [(Clock.Instant, Event)] { + + static func parse( + _ dsl: String, + theme: Theme, + location: SourceLocation + ) throws -> [(Clock.Instant, Event)] { var emissions = [(Clock.Instant, Event)]() var when = Clock.Instant(when: .steps(0)) var string: String? var grouping = 0 - + for index in dsl.indices { let ch = dsl[index] switch theme.token(dsl[index], inValue: string != nil) { case .step: if string == nil { - if grouping == 0 { - when = when.advanced(by: .steps(1)) - } else { + guard grouping == 0 else { throw ParseFailure.stepInGroup(dsl, index, location) } + when = when.advanced(by: .steps(1)) } else { string?.append(ch) } @@ -130,11 +133,10 @@ extension AsyncSequenceValidationDiagram { emissions.append((when, .value(value, index))) } case .beginGroup: - if grouping == 0 { - when = when.advanced(by: .steps(1)) - } else { + guard grouping == 0 else { throw ParseFailure.nestedGroup(dsl, index, location) } + when = when.advanced(by: .steps(1)) grouping += 1 case .endGroup: grouping -= 1 diff --git a/Sources/AsyncSequenceValidation/Expectation.swift b/Sources/AsyncSequenceValidation/Expectation.swift index 63121d66..89040ef2 100644 --- a/Sources/AsyncSequenceValidation/Expectation.swift +++ b/Sources/AsyncSequenceValidation/Expectation.swift @@ -18,7 +18,7 @@ extension AsyncSequenceValidationDiagram { } public var expected: [Event] public var actual: [(Clock.Instant, Result)] - + func reconstitute(_ result: Result, theme: Theme) -> String { var reconstituted = "" switch result { @@ -39,9 +39,13 @@ extension AsyncSequenceValidationDiagram { } return reconstituted } - - func reconstitute(_ events: [Clock.Instant : [Result]], theme: Theme, end: Clock.Instant) -> String { - var now = Clock.Instant(when: .steps(1)) // adjust for the offset index + + func reconstitute( + _ events: [Clock.Instant: [Result]], + theme: Theme, + end: Clock.Instant + ) -> String { + var now = Clock.Instant(when: .steps(1)) // adjust for the offset index var reconstituted = "" while now <= end { if let results = events[now] { @@ -61,11 +65,11 @@ extension AsyncSequenceValidationDiagram { } return reconstituted } - + public func reconstituteExpected(theme: Theme) -> String { - var events = [Clock.Instant : [Result]]() + var events = [Clock.Instant: [Result]]() var end: Clock.Instant = Clock.Instant(when: .zero) - + for expectation in expected { let when = expectation.when let result = expectation.result @@ -74,25 +78,25 @@ extension AsyncSequenceValidationDiagram { end = when } } - + return reconstitute(events, theme: theme, end: end) } - + public func reconstituteActual(theme: Theme) -> String { - var events = [Clock.Instant : [Result]]() + var events = [Clock.Instant: [Result]]() var end: Clock.Instant = Clock.Instant(when: .zero) - + for (when, result) in actual { events[when, default: []].append(result) if when > end { end = when } } - + return reconstitute(events, theme: theme, end: end) } } - + public struct ExpectationFailure: Sendable, CustomStringConvertible { public enum Kind: Sendable { case expectedFinishButGotValue(String) @@ -108,23 +112,23 @@ extension AsyncSequenceValidationDiagram { case unexpectedValue(String) case unexpectedFinish case unexpectedFailure(Error) - + case specificationViolationGotValueAfterIteration(String) case specificationViolationGotFailureAfterIteration(Error) } public var when: Clock.Instant public var kind: Kind - + public var specification: Specification? public var index: String.Index? - + init(when: Clock.Instant, kind: Kind, specification: Specification? = nil, index: String.Index? = nil) { self.when = when self.kind = kind self.specification = specification self.index = index } - + var reason: String { switch kind { case .expectedFinishButGotValue(let actual): @@ -159,7 +163,7 @@ extension AsyncSequenceValidationDiagram { return "specification violation got failure after iteration terminated" } } - + public var description: String { return reason + " at tick \(when.when.rawValue - 1)" } diff --git a/Sources/AsyncSequenceValidation/Input.swift b/Sources/AsyncSequenceValidation/Input.swift index 26e23da6..ad587751 100644 --- a/Sources/AsyncSequenceValidation/Input.swift +++ b/Sources/AsyncSequenceValidation/Input.swift @@ -13,31 +13,31 @@ extension AsyncSequenceValidationDiagram { public struct Specification: Sendable { public let specification: String public let location: SourceLocation - + init(specification: String, location: SourceLocation) { self.specification = specification self.location = location } } - + public struct Input: AsyncSequence, Sendable { public typealias Element = String - + struct State { var emissions = [(Clock.Instant, Event)]() } - + let state = ManagedCriticalState(State()) let queue: WorkQueue let index: Int - + public struct Iterator: AsyncIteratorProtocol, Sendable { let state: ManagedCriticalState let queue: WorkQueue let index: Int var active: (Clock.Instant, [Result])? var eventIndex = 0 - + mutating func apply(when: Clock.Instant, results: [Result]) async throws -> Element? { let token = queue.prepare() if eventIndex + 1 >= results.count { @@ -52,17 +52,22 @@ extension AsyncSequenceValidationDiagram { } return try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { continuation in - queue.enqueue(Context.currentJob, deadline: when, continuation: continuation, results[eventIndex], index: index, token: token) + queue.enqueue( + Context.currentJob, + deadline: when, + continuation: continuation, + results[eventIndex], + index: index, + token: token + ) } } onCancel: { [queue] in queue.cancel(token) } } - + public mutating func next() async throws -> Element? { - if let (when, results) = active { - return try await apply(when: when, results: results) - } else { + guard let (when, results) = active else { let next = state.withCriticalRegion { state -> (Clock.Instant, Event)? in guard state.emissions.count > 0 else { return nil @@ -77,36 +82,37 @@ extension AsyncSequenceValidationDiagram { active = (when, results) return try await apply(when: when, results: results) } + return try await apply(when: when, results: results) } } - + public func makeAsyncIterator() -> Iterator { Iterator(state: state, queue: queue, index: index) } - + func parse(_ dsl: String, theme: Theme, location: SourceLocation) throws { let emissions = try Event.parse(dsl, theme: theme, location: location) state.withCriticalRegion { state in state.emissions = emissions } } - + var end: Clock.Instant? { return state.withCriticalRegion { state in state.emissions.map { $0.0 }.sorted().last } } } - + public struct InputList: RandomAccessCollection, Sendable { let state = ManagedCriticalState([Input]()) let queue: WorkQueue - + public var startIndex: Int { return 0 } public var endIndex: Int { state.withCriticalRegion { $0.count } } - + public subscript(position: Int) -> AsyncSequenceValidationDiagram.Input { get { return state.withCriticalRegion { state in diff --git a/Sources/AsyncSequenceValidation/Job.swift b/Sources/AsyncSequenceValidation/Job.swift index 461af50d..0a081870 100644 --- a/Sources/AsyncSequenceValidation/Job.swift +++ b/Sources/AsyncSequenceValidation/Job.swift @@ -13,11 +13,11 @@ import _CAsyncSequenceValidationSupport struct Job: Hashable, @unchecked Sendable { let job: JobRef - + init(_ job: JobRef) { self.job = job } - + func execute() { _swiftJobRun(unsafeBitCast(job, to: UnownedJob.self), AsyncSequenceValidationDiagram.Context.unownedExecutor) } diff --git a/Sources/AsyncSequenceValidation/SourceLocation.swift b/Sources/AsyncSequenceValidation/SourceLocation.swift index c0107cc3..90b5fc0f 100644 --- a/Sources/AsyncSequenceValidation/SourceLocation.swift +++ b/Sources/AsyncSequenceValidation/SourceLocation.swift @@ -12,12 +12,12 @@ public struct SourceLocation: Sendable, CustomStringConvertible { public var file: StaticString public var line: UInt - + public init(file: StaticString, line: UInt) { self.file = file self.line = line } - + public var description: String { return "\(file):\(line)" } diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 50ed45ff..80ad44cd 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -47,45 +47,49 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { final class TaskDriver { let work: (TaskDriver) -> Void let queue: WorkQueue -#if canImport(Darwin) + #if canImport(Darwin) var thread: pthread_t? -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) var thread = pthread_t() -#elseif canImport(WinSDK) -#error("TODO: Port TaskDriver threading to windows") -#endif - + #elseif canImport(WinSDK) + #error("TODO: Port TaskDriver threading to windows") + #endif + init(queue: WorkQueue, _ work: @escaping (TaskDriver) -> Void) { self.queue = queue self.work = work } - + func start() { -#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) - pthread_create(&thread, nil, start_thread, - Unmanaged.passRetained(self).toOpaque()) -#elseif canImport(WinSDK) -#error("TODO: Port TaskDriver threading to windows") -#endif + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) + pthread_create( + &thread, + nil, + start_thread, + Unmanaged.passRetained(self).toOpaque() + ) + #elseif canImport(WinSDK) + #error("TODO: Port TaskDriver threading to windows") + #endif } - + func run() { -#if canImport(Darwin) + #if canImport(Darwin) pthread_setname_np("Validation Diagram Clock Driver") -#endif + #endif work(self) } - + func join() { -#if canImport(Darwin) + #if canImport(Darwin) pthread_join(thread!, nil) -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_join(thread, nil) -#elseif canImport(WinSDK) -#error("TODO: Port TaskDriver threading to windows") -#endif + #elseif canImport(WinSDK) + #error("TODO: Port TaskDriver threading to windows") + #endif } - + func enqueue(_ job: JobRef) { let job = Job(job) queue.enqueue(AsyncSequenceValidationDiagram.Context.currentJob) { @@ -96,4 +100,3 @@ final class TaskDriver { } } } - diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index 8dc86832..b19f6e5a 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -17,47 +17,59 @@ import AsyncAlgorithms internal func _swiftJobRun( _ job: UnownedJob, _ executor: UnownedSerialExecutor -) -> () +) public protocol AsyncSequenceValidationTest: Sendable { var inputs: [AsyncSequenceValidationDiagram.Specification] { get } var output: AsyncSequenceValidationDiagram.Specification { get } - - func test(with clock: C, activeTicks: [C.Instant], output: AsyncSequenceValidationDiagram.Specification, _ event: (String) -> Void) async throws + + func test( + with clock: C, + activeTicks: [C.Instant], + output: AsyncSequenceValidationDiagram.Specification, + _ event: (String) -> Void + ) async throws } extension AsyncSequenceValidationDiagram { - struct Test: AsyncSequenceValidationTest, @unchecked Sendable where Operation.Element == String { + struct Test: AsyncSequenceValidationTest, @unchecked Sendable + where Operation.Element == String { let inputs: [Specification] let sequence: Operation let output: Specification - - func test(with clock: C, activeTicks: [C.Instant], output: Specification, _ event: (String) -> Void) async throws { + + func test( + with clock: C, + activeTicks: [C.Instant], + output: Specification, + _ event: (String) -> Void + ) async throws { var iterator = sequence.makeAsyncIterator() do { for tick in activeTicks { if tick != clock.now { try await clock.sleep(until: tick, tolerance: nil) } - if let item = try await iterator.next() { - event(item) - } else { + guard let item = try await iterator.next() else { break } + event(item) } do { - if let pastEnd = try await iterator.next(){ + if let pastEnd = try await iterator.next() { let failure = ExpectationFailure( when: Context.clock!.now, kind: .specificationViolationGotValueAfterIteration(pastEnd), - specification: output) + specification: output + ) Context.specificationFailures.append(failure) } } catch { let failure = ExpectationFailure( when: Context.clock!.now, kind: .specificationViolationGotFailureAfterIteration(error), - specification: output) + specification: output + ) Context.specificationFailures.append(failure) } } catch { @@ -65,9 +77,9 @@ extension AsyncSequenceValidationDiagram { } } } - + struct Context { -#if swift(<5.9) + #if swift(<5.9) final class ClockExecutor: SerialExecutor { func enqueue(_ job: UnownedJob) { job._runSynchronously(on: self.asUnownedSerialExecutor()) @@ -77,24 +89,24 @@ extension AsyncSequenceValidationDiagram { UnownedSerialExecutor(ordinary: self) } } - + private static let _executor = ClockExecutor() - + static var unownedExecutor: UnownedSerialExecutor { _executor.asUnownedSerialExecutor() } -#else + #else @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) - final class ClockExecutor_5_9: SerialExecutor { + final class ClockExecutor_5_9: SerialExecutor { func enqueue(_ job: __owned ExecutorJob) { job.runSynchronously(on: asUnownedSerialExecutor()) } - + 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") @@ -106,36 +118,33 @@ extension AsyncSequenceValidationDiagram { 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 { + guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else { return ClockExecutor_Pre5_9() } + return ClockExecutor_5_9() }() - + static var unownedExecutor: UnownedSerialExecutor { (_executor as! any SerialExecutor).asUnownedSerialExecutor() } -#endif + #endif static var clock: Clock? - - - + static var driver: TaskDriver? - + static var currentJob: Job? - + static var specificationFailures = [ExpectationFailure]() } - + enum ActualResult { case success(String?) case failure(Error) case none - + init(_ result: Result?) { if let result = result { switch result { @@ -149,7 +158,7 @@ extension AsyncSequenceValidationDiagram { } } } - + static func validate( inputs: [Specification], output: Specification, @@ -160,21 +169,21 @@ extension AsyncSequenceValidationDiagram { let result = ExpectationResult(expected: expected, actual: actual) var failures = Context.specificationFailures Context.specificationFailures.removeAll() - + let actualTimes = actual.map { when, _ in when } let expectedTimes = expected.map { $0.when } - + var expectedMap = [Clock.Instant: [ExpectationResult.Event]]() var actualMap = [Clock.Instant: [Result]]() - + for event in expected { expectedMap[event.when, default: []].append(event) } - + for (when, result) in actual { actualMap[when, default: []].append(result) } - + let allTimes = Set(actualTimes + expectedTimes).sorted() for when in allTimes { let expectedResults = expectedMap[when] ?? [] @@ -192,21 +201,24 @@ extension AsyncSequenceValidationDiagram { when: when, kind: .expectedMismatch(expected, actual), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } case (.none, .some(let actual)): let failure = ExpectationFailure( when: when, kind: .expectedFinishButGotValue(actual), - specification: output) + specification: output + ) failures.append(failure) case (.some(let expected), .none): let failure = ExpectationFailure( when: when, kind: .expectedValueButGotFinished(expected), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) case (.none, .none): break @@ -217,14 +229,16 @@ extension AsyncSequenceValidationDiagram { when: when, kind: .expectedValueButGotFailure(expected, actual), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } else { let failure = ExpectationFailure( when: when, kind: .expectedFinishButGotFailure(actual), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } case (.success(let expected), .none): @@ -234,14 +248,16 @@ extension AsyncSequenceValidationDiagram { when: when, kind: .expectedValue(expected), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) case .none: let failure = ExpectationFailure( when: when, kind: .expectedFinish, specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } case (.failure(let expected), .success(let actual)): @@ -250,14 +266,16 @@ extension AsyncSequenceValidationDiagram { when: when, kind: .expectedFailureButGotValue(expected, actual), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } else { let failure = ExpectationFailure( when: when, kind: .expectedFailureButGotFinish(expected), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } case (.failure, .failure): @@ -267,7 +285,8 @@ extension AsyncSequenceValidationDiagram { when: when, kind: .expectedFailure(expected), specification: output, - index: expectedEvent.offset) + index: expectedEvent.offset + ) failures.append(failure) } } @@ -279,28 +298,31 @@ extension AsyncSequenceValidationDiagram { let failure = ExpectationFailure( when: when, kind: .unexpectedValue(actual), - specification: output) + specification: output + ) failures.append(failure) case .none: let failure = ExpectationFailure( when: when, kind: .unexpectedFinish, - specification: output) + specification: output + ) failures.append(failure) } case .failure(let actual): let failure = ExpectationFailure( when: when, kind: .unexpectedFailure(actual), - specification: output) + specification: output + ) failures.append(failure) } } } - + return (result, failures) } - + public static func test( theme: Theme, @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test @@ -312,30 +334,32 @@ extension AsyncSequenceValidationDiagram { // fault in all inputs _ = diagram.inputs[index] } - + for (index, input) in diagram.inputs.enumerated() { let inputSpecification = test.inputs[index] try input.parse(inputSpecification.specification, theme: theme, location: inputSpecification.location) } - + let parsedOutput = try Event.parse(test.output.specification, theme: theme, location: test.output.location) - let cancelEvents = Set(parsedOutput.filter { when, event in - switch event { - case .cancel: return true - default: return false - } - }.map { when, _ in return when }) + let cancelEvents = Set( + parsedOutput.filter { when, event in + switch event { + case .cancel: return true + default: return false + } + }.map { when, _ in return when } + ) let activeTicks = parsedOutput.reduce(into: [Clock.Instant.init(when: .zero)]) { events, thisEvent in switch thisEvent { - case (let when, .delayNext(_)): - events.removeLast() - events.append(when.advanced(by: .steps(1))) - case (let when, _): - events.append(when) + case (let when, .delayNext(_)): + events.removeLast() + events.append(when.advanced(by: .steps(1))) + case (let when, _): + events.append(when) } } - + var expected = [ExpectationResult.Event]() for (when, event) in parsedOutput { for result in event.results { @@ -343,7 +367,7 @@ extension AsyncSequenceValidationDiagram { } } let times = parsedOutput.map { when, _ in when } - + guard let end = (times + diagram.inputs.compactMap { $0.end }).max() else { return (ExpectationResult(expected: [], actual: []), []) } @@ -356,7 +380,7 @@ extension AsyncSequenceValidationDiagram { swift_task_enqueueGlobal_hook = { job, original in Context.driver?.enqueue(job) } - + let runner = Task { do { try await test.test(with: clock, activeTicks: activeTicks, output: test.output) { event in @@ -373,7 +397,7 @@ extension AsyncSequenceValidationDiagram { } } } - + // Drain off any initial work. Work may spawn additional work to be done. // If the driver ever becomes blocked on the clock, exit early out of that // drain, because the drain cant make any forward progress if it is blocked @@ -387,7 +411,7 @@ extension AsyncSequenceValidationDiagram { } diagram.queue.advance() } - + runner.cancel() Context.clock = nil swift_task_enqueueGlobal_hook = nil @@ -397,15 +421,16 @@ extension AsyncSequenceValidationDiagram { // else wise this would cause QoS inversions Context.driver?.join() Context.driver = nil - + return validate( inputs: test.inputs, output: test.output, theme: theme, expected: expected, - actual: actual.withCriticalRegion { $0 }) + actual: actual.withCriticalRegion { $0 } + ) } - + public static func test( @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test ) throws -> (ExpectationResult, [ExpectationFailure]) { diff --git a/Sources/AsyncSequenceValidation/Theme.swift b/Sources/AsyncSequenceValidation/Theme.swift index 19e80419..fc20eeea 100644 --- a/Sources/AsyncSequenceValidation/Theme.swift +++ b/Sources/AsyncSequenceValidation/Theme.swift @@ -11,7 +11,7 @@ public protocol AsyncSequenceValidationTheme { func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token - + func description(for token: AsyncSequenceValidationDiagram.Token) -> String } @@ -35,7 +35,7 @@ extension AsyncSequenceValidationDiagram { case skip case value(String) } - + public struct ASCIITheme: AsyncSequenceValidationTheme, Sendable { public func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token { switch character { @@ -51,7 +51,7 @@ extension AsyncSequenceValidationDiagram { default: return .value(String(character)) } } - + public func description(for token: AsyncSequenceValidationDiagram.Token) -> String { switch token { case .step: return "-" diff --git a/Sources/AsyncSequenceValidation/WorkQueue.swift b/Sources/AsyncSequenceValidation/WorkQueue.swift index 784e5e82..82d56e24 100644 --- a/Sources/AsyncSequenceValidation/WorkQueue.swift +++ b/Sources/AsyncSequenceValidation/WorkQueue.swift @@ -12,10 +12,16 @@ struct WorkQueue: Sendable { enum Item: CustomStringConvertible, Comparable { case blocked(Token, AsyncSequenceValidationDiagram.Clock.Instant, UnsafeContinuation) - case emit(Token, AsyncSequenceValidationDiagram.Clock.Instant, UnsafeContinuation, Result, Int) + case emit( + Token, + AsyncSequenceValidationDiagram.Clock.Instant, + UnsafeContinuation, + Result, + Int + ) case work(Token, @Sendable () -> Void) case cancelled(Token) - + func run() { switch self { case .blocked(_, _, let continuation): @@ -28,7 +34,7 @@ struct WorkQueue: Sendable { break } } - + var description: String { switch self { case .blocked(let token, let when, _): @@ -41,7 +47,7 @@ struct WorkQueue: Sendable { return "cancelled #\(token)" } } - + var token: Token { switch self { case .blocked(let token, _, _): return token @@ -50,14 +56,14 @@ struct WorkQueue: Sendable { case .cancelled(let token): return token } } - + var isCancelled: Bool { switch self { case .cancelled: return true default: return false } } - + func cancelling() -> Item { switch self { case .blocked(let token, _, let continuation): @@ -71,7 +77,7 @@ struct WorkQueue: Sendable { default: return self } } - + // the side order is repsected first since that is the logical flow of predictable events // then the generation is taken into account static func < (_ lhs: Item, _ rhs: Item) -> Bool { @@ -82,28 +88,28 @@ struct WorkQueue: Sendable { return lhs.token.generation < rhs.token.generation } } - + // all tokens are distinct so we know the generation of when it was enqueued // always means distinct equality (for ordering) static func == (_ lhs: Item, _ rhs: Item) -> Bool { return lhs.token == rhs.token } } - + struct State { // the nil Job in these two structures represent the root job in the TaskDriver - var queues = [Job? : [Item]]() + var queues = [Job?: [Item]]() var jobs: [Job?] = [nil] - var items = [Token : Item]() - + var items = [Token: Item]() + var now = AsyncSequenceValidationDiagram.Clock.Instant(when: .zero) var generation = 0 - + mutating func drain() -> [Item] { var items = [Item]() // store off the jobs such that we can only visit the active queues var jobs = self.jobs - + while true { let startingCount = items.count var jobsToRemove = Set() @@ -160,32 +166,32 @@ struct WorkQueue: Sendable { break } } - + return items } } - + let state = ManagedCriticalState(State()) - + var now: AsyncSequenceValidationDiagram.Clock.Instant { state.withCriticalRegion { $0.now } } - + struct Token: Hashable, CustomStringConvertible { var generation: Int - + var description: String { return generation.description } } - + func prepare() -> Token { state.withCriticalRegion { state in defer { state.generation += 1 } return Token(generation: state.generation) } } - + func cancel(_ token: Token) { state.withCriticalRegion { state in if let existing = state.items[token] { @@ -212,16 +218,24 @@ struct WorkQueue: Sendable { } } } - - func enqueue(_ job: Job?, deadline: AsyncSequenceValidationDiagram.Clock.Instant, continuation: UnsafeContinuation, token: Token) { + + func enqueue( + _ job: Job?, + deadline: AsyncSequenceValidationDiagram.Clock.Instant, + continuation: UnsafeContinuation, + token: Token + ) { state.withCriticalRegion { state in if state.queues[job] == nil, let job = job { state.jobs.append(job) } if state.items[token]?.isCancelled == true { - let item: Item = .work(token, { - continuation.resume(throwing: CancellationError()) - }) + let item: Item = .work( + token, + { + continuation.resume(throwing: CancellationError()) + } + ) state.queues[job, default: []].append(item) state.items[token] = item } else { @@ -231,16 +245,27 @@ struct WorkQueue: Sendable { } } } - - func enqueue(_ job: Job?, deadline: AsyncSequenceValidationDiagram.Clock.Instant, continuation: UnsafeContinuation, _ result: Result, index: Int, token: Token) { + + func enqueue( + _ job: Job?, + deadline: AsyncSequenceValidationDiagram.Clock.Instant, + continuation: UnsafeContinuation, + _ result: Result, + index: Int, + token: Token + ) { state.withCriticalRegion { state in if state.queues[job] == nil, let job = job { state.jobs.append(job) } if state.items[token]?.isCancelled == true { - let item: Item = .work(token, { - continuation.resume(returning: nil) // the input sequences should not throw cancellation errors - }) + let item: Item = .work( + token, + { + // the input sequences should not throw cancellation errors + continuation.resume(returning: nil) + } + ) state.queues[job, default: []].append(item) state.items[token] = item } else { @@ -250,7 +275,7 @@ struct WorkQueue: Sendable { } } } - + func enqueue(_ job: Job?, work: @Sendable @escaping () -> Void) { state.withCriticalRegion { state in if state.queues[job] == nil, let job = job { @@ -263,7 +288,7 @@ struct WorkQueue: Sendable { state.items[token] = item } } - + func drain() { // keep draining until there is no recursive work to do while true { @@ -279,7 +304,7 @@ struct WorkQueue: Sendable { } } } - + func advance() { // drain off the advancement var items: [Item] = state.withCriticalRegion { state in @@ -292,7 +317,7 @@ struct WorkQueue: Sendable { for item in items { item.run() } - + // and cleanup any additional recursive items drain() } diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index e51a4817..21c40d32 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -13,169 +13,169 @@ 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) - } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) + 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_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_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) } - - 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 = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + 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) } - - 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() - 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_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 } - - func test_interspersed_throwing_closure() async { - let source = [1, 2] - let expected = [1] - var actual = [Int]() - let sequence = source.async.interspersed(with: { 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) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) } - - 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) + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed(with: { throw Failure() }) + + 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_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) + 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() } - 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) + 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() - 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(()) + // Information the parent task that we are consuming + await lockStepChannel.send(()) - while let _ = await iterator.next() {} + while let _ = await iterator.next() {} - await lockStepChannel.send(()) - } + 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() - await group.waitForAll() - } + await group.waitForAll() } + } } diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index db223f3e..ef5d4cad 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -17,12 +17,12 @@ import XCTest public struct InfiniteAsyncSequence: AsyncSequence, Sendable { public typealias Element = Value let value: Value - - public struct AsyncIterator : AsyncIteratorProtocol, Sendable { - + + public struct AsyncIterator: AsyncIteratorProtocol, Sendable { + @usableFromInline let value: Value - + @inlinable public mutating func next() async throws -> Element? { guard !Task.isCancelled else { @@ -38,21 +38,33 @@ public struct InfiniteAsyncSequence: AsyncSequence, Sendable { final class _ThroughputMetric: NSObject, XCTMetric, @unchecked Sendable { var eventCount = 0 - - override init() { } - - func reportMeasurements(from startTime: XCTPerformanceMeasurementTimestamp, to endTime: XCTPerformanceMeasurementTimestamp) throws -> [XCTPerformanceMeasurement] { - return [XCTPerformanceMeasurement(identifier: "com.swift.AsyncAlgorithms.Throughput", displayName: "Throughput", doubleValue: Double(eventCount) / (endTime.date.timeIntervalSinceReferenceDate - startTime.date.timeIntervalSinceReferenceDate), unitSymbol: " Events/sec", polarity: .prefersLarger)] + + override init() {} + + func reportMeasurements( + from startTime: XCTPerformanceMeasurementTimestamp, + to endTime: XCTPerformanceMeasurementTimestamp + ) throws -> [XCTPerformanceMeasurement] { + return [ + XCTPerformanceMeasurement( + identifier: "com.swift.AsyncAlgorithms.Throughput", + displayName: "Throughput", + doubleValue: Double(eventCount) + / (endTime.date.timeIntervalSinceReferenceDate - startTime.date.timeIntervalSinceReferenceDate), + unitSymbol: " Events/sec", + polarity: .prefersLarger + ) + ] } - + func copy(with zone: NSZone? = nil) -> Any { return self } - + func willBeginMeasuring() { eventCount = 0 } - func didStopMeasuring() { } + func didStopMeasuring() {} } extension XCTestCase { @@ -85,7 +97,9 @@ extension XCTestCase { } } - public func measureThrowingChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { + public func measureThrowingChannelThroughput( + output: @Sendable @escaping @autoclosure () -> Output + ) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 @@ -114,14 +128,17 @@ extension XCTestCase { } } - public func measureSequenceThroughput( output: @autoclosure () -> Output, _ sequenceBuilder: (InfiniteAsyncSequence) -> S) async where S: Sendable { + public func measureSequenceThroughput( + output: @autoclosure () -> Output, + _ sequenceBuilder: (InfiniteAsyncSequence) -> S + ) async where S: Sendable { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 - + measure(metrics: [metric]) { let infSeq = InfiniteAsyncSequence(value: output()) let seq = sequenceBuilder(infSeq) - + let exp = self.expectation(description: "Finished") let iterTask = Task { var eventCount = 0 @@ -137,16 +154,20 @@ extension XCTestCase { self.wait(for: [exp], timeout: sampleTime * 2) } } - - public func measureSequenceThroughput(firstOutput: @autoclosure () -> Output, secondOutput: @autoclosure () -> Output, _ sequenceBuilder: (InfiniteAsyncSequence, InfiniteAsyncSequence) -> S) async where S: Sendable { + + public func measureSequenceThroughput( + firstOutput: @autoclosure () -> Output, + secondOutput: @autoclosure () -> Output, + _ sequenceBuilder: (InfiniteAsyncSequence, InfiniteAsyncSequence) -> S + ) async where S: Sendable { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 - + measure(metrics: [metric]) { let firstInfSeq = InfiniteAsyncSequence(value: firstOutput()) let secondInfSeq = InfiniteAsyncSequence(value: secondOutput()) let seq = sequenceBuilder(firstInfSeq, secondInfSeq) - + let exp = self.expectation(description: "Finished") let iterTask = Task { var eventCount = 0 @@ -162,17 +183,23 @@ extension XCTestCase { self.wait(for: [exp], timeout: sampleTime * 2) } } - - public func measureSequenceThroughput(firstOutput: @autoclosure () -> Output, secondOutput: @autoclosure () -> Output, thirdOutput: @autoclosure () -> Output, _ sequenceBuilder: (InfiniteAsyncSequence, InfiniteAsyncSequence, InfiniteAsyncSequence) -> S) async where S: Sendable { + + public func measureSequenceThroughput( + firstOutput: @autoclosure () -> Output, + secondOutput: @autoclosure () -> Output, + thirdOutput: @autoclosure () -> Output, + _ sequenceBuilder: (InfiniteAsyncSequence, InfiniteAsyncSequence, InfiniteAsyncSequence) + -> S + ) async where S: Sendable { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 - + measure(metrics: [metric]) { let firstInfSeq = InfiniteAsyncSequence(value: firstOutput()) let secondInfSeq = InfiniteAsyncSequence(value: secondOutput()) let thirdInfSeq = InfiniteAsyncSequence(value: thirdOutput()) let seq = sequenceBuilder(firstInfSeq, secondInfSeq, thirdInfSeq) - + let exp = self.expectation(description: "Finished") let iterTask = Task { var eventCount = 0 @@ -187,16 +214,19 @@ extension XCTestCase { iterTask.cancel() self.wait(for: [exp], timeout: sampleTime * 2) } -} - - public func measureSequenceThroughput( source: Source, _ sequenceBuilder: (Source) -> S) async where S: Sendable, Source: Sendable { + } + + public func measureSequenceThroughput( + source: Source, + _ sequenceBuilder: (Source) -> S + ) async where S: Sendable, Source: Sendable { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 - + measure(metrics: [metric]) { let infSeq = source let seq = sequenceBuilder(infSeq) - + let exp = self.expectation(description: "Finished") let iterTask = Task { var eventCount = 0 @@ -215,26 +245,26 @@ extension XCTestCase { } final class TestMeasurements: XCTestCase { - struct PassthroughSequence : AsyncSequence, Sendable where S : Sendable, S.AsyncIterator : Sendable { + struct PassthroughSequence: AsyncSequence, Sendable where S: Sendable, S.AsyncIterator: Sendable { typealias Element = S.Element - - struct AsyncIterator : AsyncIteratorProtocol, Sendable { - + + struct AsyncIterator: AsyncIteratorProtocol, Sendable { + @usableFromInline - var base : S.AsyncIterator - + var base: S.AsyncIterator + @inlinable mutating func next() async throws -> Element? { return try await base.next() } } - - let base : S + + let base: S func makeAsyncIterator() -> AsyncIterator { .init(base: base.makeAsyncIterator()) } } - + public func testThroughputTesting() async { await self.measureSequenceThroughput(output: 1) { PassthroughSequence(base: $0) diff --git a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift index d891cf91..778b62de 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift @@ -11,7 +11,7 @@ import XCTest -fileprivate enum _XCTAssertion { +private enum _XCTAssertion { case equal case equalWithAccuracy case identical @@ -30,9 +30,9 @@ fileprivate enum _XCTAssertion { case fail case throwsError case noThrow - + var name: String? { - switch(self) { + switch self { case .equal: return "XCTAssertEqual" case .equalWithAccuracy: return "XCTAssertEqual" case .identical: return "XCTAssertIdentical" @@ -55,18 +55,18 @@ fileprivate enum _XCTAssertion { } } -fileprivate enum _XCTAssertionResult { +private enum _XCTAssertionResult { case success case expectedFailure(String?) case unexpectedFailure(Swift.Error) - + var isExpected: Bool { switch self { case .unexpectedFailure(_): return false default: return true } } - + func failureDescription(_ assertion: _XCTAssertion) -> String { let explanation: String switch self { @@ -75,23 +75,28 @@ fileprivate enum _XCTAssertionResult { case .expectedFailure(_): explanation = "failed" case .unexpectedFailure(let error): explanation = "threw error \"\(error)\"" } - - if let name = assertion.name { - return "\(name) \(explanation)" - } else { + + guard let name = assertion.name else { return explanation } + return "\(name) \(explanation)" } } -private func _XCTEvaluateAssertion(_ assertion: _XCTAssertion, message: () -> String, file: StaticString, line: UInt, expression: () throws -> _XCTAssertionResult) { +private func _XCTEvaluateAssertion( + _ assertion: _XCTAssertion, + message: () -> String, + file: StaticString, + line: UInt, + expression: () throws -> _XCTAssertionResult +) { let result: _XCTAssertionResult do { result = try expression() } catch { result = .unexpectedFailure(error) } - + switch result { case .success: return @@ -100,22 +105,34 @@ private func _XCTEvaluateAssertion(_ assertion: _XCTAssertion, message: () -> St } } -fileprivate func _XCTAssertEqual(_ expression1: () throws -> T, _ expression2: () throws -> T, _ equal: (T, T) -> Bool, _ message: () -> String, file: StaticString = #filePath, line: UInt = #line) { +private func _XCTAssertEqual( + _ expression1: () throws -> T, + _ expression2: () throws -> T, + _ equal: (T, T) -> Bool, + _ message: () -> String, + file: StaticString = #filePath, + line: UInt = #line +) { _XCTEvaluateAssertion(.equal, message: message, file: file, line: line) { let (value1, value2) = (try expression1(), try expression2()) - if equal(value1, value2) { - return .success - } else { + guard equal(value1, value2) else { return .expectedFailure("(\"\(value1)\") is not equal to (\"\(value2)\")") } + return .success } } -public func XCTAssertEqual(_ expression1: @autoclosure () throws -> (A, B), _ expression2: @autoclosure () throws -> (A, B), _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { +public func XCTAssertEqual( + _ expression1: @autoclosure () throws -> (A, B), + _ expression2: @autoclosure () throws -> (A, B), + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) { _XCTAssertEqual(expression1, expression2, { $0 == $1 }, message, file: file, line: line) } -fileprivate func ==(_ lhs: [(A, B)], _ rhs: [(A, B)]) -> Bool { +private func == (_ lhs: [(A, B)], _ rhs: [(A, B)]) -> Bool { guard lhs.count == rhs.count else { return false } @@ -127,15 +144,27 @@ fileprivate func ==(_ lhs: [(A, B)], _ rhs: [(A, B)] return true } -public func XCTAssertEqual(_ expression1: @autoclosure () throws -> [(A, B)], _ expression2: @autoclosure () throws -> [(A, B)], _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { +public func XCTAssertEqual( + _ expression1: @autoclosure () throws -> [(A, B)], + _ expression2: @autoclosure () throws -> [(A, B)], + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) { _XCTAssertEqual(expression1, expression2, { $0 == $1 }, message, file: file, line: line) } -public func XCTAssertEqual(_ expression1: @autoclosure () throws -> (A, B, C), _ expression2: @autoclosure () throws -> (A, B, C), _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { +public func XCTAssertEqual( + _ expression1: @autoclosure () throws -> (A, B, C), + _ expression2: @autoclosure () throws -> (A, B, C), + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) { _XCTAssertEqual(expression1, expression2, { $0 == $1 }, message, file: file, line: line) } -fileprivate func ==(_ lhs: [(A, B, C)], _ rhs: [(A, B, C)]) -> Bool { +private func == (_ lhs: [(A, B, C)], _ rhs: [(A, B, C)]) -> Bool { guard lhs.count == rhs.count else { return false } @@ -147,48 +176,58 @@ fileprivate func ==(_ lhs: [(A, B, C)] return true } -public func XCTAssertEqual(_ expression1: @autoclosure () throws -> [(A, B, C)], _ expression2: @autoclosure () throws -> [(A, B, C)], _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { +public func XCTAssertEqual( + _ expression1: @autoclosure () throws -> [(A, B, C)], + _ expression2: @autoclosure () throws -> [(A, B, C)], + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) { _XCTAssertEqual(expression1, expression2, { $0 == $1 }, message, file: file, line: line) } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) internal func XCTAssertThrowsError( - _ expression: @autoclosure () async throws -> T, - file: StaticString = #file, - line: UInt = #line, - verify: (Error) -> Void = { _ in } + _ expression: @autoclosure () async throws -> T, + file: StaticString = #file, + line: UInt = #line, + verify: (Error) -> Void = { _ in } ) async { - do { - _ = try await expression() - XCTFail("Expression did not throw error", file: file, line: line) - } catch { - verify(error) - } + do { + _ = try await expression() + XCTFail("Expression did not throw error", file: file, line: line) + } catch { + 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) { + + 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 } @@ -200,7 +239,13 @@ class WaiterDelegate: NSObject, XCTWaiterDelegate { extension XCTestCase { @_disfavoredOverload - func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async { + 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) diff --git a/Tests/AsyncAlgorithmsTests/Support/Failure.swift b/Tests/AsyncAlgorithmsTests/Support/Failure.swift index 4a405af4..1eaebcfe 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Failure.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Failure.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -struct Failure: Error, Equatable { } +struct Failure: Error, Equatable {} func throwOn(_ toThrowOn: T, _ value: T) throws -> T { if value == toThrowOn { diff --git a/Tests/AsyncAlgorithmsTests/Support/Gate.swift b/Tests/AsyncAlgorithmsTests/Support/Gate.swift index 6bf4137c..cc772829 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Gate.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Gate.swift @@ -17,9 +17,9 @@ public struct Gate: Sendable { case open case pending(UnsafeContinuation) } - + let state = ManagedCriticalState(State.closed) - + public func `open`() { state.withCriticalRegion { state -> UnsafeContinuation? in switch state { @@ -34,7 +34,7 @@ public struct Gate: Sendable { } }?.resume() } - + public func enter() async { var other: UnsafeContinuation? await withUnsafeContinuation { (continuation: UnsafeContinuation) in @@ -45,7 +45,7 @@ public struct Gate: Sendable { return nil case .open: state = .closed - return continuation + return continuation case .pending(let existing): other = existing state = .pending(continuation) diff --git a/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift b/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift index 71c618b4..9167a4c1 100644 --- a/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift @@ -13,7 +13,7 @@ public struct GatedSequence { let elements: [Element] let gates: [Gate] var index = 0 - + public mutating func advance() { defer { index += 1 } guard index < gates.count else { @@ -21,7 +21,7 @@ public struct GatedSequence { } gates[index].open() } - + public init(_ elements: [Element]) { self.elements = elements self.gates = elements.map { _ in Gate() } @@ -31,11 +31,11 @@ public struct GatedSequence { extension GatedSequence: AsyncSequence { public struct Iterator: AsyncIteratorProtocol { var gatedElements: [(Element, Gate)] - + init(elements: [Element], gates: [Gate]) { gatedElements = Array(zip(elements, gates)) } - + public mutating func next() async -> Element? { guard gatedElements.count > 0 else { return nil @@ -45,10 +45,10 @@ extension GatedSequence: AsyncSequence { return element } } - + public func makeAsyncIterator() -> Iterator { Iterator(elements: elements, gates: gates) } } -extension GatedSequence: Sendable where Element: Sendable { } +extension GatedSequence: Sendable where Element: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/Support/Indefinite.swift b/Tests/AsyncAlgorithmsTests/Support/Indefinite.swift index 62beec78..82e73726 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Indefinite.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Indefinite.swift @@ -11,11 +11,11 @@ struct Indefinite: Sequence, IteratorProtocol, Sendable { let value: Element - + func next() -> Element? { return value } - + func makeIterator() -> Indefinite { self } diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift b/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift index 40ec8467..a497ce81 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift @@ -14,78 +14,78 @@ import AsyncAlgorithms public struct ManualClock: Clock { public struct Step: DurationProtocol { fileprivate var rawValue: Int - + fileprivate init(_ rawValue: Int) { self.rawValue = rawValue } - + public static func + (lhs: ManualClock.Step, rhs: ManualClock.Step) -> ManualClock.Step { return .init(lhs.rawValue + rhs.rawValue) } - + public static func - (lhs: ManualClock.Step, rhs: ManualClock.Step) -> ManualClock.Step { .init(lhs.rawValue - rhs.rawValue) } - + public static func / (lhs: ManualClock.Step, rhs: Int) -> ManualClock.Step { .init(lhs.rawValue / rhs) } - + public static func * (lhs: ManualClock.Step, rhs: Int) -> ManualClock.Step { .init(lhs.rawValue * rhs) } - + public static func / (lhs: ManualClock.Step, rhs: ManualClock.Step) -> Double { Double(lhs.rawValue) / Double(rhs.rawValue) } - + public static func < (lhs: ManualClock.Step, rhs: ManualClock.Step) -> Bool { lhs.rawValue < rhs.rawValue } - + public static var zero: ManualClock.Step { .init(0) } - + public static func steps(_ amount: Int) -> Step { return Step(amount) } } - + public struct Instant: InstantProtocol, CustomStringConvertible { public typealias Duration = Step - + internal let rawValue: Int - + internal init(_ rawValue: Int) { self.rawValue = rawValue } - + public static func < (lhs: ManualClock.Instant, rhs: ManualClock.Instant) -> Bool { return lhs.rawValue < rhs.rawValue } - + public func advanced(by duration: ManualClock.Step) -> ManualClock.Instant { .init(rawValue + duration.rawValue) } - + public func duration(to other: ManualClock.Instant) -> ManualClock.Step { .init(other.rawValue - rawValue) } - + public var description: String { return "tick \(rawValue)" } } - + fileprivate struct Wakeup { let generation: Int let continuation: UnsafeContinuation let deadline: Instant } - + fileprivate enum Scheduled: Hashable, Comparable, CustomStringConvertible { case cancelled(Int) case wakeup(Wakeup) - + func hash(into hasher: inout Hasher) { switch self { case .cancelled(let generation): @@ -94,14 +94,14 @@ public struct ManualClock: Clock { hasher.combine(wakeup.generation) } } - + var description: String { switch self { case .cancelled: return "Cancelled wakeup" case .wakeup(let wakeup): return "Wakeup at \(wakeup.deadline)" } } - + static func == (_ lhs: Scheduled, _ rhs: Scheduled) -> Bool { switch (lhs, rhs) { case (.cancelled(let lhsGen), .cancelled(let rhsGen)): @@ -114,7 +114,7 @@ public struct ManualClock: Clock { return lhs.generation == rhs.generation } } - + static func < (lhs: ManualClock.Scheduled, rhs: ManualClock.Scheduled) -> Bool { switch (lhs, rhs) { case (.cancelled(let lhsGen), .cancelled(let rhsGen)): @@ -127,14 +127,14 @@ public struct ManualClock: Clock { return lhs.generation < rhs.generation } } - + var deadline: Instant? { switch self { case .cancelled: return nil case .wakeup(let wakeup): return wakeup.deadline } } - + func resume() { switch self { case .wakeup(let wakeup): @@ -144,54 +144,52 @@ public struct ManualClock: Clock { } } } - + fileprivate struct State { var generation = 0 var scheduled = Set() var now = Instant(0) var hasSleepers = false } - + fileprivate let state = ManagedCriticalState(State()) - + public var now: Instant { state.withCriticalRegion { $0.now } } - + public var minimumResolution: Step { return .zero } - public init() { } - + public init() {} + fileprivate func cancel(_ generation: Int) { state.withCriticalRegion { state -> UnsafeContinuation? in - if let existing = state.scheduled.remove(.cancelled(generation)) { - switch existing { - case .wakeup(let wakeup): - return wakeup.continuation - default: - return nil - } - } else { + guard let existing = state.scheduled.remove(.cancelled(generation)) else { // insert the cancelled state for when it comes in to be scheduled as a wakeup state.scheduled.insert(.cancelled(generation)) return nil } + switch existing { + case .wakeup(let wakeup): + return wakeup.continuation + default: + return nil + } }?.resume(throwing: CancellationError()) } - + var hasSleepers: Bool { state.withCriticalRegion { $0.hasSleepers } } - + public func advance() { let pending = state.withCriticalRegion { state -> Set in state.now = state.now.advanced(by: .steps(1)) let pending = state.scheduled.filter { item in - if let deadline = item.deadline { - return deadline <= state.now - } else { + guard let deadline = item.deadline else { return false } + return deadline <= state.now } state.scheduled.subtract(pending) if pending.count > 0 { @@ -203,42 +201,40 @@ public struct ManualClock: Clock { item.resume() } } - + public func advance(by steps: Step) { for _ in 0.., deadline: Instant) { let resumption = state.withCriticalRegion { state -> (UnsafeContinuation, Result)? in let wakeup = Wakeup(generation: generation, continuation: continuation, deadline: deadline) - if let existing = state.scheduled.remove(.wakeup(wakeup)) { - switch existing { - case .wakeup: - fatalError() - case .cancelled: - // dont bother adding it back because it has been cancelled before we got here - return (continuation, .failure(CancellationError())) - } - } else { + guard let existing = state.scheduled.remove(.wakeup(wakeup)) else { // there is no cancelled placeholder so let it run free - if deadline > state.now { - // the deadline is in the future so run it then - state.hasSleepers = true - state.scheduled.insert(.wakeup(wakeup)) - return nil - } else { + guard deadline > state.now else { // the deadline is now or in the past so run it immediately return (continuation, .success(())) } + // the deadline is in the future so run it then + state.hasSleepers = true + state.scheduled.insert(.wakeup(wakeup)) + return nil + } + switch existing { + case .wakeup: + fatalError() + case .cancelled: + // dont bother adding it back because it has been cancelled before we got here + return (continuation, .failure(CancellationError())) } } if let resumption = resumption { resumption.0.resume(with: resumption.1) } } - + public func sleep(until deadline: Instant, tolerance: Step? = nil) async throws { let generation = state.withCriticalRegion { state -> Int in defer { state.generation += 1 } diff --git a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift index 1f351d69..57d81836 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift @@ -13,7 +13,7 @@ final class ReportingSequence: Sequence, IteratorProtocol { enum Event: Equatable, CustomStringConvertible { case next case makeIterator - + var description: String { switch self { case .next: return "next()" @@ -21,14 +21,14 @@ final class ReportingSequence: Sequence, IteratorProtocol { } } } - + var events = [Event]() var elements: [Element] init(_ elements: [Element]) { self.elements = elements } - + func next() -> Element? { events.append(.next) guard elements.count > 0 else { @@ -36,7 +36,7 @@ final class ReportingSequence: Sequence, IteratorProtocol { } return elements.removeFirst() } - + func makeIterator() -> ReportingSequence { events.append(.makeIterator) return self @@ -47,7 +47,7 @@ final class ReportingAsyncSequence: AsyncSequence, AsyncItera enum Event: Equatable, CustomStringConvertible { case next case makeAsyncIterator - + var description: String { switch self { case .next: return "next()" @@ -55,14 +55,14 @@ final class ReportingAsyncSequence: AsyncSequence, AsyncItera } } } - + var events = [Event]() var elements: [Element] init(_ elements: [Element]) { self.elements = elements } - + func next() async -> Element? { events.append(.next) guard elements.count > 0 else { @@ -70,7 +70,7 @@ final class ReportingAsyncSequence: AsyncSequence, AsyncItera } return elements.removeFirst() } - + func makeAsyncIterator() -> ReportingAsyncSequence { events.append(.makeAsyncIterator) return self diff --git a/Tests/AsyncAlgorithmsTests/Support/Validator.swift b/Tests/AsyncAlgorithmsTests/Support/Validator.swift index 56d6fda0..86e6fc24 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Validator.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Validator.swift @@ -17,17 +17,17 @@ public struct Validator: Sendable { case ready case pending(UnsafeContinuation) } - + private struct State: Sendable { var collected = [Element]() var failure: Error? var ready: Ready = .idle } - + private struct Envelope: @unchecked Sendable { var contents: Contents } - + private let state = ManagedCriticalState(State()) private func ready(_ apply: (inout State) -> Void) { @@ -45,7 +45,7 @@ public struct Validator: Sendable { } }?.resume() } - + internal func step() async { await withUnsafeContinuation { (continuation: UnsafeContinuation) in state.withCriticalRegion { state -> UnsafeContinuation? in @@ -64,17 +64,20 @@ public struct Validator: Sendable { } let onEvent: (@Sendable (Result) async -> Void)? - + init(onEvent: @Sendable @escaping (Result) async -> Void) { self.onEvent = onEvent } - + public init() { self.onEvent = nil } - - public func test(_ sequence: S, onFinish: @Sendable @escaping (inout S.AsyncIterator) async -> Void) where S.Element == Element { + + public func test( + _ sequence: S, + onFinish: @Sendable @escaping (inout S.AsyncIterator) async -> Void + ) where S.Element == Element { let envelope = Envelope(contents: sequence) Task { var iterator = envelope.contents.makeAsyncIterator() @@ -97,18 +100,18 @@ public struct Validator: Sendable { await onFinish(&iterator) } } - + public func validate() async -> [Element] { await step() return current } - + public var current: [Element] { return state.withCriticalRegion { state in return state.collected } } - + public var failure: Error? { return state.withCriticalRegion { state in return state.failure diff --git a/Tests/AsyncAlgorithmsTests/Support/ViolatingSequence.swift b/Tests/AsyncAlgorithmsTests/Support/ViolatingSequence.swift index 456c9eb9..71203068 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ViolatingSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ViolatingSequence.swift @@ -13,7 +13,7 @@ extension AsyncSequence { func violatingSpecification(returningPastEndIteration element: Element) -> SpecificationViolatingSequence { SpecificationViolatingSequence(self, kind: .producing(element)) } - + func violatingSpecification(throwingPastEndIteration error: Error) -> SpecificationViolatingSequence { SpecificationViolatingSequence(self, kind: .throwing(error)) } @@ -24,10 +24,10 @@ struct SpecificationViolatingSequence { case producing(Base.Element) case throwing(Error) } - + let base: Base let kind: Kind - + init(_ base: Base, kind: Kind) { self.base = base self.kind = kind @@ -36,13 +36,13 @@ struct SpecificationViolatingSequence { extension SpecificationViolatingSequence: AsyncSequence { typealias Element = Base.Element - + struct Iterator: AsyncIteratorProtocol { var iterator: Base.AsyncIterator let kind: Kind var finished = false var violated = false - + mutating func next() async throws -> Element? { if finished { if violated { @@ -66,7 +66,7 @@ extension SpecificationViolatingSequence: AsyncSequence { } } } - + func makeAsyncIterator() -> Iterator { Iterator(iterator: base.makeAsyncIterator(), kind: kind) } diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 4fea0ef5..9b6fdf3c 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -86,9 +86,9 @@ final class TestAdjacentPairs: XCTestCase { } finished.fulfill() } - + // ensure the other task actually starts - + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index 7bb45f89..83026953 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -172,7 +172,10 @@ final class TestBuffer: XCTestCase { } } - func test_given_a_buffered_with_unbounded_sequence_when_cancelling_consumer_then_the_iteration_finishes_and_the_base_is_cancelled() async { + func + test_given_a_buffered_with_unbounded_sequence_when_cancelling_consumer_then_the_iteration_finishes_and_the_base_is_cancelled() + async + { // Given let buffered = Indefinite(value: 1).async.buffer(policy: .unbounded) @@ -292,7 +295,10 @@ final class TestBuffer: XCTestCase { XCTAssertNil(pastFailure) } - func test_given_a_buffered_bounded_sequence_when_cancelling_consumer_then_the_iteration_finishes_and_the_base_is_cancelled() async { + func + test_given_a_buffered_bounded_sequence_when_cancelling_consumer_then_the_iteration_finishes_and_the_base_is_cancelled() + async + { // Given let buffered = Indefinite(value: 1).async.buffer(policy: .bounded(3)) diff --git a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift index 2c8841d8..7ddeafcb 100644 --- a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift +++ b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift @@ -15,16 +15,16 @@ import AsyncAlgorithms final class TestBufferedByteIterator: XCTestCase { actor Isolated { var value: T - + init(_ value: T) { self.value = value } - + func update(_ value: T) async { self.value = value } } - + func test_immediately_empty() async throws { let reloaded = Isolated(false) var iterator = AsyncBufferedByteIterator(capacity: 3) { buffer in @@ -39,7 +39,7 @@ final class TestBufferedByteIterator: XCTestCase { wasReloaded = await reloaded.value XCTAssertTrue(wasReloaded) } - + func test_one_pass() async throws { let reloaded = Isolated(0) var iterator = AsyncBufferedByteIterator(capacity: 3) { buffer in @@ -52,7 +52,7 @@ final class TestBufferedByteIterator: XCTestCase { buffer.copyBytes(from: [1, 2, 3]) return 3 } - + var reloadCount = await reloaded.value XCTAssertEqual(reloadCount, 0) var byte = try await iterator.next() @@ -76,7 +76,7 @@ final class TestBufferedByteIterator: XCTestCase { reloadCount = await reloaded.value XCTAssertEqual(reloadCount, 2) } - + func test_three_pass() async throws { let reloaded = Isolated(0) var iterator = AsyncBufferedByteIterator(capacity: 3) { buffer in @@ -89,10 +89,10 @@ final class TestBufferedByteIterator: XCTestCase { buffer.copyBytes(from: [1, 2, 3]) return 3 } - + var reloadCount = await reloaded.value XCTAssertEqual(reloadCount, 0) - + for n in 1...3 { var byte = try await iterator.next() XCTAssertEqual(byte, 1) @@ -107,8 +107,7 @@ final class TestBufferedByteIterator: XCTestCase { reloadCount = await reloaded.value XCTAssertEqual(reloadCount, n) } - - + var byte = try await iterator.next() XCTAssertNil(byte) reloadCount = await reloaded.value @@ -118,7 +117,7 @@ final class TestBufferedByteIterator: XCTestCase { reloadCount = await reloaded.value XCTAssertEqual(reloadCount, 4) } - + func test_three_pass_throwing() async throws { let reloaded = Isolated(0) var iterator = AsyncBufferedByteIterator(capacity: 3) { buffer in @@ -134,10 +133,10 @@ final class TestBufferedByteIterator: XCTestCase { buffer.copyBytes(from: [1, 2, 3]) return 3 } - + var reloadCount = await reloaded.value XCTAssertEqual(reloadCount, 0) - + for n in 1...3 { do { var byte = try await iterator.next() @@ -156,10 +155,9 @@ final class TestBufferedByteIterator: XCTestCase { XCTAssertEqual(n, 3) break } - + } - - + var byte = try await iterator.next() XCTAssertNil(byte) reloadCount = await reloaded.value @@ -169,11 +167,11 @@ final class TestBufferedByteIterator: XCTestCase { reloadCount = await reloaded.value XCTAssertEqual(reloadCount, 3) } - + func test_cancellation() async { struct RepeatingBytes: AsyncSequence { typealias Element = UInt8 - + func makeAsyncIterator() -> AsyncBufferedByteIterator { AsyncBufferedByteIterator(capacity: 3) { buffer in buffer.copyBytes(from: [1, 2, 3]) diff --git a/Tests/AsyncAlgorithmsTests/TestChain.swift b/Tests/AsyncAlgorithmsTests/TestChain.swift index 6bb2cb55..03513f64 100644 --- a/Tests/AsyncAlgorithmsTests/TestChain.swift +++ b/Tests/AsyncAlgorithmsTests/TestChain.swift @@ -29,7 +29,7 @@ final class TestChain2: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain2_outputs_elements_from_first_sequence_and_throws_when_first_throws() async throws { let chained = chain([1, 2, 3].async.map { try throwOn(3, $0) }, [4, 5, 6].async) var iterator = chained.makeAsyncIterator() @@ -48,7 +48,7 @@ final class TestChain2: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain2_outputs_elements_from_sequences_and_throws_when_second_throws() async throws { let chained = chain([1, 2, 3].async, [4, 5, 6].async.map { try throwOn(5, $0) }) var iterator = chained.makeAsyncIterator() @@ -67,7 +67,7 @@ final class TestChain2: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain2_finishes_when_task_is_cancelled() async { let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") @@ -115,7 +115,7 @@ final class TestChain3: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain3_outputs_elements_from_first_sequence_and_throws_when_first_throws() async throws { let chained = chain([1, 2, 3].async.map { try throwOn(3, $0) }, [4, 5, 6].async, [7, 8, 9].async) var iterator = chained.makeAsyncIterator() @@ -134,7 +134,7 @@ final class TestChain3: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain3_outputs_elements_from_sequences_and_throws_when_second_throws() async throws { let chained = chain([1, 2, 3].async, [4, 5, 6].async.map { try throwOn(5, $0) }, [7, 8, 9].async) var iterator = chained.makeAsyncIterator() @@ -153,7 +153,7 @@ final class TestChain3: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain3_outputs_elements_from_sequences_and_throws_when_third_throws() async throws { let chained = chain([1, 2, 3].async, [4, 5, 6].async, [7, 8, 9].async.map { try throwOn(8, $0) }) var iterator = chained.makeAsyncIterator() @@ -172,7 +172,7 @@ final class TestChain3: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_chain3_finishes_when_task_is_cancelled() async { let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 7845b1a0..8cd5e8e8 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -123,7 +123,9 @@ final class TestChunk: XCTestCase { } func test_time_equalChunks() 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") } + 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 { "ABC- DEF- GHI- |" $0.inputs[0].chunked(by: .repeating(every: .steps(4), clock: $0.clock)).map(concatCharacters) @@ -132,7 +134,9 @@ final class TestChunk: XCTestCase { } func test_time_unequalChunks() 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") } + 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 { "AB------ A------- ABCDEFG- |" $0.inputs[0].chunked(by: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -141,7 +145,9 @@ final class TestChunk: XCTestCase { } func test_time_emptyChunks() 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") } + 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 { "-- 1- --|" $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock)).map(concatCharacters) @@ -150,7 +156,9 @@ final class TestChunk: XCTestCase { } func test_time_error() 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") } + 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 { "AB^" $0.inputs[0].chunked(by: .repeating(every: .steps(5), clock: $0.clock)).map(concatCharacters) @@ -159,7 +167,9 @@ final class TestChunk: XCTestCase { } func test_time_unsignaledTrailingChunk() 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") } + 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 { "111-111|" $0.inputs[0].chunked(by: .repeating(every: .steps(4), clock: $0.clock)).map(sumCharacters) @@ -168,7 +178,9 @@ final class TestChunk: XCTestCase { } func test_timeAndCount_timeAlwaysPrevails() 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") } + 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 { "AB------ A------- ABCDEFG- |" $0.inputs[0].chunks(ofCount: 42, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -177,7 +189,9 @@ final class TestChunk: XCTestCase { } func test_timeAndCount_countAlwaysPrevails() 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") } + 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 { "AB --A-B -|" $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -186,7 +200,9 @@ final class TestChunk: XCTestCase { } func test_timeAndCount_countResetsAfterCount() 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") } + 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 { "ABCDE --- ABCDE |" $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -195,7 +211,9 @@ final class TestChunk: XCTestCase { } func test_timeAndCount_countResetsAfterSignal() 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") } + 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 { "AB------ ABCDE |" $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -204,7 +222,9 @@ final class TestChunk: XCTestCase { } func test_timeAndCount_error() 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") } + 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 { "ABC^" $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -279,7 +299,9 @@ final class TestChunk: XCTestCase { func test_projection() { validate { "A'Aa''ab' b'BB''bb' 'cc''CC' |" - $0.inputs[0].chunked(on: { $0.first!.lowercased() }).map { concatCharacters($0.1.map( {String($0.first!)} ) ) } + $0.inputs[0].chunked(on: { $0.first!.lowercased() }).map { + concatCharacters($0.1.map({ String($0.first!) })) + } "-- - 'AAa' - - 'bBb' - ['cC'|]" } } @@ -287,7 +309,9 @@ final class TestChunk: XCTestCase { func test_projection_singleValue() { validate { "A----|" - $0.inputs[0].chunked(on: { $0.first!.lowercased() }).map { concatCharacters($0.1.map( {String($0.first!)} ) ) } + $0.inputs[0].chunked(on: { $0.first!.lowercased() }).map { + concatCharacters($0.1.map({ String($0.first!) })) + } "-----[A|]" } } @@ -295,7 +319,7 @@ final class TestChunk: XCTestCase { func test_projection_singleGroup() { validate { "ABCDE|" - $0.inputs[0].chunked(on: { _ in 42 }).map { concatCharacters($0.1.map( {String($0.first!)} ) ) } + $0.inputs[0].chunked(on: { _ in 42 }).map { concatCharacters($0.1.map({ String($0.first!) })) } "-----['ABCDE'|]" } } @@ -303,7 +327,9 @@ final class TestChunk: XCTestCase { func test_projection_error() { validate { "Aa^" - $0.inputs[0].chunked(on: { $0.first!.lowercased() }).map { concatCharacters($0.1.map( {String($0.first!)} ) ) } + $0.inputs[0].chunked(on: { $0.first!.lowercased() }).map { + concatCharacters($0.1.map({ String($0.first!) })) + } "--^" } } diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index cb0f7324..93fe23c9 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -20,7 +20,7 @@ final class TestCombineLatest2: XCTestCase { let actual = await Array(sequence) XCTAssertGreaterThanOrEqual(actual.count, 3) } - + func test_throwing_combineLatest1() async { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -33,7 +33,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(error as? Failure, Failure()) } } - + func test_throwing_combineLatest2() async { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -46,7 +46,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(error as? Failure, Failure()) } } - + func test_ordering1() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -64,31 +64,31 @@ final class TestCombineLatest2: XCTestCase { value = validator.current XCTAssertEqual(value, []) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) - + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) } - + func test_ordering2() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -106,31 +106,31 @@ final class TestCombineLatest2: XCTestCase { value = validator.current XCTAssertEqual(value, []) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) - + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) } - + func test_ordering3() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -148,31 +148,31 @@ final class TestCombineLatest2: XCTestCase { value = validator.current XCTAssertEqual(value, []) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) - + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) } - + func test_ordering4() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -190,31 +190,31 @@ final class TestCombineLatest2: XCTestCase { value = validator.current XCTAssertEqual(value, []) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) - + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) } - + func test_throwing_ordering1() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -236,25 +236,25 @@ final class TestCombineLatest2: XCTestCase { value = validator.current XCTAssertEqual(value, []) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b")]) - + XCTAssertEqual(validator.failure as? Failure, Failure()) - + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b")]) } - + func test_throwing_ordering2() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -276,25 +276,25 @@ final class TestCombineLatest2: XCTestCase { value = validator.current XCTAssertEqual(value, []) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a")]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a")]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a")]) - + XCTAssertEqual(validator.failure as? Failure, Failure()) - + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a")]) } - + func test_cancellation() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") @@ -319,15 +319,15 @@ final class TestCombineLatest2: XCTestCase { await fulfillment(of: [finished], timeout: 1.0) } - func test_combineLatest_when_cancelled() async { - let t = Task { - try? await Task.sleep(nanoseconds: 1_000_000_000) - let c1 = Indefinite(value: "test1").async - let c2 = Indefinite(value: "test1").async - for await _ in combineLatest(c1, c2) {} - } - t.cancel() + func test_combineLatest_when_cancelled() async { + let t = Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) + let c1 = Indefinite(value: "test1").async + let c2 = Indefinite(value: "test1").async + for await _ in combineLatest(c1, c2) {} } + t.cancel() + } } final class TestCombineLatest3: XCTestCase { @@ -339,7 +339,7 @@ final class TestCombineLatest3: XCTestCase { let actual = await Array(sequence) XCTAssertGreaterThanOrEqual(actual.count, 3) } - + func test_ordering1() async { var a = GatedSequence([1, 2, 3]) var b = GatedSequence(["a", "b", "c"]) @@ -361,36 +361,42 @@ final class TestCombineLatest3: XCTestCase { value = validator.current XCTAssertEqual(value, []) c.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4)]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4)]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4)]) c.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5)]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5)]) b.advance() - + value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5)]) c.advance() - + 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)]) + XCTAssertEqual( + value, + [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)] + ) 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)]) + 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 3e2e5198..3cb64a06 100644 --- a/Tests/AsyncAlgorithmsTests/TestCompacted.swift +++ b/Tests/AsyncAlgorithmsTests/TestCompacted.swift @@ -23,7 +23,7 @@ final class TestCompacted: XCTestCase { } XCTAssertEqual(expected, actual) } - + func test_compacted_produces_nil_next_element_when_iteration_is_finished() async { let source = [1, 2, nil, 3, 4, nil, 5] let expected = source.compactMap { $0 } @@ -37,7 +37,7 @@ final class TestCompacted: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_compacted_is_equivalent_to_compactMap_when_input_as_no_nil_elements() async { let source: [Int?] = [1, 2, 3, 4, 5] let expected = source.compactMap { $0 } @@ -48,7 +48,7 @@ final class TestCompacted: XCTestCase { } XCTAssertEqual(expected, actual) } - + func test_compacted_throws_when_root_sequence_throws() async throws { let sequence = [1, nil, 3, 4, 5, nil, 7].async.map { try throwOn(4, $0) }.compacted() var iterator = sequence.makeAsyncIterator() @@ -65,7 +65,7 @@ final class TestCompacted: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_compacted_finishes_when_iteration_task_is_cancelled() async { let value: String? = "test" let source = Indefinite(value: value) diff --git a/Tests/AsyncAlgorithmsTests/TestDebounce.swift b/Tests/AsyncAlgorithmsTests/TestDebounce.swift index 5d296054..317f0bca 100644 --- a/Tests/AsyncAlgorithmsTests/TestDebounce.swift +++ b/Tests/AsyncAlgorithmsTests/TestDebounce.swift @@ -14,7 +14,9 @@ import AsyncAlgorithms final class TestDebounce: XCTestCase { func test_delayingValues() 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") } + 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 { "abcd----e---f-g----|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -23,7 +25,9 @@ final class TestDebounce: XCTestCase { } func test_delayingValues_dangling_last() 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") } + 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 { "abcd----e---f-g-|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -31,27 +35,32 @@ final class TestDebounce: XCTestCase { } } - func test_finishDoesntDebounce() 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") } + 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|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) "-[a|]" } } - + func test_throwDoesntDebounce() 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") } + 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^" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) "-^" } } - + func test_noValues() 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") } + 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 { "----|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -59,24 +68,28 @@ final class TestDebounce: XCTestCase { } } - func test_Rethrows() async 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") } + func test_Rethrows() async 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") + } - let debounce = [1].async.debounce(for: .zero, clock: ContinuousClock()) - for await _ in debounce {} + let debounce = [1].async.debounce(for: .zero, clock: ContinuousClock()) + for await _ in debounce {} - let throwingDebounce = [1].async.map { try throwOn(2, $0) }.debounce(for: .zero, clock: ContinuousClock()) - for try await _ in throwingDebounce {} - } + let throwingDebounce = [1].async.map { try throwOn(2, $0) }.debounce(for: .zero, clock: ContinuousClock()) + for try await _ in throwingDebounce {} + } func test_debounce_when_cancelled() async 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") } + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { + throw XCTSkip("Skipped due to Clock/Instant/Duration availability") + } - let t = Task { - try? await Task.sleep(nanoseconds: 1_000_000_000) - let c1 = Indefinite(value: "test1").async - for await _ in c1.debounce(for: .seconds(1), clock: .continuous) {} - } - t.cancel() + let t = Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) + let c1 = Indefinite(value: "test1").async + for await _ in c1.debounce(for: .seconds(1), clock: .continuous) {} + } + t.cancel() } } diff --git a/Tests/AsyncAlgorithmsTests/TestDictionary.swift b/Tests/AsyncAlgorithmsTests/TestDictionary.swift index 8f6f8318..9a3ab0fd 100644 --- a/Tests/AsyncAlgorithmsTests/TestDictionary.swift +++ b/Tests/AsyncAlgorithmsTests/TestDictionary.swift @@ -19,7 +19,7 @@ final class TestDictionary: XCTestCase { let actual = await Dictionary(uniqueKeysWithValues: source.async) XCTAssertEqual(expected, actual) } - + func test_throwing_uniqueKeysAndValues() async { let source = Array([1, 2, 3, 4, 5, 6]) let input = source.async.map { (value: Int) async throws -> (Int, Int) in @@ -33,14 +33,14 @@ final class TestDictionary: XCTestCase { XCTAssertEqual((error as NSError).code, -1) } } - + func test_uniquingWith() async { let source = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] let expected = Dictionary(source) { first, _ in first } let actual = await Dictionary(source.async) { first, _ in first } XCTAssertEqual(expected, actual) } - + func test_throwing_uniquingWith() async { let source = Array([1, 2, 3, 4, 5, 6]) let input = source.async.map { (value: Int) async throws -> (Int, Int) in @@ -54,16 +54,16 @@ final class TestDictionary: XCTestCase { XCTAssertEqual((error as NSError).code, -1) } } - + func test_grouping() async { let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] let expected = Dictionary(grouping: source, by: { $0.first! }) let actual = await Dictionary(grouping: source.async, by: { $0.first! }) XCTAssertEqual(expected, actual) } - + func test_throwing_grouping() async { - let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] + let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] let input = source.async.map { (value: String) async throws -> String in if value == "Kweku" { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } return value diff --git a/Tests/AsyncAlgorithmsTests/TestJoin.swift b/Tests/AsyncAlgorithmsTests/TestJoin.swift index c60b71da..b0b12d1b 100644 --- a/Tests/AsyncAlgorithmsTests/TestJoin.swift +++ b/Tests/AsyncAlgorithmsTests/TestJoin.swift @@ -13,8 +13,10 @@ import XCTest import AsyncAlgorithms extension Sequence where Element: Sequence, Element.Element: Equatable & Sendable { - func nestedAsync(throwsOn bad: Element.Element) -> AsyncSyncSequence<[AsyncThrowingMapSequence,Element.Element>]> { - let array: [AsyncThrowingMapSequence,Element.Element>] = self.map { $0.async }.map { + func nestedAsync( + throwsOn bad: Element.Element + ) -> AsyncSyncSequence<[AsyncThrowingMapSequence, Element.Element>]> { + let array: [AsyncThrowingMapSequence, Element.Element>] = self.map { $0.async }.map { $0.map { try throwOn(bad, $0) } } return array.async @@ -22,7 +24,7 @@ extension Sequence where Element: Sequence, Element.Element: Equatable & Sendabl } extension Sequence where Element: Sequence, Element.Element: Sendable { - var nestedAsync : AsyncSyncSequence<[AsyncSyncSequence]> { + var nestedAsync: AsyncSyncSequence<[AsyncSyncSequence]> { return self.map { $0.async }.async } } @@ -105,7 +107,7 @@ final class TestJoinedBySeparator: XCTestCase { } func test_cancellation() async { - let source : AsyncSyncSequence<[AsyncSyncSequence>]> = [Indefinite(value: "test").async].async + let source: AsyncSyncSequence<[AsyncSyncSequence>]> = [Indefinite(value: "test").async].async let sequence = source.joined(separator: ["past indefinite"].async) let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") @@ -189,7 +191,7 @@ final class TestJoined: XCTestCase { } func test_cancellation() async { - let source : AsyncSyncSequence<[AsyncSyncSequence>]> = [Indefinite(value: "test").async].async + let source: AsyncSyncSequence<[AsyncSyncSequence>]> = [Indefinite(value: "test").async].async let sequence = source.joined() let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") diff --git a/Tests/AsyncAlgorithmsTests/TestLazy.swift b/Tests/AsyncAlgorithmsTests/TestLazy.swift index 3ff8c5c1..c9baca3b 100644 --- a/Tests/AsyncAlgorithmsTests/TestLazy.swift +++ b/Tests/AsyncAlgorithmsTests/TestLazy.swift @@ -21,34 +21,34 @@ final class TestLazy: XCTestCase { for await item in sequence { collected.append(item) } - + XCTAssertEqual(expected, collected) } - + func test_lazy_outputs_elements_and_finishes_when_source_is_set() async { let expected: Set = [1, 2, 3, 4] let sequence = expected.async - + var collected = Set() for await item in sequence { collected.insert(item) } - + XCTAssertEqual(expected, collected) } - + func test_lazy_finishes_without_elements_when_source_is_empty() async { let expected = [Int]() let sequence = expected.async - + var collected = [Int]() for await item in sequence { collected.append(item) } - + XCTAssertEqual(expected, collected) } - + func test_lazy_triggers_expected_iterator_events_when_source_is_iterated() async { let expected = [1, 2, 3] let expectedEvents = [ @@ -56,7 +56,7 @@ final class TestLazy: XCTestCase { .next, .next, .next, - .next + .next, ] let source = ReportingSequence(expected) let sequence = source.async @@ -71,7 +71,7 @@ final class TestLazy: XCTestCase { XCTAssertEqual(expected, collected) XCTAssertEqual(expectedEvents, source.events) } - + func test_lazy_stops_triggering_iterator_events_when_source_is_pastEnd() async { let expected = [1, 2, 3] let expectedEvents = [ @@ -79,7 +79,7 @@ final class TestLazy: XCTestCase { .next, .next, .next, - .next + .next, ] let source = ReportingSequence(expected) let sequence = source.async @@ -101,7 +101,7 @@ final class TestLazy: XCTestCase { // ensure that iterating past the end does not invoke next again XCTAssertEqual(expectedEvents, source.events) } - + func test_lazy_finishes_when_task_is_cancelled() async { let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") diff --git a/Tests/AsyncAlgorithmsTests/TestManualClock.swift b/Tests/AsyncAlgorithmsTests/TestManualClock.swift index 15cc97bb..f27e55b8 100644 --- a/Tests/AsyncAlgorithmsTests/TestManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/TestManualClock.swift @@ -32,7 +32,7 @@ final class TestManualClock: XCTestCase { await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } - + func test_sleep_cancel() async { let clock = ManualClock() let start = clock.now @@ -55,7 +55,7 @@ final class TestManualClock: XCTestCase { XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } - + func test_sleep_cancel_before_advance() async { let clock = ManualClock() let start = clock.now diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index c8d5e1ce..a2779e3d 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -30,7 +30,7 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(Set(collected).sorted(), expected) } - + func test_merge_makes_sequence_with_elements_from_sources_when_first_is_longer() async { let first = [1, 2, 3, 4, 5, 6, 7] let second = [8, 9, 10] @@ -48,7 +48,7 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(Set(collected).sorted(), expected) } - + func test_merge_makes_sequence_with_elements_from_sources_when_second_is_longer() async { let first = [1, 2, 3] let second = [4, 5, 6, 7] @@ -66,8 +66,10 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(Set(collected).sorted(), expected) } - - func test_merge_produces_three_elements_from_first_and_throws_when_first_is_longer_and_throws_after_three_elements() async throws { + + func test_merge_produces_three_elements_from_first_and_throws_when_first_is_longer_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5] let second = [6, 7, 8] @@ -89,8 +91,11 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(collected.intersection(expected), expected) } - - func test_merge_produces_three_elements_from_first_and_throws_when_first_is_shorter_and_throws_after_three_elements() async throws { + + func + test_merge_produces_three_elements_from_first_and_throws_when_first_is_shorter_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5] let second = [6, 7, 8, 9, 10, 11] @@ -112,8 +117,11 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(collected.intersection(expected), expected) } - - func test_merge_produces_three_elements_from_second_and_throws_when_second_is_longer_and_throws_after_three_elements() async throws { + + func + test_merge_produces_three_elements_from_second_and_throws_when_second_is_longer_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3] let second = [4, 5, 6, 7, 8] @@ -135,8 +143,11 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(collected.intersection(expected), expected) } - - func test_merge_produces_three_elements_from_second_and_throws_when_second_is_shorter_and_throws_after_three_elements() async throws { + + func + test_merge_produces_three_elements_from_second_and_throws_when_second_is_shorter_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5, 6, 7] let second = [7, 8, 9, 10, 11] @@ -158,7 +169,7 @@ final class TestMerge2: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(collected.intersection(expected), expected) } - + func test_merge_makes_sequence_with_ordered_elements_when_sources_follow_a_timeline() { validate { "a-c-e-g-|" @@ -167,7 +178,7 @@ final class TestMerge2: XCTestCase { "abcdefgh|" } } - + func test_merge_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") @@ -192,15 +203,15 @@ final class TestMerge2: XCTestCase { await fulfillment(of: [finished], timeout: 1.0) } - func test_merge_when_cancelled() async { - let t = Task { - try? await Task.sleep(nanoseconds: 1_000_000_000) - let c1 = Indefinite(value: "test1").async - let c2 = Indefinite(value: "test1").async - for await _ in merge(c1, c2) {} - } - t.cancel() + func test_merge_when_cancelled() async { + let t = Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) + let c1 = Indefinite(value: "test1").async + let c2 = Indefinite(value: "test1").async + for await _ in merge(c1, c2) {} } + t.cancel() + } } final class TestMerge3: XCTestCase { @@ -279,7 +290,7 @@ final class TestMerge3: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(Set(collected).sorted(), expected) } - + func test_merge_makes_sequence_with_elements_from_sources_when_first_and_second_are_longer() async { let first = [1, 2, 3, 4, 5] let second = [6, 7, 8, 9] @@ -337,7 +348,9 @@ final class TestMerge3: XCTestCase { XCTAssertEqual(Set(collected).sorted(), expected) } - func test_merge_produces_three_elements_from_first_and_throws_when_first_is_longer_and_throws_after_three_elements() async throws { + func test_merge_produces_three_elements_from_first_and_throws_when_first_is_longer_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5] let second = [6, 7, 8] let third = [9, 10, 11] @@ -361,7 +374,10 @@ final class TestMerge3: XCTestCase { XCTAssertEqual(collected.intersection(expected), expected) } - func test_merge_produces_three_elements_from_first_and_throws_when_first_is_shorter_and_throws_after_three_elements() async throws { + func + test_merge_produces_three_elements_from_first_and_throws_when_first_is_shorter_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5] let second = [6, 7, 8, 9, 10, 11] let third = [12, 13, 14] @@ -385,7 +401,10 @@ final class TestMerge3: XCTestCase { XCTAssertEqual(collected.intersection(expected), expected) } - func test_merge_produces_three_elements_from_second_and_throws_when_second_is_longer_and_throws_after_three_elements() async throws { + func + test_merge_produces_three_elements_from_second_and_throws_when_second_is_longer_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3] let second = [4, 5, 6, 7, 8] let third = [9, 10, 11] @@ -409,7 +428,10 @@ final class TestMerge3: XCTestCase { XCTAssertEqual(collected.intersection(expected), expected) } - func test_merge_produces_three_elements_from_second_and_throws_when_second_is_shorter_and_throws_after_three_elements() async throws { + func + test_merge_produces_three_elements_from_second_and_throws_when_second_is_shorter_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5, 6, 7] let second = [7, 8, 9, 10, 11] let third = [12, 13, 14] @@ -432,8 +454,10 @@ final class TestMerge3: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(collected.intersection(expected), expected) } - - func test_merge_produces_three_elements_from_third_and_throws_when_third_is_longer_and_throws_after_three_elements() async throws { + + func test_merge_produces_three_elements_from_third_and_throws_when_third_is_longer_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3] let second = [4, 5, 6] let third = [7, 8, 9, 10, 11] @@ -457,7 +481,10 @@ final class TestMerge3: XCTestCase { XCTAssertEqual(collected.intersection(expected), expected) } - func test_merge_produces_three_elements_from_third_and_throws_when_third_is_shorter_and_throws_after_three_elements() async throws { + func + test_merge_produces_three_elements_from_third_and_throws_when_third_is_shorter_and_throws_after_three_elements() + async throws + { let first = [1, 2, 3, 4, 5, 6, 7] let second = [7, 8, 9, 10, 11] let third = [12, 13, 14, 15] @@ -480,7 +507,7 @@ final class TestMerge3: XCTestCase { XCTAssertNil(pastEnd) XCTAssertEqual(collected.intersection(expected), expected) } - + func test_merge_makes_sequence_with_ordered_elements_when_sources_follow_a_timeline() { validate { "a---e---|" @@ -516,43 +543,43 @@ final class TestMerge3: XCTestCase { await fulfillment(of: [finished], timeout: 1.0) } - // MARK: - IteratorInitialized + // MARK: - IteratorInitialized - func testIteratorInitialized_whenInitial() async throws { - let reportingSequence1 = ReportingAsyncSequence([1]) - let reportingSequence2 = ReportingAsyncSequence([2]) - let merge = merge(reportingSequence1, reportingSequence2) + func testIteratorInitialized_whenInitial() async throws { + let reportingSequence1 = ReportingAsyncSequence([1]) + let reportingSequence2 = ReportingAsyncSequence([2]) + let merge = merge(reportingSequence1, reportingSequence2) - _ = merge.makeAsyncIterator() + _ = merge.makeAsyncIterator() - // We need to give the task that consumes the upstream - // a bit of time to make the iterators - try await Task.sleep(nanoseconds: 1000000) + // We need to give the task that consumes the upstream + // a bit of time to make the iterators + try await Task.sleep(nanoseconds: 1_000_000) - XCTAssertEqual(reportingSequence1.events, []) - XCTAssertEqual(reportingSequence2.events, []) - } + XCTAssertEqual(reportingSequence1.events, []) + XCTAssertEqual(reportingSequence2.events, []) + } - // MARK: - IteratorDeinitialized + // MARK: - IteratorDeinitialized - func testIteratorDeinitialized_whenMerging() async throws { - let merge = merge([1].async, [2].async) + func testIteratorDeinitialized_whenMerging() async throws { + let merge = merge([1].async, [2].async) - var iterator: _! = merge.makeAsyncIterator() + var iterator: _! = merge.makeAsyncIterator() - let nextValue = await iterator.next() - XCTAssertNotNil(nextValue) + let nextValue = await iterator.next() + XCTAssertNotNil(nextValue) - iterator = nil - } + iterator = nil + } - func testIteratorDeinitialized_whenFinished() async throws { - let merge = merge(Array().async, [].async) + func testIteratorDeinitialized_whenFinished() async throws { + let merge = merge([Int]().async, [].async) - var iterator: _? = merge.makeAsyncIterator() - let firstValue = await iterator?.next() - XCTAssertNil(firstValue) + var iterator: _? = merge.makeAsyncIterator() + let firstValue = await iterator?.next() + XCTAssertNil(firstValue) - iterator = nil - } + iterator = nil + } } diff --git a/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift b/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift index c04683f8..b1a056d9 100644 --- a/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift +++ b/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift @@ -19,28 +19,28 @@ final class TestRangeReplaceableCollection: XCTestCase { let actual = await String(source.async) XCTAssertEqual(expected, actual) } - + func test_Data() async { let source = Data([1, 2, 3]) let expected = source let actual = await Data(source.async) XCTAssertEqual(expected, actual) } - + func test_ContiguousArray() async { let source = ContiguousArray([1, 2, 3]) let expected = source let actual = await ContiguousArray(source.async) XCTAssertEqual(expected, actual) } - + func test_Array() async { let source = Array([1, 2, 3]) let expected = source let actual = await Array(source.async) XCTAssertEqual(expected, actual) } - + func test_throwing() async { let source = Array([1, 2, 3, 4, 5, 6]) let input = source.async.map { (value: Int) async throws -> Int in diff --git a/Tests/AsyncAlgorithmsTests/TestReductions.swift b/Tests/AsyncAlgorithmsTests/TestReductions.swift index 24e7e4e4..ba4366ea 100644 --- a/Tests/AsyncAlgorithmsTests/TestReductions.swift +++ b/Tests/AsyncAlgorithmsTests/TestReductions.swift @@ -26,7 +26,7 @@ final class TestReductions: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_inclusive_reductions() async { let sequence = [1, 2, 3, 4].async.reductions { $0 + $1 } var iterator = sequence.makeAsyncIterator() @@ -38,7 +38,7 @@ final class TestReductions: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_throw_upstream_reductions() async throws { let sequence = [1, 2, 3, 4].async .map { try throwOn(3, $0) } @@ -59,7 +59,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_throw_upstream_inclusive_reductions() async throws { let sequence = [1, 2, 3, 4].async .map { try throwOn(3, $0) } @@ -78,7 +78,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_throwing_reductions() async throws { let sequence = [1, 2, 3, 4].async.reductions("") { (partial, value) throws -> String in partial + "\(value)" @@ -92,7 +92,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_throwing_inclusive_reductions() async throws { let sequence = [1, 2, 3, 4].async.reductions { (lhs, rhs) throws -> Int in lhs + rhs @@ -106,7 +106,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_throw_upstream_reductions_throws() async throws { let sequence = [1, 2, 3, 4].async .map { try throwOn(3, $0) } @@ -127,7 +127,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_throw_upstream_inclusive_reductions_throws() async throws { let sequence = [1, 2, 3, 4].async .map { try throwOn(3, $0) } @@ -148,7 +148,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_reductions_into() async { let sequence = [1, 2, 3, 4].async.reductions(into: "") { partial, value in partial.append("\(value)") @@ -162,7 +162,7 @@ final class TestReductions: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_throwing_reductions_into() async throws { let sequence = [1, 2, 3, 4].async.reductions(into: "") { (partial, value) throws -> Void in partial.append("\(value)") @@ -176,7 +176,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_throwing_reductions_into_throws() async throws { let sequence = [1, 2, 3, 4].async.reductions(into: "") { partial, value in _ = try throwOn("12", partial) @@ -196,7 +196,7 @@ final class TestReductions: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_cancellation() async { let source = Indefinite(value: "test") let sequence = source.async.reductions(into: "") { partial, value in diff --git a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift index 932585b0..1730f636 100644 --- a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift +++ b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift @@ -27,7 +27,7 @@ final class TestRemoveDuplicates: XCTestCase { func test_removeDuplicates_with_closure() async { let source = [1, 2.001, 2.005, 2.011, 3, 4, 5, 6, 5, 5] let expected = [1, 2.001, 2.011, 3, 4, 5, 6, 5] - let sequence = source.async.removeDuplicates() { abs($0 - $1) < 0.01 } + let sequence = source.async.removeDuplicates { abs($0 - $1) < 0.01 } var actual = [Double]() for await item in sequence { actual.append(item) @@ -39,7 +39,7 @@ final class TestRemoveDuplicates: XCTestCase { let source = [1, 2, 2, 2, 3, -1, 5, 6, 5, 5] let expected = [1, 2, 3] var actual = [Int]() - let sequence = source.async.removeDuplicates() { prev, next in + let sequence = source.async.removeDuplicates { prev, next in let next = try throwOn(-1, next) return prev == next } @@ -59,12 +59,14 @@ final class TestRemoveDuplicates: XCTestCase { let source = [1, 2, 2, 2, 3, -1, 5, 6, 5, 5] let expected = [1, 2, 3] var actual = [Int]() - let throwingSequence = source.async.map ({ - if $0 < 0 { - throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) - } - return $0 - } as @Sendable (Int) throws -> Int) + let throwingSequence = source.async.map( + { + if $0 < 0 { + throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) + } + return $0 + } as @Sendable (Int) throws -> Int + ) do { for try await item in throwingSequence.removeDuplicates() { diff --git a/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift b/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift index d652901a..0de4728a 100644 --- a/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift +++ b/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift @@ -19,21 +19,21 @@ final class TestSetAlgebra: XCTestCase { let actual = await Set(source.async) XCTAssertEqual(expected, actual) } - + func test_Set_duplicate() async { let source = [1, 2, 3, 3] let expected = Set(source) let actual = await Set(source.async) XCTAssertEqual(expected, actual) } - + func test_IndexSet() async { let source = [1, 2, 3] let expected = IndexSet(source) let actual = await IndexSet(source.async) XCTAssertEqual(expected, actual) } - + func test_throwing() async { let source = Array([1, 2, 3, 4, 5, 6]) let input = source.async.map { (value: Int) async throws -> Int in diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index d7b60db3..bf027233 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -14,146 +14,178 @@ import AsyncAlgorithms final class TestThrottle: XCTestCase { func test_rate_0() 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") } + 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) "abcdefghijk|" } } - + func test_rate_0_leading_edge() 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") } + 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) "abcdefghijk|" } } - + func test_rate_1() 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") } + 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) "abcdefghijk|" } } - + func test_rate_1_leading_edge() 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") } + 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) "abcdefghijk|" } } - + func test_rate_2() 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") } + 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) "a-c-e-g-i-k|" } } - + func test_rate_2_leading_edge() 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") } + 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) "a-b-d-f-h-j|" } } - + func test_rate_3() 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") } + 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) "a--d--g--j--[k|]" } } - + func test_rate_3_leading_edge() 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") } + 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) "a--b--e--h--[k|]" } } - + func test_throwing() 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") } + 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) "a-c-e-^" } } - + func test_throwing_leading_edge() 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") } + 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) "a-b-d-^" } } - + func test_emission_2_rate_1() 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") } + 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) "-a-b-c-d-e-f-g-h-i-j-k-|" } } - + func test_emission_2_rate_2() 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") } + 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) "-a-b-c-d-e-f-g-h-i-j-k-|" } } - + func test_emission_3_rate_2() 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") } + 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) "--a--b--c--d--e--f--g|" } } - + func test_emission_2_rate_3() 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") } + 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) "-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") } + 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|]" - } + "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") } + 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|]" - } + "abcdefghijkl|" + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: true) + "a--d--g--j--[l|]" + } } } diff --git a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift index 6110c884..20a0ddc6 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift @@ -52,7 +52,9 @@ final class TestThrowingChannel: XCTestCase { XCTAssertEqual(collected, expected) } - func test_asyncThrowingChannel_resumes_producers_and_discards_additional_elements_when_finish_is_called() async throws { + func test_asyncThrowingChannel_resumes_producers_and_discards_additional_elements_when_finish_is_called() + async throws + { // Given: an AsyncThrowingChannel let sut = AsyncThrowingChannel() @@ -139,7 +141,6 @@ final class TestThrowingChannel: XCTestCase { return try await iterator.next() } - // When: finishing the channel sut.finish() diff --git a/Tests/AsyncAlgorithmsTests/TestTimer.swift b/Tests/AsyncAlgorithmsTests/TestTimer.swift index 65db910e..b54647ca 100644 --- a/Tests/AsyncAlgorithmsTests/TestTimer.swift +++ b/Tests/AsyncAlgorithmsTests/TestTimer.swift @@ -15,31 +15,39 @@ import AsyncSequenceValidation final class TestTimer: XCTestCase { func test_tick1() 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") } + 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 { AsyncTimerSequence(interval: .steps(1), clock: $0.clock).map { _ in "x" } "xxxxxxx[;|]" } } - + func test_tick2() 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") } + 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 { AsyncTimerSequence(interval: .steps(2), clock: $0.clock).map { _ in "x" } "-x-x-x-[;|]" } } - + func test_tick3() 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") } + 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 { AsyncTimerSequence(interval: .steps(3), clock: $0.clock).map { _ in "x" } "--x--x-[;|]" } } - + func test_tick2_event_skew3() 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") } + 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 { diagram in AsyncTimerSequence(interval: .steps(2), clock: diagram.clock).map { [diagram] (_) -> String in try? await diagram.clock.sleep(until: diagram.clock.now.advanced(by: .steps(3))) diff --git a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift index c002d64a..b4b646f2 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift @@ -22,7 +22,7 @@ final class TestValidationDiagram: XCTestCase { "A--B--C---|" } } - + func test_diagram_space_noop() { validate { " a -- b -- c ---|" @@ -30,7 +30,7 @@ final class TestValidationDiagram: XCTestCase { " A- - B - - C - -- | " } } - + func test_diagram_string_input() { validate { "'foo''bar''baz'|" @@ -38,7 +38,7 @@ final class TestValidationDiagram: XCTestCase { "fbb|" } } - + func test_diagram_string_input_expectation() { validate { "'foo''bar''baz'|" @@ -46,7 +46,7 @@ final class TestValidationDiagram: XCTestCase { "'foo''bar''baz'|" } } - + func test_diagram_string_dsl_contents() { validate { "'foo-''bar^''baz|'|" @@ -54,7 +54,7 @@ final class TestValidationDiagram: XCTestCase { "'foo-''bar^''baz|'|" } } - + func test_diagram_grouping_source() { validate { "[abc]def|" @@ -62,7 +62,7 @@ final class TestValidationDiagram: XCTestCase { "[abc]def|" } } - + func test_diagram_groups_of_one() { validate { " a b c def|" @@ -70,7 +70,7 @@ final class TestValidationDiagram: XCTestCase { "[a][b][c]def|" } } - + func test_diagram_emoji() { struct EmojiTokens: AsyncSequenceValidationTheme { func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token { @@ -85,7 +85,7 @@ final class TestValidationDiagram: XCTestCase { default: return .value(String(character)) } } - + func description(for token: AsyncSequenceValidationDiagram.Token) -> String { switch token { case .step: return "โž–" @@ -102,14 +102,14 @@ final class TestValidationDiagram: XCTestCase { } } } - + validate(theme: EmojiTokens()) { "โž–๐Ÿ”ดโž–๐ŸŸ โž–๐ŸŸกโž–๐ŸŸขโž–โŒ" $0.inputs[0] "โž–๐Ÿ”ดโž–๐ŸŸ โž–๐ŸŸกโž–๐ŸŸขโž–โŒ" } } - + func test_cancel_event() { validate { "a--b- - c--|" @@ -117,7 +117,7 @@ final class TestValidationDiagram: XCTestCase { "a--b-[;|]" } } - + func test_diagram_failure_mismatch_value() { validate(expectedFailures: ["expected \"X\" but got \"C\" at tick 6"]) { "a--b--c---|" @@ -125,25 +125,29 @@ final class TestValidationDiagram: XCTestCase { "A--B--X---|" } } - + func test_diagram_failure_value_for_finish() { - validate(expectedFailures: ["expected finish but got \"C\" at tick 6", - "unexpected finish at tick 10"]) { + validate(expectedFailures: [ + "expected finish but got \"C\" at tick 6", + "unexpected finish at tick 10", + ]) { "a--b--c---|" $0.inputs[0].map { item in await Task { item.capitalized }.value } "A--B--|" } } - + func test_diagram_failure_finish_for_value() { - validate(expectedFailures: ["expected \"C\" but got finish at tick 6", - "expected finish at tick 7"]) { + validate(expectedFailures: [ + "expected \"C\" but got finish at tick 6", + "expected finish at tick 7", + ]) { "a--b--|" $0.inputs[0].map { item in await Task { item.capitalized }.value } "A--B--C|" } } - + func test_diagram_failure_finish_for_error() { validate(expectedFailures: ["expected failure but got finish at tick 6"]) { "a--b--|" @@ -151,7 +155,7 @@ final class TestValidationDiagram: XCTestCase { "A--B--^" } } - + func test_diagram_failure_error_for_finish() { validate(expectedFailures: ["expected finish but got failure at tick 6"]) { "a--b--^" @@ -159,25 +163,29 @@ final class TestValidationDiagram: XCTestCase { "A--B--|" } } - + func test_diagram_failure_value_for_error() { - validate(expectedFailures: ["expected failure but got \"C\" at tick 6", - "unexpected finish at tick 7"]) { + validate(expectedFailures: [ + "expected failure but got \"C\" at tick 6", + "unexpected finish at tick 7", + ]) { "a--b--c|" $0.inputs[0].map { item in await Task { item.capitalized }.value } "A--B--^" } } - + func test_diagram_failure_error_for_value() { - validate(expectedFailures: ["expected \"C\" but got failure at tick 6", - "expected finish at tick 7"]) { + validate(expectedFailures: [ + "expected \"C\" but got failure at tick 6", + "expected finish at tick 7", + ]) { "a--b--^" $0.inputs[0].map { item in await Task { item.capitalized }.value } "A--B--C|" } } - + func test_diagram_failure_expected_value() { validate(expectedFailures: ["expected \"C\" at tick 6"]) { "a--b---|" @@ -185,16 +193,18 @@ final class TestValidationDiagram: XCTestCase { "A--B--C|" } } - + func test_diagram_failure_expected_failure() { - validate(expectedFailures: ["expected failure at tick 6", - "unexpected finish at tick 7"]) { + validate(expectedFailures: [ + "expected failure at tick 6", + "unexpected finish at tick 7", + ]) { "a--b---|" $0.inputs[0].map { item in await Task { item.capitalized }.value } "A--B--^" } } - + func test_diagram_failure_unexpected_value() { validate(expectedFailures: ["unexpected \"C\" at tick 6"]) { "a--b--c|" @@ -202,16 +212,18 @@ final class TestValidationDiagram: XCTestCase { "A--B---|" } } - + func test_diagram_failure_unexpected_failure() { - validate(expectedFailures: ["unexpected failure at tick 6", - "expected finish at tick 7"]) { + validate(expectedFailures: [ + "unexpected failure at tick 6", + "expected finish at tick 7", + ]) { "a--b--^|" $0.inputs[0].map { item in await Task { item.capitalized }.value } "A--B---|" } } - + func test_diagram_parse_failure_unbalanced_group() { validate(expectedFailures: ["validation diagram unbalanced grouping"]) { " ab|" @@ -219,7 +231,7 @@ final class TestValidationDiagram: XCTestCase { "[ab|" } } - + func test_diagram_parse_failure_unbalanced_group_input() { validate(expectedFailures: ["validation diagram unbalanced grouping"]) { "[ab|" @@ -227,7 +239,7 @@ final class TestValidationDiagram: XCTestCase { " ab|" } } - + func test_diagram_parse_failure_nested_group() { validate(expectedFailures: ["validation diagram nested grouping"]) { " ab|" @@ -235,7 +247,7 @@ final class TestValidationDiagram: XCTestCase { "[[ab|" } } - + func test_diagram_parse_failure_nested_group_input() { validate(expectedFailures: ["validation diagram nested grouping"]) { "[[ab|" @@ -243,7 +255,7 @@ final class TestValidationDiagram: XCTestCase { " ab|" } } - + func test_diagram_parse_failure_step_in_group() { validate(expectedFailures: ["validation diagram step symbol in group"]) { " ab|" @@ -251,7 +263,7 @@ final class TestValidationDiagram: XCTestCase { "[a-]b|" } } - + func test_diagram_parse_failure_step_in_group_input() { validate(expectedFailures: ["validation diagram step symbol in group"]) { "[a-]b|" @@ -259,7 +271,7 @@ final class TestValidationDiagram: XCTestCase { " ab|" } } - + func test_diagram_specification_produce_past_end() { validate(expectedFailures: ["specification violation got \"d\" after iteration terminated at tick 9"]) { "a--b--c--|" @@ -267,7 +279,7 @@ final class TestValidationDiagram: XCTestCase { "a--b--c--|" } } - + func test_diagram_specification_throw_past_end() { validate(expectedFailures: ["specification violation got failure after iteration terminated at tick 9"]) { "a--b--c--|" @@ -275,7 +287,7 @@ final class TestValidationDiagram: XCTestCase { "a--b--c--|" } } - + func test_delayNext() { validate { "xxx--- |" @@ -293,7 +305,9 @@ final class TestValidationDiagram: XCTestCase { } func test_delayNext_into_emptyTick() 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") } + 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 { "xx|" LaggingAsyncSequence($0.inputs[0], delayBy: .steps(3), using: $0.clock) @@ -311,20 +325,19 @@ final class TestValidationDiagram: XCTestCase { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct LaggingAsyncSequence : AsyncSequence { +struct LaggingAsyncSequence: AsyncSequence { typealias Element = Base.Element - struct Iterator : AsyncIteratorProtocol { + struct Iterator: AsyncIteratorProtocol { var base: Base.AsyncIterator let delay: C.Instant.Duration let clock: C mutating func next() async throws -> Element? { - if let value = try await base.next() { - try await clock.sleep(until: clock.now.advanced(by: delay), tolerance: nil) - return value - } else { + guard let value = try await base.next() else { return nil } + try await clock.sleep(until: clock.now.advanced(by: delay), tolerance: nil) + return value } } diff --git a/Tests/AsyncAlgorithmsTests/TestValidator.swift b/Tests/AsyncAlgorithmsTests/TestValidator.swift index 67d2f37e..4b22bcde 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidator.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidator.swift @@ -27,13 +27,13 @@ final class TestValidator: XCTestCase { await fulfillment(of: [entered], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } - + func test_gatedSequence() async { var gated = GatedSequence([1, 2, 3]) let expectations = [ expectation(description: "item 1"), expectation(description: "item 2"), - expectation(description: "item 3") + expectation(description: "item 3"), ] let started = expectation(description: "started") let finished = expectation(description: "finished") @@ -65,7 +65,7 @@ final class TestValidator: XCTestCase { XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2, 3]) await fulfillment(of: [finished], timeout: 1.0) } - + func test_gatedSequence_throwing() async { var gated = GatedSequence([1, 2, 3]) let expectations = [ @@ -104,7 +104,7 @@ final class TestValidator: XCTestCase { XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) XCTAssertEqual(failure.withCriticalRegion { $0 as? Failure }, Failure()) } - + func test_validator() async { var a = GatedSequence([1, 2, 3]) let finished = expectation(description: "finished") @@ -118,19 +118,19 @@ final class TestValidator: XCTestCase { var value = await validator.validate() XCTAssertEqual(value, []) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [2]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [2, 3]) a.advance() - + value = await validator.validate() XCTAssertEqual(value, [2, 3, 4]) a.advance() - + 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 4c2fb229..062df130 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -160,13 +160,13 @@ final class TestZip2: XCTestCase { } func test_zip_when_cancelled() async { - let t = Task { - try? await Task.sleep(nanoseconds: 1_000_000_000) - let c1 = Indefinite(value: "test1").async - let c2 = Indefinite(value: "test1").async - for await _ in zip(c1, c2) {} - } - t.cancel() + let t = Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) + let c1 = Indefinite(value: "test1").async + let c2 = Indefinite(value: "test1").async + for await _ in zip(c1, c2) {} + } + t.cancel() } } diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index e592f92f..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 19c52d83..00000000 --- a/docker/docker-compose.2004.57.yaml +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 56d83dfc..00000000 --- a/docker/docker-compose.2004.58.yaml +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index f28e21d2..00000000 --- a/docker/docker-compose.2204.main.yaml +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 8d1d9a33..00000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# 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