From df97fdaced64506c9c44df8789aa28f29b4579f1 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 24 Mar 2022 12:28:19 -0700 Subject: [PATCH 001/149] Update the code of conduct to avoid it going out of sync (#102) --- CODE_OF_CONDUCT.md | 54 ++-------------------------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2b0a6035..80916098 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,55 +1,5 @@ # Code of Conduct -To be a truly great community, Swift.org needs to welcome developers from all walks of life, -with different backgrounds, and with a wide range of experience. A diverse and friendly -community will have more great ideas, more unique perspectives, and produce more great -code. We will work diligently to make the Swift community welcoming to everyone. -To give clarity of what is expected of our members, Swift.org has adopted the code of conduct -defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source -communities, and we think it articulates our values well. The full text is copied below: +The code of conduct for this project can be found at https://swift.org/code-of-conduct. -### Contributor Code of Conduct v1.3 -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or patches, -and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or -nationality. - -Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other’s private information, such as physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of -Conduct, or to ban temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at [conduct@swift.org](mailto:conduct@swift.org). All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to the -circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter -of an incident. - -*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).* - -### Reporting -A working group of community members is committed to promptly addressing any [reported -issues](mailto:conduct@swift.org). Working group members are volunteers appointed by the project lead, with a -preference for individuals with varied backgrounds and perspectives. Membership is expected -to change regularly, and may grow or shrink. + From 21f20e903092aababb1f9858408a99e2769ea760 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 24 Mar 2022 15:14:40 -0700 Subject: [PATCH 002/149] Only emit diagnostics for unexpected diagram mismatches (#103) --- .../Support/ValidationTest.swift | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/Support/ValidationTest.swift b/Tests/AsyncAlgorithmsTests/Support/ValidationTest.swift index be25cd40..2a7f5667 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ValidationTest.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ValidationTest.swift @@ -14,10 +14,10 @@ import AsyncAlgorithms import AsyncSequenceValidation extension XCTestCase { - func recordFailure(_ description: String, detail: String? = nil, system: Bool = false, at location: AsyncSequenceValidation.SourceLocation) { + func recordFailure(_ description: String, system: Bool = false, at location: AsyncSequenceValidation.SourceLocation) { #if canImport(Darwin) let context = XCTSourceCodeContext(location: XCTSourceCodeLocation(filePath: location.file.description, lineNumber: Int(location.line))) - let issue = XCTIssue(type: system ? .system : .assertionFailure, compactDescription: description, detailedDescription: detail, sourceCodeContext: context, associatedError: nil, attachments: []) + let issue = XCTIssue(type: system ? .system : .assertionFailure, compactDescription: description, detailedDescription: nil, sourceCodeContext: context, associatedError: nil, attachments: []) record(issue) #else XCTFail(description, file: location.file, line: location.line) @@ -26,25 +26,17 @@ extension XCTestCase { 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]() let baseLoc = AsyncSequenceValidation.SourceLocation(file: file, line: line) + var accountedFailures = [AsyncSequenceValidationDiagram.ExpectationFailure]() do { - let (result, failures) = try AsyncSequenceValidationDiagram.test(theme: theme, build) - var detail: String? - if failures.count > 0 { - detail = """ - Expected - \(result.reconstituteExpected(theme: theme)) - Actual - \(result.reconstituteActual(theme: theme)) - """ - print("Expected") - print(result.reconstituteExpected(theme: theme)) - print("Actual") - print(result.reconstituteActual(theme: theme)) - } + (result, failures) = try AsyncSequenceValidationDiagram.test(theme: theme, build) for failure in failures { if expectations.remove(failure.description) == nil { - recordFailure(failure.description, detail: detail, at: failure.specification?.location ?? baseLoc) + recordFailure(failure.description, at: failure.specification?.location ?? baseLoc) + } else { + accountedFailures.append(failure) } } } catch { @@ -52,6 +44,18 @@ extension XCTestCase { recordFailure("\(error)", system: true, at: (error as? SourceFailure)?.location ?? baseLoc) } } + // If no failures were expected and the result reconstitues to something different + // than what was expected, dump that out as a failure for easier diagnostics, this + // likely should be done via attachments but that does not display inline code + // nicely. Ideally we would want to have this display as a runtime warning but those + // do not have source line attribution; for now XCTFail is good enough. + if let result = result, expectedFailures.count == 0 { + let expected = result.reconstituteExpected(theme: theme) + let actual = result.reconstituteActual(theme: theme) + if expected != actual { + XCTFail("Validation failure:\nExpected:\n\(expected)\nActual:\n\(actual)", file: file, line: line) + } + } // any remaining expectations are failures that were expected but did not happen for expectation in expectations { XCTFail("Expected failure: \(expectation) did not occur.", file: file, line: line) From db847ef41037d9b279cb51c6e167e5cb3f4abfdc Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 24 Mar 2022 16:03:21 -0700 Subject: [PATCH 003/149] Remove shims for use with the swift development toolchains (#105) --- Package.swift | 4 +- README.md | 17 +- Sources/AsyncAlgorithms/Reexport.swift | 12 - Sources/AsyncSequenceValidation/Test.swift | 2 +- Sources/ClockShims/Clock.swift | 76 -- Sources/ClockShims/ContinuousClock.swift | 173 --- Sources/ClockShims/Duration.swift | 318 ----- Sources/ClockShims/DurationProtocol.swift | 35 - Sources/ClockShims/Instant.swift | 54 - Sources/ClockShims/Int128.swift | 1334 -------------------- Sources/ClockShims/SuspendingClock.swift | 162 --- Sources/ClockShims/Task.swift | 23 - 12 files changed, 11 insertions(+), 2199 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/Reexport.swift delete mode 100644 Sources/ClockShims/Clock.swift delete mode 100644 Sources/ClockShims/ContinuousClock.swift delete mode 100644 Sources/ClockShims/Duration.swift delete mode 100644 Sources/ClockShims/DurationProtocol.swift delete mode 100644 Sources/ClockShims/Instant.swift delete mode 100644 Sources/ClockShims/Int128.swift delete mode 100644 Sources/ClockShims/SuspendingClock.swift delete mode 100644 Sources/ClockShims/Task.swift diff --git a/Package.swift b/Package.swift index efe5a084..088e9654 100644 --- a/Package.swift +++ b/Package.swift @@ -12,14 +12,12 @@ let package = Package( ], products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), - .library(name: "ClockShims", type: .static, targets: ["ClockShims"]), .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]) ], dependencies: [], targets: [ - .target(name: "AsyncAlgorithms", dependencies: ["ClockShims"]), - .target(name: "ClockShims"), + .target(name: "AsyncAlgorithms"), .target( name: "AsyncSequenceValidation", dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), diff --git a/README.md b/README.md index 5cf25bbe..afe6004f 100644 --- a/README.md +++ b/README.md @@ -86,18 +86,21 @@ Finally, add `import AsyncAlgorithms` to your source code. ## Getting Started +⚠️ Please note that this package currently requires a recent [Swift Trunk Development toolchain](https://www.swift.org/download/#trunk-development-main). More information on how to use custom toolchains with Xcode can be viewed [here](https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/AlternativeToolchains.html). + ### Building/Testing Using Xcode on macOS - 1. Open the `swift-async-algorithms` package directory in Xcode - 2. Build or Test in Xcode as normal - -### Building/Testing Using Swift Package Manager + 1. Download the most recent development Xcode toolchain. + 2. Install the package + 4. Select the development toolchain in Xcode + 4. Open the `swift-async-algorithms` package directory in Xcode + 5. Build or Test in Xcode as normal - 1. In the `swift-async-algorithms` directory run `swift build` or `swift test` accordingly +⚠️ Note: `swift test` does not currently work properly with custom toolchains for this package. ### Building/Testing on Linux - 1. Download the most recent toolchain for your Linux distribution + 1. Download the most recent development toolchain for your Linux distribution 2. Decompress the archive to a path in which the `swift` executable is in the binary search path environment variable (`$PATH`) 3. In the `swift-async-algorithms` directory run `swift build` or `swift test` accordingly @@ -105,8 +108,6 @@ Finally, add `import AsyncAlgorithms` to your source code. The Swift Async Algorithms package has a goal of being source stable as soon as possible; version numbers will follow [Semantic Versioning](https://semver.org/). Source breaking changes to public API can only land in a new major version. -Before the version 1.0 the `swift-async-algorithms` package will not be source or ABI stable. Particularly the shims associated with `Clock`, `Instant` and `Duration` are present just to provide backwards compatability to older toolchains. As soon as this is available widely the shims will be removed; which will be an ABI breaking change. - The public API of version 1.0 of the `swift-async-algorithms` package will consist of non-underscored declarations that are marked `public` in the `AsyncAlgorithms` module. Interfaces that aren't part of the public API may continue to change in any release, including patch releases. Future minor versions of the package may introduce changes to these rules as needed. diff --git a/Sources/AsyncAlgorithms/Reexport.swift b/Sources/AsyncAlgorithms/Reexport.swift deleted file mode 100644 index 0a677648..00000000 --- a/Sources/AsyncAlgorithms/Reexport.swift +++ /dev/null @@ -1,12 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -@_exported import ClockShims diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index cc5fe969..ba7a5c3f 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -34,7 +34,7 @@ extension AsyncSequenceValidationDiagram { 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 { diff --git a/Sources/ClockShims/Clock.swift b/Sources/ClockShims/Clock.swift deleted file mode 100644 index dc7256f3..00000000 --- a/Sources/ClockShims/Clock.swift +++ /dev/null @@ -1,76 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -import Swift - -/// A mechanism in which to measure time, and delay work until a given point -/// in time. -/// -/// Types that conform to the `Clock` protocol define a concept of "now" which -/// is the specific instant in time that property is accessed. Any pair of calls -/// to the `now` property may have a minimum duration between them - this -/// minimum resolution is exposed by the `minimumResolution` property to inform -/// any user of the type the expected granularity of accuracy. -/// -/// One of the primary uses for clocks is to schedule task sleeping. This method -/// resumes the calling task after a given deadline has been met or passed with -/// a given tolerance value. The tolerance is expected as a leeway around the -/// deadline. The clock may reschedule tasks within the tolerance to ensure -/// efficient execution of resumptions by reducing potential operating system -/// wake-ups. If no tolerance is specified (i.e. nil is passed in) the sleep -/// function is expected to schedule with a default tolerance strategy. -/// -/// For more information about specific clocks see `ContinuousClock` and -/// `SuspendingClock`. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -public protocol Clock: Sendable { - associatedtype Instant: InstantProtocol - - var now: Instant { get } - var minimumResolution: Instant.Duration { get } - - func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws -} - - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Clock { - /// Measure the elapsed time to execute a closure. - /// - /// let clock = ContinuousClock() - /// let elapsed = clock.measure { - /// someWork() - /// } - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) - public func measure(_ work: () throws -> Void) rethrows -> Instant.Duration { - let start = now - try work() - let end = now - return start.duration(to: end) - } - - /// Measure the elapsed time to execute an asynchronous closure. - /// - /// let clock = ContinuousClock() - /// let elapsed = await clock.measure { - /// await someWork() - /// } - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) - public func measure( - _ work: () async throws -> Void - ) async rethrows -> Instant.Duration { - let start = now - try await work() - let end = now - return start.duration(to: end) - } -} - diff --git a/Sources/ClockShims/ContinuousClock.swift b/Sources/ClockShims/ContinuousClock.swift deleted file mode 100644 index bd528a52..00000000 --- a/Sources/ClockShims/ContinuousClock.swift +++ /dev/null @@ -1,173 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -import Swift -#if canImport(Darwin) -@_implementationOnly import Darwin -#elseif canImport(Glibc) -@_implementationOnly import Glibc -#else -#error("Unsupported platform") -#endif - -/// A clock that measures time that always increments but does not stop -/// incrementing while the system is asleep. -/// -/// `ContinuousClock` can be considered as a stopwatch style time. The frame of -/// reference of the `Instant` may be bound to process launch, machine boot or -/// some other locally defined reference point. This means that the instants are -/// only comparable locally during the execution of a program. -/// -/// This clock is suitable for high resolution measurements of execution. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -public struct ContinuousClock { - /// A continuous point in time used for `ContinuousClock`. - public struct Instant: Codable, Sendable { - internal var _value: Duration - - internal init(_value: Duration) { - self._value = _value - } - } - - public init() { } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Clock where Self == ContinuousClock { - /// A clock that measures time that always increments but does not stop - /// incrementing while the system is asleep. - /// - /// try await Task.sleep(until: .now + .seconds(3), clock: .continuous) - /// - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) - public static var continuous: ContinuousClock { return ContinuousClock() } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension ContinuousClock: Clock { - /// The current continuous instant. - public var now: ContinuousClock.Instant { - ContinuousClock.now - } - - /// The minimum non-zero resolution between any two calls to `now`. - public var minimumResolution: Duration { - var ts = timespec() -#if canImport(Darwin) - clock_getres(CLOCK_MONOTONIC, &ts) -#elseif canImport(Glibc) - clock_getres(CLOCK_BOOTTIME, &ts) -#endif - return .seconds(ts.tv_sec) + .nanoseconds(ts.tv_nsec) - } - - /// The current continuous instant. - public static var now: ContinuousClock.Instant { - var ts = timespec() -#if canImport(Darwin) - clock_gettime(CLOCK_MONOTONIC, &ts) -#elseif canImport(Glibc) - clock_gettime(CLOCK_BOOTTIME, &ts) -#endif - return ContinuousClock.Instant(_value: - .seconds(ts.tv_sec) + .nanoseconds(ts.tv_nsec)) - } - - /// Suspend task execution until a given deadline within a tolerance. - /// If no tolerance is specified then the system may adjust the deadline - /// to coalesce CPU wake-ups to more efficiently process the wake-ups in - /// a more power efficient manner. - /// - /// If the task is canceled before the time ends, this function throws - /// `CancellationError`. - /// - /// This function doesn't block the underlying thread. - public func sleep( - until deadline: Instant, tolerance: Duration? = nil - ) async throws { - let duration = deadline - .now - let (seconds, attoseconds) = duration.components - let nanoseconds = attoseconds / 1_000_000_000 + seconds * 1_000_000_000 - if nanoseconds > 0 { - try await Task.sleep(nanoseconds: UInt64(nanoseconds)) - } - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension ContinuousClock.Instant: InstantProtocol { - public static var now: ContinuousClock.Instant { ContinuousClock.now } - - public func advanced(by duration: Duration) -> ContinuousClock.Instant { - return ContinuousClock.Instant(_value: _value + duration) - } - - public func duration(to other: ContinuousClock.Instant) -> Duration { - other._value - _value - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(_value) - } - - public static func == ( - _ lhs: ContinuousClock.Instant, _ rhs: ContinuousClock.Instant - ) -> Bool { - return lhs._value == rhs._value - } - - public static func < ( - _ lhs: ContinuousClock.Instant, _ rhs: ContinuousClock.Instant - ) -> Bool { - return lhs._value < rhs._value - } - - @_alwaysEmitIntoClient - @inlinable - public static func + ( - _ lhs: ContinuousClock.Instant, _ rhs: Duration - ) -> ContinuousClock.Instant { - lhs.advanced(by: rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func += ( - _ lhs: inout ContinuousClock.Instant, _ rhs: Duration - ) { - lhs = lhs.advanced(by: rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func - ( - _ lhs: ContinuousClock.Instant, _ rhs: Duration - ) -> ContinuousClock.Instant { - lhs.advanced(by: .zero - rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func -= ( - _ lhs: inout ContinuousClock.Instant, _ rhs: Duration - ) { - lhs = lhs.advanced(by: .zero - rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func - ( - _ lhs: ContinuousClock.Instant, _ rhs: ContinuousClock.Instant - ) -> Duration { - rhs.duration(to: lhs) - } -} diff --git a/Sources/ClockShims/Duration.swift b/Sources/ClockShims/Duration.swift deleted file mode 100644 index 73d35be6..00000000 --- a/Sources/ClockShims/Duration.swift +++ /dev/null @@ -1,318 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A representation of high precision time. -/// -/// `Duration` represents an elapsed time value with high precision in an -/// integral form. It may be used for measurements of varying clock sources. In -/// those cases it represents the elapsed time measured by that clock. -/// Calculations using `Duration` may span from a negative value to a positive -/// value and have a suitable range to at least cover attosecond scale for both -/// small elapsed durations like sub-second precision to durations that span -/// centuries. -/// -/// Typical construction of `Duration` values should be created via the -/// static methods for specific time values. -/// -/// var d: Duration = .seconds(3) -/// d += .milliseconds(33) -/// print(d) // 3.033 seconds -/// -/// `Duration` itself does not ferry any additional information other than the -/// temporal measurement component; specifically leap seconds should be -/// represented as an additional accessor since that is specific only to certain -/// clock implementations. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -@frozen -public struct Duration: Sendable { - /// The low 64 bits of a 128-bit signed integer value counting attoseconds. - @usableFromInline - internal var _low: UInt64 - - /// The high 64 bits of a 128-bit signed integer value counting attoseconds. - @usableFromInline - internal var _high: Int64 - - @inlinable - internal init(_high: Int64, low: UInt64) { - self._low = low - self._high = _high - } - - internal init(_attoseconds: _Int128) { - self.init(_high: _attoseconds.high, low: _attoseconds.low) - } - - /// Construct a `Duration` by adding attoseconds to a seconds value. - /// - /// This is useful for when an external decomposed components of a `Duration` - /// has been stored and needs to be reconstituted. Since the values are added - /// no precondition is expressed for the attoseconds being limited to 1e18. - /// - /// let d1 = Duration( - /// secondsComponent: 3, - /// attosecondsComponent: 123000000000000000) - /// print(d1) // 3.123 seconds - /// - /// let d2 = Duration( - /// secondsComponent: 3, - /// attosecondsComponent: -123000000000000000) - /// print(d2) // 2.877 seconds - /// - /// let d3 = Duration( - /// secondsComponent: -3, - /// attosecondsComponent: -123000000000000000) - /// print(d3) // -3.123 seconds - /// - /// - Parameters: - /// - secondsComponent: The seconds component portion of the `Duration` - /// value. - /// - attosecondsComponent: The attosecond component portion of the - /// `Duration` value. - public init(secondsComponent: Int64, attosecondsComponent: Int64) { - self = Duration.seconds(secondsComponent) + - Duration(_attoseconds: _Int128(attosecondsComponent)) - } - - internal var _attoseconds: _Int128 { - _Int128(high: _high, low: _low) - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration { - /// The composite components of the `Duration`. - /// - /// This is intended for facilitating conversions to existing time types. The - /// attoseconds value will not exceed 1e18 or be lower than -1e18. - - public var components: (seconds: Int64, attoseconds: Int64) { - let (seconds, attoseconds) = _attoseconds.dividedBy1e18() - return (Int64(seconds), Int64(attoseconds)) - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration { - /// Construct a `Duration` given a number of seconds represented as a - /// `BinaryInteger`. - /// - /// let d: Duration = .seconds(77) - /// - /// - Returns: A `Duration` representing a given number of seconds. - - public static func seconds(_ seconds: T) -> Duration { - return Duration(_attoseconds: - _Int128(seconds).multiplied(by: 1_000_000_000_000_000_000 as UInt64)) - } - - /// Construct a `Duration` given a number of seconds represented as a - /// `Double` by converting the value into the closest attosecond scale value. - /// - /// let d: Duration = .seconds(22.93) - /// - /// - Returns: A `Duration` representing a given number of seconds. - - public static func seconds(_ seconds: Double) -> Duration { - return Duration(_attoseconds: _Int128(seconds * 1_000_000_000_000_000_000)) - } - - /// Construct a `Duration` given a number of milliseconds represented as a - /// `BinaryInteger`. - /// - /// let d: Duration = .milliseconds(645) - /// - /// - Returns: A `Duration` representing a given number of milliseconds. - - public static func milliseconds( - _ milliseconds: T - ) -> Duration { - return Duration(_attoseconds: - _Int128(milliseconds).multiplied(by: 1_000_000_000_000_000 as UInt64)) - } - - /// Construct a `Duration` given a number of seconds milliseconds as a - /// `Double` by converting the value into the closest attosecond scale value. - /// - /// let d: Duration = .milliseconds(88.3) - /// - /// - Returns: A `Duration` representing a given number of milliseconds. - - public static func milliseconds(_ milliseconds: Double) -> Duration { - return Duration(_attoseconds: - _Int128(milliseconds * 1_000_000_000_000_000)) - } - - /// Construct a `Duration` given a number of microseconds represented as a - /// `BinaryInteger`. - /// - /// let d: Duration = .microseconds(12) - /// - /// - Returns: A `Duration` representing a given number of microseconds. - - public static func microseconds( - _ microseconds: T - ) -> Duration { - return Duration(_attoseconds: - _Int128(microseconds).multiplied(by: 1_000_000_000_000 as UInt64)) - } - - /// Construct a `Duration` given a number of seconds microseconds as a - /// `Double` by converting the value into the closest attosecond scale value. - /// - /// let d: Duration = .microseconds(382.9) - /// - /// - Returns: A `Duration` representing a given number of microseconds. - - public static func microseconds(_ microseconds: Double) -> Duration { - return Duration(_attoseconds: - _Int128(microseconds * 1_000_000_000_000)) - } - - /// Construct a `Duration` given a number of nanoseconds represented as a - /// `BinaryInteger`. - /// - /// let d: Duration = .nanoseconds(1929) - /// - /// - Returns: A `Duration` representing a given number of nanoseconds. - - public static func nanoseconds( - _ nanoseconds: T - ) -> Duration { - return Duration(_attoseconds: - _Int128(nanoseconds).multiplied(by: 1_000_000_000)) - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: Codable { - - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - let high = try container.decode(Int64.self) - let low = try container.decode(UInt64.self) - self.init(_high: high, low: low) - } - - - public func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - try container.encode(_high) - try container.encode(_low) - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(_attoseconds) - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: Equatable { - public static func == (_ lhs: Duration, _ rhs: Duration) -> Bool { - return lhs._attoseconds == rhs._attoseconds - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: Comparable { - public static func < (_ lhs: Duration, _ rhs: Duration) -> Bool { - return lhs._attoseconds < rhs._attoseconds - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: AdditiveArithmetic { - - public static var zero: Duration { Duration(_attoseconds: 0) } - - - public static func + (_ lhs: Duration, _ rhs: Duration) -> Duration { - return Duration(_attoseconds: lhs._attoseconds + rhs._attoseconds) - } - - - public static func - (_ lhs: Duration, _ rhs: Duration) -> Duration { - return Duration(_attoseconds: lhs._attoseconds - rhs._attoseconds) - } - - - public static func += (_ lhs: inout Duration, _ rhs: Duration) { - lhs = lhs + rhs - } - - - public static func -= (_ lhs: inout Duration, _ rhs: Duration) { - lhs = lhs - rhs - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration { - - public static func / (_ lhs: Duration, _ rhs: Double) -> Duration { - return Duration(_attoseconds: - _Int128(Double(lhs._attoseconds) / rhs)) - } - - - public static func /= (_ lhs: inout Duration, _ rhs: Double) { - lhs = lhs / rhs - } - - - public static func / ( - _ lhs: Duration, _ rhs: T - ) -> Duration { - Duration(_attoseconds: lhs._attoseconds / _Int128(rhs)) - } - - - public static func /= (_ lhs: inout Duration, _ rhs: T) { - lhs = lhs / rhs - } - - - public static func / (_ lhs: Duration, _ rhs: Duration) -> Double { - Double(lhs._attoseconds) / Double(rhs._attoseconds) - } - - - public static func * (_ lhs: Duration, _ rhs: Double) -> Duration { - Duration(_attoseconds: _Int128(Double(lhs._attoseconds) * rhs)) - } - - - public static func * ( - _ lhs: Duration, _ rhs: T - ) -> Duration { - Duration(_attoseconds: lhs._attoseconds * _Int128(rhs)) - } - - - public static func *= (_ lhs: inout Duration, _ rhs: T) { - lhs = lhs * rhs - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: CustomStringConvertible { - - public var description: String { - return (Double(_attoseconds) / 1e18).description + " seconds" - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Duration: DurationProtocol { } diff --git a/Sources/ClockShims/DurationProtocol.swift b/Sources/ClockShims/DurationProtocol.swift deleted file mode 100644 index 10845f8c..00000000 --- a/Sources/ClockShims/DurationProtocol.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A type that defines a duration for a given `InstantProtocol` type. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -public protocol DurationProtocol: Comparable, AdditiveArithmetic, Sendable { - static func / (_ lhs: Self, _ rhs: Int) -> Self - static func /= (_ lhs: inout Self, _ rhs: Int) - static func * (_ lhs: Self, _ rhs: Int) -> Self - static func *= (_ lhs: inout Self, _ rhs: Int) - - static func / (_ lhs: Self, _ rhs: Self) -> Double -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension DurationProtocol { - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) - public static func /= (_ lhs: inout Self, _ rhs: Int) { - lhs = lhs / rhs - } - - - public static func *= (_ lhs: inout Self, _ rhs: Int) { - lhs = lhs * rhs - } -} diff --git a/Sources/ClockShims/Instant.swift b/Sources/ClockShims/Instant.swift deleted file mode 100644 index d5b5b9ba..00000000 --- a/Sources/ClockShims/Instant.swift +++ /dev/null @@ -1,54 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -// A type that defines a specific point in time for a given `Clock`. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -public protocol InstantProtocol: Comparable, Hashable, Sendable { - associatedtype Duration: DurationProtocol - func advanced(by duration: Duration) -> Self - func duration(to other: Self) -> Duration -} - -/* -disabled for now - this perturbs operator resolution -extension InstantProtocol { - @_alwaysEmitIntoClient - @inlinable - public static func + (_ lhs: Self, _ rhs: Duration) -> Self { - lhs.advanced(by: rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func += (_ lhs: inout Self, _ rhs: Duration) { - lhs = lhs.advanced(by: rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func - (_ lhs: Self, _ rhs: Duration) -> Self { - lhs.advanced(by: .zero - rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func -= (_ lhs: inout Self, _ rhs: Duration) { - lhs = lhs.advanced(by: .zero - rhs) - } - - @_alwaysEmitIntoClient - @inlinable - public static func - (_ lhs: Self, _ rhs: Self) -> Duration { - rhs.duration(to: lhs) - } -} -*/ diff --git a/Sources/ClockShims/Int128.swift b/Sources/ClockShims/Int128.swift deleted file mode 100644 index 3e468dc8..00000000 --- a/Sources/ClockShims/Int128.swift +++ /dev/null @@ -1,1334 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A 128-bit unsigned integer type. -internal struct _UInt128 { - internal typealias High = UInt64 - internal typealias Low = UInt64 - - /// The low part of the value. - internal var low: Low - - /// The high part of the value. - internal var high: High - - /// Creates a new instance from the given tuple of high and low parts. - /// - /// - Parameter value: The tuple to use as the source of the new instance's - /// high and low parts. - internal init(_ value: (high: High, low: Low)) { - self.low = value.low - self.high = value.high - } - - internal init(high: High, low: Low) { - self.low = low - self.high = high - } - - internal init() { - self.init(high: 0, low: 0) - } - - internal init(bitPattern v: _Int128) { - self.init(high: High(bitPattern: v.high), low: v.low) - } - - internal static var zero: Self { Self(high: 0, low: 0) } - internal static var one: Self { Self(high: 0, low: 1) } -} - -extension _UInt128: CustomStringConvertible { - internal var description: String { - String(self, radix: 10) - } -} - -extension _UInt128: CustomDebugStringConvertible { - internal var debugDescription: String { - description - } -} - -extension _UInt128: Equatable { - internal static func == (_ lhs: Self, _ rhs: Self) -> Bool { - return (lhs.high, lhs.low) == (rhs.high, rhs.low) - } -} - -extension _UInt128: Comparable { - internal static func < (_ lhs: Self, _ rhs: Self) -> Bool { - (lhs.high, lhs.low) < (rhs.high, rhs.low) - } -} - -extension _UInt128: Hashable { - internal func hash(into hasher: inout Hasher) { - hasher.combine(low) - hasher.combine(high) - } -} - -extension _UInt128 { - internal var components: (high: High, low: Low) { - @inline(__always) get { (high, low) } - @inline(__always) set { (self.high, self.low) = (newValue.high, newValue.low) } - } -} - -extension _UInt128: AdditiveArithmetic { - internal static func - (_ lhs: Self, _ rhs: Self) -> Self { - let (result, overflow) = lhs.subtractingReportingOverflow(rhs) - precondition(!overflow, "Overflow in -") - return result - } - - internal static func -= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.subtractingReportingOverflow(rhs) - precondition(!overflow, "Overflow in -=") - lhs = result - } - - internal static func + (_ lhs: Self, _ rhs: Self) -> Self { - let (result, overflow) = lhs.addingReportingOverflow(rhs) - precondition(!overflow, "Overflow in +") - return result - } - - internal static func += (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.addingReportingOverflow(rhs) - precondition(!overflow, "Overflow in +=") - lhs = result - } -} - -extension _UInt128: Numeric { - internal typealias Magnitude = _UInt128 - - internal var magnitude: Magnitude { - return self - } - - internal init(_ magnitude: Magnitude) { - self.init(high: High(magnitude.high), low: magnitude.low) - } - - internal init(_ source: T) { - guard let result = Self(exactly: source) else { - preconditionFailure("Value is outside the representable range") - } - self = result - } - - internal init?(exactly source: T) { - // Can't represent a negative 'source' if Self is unsigned. - guard Self.isSigned || source >= 0 else { - return nil - } - - // Is 'source' entirely representable in Low? - if let low = Low(exactly: source.magnitude) { - self.init(source._isNegative ? (~0, low._twosComplement) : (0, low)) - } else { - // At this point we know source.bitWidth > High.bitWidth, or else we - // would've taken the first branch. - let lowInT = source & T(~0 as Low) - let highInT = source >> Low.bitWidth - - let low = Low(lowInT) - guard let high = High(exactly: highInT) else { - return nil - } - self.init(high: high, low: low) - } - } - - internal static func * (_ lhs: Self, _ rhs: Self) -> Self { - let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs) - precondition(!overflow, "Overflow in *") - return result - } - - internal static func *= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs) - precondition(!overflow, "Overflow in *=") - lhs = result - } -} - -extension _UInt128 { - internal struct Words { - internal var _value: _UInt128 - - internal init(_ value: _UInt128) { - self._value = value - } - } -} - -extension _UInt128.Words: RandomAccessCollection { - internal typealias Element = UInt - internal typealias Index = Int - internal typealias Indices = Range - internal typealias SubSequence = Slice - - internal var count: Int { 128 / UInt.bitWidth } - internal var startIndex: Int { 0 } - internal var endIndex: Int { count } - internal var indices: Indices { startIndex ..< endIndex } - internal func index(after i: Int) -> Int { i + 1 } - internal func index(before i: Int) -> Int { i - 1 } - - internal subscript(position: Int) -> UInt { - get { - precondition(position >= 0 && position < endIndex, - "Word index out of range") - let shift = position &* UInt.bitWidth - assert(shift < _UInt128.bitWidth) - - let r = _wideMaskedShiftRight( - _value.components, UInt64(truncatingIfNeeded: shift)) - return r.low._lowWord - } - } -} - -extension _UInt128: FixedWidthInteger { - @_transparent - internal var _lowWord: UInt { - low._lowWord - } - - internal var words: Words { - Words(self) - } - - internal static var isSigned: Bool { - false - } - - internal static var max: Self { - self.init(high: High.max, low: Low.max) - } - - internal static var min: Self { - self.init(high: High.min, low: Low.min) - } - - internal static var bitWidth: Int { 128 } - - internal func addingReportingOverflow( - _ rhs: Self - ) -> (partialValue: Self, overflow: Bool) { - let (r, o) = _wideAddReportingOverflow22(self.components, rhs.components) - return (Self(r), o) - } - - internal func subtractingReportingOverflow( - _ rhs: Self - ) -> (partialValue: Self, overflow: Bool) { - let (r, o) = _wideSubtractReportingOverflow22( - self.components, rhs.components) - return (Self(r), o) - } - - internal func multipliedReportingOverflow( - by rhs: Self - ) -> (partialValue: Self, overflow: Bool) { - let h1 = self.high.multipliedReportingOverflow(by: rhs.low) - let h2 = self.low.multipliedReportingOverflow(by: rhs.high) - let h3 = h1.partialValue.addingReportingOverflow(h2.partialValue) - let (h, l) = self.low.multipliedFullWidth(by: rhs.low) - let high = h3.partialValue.addingReportingOverflow(h) - let overflow = ( - (self.high != 0 && rhs.high != 0) - || h1.overflow || h2.overflow || h3.overflow || high.overflow) - return (Self(high: high.partialValue, low: l), overflow) - } - - /// Returns the product of this value and the given 64-bit value, along with a - /// Boolean value indicating whether overflow occurred in the operation. - internal func multipliedReportingOverflow( - by other: UInt64 - ) -> (partialValue: Self, overflow: Bool) { - let h1 = self.high.multipliedReportingOverflow(by: other) - let (h2, l) = self.low.multipliedFullWidth(by: other) - let high = h1.partialValue.addingReportingOverflow(h2) - let overflow = h1.overflow || high.overflow - return (Self(high: high.partialValue, low: l), overflow) - } - - internal func multiplied(by other: UInt64) -> Self { - let r = multipliedReportingOverflow(by: other) - precondition(!r.overflow, "Overflow in multiplication") - return r.partialValue - } - - internal func quotientAndRemainder( - dividingBy other: Self - ) -> (quotient: Self, remainder: Self) { - let (q, r) = _wideDivide22( - self.magnitude.components, by: other.magnitude.components) - let quotient = Self.Magnitude(q) - let remainder = Self.Magnitude(r) - return (quotient, remainder) - } - - internal func dividedReportingOverflow( - by other: Self - ) -> (partialValue: Self, overflow: Bool) { - if other == Self.zero { - return (self, true) - } - if Self.isSigned && other == -1 && self == .min { - return (self, true) - } - return (quotientAndRemainder(dividingBy: other).quotient, false) - } - - internal func remainderReportingOverflow( - dividingBy other: Self - ) -> (partialValue: Self, overflow: Bool) { - if other == Self.zero { - return (self, true) - } - if Self.isSigned && other == -1 && self == .min { - return (0, true) - } - return (quotientAndRemainder(dividingBy: other).remainder, false) - } - - internal func multipliedFullWidth( - by other: Self - ) -> (high: Self, low: Magnitude) { - let isNegative = Self.isSigned && (self._isNegative != other._isNegative) - - func sum(_ x: Low, _ y: Low, _ z: Low) -> (high: Low, low: Low) { - let (sum1, overflow1) = x.addingReportingOverflow(y) - let (sum2, overflow2) = sum1.addingReportingOverflow(z) - let carry: Low = (overflow1 ? 1 : 0) + (overflow2 ? 1 : 0) - return (carry, sum2) - } - - let lhs = self.magnitude - let rhs = other.magnitude - - let a = rhs.low.multipliedFullWidth(by: lhs.low) - let b = rhs.low.multipliedFullWidth(by: lhs.high) - let c = rhs.high.multipliedFullWidth(by: lhs.low) - let d = rhs.high.multipliedFullWidth(by: lhs.high) - - let mid1 = sum(a.high, b.low, c.low) - let mid2 = sum(b.high, c.high, d.low) - - let low = _UInt128(high: mid1.low, low: a.low) - let high = _UInt128( - high: High(mid2.high + d.high), - low: mid1.high + mid2.low) - - if isNegative { - let (lowComplement, overflow) = (~low).addingReportingOverflow(.one) - return (~high + (overflow ? 1 : 0), lowComplement) - } else { - return (high, low) - } - } - - internal func dividingFullWidth( - _ dividend: (high: Self, low: Self.Magnitude) - ) -> (quotient: Self, remainder: Self) { - let (q, r) = _wideDivide42( - (dividend.high.components, dividend.low.components), - by: self.components) - return (Self(q), Self(r)) - } - - #if false // This triggers an unexpected type checking issue with `~0` in an - // lldb test - internal static prefix func ~(x: Self) -> Self { - Self(high: ~x.high, low: ~x.low) - } - #endif - - internal static func &= (_ lhs: inout Self, _ rhs: Self) { - lhs.low &= rhs.low - lhs.high &= rhs.high - } - - internal static func |= (_ lhs: inout Self, _ rhs: Self) { - lhs.low |= rhs.low - lhs.high |= rhs.high - } - - internal static func ^= (_ lhs: inout Self, _ rhs: Self) { - lhs.low ^= rhs.low - lhs.high ^= rhs.high - } - - internal static func <<= (_ lhs: inout Self, _ rhs: Self) { - if Self.isSigned && rhs._isNegative { - lhs >>= 0 - rhs - return - } - - // Shift is larger than this type's bit width. - if rhs.high != High.zero || rhs.low >= Self.bitWidth { - lhs = 0 - return - } - - lhs &<<= rhs - } - - internal static func >>= (_ lhs: inout Self, _ rhs: Self) { - if Self.isSigned && rhs._isNegative { - lhs <<= 0 - rhs - return - } - - // Shift is larger than this type's bit width. - if rhs.high != High.zero || rhs.low >= Self.bitWidth { - lhs = lhs._isNegative ? ~0 : 0 - return - } - - lhs &>>= rhs - } - - internal static func &<< (lhs: Self, rhs: Self) -> Self { - Self(_wideMaskedShiftLeft(lhs.components, rhs.low)) - } - - internal static func &>> (lhs: Self, rhs: Self) -> Self { - Self(_wideMaskedShiftRight(lhs.components, rhs.low)) - } - - internal static func &<<= (lhs: inout Self, rhs: Self) { - _wideMaskedShiftLeft(&lhs.components, rhs.low) - } - - internal static func &>>= (lhs: inout Self, rhs: Self) { - _wideMaskedShiftRight(&lhs.components, rhs.low) - } - - internal static func / ( - _ lhs: Self, _ rhs: Self - ) -> Self { - var lhs = lhs - lhs /= rhs - return lhs - } - - internal static func /= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.dividedReportingOverflow(by: rhs) - precondition(!overflow, "Overflow in /=") - lhs = result - } - - internal static func % ( - _ lhs: Self, _ rhs: Self - ) -> Self { - var lhs = lhs - lhs %= rhs - return lhs - } - - internal static func %= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.remainderReportingOverflow(dividingBy: rhs) - precondition(!overflow, "Overflow in %=") - lhs = result - } - - internal init(_truncatingBits bits: UInt) { - low = Low(_truncatingBits: bits) - high = High(_truncatingBits: bits >> UInt(Low.bitWidth)) - } - - internal init(integerLiteral x: Int64) { - self.init(x) - } - - internal var leadingZeroBitCount: Int { - (high == High.zero - ? High.bitWidth + low.leadingZeroBitCount - : high.leadingZeroBitCount) - } - - internal var trailingZeroBitCount: Int { - (low == Low.zero - ? Low.bitWidth + high.trailingZeroBitCount - : low.trailingZeroBitCount) - } - - internal var nonzeroBitCount: Int { - high.nonzeroBitCount + low.nonzeroBitCount - } - - internal var byteSwapped: Self { - Self( - high: High(truncatingIfNeeded: low.byteSwapped), - low: Low(truncatingIfNeeded: high.byteSwapped)) - } -} - -extension _UInt128: Sendable {} -/// A 128-bit signed integer type. -internal struct _Int128 { - internal typealias High = Int64 - internal typealias Low = UInt64 - - /// The low part of the value. - internal var low: Low - - /// The high part of the value. - internal var high: High - - /// Creates a new instance from the given tuple of high and low parts. - /// - /// - Parameter value: The tuple to use as the source of the new instance's - /// high and low parts. - internal init(_ value: (high: High, low: Low)) { - self.low = value.low - self.high = value.high - } - - internal init(high: High, low: Low) { - self.low = low - self.high = high - } - - internal init() { - self.init(high: 0, low: 0) - } - - internal init(bitPattern v: _UInt128) { - self.init(high: High(bitPattern: v.high), low: v.low) - } - - internal static var zero: Self { Self(high: 0, low: 0) } - internal static var one: Self { Self(high: 0, low: 1) } -} - -extension _Int128: CustomStringConvertible { - internal var description: String { - String(self, radix: 10) - } -} - -extension _Int128: CustomDebugStringConvertible { - internal var debugDescription: String { - description - } -} - -extension _Int128: Equatable { - internal static func == (_ lhs: Self, _ rhs: Self) -> Bool { - return (lhs.high, lhs.low) == (rhs.high, rhs.low) - } -} - -extension _Int128: Comparable { - internal static func < (_ lhs: Self, _ rhs: Self) -> Bool { - (lhs.high, lhs.low) < (rhs.high, rhs.low) - } -} - -extension _Int128: Hashable { - internal func hash(into hasher: inout Hasher) { - hasher.combine(low) - hasher.combine(high) - } -} - -extension _Int128 { - internal var components: (high: High, low: Low) { - @inline(__always) get { (high, low) } - @inline(__always) set { (self.high, self.low) = (newValue.high, newValue.low) } - } -} - -extension _Int128: AdditiveArithmetic { - internal static func - (_ lhs: Self, _ rhs: Self) -> Self { - let (result, overflow) = lhs.subtractingReportingOverflow(rhs) - precondition(!overflow, "Overflow in -") - return result - } - - internal static func -= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.subtractingReportingOverflow(rhs) - precondition(!overflow, "Overflow in -=") - lhs = result - } - - internal static func + (_ lhs: Self, _ rhs: Self) -> Self { - let (result, overflow) = lhs.addingReportingOverflow(rhs) - precondition(!overflow, "Overflow in +") - return result - } - - internal static func += (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.addingReportingOverflow(rhs) - precondition(!overflow, "Overflow in +=") - lhs = result - } -} - -extension _Int128: Numeric { - internal typealias Magnitude = _UInt128 - - internal var magnitude: Magnitude { - var result = _UInt128(bitPattern: self) - guard high._isNegative else { return result } - result.high = ~result.high - result.low = ~result.low - return result.addingReportingOverflow(.one).partialValue - } - - internal init(_ magnitude: Magnitude) { - self.init(high: High(magnitude.high), low: magnitude.low) - } - - internal init(_ source: T) { - guard let result = Self(exactly: source) else { - preconditionFailure("Value is outside the representable range") - } - self = result - } - - internal init?(exactly source: T) { - // Can't represent a negative 'source' if Self is unsigned. - guard Self.isSigned || source >= 0 else { - return nil - } - - // Is 'source' entirely representable in Low? - if let low = Low(exactly: source.magnitude) { - self.init(source._isNegative ? (~0, low._twosComplement) : (0, low)) - } else { - // At this point we know source.bitWidth > High.bitWidth, or else we - // would've taken the first branch. - let lowInT = source & T(~0 as Low) - let highInT = source >> Low.bitWidth - - let low = Low(lowInT) - guard let high = High(exactly: highInT) else { - return nil - } - self.init(high: high, low: low) - } - } - - internal static func * (_ lhs: Self, _ rhs: Self) -> Self { - let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs) - precondition(!overflow, "Overflow in *") - return result - } - - internal static func *= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs) - precondition(!overflow, "Overflow in *=") - lhs = result - } -} - -extension _Int128 { - internal struct Words { - internal var _value: _Int128 - - internal init(_ value: _Int128) { - self._value = value - } - } -} - -extension _Int128.Words: RandomAccessCollection { - internal typealias Element = UInt - internal typealias Index = Int - internal typealias Indices = Range - internal typealias SubSequence = Slice - - internal var count: Int { 128 / UInt.bitWidth } - internal var startIndex: Int { 0 } - internal var endIndex: Int { count } - internal var indices: Indices { startIndex ..< endIndex } - internal func index(after i: Int) -> Int { i + 1 } - internal func index(before i: Int) -> Int { i - 1 } - - internal subscript(position: Int) -> UInt { - get { - precondition(position >= 0 && position < endIndex, - "Word index out of range") - let shift = position &* UInt.bitWidth - assert(shift < _Int128.bitWidth) - - let r = _wideMaskedShiftRight( - _value.components, UInt64(truncatingIfNeeded: shift)) - return r.low._lowWord - } - } -} - -extension _Int128: FixedWidthInteger { - @_transparent - internal var _lowWord: UInt { - low._lowWord - } - - internal var words: Words { - Words(self) - } - - internal static var isSigned: Bool { - true - } - - internal static var max: Self { - self.init(high: High.max, low: Low.max) - } - - internal static var min: Self { - self.init(high: High.min, low: Low.min) - } - - internal static var bitWidth: Int { 128 } - - internal func addingReportingOverflow( - _ rhs: Self - ) -> (partialValue: Self, overflow: Bool) { - let (r, o) = _wideAddReportingOverflow22(self.components, rhs.components) - return (Self(r), o) - } - - internal func subtractingReportingOverflow( - _ rhs: Self - ) -> (partialValue: Self, overflow: Bool) { - let (r, o) = _wideSubtractReportingOverflow22( - self.components, rhs.components) - return (Self(r), o) - } - - internal func multipliedReportingOverflow( - by rhs: Self - ) -> (partialValue: Self, overflow: Bool) { - let isNegative = (self._isNegative != rhs._isNegative) - let (p, overflow) = self.magnitude.multipliedReportingOverflow( - by: rhs.magnitude) - let r = _Int128(bitPattern: isNegative ? p._twosComplement : p) - return (r, overflow || (isNegative != r._isNegative)) - } - - /// Returns the product of this value and the given 64-bit value, along with a - /// Boolean value indicating whether overflow occurred in the operation. - internal func multipliedReportingOverflow( - by other: UInt64 - ) -> (partialValue: Self, overflow: Bool) { - let isNegative = self._isNegative - let (p, overflow) = self.magnitude.multipliedReportingOverflow(by: other) - let r = _Int128(bitPattern: isNegative ? p._twosComplement : p) - return (r, overflow || (isNegative != r._isNegative)) - } - - internal func multiplied(by other: UInt64) -> Self { - let r = multipliedReportingOverflow(by: other) - precondition(!r.overflow, "Overflow in multiplication") - return r.partialValue - } - - internal func quotientAndRemainder( - dividingBy other: Self - ) -> (quotient: Self, remainder: Self) { - let (q, r) = _wideDivide22( - self.magnitude.components, by: other.magnitude.components) - let quotient = Self.Magnitude(q) - let remainder = Self.Magnitude(r) - let isNegative = (self.high._isNegative != other.high._isNegative) - let quotient_ = (isNegative - ? quotient == Self.min.magnitude ? Self.min : 0 - Self(quotient) - : Self(quotient)) - let remainder_ = (self.high._isNegative - ? 0 - Self(remainder) - : Self(remainder)) - return (quotient_, remainder_) - } - - internal func dividedReportingOverflow( - by other: Self - ) -> (partialValue: Self, overflow: Bool) { - if other == Self.zero { - return (self, true) - } - if Self.isSigned && other == -1 && self == .min { - return (self, true) - } - return (quotientAndRemainder(dividingBy: other).quotient, false) - } - - internal func remainderReportingOverflow( - dividingBy other: Self - ) -> (partialValue: Self, overflow: Bool) { - if other == Self.zero { - return (self, true) - } - if Self.isSigned && other == -1 && self == .min { - return (0, true) - } - return (quotientAndRemainder(dividingBy: other).remainder, false) - } - - internal func multipliedFullWidth( - by other: Self - ) -> (high: Self, low: Magnitude) { - let isNegative = Self.isSigned && (self._isNegative != other._isNegative) - - func sum(_ x: Low, _ y: Low, _ z: Low) -> (high: Low, low: Low) { - let (sum1, overflow1) = x.addingReportingOverflow(y) - let (sum2, overflow2) = sum1.addingReportingOverflow(z) - let carry: Low = (overflow1 ? 1 : 0) + (overflow2 ? 1 : 0) - return (carry, sum2) - } - - let lhs = self.magnitude - let rhs = other.magnitude - - let a = rhs.low.multipliedFullWidth(by: lhs.low) - let b = rhs.low.multipliedFullWidth(by: lhs.high) - let c = rhs.high.multipliedFullWidth(by: lhs.low) - let d = rhs.high.multipliedFullWidth(by: lhs.high) - - let mid1 = sum(a.high, b.low, c.low) - let mid2 = sum(b.high, c.high, d.low) - - let low = _UInt128(high: mid1.low, low: a.low) - let high = _Int128( - high: High(mid2.high + d.high), - low: mid1.high + mid2.low) - - if isNegative { - let (lowComplement, overflow) = (~low).addingReportingOverflow(.one) - return (~high + (overflow ? 1 : 0), lowComplement) - } else { - return (high, low) - } - } - - internal func dividingFullWidth( - _ dividend: (high: Self, low: Self.Magnitude) - ) -> (quotient: Self, remainder: Self) { - let m = _wideMagnitude22(dividend) - let (quotient, remainder) = self.magnitude.dividingFullWidth(m) - - let isNegative = (self.high._isNegative != dividend.high.high._isNegative) - let quotient_ = (isNegative - ? (quotient == Self.min.magnitude ? Self.min : 0 - Self(quotient)) - : Self(quotient)) - let remainder_ = (dividend.high.high._isNegative - ? 0 - Self(remainder) - : Self(remainder)) - return (quotient_, remainder_) - } - - #if false // This triggers an unexpected type checking issue with `~0` in an - // lldb test - internal static prefix func ~(x: Self) -> Self { - Self(high: ~x.high, low: ~x.low) - } - #endif - - internal static func &= (_ lhs: inout Self, _ rhs: Self) { - lhs.low &= rhs.low - lhs.high &= rhs.high - } - - internal static func |= (_ lhs: inout Self, _ rhs: Self) { - lhs.low |= rhs.low - lhs.high |= rhs.high - } - - internal static func ^= (_ lhs: inout Self, _ rhs: Self) { - lhs.low ^= rhs.low - lhs.high ^= rhs.high - } - - internal static func <<= (_ lhs: inout Self, _ rhs: Self) { - if Self.isSigned && rhs._isNegative { - lhs >>= 0 - rhs - return - } - - // Shift is larger than this type's bit width. - if rhs.high != High.zero || rhs.low >= Self.bitWidth { - lhs = 0 - return - } - - lhs &<<= rhs - } - - internal static func >>= (_ lhs: inout Self, _ rhs: Self) { - if Self.isSigned && rhs._isNegative { - lhs <<= 0 - rhs - return - } - - // Shift is larger than this type's bit width. - if rhs.high != High.zero || rhs.low >= Self.bitWidth { - lhs = lhs._isNegative ? ~0 : 0 - return - } - - lhs &>>= rhs - } - - internal static func &<< (lhs: Self, rhs: Self) -> Self { - Self(_wideMaskedShiftLeft(lhs.components, rhs.low)) - } - - internal static func &>> (lhs: Self, rhs: Self) -> Self { - Self(_wideMaskedShiftRight(lhs.components, rhs.low)) - } - - internal static func &<<= (lhs: inout Self, rhs: Self) { - _wideMaskedShiftLeft(&lhs.components, rhs.low) - } - - internal static func &>>= (lhs: inout Self, rhs: Self) { - _wideMaskedShiftRight(&lhs.components, rhs.low) - } - - internal static func / ( - _ lhs: Self, _ rhs: Self - ) -> Self { - var lhs = lhs - lhs /= rhs - return lhs - } - - internal static func /= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.dividedReportingOverflow(by: rhs) - precondition(!overflow, "Overflow in /=") - lhs = result - } - - internal static func % ( - _ lhs: Self, _ rhs: Self - ) -> Self { - var lhs = lhs - lhs %= rhs - return lhs - } - - internal static func %= (_ lhs: inout Self, _ rhs: Self) { - let (result, overflow) = lhs.remainderReportingOverflow(dividingBy: rhs) - precondition(!overflow, "Overflow in %=") - lhs = result - } - - internal init(_truncatingBits bits: UInt) { - low = Low(_truncatingBits: bits) - high = High(_truncatingBits: bits >> UInt(Low.bitWidth)) - } - - internal init(integerLiteral x: Int64) { - self.init(x) - } - - internal var leadingZeroBitCount: Int { - (high == High.zero - ? High.bitWidth + low.leadingZeroBitCount - : high.leadingZeroBitCount) - } - - internal var trailingZeroBitCount: Int { - (low == Low.zero - ? Low.bitWidth + high.trailingZeroBitCount - : low.trailingZeroBitCount) - } - - internal var nonzeroBitCount: Int { - high.nonzeroBitCount + low.nonzeroBitCount - } - - internal var byteSwapped: Self { - Self( - high: High(truncatingIfNeeded: low.byteSwapped), - low: Low(truncatingIfNeeded: high.byteSwapped)) - } -} - -extension _Int128: Sendable {} - -extension BinaryInteger { - @inline(__always) - fileprivate var _isNegative: Bool { self < Self.zero } -} - -extension FixedWidthInteger { - @inline(__always) - fileprivate var _twosComplement: Self { - ~self &+ 1 - } -} - -private typealias _Wide2 = - (high: F, low: F.Magnitude) - -private typealias _Wide3 = - (high: F, mid: F.Magnitude, low: F.Magnitude) - -private typealias _Wide4 = - (high: _Wide2, low: (high: F.Magnitude, low: F.Magnitude)) - -private func _wideMagnitude22( - _ v: _Wide2 -) -> _Wide2 { - var result = (high: F.Magnitude(truncatingIfNeeded: v.high), low: v.low) - guard F.isSigned && v.high._isNegative else { return result } - result.high = ~result.high - result.low = ~result.low - return _wideAddReportingOverflow22(result, (high: 0, low: 1)).partialValue -} - -private func _wideAddReportingOverflow22( - _ lhs: _Wide2, _ rhs: _Wide2 -) -> (partialValue: _Wide2, overflow: Bool) { - let (low, lowOverflow) = lhs.low.addingReportingOverflow(rhs.low) - let (high, highOverflow) = lhs.high.addingReportingOverflow(rhs.high) - let overflow = highOverflow || high == F.max && lowOverflow - let result = (high: high &+ (lowOverflow ? 1 : 0), low: low) - return (partialValue: result, overflow: overflow) -} - -private func _wideAdd22( - _ lhs: inout _Wide2, _ rhs: _Wide2 -) { - let (result, overflow) = _wideAddReportingOverflow22(lhs, rhs) - precondition(!overflow, "Overflow in +") - lhs = result -} - -private func _wideAddReportingOverflow33( - _ lhs: _Wide3, _ rhs: _Wide3 -) -> ( - partialValue: _Wide3, - overflow: Bool -) { - let (low, lowOverflow) = - _wideAddReportingOverflow22((lhs.mid, lhs.low), (rhs.mid, rhs.low)) - let (high, highOverflow) = lhs.high.addingReportingOverflow(rhs.high) - let result = (high: high &+ (lowOverflow ? 1 : 0), mid: low.high, low: low.low) - let overflow = highOverflow || (high == F.max && lowOverflow) - return (partialValue: result, overflow: overflow) -} - -private func _wideSubtractReportingOverflow22( - _ lhs: _Wide2, _ rhs: _Wide2 -) -> (partialValue: (high: F, low: F.Magnitude), overflow: Bool) { - let (low, lowOverflow) = lhs.low.subtractingReportingOverflow(rhs.low) - let (high, highOverflow) = lhs.high.subtractingReportingOverflow(rhs.high) - let result = (high: high &- (lowOverflow ? 1 : 0), low: low) - let overflow = highOverflow || high == F.min && lowOverflow - return (partialValue: result, overflow: overflow) -} - -private func _wideSubtract22( - _ lhs: inout _Wide2, _ rhs: _Wide2 -) { - let (result, overflow) = _wideSubtractReportingOverflow22(lhs, rhs) - precondition(!overflow, "Overflow in -") - lhs = result -} - -private func _wideSubtractReportingOverflow33( - _ lhs: _Wide3, _ rhs: _Wide3 -) -> ( - partialValue: _Wide3, - overflow: Bool -) { - let (low, lowOverflow) = - _wideSubtractReportingOverflow22((lhs.mid, lhs.low), (rhs.mid, rhs.low)) - let (high, highOverflow) = lhs.high.subtractingReportingOverflow(rhs.high) - let result = (high: high &- (lowOverflow ? 1 : 0), mid: low.high, low: low.low) - let overflow = highOverflow || (high == F.min && lowOverflow) - return (partialValue: result, overflow: overflow) -} - -private func _wideMaskedShiftLeft( - _ lhs: _Wide2, _ rhs: F.Magnitude -) -> _Wide2 { - let bitWidth = F.bitWidth + F.Magnitude.bitWidth - assert(bitWidth.nonzeroBitCount == 1) - - // Mask rhs by the bit width of the wide value. - let rhs = rhs & F.Magnitude(bitWidth &- 1) - - guard rhs < F.Magnitude.bitWidth else { - let s = rhs &- F.Magnitude(F.Magnitude.bitWidth) - return (high: F(truncatingIfNeeded: lhs.low &<< s), low: 0) - } - - guard rhs != F.Magnitude.zero else { return lhs } - var high = lhs.high &<< F(rhs) - let rollover = F.Magnitude(F.bitWidth) &- rhs - high |= F(truncatingIfNeeded: lhs.low &>> rollover) - let low = lhs.low &<< rhs - return (high, low) -} - -private func _wideMaskedShiftLeft( - _ lhs: inout _Wide2, _ rhs: F.Magnitude -) { - lhs = _wideMaskedShiftLeft(lhs, rhs) -} - -private func _wideMaskedShiftRight( - _ lhs: _Wide2, _ rhs: F.Magnitude -) -> _Wide2 { - let bitWidth = F.bitWidth + F.Magnitude.bitWidth - assert(bitWidth.nonzeroBitCount == 1) - - // Mask rhs by the bit width of the wide value. - let rhs = rhs & F.Magnitude(bitWidth &- 1) - - guard rhs < F.bitWidth else { - let s = F(rhs &- F.Magnitude(F.bitWidth)) - return ( - high: lhs.high._isNegative ? ~0 : 0, - low: F.Magnitude(truncatingIfNeeded: lhs.high &>> s)) - } - - guard rhs != F.zero else { return lhs } - var low = lhs.low &>> rhs - let rollover = F(F.bitWidth) &- F(rhs) - low |= F.Magnitude(truncatingIfNeeded: lhs.high &<< rollover) - let high = lhs.high &>> rhs - return (high, low) -} - -private func _wideMaskedShiftRight( - _ lhs: inout _Wide2, _ rhs: F.Magnitude -) { - lhs = _wideMaskedShiftRight(lhs, rhs) -} - -/// Returns the quotient and remainder after dividing a triple-width magnitude -/// `lhs` by a double-width magnitude `rhs`. -/// -/// This operation is conceptually that described by Burnikel and Ziegler -/// (1998). -private func _wideDivide32( - _ lhs: _Wide3, by rhs: _Wide2 -) -> (quotient: F, remainder: _Wide2) { - // The following invariants are guaranteed to hold by dividingFullWidth or - // quotientAndRemainder before this function is invoked: - assert(lhs.high != F.zero) - assert(rhs.high.leadingZeroBitCount == 0) - assert((high: lhs.high, low: lhs.mid) < rhs) - - // Estimate the quotient with a 2/1 division using just the top digits. - var quotient = (lhs.high == rhs.high - ? F.max - : rhs.high.dividingFullWidth((high: lhs.high, low: lhs.mid)).quotient) - - // Compute quotient * rhs. - // TODO: This could be performed more efficiently. - let p1 = quotient.multipliedFullWidth(by: F(rhs.low)) - let p2 = quotient.multipliedFullWidth(by: rhs.high) - let product = _wideAddReportingOverflow33( - (high: F.zero, mid: F.Magnitude(p1.high), low: p1.low), - (high: p2.high, mid: p2.low, low: .zero)).partialValue - - // Compute the remainder after decrementing quotient as necessary. - var remainder = lhs - - while remainder < product { - quotient = quotient &- 1 - remainder = _wideAddReportingOverflow33( - remainder, - (high: F.zero, mid: F.Magnitude(rhs.high), low: rhs.low)).partialValue - } - remainder = _wideSubtractReportingOverflow33(remainder, product).partialValue - assert(remainder.high == 0) - return (quotient, (high: F(remainder.mid), low: remainder.low)) -} - -/// Returns the quotient and remainder after dividing a double-width -/// magnitude `lhs` by a double-width magnitude `rhs`. -private func _wideDivide22( - _ lhs: _Wide2, by rhs: _Wide2 -) -> (quotient: _Wide2, remainder: _Wide2) { - guard _fastPath(rhs > (F.zero, F.Magnitude.zero)) else { - fatalError("Division by zero") - } - guard rhs < lhs else { - if _fastPath(rhs > lhs) { return (quotient: (0, 0), remainder: lhs) } - return (quotient: (0, 1), remainder: (0, 0)) - } - - if lhs.high == F.zero { - let (quotient, remainder) = - lhs.low.quotientAndRemainder(dividingBy: rhs.low) - return ((0, quotient), (0, remainder)) - } - - if rhs.high == F.zero { - let (x, a) = lhs.high.quotientAndRemainder(dividingBy: F(rhs.low)) - let (y, b) = (a == F.zero - ? lhs.low.quotientAndRemainder(dividingBy: rhs.low) - : rhs.low.dividingFullWidth((F.Magnitude(a), lhs.low))) - return (quotient: (high: x, low: y), remainder: (high: 0, low: b)) - } - - // Left shift both rhs and lhs, then divide and right shift the remainder. - let shift = F.Magnitude(rhs.high.leadingZeroBitCount) - let rollover = F.Magnitude(F.bitWidth + F.Magnitude.bitWidth) &- shift - let rhs = _wideMaskedShiftLeft(rhs, shift) - let high = _wideMaskedShiftRight(lhs, rollover).low - let lhs = _wideMaskedShiftLeft(lhs, shift) - let (quotient, remainder) = _wideDivide32( - (F(high), F.Magnitude(lhs.high), lhs.low), by: rhs) - return ( - quotient: (high: 0, low: F.Magnitude(quotient)), - remainder: _wideMaskedShiftRight(remainder, shift)) -} - -/// Returns the quotient and remainder after dividing a quadruple-width -/// magnitude `lhs` by a double-width magnitude `rhs`. -private func _wideDivide42( - _ lhs: _Wide4, by rhs: _Wide2 -) -> (quotient: _Wide2, remainder: _Wide2) { - guard _fastPath(rhs > (F.zero, F.Magnitude.zero)) else { - fatalError("Division by zero") - } - guard _fastPath(rhs >= lhs.high) else { - fatalError("Division results in an overflow") - } - - if lhs.high == (F.zero, F.Magnitude.zero) { - return _wideDivide22((high: F(lhs.low.high), low: lhs.low.low), by: rhs) - } - - if rhs.high == F.zero { - let a = F.Magnitude(lhs.high.high) % rhs.low - let b = (a == F.Magnitude.zero - ? lhs.high.low % rhs.low - : rhs.low.dividingFullWidth((a, lhs.high.low)).remainder) - let (x, c) = (b == F.Magnitude.zero - ? lhs.low.high.quotientAndRemainder(dividingBy: rhs.low) - : rhs.low.dividingFullWidth((b, lhs.low.high))) - let (y, d) = (c == F.Magnitude.zero - ? lhs.low.low.quotientAndRemainder(dividingBy: rhs.low) - : rhs.low.dividingFullWidth((c, lhs.low.low))) - return (quotient: (high: F(x), low: y), remainder: (high: 0, low: d)) - } - - // Left shift both rhs and lhs, then divide and right shift the remainder. - let shift = F.Magnitude(rhs.high.leadingZeroBitCount) - let rollover = F.Magnitude(F.bitWidth + F.Magnitude.bitWidth) &- shift - let rhs = _wideMaskedShiftLeft(rhs, shift) - - let lh1 = _wideMaskedShiftLeft(lhs.high, shift) - let lh2 = _wideMaskedShiftRight(lhs.low, rollover) - let lhs = ( - high: (high: lh1.high | F(lh2.high), low: lh1.low | lh2.low), - low: _wideMaskedShiftLeft(lhs.low, shift)) - - if - lhs.high.high == F.Magnitude.zero, - (high: F(lhs.high.low), low: lhs.low.high) < rhs - { - let (quotient, remainder) = _wideDivide32( - (F(lhs.high.low), lhs.low.high, lhs.low.low), - by: rhs) - return ( - quotient: (high: 0, low: F.Magnitude(quotient)), - remainder: _wideMaskedShiftRight(remainder, shift)) - } - let (x, a) = _wideDivide32( - (lhs.high.high, lhs.high.low, lhs.low.high), by: rhs) - let (y, b) = _wideDivide32((a.high, a.low, lhs.low.low), by: rhs) - return ( - quotient: (high: x, low: F.Magnitude(y)), - remainder: _wideMaskedShiftRight(b, shift)) -} - - -extension _UInt128: UnsignedInteger {} -extension _Int128: SignedNumeric, SignedInteger {} - - -extension _Int128 { - internal func dividedBy1e18() -> (quotient: Self, remainder: Self) { - let m = _Int128(high: 664613997892457936, low: 8336148766501648893) - var q = self.multipliedFullWidth(by: m).high - q &>>= 55 - // Add 1 to q if self is negative - q &+= _Int128(bitPattern: _UInt128(bitPattern: self) &>> 127) - let r = self &- q &* (1000000000000000000 as _Int128) - return (q, r) - } -} -extension _Int128 { - internal func dividedBy1e15() -> (quotient: Self, remainder: Self) { - let m = _Int128(high: -8062150356639896359, low: 1125115960621402641) - var q = self.multipliedFullWidth(by: m).high - q &+= self - q &>>= 49 - // Add 1 to q if self is negative - q &+= _Int128(bitPattern: _UInt128(bitPattern: self) &>> 127) - let r = self &- q &* (1000000000000000 as _Int128) - return (q, r) - } -} -extension _Int128 { - internal func dividedBy1e12() -> (quotient: Self, remainder: Self) { - let m = _Int128(high: 2535301200456458802, low: 18325113820324532597) - var q = self.multipliedFullWidth(by: m).high - q &>>= 37 - // Add 1 to q if self is negative - q &+= _Int128(bitPattern: _UInt128(bitPattern: self) &>> 127) - let r = self &- q &* (1000000000000 as _Int128) - return (q, r) - } -} -extension _Int128 { - internal func dividedBy1e9() -> (quotient: Self, remainder: Self) { - let m = _Int128(high: 4951760157141521099, low: 11003425581274142745) - var q = self.multipliedFullWidth(by: m).high - q &>>= 28 - // Add 1 to q if self is negative - q &+= _Int128(bitPattern: _UInt128(bitPattern: self) &>> 127) - let r = self &- q &* (1000000000 as _Int128) - return (q, r) - } -} -extension _Int128 { - internal func dividedBy1e6() -> (quotient: Self, remainder: Self) { - let m = _Int128(high: 604462909807314587, low: 6513323971497958161) - var q = self.multipliedFullWidth(by: m).high - q &>>= 15 - // Add 1 to q if self is negative - q &+= _Int128(bitPattern: _UInt128(bitPattern: self) &>> 127) - let r = self &- q &* (1000000 as _Int128) - return (q, r) - } -} -extension _Int128 { - internal func dividedBy1e3() -> (quotient: Self, remainder: Self) { - let m = _Int128(high: 4722366482869645213, low: 12838933875301847925) - var q = self.multipliedFullWidth(by: m).high - q &>>= 8 - // Add 1 to q if self is negative - q &+= _Int128(bitPattern: _UInt128(bitPattern: self) &>> 127) - let r = self &- q &* (1000 as _Int128) - return (q, r) - } -} diff --git a/Sources/ClockShims/SuspendingClock.swift b/Sources/ClockShims/SuspendingClock.swift deleted file mode 100644 index 821ac1c7..00000000 --- a/Sources/ClockShims/SuspendingClock.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -import Swift -#if canImport(Darwin) -@_implementationOnly import Darwin -#elseif canImport(Glibc) -@_implementationOnly import Glibc -#else -#error("Unsupported platform") -#endif - -/// A clock that measures time that always increments but stops incrementing -/// while the system is asleep. -/// -/// `SuspendingClock` can be considered as a system awake time clock. The frame -/// of reference of the `Instant` may be bound machine boot or some other -/// locally defined reference point. This means that the instants are -/// only comparable on the same machine in the same booted session. -/// -/// This clock is suitable for high resolution measurements of execution. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -public struct SuspendingClock { - public struct Instant: Codable, Sendable { - internal var _value: Duration - - internal init(_value: Duration) { - self._value = _value - } - } - - public init() { } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension Clock where Self == SuspendingClock { - /// A clock that measures time that always increments but stops incrementing - /// while the system is asleep. - /// - /// try await Task.sleep(until: .now + .seconds(3), clock: .suspending) - /// - public static var suspending: SuspendingClock { return SuspendingClock() } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension SuspendingClock: Clock { - /// The current instant accounting for machine suspension. - public var now: SuspendingClock.Instant { - SuspendingClock.now - } - - /// The current instant accounting for machine suspension. - public static var now: SuspendingClock.Instant { - var ts = timespec() -#if canImport(Darwin) - clock_gettime(CLOCK_UPTIME_RAW, &ts) -#elseif canImport(Glibc) - clock_gettime(CLOCK_MONOTONIC_RAW, &ts) -#endif - return SuspendingClock.Instant(_value: - .seconds(ts.tv_sec) + .nanoseconds(ts.tv_nsec)) - } - - /// The minimum non-zero resolution between any two calls to `now`. - public var minimumResolution: Duration { - var ts = timespec() -#if canImport(Darwin) - clock_getres(CLOCK_UPTIME_RAW, &ts) -#elseif canImport(Glibc) - clock_getres(CLOCK_MONOTONIC, &ts) -#endif - return .seconds(ts.tv_sec) + .nanoseconds(ts.tv_nsec) - } - - /// Suspend task execution until a given deadline within a tolerance. - /// If no tolerance is specified then the system may adjust the deadline - /// to coalesce CPU wake-ups to more efficiently process the wake-ups in - /// a more power efficient manner. - /// - /// If the task is canceled before the time ends, this function throws - /// `CancellationError`. - /// - /// This function doesn't block the underlying thread. - public func sleep( - until deadline: Instant, tolerance: Duration? = nil - ) async throws { - let duration = deadline - .now - let (seconds, attoseconds) = duration.components - let nanoseconds = attoseconds / 1_000_000_000 + seconds * 1_000_000_000 - if nanoseconds > 0 { - try await Task.sleep(nanoseconds: UInt64(nanoseconds)) - } - } -} - -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) -extension SuspendingClock.Instant: InstantProtocol { - public static var now: SuspendingClock.Instant { SuspendingClock().now } - - public func advanced(by duration: Duration) -> SuspendingClock.Instant { - SuspendingClock.Instant(_value: _value + duration) - } - - public func duration(to other: SuspendingClock.Instant) -> Duration { - other._value - _value - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(_value) - } - - public static func == ( - _ lhs: SuspendingClock.Instant, _ rhs: SuspendingClock.Instant - ) -> Bool { - return lhs._value == rhs._value - } - - public static func < ( - _ lhs: SuspendingClock.Instant, _ rhs: SuspendingClock.Instant - ) -> Bool { - return lhs._value < rhs._value - } - - public static func + ( - _ lhs: SuspendingClock.Instant, _ rhs: Duration - ) -> SuspendingClock.Instant { - lhs.advanced(by: rhs) - } - - public static func += ( - _ lhs: inout SuspendingClock.Instant, _ rhs: Duration - ) { - lhs = lhs.advanced(by: rhs) - } - - public static func - ( - _ lhs: SuspendingClock.Instant, _ rhs: Duration - ) -> SuspendingClock.Instant { - lhs.advanced(by: .zero - rhs) - } - - public static func -= ( - _ lhs: inout SuspendingClock.Instant, _ rhs: Duration - ) { - lhs = lhs.advanced(by: .zero - rhs) - } - - public static func - ( - _ lhs: SuspendingClock.Instant, _ rhs: SuspendingClock.Instant - ) -> Duration { - rhs.duration(to: lhs) - } -} - diff --git a/Sources/ClockShims/Task.swift b/Sources/ClockShims/Task.swift deleted file mode 100644 index 14ebfaee..00000000 --- a/Sources/ClockShims/Task.swift +++ /dev/null @@ -1,23 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -extension Task where Success == Never, Failure == Never { - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) - @_disfavoredOverload - public static func sleep( - until deadine: C.Instant, - tolerance: C.Instant.Duration? = nil, - clock: C - ) async throws { - try await clock.sleep(until: deadine, tolerance: tolerance) - } -} From 1656fd88391d601795cf1ec88251db6c5d9e56c5 Mon Sep 17 00:00:00 2001 From: jamieQ Date: Fri, 25 Mar 2022 16:49:10 -0500 Subject: [PATCH 004/149] Fix typo in AsyncChannel documentation (#108) fix typo --- Sources/AsyncAlgorithms/AsyncChannel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index e45ab373..edbb046f 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -/// A channel for sending elements from on task to another with back pressure. +/// A channel for sending elements from one task to another with back pressure. /// /// The `AsyncChannel` class is intended to be used as a communication type between tasks, /// particularly when one task produces values and another task consumes those values. The back From f9369af30c4d1ea665e756b026c02fbf8a2c3b6d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 25 Mar 2022 14:49:20 -0700 Subject: [PATCH 005/149] Add instructions for building with swift 5.6 toolchains (#106) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index afe6004f..51344b1b 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,11 @@ Finally, add `import AsyncAlgorithms` to your source code. 1. Download the most recent development toolchain for your Linux distribution 2. Decompress the archive to a path in which the `swift` executable is in the binary search path environment variable (`$PATH`) 3. In the `swift-async-algorithms` directory run `swift build` or `swift test` accordingly + +### Building with Swift 5.6 + + 1. `git checkout swift-5.6` + 2. run `swift build` or `swift test` accordingly ## Source Stability From 0dfb244b1b3b662a56360f07a85b0eaaf3919132 Mon Sep 17 00:00:00 2001 From: Reza Shirazian Date: Sun, 27 Mar 2022 08:19:52 -0700 Subject: [PATCH 006/149] Fixing a few typos (#115) This PR fixes a few typos across various comments in the code. --- Sources/AsyncAlgorithms/AsyncBufferSequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncChain2Sequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncChain3Sequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncDebounceSequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncLazySequence.swift | 4 ++-- Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift | 4 ++-- Sources/AsyncAlgorithms/Rethrow.swift | 2 +- Sources/AsyncSequenceValidation/WorkQueue.swift | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift index 258b66ee..b5867ab0 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift @@ -203,7 +203,7 @@ extension AsyncSequence where Element: Sendable { /// /// Use the `buffer(_:)` method to account for `AsyncSequence` types that may produce elements faster /// than they are iterated. The `createBuffer` closure returns a backing buffer for storing elements and dealing with - /// behavioral charcteristics of the `buffer(_:)` algorithm. + /// behavioral characteristics of the `buffer(_:)` algorithm. /// /// - Parameter createBuffer: A closure that constructs a new `AsyncBuffer` actor to store buffered values. /// - Returns: An asynchronous sequence that buffers elements using the specified `AsyncBuffer`. diff --git a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift index ed7ab7e4..59d3cfee 100644 --- a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift @@ -15,7 +15,7 @@ /// - Parameters: /// - s1: The first asynchronous sequence. /// - s2: The second asynchronous sequence. -/// - Returns: An asynchonous sequence that iterates first over the elements of `s1`, and +/// - 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 { diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index c0929ee0..8185a223 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -16,7 +16,7 @@ /// - s1: The first asynchronous sequence. /// - s2: The second asynchronous sequence. /// - s3: The third asynchronous sequence. -/// - Returns: An asynchonous sequence that iterates first over the elements of `s1`, and +/// - 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 { diff --git a/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift index 9438a791..9ad9aefc 100644 --- a/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift @@ -11,7 +11,7 @@ extension AsyncSequence { /// Creates an asynchronous sequence that emits the latest element after a given quiescence period - /// has elapsed by using a spectified Clock. + /// has elapsed by using a specified Clock. @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence { AsyncDebounceSequence(self, interval: interval, tolerance: tolerance, clock: clock) diff --git a/Sources/AsyncAlgorithms/AsyncLazySequence.swift b/Sources/AsyncAlgorithms/AsyncLazySequence.swift index b8eed7e9..9c2aa1e2 100644 --- a/Sources/AsyncAlgorithms/AsyncLazySequence.swift +++ b/Sources/AsyncAlgorithms/AsyncLazySequence.swift @@ -22,10 +22,10 @@ extension Sequence { /// An asynchronous sequence composed from a synchronous sequence. /// /// Asynchronous lazy sequences can be used to interface existing or pre-calculated -/// data to interoperate with other asynchronous sequences and algoritms based on +/// data to interoperate with other asynchronous sequences and algorithms based on /// asynchronous sequences. /// -/// This functions similarly to `LazySequence` by accessing elemetns sequentially +/// This functions similarly to `LazySequence` by accessing elements sequentially /// in the iterator's `next()` method. @frozen public struct AsyncLazySequence: AsyncSequence { diff --git a/Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift index ac220ec2..eeaf0246 100644 --- a/Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift @@ -174,7 +174,7 @@ extension Merge2StateMachine.Either where Base1.Element == Base2.Element { /// An asynchronous sequence of elements from two underlying asynchronous sequences /// /// In a `AsyncMerge2Sequence` instance, the *i*th element is the *i*th element -/// resolved in sequential order out of the two underyling asynchronous sequences. +/// resolved in sequential order out of the two underlying asynchronous sequences. /// Use the `merge(_:_:)` function to create an `AsyncMerge2Sequence`. public struct AsyncMerge2Sequence: AsyncSequence, Sendable where diff --git a/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift index e34ce6c1..efbbf9a8 100644 --- a/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift @@ -23,7 +23,7 @@ where /// An asynchronous sequence of elements from three underlying asynchronous sequences /// /// In a `AsyncMerge3Sequence` instance, the *i*th element is the *i*th element -/// resolved in sequential order out of the two underyling asynchronous sequences. +/// resolved in sequential order out of the two underlying asynchronous sequences. /// Use the `merge(_:_:_:)` function to create an `AsyncMerge3Sequence`. public struct AsyncMerge3Sequence: AsyncSequence, Sendable where @@ -206,7 +206,7 @@ where return try await apply(task1, task2, nil) // 2 terminal - // these can be permuted in place since they dont need to run two or more tasks at once + // these can be permuted in place since they don't need to run two or more tasks at once case (.terminal, .terminal, .idle(var iterator3)): do { if let value = try await iterator3.next() { diff --git a/Sources/AsyncAlgorithms/Rethrow.swift b/Sources/AsyncAlgorithms/Rethrow.swift index 13c2e8bf..6edf4a41 100644 --- a/Sources/AsyncAlgorithms/Rethrow.swift +++ b/Sources/AsyncAlgorithms/Rethrow.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -// This is a hack around the fact that we dont have generic effects +// This is a hack around the fact that we don't have generic effects // alternatively in the use cases we would want `rethrows(unsafe)` // or something like that to avoid this nifty hack... diff --git a/Sources/AsyncSequenceValidation/WorkQueue.swift b/Sources/AsyncSequenceValidation/WorkQueue.swift index 66722c3b..929ff558 100644 --- a/Sources/AsyncSequenceValidation/WorkQueue.swift +++ b/Sources/AsyncSequenceValidation/WorkQueue.swift @@ -144,7 +144,7 @@ struct WorkQueue: Sendable { items.append(queue.removeFirst()) } queues[job] = queue - // if there is nothing left in this queue then dont bother with it anymore + // if there is nothing left in this queue then don't bother with it anymore if queue.count == 0 { jobsToRemove.insert(jobIndex) } From 739815f65d9f36b5694bdb382cc84813ca0054e9 Mon Sep 17 00:00:00 2001 From: Vikram Kriplaney Date: Mon, 28 Mar 2022 20:08:46 +0200 Subject: [PATCH 007/149] Fix zip example (#119) --- Guides/Zip.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Guides/Zip.md b/Guides/Zip.md index faedd099..b5fd0477 100644 --- a/Guides/Zip.md +++ b/Guides/Zip.md @@ -6,10 +6,10 @@ 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 +let appleFeed = URL(string: "http://www.example.com/ticker?symbol=AAPL")!.lines +let nasdaqFeed = URL(string: "http://www.example.com/ticker?symbol=^IXIC")!.lines -for try await (apple, nasdaq) = zip(appleFeed, nasdaqFeed) { +for try await (apple, nasdaq) in zip(appleFeed, nasdaqFeed) { print("APPL: \(apple) NASDAQ: \(nasdaq)") } ``` From a8bba845985396a4eeb6d9a14a19d44858f62056 Mon Sep 17 00:00:00 2001 From: Jordan Kay Date: Mon, 28 Mar 2022 20:02:51 -0400 Subject: [PATCH 008/149] Fix spelling and typos (#122) * Fix spelling * Fix typos --- Guides/CombineLatest.md | 6 +++--- Guides/Compacted.md | 2 +- Guides/Effects.md | 2 +- Guides/Joined.md | 2 +- Guides/Timer.md | 2 +- Guides/Zip.md | 2 +- README.md | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Guides/CombineLatest.md b/Guides/CombineLatest.md index 14621725..e0fd7f47 100644 --- a/Guides/CombineLatest.md +++ b/Guides/CombineLatest.md @@ -64,7 +64,7 @@ public struct AsyncCombineLatest3Sequence Date: Mon, 28 Mar 2022 17:09:22 -0700 Subject: [PATCH 009/149] #45 - Two sided performance test for merge (#114) This PR extends `measureSequenceThroughput` to take two outputs so functions like merge/ zip etc can be tested by both sides. --- .../Performance/TestThroughput.swift | 24 ++++----- .../Performance/ThroughputMeasurement.swift | 51 +++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index 95fd3be9..2d490cc9 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -15,13 +15,13 @@ import AsyncAlgorithms #if canImport(Darwin) final class TestThroughput: XCTestCase { func test_chain2() async { - await measureSequenceThroughput(output: 1) { - chain($0, [].async) + await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { + chain($0, $1) } } func test_chain3() async { - await measureSequenceThroughput(output: 1) { - chain($0, [].async, [].async) + await measureSequenceThroughput(firstOutput: 1, secondOutput: 2, thirdOutput: 3) { + chain($0, $1, $2) } } func test_compacted() async { @@ -40,13 +40,13 @@ final class TestThroughput: XCTestCase { } } func test_merge2() async { - await measureSequenceThroughput(output: 1) { - merge($0, (0..<10).async) + await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { + merge($0, $1) } } func test_merge3() async { - await measureSequenceThroughput(output: 1) { - merge($0, (0..<10).async, (0..<10).async) + await measureSequenceThroughput(firstOutput: 1, secondOutput: 2, thirdOutput: 3) { + merge($0, $1, $2) } } func test_removeDuplicates() async { @@ -55,13 +55,13 @@ final class TestThroughput: XCTestCase { } } func test_zip2() async { - await measureSequenceThroughput(output: 1) { - zip($0, Indefinite(value: 2).async) + await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { + zip($0, $1) } } func test_zip3() async { - await measureSequenceThroughput(output: 1) { - zip($0, Indefinite(value: 2).async, Indefinite(value: 3).async) + await measureSequenceThroughput(firstOutput: 1, secondOutput: 2, thirdOutput: 3) { + zip($0, $1, $2) } } } diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index c1fc811b..1ebf1e45 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -87,6 +87,57 @@ extension XCTestCase { } } + 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(), duration: sampleTime) + let secondInfSeq = InfiniteAsyncSequence(value: secondOutput(), duration: sampleTime) + let seq = sequenceBuilder(firstInfSeq, secondInfSeq) + + let exp = self.expectation(description: "Finished") + let iterTask = Task { + var eventCount = 0 + for try await _ in seq { + eventCount += 1 + } + metric.eventCount = eventCount + exp.fulfill() + return eventCount + } + usleep(UInt32(sampleTime * Double(USEC_PER_SEC))) + iterTask.cancel() + 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 { + let metric = _ThroughputMetric() + let sampleTime: Double = 0.1 + + measure(metrics: [metric]) { + let firstInfSeq = InfiniteAsyncSequence(value: firstOutput(), duration: sampleTime) + let secondInfSeq = InfiniteAsyncSequence(value: secondOutput(), duration: sampleTime) + let thirdInfSeq = InfiniteAsyncSequence(value: thirdOutput(), duration: sampleTime) + let seq = sequenceBuilder(firstInfSeq, secondInfSeq, thirdInfSeq) + + let exp = self.expectation(description: "Finished") + let iterTask = Task { + var eventCount = 0 + for try await _ in seq { + eventCount += 1 + } + metric.eventCount = eventCount + exp.fulfill() + return eventCount + } + usleep(UInt32(sampleTime * Double(USEC_PER_SEC))) + iterTask.cancel() + self.wait(for: [exp], timeout: sampleTime * 2) + } +} + public func measureSequenceThroughput( source: Source, _ sequenceBuilder: (Source) -> S) async where S: Sendable, Source: Sendable { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 From 9ce65b089c62ffb0e4e945e59c1c8004c984cc3f Mon Sep 17 00:00:00 2001 From: Tiago Lopes Date: Mon, 28 Mar 2022 21:54:26 -0300 Subject: [PATCH 010/149] Fix the README link to the Swift Forums (#123) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52230e2c..88c22465 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This package has three main goals: The foundation for AsyncAlgorithms was included in Swift 5.5 from [AsyncSequence](https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md). Swift 5.5 also brings the ability to use a natural `for/in` loop with `await` to process the values in an `AsyncSequence` and `Sequence`-equivalent API like `map` and `filter`. Structured concurrency allows us to write code where intermediate state is simply a local variable, `try` can be used directly on functions that `throw`, and generally treat the logic for asynchronous code similar to that of synchronous code. -This package is the home for these APIs. Development and API design take place on [GitHub](https://github.com/apple/swift-async-algorithms) and the [Swift Forums](https://forums.swift.org/c/related-projects/). +This package is the home for these APIs. Development and API design take place on [GitHub](https://github.com/apple/swift-async-algorithms) and the [Swift Forums](https://forums.swift.org/c/related-projects/swift-async-algorithms). ## Contents From 4b5a3ef0506ed28800fd485825f6e60a9d9c61d3 Mon Sep 17 00:00:00 2001 From: Reza Shirazian Date: Tue, 29 Mar 2022 09:10:51 -0700 Subject: [PATCH 011/149] measureSequenceThroughput-duration-clean-up (#125) duration and start property in measureSequenceThroughput is vestigial from early development. This PR cleans that up. --- .../Performance/ThroughputMeasurement.swift | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index 1ebf1e45..f9356aae 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -17,19 +17,12 @@ import Foundation public struct InfiniteAsyncSequence: AsyncSequence, Sendable { public typealias Element = Value let value: Value - let duration: Double public struct AsyncIterator : AsyncIteratorProtocol, Sendable { @usableFromInline let value: Value - @usableFromInline - let duration: Double - - @usableFromInline - var start: Double? = nil - @inlinable public mutating func next() async throws -> Element? { guard !Task.isCancelled else { @@ -39,7 +32,7 @@ public struct InfiniteAsyncSequence: AsyncSequence, Sendable { } } public func makeAsyncIterator() -> AsyncIterator { - return AsyncIterator(value: value, duration: duration) + return AsyncIterator(value: value) } } @@ -68,7 +61,7 @@ extension XCTestCase { let sampleTime: Double = 0.1 measure(metrics: [metric]) { - let infSeq = InfiniteAsyncSequence(value: output(), duration: sampleTime) + let infSeq = InfiniteAsyncSequence(value: output()) let seq = sequenceBuilder(infSeq) let exp = self.expectation(description: "Finished") @@ -92,8 +85,8 @@ extension XCTestCase { let sampleTime: Double = 0.1 measure(metrics: [metric]) { - let firstInfSeq = InfiniteAsyncSequence(value: firstOutput(), duration: sampleTime) - let secondInfSeq = InfiniteAsyncSequence(value: secondOutput(), duration: sampleTime) + let firstInfSeq = InfiniteAsyncSequence(value: firstOutput()) + let secondInfSeq = InfiniteAsyncSequence(value: secondOutput()) let seq = sequenceBuilder(firstInfSeq, secondInfSeq) let exp = self.expectation(description: "Finished") @@ -117,9 +110,9 @@ extension XCTestCase { let sampleTime: Double = 0.1 measure(metrics: [metric]) { - let firstInfSeq = InfiniteAsyncSequence(value: firstOutput(), duration: sampleTime) - let secondInfSeq = InfiniteAsyncSequence(value: secondOutput(), duration: sampleTime) - let thirdInfSeq = InfiniteAsyncSequence(value: thirdOutput(), duration: sampleTime) + 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") From b136d48e7fdef7689a993369f253d77376463126 Mon Sep 17 00:00:00 2001 From: Laszlo Teveli Date: Tue, 29 Mar 2022 19:01:55 +0200 Subject: [PATCH 012/149] Adjacent Pairs (#111) --- Guides/AdjacentPairs.md | 49 +++++++++++ Guides/Effects.md | 1 + README.md | 1 + .../AdjacentPairsSequence.swift | 87 +++++++++++++++++++ .../TestAdjacentPairs.swift | 60 +++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 Guides/AdjacentPairs.md create mode 100644 Sources/AsyncAlgorithms/AdjacentPairsSequence.swift create mode 100644 Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift diff --git a/Guides/AdjacentPairs.md b/Guides/AdjacentPairs.md new file mode 100644 index 00000000..13765e3b --- /dev/null +++ b/Guides/AdjacentPairs.md @@ -0,0 +1,49 @@ +# AdjacentPairs + +* Author(s): [László Teveli](https://github.com/tevelee) + +[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift)] + +The `adjacentPairs()` API serve the purpose of collecting adjacent values. This operation is available for any `AsyncSequence` by calling the `adjacentPairs()` method. + +```swift +extension AsyncSequence { + public func adjacentPairs() -> AsyncAdjacentPairsSequence +} +``` + +## Detailed Design + +The `adjacentPairs()` algorithm produces elements of tuple (size of 2), containing a pair of the original `Element` type. + +The interface for this algorithm is available on all `AsyncSequence` types. The returned `AsyncAdjacentPairsSequence` conditionally conforms to `Sendable`. + +Its iterator keeps track of the previous element returned in the `next()` function and updates it in every turn. + +```swift +for await (first, second) in (1...5).async.adjacentPairs() { + print("First: \(first), Second: \(second)") +} + +// First: 1, Second: 2 +// First: 2, Second: 3 +// First: 3, Second: 4 +// First: 4, Second: 5 +``` + +It composes well with the [Dictionary.init(_:uniquingKeysWith:)](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md) API that deals with `AsyncSequence` of tuples. + +```swift +Dictionary(uniqueKeysWithValues: url.lines.adjacentPairs()) +``` + +## Alternatives Considered + +This functionality is often written as a `zip` of a sequence together with itself, dropping its first element (`zip(source, source.dropFirst())`). + +It's such a dominant use-case, the [swift-algorithms](https://github.com/apple/swift-algorithms) package also [introduced](https://github.com/apple/swift-algorithms/pull/119) it to its collection of algorithms. + +## Credits/Inspiration + +The synchronous counterpart in [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md). diff --git a/Guides/Effects.md b/Guides/Effects.md index d14482e7..766d54de 100644 --- a/Guides/Effects.md +++ b/Guides/Effects.md @@ -1,5 +1,6 @@ | Type | Throws | Sendability | |-----------------------------------------------------|--------------|-------------| +| `AsyncAdjacentPairsSequence` | rethrows | Conditional | | `AsyncBufferedByteIterator` | throws | Sendable | | `AsyncBufferSequence` | rethrows | Conditional | | `AsyncBufferSequence.Iterator` | rethrows | Conditional | diff --git a/README.md b/README.md index 88c22465..e646c767 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This package is the home for these APIs. Development and API design take place o - [`AsyncBufferedByteIterator`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/BufferedBytes.md): A highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. #### Other useful asynchronous sequences +- [`adjacentPairs()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/AdjacentPairs.md): Collects tuples of adjacent elements. - [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Chunked.md): Collect values into chunks. - [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Compacted.md): Remove nil values from an asynchronous sequence. - [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values. diff --git a/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift new file mode 100644 index 00000000..2186e364 --- /dev/null +++ b/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// An `AsyncSequence` that iterates over the adjacent pairs of the original +/// `AsyncSequence`. +@frozen +public struct AsyncAdjacentPairsSequence: AsyncSequence { + public typealias Element = (Base.Element, Base.Element) + + @usableFromInline + let base: Base + + @inlinable + init(_ base: Base) { + self.base = base + } + + /// The iterator for an `AsyncAdjacentPairsSequence` instance. + @frozen + public struct Iterator: AsyncIteratorProtocol { + public typealias Element = (Base.Element, Base.Element) + + @usableFromInline + var base: Base.AsyncIterator + + @usableFromInline + internal var previousElement: Base.Element? + + @inlinable + init(_ base: Base.AsyncIterator) { + self.base = base + } + + @inlinable + public mutating func next() async rethrows -> (Base.Element, Base.Element)? { + if previousElement == nil { + previousElement = try await base.next() + } + + guard let previous = previousElement, let next = try await base.next() else { + return nil + } + + previousElement = next + return (previous, next) + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(base.makeAsyncIterator()) + } +} + +extension AsyncSequence { + /// An `AsyncSequence` that iterates over the adjacent pairs of the original + /// original `AsyncSequence`. + /// + /// ``` + /// for await (first, second) in (1...5).async.adjacentPairs() { + /// print("First: \(first), Second: \(second)") + /// } + /// + /// // First: 1, Second: 2 + /// // First: 2, Second: 3 + /// // First: 3, Second: 4 + /// // First: 4, Second: 5 + /// ``` + /// + /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements + /// or the original `AsyncSequence`. + @inlinable + public func adjacentPairs() -> AsyncAdjacentPairsSequence { + AsyncAdjacentPairsSequence(self) + } +} + +extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } +extension AsyncAdjacentPairsSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift new file mode 100644 index 00000000..ade9c8f4 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import XCTest +import AsyncAlgorithms + +final class TestAdjacentPairs: XCTestCase { + func test_adjacentPairs() async { + let source = 1...5 + let expected = [(1,2), (2,3), (3,4), (4,5)] + let sequence = source.async.adjacentPairs() + var actual: [(Int, Int)] = [] + for await item in sequence { + actual.append(item) + } + XCTAssertEqual(expected, actual) + } + + func test_empty() async { + let source = 0..<1 + let expected: [(Int, Int)] = [] + let sequence = source.async.adjacentPairs() + var actual: [(Int, Int)] = [] + for await item in sequence { + actual.append(item) + } + XCTAssertEqual(expected, actual) + } + + func test_cancellation() async { + let source = Indefinite(value: 0) + let sequence = source.async.adjacentPairs() + let finished = expectation(description: "finished") + let iterated = expectation(description: "iterated") + let task = Task { + var firstIteration = false + for await _ in sequence { + if !firstIteration { + firstIteration = true + iterated.fulfill() + } + } + finished.fulfill() + } + // ensure the other task actually starts + wait(for: [iterated], timeout: 1.0) + // cancellation should ensure the loop finishes + // without regards to the remaining underlying sequence + task.cancel() + wait(for: [finished], timeout: 1.0) + } +} From b8a8d5d2aeb1cbc3d1010f6461631c0e1a2f4232 Mon Sep 17 00:00:00 2001 From: Laszlo Teveli Date: Tue, 29 Mar 2022 21:15:04 +0200 Subject: [PATCH 013/149] Rename AdjacentPairsSequence.swift (#126) --- ...jacentPairsSequence.swift => AsyncAdjacentPairsSequence.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/AsyncAlgorithms/{AdjacentPairsSequence.swift => AsyncAdjacentPairsSequence.swift} (100%) diff --git a/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift similarity index 100% rename from Sources/AsyncAlgorithms/AdjacentPairsSequence.swift rename to Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift From 442d20e8c8955d11112102630325fb533a53edbd Mon Sep 17 00:00:00 2001 From: Tiago Lopes Date: Wed, 30 Mar 2022 14:20:19 -0300 Subject: [PATCH 014/149] Fix `TestRangeReplaceableCollection.swift` file name (#128) --- Guides/Collections.md | 2 +- ...bleCollection.swift => TestRangeReplaceableCollection.swift} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Tests/AsyncAlgorithmsTests/{TestRangeReplacableCollection.swift => TestRangeReplaceableCollection.swift} (96%) diff --git a/Guides/Collections.md b/Guides/Collections.md index 366918d8..8928c773 100644 --- a/Guides/Collections.md +++ b/Guides/Collections.md @@ -7,7 +7,7 @@ [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/SetAlgebra.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestDictionary.swift), -[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift), +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift), [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift), ] diff --git a/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift b/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift similarity index 96% rename from Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift rename to Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift index b0c1a8c6..c04683f8 100644 --- a/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift +++ b/Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift @@ -12,7 +12,7 @@ import XCTest import AsyncAlgorithms -final class TestRangeReplacableCollection: XCTestCase { +final class TestRangeReplaceableCollection: XCTestCase { func test_String() async { let source = "abc" let expected = source From f1581d097357c61420fb0920d04a6e093aa6baf6 Mon Sep 17 00:00:00 2001 From: Tiago Lopes Date: Wed, 30 Mar 2022 14:20:47 -0300 Subject: [PATCH 015/149] Fix a grammatical sentence in Collections.md (#127) --- Guides/Collections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guides/Collections.md b/Guides/Collections.md index 8928c773..917184d6 100644 --- a/Guides/Collections.md +++ b/Guides/Collections.md @@ -22,7 +22,7 @@ This type of functionality can be useful for examples and testing, and also for Three categories of initializers will be added to provide initializers for those three primary types: `Array`, `Dictionary` and `Set`. However these initializers can be written in a generic fashion such that they can apply to all similar collections. -`RangeReplaceableCollection` will gain a new that constructs a collection given an `AsyncSequence`. This will allow for creating arrays from asynchronous sequences but also allow for creating types like `Data` or `ContiguousArray`. Because of the nature of asynchronous sequences, this initializer must be asynchronous and declare that it rethrows errors from the base asynchronous sequence. +`RangeReplaceableCollection` will gain a new initializer that constructs a collection given an `AsyncSequence`. This will allow for creating arrays from asynchronous sequences but also allow for creating types like `Data` or `ContiguousArray`. Because of the nature of asynchronous sequences, this initializer must be asynchronous and declare that it rethrows errors from the base asynchronous sequence. ```swift extension RangeReplaceableCollection { From 6971c8fc67f3075744485ef56b77d451da45fd1f Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Fri, 1 Apr 2022 10:24:42 -0600 Subject: [PATCH 016/149] Update README.md (#134) Insert a forgotten word. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e646c767..428d433d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ This package is the home for these APIs. Development and API design take place o Each algorithm has specific behavioral effects. For throwing effects these can either be if the sequence throws, does not throw, or rethrows errors. Sendability effects in some asynchronous sequences are conditional whereas others require the composed parts to all be sendable to satisfy a requirement of `Sendable`. The effects are [listed here](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Effects.md). -## Adding Swift Algorithms as a Dependency +## Adding Swift Async Algorithms as a Dependency To use the `AsyncAlgorithms` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: From 18749897a885e596d37efc1c4c911b4581d5b3cb Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 1 Apr 2022 11:40:44 -0700 Subject: [PATCH 017/149] #113 Define the first element of throttled sequences by not exceeding the rate (and fix a expectation misalignment of events) (#133) * #113 Define the first element of throttled sequences by not exceeding the rate (and fix a expectation misalignment of events) * Update guide for throttle referencing the first element and duration measurement --- Guides/Throttle.md | 2 ++ .../AsyncAlgorithms/AsyncThrottleSequence.swift | 7 +++++-- Sources/AsyncSequenceValidation/Expectation.swift | 2 +- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 14 +++++++------- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Guides/Throttle.md b/Guides/Throttle.md index cd3e0f45..cc8f71f6 100644 --- a/Guides/Throttle.md +++ b/Guides/Throttle.md @@ -77,6 +77,8 @@ extension AsyncThrottleSequence.Iterator: Sendable The `AsyncThrottleSequence` and its `Iterator` are conditionally `Sendable` if the base types comprising it are `Sendable`. +The time in which events are measured are from the previous emission if present. If a duration has elapsed between the last emission and the point in time the throttle is measured then that duration is counted as elapsed. The first element is considered not throttled because no interval can be constructed from the start to the first element. + ## Alternatives Considered It was considered to only provide the "latest" style APIs, however the reduction version grants more flexibility and can act as a funnel to the implementations of `latest`. diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index c8894eba..4d53f77d 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -64,6 +64,7 @@ extension AsyncThrottleSequence: AsyncSequence { /// The iterator for an `AsyncThrottleSequence` instance. public struct Iterator: AsyncIteratorProtocol { var base: Base.AsyncIterator + var last: C.Instant? let interval: C.Instant.Duration let clock: C let reducing: @Sendable (Reduced?, Base.Element) async -> Reduced @@ -77,13 +78,15 @@ extension AsyncThrottleSequence: AsyncSequence { public mutating func next() async rethrows -> Reduced? { var reduced: Reduced? - let start = clock.now + let start = last ?? clock.now repeat { guard let element = try await base.next() else { return nil } let reduction = await reducing(reduced, element) - if start.duration(to: clock.now) >= interval { + let now = clock.now + if start.duration(to: now) >= interval || last == nil { + last = now return reduction } else { reduced = reduction diff --git a/Sources/AsyncSequenceValidation/Expectation.swift b/Sources/AsyncSequenceValidation/Expectation.swift index 804b224a..ca682b99 100644 --- a/Sources/AsyncSequenceValidation/Expectation.swift +++ b/Sources/AsyncSequenceValidation/Expectation.swift @@ -42,7 +42,7 @@ extension AsyncSequenceValidationDiagram { } func reconstitute(_ events: [Clock.Instant : [Result]], theme: Theme, end: Clock.Instant) -> String { - var now = Clock.Instant(when: .zero) + var now = Clock.Instant(when: .steps(1)) // adjust for the offset index var reconstituted = "" while now <= end { if let results = events[now] { diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index b46e6bf9..972f04c1 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -49,7 +49,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) - "-b-d-f-h-j-|" + "a-c-e-g-i-k|" } } @@ -57,7 +57,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) - "-a-c-e-g-i-|" + "a-b-d-f-h-j|" } } @@ -65,7 +65,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) - "--c--f--i--|" + "a--d--g--j-|" } } @@ -73,7 +73,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) - "--a--d--g--|" + "a--b--e--h-|" } } @@ -81,7 +81,7 @@ final class TestThrottle: XCTestCase { validate { "abcdef^hijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) - "-b-d-f^" + "a-c-e-^" } } @@ -89,7 +89,7 @@ final class TestThrottle: XCTestCase { validate { "abcdef^hijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) - "-a-c-e^" + "a-b-d-^" } } @@ -121,7 +121,7 @@ final class TestThrottle: XCTestCase { validate { "-a-b-c-d-e-f-g-h-i-j-k-|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) - "---b---d---f---h---j---|" + "-a---c---e---g---i---k-|" } } } From b6755e6313fcaf604e130ede974ba16aee700dd8 Mon Sep 17 00:00:00 2001 From: Yuka Ezura <2020337+ezura@users.noreply.github.com> Date: Fri, 8 Apr 2022 04:18:56 +0900 Subject: [PATCH 018/149] Fix typo (#138) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 428d433d..4b1e2536 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ This package is the home for these APIs. Development and API design take place o - [`RangeReplaceableCollection.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): Creates a new instance of a collection containing the elements of an asynchronous sequence. - [`Dictionary.init(uniqueKeysWithValues:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): Creates a new dictionary from the key-value pairs in the given asynchronous sequence. - [`Dictionary.init(_:uniquingKeysWith:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): 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. -- [`Dictionary.init(grouping:by:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): /// 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. +- [`Dictionary.init(grouping:by:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): 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. - [`SetAlgebra.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): Creates a new set from an asynchronous sequence of items. From c174e4603ead188d5a7cb33f5c14ac02a75923fd Mon Sep 17 00:00:00 2001 From: k-kohey Date: Sat, 9 Apr 2022 00:47:25 +0900 Subject: [PATCH 019/149] fix merge example (#140) --- Guides/Merge.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Guides/Merge.md b/Guides/Merge.md index 60191694..bed7c4bb 100644 --- a/Guides/Merge.md +++ b/Guides/Merge.md @@ -6,10 +6,10 @@ Merges two or more asynchronous sequences sharing the same element type into one singular asynchronous sequence. ```swift -let appleFeed = URL("http://www.example.com/ticker?symbol=AAPL").lines.map { "AAPL: " + $0 } -let nasdaqFeed = URL("http://www.example.com/ticker?symbol=^IXIC").lines.map { "^IXIC: " + $0 } +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 } -for try await ticker = merge(appleFeed, nasdaqFeed) { +for try await ticker in merge(appleFeed, nasdaqFeed) { print(ticker) } ``` From 9d03d90a132dd7749a5de6a1324cbd38af6ff9d5 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 8 Apr 2022 10:44:06 -0700 Subject: [PATCH 020/149] #120 Ensure concurrent access to `AsyncBufferedByteIterator` does not crash from dangling pointers (#121) --- .../AsyncBufferedByteIterator.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift index e8ee7394..07063bf8 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift @@ -110,6 +110,24 @@ internal struct _AsyncBytesBuffer: @unchecked Sendable { } try Task.checkCancellation() do { + // If two tasks have access to this iterator then the references on + // the storage will be non uniquely owned. This means that any reload + // must happen into it's own fresh buffer. The consumption of those + // bytes between two tasks are inherently defined as potential + // duplication by the nature of sending that buffer across the two + // tasks - this means that the brief period in which they may be + // sharing non reloaded bytes is to be expected; basically in that + // edge case of making the iterator and sending that across to two + // places to iterate is asking for something bizzare and the answer + // should not be crash, but it definitely cannot be consistent. + // + // The unique ref check is here to prevent the potentials of a crashing + // secnario. + if !isKnownUniquelyReferenced(&storage) { + // The count is not mutated across invocations so the access is safe. + let capacity = storage.buffer.count + storage = Storage(capacity: capacity) + } let readSize: Int = try await readFunction(storage.buffer) if readSize == 0 { finished = true From 6a8aec2a1fe969ed60c33054109cbaf050cd7315 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 8 Apr 2022 10:44:30 -0700 Subject: [PATCH 021/149] #131 Cancel sends as if they were terminal events on task cancellation (#132) --- Sources/AsyncAlgorithms/AsyncChannel.swift | 87 ++++++++++++------- .../AsyncThrowingChannel.swift | 87 ++++++++++++------- Tests/AsyncAlgorithmsTests/TestChannel.swift | 30 +++++++ 3 files changed, 146 insertions(+), 58 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index edbb046f..c256fd09 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -155,40 +155,69 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } + func cancelSend() { + let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in + if state.terminal { + return ([], []) + } + state.terminal = true + switch state.emission { + case .idle: + return ([], []) + case .pending(let nexts): + state.emission = .idle + return (nexts, []) + case .awaiting(let nexts): + state.emission = .idle + return ([], nexts) + } + } + for send in sends { + send.resume(returning: nil) + } + for next in nexts { + next.continuation?.resume(returning: nil) + } + } + func _send(_ result: Result) async { - let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - if state.terminal { - return UnsafeResumption(continuation: continuation, success: nil) - } - switch result { - case .success(let value): - if value == nil { + await withTaskCancellationHandler { + cancelSend() + } operation: { + let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in + state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + if state.terminal { + return UnsafeResumption(continuation: continuation, success: nil) + } + switch result { + case .success(let value): + if value == nil { + state.terminal = true + } + case .failure: state.terminal = true } - case .failure: - state.terminal = true - } - switch state.emission { - case .idle: - state.emission = .pending([continuation]) - return nil - case .pending(var sends): - sends.append(continuation) - state.emission = .pending(sends) - return nil - case .awaiting(var nexts): - let next = nexts.removeFirst().continuation - if nexts.count == 0 { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) + switch state.emission { + case .idle: + state.emission = .pending([continuation]) + return nil + case .pending(var sends): + sends.append(continuation) + state.emission = .pending(sends) + return nil + case .awaiting(var nexts): + let next = nexts.removeFirst().continuation + if nexts.count == 0 { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } + return UnsafeResumption(continuation: continuation, success: next) } - return UnsafeResumption(continuation: continuation, success: next) - } - }?.resume() + }?.resume() + } + continuation?.resume(with: result) } - continuation?.resume(with: result) } /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made. diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index a0d545ef..575d88e7 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -153,40 +153,69 @@ public final class AsyncThrowingChannel: Asyn } } + func cancelSend() { + let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in + if state.terminal { + return ([], []) + } + state.terminal = true + switch state.emission { + case .idle: + return ([], []) + case .pending(let nexts): + state.emission = .idle + return (nexts, []) + case .awaiting(let nexts): + state.emission = .idle + return ([], nexts) + } + } + for send in sends { + send.resume(returning: nil) + } + for next in nexts { + next.continuation?.resume(returning: nil) + } + } + func _send(_ result: Result) async { - let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - if state.terminal { - return UnsafeResumption(continuation: continuation, success: nil) - } - switch result { - case .success(let value): - if value == nil { + await withTaskCancellationHandler { + cancelSend() + } operation: { + let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in + state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + if state.terminal { + return UnsafeResumption(continuation: continuation, success: nil) + } + switch result { + case .success(let value): + if value == nil { + state.terminal = true + } + case .failure: state.terminal = true } - case .failure: - state.terminal = true - } - switch state.emission { - case .idle: - state.emission = .pending([continuation]) - return nil - case .pending(var sends): - sends.append(continuation) - state.emission = .pending(sends) - return nil - case .awaiting(var nexts): - let next = nexts.removeFirst().continuation - if nexts.count == 0 { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) + switch state.emission { + case .idle: + state.emission = .pending([continuation]) + return nil + case .pending(var sends): + sends.append(continuation) + state.emission = .pending(sends) + return nil + case .awaiting(var nexts): + let next = nexts.removeFirst().continuation + if nexts.count == 0 { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } + return UnsafeResumption(continuation: continuation, success: next) } - return UnsafeResumption(continuation: continuation, success: next) - } - }?.resume() + }?.resume() + } + continuation?.resume(with: result) } - continuation?.resume(with: result) } /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made. diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index e42d7ec4..50cb7626 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -117,6 +117,36 @@ final class TestChannel: XCTestCase { XCTAssertNil(value) } + func test_sendCancellation() async { + let channel = AsyncChannel() + let notYetDone = expectation(description: "not yet done") + notYetDone.isInverted = true + let done = expectation(description: "done") + let task = Task { + await channel.send(1) + notYetDone.fulfill() + done.fulfill() + } + wait(for: [notYetDone], timeout: 0.1) + task.cancel() + wait(for: [done], timeout: 1.0) + } + + func test_sendCancellation_throwing() async { + let channel = AsyncThrowingChannel() + let notYetDone = expectation(description: "not yet done") + notYetDone.isInverted = true + let done = expectation(description: "done") + let task = Task { + await channel.send(1) + notYetDone.fulfill() + done.fulfill() + } + wait(for: [notYetDone], timeout: 0.1) + task.cancel() + wait(for: [done], timeout: 1.0) + } + func test_cancellation_throwing() async throws { let channel = AsyncThrowingChannel() let ready = expectation(description: "ready") From 94a7ecec28d40d34004acc3cdc8340e6313c2f03 Mon Sep 17 00:00:00 2001 From: Kristoffer Johansson Date: Fri, 8 Apr 2022 23:15:55 +0200 Subject: [PATCH 022/149] Moves `XCTestCase.validate` into its own module (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move XCTestCase.validate into it’s own module * Remove unnecessary import statement --- Package.swift | 13 +++++++++++-- .../AsyncAlgorithms_XCTest}/ValidationTest.swift | 0 .../AsyncAlgorithmsTests/TestValidationTests.swift | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) rename {Tests/AsyncAlgorithmsTests/Support => Sources/AsyncAlgorithms_XCTest}/ValidationTest.swift (100%) diff --git a/Package.swift b/Package.swift index 088e9654..fd3a36fc 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,8 @@ let package = Package( products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), - .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]) + .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), + .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], dependencies: [], targets: [ @@ -22,9 +23,17 @@ let package = Package( name: "AsyncSequenceValidation", dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), .systemLibrary(name: "_CAsyncSequenceValidationSupport"), + .target( + name: "AsyncAlgorithms_XCTest", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], + swiftSettings: [ + .unsafeFlags([ + "-Xfrontend", "-disable-availability-checking" + ]) + ]), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], swiftSettings: [ .unsafeFlags([ "-Xfrontend", "-disable-availability-checking" diff --git a/Tests/AsyncAlgorithmsTests/Support/ValidationTest.swift b/Sources/AsyncAlgorithms_XCTest/ValidationTest.swift similarity index 100% rename from Tests/AsyncAlgorithmsTests/Support/ValidationTest.swift rename to Sources/AsyncAlgorithms_XCTest/ValidationTest.swift diff --git a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift index 2f42c5f2..f60259c1 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift @@ -12,6 +12,7 @@ import XCTest import AsyncAlgorithms import AsyncSequenceValidation +@testable import AsyncAlgorithms_XCTest final class TestValidationDiagram: XCTestCase { func test_diagram() { From a3576e669ba8316143646d08bcfad941171979b0 Mon Sep 17 00:00:00 2001 From: Kyle <43724855+Kyle-Ye@users.noreply.github.com> Date: Tue, 12 Apr 2022 05:25:33 +0800 Subject: [PATCH 023/149] Add docc support and publish documentation to GitHub pages (#137) * Move Guides into docc base location * Add Github gh-page support * Conditionally add docc-plugin dependency * Temp skip AsyncSequenceValidation target --- Package.swift | 15 ++++ .../AsyncAlgorithms.docc/AsyncAlgorithms.md | 36 ++++++++++ .../Guides}/AdjacentPairs.md | 0 .../Guides}/BufferedBytes.md | 0 .../AsyncAlgorithms.docc/Guides}/Chain.md | 0 .../AsyncAlgorithms.docc/Guides}/Channel.md | 0 .../AsyncAlgorithms.docc/Guides}/Chunked.md | 0 .../Guides}/Collections.md | 0 .../Guides}/CombineLatest.md | 0 .../AsyncAlgorithms.docc/Guides}/Compacted.md | 0 .../AsyncAlgorithms.docc/Guides}/Debounce.md | 0 .../AsyncAlgorithms.docc/Guides}/Effects.md | 0 .../Guides}/Intersperse.md | 0 .../AsyncAlgorithms.docc/Guides}/Joined.md | 0 .../AsyncAlgorithms.docc/Guides}/Lazy.md | 0 .../AsyncAlgorithms.docc/Guides}/Merge.md | 0 .../Guides}/Reductions.md | 0 .../Guides}/RemoveDuplicates.md | 0 .../AsyncAlgorithms.docc/Guides}/Select.md | 1 - .../AsyncAlgorithms.docc/Guides}/Throttle.md | 0 .../AsyncAlgorithms.docc/Guides}/Timer.md | 0 .../AsyncAlgorithms.docc/Guides}/Zip.md | 0 .../AsyncSequenceValidation.md | 14 ++++ .../Validation.md | 0 bin/update-gh-pages-documentation-site | 71 +++++++++++++++++++ 25 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/AdjacentPairs.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/BufferedBytes.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Chain.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Channel.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Chunked.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Collections.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/CombineLatest.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Compacted.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Debounce.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Effects.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Intersperse.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Joined.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Lazy.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Merge.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Reductions.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/RemoveDuplicates.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Select.md (99%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Throttle.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Timer.md (100%) rename {Guides => Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides}/Zip.md (100%) create mode 100644 Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/AsyncSequenceValidation.md rename {Guides => Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc}/Validation.md (100%) create mode 100755 bin/update-gh-pages-documentation-site diff --git a/Package.swift b/Package.swift index fd3a36fc..afc2378f 100644 --- a/Package.swift +++ b/Package.swift @@ -41,3 +41,18 @@ let package = Package( ]), ] ) + +#if canImport(Darwin) +import Darwin +let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil +#elseif canImport(Glibc) +import Glibc +let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil +#else +let buildingDocs = false +#endif + +// Only require the docc plugin when building documentation +package.dependencies += buildingDocs ? [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), +] : [] diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md new file mode 100644 index 00000000..8a0cbbf7 --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md @@ -0,0 +1,36 @@ +# ``AsyncAlgorithms`` + +**Swift Async Algorithms** is an open-source package of asynchronous sequence and advanced algorithms that involve concurrency, along with their related types. + +## Overview + +This package has three main goals: + +- First-class integration with `async/await` +- Provide a home for time-based algorithms +- Be cross-platform and open source + +## Topics + +### Getting Started + +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- diff --git a/Guides/AdjacentPairs.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/AdjacentPairs.md similarity index 100% rename from Guides/AdjacentPairs.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/AdjacentPairs.md diff --git a/Guides/BufferedBytes.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md similarity index 100% rename from Guides/BufferedBytes.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md diff --git a/Guides/Chain.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md similarity index 100% rename from Guides/Chain.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md diff --git a/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md similarity index 100% rename from Guides/Channel.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md diff --git a/Guides/Chunked.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md similarity index 100% rename from Guides/Chunked.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md diff --git a/Guides/Collections.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md similarity index 100% rename from Guides/Collections.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md diff --git a/Guides/CombineLatest.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md similarity index 100% rename from Guides/CombineLatest.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md diff --git a/Guides/Compacted.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Compacted.md similarity index 100% rename from Guides/Compacted.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Compacted.md diff --git a/Guides/Debounce.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md similarity index 100% rename from Guides/Debounce.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md diff --git a/Guides/Effects.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md similarity index 100% rename from Guides/Effects.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md diff --git a/Guides/Intersperse.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md similarity index 100% rename from Guides/Intersperse.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md diff --git a/Guides/Joined.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Joined.md similarity index 100% rename from Guides/Joined.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Joined.md diff --git a/Guides/Lazy.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md similarity index 100% rename from Guides/Lazy.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md diff --git a/Guides/Merge.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md similarity index 100% rename from Guides/Merge.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md diff --git a/Guides/Reductions.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Reductions.md similarity index 100% rename from Guides/Reductions.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Reductions.md diff --git a/Guides/RemoveDuplicates.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RemoveDuplicates.md similarity index 100% rename from Guides/RemoveDuplicates.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RemoveDuplicates.md diff --git a/Guides/Select.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md similarity index 99% rename from Guides/Select.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md index 05982011..fb0389cc 100644 --- a/Guides/Select.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md @@ -50,5 +50,4 @@ This means that `withTaskGroup` is highly suited to run work in parallel, wherea ## Future Directions - ## Credits/Inspiration diff --git a/Guides/Throttle.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Throttle.md similarity index 100% rename from Guides/Throttle.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Throttle.md diff --git a/Guides/Timer.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md similarity index 100% rename from Guides/Timer.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md diff --git a/Guides/Zip.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md similarity index 100% rename from Guides/Zip.md rename to Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md diff --git a/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/AsyncSequenceValidation.md b/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/AsyncSequenceValidation.md new file mode 100644 index 00000000..e1c0d3f2 --- /dev/null +++ b/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/AsyncSequenceValidation.md @@ -0,0 +1,14 @@ +# ``AsyncSequenceValidation`` + + +## Overview + +Testing is a critical area of focus for any package to make it robust, catch bugs, and explain the expected behaviors in a documented manner. Testing things that are asynchronous can be difficult, testing things that are asynchronous multiple times can be even more difficult. + +Types that implement `AsyncSequence` can often be described in deterministic actions given particular inputs. For the inputs, the events can be described as a discrete set: values, errors being thrown, the terminal state of returning a `nil` value from the iterator, or advancing in time and not doing anything. Likewise, the expected output has a discrete set of events: values, errors being caught, the terminal state of receiving a `nil` value from the iterator, or advancing in time and not doing anything. + +## Topics + +### Getting Started + +- diff --git a/Guides/Validation.md b/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/Validation.md similarity index 100% rename from Guides/Validation.md rename to Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/Validation.md diff --git a/bin/update-gh-pages-documentation-site b/bin/update-gh-pages-documentation-site new file mode 100755 index 00000000..0c1e59aa --- /dev/null +++ b/bin/update-gh-pages-documentation-site @@ -0,0 +1,71 @@ +#!/bin/bash +# +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2022 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for Swift project authors +# +# Updates the GitHub Pages documentation site thats published from the 'docs' +# subdirectory in the 'gh-pages' branch of this repository. +# +# This script should be run by someone with commit access to the 'gh-pages' branch +# at a regular frequency so that the documentation content on the GitHub Pages site +# is up-to-date with the content in this repo. +# + +export BUILDING_FOR_DOCUMENTATION_GENERATION=1 + +set -eu + +# A `realpath` alternative using the default C implementation. +filepath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} + +SWIFT_ASYNC_ALGORITHMS_ROOT="$(dirname $(dirname $(filepath $0)))" + +ASYNC_ALGORITHMS_BUILD_DIR="$SWIFT_ASYNC_ALGORITHMS_ROOT"/.build/async-algorithms-gh-pages-build + +# Set current directory to the repository root +cd "$SWIFT_ASYNC_ALGORITHMS_ROOT" + +# Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory +git fetch +git worktree add --checkout gh-pages origin/gh-pages + +# Pretty print DocC JSON output so that it can be consistently diffed between commits +export DOCC_JSON_PRETTYPRINT="YES" + +# Generate documentation for the 'AsyncAlgorithms' target and output it +# to the /docs subdirectory in the gh-pages worktree directory. +swift package \ + --allow-writing-to-directory "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" \ + generate-documentation \ + --target AsyncAlgorithms \ + --disable-indexing \ + --transform-for-static-hosting \ + --hosting-base-path swift-async-algorithms \ + --output-path "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" + +# Save the current commit we've just built documentation from in a variable +CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` + +# Commit and push our changes to the gh-pages branch +cd gh-pages +git add docs + +if [ -n "$(git status --porcelain)" ]; then + echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin." + git commit -m "Update GitHub Pages documentation site to $CURRENT_COMMIT_HASH" + git push origin HEAD:gh-pages +else + # No changes found, nothing to commit. + echo "No documentation changes found." +fi + +# Delete the git worktree we created +cd .. +git worktree remove gh-pages From 5b78eaf4236a93bb90de4e11ff7100779cd6ae70 Mon Sep 17 00:00:00 2001 From: Tiago Lopes Date: Mon, 11 Apr 2022 19:06:39 -0300 Subject: [PATCH 024/149] Fix some typos in Chain.md (#144) --- .../AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md index 60938b0a..190e6c3e 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md @@ -9,8 +9,8 @@ This operation is available for all `AsyncSequence` types who share the same `El ```swift let preamble = [ - "// Some header to add as a preamble" - "//" + "// Some header to add as a preamble", + "//", "" ].async let lines = chain(preamble, URL(fileURLWithPath: "/tmp/Sample.swift").lines) @@ -60,7 +60,7 @@ extension AsyncChain3Sequence.Iterator: Sendable where Base1.AsyncIterator: Send The `chain(_:...)` function takes two or more sequences as arguments. -The resulting `AsyncChainSequence` type is an asynchronous sequence, with conditional conformance to `Sendable` when the arguments conform. +The resulting `AsyncChainSequence` type is an asynchronous sequence, with conditional conformance to `Sendable` when the arguments also conform to it. When any of the asynchronous sequences being chained together come to their end of iteration, the `AsyncChainSequence` iteration proceeds to the next asynchronous sequence. When the last asynchronous sequence reaches the end of iteration, the `AsyncChainSequence` then ends its iteration. From b3d4ffb94c10b5394b4788fdd958fac9308d45f7 Mon Sep 17 00:00:00 2001 From: Tiago Lopes Date: Mon, 11 Apr 2022 20:53:54 -0300 Subject: [PATCH 025/149] Fix broken links to guides in README.md (#146) --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4b1e2536..e3c957a9 100644 --- a/README.md +++ b/README.md @@ -20,51 +20,51 @@ This package is the home for these APIs. Development and API design take place o #### Combining asynchronous sequences -- [`chain(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Chain.md): Concatenates two or more asynchronous sequences with the same element type. -- [`combineLatest(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/CombineLatest.md): Combines two or more asynchronous sequences into an asynchronous sequence producing a tuple of elements from those base asynchronous sequences that updates when any of the base sequences produce a value. -- [`merge(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Merge.md): Merges two or more asynchronous sequence into a single asynchronous sequence producing the elements of all of the underlying asynchronous sequences. -- [`zip(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Zip.md): Creates an asynchronous sequence of pairs built out of underlying asynchronous sequences. -- [`joined(separator:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Joined.md): Concatenated elements of an asynchronous sequence of asynchronous sequences, inserting the given separator between each element. +- [`chain(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md): Concatenates two or more asynchronous sequences with the same element type. +- [`combineLatest(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md): Combines two or more asynchronous sequences into an asynchronous sequence producing a tuple of elements from those base asynchronous sequences that updates when any of the base sequences produce a value. +- [`merge(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md): Merges two or more asynchronous sequence into a single asynchronous sequence producing the elements of all of the underlying asynchronous sequences. +- [`zip(_:...)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md): Creates an asynchronous sequence of pairs built out of underlying asynchronous sequences. +- [`joined(separator:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Joined.md): Concatenated elements of an asynchronous sequence of asynchronous sequences, inserting the given separator between each element. #### Creating asynchronous sequences -- [`async`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Lazy.md): Create an asynchronous sequence composed from a synchronous sequence. -- [`AsyncChannel`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Channel.md): An asynchronous sequence with back pressure sending semantics. -- [`AsyncThrowingChannel`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Channel.md): An asynchronous sequence with back pressure sending semantics that can emit failures. +- [`async`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md): Create an asynchronous sequence composed from a synchronous sequence. +- [`AsyncChannel`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md): An asynchronous sequence with back pressure sending semantics. +- [`AsyncThrowingChannel`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md): An asynchronous sequence with back pressure sending semantics that can emit failures. #### Performance optimized asynchronous iterators -- [`AsyncBufferedByteIterator`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/BufferedBytes.md): A highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. +- [`AsyncBufferedByteIterator`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md): A highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. #### Other useful asynchronous sequences -- [`adjacentPairs()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/AdjacentPairs.md): Collects tuples of adjacent elements. -- [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Chunked.md): Collect values into chunks. -- [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Compacted.md): Remove nil values from an asynchronous sequence. -- [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values. -- [`interspersed(with:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Intersperse.md): Place a value between every two elements of an asynchronous sequence. +- [`adjacentPairs()`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/AdjacentPairs.md): Collects tuples of adjacent elements. +- [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md): Collect values into chunks. +- [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Compacted.md): Remove nil values from an asynchronous sequence. +- [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values. +- [`interspersed(with:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md): Place a value between every two elements of an asynchronous sequence. #### Asynchronous Sequences that transact in time -- [`debounce(for:tolerance:clock:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Debounce.md): Emit values after a quiescence period has been reached. -- [`throttle(for:clock:reducing:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Throttle.md): Ensure a minimum interval has elapsed between events. -- [`AsyncTimerSequence`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Timer.md): Emit the value of now at a given interval repeatedly. +- [`debounce(for:tolerance:clock:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md): Emit values after a quiescence period has been reached. +- [`throttle(for:clock:reducing:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Throttle.md): Ensure a minimum interval has elapsed between events. +- [`AsyncTimerSequence`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md): Emit the value of now at a given interval repeatedly. #### Obtaining all values from an asynchronous sequence -- [`RangeReplaceableCollection.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): Creates a new instance of a collection containing the elements of an asynchronous sequence. -- [`Dictionary.init(uniqueKeysWithValues:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): Creates a new dictionary from the key-value pairs in the given asynchronous sequence. -- [`Dictionary.init(_:uniquingKeysWith:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): 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. -- [`Dictionary.init(grouping:by:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): 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. -- [`SetAlgebra.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md): Creates a new set from an asynchronous sequence of items. +- [`RangeReplaceableCollection.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): Creates a new instance of a collection containing the elements of an asynchronous sequence. +- [`Dictionary.init(uniqueKeysWithValues:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): Creates a new dictionary from the key-value pairs in the given asynchronous sequence. +- [`Dictionary.init(_:uniquingKeysWith:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): 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. +- [`Dictionary.init(grouping:by:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): 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. +- [`SetAlgebra.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): Creates a new set from an asynchronous sequence of items. #### Task management -- [`Task.select(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Select.md): Determine the first task to complete of a sequence of tasks. +- [`Task.select(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md): Determine the first task to complete of a sequence of tasks. #### Effects -Each algorithm has specific behavioral effects. For throwing effects these can either be if the sequence throws, does not throw, or rethrows errors. Sendability effects in some asynchronous sequences are conditional whereas others require the composed parts to all be sendable to satisfy a requirement of `Sendable`. The effects are [listed here](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Effects.md). +Each algorithm has specific behavioral effects. For throwing effects these can either be if the sequence throws, does not throw, or rethrows errors. Sendability effects in some asynchronous sequences are conditional whereas others require the composed parts to all be sendable to satisfy a requirement of `Sendable`. The effects are [listed here](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md). ## Adding Swift Async Algorithms as a Dependency From d2164b9ef6f1fe8c7d071847dd8465ca64769fec Mon Sep 17 00:00:00 2001 From: Tiago Lopes Date: Tue, 12 Apr 2022 11:23:05 -0300 Subject: [PATCH 026/149] Fix redundant where clauses in AsyncBufferSequence extension (#141) --- Sources/AsyncAlgorithms/AsyncBufferSequence.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift index b5867ab0..16082203 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift @@ -234,8 +234,8 @@ public struct AsyncBufferSequence wher } } -extension AsyncBufferSequence: Sendable where Base: Sendable, Base.AsyncIterator: Sendable, Base.Element: Sendable { } -extension AsyncBufferSequence.Iterator: Sendable where Base: Sendable, Base.AsyncIterator: Sendable, Base.Element: Sendable { } +extension AsyncBufferSequence: Sendable where Base: Sendable { } +extension AsyncBufferSequence.Iterator: Sendable where Base: Sendable { } extension AsyncBufferSequence: AsyncSequence { public typealias Element = Buffer.Output From 047ab6abef71bdb6758c7bed661a04c04f5b056a Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Fri, 15 Apr 2022 19:05:40 +0200 Subject: [PATCH 027/149] asyncChannel: fixes potential crashes when no more awaiting (#143) This commit fixes crashes where the state is "awaiting" with no more awaiting clients. The state is now set to .idle to reflect the reality. --- Sources/AsyncAlgorithms/AsyncChannel.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index c256fd09..f6d132f4 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -89,7 +89,11 @@ public final class AsyncChannel: AsyncSequence, Sendable { switch self { case .awaiting(var awaiting): let continuation = awaiting.remove(Awaiting(placeholder: generation))?.continuation - self = .awaiting(awaiting) + if awaiting.isEmpty { + self = .idle + } else { + self = .awaiting(awaiting) + } return continuation case .idle: self = .awaiting([Awaiting(cancelled: generation)]) @@ -145,7 +149,11 @@ public final class AsyncChannel: AsyncSequence, Sendable { nexts.remove(Awaiting(placeholder: generation)) cancelled = true } - state.emission = .awaiting(nexts) + if nexts.isEmpty { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } return nil } }?.resume() From 6d5d6319f46e3a83ae4107ea49b83f778afa284b Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Thu, 21 Apr 2022 20:10:14 +0200 Subject: [PATCH 028/149] asyncThrowingChannel: fixes potential crashes when no more awaiting (#150) This commit fixes crashes where the state is "awaiting" with no more awaiting clients. The state is now set to .idle to reflect the reality. --- Sources/AsyncAlgorithms/AsyncThrowingChannel.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 575d88e7..205729e3 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -88,7 +88,11 @@ public final class AsyncThrowingChannel: Asyn switch self { case .awaiting(var awaiting): let continuation = awaiting.remove(Awaiting(placeholder: generation))?.continuation - self = .awaiting(awaiting) + if awaiting.isEmpty { + self = .idle + } else { + self = .awaiting(awaiting) + } return continuation case .idle: self = .awaiting([Awaiting(cancelled: generation)]) @@ -143,7 +147,11 @@ public final class AsyncThrowingChannel: Asyn nexts.remove(Awaiting(placeholder: generation)) cancelled = true } - state.emission = .awaiting(nexts) + if nexts.isEmpty { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } return nil } }?.resume() From bbbe08d33123b1ca8bf7189e6b77cdd19fd864af Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Tue, 26 Apr 2022 02:50:09 +0200 Subject: [PATCH 029/149] zip: improve unit tests readability (#151) --- Tests/AsyncAlgorithmsTests/TestZip.swift | 131 +++++++++++++++-------- 1 file changed, 89 insertions(+), 42 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index b6343c16..90c5dcfa 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -13,111 +13,129 @@ import AsyncAlgorithms final class TestZip2: XCTestCase { - func test_zip() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_all_sequences_have_same_size() async { let a = [1, 2, 3] let b = ["a", "b", "c"] + let expected = Array(zip(a, b)) let actual = await Array(zip(a.async, b.async)) XCTAssertEqual(expected, actual) } - func test_zip_first_longer() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] + let expected = Array(zip(a, b)) let actual = await Array(zip(a.async, b.async)) XCTAssertEqual(expected, actual) } - func test_zip_second_longer() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] + let expected = Array(zip(a, b)) let actual = await Array(zip(a.async, b.async)) XCTAssertEqual(expected, actual) } - func test_iterate_past_end() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_all_sequences_have_same_size() async { let a = [1, 2, 3] let b = ["a", "b", "c"] let sequence = zip(a.async, b.async) var iterator = sequence.makeAsyncIterator() + + let expected = Array(zip(a, b)) var collected = [(Int, String)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a"), (2, "b"), (3, "c")], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_iterate_past_end_first_longer() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] let sequence = zip(a.async, b.async) var iterator = sequence.makeAsyncIterator() + + let expected = Array(zip(a, b)) var collected = [(Int, String)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a"), (2, "b"), (3, "c")], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_iterate_past_end_second_longer() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] let sequence = zip(a.async, b.async) var iterator = sequence.makeAsyncIterator() + + let expected = Array(zip(a, b)) var collected = [(Int, String)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a"), (2, "b"), (3, "c")], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_first_throwing() async throws { + func test_zip_produces_one_element_and_throws_when_first_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] let sequence = zip(a.async.map { try throwOn(2, $0) }, b.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a")] var collected = [(Int, String)]() do { while let item = try await iterator.next() { collected.append(item) } - XCTFail() + XCTFail("Zipped sequence should throw after one collected element") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([(1, "a")], collected) + XCTAssertEqual(expected, collected) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_second_throwing() async throws { + func test_zip_produces_one_element_and_throws_when_second_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] let sequence = zip(a.async, b.async.map { try throwOn("b", $0) }) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a")] var collected = [(Int, String)]() do { while let item = try await iterator.next() { collected.append(item) } - XCTFail() + XCTFail("Zipped sequence should throw after one collected element") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([(1, "a")], collected) + XCTAssertEqual(expected, collected) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_cancellation() async { + func test_zip_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") let sequence = zip(source1.async, source2.async) @@ -143,159 +161,188 @@ final class TestZip2: XCTestCase { } final class TestZip3: XCTestCase { - func test_zip() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_all_sequences_have_same_size() async { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3] + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] let actual = await Array(zip(a.async, b.async, c.async)) - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], actual) + XCTAssertEqual(expected, actual) } - func test_zip_first_longer() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] let c = [1, 2, 3] + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] let actual = await Array(zip(a.async, b.async, c.async)) - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], actual) + XCTAssertEqual(expected, actual) } - func test_zip_second_longer() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] let c = [1, 2, 3] + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] let actual = await Array(zip(a.async, b.async, c.async)) - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], actual) + XCTAssertEqual(expected, actual) } - func test_zip_third_longer() async { + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_third_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3, 4, 5] + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] let actual = await Array(zip(a.async, b.async, c.async)) - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], actual) + XCTAssertEqual(expected, actual) } - func test_iterate_past_end() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_all_sequences_have_same_size() async { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3] let sequence = zip(a.async, b.async, c.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] var collected = [(Int, String, Int)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_iterate_past_end_first_longer() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] let c = [1, 2, 3] let sequence = zip(a.async, b.async, c.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] var collected = [(Int, String, Int)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_iterate_past_end_second_longer() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] let c = [1, 2, 3] let sequence = zip(a.async, b.async, c.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] var collected = [(Int, String, Int)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_iterate_past_end_third_longer() async { + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_third_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3, 4, 5] let sequence = zip(a.async, b.async, c.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1), (2, "b", 2), (3, "c", 3)] var collected = [(Int, String, Int)]() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual([(1, "a", 1), (2, "b", 2), (3, "c", 3)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_first_throwing() async throws { + func test_zip_produces_one_element_and_throws_when_first_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3] let sequence = zip(a.async.map { try throwOn(2, $0) }, b.async, c.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1)] var collected = [(Int, String, Int)]() do { while let item = try await iterator.next() { collected.append(item) } - XCTFail() + XCTFail("Zipped sequence should throw after one collected element") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([(1, "a", 1)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_second_throwing() async throws { + func test_zip_produces_one_element_and_throws_when_second_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3] let sequence = zip(a.async, b.async.map { try throwOn("b", $0) }, c.async) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1)] var collected = [(Int, String, Int)]() do { while let item = try await iterator.next() { collected.append(item) } - XCTFail() + XCTFail("Zipped sequence should throw after one collected element") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([(1, "a", 1)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_third_throwing() async throws { + func test_zip_produces_one_element_and_throws_when_third_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] let c = [1, 2, 3] let sequence = zip(a.async, b.async, c.async.map { try throwOn(2, $0) }) var iterator = sequence.makeAsyncIterator() + + let expected = [(1, "a", 1)] var collected = [(Int, String, Int)]() do { while let item = try await iterator.next() { collected.append(item) } - XCTFail() + XCTFail("Zipped sequence should throw after one collected element") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([(1, "a", 1)], collected) + XCTAssertEqual(expected, collected) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_cancellation() async { + func test_zip_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") let source3 = Indefinite(value: "test3") From d185b2068b0a50e32c122e6c5c68b61eb3891315 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 27 Apr 2022 16:08:26 -0700 Subject: [PATCH 030/149] [SAA-0001] Zip (#149) --- Evolution/0001-zip.md | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Evolution/0001-zip.md diff --git a/Evolution/0001-zip.md b/Evolution/0001-zip.md new file mode 100644 index 00000000..1ba48205 --- /dev/null +++ b/Evolution/0001-zip.md @@ -0,0 +1,110 @@ +# Zip + +* Proposal: [SAA-0001](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0001-zip.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** + +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestZip.swift)] +* Decision Notes: +* Bugs: + +## Introduction + +The swift standard library has a function that allows for the combining of two sequences into one sequence of tuples of the elements of the base sequences. This concept can be achieved for `AsyncSequence` with the iteration being asynchronous but also each side being concurrently iterated while still rethrowing potential failures. This proposal covers that parity between `AsyncSequence` and `Sequence`. It is often times useful to describe asynchronous sequences of events as paired occurrences. The fundamental algorithm for this is zip. + +## Detailed Design + +Zip 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 + +for try await (apple, nasdaq) in zip(appleFeed, nasdaqFeed) { + print("APPL: \(apple) NASDAQ: \(nasdaq)") +} +``` + +Given some sample inputs the following zipped events can be expected. + +| Timestamp | appleFeed | nasdaqFeed | combined output | +| ----------- | --------- | ---------- | ----------------------------- | +| 11:40 AM | 173.91 | | | +| 12:25 AM | | 14236.78 | AAPL: 173.91 NASDAQ: 14236.78 | +| 12:40 AM | | 14218.34 | | +| 1:15 PM | 173.00 | | AAPL: 173.00 NASDAQ: 14218.34 | + +This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. + +```swift +public func zip(_ base1: Base1, _ base2: Base2) -> AsyncZip2Sequence + +public func zip(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncZip3Sequence + +public struct AsyncZip2Sequence: Sendable + where + Base1: Sendable, Base2: Sendable, + Base1.Element: Sendable, Base2.Element: Sendable, + Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + public typealias Element = (Base1.Element, Base2.Element) + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +public struct AsyncZip3Sequence: Sendable + where + Base1: Sendable, Base2: Sendable, Base3: Sendable + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable + Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + public typealias Element = (Base1.Element, Base2.Element, Base3.Element) + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +``` + +The `zip(_:...)` function takes two or more asynchronous sequences as arguments with the resulting `AsyncZipSequence` which is an asynchronous sequence. + +Each iteration of an `AsyncZipSequence` will await for all base iterators to produce a value. This iteration will be done concurrently to produce a singular tuple result. If any of the base iterations terminates by returning `nil` from its iteration, the `AsyncZipSequence` iteration is immediately considered unsatisfiable and returns `nil` and all iterations of other bases will be cancelled. If any iteration of the bases throws an error, then the other iterations concurrently running are cancelled and the produced error is rethrown, terminating the iteration. + +`AsyncZipSequence` requires that the iterations are done concurrently. This means that the base sequences, their elements, and iterators must all be `Sendable`. That makes `AsyncZipSequence` inherently `Sendable`. + +The source of throwing of `AsyncZipSequence` is determined by its bases. That means that if any base can throw an error then the iteration of the `AsyncZipSequence` can throw. If no bases can throw, then the `AsyncZipSequence` does not throw. + +### Naming + +The `zip(_:...)` function takes its name from the Swift standard library function of the same name. The `AsyncZipSequence` family of types take their name from the same family from the standard library for the type returned by `zip(_:_:)`. The one difference is that this asynchronous version allows for the affordance of recognizing the eventual variadic generic need of expanding a zip of more than just two sources. + +It is common in some libraries to have a `ZipMap` or some other combination of `zip` and `map`. This is a common usage pattern, but leaving a singular type for composition feels considerably more approachable. + +### Comparison with other libraries + +**Swift** The swift standard library has an [API definition of zip](https://developer.apple.com/documentation/swift/1541125-zip) as a top level function for combining two sequences. + +**ReactiveX** ReactiveX has an [API definition of Zip](https://reactivex.io/documentation/operators/zip.html) as a top level function for combining Observables. + +**Combine** Combine has an [API definition of zip](https://developer.apple.com/documentation/combine/publisher/zip(_:)/) as an operator style method for combining Publishers. + +## Effect on API resilience + +### `@frozen` and `@inlinable` + +These types utilize rethrowing mechanisms that are awaiting an implementation in the compiler for supporting implementation based rethrows. So none of them are marked as frozen or marked as inlinable. This feature (discussed as `rethrows(unsafe)` or `rethrows(SourceOfRethrowyness)` has not yet been reviewed or implemented. The current implementation takes liberties with an internal protocol to accomplish this task. Future revisions will remove that protocol trick to replace it with proper rethrows semantics at the actual call site. The types are expected to be stable boundaries to prevent that workaround for the compilers yet to be supported rethrowing (or TaskGroup rethrowing) mechanisms. As soon as that feature is resolved; a more detailed investigation on performance impact of inlining and frozen should be done before 1.0. + +## Alternatives considered + +It was considered to have zip be shaped as an extension method on `AsyncSequence` however that infers a "primary-ness" of one `AsyncSequence` over another. Since the standard library spells this as a global function (which infers no preference to one side or another) it was decided that having symmetry between the asynchronous version and the synchronous version inferred the right connotations. + +There are other methods with similar behavior that could be controlled by options passed in. This concept has merit but was initially disregarded since that would complicate the interface. Design-wise this is still an open question if having a "zip-behavior-options" parameter to encompass combining the latest values or zipping based upon a preference to a "primary" side or not is meaningful. + +It is common to have a zip+map to create structures instead of tuples, however that was disregarded since that concept could easily be expressed by composing zip and map. + From 6b59cfb8d075c011f9d74e0a300b84f726a45d75 Mon Sep 17 00:00:00 2001 From: Kevin Tan Date: Tue, 3 May 2022 21:17:53 -0700 Subject: [PATCH 031/149] Minor documentation improvements (#153) --- Sources/AsyncAlgorithms/AsyncChannel.swift | 8 ++++---- Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index f6d132f4..a9922a90 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -13,10 +13,10 @@ /// /// The `AsyncChannel` class is intended to be used as a communication type between tasks, /// particularly when one task produces values and another task consumes those values. The back -/// pressure applied by `send(_:)` and `finish()` via the suspension/resume ensure that -/// the production of values does not exceed the consumption of values from iteration. Each of these -/// methods suspends after enqueuing the event and is resumed when the next call to `next()` -/// on the `Iterator` is made. +/// pressure applied by `send(_:)` and `finish()` via the suspension/resume ensures that +/// the production of values does not exceed the consumption of values from iteration. Each of these +/// methods suspends after enqueuing the event and is resumed when the next call to `next()` +/// on the `Iterator` is made. public final class AsyncChannel: AsyncSequence, Sendable { /// The iterator for a `AsyncChannel` instance. public struct Iterator: AsyncIteratorProtocol, Sendable { diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index 7a9e9a3c..98154625 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -37,6 +37,7 @@ extension AsyncSequence { /// error, `AsyncChunkedByGroupSequence` will rethrow that error immediately and discard /// any current group. /// +/// let numbers = [10, 20, 30, 10, 40, 40, 10, 20].async /// let chunks = numbers.chunked { $0 <= $1 } /// for await numberChunk in chunks { /// print(numberChunk) From a973b06d06f2be355c562ec3ce031373514b03f5 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Wed, 4 May 2022 16:07:33 -0400 Subject: [PATCH 032/149] Fix async channel finishes all (#152) * asyncChannel: enforce termination for all when finished * asyncThrowingChannel: enforce termination for all when finished * asyncChannel: improve unit tests readability * asyncChannel: adapt the Guide to the new finish behaviour * asyncChannel: adapt the Guide to the new finish behaviour --- .../AsyncAlgorithms.docc/Guides/Channel.md | 10 +- Sources/AsyncAlgorithms/AsyncChannel.swift | 32 ++- .../AsyncThrowingChannel.swift | 31 +-- Tests/AsyncAlgorithmsTests/TestChannel.swift | 215 +++++++++++++++--- 4 files changed, 213 insertions(+), 75 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md index c907cc09..974903b5 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md @@ -14,7 +14,7 @@ ## Proposed Solution -To achieve a system that supports back pressure and allows for the communication of more than one value from one task to another we are introducing a new type, the _channel_. The channel will be a reference-type asynchronous sequence with an asynchronous sending capability that awaits the consumption of iteration. Each value sent by the channel, or finish transmitted, will await the consumption of that value or event by iteration. That awaiting behavior will allow for the affordance of back pressure applied from the consumption site to be transmitted to the production site. This means that the rate of production cannot exceed the rate of consumption, and that the rate of consumption cannot exceed the rate of production. +To achieve a system that supports back pressure and allows for the communication of more than one value from one task to another we are introducing a new type, the _channel_. The channel will be a reference-type asynchronous sequence with an asynchronous sending capability that awaits the consumption of iteration. Each value sent by the channel will await the consumption of that value by iteration. That awaiting behavior will allow for the affordance of back pressure applied from the consumption site to be transmitted to the production site. This means that the rate of production cannot exceed the rate of consumption, and that the rate of consumption cannot exceed the rate of production. Sending a terminal event to the channel will instantly resume all pending operations for every producers and consumers. ## Detailed Design @@ -31,7 +31,7 @@ public final class AsyncChannel: AsyncSequence, Sendable { public init(element elementType: Element.Type = Element.self) public func send(_ element: Element) async - public func finish() async + public func finish() public func makeAsyncIterator() -> Iterator } @@ -45,13 +45,13 @@ public final class AsyncThrowingChannel: Asyn public func send(_ element: Element) async public func fail(_ error: Error) async where Failure == Error - public func finish() async + public func finish() public func makeAsyncIterator() -> Iterator } ``` -Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. The back pressure applied by `send(_:)`, `fail(_:)` and `finish()` via the suspension/resume ensure that the production of values does not exceed the consumption of values from iteration. Each of these methods suspend after enqueuing the event and are resumed when the next call to `next()` on the `Iterator` is made. +Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` and `fail(_:)` via the suspension/resume ensure that the production of values does not exceed the consumption of values from iteration. Each of these methods suspend after enqueuing the event and are resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. ```swift let channel = AsyncChannel() @@ -59,7 +59,7 @@ Task { while let resultOfLongCalculation = doLongCalculations() { await channel.send(resultOfLongCalculation) } - await channel.finish() + channel.finish() } for await calculationResult in channel { diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index a9922a90..0f30a6f6 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -131,7 +131,12 @@ public final class AsyncChannel: AsyncSequence, Sendable { func next(_ generation: Int) async -> Element? { return await withUnsafeContinuation { continuation in var cancelled = false + var terminal = false state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + if state.terminal { + terminal = true + return nil + } switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) @@ -157,13 +162,13 @@ public final class AsyncChannel: AsyncSequence, Sendable { return nil } }?.resume() - if cancelled { + if cancelled || terminal { continuation.resume(returning: nil) } } } - func cancelSend() { + func finishAll() { let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in if state.terminal { return ([], []) @@ -188,23 +193,15 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } - func _send(_ result: Result) async { + func _send(_ element: Element) async { await withTaskCancellationHandler { - cancelSend() + finishAll() } operation: { let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in state.withCriticalRegion { state -> UnsafeResumption?, Never>? in if state.terminal { return UnsafeResumption(continuation: continuation, success: nil) } - switch result { - case .success(let value): - if value == nil { - state.terminal = true - } - case .failure: - state.terminal = true - } switch state.emission { case .idle: state.emission = .pending([continuation]) @@ -224,20 +221,19 @@ public final class AsyncChannel: AsyncSequence, Sendable { } }?.resume() } - continuation?.resume(with: result) + continuation?.resume(returning: element) } } /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made. /// If the channel is already finished then this returns immediately public func send(_ element: Element) async { - await _send(.success(element)) + await _send(element) } - /// Send a finish to an awaiting iteration. This function will resume when the next call to `next()` is made. - /// If the channel is already finished then this returns immediately - public func finish() async { - await _send(.success(nil)) + /// Send a finish to all awaiting iterations. + public func finish() { + finishAll() } /// Create an `Iterator` for iteration of an `AsyncChannel` diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 205729e3..58c9e8c9 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -129,7 +129,12 @@ public final class AsyncThrowingChannel: Asyn func next(_ generation: Int) async throws -> Element? { return try await withUnsafeThrowingContinuation { continuation in var cancelled = false + var terminal = false state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + if state.terminal { + terminal = true + return nil + } switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) @@ -155,13 +160,13 @@ public final class AsyncThrowingChannel: Asyn return nil } }?.resume() - if cancelled { + if cancelled || terminal { continuation.resume(returning: nil) } } } - func cancelSend() { + func finishAll() { let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in if state.terminal { return ([], []) @@ -186,23 +191,20 @@ public final class AsyncThrowingChannel: Asyn } } - func _send(_ result: Result) async { + func _send(_ result: Result) async { await withTaskCancellationHandler { - cancelSend() + finishAll() } operation: { let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in state.withCriticalRegion { state -> UnsafeResumption?, Never>? in if state.terminal { return UnsafeResumption(continuation: continuation, success: nil) } - switch result { - case .success(let value): - if value == nil { - state.terminal = true - } - case .failure: + + if case .failure = result { state.terminal = true } + switch state.emission { case .idle: state.emission = .pending([continuation]) @@ -222,7 +224,7 @@ public final class AsyncThrowingChannel: Asyn } }?.resume() } - continuation?.resume(with: result) + continuation?.resume(with: result.map { $0 as Element? }) } } @@ -238,10 +240,9 @@ public final class AsyncThrowingChannel: Asyn await _send(.failure(error)) } - /// Send a finish to an awaiting iteration. This function will resume when the next call to `next()` is made. - /// If the channel is already finished then this returns immediately - public func finish() async { - await _send(.success(nil)) + /// Send a finish to all awaiting iterations. + public func finish() { + finishAll() } public func makeAsyncIterator() -> Iterator { diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index 50cb7626..891ec434 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -13,13 +13,16 @@ import AsyncAlgorithms final class TestChannel: XCTestCase { - func test_channel() async { + func test_asyncChannel_delivers_values_when_two_producers_and_two_consumers() async { + let (sentFromProducer1, sentFromProducer2) = ("test1", "test2") + let expected = Set([sentFromProducer1, sentFromProducer2]) + let channel = AsyncChannel() Task { - await channel.send("test1") + await channel.send(sentFromProducer1) } Task { - await channel.send("test2") + await channel.send(sentFromProducer2) } let t: Task = Task { @@ -28,13 +31,17 @@ final class TestChannel: XCTestCase { return value } var iterator = channel.makeAsyncIterator() - let value = await iterator.next() - let other = await t.value - - XCTAssertEqual(Set([value, other]), Set(["test1", "test2"])) + + let (collectedFromConsumer1, collectedFromConsumer2) = (await t.value, await iterator.next()) + let collected = Set([collectedFromConsumer1, collectedFromConsumer2]) + + XCTAssertEqual(collected, expected) } - func test_throwing_channel() async throws { + func test_asyncThrowingChannel_delivers_values_when_two_producers_and_two_consumers() async throws { + let (sentFromProducer1, sentFromProducer2) = ("test1", "test2") + let expected = Set([sentFromProducer1, sentFromProducer2]) + let channel = AsyncThrowingChannel() Task { await channel.send("test1") @@ -49,13 +56,14 @@ final class TestChannel: XCTestCase { return value } var iterator = channel.makeAsyncIterator() - let value = try await iterator.next() - let other = try await t.value + + let (collectedFromConsumer1, collectedFromConsumer2) = (try await t.value, try await iterator.next()) + let collected = Set([collectedFromConsumer1, collectedFromConsumer2]) - XCTAssertEqual(Set([value, other]), Set(["test1", "test2"])) + XCTAssertEqual(collected, expected) } - func test_throwing() async { + func test_asyncThrowingChannel_throws_when_fail_is_called() async { let channel = AsyncThrowingChannel() Task { await channel.fail(Failure()) @@ -63,37 +71,170 @@ final class TestChannel: XCTestCase { var iterator = channel.makeAsyncIterator() do { let _ = try await iterator.next() - XCTFail() + XCTFail("The AsyncThrowingChannel should have thrown") } catch { XCTAssertEqual(error as? Failure, Failure()) } } - - func test_send_finish() async { + + func test_asyncChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called() async { let channel = AsyncChannel() let complete = ManagedCriticalState(false) let finished = expectation(description: "finished") + Task { - await channel.finish() + channel.finish() complete.withCriticalRegion { $0 = true } finished.fulfill() } - XCTAssertFalse(complete.withCriticalRegion { $0 }) - let value = ManagedCriticalState(nil) + + let valueFromConsumer1 = ManagedCriticalState(nil) + let valueFromConsumer2 = ManagedCriticalState(nil) + let received = expectation(description: "received") + received.expectedFulfillmentCount = 2 + let pastEnd = expectation(description: "pastEnd") + pastEnd.expectedFulfillmentCount = 2 + Task { var iterator = channel.makeAsyncIterator() let ending = await iterator.next() - value.withCriticalRegion { $0 = ending } + valueFromConsumer1.withCriticalRegion { $0 = ending } received.fulfill() let item = await iterator.next() XCTAssertNil(item) pastEnd.fulfill() } + + Task { + var iterator = channel.makeAsyncIterator() + let ending = await iterator.next() + valueFromConsumer2.withCriticalRegion { $0 = ending } + received.fulfill() + let item = await iterator.next() + XCTAssertNil(item) + pastEnd.fulfill() + } + wait(for: [finished, received], timeout: 1.0) + XCTAssertTrue(complete.withCriticalRegion { $0 }) - XCTAssertEqual(value.withCriticalRegion { $0 }, nil) + XCTAssertEqual(valueFromConsumer1.withCriticalRegion { $0 }, nil) + XCTAssertEqual(valueFromConsumer2.withCriticalRegion { $0 }, nil) + + wait(for: [pastEnd], timeout: 1.0) + let additionalSend = expectation(description: "additional send") + Task { + await channel.send("test") + additionalSend.fulfill() + } + wait(for: [additionalSend], timeout: 1.0) + } + + func test_asyncChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called2() async throws { + let channel = AsyncChannel() + let complete = ManagedCriticalState(false) + let finished = expectation(description: "finished") + + let valueFromConsumer1 = ManagedCriticalState(nil) + let valueFromConsumer2 = ManagedCriticalState(nil) + + let received = expectation(description: "received") + received.expectedFulfillmentCount = 2 + + let pastEnd = expectation(description: "pastEnd") + pastEnd.expectedFulfillmentCount = 2 + + Task(priority: .high) { + var iterator = channel.makeAsyncIterator() + let ending = await iterator.next() + valueFromConsumer1.withCriticalRegion { $0 = ending } + received.fulfill() + let item = await iterator.next() + XCTAssertNil(item) + pastEnd.fulfill() + } + + Task(priority: .high) { + var iterator = channel.makeAsyncIterator() + let ending = await iterator.next() + valueFromConsumer2.withCriticalRegion { $0 = ending } + received.fulfill() + let item = await iterator.next() + XCTAssertNil(item) + pastEnd.fulfill() + } + + try await Task.sleep(nanoseconds: 1_000_000_000) + + Task(priority: .low) { + channel.finish() + complete.withCriticalRegion { $0 = true } + finished.fulfill() + } + + wait(for: [finished, received], timeout: 1.0) + + XCTAssertTrue(complete.withCriticalRegion { $0 }) + XCTAssertEqual(valueFromConsumer1.withCriticalRegion { $0 }, nil) + XCTAssertEqual(valueFromConsumer2.withCriticalRegion { $0 }, nil) + + wait(for: [pastEnd], timeout: 1.0) + let additionalSend = expectation(description: "additional send") + Task { + await channel.send("test") + additionalSend.fulfill() + } + wait(for: [additionalSend], timeout: 1.0) + } + + func test_asyncThrowingChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called() async { + let channel = AsyncThrowingChannel() + let complete = ManagedCriticalState(false) + let finished = expectation(description: "finished") + + Task { + channel.finish() + complete.withCriticalRegion { $0 = true } + finished.fulfill() + } + + let valueFromConsumer1 = ManagedCriticalState(nil) + let valueFromConsumer2 = ManagedCriticalState(nil) + + let received = expectation(description: "received") + received.expectedFulfillmentCount = 2 + + let pastEnd = expectation(description: "pastEnd") + pastEnd.expectedFulfillmentCount = 2 + + Task { + var iterator = channel.makeAsyncIterator() + let ending = try await iterator.next() + valueFromConsumer1.withCriticalRegion { $0 = ending } + received.fulfill() + let item = try await iterator.next() + XCTAssertNil(item) + pastEnd.fulfill() + } + + Task { + var iterator = channel.makeAsyncIterator() + let ending = try await iterator.next() + valueFromConsumer2.withCriticalRegion { $0 = ending } + received.fulfill() + let item = try await iterator.next() + XCTAssertNil(item) + pastEnd.fulfill() + } + + wait(for: [finished, received], timeout: 1.0) + + XCTAssertTrue(complete.withCriticalRegion { $0 }) + XCTAssertEqual(valueFromConsumer1.withCriticalRegion { $0 }, nil) + XCTAssertEqual(valueFromConsumer2.withCriticalRegion { $0 }, nil) + wait(for: [pastEnd], timeout: 1.0) let additionalSend = expectation(description: "additional send") Task { @@ -103,7 +244,7 @@ final class TestChannel: XCTestCase { wait(for: [additionalSend], timeout: 1.0) } - func test_cancellation() async { + func test_asyncChannel_ends_iterator_when_task_is_cancelled() async { let channel = AsyncChannel() let ready = expectation(description: "ready") let task: Task = Task { @@ -116,8 +257,22 @@ final class TestChannel: XCTestCase { let value = await task.value XCTAssertNil(value) } + + func test_asyncThrowingChannel_ends_iterator_when_task_is_cancelled() async throws { + let channel = AsyncThrowingChannel() + let ready = expectation(description: "ready") + let task: Task = Task { + var iterator = channel.makeAsyncIterator() + ready.fulfill() + return try await iterator.next() + } + wait(for: [ready], timeout: 1.0) + task.cancel() + let value = try await task.value + XCTAssertNil(value) + } - func test_sendCancellation() async { + func test_asyncChannel_resumes_send_when_task_is_cancelled() async { let channel = AsyncChannel() let notYetDone = expectation(description: "not yet done") notYetDone.isInverted = true @@ -132,7 +287,7 @@ final class TestChannel: XCTestCase { wait(for: [done], timeout: 1.0) } - func test_sendCancellation_throwing() async { + func test_asyncThrowingChannel_resumes_send_when_task_is_cancelled() async { let channel = AsyncThrowingChannel() let notYetDone = expectation(description: "not yet done") notYetDone.isInverted = true @@ -146,18 +301,4 @@ final class TestChannel: XCTestCase { task.cancel() wait(for: [done], timeout: 1.0) } - - func test_cancellation_throwing() async throws { - let channel = AsyncThrowingChannel() - let ready = expectation(description: "ready") - let task: Task = Task { - var iterator = channel.makeAsyncIterator() - ready.fulfill() - return try await iterator.next() - } - wait(for: [ready], timeout: 1.0) - task.cancel() - let value = try await task.value - XCTAssertNil(value) - } } From 6e9a5757bc36d1190a5c3b985f5e2539dc048c04 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 31 May 2022 03:29:53 +0100 Subject: [PATCH 033/149] Fix typo in `BufferedBytes.md` (#158) `BufferedAsyncByteIterator` -> `AsyncBufferedByteIterator` --- .../AsyncAlgorithms.docc/Guides/BufferedBytes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md index ce1d0ef2..af576312 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md @@ -17,7 +17,7 @@ struct AsyncBytes: AsyncSequence { } public func makeAsyncIterator() -> AsyncBufferedByteIterator { - return BufferedAsyncByteIterator(capacity: 16384) { buffer in + return AsyncBufferedByteIterator(capacity: 16384) { buffer in // This runs once every 16384 invocations of next() return try await handle.read(into: buffer) } From 434591a571a8c4fe073500926414356d3b40f460 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Mon, 13 Jun 2022 10:18:45 -0700 Subject: [PATCH 034/149] Update availability to be in sync with Clock/Instant/Duration (#159) --- .../AsyncChunksOfCountOrSignalSequence.swift | 8 ++++---- .../AsyncAlgorithms/AsyncDebounceSequence.swift | 8 ++++---- .../AsyncAlgorithms/AsyncThrottleSequence.swift | 16 ++++++++-------- Sources/AsyncAlgorithms/AsyncTimerSequence.swift | 10 +++++----- .../AsyncSequenceValidationDiagram.swift | 2 +- Sources/AsyncSequenceValidation/Clock.swift | 4 ++-- Sources/AsyncSequenceValidation/Event.swift | 2 +- .../AsyncSequenceValidation/Expectation.swift | 2 +- Sources/AsyncSequenceValidation/Input.swift | 2 +- Sources/AsyncSequenceValidation/Job.swift | 2 +- Sources/AsyncSequenceValidation/TaskDriver.swift | 4 ++-- Sources/AsyncSequenceValidation/Test.swift | 4 ++-- Sources/AsyncSequenceValidation/Theme.swift | 6 +++--- Sources/AsyncSequenceValidation/WorkQueue.swift | 2 +- 14 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index 70ea2d2d..e5b167c7 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -31,25 +31,25 @@ 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 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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 { AsyncChunksOfCountOrSignalSequence(self, count: count, signal: timer) } /// Creates an asynchronous sequence that creates chunks of a given count or when an `AsyncTimerSequence` fires. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 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 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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 { AsyncChunksOfCountOrSignalSequence(self, count: nil, signal: timer) } /// Creates an asynchronous sequence that creates chunks when an `AsyncTimerSequence` fires. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func chunked(by timer: AsyncTimerSequence) -> AsyncChunksOfCountOrSignalSequence> { chunked(by: timer, into: [Element].self) } diff --git a/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift index 9ad9aefc..304dc01d 100644 --- a/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift @@ -12,14 +12,14 @@ extension AsyncSequence { /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed by using a specified Clock. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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 { 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 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence { debounce(for: interval, tolerance: tolerance, clock: .continuous) } @@ -27,7 +27,7 @@ extension AsyncSequence { /// An `AsyncSequence` that emits the latest element after a given quiescence period /// has elapsed. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct AsyncDebounceSequence: Sendable where Base.AsyncIterator: Sendable, Base.Element: Sendable, Base: Sendable { let base: Base @@ -43,7 +43,7 @@ public struct AsyncDebounceSequence: Sendable } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncDebounceSequence: AsyncSequence { public typealias Element = Base.Element diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index 4d53f77d..ad590afa 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -11,19 +11,19 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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 { @@ -35,14 +35,14 @@ extension AsyncSequence { } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) + @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) } } /// A rate-limited `AsyncSequence` by emitting values at most every specified interval. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct AsyncThrottleSequence { let base: Base let interval: C.Instant.Duration @@ -57,7 +57,7 @@ public struct AsyncThrottleSequence { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncThrottleSequence: AsyncSequence { public typealias Element = Reduced @@ -100,8 +100,8 @@ extension AsyncThrottleSequence: AsyncSequence { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncThrottleSequence.Iterator: Sendable where Base.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index c7ea5849..795cfa4e 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// /// An `AsyncSequence` that produces elements at regular intervals. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct AsyncTimerSequence: AsyncSequence { public typealias Element = C.Instant @@ -71,7 +71,7 @@ public struct AsyncTimerSequence: AsyncSequence { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@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 { @@ -79,7 +79,7 @@ extension AsyncTimerSequence { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@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 { @@ -87,8 +87,8 @@ extension AsyncTimerSequence where C == SuspendingClock { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence: Sendable { } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence.Iterator: Sendable { } diff --git a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift index 09f55d8f..63c59ad0 100644 --- a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift +++ b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift @@ -11,7 +11,7 @@ import _CAsyncSequenceValidationSupport -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @resultBuilder public struct AsyncSequenceValidationDiagram : Sendable { public struct Component { diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index 906d25f4..66f8a532 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -11,7 +11,7 @@ import AsyncAlgorithms -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public struct Clock { let queue: WorkQueue @@ -22,7 +22,7 @@ extension AsyncSequenceValidationDiagram { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram.Clock: Clock { public struct Step: DurationProtocol, Hashable, CustomStringConvertible { internal var rawValue: Int diff --git a/Sources/AsyncSequenceValidation/Event.swift b/Sources/AsyncSequenceValidation/Event.swift index 9c608474..0ee11fe5 100644 --- a/Sources/AsyncSequenceValidation/Event.swift +++ b/Sources/AsyncSequenceValidation/Event.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { struct Failure: Error, Equatable { } diff --git a/Sources/AsyncSequenceValidation/Expectation.swift b/Sources/AsyncSequenceValidation/Expectation.swift index ca682b99..1cc1a439 100644 --- a/Sources/AsyncSequenceValidation/Expectation.swift +++ b/Sources/AsyncSequenceValidation/Expectation.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public struct ExpectationResult: Sendable { public struct Event: Sendable { diff --git a/Sources/AsyncSequenceValidation/Input.swift b/Sources/AsyncSequenceValidation/Input.swift index c97dbcda..7cd31e0c 100644 --- a/Sources/AsyncSequenceValidation/Input.swift +++ b/Sources/AsyncSequenceValidation/Input.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public struct Specification: Sendable { public let specification: String diff --git a/Sources/AsyncSequenceValidation/Job.swift b/Sources/AsyncSequenceValidation/Job.swift index 0b0e44cb..87ccd44e 100644 --- a/Sources/AsyncSequenceValidation/Job.swift +++ b/Sources/AsyncSequenceValidation/Job.swift @@ -11,7 +11,7 @@ import _CAsyncSequenceValidationSupport -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct Job: Hashable, @unchecked Sendable { let job: JobRef diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 3df15ba9..28140ebb 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -20,7 +20,7 @@ import _CAsyncSequenceValidationSupport #endif #if canImport(Darwin) -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return nil @@ -34,7 +34,7 @@ func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { #error("TODO: Port TaskDriver threading to windows") #endif -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class TaskDriver { let work: (TaskDriver) -> Void let queue: WorkQueue diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index ba7a5c3f..981dd4d7 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -19,7 +19,7 @@ internal func _swiftJobRun( _ executor: UnownedSerialExecutor ) -> () -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol AsyncSequenceValidationTest: Sendable { var inputs: [AsyncSequenceValidationDiagram.Specification] { get } var output: AsyncSequenceValidationDiagram.Specification { get } @@ -27,7 +27,7 @@ public protocol AsyncSequenceValidationTest: Sendable { func test(with clock: C, activeTicks: [C.Instant], output: AsyncSequenceValidationDiagram.Specification, _ event: (String) -> Void) async throws } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { struct Test: AsyncSequenceValidationTest, @unchecked Sendable where Operation.Element == String { let inputs: [Specification] diff --git a/Sources/AsyncSequenceValidation/Theme.swift b/Sources/AsyncSequenceValidation/Theme.swift index d82229ed..5d627292 100644 --- a/Sources/AsyncSequenceValidation/Theme.swift +++ b/Sources/AsyncSequenceValidation/Theme.swift @@ -9,21 +9,21 @@ // //===----------------------------------------------------------------------===// -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol AsyncSequenceValidationTheme { func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token func description(for token: AsyncSequenceValidationDiagram.Token) -> String } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationTheme where Self == AsyncSequenceValidationDiagram.ASCIITheme { public static var ascii: AsyncSequenceValidationDiagram.ASCIITheme { return AsyncSequenceValidationDiagram.ASCIITheme() } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public enum Token: Sendable { case step diff --git a/Sources/AsyncSequenceValidation/WorkQueue.swift b/Sources/AsyncSequenceValidation/WorkQueue.swift index 929ff558..769ea6f8 100644 --- a/Sources/AsyncSequenceValidation/WorkQueue.swift +++ b/Sources/AsyncSequenceValidation/WorkQueue.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct WorkQueue: Sendable { enum Item: CustomStringConvertible, Comparable { case blocked(Token, AsyncSequenceValidationDiagram.Clock.Instant, UnsafeContinuation) From 07b8d74cc7329d7ecacaf5d8b3635cacba1027fa Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Mon, 13 Jun 2022 11:04:03 -0700 Subject: [PATCH 035/149] [SAA-0002] Merge (#156) --- Evolution/0002-merge.md | 103 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 Evolution/0002-merge.md diff --git a/Evolution/0002-merge.md b/Evolution/0002-merge.md new file mode 100644 index 00000000..ff15f23d --- /dev/null +++ b/Evolution/0002-merge.md @@ -0,0 +1,103 @@ +# Merge + +* Proposal: [SAA-0002](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0002-merge.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** + +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Asyncmerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)] +* Decision Notes: +* Bugs: + +## Introduction + +In the category of combinations of asynchronous sequences there are a few different potential behaviors. This category all take two or more `AsyncSequence` types and produce one `AsyncSequence`. One fundamental behavior is taking all values produced by the inputs and resuming the iteration of the singular downstream `AsyncSequence` with those values. This shape is called merge. + +## Detailed Design + +Merge takes two or more asynchronous sequences sharing the same element type and combines them into one singular asynchronous sequence of those elements. + +```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 } + +for try await ticker in merge(appleFeed, nasdaqFeed) { + print(ticker) +} +``` + +Given some sample inputs the following merged events can be expected. + +| Timestamp | appleFeed | nasdaqFeed | merged output | +| ----------- | --------- | ---------- | --------------- | +| 11:40 AM | 173.91 | | AAPL: 173.91 | +| 12:25 AM | | 14236.78 | ^IXIC: 14236.78 | +| 12:40 AM | | 14218.34 | ^IXIC: 14218.34 | +| 1:15 PM | 173.00 | | AAPL: 173.00 | + +This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. + +```swift +public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence + +public func merge(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncMerge3Sequence + +public struct AsyncMerge2Sequence: Sendable + where + Base1.Element == Base2.Element, + Base1: Sendable, Base2: Sendable, + Base1.Element: Sendable, Base2.Element: Sendable, + Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + public typealias Element = Base1.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +public struct AsyncMerge3Sequence: Sendable + where + Base1.Element == Base2.Element, Base1.Element == Base3.Element, + Base1: Sendable, Base2: Sendable, Base3: Sendable + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable + Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + public typealias Element = Base1.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +``` + +The `merge(_:...)` function takes two or more asynchronous sequences as arguments and produces an `AsyncMergeSequence` which is an asynchronous sequence. + +Since the bases comprising the `AsyncMergeSequence` must be iterated concurrently to produce the latest value, those sequences must be able to be sent to child tasks. This means that a prerequisite of the bases must be that the base asynchronous sequences, their iterators, and the elements they produce must be `Sendable`. + +When iterating a `AsyncMergeSequence`, the sequence terminates when all of the base asynchronous sequences terminate, since this means there is no potential for any further elements to be produced. + +The throwing behavior of `AsyncMergeSequence` is that if any of the bases throw, then the composed asynchronous sequence throws on its iteration. If at any point an error is thrown by any base, the other iterations are cancelled and the thrown error is immediately thrown to the consuming iteration. + +### Naming + +Since the inherent behavior of `merge(_:...)` merges values from multiple streams into a singular asynchronous sequence, the naming is intended to be quite literal. There are precedent terms of art in other frameworks and libraries (listed in the comparison section). Other naming takes the form of "withLatestFrom". This was disregarded since the "with" prefix is often most associated with the passing of a closure and some sort of contextual concept; `withUnsafePointer` or `withUnsafeContinuation` are prime examples. + +### Comparison with other libraries + +**ReactiveX** ReactiveX has an [API definition of Merge](https://reactivex.io/documentation/operators/merge.html) as a top level function for merging Observables. + +**Combine** Combine has an [API definition of merge(with:)](https://developer.apple.com/documentation/combine/publisher/merge(with:)-7qt71/) as an operator style method for merging Publishers. + +## Effect on API resilience + +### `@frozen` and `@inlinable` + +These types utilize rethrowing mechanisms that are awaiting an implementation in the compiler for supporting implementation based rethrows. So none of them are marked as frozen or marked as inlinable. This feature (discussed as `rethrows(unsafe)` or `rethrows(SourceOfRethrowyness)` has not yet been reviewed or implemented. The current implementation takes liberties with an internal protocol to accomplish this task. Future revisions will remove that protocol trick to replace it with proper rethrows semantics at the actual call site. The types are expected to be stable boundaries to prevent that workaround for the compilers yet to be supported rethrowing (or TaskGroup rethrowing) mechanisms. As soon as that feature is resolved; a more detailed investigation on performance impact of inlining and frozen should be done before 1.0. + +## Alternatives considered + +It was considered to have merge be shaped as an extension method on `AsyncSequence` however that infers a "primary-ness" of one `AsyncSequence` over another. Since the behavior of this as a global function (which infers no preference to one side or another) it was decided that having symmetry between the asynchronous version and the synchronous version inferred the right connotations. From 9a3e94d440460c0cae5a97467f8e4787f370f735 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Thu, 16 Jun 2022 08:28:45 +0200 Subject: [PATCH 036/149] compacted: rename unit tests (#166) --- Tests/AsyncAlgorithmsTests/TestCompacted.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/TestCompacted.swift b/Tests/AsyncAlgorithmsTests/TestCompacted.swift index d2876130..ddba2fa8 100644 --- a/Tests/AsyncAlgorithmsTests/TestCompacted.swift +++ b/Tests/AsyncAlgorithmsTests/TestCompacted.swift @@ -13,7 +13,7 @@ import AsyncAlgorithms final class TestCompacted: XCTestCase { - func test_compacting() async { + func test_compacted_is_equivalent_to_compactMap_when_input_as_nil_elements() async { let source = [1, 2, nil, 3, 4, nil, 5] let expected = source.compactMap { $0 } let sequence = source.async.compacted() @@ -24,7 +24,7 @@ final class TestCompacted: XCTestCase { XCTAssertEqual(expected, actual) } - func test_compacting_past_end() async { + 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 } let sequence = source.async.compacted() @@ -38,7 +38,7 @@ final class TestCompacted: XCTestCase { XCTAssertNil(pastEnd) } - func test_compactingNonNils() async { + 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 } let sequence = source.async.compacted() @@ -49,7 +49,7 @@ final class TestCompacted: XCTestCase { XCTAssertEqual(expected, actual) } - func test_throwing() async throws { + 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() var collected = [Int]() @@ -66,7 +66,7 @@ final class TestCompacted: XCTestCase { XCTAssertNil(pastEnd) } - func test_cancellation() async { + func test_compacted_finishes_when_iteration_task_is_cancelled() async { let value: String? = "test" let source = Indefinite(value: value) let sequence = source.async.compacted() From cca423ff03ab657062f3781847e65a867b51bb01 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 16 Jun 2022 13:06:20 -0700 Subject: [PATCH 037/149] Remove the disable of availability and mark methods as available to clock/instant/duration (#165) * Remove the disable of availability and mark methods as available to clock/instant/duration * Rework availability to be more permissive for tests * Use hand coded values instead of defines (only available via Darwin versions of Dispatch) * Adjust additional test availability * Correct debounce test availability guard * Update build instructions to indicate Xcode requirements * Ensure the clock property of the validation diagram is only available for targets that can support it --- Package.swift | 14 +------ README.md | 15 +------ .../AsyncSequenceValidationDiagram.swift | 10 +++-- Sources/AsyncSequenceValidation/Clock.swift | 29 +++++++++++-- Sources/AsyncSequenceValidation/Event.swift | 1 - .../AsyncSequenceValidation/Expectation.swift | 1 - Sources/AsyncSequenceValidation/Input.swift | 1 - Sources/AsyncSequenceValidation/Job.swift | 1 - .../AsyncSequenceValidation/TaskDriver.swift | 2 - Sources/AsyncSequenceValidation/Test.swift | 8 ++-- Sources/AsyncSequenceValidation/Theme.swift | 3 -- .../AsyncSequenceValidation/WorkQueue.swift | 1 - Tests/AsyncAlgorithmsTests/TestChunk.swift | 31 +++++++++----- Tests/AsyncAlgorithmsTests/TestDebounce.swift | 15 ++++--- .../AsyncAlgorithmsTests/TestTaskSelect.swift | 30 ++++++++++--- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 42 ++++++++++++------- Tests/AsyncAlgorithmsTests/TestTimer.swift | 12 ++++-- .../TestValidationTests.swift | 4 +- 18 files changed, 133 insertions(+), 87 deletions(-) diff --git a/Package.swift b/Package.swift index afc2378f..c43747c0 100644 --- a/Package.swift +++ b/Package.swift @@ -25,20 +25,10 @@ let package = Package( .systemLibrary(name: "_CAsyncSequenceValidationSupport"), .target( name: "AsyncAlgorithms_XCTest", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], - swiftSettings: [ - .unsafeFlags([ - "-Xfrontend", "-disable-availability-checking" - ]) - ]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], - swiftSettings: [ - .unsafeFlags([ - "-Xfrontend", "-disable-availability-checking" - ]) - ]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), ] ) diff --git a/README.md b/README.md index e3c957a9..cde1208f 100644 --- a/README.md +++ b/README.md @@ -87,28 +87,17 @@ Finally, add `import AsyncAlgorithms` to your source code. ## Getting Started -⚠️ Please note that this package currently requires a recent [Swift Trunk Development toolchain](https://www.swift.org/download/#trunk-development-main). More information on how to use custom toolchains with Xcode can be viewed [here](https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/AlternativeToolchains.html). +⚠️ Please note that this package requires Xcode 14 on macOS hosts. Previous versions of Xcode do not contain the required Swift version. ### Building/Testing Using Xcode on macOS - 1. Download the most recent development Xcode toolchain. - 2. Install the package - 4. Select the development toolchain in Xcode - 4. Open the `swift-async-algorithms` package directory in Xcode - 5. Build or Test in Xcode as normal - -⚠️ Note: `swift test` does not currently work properly with custom toolchains for this package. + 1. In the `swift-async-algorithms` directory run `swift build` or `swift test` accordingly ### Building/Testing on Linux 1. Download the most recent development toolchain for your Linux distribution 2. Decompress the archive to a path in which the `swift` executable is in the binary search path environment variable (`$PATH`) 3. In the `swift-async-algorithms` directory run `swift build` or `swift test` accordingly - -### Building with Swift 5.6 - - 1. `git checkout swift-5.6` - 2. run `swift build` or `swift test` accordingly ## Source Stability diff --git a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift index 63c59ad0..b7ee6547 100644 --- a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift +++ b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift @@ -11,7 +11,6 @@ import _CAsyncSequenceValidationSupport -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @resultBuilder public struct AsyncSequenceValidationDiagram : Sendable { public struct Component { @@ -97,15 +96,20 @@ public struct AsyncSequenceValidationDiagram : Sendable { } let queue: WorkQueue + let _clock: Clock public var inputs: InputList - public let clock: Clock + + @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 self.inputs = InputList(queue: queue) - self.clock = Clock(queue: queue) + self._clock = Clock(queue: queue) } } diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index 66f8a532..5eeb7360 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -11,7 +11,6 @@ import AsyncAlgorithms -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public struct Clock { let queue: WorkQueue @@ -22,8 +21,20 @@ extension AsyncSequenceValidationDiagram { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncSequenceValidationDiagram.Clock: Clock { + +public protocol TestClock: Sendable { + associatedtype Instant: TestInstant + + var now: Instant { get } + + func sleep(until deadline: Self.Instant, tolerance: Self.Instant.Duration?) async throws +} + +public protocol TestInstant: Equatable { + associatedtype Duration +} + +extension AsyncSequenceValidationDiagram.Clock { public struct Step: DurationProtocol, Hashable, CustomStringConvertible { internal var rawValue: Int @@ -66,7 +77,7 @@ extension AsyncSequenceValidationDiagram.Clock: Clock { } } - public struct Instant: InstantProtocol, CustomStringConvertible { + public struct Instant: CustomStringConvertible { public typealias Duration = Step let when: Step @@ -111,3 +122,13 @@ extension AsyncSequenceValidationDiagram.Clock: Clock { } } } + +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: TestClock { } + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension AsyncSequenceValidationDiagram.Clock: Clock { } diff --git a/Sources/AsyncSequenceValidation/Event.swift b/Sources/AsyncSequenceValidation/Event.swift index 0ee11fe5..70101475 100644 --- a/Sources/AsyncSequenceValidation/Event.swift +++ b/Sources/AsyncSequenceValidation/Event.swift @@ -9,7 +9,6 @@ // //===----------------------------------------------------------------------===// -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { struct Failure: Error, Equatable { } diff --git a/Sources/AsyncSequenceValidation/Expectation.swift b/Sources/AsyncSequenceValidation/Expectation.swift index 1cc1a439..63121d66 100644 --- a/Sources/AsyncSequenceValidation/Expectation.swift +++ b/Sources/AsyncSequenceValidation/Expectation.swift @@ -9,7 +9,6 @@ // //===----------------------------------------------------------------------===// -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public struct ExpectationResult: Sendable { public struct Event: Sendable { diff --git a/Sources/AsyncSequenceValidation/Input.swift b/Sources/AsyncSequenceValidation/Input.swift index 7cd31e0c..35ede268 100644 --- a/Sources/AsyncSequenceValidation/Input.swift +++ b/Sources/AsyncSequenceValidation/Input.swift @@ -9,7 +9,6 @@ // //===----------------------------------------------------------------------===// -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public struct Specification: Sendable { public let specification: String diff --git a/Sources/AsyncSequenceValidation/Job.swift b/Sources/AsyncSequenceValidation/Job.swift index 87ccd44e..44dedbc8 100644 --- a/Sources/AsyncSequenceValidation/Job.swift +++ b/Sources/AsyncSequenceValidation/Job.swift @@ -11,7 +11,6 @@ import _CAsyncSequenceValidationSupport -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct Job: Hashable, @unchecked Sendable { let job: JobRef diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 28140ebb..9f45c1f6 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -20,7 +20,6 @@ import _CAsyncSequenceValidationSupport #endif #if canImport(Darwin) -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return nil @@ -34,7 +33,6 @@ func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { #error("TODO: Port TaskDriver threading to windows") #endif -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class TaskDriver { let work: (TaskDriver) -> Void let queue: WorkQueue diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index 981dd4d7..35177cf2 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -19,22 +19,20 @@ internal func _swiftJobRun( _ executor: UnownedSerialExecutor ) -> () -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 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 } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { 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 { @@ -265,7 +263,7 @@ extension AsyncSequenceValidationDiagram { @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test ) throws -> (ExpectationResult, [ExpectationFailure]) { let diagram = AsyncSequenceValidationDiagram() - let clock = diagram.clock + let clock = diagram._clock let test = build(diagram) for index in 0.. AsyncSequenceValidationDiagram.Token func description(for token: AsyncSequenceValidationDiagram.Token) -> String } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationTheme where Self == AsyncSequenceValidationDiagram.ASCIITheme { public static var ascii: AsyncSequenceValidationDiagram.ASCIITheme { return AsyncSequenceValidationDiagram.ASCIITheme() } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram { public enum Token: Sendable { case step diff --git a/Sources/AsyncSequenceValidation/WorkQueue.swift b/Sources/AsyncSequenceValidation/WorkQueue.swift index 769ea6f8..784e5e82 100644 --- a/Sources/AsyncSequenceValidation/WorkQueue.swift +++ b/Sources/AsyncSequenceValidation/WorkQueue.swift @@ -9,7 +9,6 @@ // //===----------------------------------------------------------------------===// -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct WorkQueue: Sendable { enum Item: CustomStringConvertible, Comparable { case blocked(Token, AsyncSequenceValidationDiagram.Clock.Instant, UnsafeContinuation) diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index ba340a80..4970cdc0 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -22,7 +22,6 @@ func concatCharacters(_ array: [String]) -> String { } final class TestChunk: XCTestCase { - func test_signal_equalChunks() { validate { "ABC- DEF- GHI- |" @@ -113,7 +112,8 @@ final class TestChunk: XCTestCase { } } - func test_time_equalChunks() { + 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") } validate { "ABC- DEF- GHI- |" $0.inputs[0].chunked(by: .repeating(every: .steps(4), clock: $0.clock)).map(concatCharacters) @@ -121,7 +121,8 @@ final class TestChunk: XCTestCase { } } - func test_time_unequalChunks() { + 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") } validate { "AB------ A------- ABCDEFG- |" $0.inputs[0].chunked(by: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -129,7 +130,8 @@ final class TestChunk: XCTestCase { } } - func test_time_emptyChunks() { + 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") } validate { "-- 1- --|" $0.inputs[0].chunked(by: .repeating(every: .steps(2), clock: $0.clock)).map(concatCharacters) @@ -137,7 +139,8 @@ final class TestChunk: XCTestCase { } } - func test_time_error() { + 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") } validate { "AB^" $0.inputs[0].chunked(by: .repeating(every: .steps(5), clock: $0.clock)).map(concatCharacters) @@ -145,7 +148,8 @@ final class TestChunk: XCTestCase { } } - func test_time_unsignaledTrailingChunk() { + 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") } validate { "111-111|" $0.inputs[0].chunked(by: .repeating(every: .steps(4), clock: $0.clock)).map(sumCharacters) @@ -153,7 +157,8 @@ final class TestChunk: XCTestCase { } } - func test_timeAndCount_timeAlwaysPrevails() { + 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") } validate { "AB------ A------- ABCDEFG- |" $0.inputs[0].chunks(ofCount: 42, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -161,7 +166,8 @@ final class TestChunk: XCTestCase { } } - func test_timeAndCount_countAlwaysPrevails() { + 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") } validate { "AB --A-B -|" $0.inputs[0].chunks(ofCount: 2, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -169,7 +175,8 @@ final class TestChunk: XCTestCase { } } - func test_timeAndCount_countResetsAfterCount() { + 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") } validate { "ABCDE --- ABCDE |" $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -177,7 +184,8 @@ final class TestChunk: XCTestCase { } } - func test_timeAndCount_countResetsAfterSignal() { + 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") } validate { "AB------ ABCDE |" $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) @@ -185,7 +193,8 @@ final class TestChunk: XCTestCase { } } - func test_timeAndCount_error() { + 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") } validate { "ABC^" $0.inputs[0].chunks(ofCount: 5, or: .repeating(every: .steps(8), clock: $0.clock)).map(concatCharacters) diff --git a/Tests/AsyncAlgorithmsTests/TestDebounce.swift b/Tests/AsyncAlgorithmsTests/TestDebounce.swift index 27643dfb..2005c134 100644 --- a/Tests/AsyncAlgorithmsTests/TestDebounce.swift +++ b/Tests/AsyncAlgorithmsTests/TestDebounce.swift @@ -13,7 +13,8 @@ import XCTest import AsyncAlgorithms final class TestDebounce: XCTestCase { - func test_delayingValues() { + 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") } validate { "abcd----e---f-g----|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -21,7 +22,8 @@ final class TestDebounce: XCTestCase { } } - func test_delayingValues_dangling_last() { + 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") } validate { "abcd----e---f-g-|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -30,7 +32,8 @@ final class TestDebounce: XCTestCase { } - func test_finishDoesntDebounce() { + 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") } validate { "a|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -38,7 +41,8 @@ final class TestDebounce: XCTestCase { } } - func test_throwDoesntDebounce() { + 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") } validate { "a^" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) @@ -46,7 +50,8 @@ final class TestDebounce: XCTestCase { } } - func test_noValues() { + 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") } validate { "----|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) diff --git a/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift b/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift index d054257b..e70b3adf 100644 --- a/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift +++ b/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift @@ -18,7 +18,11 @@ final class TestTaskSelect: XCTestCase { let firstValue = await Task.select(Task { return 1 }, Task { - try! await Task.sleep(until: .now + .seconds(2), clock: .continuous) + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + try! await Task.sleep(until: .now + .seconds(2), clock: .continuous) + } else { + try! await Task.sleep(nanoseconds: 2_000_000_000) + } return 2 }).value XCTAssertEqual(firstValue, 1) @@ -26,7 +30,11 @@ final class TestTaskSelect: XCTestCase { func test_second() async { let firstValue = await Task.select(Task { - try! await Task.sleep(until: .now + .seconds(2), clock: .continuous) + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + try! await Task.sleep(until: .now + .seconds(2), clock: .continuous) + } else { + try! await Task.sleep(nanoseconds: 2_000_000_000) + } return 1 }, Task { return 2 @@ -37,7 +45,11 @@ final class TestTaskSelect: XCTestCase { func test_throwing() async { do { _ = try await Task.select(Task { () async throws -> Int in - try await Task.sleep(until: .now + .seconds(2), clock: .continuous) + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + try await Task.sleep(until: .now + .seconds(2), clock: .continuous) + } else { + try await Task.sleep(nanoseconds: 2_000_000_000) + } return 1 }, Task { () async throws -> Int in throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) @@ -59,7 +71,11 @@ final class TestTaskSelect: XCTestCase { firstCancelled.fulfill() } operation: { () -> Int in firstReady.fulfill() - try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) + } else { + try? await Task.sleep(nanoseconds: 2_000_000_000) + } return 1 } }, Task { @@ -67,7 +83,11 @@ final class TestTaskSelect: XCTestCase { secondCancelled.fulfill() } operation: { () -> Int in secondReady.fulfill() - try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) + } else { + try? await Task.sleep(nanoseconds: 2_000_000_000) + } return 1 } }) diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index 972f04c1..72c90f65 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -13,7 +13,8 @@ import XCTest import AsyncAlgorithms final class TestThrottle: XCTestCase { - func test_rate_0() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(0), clock: $0.clock) @@ -21,7 +22,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_0_leading_edge() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(0), clock: $0.clock, latest: false) @@ -29,7 +31,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_1() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) @@ -37,7 +40,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_1_leading_edge() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(1), clock: $0.clock, latest: false) @@ -45,7 +49,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_2() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) @@ -53,7 +58,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_2_leading_edge() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) @@ -61,7 +67,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_3() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) @@ -69,7 +76,8 @@ final class TestThrottle: XCTestCase { } } - func test_rate_3_leading_edge() { + 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") } validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) @@ -77,7 +85,8 @@ final class TestThrottle: XCTestCase { } } - func test_throwing() { + 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") } validate { "abcdef^hijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) @@ -85,7 +94,8 @@ final class TestThrottle: XCTestCase { } } - func test_throwing_leading_edge() { + 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") } validate { "abcdef^hijk|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) @@ -93,7 +103,8 @@ final class TestThrottle: XCTestCase { } } - func test_emission_2_rate_1() { + 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") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) @@ -101,7 +112,8 @@ final class TestThrottle: XCTestCase { } } - func test_emission_2_rate_2() { + 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") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) @@ -109,7 +121,8 @@ final class TestThrottle: XCTestCase { } } - func test_emission_3_rate_2() { + 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") } validate { "--a--b--c--d--e--f--g|" $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) @@ -117,7 +130,8 @@ final class TestThrottle: XCTestCase { } } - func test_emission_2_rate_3() { + 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") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) diff --git a/Tests/AsyncAlgorithmsTests/TestTimer.swift b/Tests/AsyncAlgorithmsTests/TestTimer.swift index 823e7701..65db910e 100644 --- a/Tests/AsyncAlgorithmsTests/TestTimer.swift +++ b/Tests/AsyncAlgorithmsTests/TestTimer.swift @@ -14,28 +14,32 @@ import AsyncAlgorithms import AsyncSequenceValidation final class TestTimer: XCTestCase { - func test_tick1() { + 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") } validate { AsyncTimerSequence(interval: .steps(1), clock: $0.clock).map { _ in "x" } "xxxxxxx[;|]" } } - func test_tick2() { + 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") } validate { AsyncTimerSequence(interval: .steps(2), clock: $0.clock).map { _ in "x" } "-x-x-x-[;|]" } } - func test_tick3() { + 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") } validate { AsyncTimerSequence(interval: .steps(3), clock: $0.clock).map { _ in "x" } "--x--x-[;|]" } } - func test_tick2_event_skew3() { + 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") } 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 f60259c1..c002d64a 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift @@ -292,7 +292,8 @@ final class TestValidationDiagram: XCTestCase { } } - func test_delayNext_into_emptyTick() { + 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") } validate { "xx|" LaggingAsyncSequence($0.inputs[0], delayBy: .steps(3), using: $0.clock) @@ -309,6 +310,7 @@ final class TestValidationDiagram: XCTestCase { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct LaggingAsyncSequence : AsyncSequence { typealias Element = Base.Element From 816cf644af149232df333ed605e4d6ecf336235a Mon Sep 17 00:00:00 2001 From: shiz <35151927+stzn@users.noreply.github.com> Date: Tue, 28 Jun 2022 01:00:33 +0900 Subject: [PATCH 038/149] Add missing back quote (#171) --- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md index 64fa7a34..8228a4e2 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md @@ -42,7 +42,7 @@ extension AsyncTimerSequence: Sendable { } extension AsyncTimerSequence.Iterator: Sendable { } ``` -Since all the types comprising `AsyncTimerSequence` and it's `Iterator` are `Sendable` these types are also `Sendable. +Since all the types comprising `AsyncTimerSequence` and it's `Iterator` are `Sendable` these types are also `Sendable`. ## Credits/Inspiration From 8a355f015f6bca86a478d86544a73ea46e943cd6 Mon Sep 17 00:00:00 2001 From: Ole Begemann Date: Mon, 27 Jun 2022 18:03:09 +0200 Subject: [PATCH 039/149] Fix a few typos (#173) --- Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift index 07063bf8..848ceeb4 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift @@ -31,7 +31,7 @@ /// } /// /// public func makeAsyncIterator() -> AsyncBufferedByteIterator { -/// return BufferedAsyncByteIterator(capacity: 16384) { buffer in +/// return AsyncBufferedByteIterator(capacity: 16384) { buffer in /// // This runs once every 16384 invocations of next() /// return try await handle.read(into: buffer) /// } @@ -112,7 +112,7 @@ internal struct _AsyncBytesBuffer: @unchecked Sendable { do { // If two tasks have access to this iterator then the references on // the storage will be non uniquely owned. This means that any reload - // must happen into it's own fresh buffer. The consumption of those + // must happen into its own fresh buffer. The consumption of those // bytes between two tasks are inherently defined as potential // duplication by the nature of sending that buffer across the two // tasks - this means that the brief period in which they may be @@ -122,7 +122,7 @@ internal struct _AsyncBytesBuffer: @unchecked Sendable { // should not be crash, but it definitely cannot be consistent. // // The unique ref check is here to prevent the potentials of a crashing - // secnario. + // scenario. if !isKnownUniquelyReferenced(&storage) { // The count is not mutated across invocations so the access is safe. let capacity = storage.buffer.count From 6df7fcdd10d26c97fc52ecbb9ce6d077f1345f61 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Mon, 27 Jun 2022 18:04:15 +0200 Subject: [PATCH 040/149] merge: improve unit tests (#161) --- Tests/AsyncAlgorithmsTests/TestMerge.swift | 323 +++++++++++++++------ 1 file changed, 242 insertions(+), 81 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index fe8ae8d8..3cf7c577 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -13,117 +13,153 @@ import AsyncAlgorithms final class TestMerge2: XCTestCase { - func test_even_values() async { - let merged = merge([1, 2, 3].async, [4, 5, 6].async) + func test_merge_makes_sequence_with_elements_from_sources_when_all_have_same_size() async { + let first = [1, 2, 3] + let second = [4, 5, 6] + + let merged = merge(first.async, second.async) var collected = [Int]() + let expected = Set(first + second).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_longer_first() async { - let merged = merge([1, 2, 3, 4, 5, 6, 7].async, [8, 9, 10].async) + 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] + + let merged = merge(first.async, second.async) var collected = [Int]() + let expected = Set(first + second).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_longer_second() async { - let merged = merge([1, 2, 3].async, [4, 5, 6, 7].async) + 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] + + let merged = merge(first.async, second.async) var collected = [Int]() + let expected = Set(first + second).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected) - let b = Set([1, 2, 3, 4, 5, 6, 7]) - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_throwing_first() async throws { - let merged = merge([1, 2, 3, 4, 5].async.map { try throwOn(4, $0) }, [6, 7, 8].async) + 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 merged = merge(first.async.map { try throwOn(4, $0) }, second.async) var collected = Set() + let expected = Set([1, 2, 3]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the first sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([1, 2, 3]), Set([1, 2, 3])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_longer_second_throwing_first() async throws { - let merged = merge([1, 2, 3, 4, 5].async.map { try throwOn(4, $0) }, [6, 7, 8, 9, 10, 11].async) + 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 merged = merge(first.async.map { try throwOn(4, $0) }, second.async) var collected = Set() + let expected = Set([1, 2, 3]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the first sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([1, 2, 3]), Set([1, 2, 3])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_throwing_second() async throws { - let merged = merge([1, 2, 3].async, [4, 5, 6, 7, 8].async.map { try throwOn(7, $0) }) + 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 merged = merge(first.async, second.async.map { try throwOn(7, $0) }) var collected = Set() + let expected = Set([4, 5, 6]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the second sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([4, 5, 6]), Set([4, 5, 6])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_longer_first_throwing_second() async throws { - let merged = merge([1, 2, 3, 4, 5, 6, 7].async, [7, 8, 9, 10, 11].async.map { try throwOn(10, $0) }) + 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 merged = merge(first.async, second.async.map { try throwOn(10, $0) }) var collected = Set() + let expected = Set([7, 8, 9]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the second sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([7, 8, 9]), Set([7, 8, 9])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_diagram() { + func test_merge_makes_sequence_with_ordered_elements_when_sources_follow_a_timeline() { validate { "a-c-e-g-|" "-b-d-f-h" @@ -132,7 +168,7 @@ final class TestMerge2: XCTestCase { } } - func test_cancellation() async { + func test_merge_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") let sequence = merge(source1.async, source2.async) @@ -158,159 +194,284 @@ final class TestMerge2: XCTestCase { } final class TestMerge3: XCTestCase { - func test_even_values() async { - let merged = merge([1, 2, 3].async, [4, 5, 6].async, [7, 8, 9].async) + func test_merge_makes_sequence_with_elements_from_sources_when_all_have_same_size() async { + let first = [1, 2, 3] + let second = [4, 5, 6] + let third = [7, 8, 9] + + let merged = merge(first.async, second.async, third.async) var collected = [Int]() + let expected = Set(first + second + third).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_longer_first() async { - let merged = merge([1, 2, 3, 4, 5].async, [6, 7, 8].async, [9, 10, 11].async) + func test_merge_makes_sequence_with_elements_from_sources_when_first_is_longer() async { + let first = [1, 2, 3, 4, 5] + let second = [6, 7, 8] + let third = [9, 10, 11] + + let merged = merge(first.async, second.async, third.async) var collected = [Int]() + let expected = Set(first + second + third).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 11]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_longer_second() async { - let merged = merge([1, 2, 3].async, [4, 5, 6, 7, 8].async, [9, 10, 11].async) + 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, 8] + let third = [9, 10, 11] + + let merged = merge(first.async, second.async, third.async) var collected = [Int]() + let expected = Set(first + second + third).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 11]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_longer_third() async { - let merged = merge([1, 2, 3].async, [4, 5, 6].async, [7, 8, 9, 10, 11].async) + func test_merge_makes_sequence_with_elements_from_sources_when_third_is_longer() async { + let first = [1, 2, 3] + let second = [4, 5, 6] + let third = [7, 8, 9, 10, 11] + + let merged = merge(first.async, second.async, third.async) var collected = [Int]() + let expected = Set(first + second + third).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 11]).sorted() - XCTAssertEqual(a, b) + 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] + let third = [10, 11] - func test_longer_first_and_third() async { - let merged = merge([1, 2, 3, 4, 5].async, [6, 7].async, [8, 9, 10, 11].async) + let merged = merge(first.async, second.async, third.async) var collected = [Int]() + let expected = Set(first + second + third).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 11]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_longer_second_and_third() async { - let merged = merge([1, 2, 3].async, [4, 5, 6, 7].async, [8, 9, 10, 11].async) + func test_merge_makes_sequence_with_elements_from_sources_when_first_and_third_are_longer() async { + let first = [1, 2, 3, 4, 5] + let second = [6, 7] + let third = [8, 9, 10, 11] + + let merged = merge(first.async, second.async, third.async) var collected = [Int]() + let expected = Set(first + second + third).sorted() + var iterator = merged.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - let a = Set(collected).sorted() - let b = Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 11]).sorted() - XCTAssertEqual(a, b) + XCTAssertEqual(Set(collected).sorted(), expected) } - func test_throwing_first() async throws { - let merged = merge([1, 2, 3, 4, 5].async.map { try throwOn(4, $0) }, [6, 7, 8].async, [9, 10, 11].async) + func test_merge_makes_sequence_with_elements_from_sources_when_second_and_third_are_longer() async { + let first = [1, 2, 3] + let second = [4, 5, 6, 7] + let third = [8, 9, 10, 11] + + let merged = merge(first.async, second.async, third.async) + var collected = [Int]() + let expected = Set(first + second + third).sorted() + + var iterator = merged.makeAsyncIterator() + while let item = await iterator.next() { + collected.append(item) + } + let pastEnd = await iterator.next() + + 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 { + let first = [1, 2, 3, 4, 5] + let second = [6, 7, 8] + let third = [9, 10, 11] + + let merged = merge(first.async.map { try throwOn(4, $0) }, second.async, third.async) var collected = Set() + let expected = Set([1, 2, 3]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the first sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([1, 2, 3]), Set([1, 2, 3])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_longer_second_throwing_first() async throws { - let merged = merge([1, 2, 3, 4, 5].async.map { try throwOn(4, $0) }, [6, 7, 8, 9, 10, 11].async, [12, 13, 14].async) + 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] + + let merged = merge(first.async.map { try throwOn(4, $0) }, second.async, third.async) var collected = Set() + let expected = Set([1, 2, 3]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the first sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([1, 2, 3]), Set([1, 2, 3])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_throwing_second() async throws { - let merged = merge([1, 2, 3].async, [4, 5, 6, 7, 8].async.map { try throwOn(7, $0) }, [9, 10, 11].async) + 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] + + let merged = merge(first.async, second.async.map { try throwOn(7, $0) }, third.async) var collected = Set() + let expected = Set([4, 5, 6]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the second sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([4, 5, 6]), Set([4, 5, 6])) let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_longer_first_throwing_second() async throws { - let merged = merge([1, 2, 3, 4, 5, 6, 7].async, [7, 8, 9, 10, 11].async.map { try throwOn(10, $0) }, [12, 13, 14].async) + 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] + + let merged = merge(first.async, second.async.map { try throwOn(10, $0) }, third.async) var collected = Set() + let expected = Set([7, 8, 9]) + var iterator = merged.makeAsyncIterator() do { while let item = try await iterator.next() { collected.insert(item) } - XCTFail() + XCTFail("Merged sequence should throw after collecting three first elements from the second sequence") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual(collected.intersection([7, 8, 9]), Set([7, 8, 9])) let pastEnd = try await iterator.next() + + 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 { + let first = [1, 2, 3] + let second = [4, 5, 6] + let third = [7, 8, 9, 10, 11] + + let merged = merge(first.async, second.async, third.async.map { try throwOn(10, $0) }) + var collected = Set() + let expected = Set([7, 8, 9]) + + var iterator = merged.makeAsyncIterator() + do { + while let item = try await iterator.next() { + collected.insert(item) + } + XCTFail("Merged sequence should throw after collecting three first elements from the third sequence") + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try await iterator.next() + + XCTAssertNil(pastEnd) + 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 { + let first = [1, 2, 3, 4, 5, 6, 7] + let second = [7, 8, 9, 10, 11] + let third = [12, 13, 14, 15] + + let merged = merge(first.async, second.async, third.async.map { try throwOn(15, $0) }) + var collected = Set() + let expected = Set([12, 13, 14]) + + var iterator = merged.makeAsyncIterator() + do { + while let item = try await iterator.next() { + collected.insert(item) + } + XCTFail("Merged sequence should throw after collecting three first elements from the third sequence") + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(collected.intersection(expected), expected) } - func test_diagram() { + func test_merge_makes_sequence_with_ordered_elements_when_sources_follow_a_timeline() { validate { "a---e---|" "-b-d-f-h|" @@ -320,7 +481,7 @@ final class TestMerge3: XCTestCase { } } - func test_cancellation() async { + func test_merge_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") let source3 = Indefinite(value: "test3") From 9baff2ad3d3691f078b04704fa1d9e154845a078 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Mon, 27 Jun 2022 18:04:37 +0200 Subject: [PATCH 041/149] [AsyncThrowingChannel] make the `fail` terminal event non async (#164) * asyncThrowingChannel: make fail set a terminal state * asyncChannel: align naming on asyncThrowingChannel * asyncChannel: remove a test base on Task.sleep * guides: update Channel with non async fail(_:) --- .../AsyncAlgorithms.docc/Guides/Channel.md | 4 +- Sources/AsyncAlgorithms/AsyncChannel.swift | 20 +-- .../AsyncThrowingChannel.swift | 116 ++++++++++++------ Tests/AsyncAlgorithmsTests/TestChannel.swift | 77 +++--------- 4 files changed, 109 insertions(+), 108 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md index 974903b5..5121c769 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md @@ -44,14 +44,14 @@ public final class AsyncThrowingChannel: Asyn public init(element elementType: Element.Type = Element.self, failure failureType: Failure.Type = Failure.self) public func send(_ element: Element) async - public func fail(_ error: Error) async where Failure == Error + public func fail(_ error: Error) where Failure == Error public func finish() public func makeAsyncIterator() -> Iterator } ``` -Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` and `fail(_:)` via the suspension/resume ensure that the production of values does not exceed the consumption of values from iteration. Each of these methods suspend after enqueuing the event and are resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. +Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` via the suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. This method suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` or `fail(_:)` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, or by throwing an error, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. ```swift let channel = AsyncChannel() diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index 0f30a6f6..facdaadf 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -13,10 +13,12 @@ /// /// The `AsyncChannel` class is intended to be used as a communication type between tasks, /// particularly when one task produces values and another task consumes those values. The back -/// pressure applied by `send(_:)` and `finish()` via the suspension/resume ensures that -/// the production of values does not exceed the consumption of values from iteration. Each of these -/// methods suspends after enqueuing the event and is resumed when the next call to `next()` -/// on the `Iterator` is made. +/// pressure applied by `send(_:)` via the suspension/resume ensures that +/// the production of values does not exceed the consumption of values from iteration. This method +/// suspends after enqueuing the event and is resumed when the next call to `next()` +/// on the `Iterator` is made, or when `finish()` is called from another Task. +/// As `finish()` induces a terminal state, there is no need for a back pressure management. +/// This function does not suspend and will finish all the pending iterations. public final class AsyncChannel: AsyncSequence, Sendable { /// The iterator for a `AsyncChannel` instance. public struct Iterator: AsyncIteratorProtocol, Sendable { @@ -168,7 +170,7 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } - func finishAll() { + func terminateAll() { let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in if state.terminal { return ([], []) @@ -195,7 +197,7 @@ public final class AsyncChannel: AsyncSequence, Sendable { func _send(_ element: Element) async { await withTaskCancellationHandler { - finishAll() + terminateAll() } operation: { let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in state.withCriticalRegion { state -> UnsafeResumption?, Never>? in @@ -225,15 +227,17 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } - /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made. + /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made + /// or when a call to `finish()` is made from another Task. /// If the channel is already finished then this returns immediately public func send(_ element: Element) async { await _send(element) } /// Send a finish to all awaiting iterations. + /// All subsequent calls to `next(_:)` will resume immediately. public func finish() { - finishAll() + terminateAll() } /// Create an `Iterator` for iteration of an `AsyncChannel` diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 58c9e8c9..5ce68961 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -11,7 +11,13 @@ /// An error-throwing channel for sending elements from on task to another with back pressure. /// -/// The `AsyncThrowingChannel` class is intended to be used as a communication types between tasks, particularly when one task produces values and another task consumes those values. The back pressure applied by `send(_:)`, `fail(_:)` and `finish()` via suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. Each of these methods suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. +/// The `AsyncThrowingChannel` class is intended to be used as a communication types between tasks, +/// particularly when one task produces values and another task consumes those values. The back +/// pressure applied by `send(_:)` via suspension/resume ensures that the production of values does +/// not exceed the consumption of values from iteration. This method suspends after enqueuing the event +/// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called +/// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no need for a back pressure management. +/// Those functions do not suspend and will finish all the pending iterations. public final class AsyncThrowingChannel: AsyncSequence, Sendable { /// The iterator for an `AsyncThrowingChannel` instance. public struct Iterator: AsyncIteratorProtocol, Sendable { @@ -78,12 +84,23 @@ public final class AsyncThrowingChannel: Asyn return lhs.generation == rhs.generation } } + + enum Termination { + case finished + case failed(Error) + } enum Emission { case idle case pending([UnsafeContinuation?, Never>]) case awaiting(Set) - + case terminated(Termination) + + var isTerminated: Bool { + guard case .terminated = self else { return false } + return true + } + mutating func cancel(_ generation: Int) -> UnsafeContinuation? { switch self { case .awaiting(var awaiting): @@ -106,9 +123,8 @@ public final class AsyncThrowingChannel: Asyn struct State { var emission: Emission = .idle var generation = 0 - var terminal = false } - + let state = ManagedCriticalState(State()) public init(_ elementType: Element.Type = Element.self) { } @@ -129,12 +145,9 @@ public final class AsyncThrowingChannel: Asyn func next(_ generation: Int) async throws -> Element? { return try await withUnsafeThrowingContinuation { continuation in var cancelled = false - var terminal = false + var potentialTermination: Termination? + state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - if state.terminal { - terminal = true - return nil - } switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) @@ -158,53 +171,78 @@ public final class AsyncThrowingChannel: Asyn state.emission = .awaiting(nexts) } return nil + case .terminated(let termination): + potentialTermination = termination + state.emission = .terminated(.finished) + return nil } }?.resume() - if cancelled || terminal { + + if cancelled { continuation.resume(returning: nil) + return + } + + switch potentialTermination { + case .none: + return + case .failed(let error): + continuation.resume(throwing: error) + return + case .finished: + continuation.resume(returning: nil) + return } } } - - func finishAll() { + + func terminateAll(error: Failure? = nil) { let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in - if state.terminal { - return ([], []) + + let nextState: Emission + if let error = error { + nextState = .terminated(.failed(error)) + } else { + nextState = .terminated(.finished) } - state.terminal = true + switch state.emission { case .idle: + state.emission = nextState return ([], []) case .pending(let nexts): - state.emission = .idle + state.emission = nextState return (nexts, []) case .awaiting(let nexts): - state.emission = .idle + state.emission = nextState return ([], nexts) + case .terminated: + return ([], []) } } + for send in sends { send.resume(returning: nil) } - for next in nexts { - next.continuation?.resume(returning: nil) + + if let error = error { + for next in nexts { + next.continuation?.resume(throwing: error) + } + } else { + for next in nexts { + next.continuation?.resume(returning: nil) + } } + } - func _send(_ result: Result) async { + func _send(_ element: Element) async { await withTaskCancellationHandler { - finishAll() + terminateAll() } operation: { let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - if state.terminal { - return UnsafeResumption(continuation: continuation, success: nil) - } - - if case .failure = result { - state.terminal = true - } - switch state.emission { case .idle: state.emission = .pending([continuation]) @@ -221,28 +259,32 @@ public final class AsyncThrowingChannel: Asyn state.emission = .awaiting(nexts) } return UnsafeResumption(continuation: continuation, success: next) + case .terminated: + return UnsafeResumption(continuation: continuation, success: nil) } }?.resume() } - continuation?.resume(with: result.map { $0 as Element? }) + continuation?.resume(returning: element) } } - /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made. + /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made + /// or when a call to `finish()`/`fail(_:)` is made from another Task. /// If the channel is already finished then this returns immediately public func send(_ element: Element) async { - await _send(.success(element)) + await _send(element) } - /// Send an error to an awaiting iteration. This function will resume when the next call to `next()` is made. - /// If the channel is already finished then this returns immediately - public func fail(_ error: Error) async where Failure == Error { - await _send(.failure(error)) + /// Send an error to all awaiting iterations. + /// All subsequent calls to `next(_:)` will resume immediately. + public func fail(_ error: Error) where Failure == Error { + terminateAll(error: error) } /// Send a finish to all awaiting iterations. + /// All subsequent calls to `next(_:)` will resume immediately. public func finish() { - finishAll() + terminateAll() } public func makeAsyncIterator() -> Iterator { diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index 891ec434..66fd1e1d 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -63,11 +63,12 @@ final class TestChannel: XCTestCase { XCTAssertEqual(collected, expected) } - func test_asyncThrowingChannel_throws_when_fail_is_called() async { + func test_asyncThrowingChannel_throws_and_discards_additional_sent_values_when_fail_is_called() async { + let sendImmediatelyResumes = expectation(description: "Send immediately resumes after fail") + let channel = AsyncThrowingChannel() - Task { - await channel.fail(Failure()) - } + channel.fail(Failure()) + var iterator = channel.makeAsyncIterator() do { let _ = try await iterator.next() @@ -75,6 +76,17 @@ final class TestChannel: XCTestCase { } catch { XCTAssertEqual(error as? Failure, Failure()) } + + do { + let pastFailure = try await iterator.next() + XCTAssertNil(pastFailure) + } catch { + XCTFail("The AsyncThrowingChannel should not fail when failure has already been fired") + } + + await channel.send("send") + sendImmediatelyResumes.fulfill() + wait(for: [sendImmediatelyResumes], timeout: 1.0) } func test_asyncChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called() async { @@ -132,63 +144,6 @@ final class TestChannel: XCTestCase { wait(for: [additionalSend], timeout: 1.0) } - func test_asyncChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called2() async throws { - let channel = AsyncChannel() - let complete = ManagedCriticalState(false) - let finished = expectation(description: "finished") - - let valueFromConsumer1 = ManagedCriticalState(nil) - let valueFromConsumer2 = ManagedCriticalState(nil) - - let received = expectation(description: "received") - received.expectedFulfillmentCount = 2 - - let pastEnd = expectation(description: "pastEnd") - pastEnd.expectedFulfillmentCount = 2 - - Task(priority: .high) { - var iterator = channel.makeAsyncIterator() - let ending = await iterator.next() - valueFromConsumer1.withCriticalRegion { $0 = ending } - received.fulfill() - let item = await iterator.next() - XCTAssertNil(item) - pastEnd.fulfill() - } - - Task(priority: .high) { - var iterator = channel.makeAsyncIterator() - let ending = await iterator.next() - valueFromConsumer2.withCriticalRegion { $0 = ending } - received.fulfill() - let item = await iterator.next() - XCTAssertNil(item) - pastEnd.fulfill() - } - - try await Task.sleep(nanoseconds: 1_000_000_000) - - Task(priority: .low) { - channel.finish() - complete.withCriticalRegion { $0 = true } - finished.fulfill() - } - - wait(for: [finished, received], timeout: 1.0) - - XCTAssertTrue(complete.withCriticalRegion { $0 }) - XCTAssertEqual(valueFromConsumer1.withCriticalRegion { $0 }, nil) - XCTAssertEqual(valueFromConsumer2.withCriticalRegion { $0 }, nil) - - wait(for: [pastEnd], timeout: 1.0) - let additionalSend = expectation(description: "additional send") - Task { - await channel.send("test") - additionalSend.fulfill() - } - wait(for: [additionalSend], timeout: 1.0) - } - func test_asyncThrowingChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called() async { let channel = AsyncThrowingChannel() let complete = ManagedCriticalState(false) From 2a211e18be0c189bc8a0f6782868d41631b2be23 Mon Sep 17 00:00:00 2001 From: shiz <35151927+stzn@users.noreply.github.com> Date: Tue, 28 Jun 2022 06:23:48 +0900 Subject: [PATCH 042/149] Fix typo in Validation.md (#172) * Add escaping characters before | * Add more escaping characters before | --- .../AsyncSequenceValidation.docc/Validation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/Validation.md b/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/Validation.md index 7851b338..47cc7e21 100644 --- a/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/Validation.md +++ b/Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/Validation.md @@ -41,7 +41,7 @@ The syntax is trivially parsable (and consequently customizable). By default, th | Symbol | Description | Example | | ------- | ----------------- | ---------- | | `-` | Advance time | `"a--b--"` | -| `|` | Termination | `"ab-|"` | +| `\|` | Termination | `"ab-\|"` | | `^` | Thrown error | `"ab-^"` | | `;` | Cancellation | `"ab;-"` | | `[` | Begin group | `"[ab]-"` | @@ -218,15 +218,15 @@ Access to the validation diagram input list is done through calls such as `$0.in | Symbol | Token | Description | Example | | ------- | ------------------------- | ----------------- | ---------- | | `-` | `.step` | Advance time | `"a--b--"` | -| `|` | `.finish` | Termination | `"ab-|"` | +| `\|` | `.finish` | Termination | `"ab-\|"` | | `^` | `.error` | Thrown error | `"ab-^"` | | `;` | `.cancel` | Cancellation | `"ab;-"` | | `[` | `.beginGroup` | Begin group | `"[ab]-"` | | `]` | `.endGroup` | End group | `"[ab]-"` | | `'` | `.beginValue` `.endValue` | Begin/End Value | `"'foo'-"` | | `,` | `.delayNext` | Delay next | `",[a,]b"` | -| ` ` | `.skip` | Skip/Ignore | `"a b- |"` | -| | `.value` | Values. | `"ab-|"` | +| ` ` | `.skip` | Skip/Ignore | `"a b- \|"` | +| | `.value` | Values. | `"ab-\|"` | There are some diagram input specifications that are not valid. The three cases are: From 6ff1f1b4116d4eadf887168d70af9e6de34ae73b Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 13 Jul 2022 09:02:40 -0700 Subject: [PATCH 043/149] Compacted proposal (#162) --- Evolution/NNNN-compacted.md | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 Evolution/NNNN-compacted.md diff --git a/Evolution/NNNN-compacted.md b/Evolution/NNNN-compacted.md new file mode 100644 index 00000000..3770a4a6 --- /dev/null +++ b/Evolution/NNNN-compacted.md @@ -0,0 +1,61 @@ +# Compacted + +* Proposal: [NNNN](NNNN-compacted.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** + +* Implementation: [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift) + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCompacted.swift) + +## Proposed Solution + +Similar to the Swift Algorithms package we propose that a new method be added to `AsyncSequence` to fit this need. + +```swift +extension AsyncSequence { + public func compacted() -> AsyncCompactedSequence + where Element == Unwrapped? +} +``` + +This is equivalent to writing `.compactMap { $0 }` from a behavioral standpoint but is easier to reason about and is more efficient since it does not need to execute or store a closure. + +## Detailed Design + +The `AsyncCompactedSequence` type from an effects standpoint works just like `AsyncCompactMapSequence`. When the base asynchronous sequence throws, the iteration of `AsyncCompactedSequence` can throw. Likewise if the base does not throw then the iteration of `AsyncCompactedSequence` does not throw. This type is conditionally `Sendable` when the base, base element, and base iterator are `Sendable. + +```swift +public struct AsyncCompactedSequence: AsyncSequence + where Base.Element == Element? { + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator { + Iterator(base.makeAsyncIterator()) + } +} + +extension AsyncCompactedSequence: Sendable + where + Base: Sendable, Base.Element: Sendable, + Base.AsyncIterator: Sendable { } + +extension AsyncCompactedSequence.Iterator: Sendable + where + Base: Sendable, Base.Element: Sendable, + Base.AsyncIterator: Sendable { } +``` + +## Effect on API resilience + +Compacted has a trivial implementation and is marked as `@frozen` and `@inlinable`. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable. + +## Alternatives considered + +None; shy of potentially eliding this since the functionality is so trivial. However the utility of this function aides in ease of use and approachability along with parity with the Swift Algorithms package. + +## Acknowledgments + +This transformation function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md) From 45aa5b950c9e7410848eef4b1a3e02aa74080302 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 13 Jul 2022 10:15:10 -0700 Subject: [PATCH 044/149] Rename compacted proposal to 0003 --- Evolution/{NNNN-compacted.md => 0003-compacted.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Evolution/{NNNN-compacted.md => 0003-compacted.md} (100%) diff --git a/Evolution/NNNN-compacted.md b/Evolution/0003-compacted.md similarity index 100% rename from Evolution/NNNN-compacted.md rename to Evolution/0003-compacted.md From 57072e617a602f2664a38902e653e9ba966acb18 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 13 Jul 2022 10:19:35 -0700 Subject: [PATCH 045/149] Correct proposal link for compacted --- Evolution/0003-compacted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evolution/0003-compacted.md b/Evolution/0003-compacted.md index 3770a4a6..7c7d026a 100644 --- a/Evolution/0003-compacted.md +++ b/Evolution/0003-compacted.md @@ -1,6 +1,6 @@ # Compacted -* Proposal: [NNNN](NNNN-compacted.md) +* Proposal: [0003](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0003-compacted.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** From f44d02eb26f79d43215d0952b6e8cddc9c369baa Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Mon, 18 Jul 2022 18:28:55 +0200 Subject: [PATCH 046/149] Improve adjacent pairs unit tests (#179) * adjacentPairs: improve unit tests * adjacentPairs: conform indentation to project standards --- .../AsyncAdjacentPairsSequence.swift | 108 ++++++++--------- .../TestAdjacentPairs.swift | 113 ++++++++++++------ 2 files changed, 129 insertions(+), 92 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 2186e364..7a69bf35 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -13,74 +13,74 @@ /// `AsyncSequence`. @frozen public struct AsyncAdjacentPairsSequence: AsyncSequence { - public typealias Element = (Base.Element, Base.Element) + public typealias Element = (Base.Element, Base.Element) - @usableFromInline - let base: Base + @usableFromInline + let base: Base - @inlinable - init(_ base: Base) { - self.base = base - } + @inlinable + init(_ base: Base) { + self.base = base + } - /// The iterator for an `AsyncAdjacentPairsSequence` instance. - @frozen - public struct Iterator: AsyncIteratorProtocol { - public typealias Element = (Base.Element, Base.Element) + /// The iterator for an `AsyncAdjacentPairsSequence` instance. + @frozen + public struct Iterator: AsyncIteratorProtocol { + public typealias Element = (Base.Element, Base.Element) - @usableFromInline - var base: Base.AsyncIterator + @usableFromInline + var base: Base.AsyncIterator - @usableFromInline - internal var previousElement: Base.Element? + @usableFromInline + internal var previousElement: Base.Element? - @inlinable - init(_ base: Base.AsyncIterator) { - self.base = base - } + @inlinable + init(_ base: Base.AsyncIterator) { + self.base = base + } - @inlinable - public mutating func next() async rethrows -> (Base.Element, Base.Element)? { - if previousElement == nil { - previousElement = try await base.next() - } + @inlinable + public mutating func next() async rethrows -> (Base.Element, Base.Element)? { + if previousElement == nil { + previousElement = try await base.next() + } - guard let previous = previousElement, let next = try await base.next() else { - return nil - } + guard let previous = previousElement, let next = try await base.next() else { + return nil + } - previousElement = next - return (previous, next) - } + previousElement = next + return (previous, next) } + } - @inlinable - public func makeAsyncIterator() -> Iterator { - Iterator(base.makeAsyncIterator()) - } + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(base.makeAsyncIterator()) + } } extension AsyncSequence { - /// An `AsyncSequence` that iterates over the adjacent pairs of the original - /// original `AsyncSequence`. - /// - /// ``` - /// for await (first, second) in (1...5).async.adjacentPairs() { - /// print("First: \(first), Second: \(second)") - /// } - /// - /// // First: 1, Second: 2 - /// // First: 2, Second: 3 - /// // First: 3, Second: 4 - /// // First: 4, Second: 5 - /// ``` - /// - /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements - /// or the original `AsyncSequence`. - @inlinable - public func adjacentPairs() -> AsyncAdjacentPairsSequence { - AsyncAdjacentPairsSequence(self) - } + /// An `AsyncSequence` that iterates over the adjacent pairs of the original + /// original `AsyncSequence`. + /// + /// ``` + /// for await (first, second) in (1...5).async.adjacentPairs() { + /// print("First: \(first), Second: \(second)") + /// } + /// + /// // First: 1, Second: 2 + /// // First: 2, Second: 3 + /// // First: 3, Second: 4 + /// // First: 4, Second: 5 + /// ``` + /// + /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements + /// or the original `AsyncSequence`. + @inlinable + public func adjacentPairs() -> AsyncAdjacentPairsSequence { + AsyncAdjacentPairsSequence(self) + } } extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index ade9c8f4..3bcd73cf 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -13,48 +13,85 @@ import AsyncAlgorithms final class TestAdjacentPairs: XCTestCase { - func test_adjacentPairs() async { - let source = 1...5 - let expected = [(1,2), (2,3), (3,4), (4,5)] - let sequence = source.async.adjacentPairs() - var actual: [(Int, Int)] = [] - for await item in sequence { - actual.append(item) - } - XCTAssertEqual(expected, actual) + func test_adjacentPairs_produces_tuples_of_adjacent_values_of_original_element() async { + let source = 1...5 + let expected = Array(zip(source, source.dropFirst())) + + let sequence = source.async.adjacentPairs() + var actual: [(Int, Int)] = [] + for await item in sequence { + actual.append(item) } - func test_empty() async { - let source = 0..<1 - let expected: [(Int, Int)] = [] - let sequence = source.async.adjacentPairs() - var actual: [(Int, Int)] = [] - for await item in sequence { - actual.append(item) - } - XCTAssertEqual(expected, actual) + XCTAssertEqual(expected, actual) + } + + func test_adjacentPairs_forwards_termination_from_source_when_iteration_is_finished() async { + let source = 1...5 + + var iterator = source.async.adjacentPairs().makeAsyncIterator() + while let _ = await iterator.next() {} + + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + } + + func test_adjacentPairs_produces_empty_sequence_when_source_sequence_is_empty() async { + let source = 0..<1 + let expected: [(Int, Int)] = [] + + let sequence = source.async.adjacentPairs() + var actual: [(Int, Int)] = [] + for await item in sequence { + actual.append(item) } - func test_cancellation() async { - let source = Indefinite(value: 0) - let sequence = source.async.adjacentPairs() - let finished = expectation(description: "finished") - let iterated = expectation(description: "iterated") - let task = Task { - var firstIteration = false - for await _ in sequence { - if !firstIteration { - firstIteration = true - iterated.fulfill() - } - } - finished.fulfill() + XCTAssertEqual(expected, actual) + } + + func test_adjacentPairs_throws_when_source_sequence_throws() async throws { + let source = 1...5 + let expected = [(1, 2), (2, 3)] + + let sequence = source.async.map { try throwOn(4, $0) }.adjacentPairs() + var iterator = sequence.makeAsyncIterator() + var actual = [(Int, Int)]() + do { + while let value = try await iterator.next() { + actual.append(value) + } + XCTFail(".adjacentPairs should throw when the source sequence throws") + } catch { + XCTAssertEqual(error as? Failure, Failure()) + } + + XCTAssertEqual(actual, expected) + let pastEnd = try await iterator.next() + XCTAssertNil(pastEnd) + } + + func test_adjacentPairs_finishes_when_iteration_task_is_cancelled() async { + let source = Indefinite(value: 0) + let sequence = source.async.adjacentPairs() + let finished = expectation(description: "finished") + let iterated = expectation(description: "iterated") + + let task = Task { + var firstIteration = false + for await _ in sequence { + if !firstIteration { + firstIteration = true + iterated.fulfill() } - // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) - // cancellation should ensure the loop finishes - // without regards to the remaining underlying sequence - task.cancel() - wait(for: [finished], timeout: 1.0) + } + finished.fulfill() } + + // ensure the other task actually starts + wait(for: [iterated], timeout: 1.0) + // cancellation should ensure the loop finishes + // without regards to the remaining underlying sequence + task.cancel() + wait(for: [finished], timeout: 1.0) + } } From b92ba058d8c108f25eb5d2209d71a1c3330a3ae3 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 4 Aug 2022 17:42:14 +0100 Subject: [PATCH 047/149] Proposal for `joined` and `joined(separator:)` (#180) * Proposal for `joined` and `joined(separator:)` * Small fixes * Update proposal # to 0004 Co-authored-by: Philippe Hausler --- Evolution/0004-joined.md | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Evolution/0004-joined.md diff --git a/Evolution/0004-joined.md b/Evolution/0004-joined.md new file mode 100644 index 00000000..b86969c6 --- /dev/null +++ b/Evolution/0004-joined.md @@ -0,0 +1,67 @@ +# Joined + +* Proposal: [SAA-0004](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0004-joined.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Review Manager: [Franz Busch](https://github.com/FranzBusch) +* Status: **Implemented** + +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestJoin.swift)] +* Decision Notes: +* Bugs: + +## Introduction + +The `joined()` and `joined(separator:)` algorithms on `AsyncSequence`s provide APIs to concatenate an `AsyncSequence` of `AsyncSequence`s. + +```swift +extension AsyncSequence where Element: AsyncSequence { + public func joined() -> AsyncJoinedSequence +} + +extension AsyncSequence where Element: AsyncSequence { + public func joined(separator: Separator) -> AsyncJoinedBySeparatorSequence +} +``` + +## Detailed Design + +These algorithms iterate over the elements of each `AsyncSequence` one bye one, i.e. only after the iteration of one `AsyncSequence` has finished the next one will be started. + +```swift + let appleFeed = URL("http://www.example.com/ticker?symbol=AAPL").lines + let nasdaqFeed = URL("http://www.example.com/ticker?symbol=^IXIC").lines + + for try await line in [appleFeed, nasdaqFeed].async.joined() { + print("\(line)") + } + ``` + + Given some sample inputs the following combined events can be expected. + + | Timestamp | appleFeed | nasdaqFeed | output | + | ----------- | --------- | ---------- | ----------------------------- | + | 11:40 AM | 173.91 | | 173.91 | + | 12:25 AM | | 14236.78 | | + | 12:40 AM | | 14218.34 | | + | 1:15 PM | 173.00 | | 173.00 | + | 1:15 PM | | | 14236.78 | + | 1:15 PM | | | 14218.34 | + + +The `joined()` and `joined(separator:)` methods are available on `AsyncSequence`s with elements that are `AsyncSequence`s themselves and produce either an `AsyncJoinedSequence` or an `AsyncJoinedBySeparatorSequence`. + +As soon as an inner `AsyncSequence` returns `nil` the algorithm continues with iterating the next inner `AsyncSequence`. + +The throwing behaviour of `AsyncJoinedSequence` and `AsyncJoinedBySeparatorSequence` is that if any of the inner `AsyncSequence`s throws, then the composed sequence throws on its iteration. + +### Naming + +The naming follows to current method naming of the standard library's [`joined`](https://developer.apple.com/documentation/swift/array/joined(separator:)-7uber) method. +Prior art in the reactive community often names this method `concat`; however, we think that an alignment with the current method on `Sequence` is better. + +### Comparison with other libraries + +**ReactiveX** ReactiveX has an [API definition of Concat](https://reactivex.io/documentation/operators/concat.html) as a top level function for concatenating Observables. + +**Combine** Combine has an [API definition of append](https://developer.apple.com/documentation/combine/publisher/append(_:)-5yh02) which offers similar functionality but limited to concatenating two individual `Publisher`s. From e3a18a0353993042ff564e3fbb3f199eede6e190 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 4 Aug 2022 09:43:14 -0700 Subject: [PATCH 048/149] Modify the adjacent pairs guide into a proposal ready for review (#178) * Modify the adjacent pairs guide into a proposal ready for review * Update the proposal # to 0005 --- Evolution/0005-adjacent-pairs.md | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Evolution/0005-adjacent-pairs.md diff --git a/Evolution/0005-adjacent-pairs.md b/Evolution/0005-adjacent-pairs.md new file mode 100644 index 00000000..ce0b152d --- /dev/null +++ b/Evolution/0005-adjacent-pairs.md @@ -0,0 +1,55 @@ +# AdjacentPairs + +* Proposal: [SAA-0005](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0005-adjacent-pairs.md) +* Author(s): [László Teveli](https://github.com/tevelee) +* Review Manager: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift)] +* Decision Notes: +* Bugs: + +## Introduction + +The `adjacentPairs()` API serve the purpose of collecting adjacent values. This operation is available for any `AsyncSequence` by calling the `adjacentPairs()` method. + +```swift +extension AsyncSequence { + public func adjacentPairs() -> AsyncAdjacentPairsSequence +} +``` + +## Detailed Design + +The `adjacentPairs()` algorithm produces elements of tuple (size of 2), containing a pair of the original `Element` type. + +The interface for this algorithm is available on all `AsyncSequence` types. The returned `AsyncAdjacentPairsSequence` conditionally conforms to `Sendable`. + +Its iterator keeps track of the previous element returned in the `next()` function and updates it in every turn. + +```swift +for await (first, second) in (1...5).async.adjacentPairs() { + print("First: \(first), Second: \(second)") +} + +// First: 1, Second: 2 +// First: 2, Second: 3 +// First: 3, Second: 4 +// First: 4, Second: 5 +``` + +It composes well with the [Dictionary.init(_:uniquingKeysWith:)](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md) API that deals with `AsyncSequence` of tuples. + +```swift +Dictionary(uniqueKeysWithValues: url.lines.adjacentPairs()) +``` + +## Alternatives Considered + +This functionality is often written as a `zip` of a sequence together with itself, dropping its first element (`zip(source, source.dropFirst())`). + +It's such a dominant use-case, the [swift-algorithms](https://github.com/apple/swift-algorithms) package also [introduced](https://github.com/apple/swift-algorithms/pull/119) it to its collection of algorithms. + +## Credits/Inspiration + +The synchronous counterpart in [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md). From c0fdfdb8aa2b263799ebafc6f11c62f01e838f71 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 4 Aug 2022 09:44:07 -0700 Subject: [PATCH 049/149] Add an introduction and formatting for the `combineLatest` algorithm into a proposal. (#177) * Add an introduction and formatting for the `combineLatest` algorithm into a proposal * Update the proposal # to 0006 --- Evolution/0006-combineLatest.md | 95 +++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Evolution/0006-combineLatest.md diff --git a/Evolution/0006-combineLatest.md b/Evolution/0006-combineLatest.md new file mode 100644 index 00000000..3f60fac9 --- /dev/null +++ b/Evolution/0006-combineLatest.md @@ -0,0 +1,95 @@ +# Combine Latest + +* Proposal: [SAA-0006](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0006-combineLatest.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** + + +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCombineLatest2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift)] + +* Decision Notes: +* Bugs: + +## Introduction + +Similar to the `zip` algorithm there is a need to combine the latest values from multiple input asynchronous sequences. Since `AsyncSequence` augments the concept of sequence with the characteristic of time it means that the composition of elements may not just be pairwise emissions but instead be temporal composition. This means that it is useful to emit a new tuple _when_ a value is produced. The `combineLatest` algorithm provides precicely that. + +## Detailed Design + +This algorithm 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 + +for try await (apple, nasdaq) in combineLatest(appleFeed, nasdaqFeed) { + print("AAPL: \(apple) NASDAQ: \(nasdaq)") +} +``` + +Given some sample inputs the following combined events can be expected. + +| Timestamp | appleFeed | nasdaqFeed | combined output | +| ----------- | --------- | ---------- | ----------------------------- | +| 11:40 AM | 173.91 | | | +| 12:25 AM | | 14236.78 | AAPL: 173.91 NASDAQ: 14236.78 | +| 12:40 AM | | 14218.34 | AAPL: 173.91 NASDAQ: 14218.34 | +| 1:15 PM | 173.00 | | AAPL: 173.00 NASDAQ: 14218.34 | + +This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. + +```swift +public func combineLatest(_ base1: Base1, _ base2: Base2) -> AsyncCombineLatest2Sequence + +public func combineLatest(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncCombineLatest3Sequence + +public struct AsyncCombineLatest2Sequence: Sendable + where + Base1: Sendable, Base2: Sendable, + Base1.Element: Sendable, Base2.Element: Sendable, + Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + public typealias Element = (Base1.Element, Base2.Element) + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +public struct AsyncCombineLatest3Sequence: Sendable + where + Base1: Sendable, Base2: Sendable, Base3: Sendable + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable + Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + public typealias Element = (Base1.Element, Base2.Element, Base3.Element) + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +``` + +The `combineLatest(_:...)` function takes two or more asynchronous sequences as arguments and produces an `AsyncCombineLatestSequence` which is an asynchronous sequence. + +Since the bases comprising the `AsyncCombineLatestSequence` must be iterated concurrently to produce the latest value, those sequences must be able to be sent to child tasks. This means that a prerequisite of the bases must be that the base asynchronous sequences, their iterators, and the elements they produce must all be `Sendable`. + +If any of the bases terminate before the first element is produced, then the `AsyncCombineLatestSequence` iteration can never be satisfied. So, if a base's iterator returns `nil` at the first iteration, then the `AsyncCombineLatestSequence` iterator immediately returns `nil` to signify a terminal state. In this particular case, any outstanding iteration of other bases will be cancelled. After the first element is produced ,this behavior is different since the latest values can still be satisfied by at least one base. This means that beyond the construction of the first tuple comprised of the returned elements of the bases, the terminal state of the `AsyncCombineLatestSequence` iteration will only be reached when all of the base iterations reach a terminal state. + +The throwing behavior of `AsyncCombineLatestSequence` is that if any of the bases throw, then the composed asynchronous sequence throws on its iteration. If at any point (within the first iteration or afterwards), an error is thrown by any base, the other iterations are cancelled and the thrown error is immediately thrown to the consuming iteration. + +### Naming + +Since the inherent behavior of `combineLatest(_:...)` combines the latest values from multiple streams into a tuple the naming is intended to be quite literal. There are precedent terms of art in other frameworks and libraries (listed in the comparison section). Other naming takes the form of "withLatestFrom". This was disregarded since the "with" prefix is often most associated with the passing of a closure and some sort of contextual concept; `withUnsafePointer` or `withUnsafeContinuation` are prime examples. + +### Comparison with other libraries + +Combine latest often appears in libraries developed for processing events over time since the event ordering of a concept of "latest" only occurs when asynchrony is involved. + +**ReactiveX** ReactiveX has an [API definition of CombineLatest](https://reactivex.io/documentation/operators/combinelatest.html) as a top level function for combining Observables. + +**Combine** Combine has an [API definition of combineLatest](https://developer.apple.com/documentation/combine/publisher/combinelatest(_:)/) has an operator style method for combining Publishers. From cba18c3f88874efa03135a72361605fd4dc2f0d2 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Tue, 30 Aug 2022 19:47:50 +0200 Subject: [PATCH 050/149] asyncLazySequence: harmonize unit tests nomenclature (#190) --- Tests/AsyncAlgorithmsTests/TestLazy.swift | 106 ++++++++++++---------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/TestLazy.swift b/Tests/AsyncAlgorithmsTests/TestLazy.swift index 3d7f58ca..a173d80f 100644 --- a/Tests/AsyncAlgorithmsTests/TestLazy.swift +++ b/Tests/AsyncAlgorithmsTests/TestLazy.swift @@ -13,97 +13,102 @@ import AsyncAlgorithms final class TestLazy: XCTestCase { - func test_array() async { - let source = [1, 2, 3, 4] - let expected = source - let sequence = source.async - - var actual = [Int]() + func test_lazy_outputs_elements_and_finishes_when_source_is_array() async { + let expected = [1, 2, 3, 4] + let sequence = expected.async + + var collected = [Int]() for await item in sequence { - actual.append(item) + collected.append(item) } - XCTAssertEqual(expected, actual) + XCTAssertEqual(expected, collected) } - func test_set() async { - let source: Set = [1, 2, 3, 4] - let expected = source - let sequence = source.async + func test_lazy_outputs_elements_and_finishes_when_source_is_set() async { + let expected: Set = [1, 2, 3, 4] + let sequence = expected.async - var actual = Set() + var collected = Set() for await item in sequence { - actual.insert(item) + collected.insert(item) } - XCTAssertEqual(expected, actual) + XCTAssertEqual(expected, collected) } - func test_empty() async { - let source = EmptyCollection() + func test_lazy_finishes_without_elements_when_source_is_empty() async { let expected = [Int]() - let sequence = source.async + let sequence = expected.async - var actual = [Int]() + var collected = [Int]() for await item in sequence { - actual.append(item) + collected.append(item) } - XCTAssertEqual(expected, actual) + XCTAssertEqual(expected, collected) } - func test_iteration() async { - let source = ReportingSequence([1, 2, 3]) + func test_lazy_triggers_expected_iterator_events_when_source_is_iterated() async { + let expected = [1, 2, 3] + let expectedEvents = [ + ReportingSequence.Event.makeIterator, + .next, + .next, + .next, + .next + ] + let source = ReportingSequence(expected) let sequence = source.async + XCTAssertEqual(source.events, []) + var collected = [Int]() for await item in sequence { collected.append(item) } - XCTAssertEqual(collected, [1, 2, 3]) - XCTAssertEqual(source.events, [ - .makeIterator, + + 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 = [ + ReportingSequence.Event.makeIterator, .next, .next, .next, .next - ]) - } - - func test_manual_iteration() async { - let source = ReportingSequence([1, 2, 3]) + ] + let source = ReportingSequence(expected) let sequence = source.async + XCTAssertEqual(source.events, []) + var collected = [Int]() var iterator = sequence.makeAsyncIterator() while let item = await iterator.next() { collected.append(item) } - XCTAssertEqual(collected, [1, 2, 3]) - XCTAssertEqual(source.events, [ - .makeIterator, - .next, - .next, - .next, - .next - ]) + + XCTAssertEqual(expected, collected) + XCTAssertEqual(expectedEvents, source.events) + let pastEnd = await iterator.next() + XCTAssertEqual(pastEnd, nil) // ensure that iterating past the end does not invoke next again - XCTAssertEqual(source.events, [ - .makeIterator, - .next, - .next, - .next, - .next - ]) + XCTAssertEqual(expectedEvents, source.events) } - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = source.async + func test_lazy_finishes_when_task_is_cancelled() async { let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") + + let source = Indefinite(value: "test") + let sequence = source.async + let task = Task { var firstIteration = false for await _ in sequence { @@ -114,11 +119,14 @@ final class TestLazy: XCTestCase { } finished.fulfill() } + // ensure the other task actually starts wait(for: [iterated], timeout: 1.0) + // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() + wait(for: [finished], timeout: 1.0) } } From 68c8dc285b7e85866adf907b0301f15dec73dd13 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Tue, 30 Aug 2022 19:48:20 +0200 Subject: [PATCH 051/149] asyncChainSequence: harmonize unit tests nomenclature (#191) --- Tests/AsyncAlgorithmsTests/TestChain.swift | 113 +++++++++++++-------- 1 file changed, 72 insertions(+), 41 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/TestChain.swift b/Tests/AsyncAlgorithmsTests/TestChain.swift index f7cd0fef..2ef9fc85 100644 --- a/Tests/AsyncAlgorithmsTests/TestChain.swift +++ b/Tests/AsyncAlgorithmsTests/TestChain.swift @@ -13,57 +13,68 @@ import AsyncAlgorithms final class TestChain2: XCTestCase { - func test_chain() async { - let chained = chain([1, 2, 3].async, [4, 5, 6].async) + func test_chain2_concatenates_elements_from_sequences_and_returns_nil_when_source_is_pastEnd() async { + let expected1 = [1, 2, 3] + let expected2 = [4, 5, 6] + let expected = expected1 + expected2 + let chained = chain(expected1.async, expected2.async) + var iterator = chained.makeAsyncIterator() - var actual = [Int]() + var collected = [Int]() while let item = await iterator.next() { - actual.append(item) + collected.append(item) } - XCTAssertEqual([1, 2, 3, 4, 5, 6], actual) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_throwing_first() async throws { + 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() - var actual = [Int]() + + var collected = [Int]() do { while let item = try await iterator.next() { - actual.append(item) + collected.append(item) } - XCTFail() + XCTFail("Chained sequence should throw when first sequence throws") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([1, 2], actual) + XCTAssertEqual([1, 2], collected) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_throwing_second() async throws { + 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() - var actual = [Int]() + + var collected = [Int]() do { while let item = try await iterator.next() { - actual.append(item) + collected.append(item) } - XCTFail() + XCTFail("Chained sequence should throw when second sequence throws") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([1, 2, 3, 4], actual) + XCTAssertEqual(collected, [1, 2, 3, 4]) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = chain(source.async, ["past indefinite"].async) + func test_chain2_finishes_when_task_is_cancelled() async { let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") + + let source = Indefinite(value: "test") + let sequence = chain(source.async, ["past indefinite"].async) + let task = Task { var firstIteration = false for await _ in sequence { @@ -74,84 +85,101 @@ final class TestChain2: XCTestCase { } finished.fulfill() } + // ensure the other task actually starts wait(for: [iterated], timeout: 1.0) + // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() + wait(for: [finished], timeout: 1.0) } } final class TestChain3: XCTestCase { - func test_chain() async { - let chained = chain([1, 2, 3].async, [4, 5, 6].async, [7, 8, 9].async) + func test_chain3_concatenates_elements_from_sequences_and_returns_nil_when_source_is_pastEnd() async { + let expected1 = [1, 2, 3] + let expected2 = [4, 5, 6] + let expected3 = [7, 8, 9] + let expected = expected1 + expected2 + expected3 + let chained = chain(expected1.async, expected2.async, expected3.async) var iterator = chained.makeAsyncIterator() - var actual = [Int]() + + var collected = [Int]() while let item = await iterator.next() { - actual.append(item) + collected.append(item) } - XCTAssertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], actual) + XCTAssertEqual(expected, collected) + let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - func test_throwing_first() async throws { + 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() - var actual = [Int]() + + var collected = [Int]() do { while let item = try await iterator.next() { - actual.append(item) + collected.append(item) } - XCTFail() + XCTFail("Chained sequence should throw when first sequence throws") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([1, 2], actual) + XCTAssertEqual(collected, [1, 2]) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_throwing_second() async throws { + 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() - var actual = [Int]() + + var collected = [Int]() do { while let item = try await iterator.next() { - actual.append(item) + collected.append(item) } - XCTFail() + XCTFail("Chained sequence should throw when second sequence throws") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([1, 2, 3, 4], actual) + XCTAssertEqual(collected, [1, 2, 3, 4]) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_throwing_third() async throws { + 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() - var actual = [Int]() + + var collected = [Int]() do { while let item = try await iterator.next() { - actual.append(item) + collected.append(item) } - XCTFail() + XCTFail("Chained sequence should throw when third sequence throws") } catch { XCTAssertEqual(Failure(), error as? Failure) } - XCTAssertEqual([1, 2, 3, 4, 5, 6, 7], actual) + XCTAssertEqual(collected, [1, 2, 3, 4, 5, 6, 7]) + let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = chain(source.async, ["past indefinite"].async, ["and even further"].async) + func test_chain3_finishes_when_task_is_cancelled() async { let finished = expectation(description: "finished") let iterated = expectation(description: "iterated") + + let source = Indefinite(value: "test") + let sequence = chain(source.async, ["past indefinite"].async, ["and even further"].async) + let task = Task { var firstIteration = false for await _ in sequence { @@ -162,11 +190,14 @@ final class TestChain3: XCTestCase { } finished.fulfill() } + // ensure the other task actually starts wait(for: [iterated], timeout: 1.0) + // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() + wait(for: [finished], timeout: 1.0) } } From 2b5fbf3c91294b5e741c628144a0615b021446b0 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Thu, 15 Sep 2022 00:24:29 +0200 Subject: [PATCH 052/149] [Channel] improve send cancellation (#184) * asyncChannel: introduce ChannelToken to model Pending and Awaiting * asyncChannel: harmonize send and next cancellation * asyncChannel: add .finished as an emission state * asyncThrowingChannel: harmonize send and next cancellation * channel: update documentation for cancellation --- Package.swift | 7 +- .../AsyncAlgorithms.docc/Guides/Channel.md | 2 +- Sources/AsyncAlgorithms/AsyncChannel.swift | 272 ++++++++++-------- .../AsyncThrowingChannel.swift | 230 ++++++++------- .../AsyncAlgorithms/UnsafeResumption.swift | 10 +- Tests/AsyncAlgorithmsTests/TestChannel.swift | 22 +- 6 files changed, 319 insertions(+), 224 deletions(-) diff --git a/Package.swift b/Package.swift index c43747c0..4f716a85 100644 --- a/Package.swift +++ b/Package.swift @@ -16,9 +16,12 @@ let package = Package( .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], - dependencies: [], + dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3"))], targets: [ - .target(name: "AsyncAlgorithms"), + .target( + name: "AsyncAlgorithms", + dependencies: [.product(name: "Collections", package: "swift-collections")] + ), .target( name: "AsyncSequenceValidation", dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md index 5121c769..19eed41c 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md @@ -51,7 +51,7 @@ public final class AsyncThrowingChannel: Asyn } ``` -Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` via the suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. This method suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` or `fail(_:)` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, or by throwing an error, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. +Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` via the suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. This method suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` or `fail(_:)` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, or by throwing an error, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. The calls to `send(:)` and `next()` will immediately resume when their supporting task is cancelled, other operations from other tasks will remain active. ```swift let channel = AsyncChannel() diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index facdaadf..d53a7bac 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +import OrderedCollections + /// A channel for sending elements from one task to another with back pressure. /// /// The `AsyncChannel` class is intended to be used as a communication type between tasks, @@ -34,14 +36,17 @@ public final class AsyncChannel: AsyncSequence, Sendable { guard active else { return nil } + let generation = channel.establish() - let value: Element? = await withTaskCancellationHandler { [channel] in - channel.cancel(generation) + let nextTokenStatus = ManagedCriticalState(.new) + + let value = await withTaskCancellationHandler { [channel] in + channel.cancelNext(nextTokenStatus, generation) } operation: { - await channel.next(generation) + await channel.next(nextTokenStatus, generation) } - - if let value = value { + + if let value { return value } else { active = false @@ -50,68 +55,49 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } - struct Awaiting: Hashable { + typealias Pending = ChannelToken?, Never>> + typealias Awaiting = ChannelToken> + + struct ChannelToken: Hashable { var generation: Int - var continuation: UnsafeContinuation? - let cancelled: Bool - - init(generation: Int, continuation: UnsafeContinuation) { + var continuation: Continuation? + + init(generation: Int, continuation: Continuation) { self.generation = generation self.continuation = continuation - cancelled = false } - + init(placeholder generation: Int) { self.generation = generation self.continuation = nil - cancelled = false } - - init(cancelled generation: Int) { - self.generation = generation - self.continuation = nil - cancelled = true - } - + func hash(into hasher: inout Hasher) { hasher.combine(generation) } - - static func == (_ lhs: Awaiting, _ rhs: Awaiting) -> Bool { + + static func == (_ lhs: ChannelToken, _ rhs: ChannelToken) -> Bool { return lhs.generation == rhs.generation } } + + enum ChannelTokenStatus: Equatable { + case new + case cancelled + } enum Emission { case idle - case pending([UnsafeContinuation?, Never>]) - case awaiting(Set) - - mutating func cancel(_ generation: Int) -> UnsafeContinuation? { - switch self { - case .awaiting(var awaiting): - let continuation = awaiting.remove(Awaiting(placeholder: generation))?.continuation - if awaiting.isEmpty { - self = .idle - } else { - self = .awaiting(awaiting) - } - return continuation - case .idle: - self = .awaiting([Awaiting(cancelled: generation)]) - return nil - default: - return nil - } - } + case pending(OrderedSet) + case awaiting(OrderedSet) + case finished } struct State { var emission: Emission = .idle var generation = 0 - var terminal = false } - + let state = ManagedCriticalState(State()) /// Create a new `AsyncChannel` given an element type. @@ -123,22 +109,44 @@ public final class AsyncChannel: AsyncSequence, Sendable { return state.generation } } - - func cancel(_ generation: Int) { - state.withCriticalRegion { state in - state.emission.cancel(generation) + + func cancelNext(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) { + state.withCriticalRegion { state -> UnsafeContinuation? in + let continuation: UnsafeContinuation? + + switch state.emission { + case .awaiting(var nexts): + continuation = nexts.remove(Awaiting(placeholder: generation))?.continuation + if nexts.isEmpty { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } + default: + continuation = nil + } + + nextTokenStatus.withCriticalRegion { status in + if status == .new { + status = .cancelled + } + } + + return continuation }?.resume(returning: nil) } - - func next(_ generation: Int) async -> Element? { - return await withUnsafeContinuation { continuation in + + func next(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) async -> Element? { + return await withUnsafeContinuation { (continuation: UnsafeContinuation) in var cancelled = false var terminal = false state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - if state.terminal { - terminal = true + + if nextTokenStatus.withCriticalRegion({ $0 }) == .cancelled { + cancelled = true return nil } + switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) @@ -150,94 +158,124 @@ public final class AsyncChannel: AsyncSequence, Sendable { } else { state.emission = .pending(sends) } - return UnsafeResumption(continuation: send, success: continuation) + return UnsafeResumption(continuation: send.continuation, success: continuation) case .awaiting(var nexts): - if nexts.update(with: Awaiting(generation: generation, continuation: continuation)) != nil { - nexts.remove(Awaiting(placeholder: generation)) - cancelled = true - } - if nexts.isEmpty { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } + nexts.updateOrAppend(Awaiting(generation: generation, continuation: continuation)) + state.emission = .awaiting(nexts) + return nil + case .finished: + terminal = true return nil } }?.resume() + if cancelled || terminal { continuation.resume(returning: nil) } } } - - func terminateAll() { - let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in - if state.terminal { - return ([], []) - } - state.terminal = true + + func cancelSend(_ sendTokenStatus: ManagedCriticalState, _ generation: Int) { + state.withCriticalRegion { state -> UnsafeContinuation?, Never>? in + let continuation: UnsafeContinuation?, Never>? + switch state.emission { - case .idle: - return ([], []) - case .pending(let nexts): - state.emission = .idle - return (nexts, []) - case .awaiting(let nexts): - state.emission = .idle - return ([], nexts) + case .pending(var sends): + let send = sends.remove(Pending(placeholder: generation)) + if sends.isEmpty { + state.emission = .idle + } else { + state.emission = .pending(sends) + } + continuation = send?.continuation + default: + continuation = nil } - } - for send in sends { - send.resume(returning: nil) - } - for next in nexts { - next.continuation?.resume(returning: nil) - } + + sendTokenStatus.withCriticalRegion { status in + if status == .new { + status = .cancelled + } + } + + return continuation + }?.resume(returning: nil) } - - func _send(_ element: Element) async { - await withTaskCancellationHandler { - terminateAll() - } operation: { - let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - if state.terminal { - return UnsafeResumption(continuation: continuation, success: nil) - } - switch state.emission { - case .idle: - state.emission = .pending([continuation]) - return nil - case .pending(var sends): - sends.append(continuation) - state.emission = .pending(sends) - return nil - case .awaiting(var nexts): - let next = nexts.removeFirst().continuation - if nexts.count == 0 { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } - return UnsafeResumption(continuation: continuation, success: next) + + func send(_ sendTokenStatus: ManagedCriticalState, _ generation: Int, _ element: Element) async { + let continuation = await withUnsafeContinuation { continuation in + state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + + if sendTokenStatus.withCriticalRegion({ $0 }) == .cancelled { + return UnsafeResumption(continuation: continuation, success: nil) + } + + switch state.emission { + case .idle: + state.emission = .pending([Pending(generation: generation, continuation: continuation)]) + return nil + case .pending(var sends): + sends.updateOrAppend(Pending(generation: generation, continuation: continuation)) + state.emission = .pending(sends) + return nil + case .awaiting(var nexts): + let next = nexts.removeFirst().continuation + if nexts.count == 0 { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) } - }?.resume() - } - continuation?.resume(returning: element) + return UnsafeResumption(continuation: continuation, success: next) + case .finished: + return UnsafeResumption(continuation: continuation, success: nil) + } + }?.resume() } + continuation?.resume(returning: element) } - + /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made /// or when a call to `finish()` is made from another Task. /// If the channel is already finished then this returns immediately + /// If the task is cancelled, this function will resume. Other sending operations from other tasks will remain active. public func send(_ element: Element) async { - await _send(element) + let generation = establish() + let sendTokenStatus = ManagedCriticalState(.new) + + await withTaskCancellationHandler { [weak self] in + self?.cancelSend(sendTokenStatus, generation) + } operation: { + await send(sendTokenStatus, generation, element) + } } /// Send a finish to all awaiting iterations. /// All subsequent calls to `next(_:)` will resume immediately. public func finish() { - terminateAll() + let (sends, nexts) = state.withCriticalRegion { state -> (OrderedSet, OrderedSet) in + let result: (OrderedSet, OrderedSet) + + switch state.emission { + case .idle: + result = ([], []) + case .pending(let nexts): + result = (nexts, []) + case .awaiting(let nexts): + result = ([], nexts) + case .finished: + result = ([], []) + } + + state.emission = .finished + + return result + } + for send in sends { + send.continuation?.resume(returning: nil) + } + for next in nexts { + next.continuation?.resume(returning: nil) + } } /// Create an `Iterator` for iteration of an `AsyncChannel` diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 5ce68961..473482ea 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +import OrderedCollections + /// An error-throwing channel for sending elements from on task to another with back pressure. /// /// The `AsyncThrowingChannel` class is intended to be used as a communication types between tasks, @@ -32,12 +34,15 @@ public final class AsyncThrowingChannel: Asyn guard active else { return nil } + let generation = channel.establish() + let nextTokenStatus = ManagedCriticalState(.new) + do { - let value: Element? = try await withTaskCancellationHandler { [channel] in - channel.cancel(generation) + let value = try await withTaskCancellationHandler { [channel] in + channel.cancelNext(nextTokenStatus, generation) } operation: { - try await channel.next(generation) + try await channel.next(nextTokenStatus, generation) } if let value = value { @@ -52,39 +57,39 @@ public final class AsyncThrowingChannel: Asyn } } } - - struct Awaiting: Hashable { + + typealias Pending = ChannelToken?, Never>> + typealias Awaiting = ChannelToken> + + struct ChannelToken: Hashable { var generation: Int - var continuation: UnsafeContinuation? - let cancelled: Bool - - init(generation: Int, continuation: UnsafeContinuation) { + var continuation: Continuation? + + init(generation: Int, continuation: Continuation) { self.generation = generation self.continuation = continuation - cancelled = false } - + init(placeholder generation: Int) { self.generation = generation self.continuation = nil - cancelled = false } - - init(cancelled generation: Int) { - self.generation = generation - self.continuation = nil - cancelled = true - } - + func hash(into hasher: inout Hasher) { hasher.combine(generation) } - - static func == (_ lhs: Awaiting, _ rhs: Awaiting) -> Bool { + + static func == (_ lhs: ChannelToken, _ rhs: ChannelToken) -> Bool { return lhs.generation == rhs.generation } } + + enum ChannelTokenStatus: Equatable { + case new + case cancelled + } + enum Termination { case finished case failed(Error) @@ -92,32 +97,9 @@ public final class AsyncThrowingChannel: Asyn enum Emission { case idle - case pending([UnsafeContinuation?, Never>]) - case awaiting(Set) + case pending(OrderedSet) + case awaiting(OrderedSet) case terminated(Termination) - - var isTerminated: Bool { - guard case .terminated = self else { return false } - return true - } - - mutating func cancel(_ generation: Int) -> UnsafeContinuation? { - switch self { - case .awaiting(var awaiting): - let continuation = awaiting.remove(Awaiting(placeholder: generation))?.continuation - if awaiting.isEmpty { - self = .idle - } else { - self = .awaiting(awaiting) - } - return continuation - case .idle: - self = .awaiting([Awaiting(cancelled: generation)]) - return nil - default: - return nil - } - } } struct State { @@ -135,19 +117,45 @@ public final class AsyncThrowingChannel: Asyn return state.generation } } - - func cancel(_ generation: Int) { - state.withCriticalRegion { state in - state.emission.cancel(generation) + + func cancelNext(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) { + state.withCriticalRegion { state -> UnsafeContinuation? in + let continuation: UnsafeContinuation? + + switch state.emission { + case .awaiting(var nexts): + continuation = nexts.remove(Awaiting(placeholder: generation))?.continuation + if nexts.isEmpty { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } + default: + continuation = nil + } + + nextTokenStatus.withCriticalRegion { status in + if status == .new { + status = .cancelled + } + } + + return continuation }?.resume(returning: nil) } - func next(_ generation: Int) async throws -> Element? { - return try await withUnsafeThrowingContinuation { continuation in + func next(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) async throws -> Element? { + return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in var cancelled = false var potentialTermination: Termination? state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + + if nextTokenStatus.withCriticalRegion({ $0 }) == .cancelled { + cancelled = true + return nil + } + switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) @@ -159,17 +167,10 @@ public final class AsyncThrowingChannel: Asyn } else { state.emission = .pending(sends) } - return UnsafeResumption(continuation: send, success: continuation) + return UnsafeResumption(continuation: send.continuation, success: continuation) case .awaiting(var nexts): - if nexts.update(with: Awaiting(generation: generation, continuation: continuation)) != nil { - nexts.remove(Awaiting(placeholder: generation)) - cancelled = true - } - if nexts.isEmpty { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } + nexts.updateOrAppend(Awaiting(generation: generation, continuation: continuation)) + state.emission = .awaiting(nexts) return nil case .terminated(let termination): potentialTermination = termination @@ -196,8 +197,67 @@ public final class AsyncThrowingChannel: Asyn } } + func cancelSend(_ sendTokenStatus: ManagedCriticalState, _ generation: Int) { + state.withCriticalRegion { state -> UnsafeContinuation?, Never>? in + let continuation: UnsafeContinuation?, Never>? + + switch state.emission { + case .pending(var sends): + let send = sends.remove(Pending(placeholder: generation)) + if sends.isEmpty { + state.emission = .idle + } else { + state.emission = .pending(sends) + } + continuation = send?.continuation + default: + continuation = nil + } + + sendTokenStatus.withCriticalRegion { status in + if status == .new { + status = .cancelled + } + } + + return continuation + }?.resume(returning: nil) + } + + func send(_ sendTokenStatus: ManagedCriticalState, _ generation: Int, _ element: Element) async { + let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in + state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + + if sendTokenStatus.withCriticalRegion({ $0 }) == .cancelled { + return UnsafeResumption(continuation: continuation, success: nil) + } + + switch state.emission { + case .idle: + state.emission = .pending([Pending(generation: generation, continuation: continuation)]) + return nil + case .pending(var sends): + sends.updateOrAppend(Pending(generation: generation, continuation: continuation)) + state.emission = .pending(sends) + return nil + case .awaiting(var nexts): + let next = nexts.removeFirst().continuation + if nexts.count == 0 { + state.emission = .idle + } else { + state.emission = .awaiting(nexts) + } + return UnsafeResumption(continuation: continuation, success: next) + case .terminated: + return UnsafeResumption(continuation: continuation, success: nil) + } + }?.resume() + } + continuation?.resume(returning: element) + } + func terminateAll(error: Failure? = nil) { - let (sends, nexts) = state.withCriticalRegion { state -> ([UnsafeContinuation?, Never>], Set) in + let (sends, nexts) = state.withCriticalRegion { state -> (OrderedSet, OrderedSet) in let nextState: Emission if let error = error { @@ -222,7 +282,7 @@ public final class AsyncThrowingChannel: Asyn } for send in sends { - send.resume(returning: nil) + send.continuation?.resume(returning: nil) } if let error = error { @@ -234,45 +294,21 @@ public final class AsyncThrowingChannel: Asyn next.continuation?.resume(returning: nil) } } - - } - - func _send(_ element: Element) async { - await withTaskCancellationHandler { - terminateAll() - } operation: { - let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in - switch state.emission { - case .idle: - state.emission = .pending([continuation]) - return nil - case .pending(var sends): - sends.append(continuation) - state.emission = .pending(sends) - return nil - case .awaiting(var nexts): - let next = nexts.removeFirst().continuation - if nexts.count == 0 { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } - return UnsafeResumption(continuation: continuation, success: next) - case .terminated: - return UnsafeResumption(continuation: continuation, success: nil) - } - }?.resume() - } - continuation?.resume(returning: element) - } } /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made /// or when a call to `finish()`/`fail(_:)` is made from another Task. /// If the channel is already finished then this returns immediately + /// If the task is cancelled, this function will resume. Other sending operations from other tasks will remain active. public func send(_ element: Element) async { - await _send(element) + let generation = establish() + let sendTokenStatus = ManagedCriticalState(.new) + + await withTaskCancellationHandler { [weak self] in + self?.cancelSend(sendTokenStatus, generation) + } operation: { + await send(sendTokenStatus, generation, element) + } } /// Send an error to all awaiting iterations. diff --git a/Sources/AsyncAlgorithms/UnsafeResumption.swift b/Sources/AsyncAlgorithms/UnsafeResumption.swift index 9eb28c5f..d87987ac 100644 --- a/Sources/AsyncAlgorithms/UnsafeResumption.swift +++ b/Sources/AsyncAlgorithms/UnsafeResumption.swift @@ -1,22 +1,22 @@ struct UnsafeResumption { - let continuation: UnsafeContinuation + let continuation: UnsafeContinuation? let result: Result - init(continuation: UnsafeContinuation, result: Result) { + init(continuation: UnsafeContinuation?, result: Result) { self.continuation = continuation self.result = result } - init(continuation: UnsafeContinuation, success: Success) { + init(continuation: UnsafeContinuation?, success: Success) { self.init(continuation: continuation, result: .success(success)) } - init(continuation: UnsafeContinuation, failure: Failure) { + init(continuation: UnsafeContinuation?, failure: Failure) { self.init(continuation: continuation, result: .failure(failure)) } func resume() { - continuation.resume(with: result) + continuation?.resume(with: result) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index 66fd1e1d..2d8797fa 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -227,7 +227,7 @@ final class TestChannel: XCTestCase { XCTAssertNil(value) } - func test_asyncChannel_resumes_send_when_task_is_cancelled() async { + func test_asyncChannel_resumes_send_when_task_is_cancelled_and_continue_remaining_send_tasks() async { let channel = AsyncChannel() let notYetDone = expectation(description: "not yet done") notYetDone.isInverted = true @@ -237,12 +237,21 @@ final class TestChannel: XCTestCase { notYetDone.fulfill() done.fulfill() } + + Task { + await channel.send(2) + } + wait(for: [notYetDone], timeout: 0.1) task.cancel() wait(for: [done], timeout: 1.0) + + var iterator = channel.makeAsyncIterator() + let received = await iterator.next() + XCTAssertEqual(received, 2) } - func test_asyncThrowingChannel_resumes_send_when_task_is_cancelled() async { + func test_asyncThrowingChannel_resumes_send_when_task_is_cancelled_and_continue_remaining_send_tasks() async throws { let channel = AsyncThrowingChannel() let notYetDone = expectation(description: "not yet done") notYetDone.isInverted = true @@ -252,8 +261,17 @@ final class TestChannel: XCTestCase { notYetDone.fulfill() done.fulfill() } + + Task { + await channel.send(2) + } + wait(for: [notYetDone], timeout: 0.1) task.cancel() wait(for: [done], timeout: 1.0) + + var iterator = channel.makeAsyncIterator() + let received = try await iterator.next() + XCTAssertEqual(received, 2) } } From f05e450f0b909c0e80670a47516c4b9700b9e5da Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 30 Sep 2022 19:12:11 +0200 Subject: [PATCH 053/149] Remove `AsyncIterator: Sendable` requirement from debounce (#198) * Remove `AsyncIterator: Sendable` requirement from debounce # Motivation The current implementation of `AsyncDebounceSequence` requires the base `AsyncIterator` to be `Sendable`. This is causing two problems: 1. It only allows users to use `debounce` if their `AsyncSequence.AsyncIterator` is `Sendable` 2. In `debounce` we are creating a lot of new `Task`s and reating `Task`s is not cheap. My main goal of this PR was to remove the `Sendable` constraint from `debounce`. # Modification This PR overhauls the implementation of `debounce` and aligns it with the implementation of the open `merge` PR https://github.com/apple/swift-async-algorithms/pull/185 . The most important changes are this: - I removed the `Sendable` requirement from the base sequences `AsyncIterator`. - Instead of creating new Tasks for the sleep and for the upstream consumption. I am now creating one Task and manipulate it by signalling continuations - I am not cancelling the sleep. Instead I am recalculating the time left to sleep when a sleep finishes. # Result In the end, this PR swaps the implementation of `AsyncDebounceSequence` and drops the `Sendable` constraint and passes all tests. Furthermore, on my local performance testing I saw up 150% speed increase in throughput. * Fix https://github.com/apple/swift-async-algorithms/issues/174 * Code review * Remove lock methods * Cleanup some unused code * Setup task after first call to next --- .../AsyncDebounceSequence.swift | 121 --- .../Debounce/AsyncDebounceSequence.swift | 99 +++ .../Debounce/DebounceStateMachine.swift | 707 ++++++++++++++++++ .../Debounce/DebounceStorage.swift | 296 ++++++++ .../Performance/TestThroughput.swift | 6 + Tests/AsyncAlgorithmsTests/TestDebounce.swift | 14 +- 6 files changed, 1120 insertions(+), 123 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncDebounceSequence.swift create mode 100644 Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift create mode 100644 Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift diff --git a/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift deleted file mode 100644 index 304dc01d..00000000 --- a/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift +++ /dev/null @@ -1,121 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -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 { - 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 { - 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.AsyncIterator: Sendable, Base.Element: Sendable, Base: Sendable { - let base: Base - let interval: C.Instant.Duration - let tolerance: C.Instant.Duration? - let clock: C - - 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 - - /// The iterator for a `AsyncDebounceSequence` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - enum Partial: Sendable { - case sleep - case produce(Result, Base.AsyncIterator) - } - var iterator: Base.AsyncIterator - var produce: Task? - var terminal = false - let interval: C.Instant.Duration - let tolerance: C.Instant.Duration? - let clock: C - - init(_ base: Base.AsyncIterator, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { - self.iterator = base - self.interval = interval - self.tolerance = tolerance - self.clock = clock - } - - public mutating func next() async rethrows -> Base.Element? { - var last: C.Instant? - var lastResult: Result? - while !terminal { - let deadline = (last ?? clock.now).advanced(by: interval) - let sleep: Task = Task { [tolerance, clock] in - try? await clock.sleep(until: deadline, tolerance: tolerance) - return .sleep - } - let produce: Task = self.produce ?? Task { [iterator] in - var iter = iterator - do { - let value = try await iter.next() - return .produce(.success(value), iter) - } catch { - return .produce(.failure(error), iter) - } - } - self.produce = nil - switch await Task.select(sleep, produce).value { - case .sleep: - self.produce = produce - if let result = lastResult { - return try result._rethrowGet() - } - break - case .produce(let result, let iter): - lastResult = result - last = clock.now - sleep.cancel() - self.iterator = iter - switch result { - case .success(let value): - if value == nil { - terminal = true - return nil - } - case .failure: - terminal = true - try result._rethrowError() - } - break - } - } - return nil - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(base.makeAsyncIterator(), interval: interval, tolerance: tolerance, clock: clock) - } -} diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift new file mode 100644 index 00000000..5ae17f14 --- /dev/null +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + /// 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 { + 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.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: Sendable { + 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. + public 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 func makeAsyncIterator() -> AsyncIterator { + let storage = DebounceStorage( + base: self.base, + interval: self.interval, + tolerance: self.tolerance, + clock: self.clock + ) + return AsyncIterator(storage: storage) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension AsyncDebounceSequence { + public struct AsyncIterator: 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 + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + try await self.storage.next() + } + } + + let internalClass: InternalClass + + fileprivate init(storage: DebounceStorage) { + self.internalClass = InternalClass(storage: storage) + } + + public mutating func next() async rethrows -> Element? { + try await self.internalClass.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift new file mode 100644 index 00000000..cd6cc1a8 --- /dev/null +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -0,0 +1,707 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct DebounceStateMachine { + typealias Element = Base.Element + + private enum State { + /// 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 + } + } + + 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 `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() + ) + } + } + + /// 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 + } + } + + /// 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 `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: + // The upstream already failed so it should never have throw 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 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: + // Since we are transitioning to `merging` before we return from `makeAsyncIterator` + // this can never happen + preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + + 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) + } + } +} diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift new file mode 100644 index 00000000..21e1ddf6 --- /dev/null +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -0,0 +1,296 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class DebounceStorage: @unchecked Sendable where Base: 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 + } + + 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 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 + self.stateMachine.withCriticalRegion { + let action = $0.next(for: continuation) + + switch action { + case .startTask(let base): + self.startTask( + stateMachine: &$0, + base: base, + downstreamContinuation: continuation + ) + + case .resumeUpstreamContinuation(let upstreamContinuation): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand. + upstreamContinuation?.resume(returning: ()) + + case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand and start the clock task. + upstreamContinuation?.resume(returning: ()) + clockContinuation?.resume(returning: deadline) + + case .resumeDownstreamContinuationWithNil(let continuation): + continuation.resume(returning: .success(nil)) + + case .resumeDownstreamContinuationWithError(let continuation, let error): + continuation.resume(returning: .failure(error)) + } + } + } + + return try result._rethrowGet() + } onCancel: { + let action = self.stateMachine.withCriticalRegion { $0.cancelled() } + + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamAndClockContinuation( + 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 + } + } + } + + 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) } + + 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 + } + } + } + + do { + try await group.waitForAll() + } catch { + // The upstream sequence threw an error + let action = self.stateMachine.withCriticalRegion { $0.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/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index 2d490cc9..f1868b5b 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -64,5 +64,11 @@ final class TestThroughput: XCTestCase { zip($0, $1, $2) } } + @available(macOS 13.0, *) + func test_debounce() async { + await measureSequenceThroughput(source: (1...).async) { + $0.debounce(for: .zero, clock: ContinuousClock()) + } + } } #endif diff --git a/Tests/AsyncAlgorithmsTests/TestDebounce.swift b/Tests/AsyncAlgorithmsTests/TestDebounce.swift index 2005c134..d82105bc 100644 --- a/Tests/AsyncAlgorithmsTests/TestDebounce.swift +++ b/Tests/AsyncAlgorithmsTests/TestDebounce.swift @@ -27,7 +27,7 @@ final class TestDebounce: XCTestCase { validate { "abcd----e---f-g-|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) - "------d----e----|" + "------d----e----[g|]" } } @@ -37,7 +37,7 @@ final class TestDebounce: XCTestCase { validate { "a|" $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) - "-|" + "-[a|]" } } @@ -58,4 +58,14 @@ 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") } + + 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 {} + } } From af9ce91db44aa0932e96f343b04470cbf4421ef2 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 4 Oct 2022 14:28:14 -0700 Subject: [PATCH 054/149] Simplify resumptions to avoid needing a resumption type and mark continuations as resumed directly in critical regions (#203) --- Sources/AsyncAlgorithms/AsyncChannel.swift | 70 ++++++++---------- .../AsyncThrowingChannel.swift | 71 ++++++++----------- .../AsyncAlgorithms/UnsafeResumption.swift | 37 ---------- 3 files changed, 61 insertions(+), 117 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/UnsafeResumption.swift diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index d53a7bac..74f1bbee 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -111,7 +111,7 @@ public final class AsyncChannel: AsyncSequence, Sendable { } func cancelNext(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state -> UnsafeContinuation? in + state.withCriticalRegion { state in let continuation: UnsafeContinuation? switch state.emission { @@ -132,25 +132,23 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } - return continuation - }?.resume(returning: nil) + continuation?.resume(returning: nil) + } } func next(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) async -> Element? { return await withUnsafeContinuation { (continuation: UnsafeContinuation) in var cancelled = false var terminal = false - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + state.withCriticalRegion { state in if nextTokenStatus.withCriticalRegion({ $0 }) == .cancelled { cancelled = true - return nil } switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) - return nil case .pending(var sends): let send = sends.removeFirst() if sends.count == 0 { @@ -158,16 +156,14 @@ public final class AsyncChannel: AsyncSequence, Sendable { } else { state.emission = .pending(sends) } - return UnsafeResumption(continuation: send.continuation, success: continuation) + send.continuation?.resume(returning: continuation) case .awaiting(var nexts): nexts.updateOrAppend(Awaiting(generation: generation, continuation: continuation)) state.emission = .awaiting(nexts) - return nil case .finished: terminal = true - return nil } - }?.resume() + } if cancelled || terminal { continuation.resume(returning: nil) @@ -176,7 +172,7 @@ public final class AsyncChannel: AsyncSequence, Sendable { } func cancelSend(_ sendTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state -> UnsafeContinuation?, Never>? in + state.withCriticalRegion { state in let continuation: UnsafeContinuation?, Never>? switch state.emission { @@ -198,26 +194,25 @@ public final class AsyncChannel: AsyncSequence, Sendable { } } - return continuation - }?.resume(returning: nil) + continuation?.resume(returning: nil) + } } func send(_ sendTokenStatus: ManagedCriticalState, _ generation: Int, _ element: Element) async { - let continuation = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in + state.withCriticalRegion { state in if sendTokenStatus.withCriticalRegion({ $0 }) == .cancelled { - return UnsafeResumption(continuation: continuation, success: nil) + continuation.resume(returning: nil) + return } switch state.emission { case .idle: state.emission = .pending([Pending(generation: generation, continuation: continuation)]) - return nil case .pending(var sends): sends.updateOrAppend(Pending(generation: generation, continuation: continuation)) state.emission = .pending(sends) - return nil case .awaiting(var nexts): let next = nexts.removeFirst().continuation if nexts.count == 0 { @@ -225,11 +220,11 @@ public final class AsyncChannel: AsyncSequence, Sendable { } else { state.emission = .awaiting(nexts) } - return UnsafeResumption(continuation: continuation, success: next) + continuation.resume(returning: next) case .finished: - return UnsafeResumption(continuation: continuation, success: nil) + continuation.resume(returning: nil) } - }?.resume() + } } continuation?.resume(returning: element) } @@ -252,30 +247,25 @@ public final class AsyncChannel: AsyncSequence, Sendable { /// Send a finish to all awaiting iterations. /// All subsequent calls to `next(_:)` will resume immediately. public func finish() { - let (sends, nexts) = state.withCriticalRegion { state -> (OrderedSet, OrderedSet) in - let result: (OrderedSet, OrderedSet) + state.withCriticalRegion { state in + defer { state.emission = .finished } + switch state.emission { - case .idle: - result = ([], []) - case .pending(let nexts): - result = (nexts, []) + case .pending(let sends): + for send in sends { + send.continuation?.resume(returning: nil) + } case .awaiting(let nexts): - result = ([], nexts) - case .finished: - result = ([], []) + for next in nexts { + next.continuation?.resume(returning: nil) + } + default: + break } - - state.emission = .finished - - return result - } - for send in sends { - send.continuation?.resume(returning: nil) - } - for next in nexts { - next.continuation?.resume(returning: nil) } + + } /// Create an `Iterator` for iteration of an `AsyncChannel` diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 473482ea..cfc44a8b 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -119,7 +119,7 @@ public final class AsyncThrowingChannel: Asyn } func cancelNext(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state -> UnsafeContinuation? in + state.withCriticalRegion { state in let continuation: UnsafeContinuation? switch state.emission { @@ -140,8 +140,8 @@ public final class AsyncThrowingChannel: Asyn } } - return continuation - }?.resume(returning: nil) + continuation?.resume(returning: nil) + } } func next(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) async throws -> Element? { @@ -149,17 +149,16 @@ public final class AsyncThrowingChannel: Asyn var cancelled = false var potentialTermination: Termination? - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + state.withCriticalRegion { state in if nextTokenStatus.withCriticalRegion({ $0 }) == .cancelled { cancelled = true - return nil + return } switch state.emission { case .idle: state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) - return nil case .pending(var sends): let send = sends.removeFirst() if sends.count == 0 { @@ -167,17 +166,15 @@ public final class AsyncThrowingChannel: Asyn } else { state.emission = .pending(sends) } - return UnsafeResumption(continuation: send.continuation, success: continuation) + send.continuation?.resume(returning: continuation) case .awaiting(var nexts): nexts.updateOrAppend(Awaiting(generation: generation, continuation: continuation)) state.emission = .awaiting(nexts) - return nil case .terminated(let termination): potentialTermination = termination state.emission = .terminated(.finished) - return nil } - }?.resume() + } if cancelled { continuation.resume(returning: nil) @@ -198,7 +195,7 @@ public final class AsyncThrowingChannel: Asyn } func cancelSend(_ sendTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state -> UnsafeContinuation?, Never>? in + state.withCriticalRegion { state in let continuation: UnsafeContinuation?, Never>? switch state.emission { @@ -220,26 +217,25 @@ public final class AsyncThrowingChannel: Asyn } } - return continuation - }?.resume(returning: nil) + continuation?.resume(returning: nil) + } } func send(_ sendTokenStatus: ManagedCriticalState, _ generation: Int, _ element: Element) async { let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state -> UnsafeResumption?, Never>? in + state.withCriticalRegion { state in if sendTokenStatus.withCriticalRegion({ $0 }) == .cancelled { - return UnsafeResumption(continuation: continuation, success: nil) + continuation.resume(returning: nil) + return } switch state.emission { case .idle: state.emission = .pending([Pending(generation: generation, continuation: continuation)]) - return nil case .pending(var sends): sends.updateOrAppend(Pending(generation: generation, continuation: continuation)) state.emission = .pending(sends) - return nil case .awaiting(var nexts): let next = nexts.removeFirst().continuation if nexts.count == 0 { @@ -247,17 +243,17 @@ public final class AsyncThrowingChannel: Asyn } else { state.emission = .awaiting(nexts) } - return UnsafeResumption(continuation: continuation, success: next) + continuation.resume(returning: next) case .terminated: - return UnsafeResumption(continuation: continuation, success: nil) + continuation.resume(returning: nil) } - }?.resume() + } } continuation?.resume(returning: element) } func terminateAll(error: Failure? = nil) { - let (sends, nexts) = state.withCriticalRegion { state -> (OrderedSet, OrderedSet) in + state.withCriticalRegion { state in let nextState: Emission if let error = error { @@ -269,29 +265,24 @@ public final class AsyncThrowingChannel: Asyn switch state.emission { case .idle: state.emission = nextState - return ([], []) - case .pending(let nexts): + case .pending(let sends): state.emission = nextState - return (nexts, []) + for send in sends { + send.continuation?.resume(returning: nil) + } case .awaiting(let nexts): state.emission = nextState - return ([], nexts) + if let error = error { + for next in nexts { + next.continuation?.resume(throwing: error) + } + } else { + for next in nexts { + next.continuation?.resume(returning: nil) + } + } case .terminated: - return ([], []) - } - } - - for send in sends { - send.continuation?.resume(returning: nil) - } - - if let error = error { - for next in nexts { - next.continuation?.resume(throwing: error) - } - } else { - for next in nexts { - next.continuation?.resume(returning: nil) + break } } } diff --git a/Sources/AsyncAlgorithms/UnsafeResumption.swift b/Sources/AsyncAlgorithms/UnsafeResumption.swift deleted file mode 100644 index d87987ac..00000000 --- a/Sources/AsyncAlgorithms/UnsafeResumption.swift +++ /dev/null @@ -1,37 +0,0 @@ -struct UnsafeResumption { - let continuation: UnsafeContinuation? - let result: Result - - init(continuation: UnsafeContinuation?, result: Result) { - self.continuation = continuation - self.result = result - } - - init(continuation: UnsafeContinuation?, success: Success) { - self.init(continuation: continuation, result: .success(success)) - } - - init(continuation: UnsafeContinuation?, failure: Failure) { - self.init(continuation: continuation, result: .failure(failure)) - } - - func resume() { - continuation?.resume(with: result) - } -} - -extension UnsafeResumption where Failure == Error { - init(continuation: UnsafeContinuation, catching body: () throws -> Success) { - self.init(continuation: continuation, result: Result(catching: body)) - } -} - -extension UnsafeResumption where Success == Void { - init(continuation: UnsafeContinuation) { - self.init(continuation: continuation, result: .success(())) - } -} - -extension UnsafeResumption: Sendable where Success: Sendable { } - - From 4feeb83d135f135566fdb307256cf777c0b94c6b Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Wed, 5 Oct 2022 12:39:14 -0400 Subject: [PATCH 055/149] make initializers '@usableFromInline' instead of '@inlinable' to support building for library evolution (#205) --- Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift | 2 +- .../AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift | 3 +-- Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift | 4 ++-- Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift | 4 ++-- Sources/AsyncAlgorithms/AsyncJoinedSequence.swift | 2 +- Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift | 4 ++-- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index 98154625..26faafe5 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -104,7 +104,7 @@ public struct AsyncChunkedByGroupSequence Bool - @inlinable + @usableFromInline init(_ base: Base, grouping: @escaping @Sendable (Base.Element, Base.Element) -> Bool) { self.base = base self.grouping = grouping diff --git a/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift index ccd15187..2cd30422 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift @@ -84,7 +84,7 @@ public struct AsyncChunkedOnProjectionSequence Subject - @inlinable + @usableFromInline init(_ base: Base, projection: @escaping @Sendable (Base.Element) -> Subject) { self.base = base self.projection = projection @@ -98,4 +98,3 @@ public struct AsyncChunkedOnProjectionSequence 0) self.base = base diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift index 9ac7f97d..d50d721e 100644 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift @@ -33,7 +33,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal let separator: Base.Element - @inlinable + @usableFromInline internal init(_ base: Base, separator: Base.Element) { self.base = base self.separator = separator @@ -61,7 +61,7 @@ extension AsyncInterspersedSequence: AsyncSequence { @usableFromInline internal var state = State.start - @inlinable + @usableFromInline internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { self.iterator = iterator self.separator = separator diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index 22395dac..954f9e26 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -74,7 +74,7 @@ public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base @usableFromInline let base: Base - @inlinable + @usableFromInline init(_ base: Base) { self.base = base } diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index 90ac51f7..60f3aa42 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -46,7 +46,7 @@ public struct AsyncRemoveDuplicatesSequence: AsyncSequence @usableFromInline var last: Element? - @inlinable + @usableFromInline init(iterator: Base.AsyncIterator, predicate: @escaping @Sendable (Element, Element) async -> Bool) { self.iterator = iterator self.predicate = predicate @@ -105,7 +105,7 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS @usableFromInline var last: Element? - @inlinable + @usableFromInline init(iterator: Base.AsyncIterator, predicate: @escaping @Sendable (Element, Element) async throws -> Bool) { self.iterator = iterator self.predicate = predicate From 314b10c6348be6614401e212444e78674567a481 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 10 Oct 2022 16:34:50 +0100 Subject: [PATCH 056/149] Remove `AsyncIterator: Sendable` requirement from merge (#185) * Remove `AsyncIterator: Sendable` requirement from merge # Motivation Currently a lot of the operator implementations in here that consume other `AsyncSequence`s require the `AsyncIterator` to be `Sendable`. This is mostly due to the fact that we are calling `makeAsyncIterator` on the upstream `AsyncSequence` and then pass that iterator around to various newly spawned `Task`s. This has two downsides: 1. It only allows users to use operators like `merge` if their `AsyncSequence.AsyncIterator` is `Sendable` 2. In merge we are creating new `Task`s for every new demand. Creating `Task`s is not cheap. My main goal of this PR was to remove the `Sendable` constraint from `merge`. # Modification This PR overhauls the complete inner workings of the `AsyncMerge2Sequence`. It does a couple of things: 1. The main change is that instead of creating new `Task`s for every demand, we are creating one `Task` when the `AsyncIterator` is created. This task has as child task for every upstream sequence. 2. When calling `next` we are signalling the child tasks to demand from the upstream 3. A new state machine that is synchronizing the various concurrent operations that can happen 4. Handling cancellation since we are creating a bunch of continuations. # Result In the end, this PR swaps the implementation of `AsyncMerge2Sequence` and drops the `Sendable` constraint and passes all tests. Furthermore, on my local performance testing I saw up 50% speed increase in throughput. # Open points 1. I need to make this sequence re-throwing but before going down that rabbit whole I wanna get buy-in on the implementation. 2. We should discuss and document if `merge` and other operators are hot or cold, i.e. if they only request if they got downstream demand 3. I need to switch `AsyncMerge3Sequence` over to the same iplementation * Split logic into multiple files, adapt merge3 and incorporate PR feedback * Add more tests and fix upstream throw behaviour * Remove internal class from the sequence * Setup task after first next * Remove unnecessary tests --- .../AsyncAlgorithms/AsyncMerge3Sequence.swift | 284 -------- Sources/AsyncAlgorithms/Locking.swift | 21 + .../Merge/AsyncMerge2Sequence.swift | 94 +++ .../Merge/AsyncMerge3Sequence.swift | 105 +++ .../Merge/MergeStateMachine.swift | 627 ++++++++++++++++++ .../AsyncAlgorithms/Merge/MergeStorage.swift | 449 +++++++++++++ ...equence.swift => Merge2StateMachine.swift} | 47 -- .../Support/Asserts.swift | 15 + Tests/AsyncAlgorithmsTests/TestMerge.swift | 40 ++ 9 files changed, 1351 insertions(+), 331 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift create mode 100644 Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift create mode 100644 Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift create mode 100644 Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Merge/MergeStorage.swift rename Sources/AsyncAlgorithms/{AsyncMerge2Sequence.swift => Merge2StateMachine.swift} (74%) diff --git a/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift deleted file mode 100644 index efbbf9a8..00000000 --- a/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift +++ /dev/null @@ -1,284 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// Creates an asynchronous sequence of elements from three underlying asynchronous sequences -public func merge(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncMerge3Sequence -where - Base1.Element == Base2.Element, - Base2.Element == Base3.Element, - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { - return AsyncMerge3Sequence(base1, base2, base3) -} - -/// An asynchronous sequence of elements from three underlying asynchronous sequences -/// -/// In a `AsyncMerge3Sequence` instance, the *i*th element is the *i*th element -/// resolved in sequential order out of the two underlying asynchronous sequences. -/// Use the `merge(_:_:_:)` function to create an `AsyncMerge3Sequence`. -public struct AsyncMerge3Sequence: AsyncSequence, Sendable -where - Base1.Element == Base2.Element, - Base2.Element == Base3.Element, - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { - public typealias Element = Base1.Element - /// An iterator for `AsyncMerge3Sequence` - public struct Iterator: AsyncIteratorProtocol, Sendable { - enum Partial: @unchecked Sendable { - case first(Result, Base1.AsyncIterator) - case second(Result, Base2.AsyncIterator) - case third(Result, Base3.AsyncIterator) - } - - var state: (PartialIteration, PartialIteration, PartialIteration) - - init(_ iterator1: Base1.AsyncIterator, _ iterator2: Base2.AsyncIterator, _ iterator3: Base3.AsyncIterator) { - state = (.idle(iterator1), .idle(iterator2), .idle(iterator3)) - } - - mutating func apply(_ task1: Task?, _ task2: Task?, _ task3: Task?) async rethrows -> Element? { - switch await Task.select([task1, task2, task3].compactMap { $0 }).value { - case .first(let result, let iterator): - do { - guard let value = try state.0.resolve(result, iterator) else { - return try await next() - } - return value - } catch { - state.1.cancel() - state.2.cancel() - throw error - } - case .second(let result, let iterator): - do { - guard let value = try state.1.resolve(result, iterator) else { - return try await next() - } - return value - } catch { - state.0.cancel() - state.2.cancel() - throw error - } - case .third(let result, let iterator): - do { - guard let value = try state.2.resolve(result, iterator) else { - return try await next() - } - return value - } catch { - state.0.cancel() - state.1.cancel() - throw error - } - } - } - - func first(_ iterator1: Base1.AsyncIterator) -> Task { - Task { - var iter = iterator1 - do { - let value = try await iter.next() - return .first(.success(value), iter) - } catch { - return .first(.failure(error), iter) - } - } - } - - func second(_ iterator2: Base2.AsyncIterator) -> Task { - Task { - var iter = iterator2 - do { - let value = try await iter.next() - return .second(.success(value), iter) - } catch { - return .second(.failure(error), iter) - } - } - } - - func third(_ iterator3: Base3.AsyncIterator) -> Task { - Task { - var iter = iterator3 - do { - let value = try await iter.next() - return .third(.success(value), iter) - } catch { - return .third(.failure(error), iter) - } - } - } - - public mutating func next() async rethrows -> Element? { - // state must have either all terminal or at least 1 idle iterator - // state may not have a saturation of pending tasks - switch state { - // three idle - case (.idle(let iterator1), .idle(let iterator2), .idle(let iterator3)): - let task1 = first(iterator1) - let task2 = second(iterator2) - let task3 = third(iterator3) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - // two idle - case (.idle(let iterator1), .idle(let iterator2), .pending(let task3)): - let task1 = first(iterator1) - let task2 = second(iterator2) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - case (.idle(let iterator1), .pending(let task2), .idle(let iterator3)): - let task1 = first(iterator1) - let task3 = third(iterator3) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - case (.pending(let task1), .idle(let iterator2), .idle(let iterator3)): - let task2 = second(iterator2) - let task3 = third(iterator3) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - - // 1 idle - case (.idle(let iterator1), .pending(let task2), .pending(let task3)): - let task1 = first(iterator1) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - case (.pending(let task1), .idle(let iterator2), .pending(let task3)): - let task2 = second(iterator2) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - case (.pending(let task1), .pending(let task2), .idle(let iterator3)): - let task3 = third(iterator3) - state = (.pending(task1), .pending(task2), .pending(task3)) - return try await apply(task1, task2, task3) - - // terminal degradations - // 1 terminal - case (.terminal, .idle(let iterator2), .idle(let iterator3)): - let task2 = second(iterator2) - let task3 = third(iterator3) - state = (.terminal, .pending(task2), .pending(task3)) - return try await apply(nil, task2, task3) - case (.terminal, .idle(let iterator2), .pending(let task3)): - let task2 = second(iterator2) - state = (.terminal, .pending(task2), .pending(task3)) - return try await apply(nil, task2, task3) - case (.terminal, .pending(let task2), .idle(let iterator3)): - let task3 = third(iterator3) - state = (.terminal, .pending(task2), .pending(task3)) - return try await apply(nil, task2, task3) - case (.idle(let iterator1), .terminal, .idle(let iterator3)): - let task1 = first(iterator1) - let task3 = third(iterator3) - state = (.pending(task1), .terminal, .pending(task3)) - return try await apply(task1, nil, task3) - case (.idle(let iterator1), .terminal, .pending(let task3)): - let task1 = first(iterator1) - state = (.pending(task1), .terminal, .pending(task3)) - return try await apply(task1, nil, task3) - case (.pending(let task1), .terminal, .idle(let iterator3)): - let task3 = third(iterator3) - state = (.pending(task1), .terminal, .pending(task3)) - return try await apply(task1, nil, task3) - case (.idle(let iterator1), .idle(let iterator2), .terminal): - let task1 = first(iterator1) - let task2 = second(iterator2) - state = (.pending(task1), .pending(task2), .terminal) - return try await apply(task1, task2, nil) - case (.idle(let iterator1), .pending(let task2), .terminal): - let task1 = first(iterator1) - state = (.pending(task1), .pending(task2), .terminal) - return try await apply(task1, task2, nil) - case (.pending(let task1), .idle(let iterator2), .terminal): - let task2 = second(iterator2) - state = (.pending(task1), .pending(task2), .terminal) - return try await apply(task1, task2, nil) - - // 2 terminal - // these can be permuted in place since they don't need to run two or more tasks at once - case (.terminal, .terminal, .idle(var iterator3)): - do { - if let value = try await iterator3.next() { - state = (.terminal, .terminal, .idle(iterator3)) - return value - } else { - state = (.terminal, .terminal, .terminal) - return nil - } - } catch { - state = (.terminal, .terminal, .terminal) - throw error - } - case (.terminal, .idle(var iterator2), .terminal): - do { - if let value = try await iterator2.next() { - state = (.terminal, .idle(iterator2), .terminal) - return value - } else { - state = (.terminal, .terminal, .terminal) - return nil - } - } catch { - state = (.terminal, .terminal, .terminal) - throw error - } - case (.idle(var iterator1), .terminal, .terminal): - do { - if let value = try await iterator1.next() { - state = (.idle(iterator1), .terminal, .terminal) - return value - } else { - state = (.terminal, .terminal, .terminal) - return nil - } - } catch { - state = (.terminal, .terminal, .terminal) - throw error - } - // 3 terminal - case (.terminal, .terminal, .terminal): - return nil - // partials - case (.pending(let task1), .pending(let task2), .pending(let task3)): - return try await apply(task1, task2, task3) - case (.pending(let task1), .pending(let task2), .terminal): - return try await apply(task1, task2, nil) - case (.pending(let task1), .terminal, .pending(let task3)): - return try await apply(task1, nil, task3) - case (.terminal, .pending(let task2), .pending(let task3)): - return try await apply(nil, task2, task3) - case (.pending(let task1), .terminal, .terminal): - return try await apply(task1, nil, nil) - case (.terminal, .pending(let task2), .terminal): - return try await apply(nil, task2, nil) - case (.terminal, .terminal, .pending(let task3)): - return try await apply(nil, nil, task3) - } - } - } - - let base1: Base1 - let base2: Base2 - let base3: Base3 - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } - - public func makeAsyncIterator() -> Iterator { - return Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator(), base3.makeAsyncIterator()) - } -} diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index eedad1ee..74396080 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -87,6 +87,27 @@ internal struct Lock { func unlock() { 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() + } + + // specialise Void return (for performance) + func withLockVoid(_ body: () throws -> Void) rethrows -> Void { + try self.withLock(body) + } } struct ManagedCriticalState { diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift new file mode 100644 index 00000000..d1a4b4ec --- /dev/null +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +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 +{ + return AsyncMerge2Sequence(base1, base2) +} + +/// An ``Swift/AsyncSequence`` that takes two upstream ``Swift/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 +{ + public typealias Element = Base1.Element + + 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``. + public init( + _ base1: Base1, + _ base2: Base2 + ) { + self.base1 = base1 + self.base2 = base2 + } +} + +extension AsyncMerge2Sequence: AsyncSequence { + public func makeAsyncIterator() -> AsyncIterator { + let storage = MergeStorage( + base1: base1, + base2: base2, + base3: nil + ) + return AsyncIterator(storage: storage) + } +} + +extension AsyncMerge2Sequence { + public struct AsyncIterator: 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 + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + try await storage.next() + } + } + + let internalClass: InternalClass + + fileprivate init(storage: MergeStorage) { + internalClass = InternalClass(storage: storage) + } + + public mutating func next() async rethrows -> Element? { + try await internalClass.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift new file mode 100644 index 00000000..579e0743 --- /dev/null +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import DequeModule + +/// Creates an asynchronous sequence of elements from two underlying asynchronous sequences +public func merge< + 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 +{ + return AsyncMerge3Sequence(base1, base2, base3) +} + +/// An ``Swift/AsyncSequence`` that takes three upstream ``Swift/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 +{ + public typealias Element = Base1.Element + + 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``. + public init( + _ base1: Base1, + _ base2: Base2, + _ base3: Base3 + ) { + self.base1 = base1 + self.base2 = base2 + self.base3 = base3 + } +} + +extension AsyncMerge3Sequence: AsyncSequence { + public func makeAsyncIterator() -> AsyncIterator { + let storage = MergeStorage( + base1: base1, + base2: base2, + base3: base3 + ) + return AsyncIterator(storage: storage) + } +} + +public extension AsyncMerge3Sequence { + struct AsyncIterator: 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 + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + try await storage.next() + } + } + + let internalClass: InternalClass + + fileprivate init(storage: MergeStorage) { + internalClass = InternalClass(storage: storage) + } + + public mutating func next() async rethrows -> Element? { + try await internalClass.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift new file mode 100644 index 00000000..ba0f2940 --- /dev/null +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -0,0 +1,627 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import DequeModule + +/// The state machine for any of the `merge` operator. +/// +/// 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 +{ + 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 + } + } + + /// 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") + } + } + + 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 `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") + } + } + + /// 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 `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 + + 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 + } + + 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 transitioning to `merging` before we return from `makeAsyncIterator` + // this can never happen + preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + + 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") + } + } +} diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift new file mode 100644 index 00000000..de4c72b8 --- /dev/null +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -0,0 +1,449 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +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 +{ + 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: ()) } + } + } + + + 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() } + + switch action { + case let .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + 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 + // For each upstream sequence we are adding a child task that + // is consuming the upstream sequence + group.addTask { + var iterator1 = base1.makeAsyncIterator() + + // This is our upstream consumption loop + loop: while true { + // We are creating a continuation before requesting the next + // element from upstream. This continuation is only resumed + // if the downstream consumer called `next` to signal his demand. + try await withUnsafeThrowingContinuation { continuation in + let action = self.lock.withLock { + self.stateMachine.childTaskSuspended(continuation) + } + + switch action { + case let .resumeContinuation(continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) + + case let .resumeContinuationWithError(continuation, error): + // This happens if another upstream already failed or if + // the task got cancelled. + continuation.resume(throwing: error) + + case .none: + break + } + } + + // We got signalled from the downstream that we have demand so let's + // request a new element from the upstream + if let element1 = try await iterator1.next() { + let action = self.lock.withLock { + self.stateMachine.elementProduced(element1) + } + + switch action { + case let .resumeContinuation(continuation, element): + // We had an outstanding demand and where the first + // upstream to produce an element so we can forward it to + // the downstream + continuation.resume(returning: element) + + case .none: + break + } + + } else { + // The upstream returned `nil` which indicates that it finished + let action = self.lock.withLock { + self.stateMachine.upstreamFinished() + } + + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. + switch action { + case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: nil) + + break loop + + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + case .none: + + break loop + } + } + } + } + + // Copy from the above just using the base2 sequence + group.addTask { + var iterator2 = base2.makeAsyncIterator() + + // This is our upstream consumption loop + loop: while true { + // We are creating a continuation before requesting the next + // element from upstream. This continuation is only resumed + // if the downstream consumer called `next` to signal his demand. + try await withUnsafeThrowingContinuation { continuation in + let action = self.lock.withLock { + self.stateMachine.childTaskSuspended(continuation) + } + + switch action { + case let .resumeContinuation(continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) + + case let .resumeContinuationWithError(continuation, error): + // This happens if another upstream already failed or if + // the task got cancelled. + continuation.resume(throwing: error) + + case .none: + break + } + } + + // We got signalled from the downstream that we have demand so let's + // request a new element from the upstream + if let element2 = try await iterator2.next() { + let action = self.lock.withLock { + self.stateMachine.elementProduced(element2) + } + + switch action { + case let .resumeContinuation(continuation, element): + // We had an outstanding demand and where the first + // upstream to produce an element so we can forward it to + // the downstream + continuation.resume(returning: element) + + case .none: + break + } + + } else { + // The upstream returned `nil` which indicates that it finished + let action = self.lock.withLock { + self.stateMachine.upstreamFinished() + } + + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. + switch action { + case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: nil) + + break loop + + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + case .none: + + break loop + } + } + } + } + + // Copy from the above just using the base3 sequence + if let base3 = base3 { + group.addTask { + var iterator3 = base3.makeAsyncIterator() + + // This is our upstream consumption loop + loop: while true { + // We are creating a continuation before requesting the next + // element from upstream. This continuation is only resumed + // if the downstream consumer called `next` to signal his demand. + try await withUnsafeThrowingContinuation { continuation in + let action = self.lock.withLock { + self.stateMachine.childTaskSuspended(continuation) + } + + switch action { + case let .resumeContinuation(continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) + + case let .resumeContinuationWithError(continuation, error): + // This happens if another upstream already failed or if + // the task got cancelled. + continuation.resume(throwing: error) + + case .none: + break + } + } + + // We got signalled from the downstream that we have demand so let's + // request a new element from the upstream + if let element3 = try await iterator3.next() { + let action = self.lock.withLock { + self.stateMachine.elementProduced(element3) + } + + switch action { + case let .resumeContinuation(continuation, element): + // We had an outstanding demand and where the first + // upstream to produce an element so we can forward it to + // the downstream + continuation.resume(returning: element) + + case .none: + break + } + + } else { + // The upstream returned `nil` which indicates that it finished + let action = self.lock.withLock { + self.stateMachine.upstreamFinished() + } + + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. + switch action { + case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: nil) + + break loop + + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + case .none: + + break loop + } + } + } + } + } + + do { + try await group.waitForAll() + } 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) + } +} + diff --git a/Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge2StateMachine.swift similarity index 74% rename from Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift rename to Sources/AsyncAlgorithms/Merge2StateMachine.swift index eeaf0246..b0f15ab6 100644 --- a/Sources/AsyncAlgorithms/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge2StateMachine.swift @@ -9,16 +9,6 @@ // //===----------------------------------------------------------------------===// -/// 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, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { - return AsyncMerge2Sequence(base1, base2) -} - struct Merge2StateMachine: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base1.Element: Sendable, Base2.Element: Sendable { typealias Element1 = Base1.Element typealias Element2 = Base2.Element @@ -170,40 +160,3 @@ extension Merge2StateMachine.Either where Base1.Element == Base2.Element { } } } - -/// An asynchronous sequence of elements from two underlying asynchronous sequences -/// -/// In a `AsyncMerge2Sequence` instance, the *i*th element is the *i*th element -/// resolved in sequential order out of the two underlying asynchronous sequences. -/// Use the `merge(_:_:)` function to create an `AsyncMerge2Sequence`. -public struct AsyncMerge2Sequence: AsyncSequence, Sendable -where - Base1.Element == Base2.Element, - Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { - public typealias Element = Base1.Element - /// An iterator for `AsyncMerge2Sequence` - public struct Iterator: AsyncIteratorProtocol, Sendable { - var state: Merge2StateMachine - init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator) { - state = Merge2StateMachine(base1, base2) - } - - public mutating func next() async rethrows -> Element? { - return try await state.next()?.value - } - } - - let base1: Base1 - let base2: Base2 - - init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } - - public func makeAsyncIterator() -> Iterator { - return Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator()) - } -} diff --git a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift index a0f84dbb..c9cdb968 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift @@ -150,3 +150,18 @@ fileprivate func ==(_ lhs: [(A, B, C)] 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 } +) async { + do { + _ = try await expression() + XCTFail("Expression did not throw error", file: file, line: line) + } catch { + verify(error) + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index 3cf7c577..76ce5344 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -505,4 +505,44 @@ final class TestMerge3: XCTestCase { task.cancel() wait(for: [finished], timeout: 1.0) } + + // MARK: - IteratorInitialized + + func testIteratorInitialized_whenInitial() async throws { + let reportingSequence1 = ReportingAsyncSequence([1]) + let reportingSequence2 = ReportingAsyncSequence([2]) + let merge = merge(reportingSequence1, reportingSequence2) + + _ = 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) + + XCTAssertEqual(reportingSequence1.events, []) + XCTAssertEqual(reportingSequence2.events, []) + } + + // MARK: - IteratorDeinitialized + + func testIteratorDeinitialized_whenMerging() async throws { + let merge = merge([1].async, [2].async) + + var iterator: _! = merge.makeAsyncIterator() + + let nextValue = await iterator.next() + XCTAssertNotNil(nextValue) + + iterator = nil + } + + func testIteratorDeinitialized_whenFinished() async throws { + let merge = merge(Array().async, [].async) + + var iterator: _? = merge.makeAsyncIterator() + let firstValue = await iterator?.next() + XCTAssertNil(firstValue) + + iterator = nil + } } From 1bd073999c66f0c604e7cb927a5d52ac1290ff17 Mon Sep 17 00:00:00 2001 From: Doug Russell Date: Mon, 10 Oct 2022 11:35:35 -0400 Subject: [PATCH 057/149] Deprecation Housekeeping (#206) `withTaskCancellationHandler(_:, operation:)` -> `withTaskCancellationHandler(operation:onCancel:)` --- .../AsyncAlgorithms/AsyncBufferSequence.swift | 4 ++-- Sources/AsyncAlgorithms/AsyncChannel.swift | 12 ++++++------ .../AsyncAlgorithms/AsyncThrowingChannel.swift | 12 ++++++------ Sources/AsyncAlgorithms/TaskSelect.swift | 16 ++++++++-------- Sources/AsyncSequenceValidation/Clock.swift | 4 ++-- Sources/AsyncSequenceValidation/Input.swift | 6 +++--- .../Support/ManualClock.swift | 4 ++-- Tests/AsyncAlgorithmsTests/TestTaskSelect.swift | 12 ++++++------ 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift index 16082203..2b43fae2 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift @@ -265,8 +265,6 @@ extension AsyncBufferSequence: AsyncSequence { func next() async rethrows -> Element? { let result: Result = await withTaskCancellationHandler { - task?.cancel() - } operation: { do { let value = try await state.next(buffer: buffer) return .success(value) @@ -274,6 +272,8 @@ extension AsyncBufferSequence: AsyncSequence { task?.cancel() return .failure(error) } + } onCancel: { + task?.cancel() } return try result._rethrowGet() } diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index 74f1bbee..5c4f5a47 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -40,10 +40,10 @@ public final class AsyncChannel: AsyncSequence, Sendable { let generation = channel.establish() let nextTokenStatus = ManagedCriticalState(.new) - let value = await withTaskCancellationHandler { [channel] in - channel.cancelNext(nextTokenStatus, generation) - } operation: { + let value = await withTaskCancellationHandler { await channel.next(nextTokenStatus, generation) + } onCancel: { [channel] in + channel.cancelNext(nextTokenStatus, generation) } if let value { @@ -237,10 +237,10 @@ public final class AsyncChannel: AsyncSequence, Sendable { let generation = establish() let sendTokenStatus = ManagedCriticalState(.new) - await withTaskCancellationHandler { [weak self] in - self?.cancelSend(sendTokenStatus, generation) - } operation: { + await withTaskCancellationHandler { await send(sendTokenStatus, generation, element) + } onCancel: { [weak self] in + self?.cancelSend(sendTokenStatus, generation) } } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index cfc44a8b..732b0906 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -39,10 +39,10 @@ public final class AsyncThrowingChannel: Asyn let nextTokenStatus = ManagedCriticalState(.new) do { - let value = try await withTaskCancellationHandler { [channel] in - channel.cancelNext(nextTokenStatus, generation) - } operation: { + let value = try await withTaskCancellationHandler { try await channel.next(nextTokenStatus, generation) + } onCancel: { [channel] in + channel.cancelNext(nextTokenStatus, generation) } if let value = value { @@ -295,10 +295,10 @@ public final class AsyncThrowingChannel: Asyn let generation = establish() let sendTokenStatus = ManagedCriticalState(.new) - await withTaskCancellationHandler { [weak self] in - self?.cancelSend(sendTokenStatus, generation) - } operation: { + await withTaskCancellationHandler { await send(sendTokenStatus, generation, element) + } onCancel: { [weak self] in + self?.cancelSend(sendTokenStatus, generation) } } diff --git a/Sources/AsyncAlgorithms/TaskSelect.swift b/Sources/AsyncAlgorithms/TaskSelect.swift index 8ff93e05..b6208248 100644 --- a/Sources/AsyncAlgorithms/TaskSelect.swift +++ b/Sources/AsyncAlgorithms/TaskSelect.swift @@ -36,14 +36,6 @@ extension Task { where Tasks.Element == Task { let state = ManagedCriticalState(TaskSelectState()) return await withTaskCancellationHandler { - let tasks = state.withCriticalRegion { state -> [Task] in - defer { state.tasks = nil } - return state.tasks ?? [] - } - for task in tasks { - task.cancel() - } - } operation: { await withUnsafeContinuation { continuation in for task in tasks { Task { @@ -61,6 +53,14 @@ extension Task { }?.cancel() } } + } onCancel: { + let tasks = state.withCriticalRegion { state -> [Task] in + defer { state.tasks = nil } + return state.tasks ?? [] + } + for task in tasks { + task.cancel() + } } } diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index 5eeb7360..6f33d15a 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -114,11 +114,11 @@ extension AsyncSequenceValidationDiagram.Clock { ) async throws { let token = queue.prepare() try await withTaskCancellationHandler { - queue.cancel(token) - } operation: { try await withUnsafeThrowingContinuation { continuation in queue.enqueue(AsyncSequenceValidationDiagram.Context.currentJob, deadline: deadline, continuation: continuation, token: token) } + } onCancel: { + queue.cancel(token) } } } diff --git a/Sources/AsyncSequenceValidation/Input.swift b/Sources/AsyncSequenceValidation/Input.swift index 35ede268..26e23da6 100644 --- a/Sources/AsyncSequenceValidation/Input.swift +++ b/Sources/AsyncSequenceValidation/Input.swift @@ -50,12 +50,12 @@ extension AsyncSequenceValidationDiagram { eventIndex = 0 } } - return try await withTaskCancellationHandler { [queue] in - queue.cancel(token) - } operation: { + return try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { continuation in queue.enqueue(Context.currentJob, deadline: when, continuation: continuation, results[eventIndex], index: index, token: token) } + } onCancel: { [queue] in + queue.cancel(token) } } diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift b/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift index f7dd2806..40ec8467 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ManualClock.swift @@ -245,11 +245,11 @@ public struct ManualClock: Clock { return state.generation } try await withTaskCancellationHandler { - cancel(generation) - } operation: { try await withUnsafeThrowingContinuation { continuation in schedule(generation, continuation: continuation, deadline: deadline) } + } onCancel: { + cancel(generation) } } } diff --git a/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift b/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift index e70b3adf..a3b49629 100644 --- a/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift +++ b/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift @@ -67,9 +67,7 @@ final class TestTaskSelect: XCTestCase { let secondCancelled = expectation(description: "second cancelled") let task = Task { _ = await Task.select(Task { - await withTaskCancellationHandler { - firstCancelled.fulfill() - } operation: { () -> Int in + await withTaskCancellationHandler { () -> Int in firstReady.fulfill() if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) @@ -77,11 +75,11 @@ final class TestTaskSelect: XCTestCase { try? await Task.sleep(nanoseconds: 2_000_000_000) } return 1 + } onCancel: { + firstCancelled.fulfill() } }, Task { - await withTaskCancellationHandler { - secondCancelled.fulfill() - } operation: { () -> Int in + await withTaskCancellationHandler { () -> Int in secondReady.fulfill() if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) @@ -89,6 +87,8 @@ final class TestTaskSelect: XCTestCase { try? await Task.sleep(nanoseconds: 2_000_000_000) } return 1 + } onCancel: { + secondCancelled.fulfill() } }) } From 76b27aebeffff69b6d1c2f2e616c0dd12909642b Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Mon, 10 Oct 2022 17:58:22 +0200 Subject: [PATCH 058/149] zip: remove Sendable constraint + limit task creation (#201) --- .../AsyncAlgorithms/AsyncZip2Sequence.swift | 165 ------- .../AsyncAlgorithms/AsyncZip3Sequence.swift | 210 --------- .../Zip/AsyncZip2Sequence.swift | 54 +++ .../Zip/AsyncZip3Sequence.swift | 58 +++ Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift | 212 +++++++++ .../Zip/Zip2StateMachine.swift | 367 +++++++++++++++ Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift | 252 ++++++++++ .../Zip/Zip3StateMachine.swift | 439 ++++++++++++++++++ Tests/AsyncAlgorithmsTests/TestZip.swift | 38 +- 9 files changed, 1401 insertions(+), 394 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncZip2Sequence.swift delete mode 100644 Sources/AsyncAlgorithms/AsyncZip3Sequence.swift create mode 100644 Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift create mode 100644 Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift create mode 100644 Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift create mode 100644 Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift create mode 100644 Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift diff --git a/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift deleted file mode 100644 index 0b91fe4f..00000000 --- a/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift +++ /dev/null @@ -1,165 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// Creates an asynchronous sequence that concurrently awaits values from two `AsyncSequence` types -/// and emits a tuple of the values. -public func zip(_ base1: Base1, _ base2: Base2) -> AsyncZip2Sequence - where Base1: Sendable, - Base2: Sendable, - Base1.AsyncIterator: Sendable, - Base2.AsyncIterator: Sendable, - Base1.Element: Sendable, - Base2.Element: Sendable { - AsyncZip2Sequence(base1, base2) -} - -/// An asynchronous sequence that concurrently awaits values from two `AsyncSequence` types -/// and emits a tuple of the values. -public struct AsyncZip2Sequence: Sendable - where Base1: Sendable, - Base2: Sendable, - Base1.AsyncIterator: Sendable, - Base2.AsyncIterator: Sendable, - Base1.Element: Sendable, - Base2.Element: Sendable { - let base1: Base1 - let base2: Base2 - - init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } -} - -extension AsyncZip2Sequence: AsyncSequence { - public typealias Element = (Base1.Element, Base2.Element) - - /// The iterator for an `AsyncZip2Sequence` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - var base1: Base1.AsyncIterator? - var base2: Base2.AsyncIterator? - - enum Partial: Sendable { - case first(Result, Base1.AsyncIterator) - case second(Result, Base2.AsyncIterator) - } - - init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator) { - self.base1 = base1 - self.base2 = base2 - } - - public mutating func next() async rethrows -> (Base1.Element, Base2.Element)? { - func iteration( - _ group: inout TaskGroup, - _ value1: inout Base1.Element?, - _ value2: inout Base2.Element?, - _ iterator1: inout Base1.AsyncIterator?, - _ iterator2: inout Base2.AsyncIterator? - ) async -> Result<(Base1.Element, Base2.Element)?, Error>? { - guard let partial = await group.next() else { - return .success(nil) - } - switch partial { - case .first(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value1 = value - iterator1 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - case .second(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value2 = value - iterator2 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - } - } - - guard let base1 = base1, let base2 = base2 else { - return nil - } - - let (result, iter1, iter2) = await withTaskGroup(of: Partial.self) { group -> (Result<(Base1.Element, Base2.Element)?, Error>, Base1.AsyncIterator?, Base2.AsyncIterator?) in - group.addTask { - var iterator = base1 - do { - let value = try await iterator.next() - return .first(.success(value), iterator) - } catch { - return .first(.failure(error), iterator) - } - } - group.addTask { - var iterator = base2 - do { - let value = try await iterator.next() - return .second(.success(value), iterator) - } catch { - return .second(.failure(error), iterator) - } - } - var res1: Base1.Element? - var res2: Base2.Element? - var iter1: Base1.AsyncIterator? - var iter2: Base2.AsyncIterator? - - if let result = await iteration(&group, &res1, &res2, &iter1, &iter2) { - return (result, nil, nil) - } - if let result = await iteration(&group, &res1, &res2, &iter1, &iter2) { - return (result, nil, nil) - } - guard let res1 = res1, let res2 = res2 else { - return (.success(nil), nil, nil) - } - - return (.success((res1, res2)), iter1, iter2) - } - do { - guard let value = try result._rethrowGet() else { - self.base1 = nil - self.base2 = nil - return nil - } - self.base1 = iter1 - self.base2 = iter2 - return value - } catch { - self.base1 = nil - self.base2 = nil - throw error - } - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator()) - } -} diff --git a/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift deleted file mode 100644 index b66a3f7e..00000000 --- a/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift +++ /dev/null @@ -1,210 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// Creates an asynchronous sequence that concurrently awaits values from three `AsyncSequence` types -/// and emits a tuple of the values. -public func zip(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncZip3Sequence - where Base1: Sendable, - Base2: Sendable, - Base3: Sendable, - Base1.AsyncIterator: Sendable, - Base2.AsyncIterator: Sendable, - Base3.AsyncIterator: Sendable, - Base1.Element: Sendable, - Base2.Element: Sendable, - Base3.Element: Sendable { - AsyncZip3Sequence(base1, base2, base3) -} - -/// An asynchronous sequence that concurrently awaits values from three `AsyncSequence` types -/// and emits a tuple of the values. -public struct AsyncZip3Sequence: Sendable - where Base1: Sendable, - Base2: Sendable, - Base3: Sendable, - Base1.AsyncIterator: Sendable, - Base2.AsyncIterator: Sendable, - Base3.AsyncIterator: Sendable, - Base1.Element: Sendable, - Base2.Element: Sendable, - Base3.Element: Sendable { - let base1: Base1 - let base2: Base2 - let base3: Base3 - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } -} - -extension AsyncZip3Sequence: AsyncSequence { - public typealias Element = (Base1.Element, Base2.Element, Base3.Element) - - /// The iterator for an `AsyncZip3Sequence` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - var base1: Base1.AsyncIterator? - var base2: Base2.AsyncIterator? - var base3: Base3.AsyncIterator? - - enum Partial: Sendable { - case first(Result, Base1.AsyncIterator) - case second(Result, Base2.AsyncIterator) - case third(Result, Base3.AsyncIterator) - } - - init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator, _ base3: Base3.AsyncIterator) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } - - public mutating func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element)? { - func iteration( - _ group: inout TaskGroup, - _ value1: inout Base1.Element?, - _ value2: inout Base2.Element?, - _ value3: inout Base3.Element?, - _ iterator1: inout Base1.AsyncIterator?, - _ iterator2: inout Base2.AsyncIterator?, - _ iterator3: inout Base3.AsyncIterator? - ) async -> Result<(Base1.Element, Base2.Element, Base3.Element)?, Error>? { - guard let partial = await group.next() else { - return .success(nil) - } - switch partial { - case .first(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value1 = value - iterator1 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - case .second(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value2 = value - iterator2 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - case .third(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value3 = value - iterator3 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - } - } - - guard let base1 = base1, let base2 = base2, let base3 = base3 else { - return nil - } - - let (result, iter1, iter2, iter3) = await withTaskGroup(of: Partial.self) { group -> (Result<(Base1.Element, Base2.Element, Base3.Element)?, Error>, Base1.AsyncIterator?, Base2.AsyncIterator?, Base3.AsyncIterator?) in - group.addTask { - var iterator = base1 - do { - let value = try await iterator.next() - return .first(.success(value), iterator) - } catch { - return .first(.failure(error), iterator) - } - } - group.addTask { - var iterator = base2 - do { - let value = try await iterator.next() - return .second(.success(value), iterator) - } catch { - return .second(.failure(error), iterator) - } - } - group.addTask { - var iterator = base3 - do { - let value = try await iterator.next() - return .third(.success(value), iterator) - } catch { - return .third(.failure(error), iterator) - } - } - var res1: Base1.Element? - var res2: Base2.Element? - var res3: Base3.Element? - var iter1: Base1.AsyncIterator? - var iter2: Base2.AsyncIterator? - var iter3: Base3.AsyncIterator? - - if let result = await iteration(&group, &res1, &res2, &res3, &iter1, &iter2, &iter3) { - return (result, nil, nil, nil) - } - if let result = await iteration(&group, &res1, &res2, &res3, &iter1, &iter2, &iter3) { - return (result, nil, nil, nil) - } - if let result = await iteration(&group, &res1, &res2, &res3, &iter1, &iter2, &iter3) { - return (result, nil, nil, nil) - } - guard let res1 = res1, let res2 = res2, let res3 = res3 else { - return (.success(nil), nil, nil, nil) - } - - return (.success((res1, res2, res3)), iter1, iter2, iter3) - } - do { - guard let value = try result._rethrowGet() else { - self.base1 = nil - self.base2 = nil - self.base3 = nil - return nil - } - self.base1 = iter1 - self.base2 = iter2 - self.base3 = iter3 - return value - } catch { - self.base1 = nil - self.base2 = nil - self.base3 = nil - throw error - } - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator(), base3.makeAsyncIterator()) - } -} diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift new file mode 100644 index 00000000..292a612d --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Creates an asynchronous sequence that concurrently awaits values from two `AsyncSequence` types +/// and emits a tuple of the values. +public func zip( + _ base1: Base1, + _ base2: Base2 +) -> AsyncZip2Sequence { + AsyncZip2Sequence(base1, base2) +} + +/// An asynchronous sequence that concurrently awaits values from two `AsyncSequence` types +/// and emits a tuple of the values. +public struct AsyncZip2Sequence: AsyncSequence +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { + public typealias Element = (Base1.Element, Base2.Element) + public typealias AsyncIterator = Iterator + + let base1: Base1 + let base2: Base2 + + init(_ base1: Base1, _ base2: Base2) { + self.base1 = base1 + self.base2 = base2 + } + + public func makeAsyncIterator() -> AsyncIterator { + Iterator( + base1, + base2 + ) + } + + public struct Iterator: AsyncIteratorProtocol { + let runtime: Zip2Runtime + + init(_ base1: Base1, _ base2: Base2) { + self.runtime = Zip2Runtime(base1, base2) + } + + public mutating func next() async rethrows -> Element? { + try await self.runtime.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift new file mode 100644 index 00000000..4a52158e --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Creates an asynchronous sequence that concurrently awaits values from three `AsyncSequence` types +/// and emits a tuple of the values. +public func zip( + _ base1: Base1, + _ base2: Base2, + _ base3: Base3 +) -> AsyncZip3Sequence { + AsyncZip3Sequence(base1, base2, base3) +} + +/// An asynchronous sequence that concurrently awaits values from three `AsyncSequence` types +/// and emits a tuple of the values. +public struct AsyncZip3Sequence: AsyncSequence +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 + + let base1: Base1 + let base2: Base2 + let base3: Base3 + + init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { + self.base1 = base1 + self.base2 = base2 + self.base3 = base3 + } + + public func makeAsyncIterator() -> AsyncIterator { + Iterator( + base1, + base2, + base3 + ) + } + + public struct Iterator: AsyncIteratorProtocol { + let runtime: Zip3Runtime + + init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { + self.runtime = Zip3Runtime(base1, base2, base3) + } + + public mutating func next() async rethrows -> Element? { + try await self.runtime.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift b/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift new file mode 100644 index 00000000..02cc26b7 --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class Zip2Runtime: Sendable +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { + typealias ZipStateMachine = Zip2StateMachine + + private let stateMachine = ManagedCriticalState(ZipStateMachine()) + private let base1: Base1 + private let base2: Base2 + + init(_ base1: Base1, _ base2: Base2) { + self.base1 = base1 + self.base2 = base2 + } + + func next() async rethrows -> (Base1.Element, Base2.Element)? { + try await withTaskCancellationHandler { + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.rootTaskIsCancelled() + } + // clean the allocated resources and state + self.handle(rootTaskIsCancelledOutput: output) + } operation: { + let results = await withUnsafeContinuation { continuation in + self.stateMachine.withCriticalRegion { stateMachine in + let output = stateMachine.newDemandFromConsumer(suspendedDemand: continuation) + switch output { + case .startTask(let suspendedDemand): + // first iteration, we start one task per base to iterate over them + self.startTask(stateMachine: &stateMachine, suspendedDemand: suspendedDemand) + + case .resumeBases(let suspendedBases): + // bases can be iterated over for 1 iteration so their next value can be retrieved + suspendedBases.forEach { $0.resume() } + + case .terminate(let suspendedDemand): + // the async sequence is already finished, immediately resuming + suspendedDemand.resume(returning: nil) + } + } + } + + guard let results else { + return nil + } + + self.stateMachine.withCriticalRegion { stateMachine in + // acknowledging the consumption of the zipped values, so we can begin another iteration on the bases + stateMachine.demandIsFulfilled() + } + + return try (results.0._rethrowGet(), results.1._rethrowGet()) + } + } + + private func handle(rootTaskIsCancelledOutput: ZipStateMachine.RootTaskIsCancelledOutput) { + switch rootTaskIsCancelledOutput { + case .terminate(let task, let suspendedBases, let suspendedDemands): + suspendedBases?.forEach { $0.resume() } + suspendedDemands?.forEach { $0?.resume(returning: nil) } + task?.cancel() + } + } + + private func startTask( + stateMachine: inout ZipStateMachine, + suspendedDemand: ZipStateMachine.SuspendedDemand + ) { + let task = Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { + var base1Iterator = self.base1.makeAsyncIterator() + + do { + while true { + await withUnsafeContinuation { continuation in + let output = self.stateMachine.withCriticalRegion { machine in + machine.newLoopFromBase1(suspendedBase: continuation) + } + + self.handle(newLoopFromBaseOutput: output) + } + + guard let element1 = try await base1Iterator.next() else { + break + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.base1HasProducedElement(element: element1) + } + + self.handle(baseHasProducedElementOutput: output) + } + } catch { + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseHasProducedFailure(error: error) + } + + self.handle(baseHasProducedFailureOutput: output) + } + + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.baseIsFinished() + } + + self.handle(baseIsFinishedOutput: output) + } + + group.addTask { + var base2Iterator = self.base2.makeAsyncIterator() + + do { + while true { + await withUnsafeContinuation { continuation in + let output = self.stateMachine.withCriticalRegion { machine in + machine.newLoopFromBase2(suspendedBase: continuation) + } + + self.handle(newLoopFromBaseOutput: output) + } + + guard let element2 = try await base2Iterator.next() else { + break + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.base2HasProducedElement(element: element2) + } + + self.handle(baseHasProducedElementOutput: output) + } + } catch { + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseHasProducedFailure(error: error) + } + + self.handle(baseHasProducedFailureOutput: output) + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseIsFinished() + } + + self.handle(baseIsFinishedOutput: output) + } + } + } + stateMachine.taskIsStarted(task: task, suspendedDemand: suspendedDemand) + } + + private func handle(newLoopFromBaseOutput: ZipStateMachine.NewLoopFromBaseOutput) { + switch newLoopFromBaseOutput { + case .none: + break + + case .resumeBases(let suspendedBases): + suspendedBases.forEach { $0.resume() } + + case .terminate(let suspendedBase): + suspendedBase.resume() + } + } + + private func handle(baseHasProducedElementOutput: ZipStateMachine.BaseHasProducedElementOutput) { + switch baseHasProducedElementOutput { + case .none: + break + + case .resumeDemand(let suspendedDemand, let result1, let result2): + suspendedDemand?.resume(returning: (result1, result2)) + } + } + + private func handle(baseHasProducedFailureOutput: ZipStateMachine.BaseHasProducedFailureOutput) { + switch baseHasProducedFailureOutput { + case .none: + break + + case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let result1, let result2): + suspendedDemand?.resume(returning: (result1, result2)) + suspendedBases.forEach { $0.resume() } + task?.cancel() + } + } + + private func handle(baseIsFinishedOutput: ZipStateMachine.BaseIsFinishedOutput) { + switch baseIsFinishedOutput { + case .terminate(let task, let suspendedBases, let suspendedDemands): + suspendedBases?.forEach { $0.resume() } + suspendedDemands?.forEach { $0?.resume(returning: nil) } + task?.cancel() + } + } + + deinit { + // clean the allocated resources and state + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.rootTaskIsCancelled() + } + + self.handle(rootTaskIsCancelledOutput: output) + } +} diff --git a/Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift b/Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift new file mode 100644 index 00000000..19ebc11f --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift @@ -0,0 +1,367 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +struct Zip2StateMachine: Sendable +where Element1: Sendable, Element2: Sendable { + typealias SuspendedDemand = UnsafeContinuation<(Result, Result)?, Never> + + private enum State { + case initial + case awaitingDemandFromConsumer( + task: Task?, + suspendedBases: [UnsafeContinuation] + ) + case awaitingBaseResults( + task: Task?, + result1: Result?, + result2: Result?, + suspendedBases: [UnsafeContinuation], + suspendedDemand: SuspendedDemand? + ) + case finished + } + + private var state: State = .initial + + mutating func taskIsStarted( + task: Task, + suspendedDemand: SuspendedDemand + ) { + switch self.state { + case .initial: + self.state = .awaitingBaseResults( + task: task, + result1: nil, + result2: nil, + suspendedBases: [], + suspendedDemand: suspendedDemand + ) + + default: + preconditionFailure("Inconsistent state, the task cannot start while the state is other than initial") + } + } + + enum NewDemandFromConsumerOutput { + case resumeBases(suspendedBases: [UnsafeContinuation]) + case startTask(suspendedDemand: SuspendedDemand) + case terminate(suspendedDemand: SuspendedDemand) + } + + mutating func newDemandFromConsumer( + suspendedDemand: UnsafeContinuation<(Result, Result)?, Never> + ) -> NewDemandFromConsumerOutput { + switch self.state { + case .initial: + return .startTask(suspendedDemand: suspendedDemand) + + case .awaitingDemandFromConsumer(let task, let suspendedBases): + self.state = .awaitingBaseResults(task: task, result1: nil, result2: nil, suspendedBases: [], suspendedDemand: suspendedDemand) + return .resumeBases(suspendedBases: suspendedBases) + + case .awaitingBaseResults: + preconditionFailure("Inconsistent state, a demand is already suspended") + + case .finished: + return .terminate(suspendedDemand: suspendedDemand) + } + } + + enum NewLoopFromBaseOutput { + case none + case resumeBases(suspendedBases: [UnsafeContinuation]) + case terminate(suspendedBase: UnsafeContinuation) + } + + mutating func newLoopFromBase1(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, var suspendedBases): + precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") + suspendedBases.append(suspendedBase) + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + return .none + + case .awaitingBaseResults(let task, let result1, let result2, var suspendedBases, let suspendedDemand): + precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") + if result1 != nil { + suspendedBases.append(suspendedBase) + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .resumeBases(suspendedBases: [suspendedBase]) + } + + case .finished: + return .terminate(suspendedBase: suspendedBase) + } + } + + mutating func newLoopFromBase2(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { + switch state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, var suspendedBases): + precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") + suspendedBases.append(suspendedBase) + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + return .none + + case .awaitingBaseResults(let task, let result1, let result2, var suspendedBases, let suspendedDemand): + precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") + if result2 != nil { + suspendedBases.append(suspendedBase) + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .resumeBases(suspendedBases: [suspendedBase]) + } + + case .finished: + return .terminate(suspendedBase: suspendedBase) + } + } + + enum BaseHasProducedElementOutput { + case none + case resumeDemand( + suspendedDemand: SuspendedDemand?, + result1: Result, + result2: Result + ) + } + + mutating func base1HasProducedElement(element: Element1) -> BaseHasProducedElementOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, _, let result2, let suspendedBases, let suspendedDemand): + if let result2 { + self.state = .awaitingBaseResults( + task: task, + result1: .success(element), + result2: result2, + suspendedBases: suspendedBases, + suspendedDemand: nil + ) + return .resumeDemand(suspendedDemand: suspendedDemand, result1: .success(element), result2: result2) + } else { + self.state = .awaitingBaseResults( + task: task, + result1: .success(element), + result2: nil, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } + + case .finished: + return .none + } + } + + mutating func base2HasProducedElement(element: Element2) -> BaseHasProducedElementOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, let result1, _, let suspendedBases, let suspendedDemand): + if let result1 { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: .success(element), + suspendedBases: suspendedBases, + suspendedDemand: nil + ) + return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: .success(element)) + } else { + self.state = .awaitingBaseResults( + task: task, + result1: nil, + result2: .success(element), + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } + + case .finished: + return .none + } + } + + enum BaseHasProducedFailureOutput { + case none + case resumeDemandAndTerminate( + task: Task?, + suspendedDemand: SuspendedDemand?, + suspendedBases: [UnsafeContinuation], + result1: Result, + result2: Result + ) + } + + mutating func baseHasProducedFailure(error: any Error) -> BaseHasProducedFailureOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .resumeDemandAndTerminate( + task: task, + suspendedDemand: suspendedDemand, + suspendedBases: suspendedBases, + result1: .failure(error), + result2: .failure(error) + ) + + case .finished: + return .none + } + } + + mutating func base2HasProducedFailure(error: Error) -> BaseHasProducedFailureOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .resumeDemandAndTerminate( + task: task, + suspendedDemand: suspendedDemand, + suspendedBases: suspendedBases, + result1: .failure(error), + result2: .failure(error) + ) + + case .finished: + return .none + } + } + + mutating func demandIsFulfilled() { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, results are not yet available to be acknowledged") + + case .awaitingBaseResults(let task, let result1, let result2, let suspendedBases, let suspendedDemand): + precondition(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") + precondition(result1 != nil && result2 != nil, "Inconsistent state, all results are not yet available to be acknowledged") + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + + case .finished: + break + } + } + + enum RootTaskIsCancelledOutput { + case terminate( + task: Task?, + suspendedBases: [UnsafeContinuation]?, + suspendedDemands: [SuspendedDemand?]? + ) + } + + mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { + switch self.state { + case .initial: + assertionFailure("Inconsistent state, the task is not started") + self.state = .finished + return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) + + case .awaitingDemandFromConsumer(let task, let suspendedBases): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) + + case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) + + case .finished: + return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) + } + } + + enum BaseIsFinishedOutput { + case terminate( + task: Task?, + suspendedBases: [UnsafeContinuation]?, + suspendedDemands: [SuspendedDemand?]? + ) + } + + mutating func baseIsFinished() -> BaseIsFinishedOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, let suspendedBases): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) + + case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) + + case .finished: + return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) + } + } +} diff --git a/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift b/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift new file mode 100644 index 00000000..ee393106 --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift @@ -0,0 +1,252 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class Zip3Runtime: Sendable +where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { + typealias ZipStateMachine = Zip3StateMachine + + private let stateMachine = ManagedCriticalState(ZipStateMachine()) + private let base1: Base1 + private let base2: Base2 + private let base3: Base3 + + init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { + self.base1 = base1 + self.base2 = base2 + self.base3 = base3 + } + + func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element)? { + try await withTaskCancellationHandler { + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.rootTaskIsCancelled() + } + // clean the allocated resources and state + self.handle(rootTaskIsCancelledOutput: output) + } operation: { + let results = await withUnsafeContinuation { continuation in + self.stateMachine.withCriticalRegion { stateMachine in + let output = stateMachine.newDemandFromConsumer(suspendedDemand: continuation) + switch output { + case .startTask(let suspendedDemand): + // first iteration, we start one task per base to iterate over them + self.startTask(stateMachine: &stateMachine, suspendedDemand: suspendedDemand) + + case .resumeBases(let suspendedBases): + // bases can be iterated over for 1 iteration so their next value can be retrieved + suspendedBases.forEach { $0.resume() } + + case .terminate(let suspendedDemand): + // the async sequence is already finished, immediately resuming + suspendedDemand.resume(returning: nil) + } + } + } + + guard let results else { + return nil + } + + self.stateMachine.withCriticalRegion { stateMachine in + // acknowledging the consumption of the zipped values, so we can begin another iteration on the bases + stateMachine.demandIsFulfilled() + } + + return try (results.0._rethrowGet(), results.1._rethrowGet(), results.2._rethrowGet()) + } + } + + private func handle(rootTaskIsCancelledOutput: ZipStateMachine.RootTaskIsCancelledOutput) { + switch rootTaskIsCancelledOutput { + case .terminate(let task, let suspendedBases, let suspendedDemands): + suspendedBases?.forEach { $0.resume() } + suspendedDemands?.forEach { $0?.resume(returning: nil) } + task?.cancel() + } + } + + private func startTask( + stateMachine: inout ZipStateMachine, + suspendedDemand: ZipStateMachine.SuspendedDemand + ) { + let task = Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { + var base1Iterator = self.base1.makeAsyncIterator() + + do { + while true { + await withUnsafeContinuation { continuation in + let output = self.stateMachine.withCriticalRegion { machine in + machine.newLoopFromBase1(suspendedBase: continuation) + } + + self.handle(newLoopFromBaseOutput: output) + } + + guard let element1 = try await base1Iterator.next() else { + break + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.base1HasProducedElement(element: element1) + } + + self.handle(baseHasProducedElementOutput: output) + } + } catch { + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseHasProducedFailure(error: error) + } + + self.handle(baseHasProducedFailureOutput: output) + } + + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.baseIsFinished() + } + + self.handle(baseIsFinishedOutput: output) + } + + group.addTask { + var base2Iterator = self.base2.makeAsyncIterator() + + do { + while true { + await withUnsafeContinuation { continuation in + let output = self.stateMachine.withCriticalRegion { machine in + machine.newLoopFromBase2(suspendedBase: continuation) + } + + self.handle(newLoopFromBaseOutput: output) + } + + guard let element2 = try await base2Iterator.next() else { + break + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.base2HasProducedElement(element: element2) + } + + self.handle(baseHasProducedElementOutput: output) + } + } catch { + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseHasProducedFailure(error: error) + } + + self.handle(baseHasProducedFailureOutput: output) + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseIsFinished() + } + + self.handle(baseIsFinishedOutput: output) + } + + group.addTask { + var base3Iterator = self.base3.makeAsyncIterator() + + do { + while true { + await withUnsafeContinuation { continuation in + let output = self.stateMachine.withCriticalRegion { machine in + machine.newLoopFromBase3(suspendedBase: continuation) + } + + self.handle(newLoopFromBaseOutput: output) + } + + guard let element3 = try await base3Iterator.next() else { + break + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.base3HasProducedElement(element: element3) + } + + self.handle(baseHasProducedElementOutput: output) + } + } catch { + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseHasProducedFailure(error: error) + } + + self.handle(baseHasProducedFailureOutput: output) + } + + let output = self.stateMachine.withCriticalRegion { machine in + machine.baseIsFinished() + } + + self.handle(baseIsFinishedOutput: output) + } + } + } + stateMachine.taskIsStarted(task: task, suspendedDemand: suspendedDemand) + } + + private func handle(newLoopFromBaseOutput: ZipStateMachine.NewLoopFromBaseOutput) { + switch newLoopFromBaseOutput { + case .none: + break + + case .resumeBases(let suspendedBases): + suspendedBases.forEach { $0.resume() } + + case .terminate(let suspendedBase): + suspendedBase.resume() + } + } + + private func handle(baseHasProducedElementOutput: ZipStateMachine.BaseHasProducedElementOutput) { + switch baseHasProducedElementOutput { + case .none: + break + + case .resumeDemand(let suspendedDemand, let result1, let result2, let result3): + suspendedDemand?.resume(returning: (result1, result2, result3)) + } + } + + private func handle(baseHasProducedFailureOutput: ZipStateMachine.BaseHasProducedFailureOutput) { + switch baseHasProducedFailureOutput { + case .none: + break + + case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let result1, let result2, let result3): + suspendedDemand?.resume(returning: (result1, result2, result3)) + suspendedBases.forEach { $0.resume() } + task?.cancel() + } + } + + private func handle(baseIsFinishedOutput: ZipStateMachine.BaseIsFinishedOutput) { + switch baseIsFinishedOutput { + case .terminate(let task, let suspendedBases, let suspendedDemands): + suspendedBases?.forEach { $0.resume() } + suspendedDemands?.forEach { $0?.resume(returning: nil) } + task?.cancel() + } + } + + deinit { + // clean the allocated resources and state + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.rootTaskIsCancelled() + } + + self.handle(rootTaskIsCancelledOutput: output) + } +} diff --git a/Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift b/Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift new file mode 100644 index 00000000..21faf292 --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift @@ -0,0 +1,439 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +struct Zip3StateMachine: Sendable +where Element1: Sendable, Element2: Sendable, Element3: Sendable { + typealias SuspendedDemand = UnsafeContinuation<(Result, Result, Result)?, Never> + + private enum State { + case initial + case awaitingDemandFromConsumer( + task: Task?, + suspendedBases: [UnsafeContinuation] + ) + case awaitingBaseResults( + task: Task?, + result1: Result?, + result2: Result?, + result3: Result?, + suspendedBases: [UnsafeContinuation], + suspendedDemand: SuspendedDemand? + ) + case finished + } + + private var state: State = .initial + + mutating func taskIsStarted( + task: Task, + suspendedDemand: SuspendedDemand + ) { + switch self.state { + case .initial: + self.state = .awaitingBaseResults( + task: task, + result1: nil, + result2: nil, + result3: nil, + suspendedBases: [], + suspendedDemand: suspendedDemand + ) + + default: + preconditionFailure("Inconsistent state, the task cannot start while the state is other than initial") + } + } + + enum NewDemandFromConsumerOutput { + case resumeBases(suspendedBases: [UnsafeContinuation]) + case startTask(suspendedDemand: SuspendedDemand) + case terminate(suspendedDemand: SuspendedDemand) + } + + mutating func newDemandFromConsumer(suspendedDemand: SuspendedDemand) -> NewDemandFromConsumerOutput { + switch self.state { + case .initial: + return .startTask(suspendedDemand: suspendedDemand) + + case .awaitingDemandFromConsumer(let task, let suspendedBases): + self.state = .awaitingBaseResults( + task: task, + result1: nil, + result2: nil, + result3: nil, + suspendedBases: [], + suspendedDemand: suspendedDemand + ) + return .resumeBases(suspendedBases: suspendedBases) + + case .awaitingBaseResults: + preconditionFailure("Inconsistent state, a demand is already suspended") + + case .finished: + return .terminate(suspendedDemand: suspendedDemand) + } + } + + enum NewLoopFromBaseOutput { + case none + case resumeBases(suspendedBases: [UnsafeContinuation]) + case terminate(suspendedBase: UnsafeContinuation) + } + + mutating func newLoopFromBase1(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, var suspendedBases): + precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") + suspendedBases.append(suspendedBase) + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + return .none + + case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): + precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") + if result1 != nil { + suspendedBases.append(suspendedBase) + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .resumeBases(suspendedBases: [suspendedBase]) + } + + case .finished: + return .terminate(suspendedBase: suspendedBase) + } + } + + mutating func newLoopFromBase2(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { + switch state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, var suspendedBases): + precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") + suspendedBases.append(suspendedBase) + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + return .none + + case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): + precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") + if result2 != nil { + suspendedBases.append(suspendedBase) + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .resumeBases(suspendedBases: [suspendedBase]) + } + + case .finished: + return .terminate(suspendedBase: suspendedBase) + } + } + + mutating func newLoopFromBase3(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { + switch state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, var suspendedBases): + precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") + suspendedBases.append(suspendedBase) + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + return .none + + case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): + precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") + if result3 != nil { + suspendedBases.append(suspendedBase) + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .resumeBases(suspendedBases: [suspendedBase]) + } + + case .finished: + return .terminate(suspendedBase: suspendedBase) + } + } + + enum BaseHasProducedElementOutput { + case none + case resumeDemand( + suspendedDemand: SuspendedDemand?, + result1: Result, + result2: Result, + result3: Result + ) + } + + mutating func base1HasProducedElement(element: Element1) -> BaseHasProducedElementOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, _, let result2, let result3, let suspendedBases, let suspendedDemand): + if let result2, let result3 { + self.state = .awaitingBaseResults( + task: task, + result1: .success(element), + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: nil + ) + return .resumeDemand(suspendedDemand: suspendedDemand, result1: .success(element), result2: result2, result3: result3) + } else { + self.state = .awaitingBaseResults( + task: task, + result1: .success(element), + result2: result2, + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } + + case .finished: + return .none + } + } + + mutating func base2HasProducedElement(element: Element2) -> BaseHasProducedElementOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, let result1, _, let result3, let suspendedBases, let suspendedDemand): + if let result1, let result3 { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: .success(element), + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: nil + ) + return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: .success(element), result3: result3) + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: .success(element), + result3: result3, + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } + + case .finished: + return .none + } + } + + mutating func base3HasProducedElement(element: Element3) -> BaseHasProducedElementOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, let result1, let result2, _, let suspendedBases, let suspendedDemand): + if let result1, let result2 { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: .success(element), + suspendedBases: suspendedBases, + suspendedDemand: nil + ) + return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: result2, result3: .success(element)) + } else { + self.state = .awaitingBaseResults( + task: task, + result1: result1, + result2: result2, + result3: .success(element), + suspendedBases: suspendedBases, + suspendedDemand: suspendedDemand + ) + return .none + } + + case .finished: + return .none + } + } + + enum BaseHasProducedFailureOutput { + case none + case resumeDemandAndTerminate( + task: Task?, + suspendedDemand: SuspendedDemand?, + suspendedBases: [UnsafeContinuation], + result1: Result, + result2: Result, + result3: Result + ) + } + + mutating func baseHasProducedFailure(error: any Error) -> BaseHasProducedFailureOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") + + case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .resumeDemandAndTerminate( + task: task, + suspendedDemand: suspendedDemand, + suspendedBases: suspendedBases, + result1: .failure(error), + result2: .failure(error), + result3: .failure(error) + ) + + case .finished: + return .none + } + } + + mutating func demandIsFulfilled() { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer: + preconditionFailure("Inconsistent state, results are not yet available to be acknowledged") + + case .awaitingBaseResults(let task, let result1, let result2, let result3, let suspendedBases, let suspendedDemand): + precondition(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") + precondition(result1 != nil && result2 != nil && result3 != nil, "Inconsistent state, all results are not yet available to be acknowledged") + self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) + + case .finished: + break + } + } + + enum RootTaskIsCancelledOutput { + case terminate( + task: Task?, + suspendedBases: [UnsafeContinuation]?, + suspendedDemands: [SuspendedDemand?]? + ) + } + + mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { + switch self.state { + case .initial: + assertionFailure("Inconsistent state, the task is not started") + self.state = .finished + return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) + + case .awaitingDemandFromConsumer(let task, let suspendedBases): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) + + case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) + + case .finished: + return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) + } + } + + enum BaseIsFinishedOutput { + case terminate( + task: Task?, + suspendedBases: [UnsafeContinuation]?, + suspendedDemands: [SuspendedDemand?]? + ) + } + + mutating func baseIsFinished() -> BaseIsFinishedOutput { + switch self.state { + case .initial: + preconditionFailure("Inconsistent state, the task is not started") + + case .awaitingDemandFromConsumer(let task, let suspendedBases): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) + + case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): + self.state = .finished + return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) + + case .finished: + return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) + } + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index 90c5dcfa..80e8caf8 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -21,7 +21,7 @@ final class TestZip2: XCTestCase { let actual = await Array(zip(a.async, b.async)) XCTAssertEqual(expected, actual) } - + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] @@ -30,7 +30,7 @@ final class TestZip2: XCTestCase { let actual = await Array(zip(a.async, b.async)) XCTAssertEqual(expected, actual) } - + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] @@ -39,7 +39,7 @@ final class TestZip2: XCTestCase { let actual = await Array(zip(a.async, b.async)) XCTAssertEqual(expected, actual) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_all_sequences_have_same_size() async { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -56,7 +56,7 @@ final class TestZip2: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] @@ -73,7 +73,7 @@ final class TestZip2: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] @@ -90,7 +90,7 @@ final class TestZip2: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_one_element_and_throws_when_first_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -112,7 +112,7 @@ final class TestZip2: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_one_element_and_throws_when_second_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -134,7 +134,7 @@ final class TestZip2: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") @@ -170,7 +170,7 @@ final class TestZip3: XCTestCase { let actual = await Array(zip(a.async, b.async, c.async)) XCTAssertEqual(expected, actual) } - + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] @@ -180,7 +180,7 @@ final class TestZip3: XCTestCase { let actual = await Array(zip(a.async, b.async, c.async)) XCTAssertEqual(expected, actual) } - + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] @@ -190,7 +190,7 @@ final class TestZip3: XCTestCase { let actual = await Array(zip(a.async, b.async, c.async)) XCTAssertEqual(expected, actual) } - + func test_zip_makes_sequence_equivalent_to_synchronous_zip_when_third_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -200,7 +200,7 @@ final class TestZip3: XCTestCase { let actual = await Array(zip(a.async, b.async, c.async)) XCTAssertEqual(expected, actual) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_all_sequences_have_same_size() async { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -218,7 +218,7 @@ final class TestZip3: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_first_is_longer() async { let a = [1, 2, 3, 4, 5] let b = ["a", "b", "c"] @@ -236,7 +236,7 @@ final class TestZip3: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_second_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c", "d", "e"] @@ -254,7 +254,7 @@ final class TestZip3: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_nil_next_element_when_iteration_is_finished_and_third_is_longer() async { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -272,7 +272,7 @@ final class TestZip3: XCTestCase { let pastEnd = await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_one_element_and_throws_when_first_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -295,7 +295,7 @@ final class TestZip3: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_one_element_and_throws_when_second_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -318,7 +318,7 @@ final class TestZip3: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_produces_one_element_and_throws_when_third_produces_one_element_and_throws() async throws { let a = [1, 2, 3] let b = ["a", "b", "c"] @@ -341,7 +341,7 @@ final class TestZip3: XCTestCase { let pastEnd = try await iterator.next() XCTAssertNil(pastEnd) } - + func test_zip_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") let source2 = Indefinite(value: "test2") From 081ca79161884a8d8aacd2dfea9dd89cd10ec2bf Mon Sep 17 00:00:00 2001 From: Doug Russell Date: Tue, 11 Oct 2022 14:54:17 -0400 Subject: [PATCH 059/149] Deprecation Housekeeping (#206) (#207) `withTaskCancellationHandler(_:, operation:)` -> `withTaskCancellationHandler(operation:onCancel:)` --- Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift | 12 ++++++------ Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift b/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift index 02cc26b7..86e875f8 100644 --- a/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift +++ b/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift @@ -24,12 +24,6 @@ where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: func next() async rethrows -> (Base1.Element, Base2.Element)? { try await withTaskCancellationHandler { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - // clean the allocated resources and state - self.handle(rootTaskIsCancelledOutput: output) - } operation: { let results = await withUnsafeContinuation { continuation in self.stateMachine.withCriticalRegion { stateMachine in let output = stateMachine.newDemandFromConsumer(suspendedDemand: continuation) @@ -59,6 +53,12 @@ where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: } return try (results.0._rethrowGet(), results.1._rethrowGet()) + } onCancel: { + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.rootTaskIsCancelled() + } + // clean the allocated resources and state + self.handle(rootTaskIsCancelledOutput: output) } } diff --git a/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift b/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift index ee393106..63e83fda 100644 --- a/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift +++ b/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift @@ -26,12 +26,6 @@ where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element)? { try await withTaskCancellationHandler { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - // clean the allocated resources and state - self.handle(rootTaskIsCancelledOutput: output) - } operation: { let results = await withUnsafeContinuation { continuation in self.stateMachine.withCriticalRegion { stateMachine in let output = stateMachine.newDemandFromConsumer(suspendedDemand: continuation) @@ -61,6 +55,12 @@ where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: } return try (results.0._rethrowGet(), results.1._rethrowGet(), results.2._rethrowGet()) + } onCancel: { + let output = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.rootTaskIsCancelled() + } + // clean the allocated resources and state + self.handle(rootTaskIsCancelledOutput: output) } } From 9d427ce1590e2767d5feade5419dd143c5968428 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 12 Oct 2022 09:25:11 -0700 Subject: [PATCH 060/149] Relax Sendable conditional conformance for AsyncJoinedBySeparatorSequence (#210) --- Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index 954f9e26..492d703c 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -141,7 +141,7 @@ public struct AsyncJoinedBySeparatorSequence Date: Wed, 12 Oct 2022 09:35:07 -0700 Subject: [PATCH 061/149] Simplify chunks algorithm by re-using merge and composition (#209) --- .../AsyncChunksOfCountOrSignalSequence.swift | 60 ++++--- .../AsyncAlgorithms/Merge2StateMachine.swift | 162 ------------------ 2 files changed, 40 insertions(+), 182 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/Merge2StateMachine.swift diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index e5b167c7..8e4ce7b5 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -56,35 +56,54 @@ extension AsyncSequence { } /// 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.AsyncIterator: Sendable, Signal.AsyncIterator: Sendable, Base.Element: Sendable, Signal.Element: Sendable { +public struct AsyncChunksOfCountOrSignalSequence: AsyncSequence, Sendable where Collected.Element == Base.Element, Base: Sendable, Signal: Sendable, Base.Element: Sendable, Signal.Element: Sendable { public typealias Element = Collected + enum Either { + case element(Base.Element) + case terminal + case signal + } + /// The iterator for a `AsyncChunksOfCountOrSignalSequence` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { + public struct Iterator: AsyncIteratorProtocol { + typealias EitherMappedBase = AsyncMapSequence + typealias EitherMappedSignal = AsyncMapSequence + typealias ChainedBase = AsyncChain2Sequence> + typealias Merged = AsyncMerge2Sequence + let count: Int? - var state: Merge2StateMachine - init(base: Base.AsyncIterator, count: Int?, signal: Signal.AsyncIterator) { + var iterator: Merged.AsyncIterator + var terminated = false + + init(iterator: Merged.AsyncIterator, count: Int?) { self.count = count - self.state = Merge2StateMachine(base, terminatesOnNil: true, signal) + self.iterator = iterator } public mutating func next() async rethrows -> Collected? { - var result : Collected? - while let next = try await state.next() { + guard !terminated else { + return nil + } + var result: Collected? + while let next = try await iterator.next() { switch next { - case .first(let element): - if result == nil { - result = Collected() - } - result!.append(element) - if result?.count == count { - return result - } - case .second(_): - if result != nil { - return result - } + case .element(let element): + if result == nil { + result = Collected() + } + result!.append(element) + if result?.count == count { + return result + } + case .terminal: + terminated = true + return result + case .signal: + if result != nil { + return result + } } } return result @@ -105,6 +124,7 @@ public struct AsyncChunksOfCountOrSignalSequence Iterator { - return Iterator(base: base.makeAsyncIterator(), count: count, signal: signal.makeAsyncIterator()) + + return Iterator(iterator: merge(chain(base.map { Either.element($0) }, [.terminal].async), signal.map { _ in Either.signal }).makeAsyncIterator(), count: count) } } diff --git a/Sources/AsyncAlgorithms/Merge2StateMachine.swift b/Sources/AsyncAlgorithms/Merge2StateMachine.swift deleted file mode 100644 index b0f15ab6..00000000 --- a/Sources/AsyncAlgorithms/Merge2StateMachine.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -struct Merge2StateMachine: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base1.Element: Sendable, Base2.Element: Sendable { - typealias Element1 = Base1.Element - typealias Element2 = Base2.Element - - let iter1TerminatesOnNil : Bool - let iter2terminatesOnNil : Bool - - enum Partial: @unchecked Sendable { - case first(Result, Base1.AsyncIterator) - case second(Result, Base2.AsyncIterator) - } - - enum Either { - case first(Base1.Element) - case second(Base2.Element) - } - - var state: (PartialIteration, PartialIteration) - - init(_ iterator1: Base1.AsyncIterator, terminatesOnNil iter1TerminatesOnNil: Bool = false, _ iterator2: Base2.AsyncIterator, terminatesOnNil iter2terminatesOnNil: Bool = false) { - self.iter1TerminatesOnNil = iter1TerminatesOnNil - self.iter2terminatesOnNil = iter2terminatesOnNil - state = (.idle(iterator1), .idle(iterator2)) - } - - mutating func apply(_ task1: Task.Partial, Never>?, _ task2: Task.Partial, Never>?) async rethrows -> Either? { - switch await Task.select([task1, task2].compactMap ({ $0 })).value { - case .first(let result, let iterator): - do { - guard let value = try state.0.resolve(result, iterator) else { - if iter1TerminatesOnNil { - state.1.cancel() - return nil - } - return try await next() - } - return .first(value) - } catch { - state.1.cancel() - throw error - } - case .second(let result, let iterator): - do { - guard let value = try state.1.resolve(result, iterator) else { - if iter2terminatesOnNil { - state.0.cancel() - return nil - } - return try await next() - } - return .second(value) - } catch { - state.0.cancel() - throw error - } - } - } - - func first(_ iterator1: Base1.AsyncIterator) -> Task { - Task { - var iter = iterator1 - do { - let value = try await iter.next() - return .first(.success(value), iter) - } catch { - return .first(.failure(error), iter) - } - } - } - - func second(_ iterator2: Base2.AsyncIterator) -> Task { - Task { - var iter = iterator2 - do { - let value = try await iter.next() - return .second(.success(value), iter) - } catch { - return .second(.failure(error), iter) - } - } - } - - /// Advances to the next element and returns it or `nil` if no next element exists. - mutating func next() async rethrows -> Either? { - if Task.isCancelled { - state.0.cancel() - state.1.cancel() - return nil - } - switch state { - case (.idle(let iterator1), .idle(let iterator2)): - let task1 = first(iterator1) - let task2 = second(iterator2) - state = (.pending(task1), .pending(task2)) - return try await apply(task1, task2) - case (.idle(let iterator1), .pending(let task2)): - let task1 = first(iterator1) - state = (.pending(task1), .pending(task2)) - return try await apply(task1, task2) - case (.pending(let task1), .idle(let iterator2)): - let task2 = second(iterator2) - state = (.pending(task1), .pending(task2)) - return try await apply(task1, task2) - case (.idle(var iterator1), .terminal): - do { - if let value = try await iterator1.next() { - state = (.idle(iterator1), .terminal) - return .first(value) - } else { - state = (.terminal, .terminal) - return nil - } - } catch { - state = (.terminal, .terminal) - throw error - } - case (.terminal, .idle(var iterator2)): - do { - if let value = try await iterator2.next() { - state = (.terminal, .idle(iterator2)) - return .second(value) - } else { - state = (.terminal, .terminal) - return nil - } - } catch { - state = (.terminal, .terminal) - throw error - } - case (.terminal, .pending(let task2)): - return try await apply(nil, task2) - case (.pending(let task1), .pending(let task2)): - return try await apply(task1, task2) - case (.pending(let task1), .terminal): - return try await apply(task1, nil) - case (.terminal, .terminal): - return nil - } - } -} - -extension Merge2StateMachine.Either where Base1.Element == Base2.Element { - var value : Base1.Element { - switch self { - case .first(let val): - return val - case .second(let val): - return val - } - } -} From c21ee2d706c2fd802c9676d40d874d7dee61c0cb Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 14 Oct 2022 16:39:53 +0100 Subject: [PATCH 062/149] Remove `Sendable` constraint for `AsyncAdjacentPairsSequence` (#212) # Motivation We want to remove any `Sendable` constraints on iterators since they ought to not be passed between `Task`s. # Modification Explicitly mark the `AsyncAdjacentPairsSequence` to be not `Sendable`. # Result Nobody can make this `Sendable`. --- Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 7a69bf35..2a37f918 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -83,5 +83,7 @@ extension AsyncSequence { } } -extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } -extension AsyncAdjacentPairsSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } +extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncAdjacentPairsSequence.Iterator: Sendable { } From be681c12e3a4f4bec91d32c1995840160fe5486d Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 14 Oct 2022 16:41:25 +0100 Subject: [PATCH 063/149] Remove `Sendable` constraint for `AsyncChain2Sequence` (#213) # Motivation We want to remove any `Sendable` constraints on iterators since they ought to not be passed between `Task`s. # Modification Explicitly mark the `AsyncChain2Sequence` to be not `Sendable`. # Result Nobody can make this `Sendable`. --- Sources/AsyncAlgorithms/AsyncChain2Sequence.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift index 59d3cfee..3e9b4c4f 100644 --- a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift @@ -80,4 +80,6 @@ extension AsyncChain2Sequence: AsyncSequence { } extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable { } -extension AsyncChain2Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { } + +@available(*, unavailable) +extension AsyncChain2Sequence.Iterator: Sendable { } From cf70e78632e990cd041fef21044e54fa5fdd1c56 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 14 Oct 2022 13:30:17 -0700 Subject: [PATCH 064/149] Convert the chain guide to a proposal (#188) --- Evolution/NNNN-chain.md | 80 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 Evolution/NNNN-chain.md diff --git a/Evolution/NNNN-chain.md b/Evolution/NNNN-chain.md new file mode 100644 index 00000000..4ef7e958 --- /dev/null +++ b/Evolution/NNNN-chain.md @@ -0,0 +1,80 @@ +# Chain + +* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-chain.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] + +## Introduction + +Combining asynchronous sequences can occur in multiple ways. One such way that is common for non asynchronous sequences is iterating a prefix sequence and then iterating a suffix sequence. The asynchronous version is just as useful and common. This algorithm has been dubbed `chain` in the swift algorithms package. + +The chain algorithm brings together two or more asynchronous sequences together sequentially where the elements from the resulting asynchronous sequence are comprised in order from the elements of the first asynchronous sequence and then the second (and so on) or until an error occurs. + +This operation is available for all `AsyncSequence` types who share the same `Element` type. + +```swift +let preamble = [ + "// Some header to add as a preamble", + "//", + "" +].async +let lines = chain(preamble, URL(fileURLWithPath: "/tmp/Sample.swift").lines) + +for try await line in lines { + print(line) +} +``` + +The above example shows how two `AsyncSequence` types can be chained together. In this case it prepends a preamble to the `lines` content of the file. + +## Detailed Design + +This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. + +```swift +public func chain(_ s1: Base1, _ s2: Base2) -> AsyncChain2Sequence where Base1.Element == Base2.Element + +public func chain(_ s1: Base1, _ s2: Base2, _ s3: Base3) -> AsyncChain3Sequence + +public struct AsyncChain2Sequence where Base1.Element == Base2.Element { + public typealias Element = Base1.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable { } +extension AsyncChain2Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { } + +public struct AsyncChain3Sequence where Base1.Element == Base2.Element, Base1.Element == Base3.Element { + public typealias Element = Base1.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } +extension AsyncChain3Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { } +``` + +The `chain(_:...)` function takes two or more sequences as arguments. + +The resulting `AsyncChainSequence` type is an asynchronous sequence, with conditional conformance to `Sendable` when the arguments also conform to it. + +When any of the asynchronous sequences being chained together come to their end of iteration, the `AsyncChainSequence` iteration proceeds to the next asynchronous sequence. When the last asynchronous sequence reaches the end of iteration, the `AsyncChainSequence` then ends its iteration. + +At any point in time, if one of the comprising asynchronous sequences throws an error during iteration, the resulting `AsyncChainSequence` iteration will throw that error and end iteration. The throwing behavior of `AsyncChainSequence` is that it will throw when any of its comprising bases throw, and will not throw when all of its comprising bases do not throw. + +### Naming + +This function's and type's name match the term of art used in other languages and libraries. + +This combinator function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md). From 3890885e80fc1553a973975c33cbd7ce7437580d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 18 Oct 2022 13:06:34 -0700 Subject: [PATCH 065/149] Renumber chain's proposal --- Evolution/{NNNN-chain.md => 0007-chain.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Evolution/{NNNN-chain.md => 0007-chain.md} (97%) diff --git a/Evolution/NNNN-chain.md b/Evolution/0007-chain.md similarity index 97% rename from Evolution/NNNN-chain.md rename to Evolution/0007-chain.md index 4ef7e958..8df4804f 100644 --- a/Evolution/NNNN-chain.md +++ b/Evolution/0007-chain.md @@ -1,6 +1,6 @@ # Chain -* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-chain.md) +* Proposal: [SAA-0007](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0007-chain.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift) | From f32328ac6f23a8e02c86d57ffb79bdb00efb1b21 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 18 Oct 2022 13:13:45 -0700 Subject: [PATCH 066/149] Convert the bytes iterator guide to a proposal (#187) --- Evolution/NNNN-bytes.md | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Evolution/NNNN-bytes.md diff --git a/Evolution/NNNN-bytes.md b/Evolution/NNNN-bytes.md new file mode 100644 index 00000000..5fd56c2d --- /dev/null +++ b/Evolution/NNNN-bytes.md @@ -0,0 +1,59 @@ +# AsyncBufferedByteIterator + +* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-bytes.md) +* Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] + +## Introduction + +Sources of bytes are a common point of asynchrony; reading from files, reading from the network, or other such tasks. Having an easy to use, uniform, and performant utility to make this approachable is key to unlocking highly scalable byte handling. This has proven useful for `FileHandle`, `URL`, and a number of others in Foundation. + +This type provides infrastructure for creating `AsyncSequence` types with an `Element` of `UInt8` backed by file descriptors or similar read sources. + +```swift +struct AsyncBytes: AsyncSequence { + public typealias Element = UInt8 + var handle: ReadableThing + + internal init(_ readable: ReadableThing) { + handle = readable + } + + public func makeAsyncIterator() -> AsyncBufferedByteIterator { + return AsyncBufferedByteIterator(capacity: 16384) { buffer in + // This runs once every 16384 invocations of next() + return try await handle.read(into: buffer) + } + } +} +``` + +## Detailed Design + +```swift +public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { + public typealias Element = UInt8 + + public init( + capacity: Int, + readFunction: @Sendable @escaping (UnsafeMutableRawBufferPointer) async throws -> Int + ) + + public mutating func next() async throws -> UInt8? +} +``` + +For each invocation of `next`, the iterator will check if a buffer has been filled. If the buffer is filled with some amount of bytes, a fast path is taken to directly return a byte out of that buffer. If the buffer is not filled, the read function is invoked to acquire the next filled buffer, at which point it takes a byte out of that buffer. + +If the read function returns `0`, indicating it didn't read any more bytes, the iterator is decided to be finished and no additional invocations to the read function are made. + +If the read function throws, the error will be thrown by the iteration. Subsequent invocations to the iterator will then return `nil` without invoking the read function. + +If the task is cancelled during the iteration, the iteration will check the cancellation only in passes where the read function is invoked, and will throw a `CancellationError`. + +### Naming + +This type was named precisely for what it does: it is an asynchronous iterator that buffers bytes. + From cc0621eb1bb3ae0e6dd0d51beedbdb1f655c911e Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 18 Oct 2022 13:14:44 -0700 Subject: [PATCH 067/149] Renumber bytes's proposal --- Evolution/{NNNN-bytes.md => 0008-bytes.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Evolution/{NNNN-bytes.md => 0008-bytes.md} (96%) diff --git a/Evolution/NNNN-bytes.md b/Evolution/0008-bytes.md similarity index 96% rename from Evolution/NNNN-bytes.md rename to Evolution/0008-bytes.md index 5fd56c2d..76e2f3a0 100644 --- a/Evolution/NNNN-bytes.md +++ b/Evolution/0008-bytes.md @@ -1,6 +1,6 @@ # AsyncBufferedByteIterator -* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-bytes.md) +* Proposal: [SAA-0008](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0008-bytes.md) * Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | From 2cfb16691a9e4339d8d5ae67f16d3f99aa6e674d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 20 Oct 2022 09:34:05 -0700 Subject: [PATCH 068/149] Task.select is no longer used and can potentially impact performance negatively (#217) --- README.md | 5 - .../AsyncAlgorithms.docc/AsyncAlgorithms.md | 1 - .../AsyncAlgorithms.docc/Guides/Select.md | 53 ---------- Sources/AsyncAlgorithms/TaskSelect.swift | 78 --------------- .../AsyncAlgorithmsTests/TestTaskSelect.swift | 99 ------------------- 5 files changed, 236 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md delete mode 100644 Sources/AsyncAlgorithms/TaskSelect.swift delete mode 100644 Tests/AsyncAlgorithmsTests/TestTaskSelect.swift diff --git a/README.md b/README.md index cde1208f..2324a873 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,6 @@ This package is the home for these APIs. Development and API design take place o - [`Dictionary.init(_:uniquingKeysWith:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): 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. - [`Dictionary.init(grouping:by:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): 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. - [`SetAlgebra.init(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Collections.md): Creates a new set from an asynchronous sequence of items. - - -#### Task management - -- [`Task.select(_:)`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md): Determine the first task to complete of a sequence of tasks. #### Effects diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md index 8a0cbbf7..1f18bc02 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md @@ -30,7 +30,6 @@ This package has three main goals: - - - -- - - - diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md deleted file mode 100644 index fb0389cc..00000000 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Select.md +++ /dev/null @@ -1,53 +0,0 @@ -# Task.select - -* Author(s): [Philippe Hausler](https://github.com/phausler) - -[ -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/TaskSelect.swift) | -[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift) -] - -## Introduction - -A fundamental part of many algorithms is being able to select the first resolved task from a given list of active tasks. This enables algorithms like `debounce` or `merge`. - -## Proposed Solution - -Selecting the first task to complete from a list of active tasks is a similar algorithm to `select(2)`. This has similar behavior to `TaskGroup` except that instead of child tasks this function transacts upon already running tasks and does not cancel them upon completion of the selection and does not need to await for the completion of all of the tasks in the list to select. - -```swift -extension Task { - public static func select( - _ tasks: Tasks - ) async -> Task - - public static func select( - _ tasks: Task... - ) async -> Task -} -``` - -## Detailed Design - -Given any number of `Task` objects that share the same `Success` and `Failure` types; `Task.select` will suspend and await each tasks result and resume when the first task has produced a result. While the calling task of `Task.select` is suspended if that task is cancelled the tasks being selected receive the cancel. This is similar to the family of `TaskGroup` with a few behavioral and structural differences. - -The `withTaskGroup` API will create efficient child tasks. -The `Task.select` API takes pre-existing tasks. - -The `withTaskGroup` API will await all child tasks to be finished before returning. -The `Task.select` API will await for the first task to be finished before returning. - -The `withTaskGroup` API will cancel all outstanding child tasks upon awaiting its return. -The `Task.select` API will let the non selected tasks keep on running. - -The `withTaskGroup` can support having 0 child tasks. -The `Task.select` API requires at least 1 task to select over, anything less is a programmer error. - -This means that `withTaskGroup` is highly suited to run work in parallel, whereas `Task.select` is intended to find the first task that provides a value. There is inherent additional cost to the non-child tasks so `Task.select` should not be used as a replacement for anywhere that is more suitable as a group, but offers more potential for advanced algorithms. - -## Alternatives Considered - -## Future Directions - - -## Credits/Inspiration diff --git a/Sources/AsyncAlgorithms/TaskSelect.swift b/Sources/AsyncAlgorithms/TaskSelect.swift deleted file mode 100644 index b6208248..00000000 --- a/Sources/AsyncAlgorithms/TaskSelect.swift +++ /dev/null @@ -1,78 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -struct TaskSelectState: Sendable { - var complete = false - var tasks: [Task]? = [] - - mutating func add(_ task: Task) -> Task? { - if var tasks = tasks { - tasks.append(task) - self.tasks = tasks - return nil - } else { - return task - } - } -} - -extension Task { - /// Determine the first task to complete of a sequence of tasks. - /// - /// - Parameters: - /// - tasks: The running tasks to obtain a result from - /// - Returns: The first task to complete from the running tasks - public static func select( - _ tasks: Tasks - ) async -> Task - where Tasks.Element == Task { - let state = ManagedCriticalState(TaskSelectState()) - return await withTaskCancellationHandler { - await withUnsafeContinuation { continuation in - for task in tasks { - Task { - _ = await task.result - let winner = state.withCriticalRegion { state -> Bool in - defer { state.complete = true } - return !state.complete - } - if winner { - continuation.resume(returning: task) - } - } - state.withCriticalRegion { state in - state.add(task) - }?.cancel() - } - } - } onCancel: { - let tasks = state.withCriticalRegion { state -> [Task] in - defer { state.tasks = nil } - return state.tasks ?? [] - } - for task in tasks { - task.cancel() - } - } - } - - /// Determine the first task to complete of a list of tasks. - /// - /// - Parameters: - /// - tasks: The running tasks to obtain a result from - /// - Returns: The first task to complete from the running tasks - public static func select( - _ tasks: Task... - ) async -> Task { - await select(tasks) - } -} - diff --git a/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift b/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift deleted file mode 100644 index a3b49629..00000000 --- a/Tests/AsyncAlgorithmsTests/TestTaskSelect.swift +++ /dev/null @@ -1,99 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -@preconcurrency import XCTest -import Dispatch -import AsyncAlgorithms - -final class TestTaskSelect: XCTestCase { - func test_first() async { - let firstValue = await Task.select(Task { - return 1 - }, Task { - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - try! await Task.sleep(until: .now + .seconds(2), clock: .continuous) - } else { - try! await Task.sleep(nanoseconds: 2_000_000_000) - } - return 2 - }).value - XCTAssertEqual(firstValue, 1) - } - - func test_second() async { - let firstValue = await Task.select(Task { - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - try! await Task.sleep(until: .now + .seconds(2), clock: .continuous) - } else { - try! await Task.sleep(nanoseconds: 2_000_000_000) - } - return 1 - }, Task { - return 2 - }).value - XCTAssertEqual(firstValue, 2) - } - - func test_throwing() async { - do { - _ = try await Task.select(Task { () async throws -> Int in - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - try await Task.sleep(until: .now + .seconds(2), clock: .continuous) - } else { - try await Task.sleep(nanoseconds: 2_000_000_000) - } - return 1 - }, Task { () async throws -> Int in - throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) - }).value - XCTFail() - } catch { - XCTAssertEqual((error as NSError).code, -1) - } - } - - func test_cancellation() async { - let firstReady = expectation(description: "first ready") - let secondReady = expectation(description: "second ready") - let firstCancelled = expectation(description: "first cancelled") - let secondCancelled = expectation(description: "second cancelled") - let task = Task { - _ = await Task.select(Task { - await withTaskCancellationHandler { () -> Int in - firstReady.fulfill() - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) - } else { - try? await Task.sleep(nanoseconds: 2_000_000_000) - } - return 1 - } onCancel: { - firstCancelled.fulfill() - } - }, Task { - await withTaskCancellationHandler { () -> Int in - secondReady.fulfill() - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - try? await Task.sleep(until: .now + .seconds(2), clock: .continuous) - } else { - try? await Task.sleep(nanoseconds: 2_000_000_000) - } - return 1 - } onCancel: { - secondCancelled.fulfill() - } - }) - } - wait(for: [firstReady, secondReady], timeout: 1.0) - task.cancel() - wait(for: [firstCancelled, secondCancelled], timeout: 1.0) - } -} From 6c6ed33514ace1b569ff122c4506a917f4aec894 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 20 Oct 2022 17:34:24 +0100 Subject: [PATCH 069/149] Remove Sendability from all Iterators (#219) # Motivation We have been discussing this a lot in the other PRs, but in general said that an `AsyncIterator` is the connection between the consuming task and the `AsyncSequence`. If an `AsyncIterator` would be `Sendable` then we don't have that relationship anymore. Therefore, no iterator of an `AsyncIterator` should be `Sendable`. # Modification Remove all `Sendable` conformances of the iterators even the conditional ones. We can always add them back later. --- .../AsyncAlgorithms.docc/Guides/Effects.md | 56 +++++++++---------- .../AsyncAlgorithms/AsyncBufferSequence.swift | 1 - .../AsyncAlgorithms/AsyncChain3Sequence.swift | 1 - .../AsyncCompactedSequence.swift | 1 - .../AsyncExclusiveReductionsSequence.swift | 1 - .../AsyncInclusiveReductionsSequence.swift | 1 - .../AsyncInterspersedSequence.swift | 2 - .../AsyncJoinedBySeparatorSequence.swift | 6 -- .../AsyncAlgorithms/AsyncJoinedSequence.swift | 4 -- .../AsyncAlgorithms/AsyncLazySequence.swift | 1 - .../AsyncRemoveDuplicatesSequence.swift | 2 - .../AsyncThrottleSequence.swift | 3 - ...cThrowingExclusiveReductionsSequence.swift | 1 - ...cThrowingInclusiveReductionsSequence.swift | 1 - .../AsyncAlgorithms/AsyncTimerSequence.swift | 3 - .../Support/GatedSequence.swift | 1 - 16 files changed, 28 insertions(+), 57 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md index 766d54de..5d7a5967 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md @@ -1,59 +1,59 @@ | Type | Throws | Sendability | |-----------------------------------------------------|--------------|-------------| | `AsyncAdjacentPairsSequence` | rethrows | Conditional | -| `AsyncBufferedByteIterator` | throws | Sendable | +| `AsyncBufferedByteIterator` | throws | Not Sendable| | `AsyncBufferSequence` | rethrows | Conditional | -| `AsyncBufferSequence.Iterator` | rethrows | Conditional | +| `AsyncBufferSequence.Iterator` | rethrows | Not Sendable| | `AsyncChain2Sequence` | rethrows | Conditional | -| `AsyncChain2Sequence.Iterator` | rethrows | Conditional | +| `AsyncChain2Sequence.Iterator` | rethrows | Not Sendable| | `AsyncChain3Sequence` | rethrows | Conditional | -| `AsyncChain3Sequence.Iterator` | rethrows | Conditional | +| `AsyncChain3Sequence.Iterator` | rethrows | Not Sendable| | `AsyncChannel` | non-throwing | Sendable | -| `AsyncChannel.Iterator` | non-throwing | Sendable | +| `AsyncChannel.Iterator` | non-throwing | Not Sendable| | `AsyncChunkedByGroupSequence` | rethrows | Conditional | -| `AsyncChunkedByGroupSequence.Iterator` | rethrows | Conditional | +| `AsyncChunkedByGroupSequence.Iterator` | rethrows | Not Sendable| | `AsyncChunkedOnProjectionSequence` | rethrows | Conditional | -| `AsyncChunkedOnProjectionSequence.Iterator` | rethrows | Conditional | +| `AsyncChunkedOnProjectionSequence.Iterator` | rethrows | Not Sendable| | `AsyncChunksOfCountOrSignalSequence` | rethrows | Sendable | -| `AsyncChunksOfCountOrSignalSequence.Iterator` | rethrows | Sendable | +| `AsyncChunksOfCountOrSignalSequence.Iterator` | rethrows | Not Sendable| | `AsyncChunksOfCountSequence` | rethrows | Conditional | -| `AsyncChunksOfCountSequence.Iterator` | rethrows | Conditional | +| `AsyncChunksOfCountSequence.Iterator` | rethrows | Not Sendable| | `AsyncCombineLatest2Sequence` | rethrows | Sendable | -| `AsyncCombineLatest2Sequence.Iterator` | rethrows | Sendable | +| `AsyncCombineLatest2Sequence.Iterator` | rethrows | Not Sendable| | `AsyncCombineLatest3Sequence` | rethrows | Sendable | -| `AsyncCombineLatest3Sequence.Iterator` | rethrows | Sendable | +| `AsyncCombineLatest3Sequence.Iterator` | rethrows | Not Sendable| | `AsyncCompactedSequence` | rethrows | Conditional | -| `AsyncCompactedSequence.Iterator` | rethrows | Conditional | +| `AsyncCompactedSequence.Iterator` | rethrows | Not Sendable| | `AsyncDebounceSequence` | rethrows | Sendable | -| `AsyncDebounceSequence.Iterator` | rethrows | Sendable | +| `AsyncDebounceSequence.Iterator` | rethrows | Not Sendable| | `AsyncExclusiveReductionsSequence` | rethrows | Conditional | -| `AsyncExclusiveReductionsSequence.Iterator` | rethrows | Conditional | +| `AsyncExclusiveReductionsSequence.Iterator` | rethrows | Not Sendable| | `AsyncInclusiveReductionsSequence` | rethrows | Conditional | -| `AsyncInclusiveReductionsSequence.Iterator` | rethrows | Conditional | +| `AsyncInclusiveReductionsSequence.Iterator` | rethrows | Not Sendable| | `AsyncInterspersedSequence` | rethrows | Conditional | -| `AsyncInterspersedSequence.Iterator` | rethrows | Conditional | +| `AsyncInterspersedSequence.Iterator` | rethrows | Not Sendable| | `AsyncJoinedSequence` | rethrows | Conditional | -| `AsyncJoinedSequence.Iterator` | rethrows | Conditional | +| `AsyncJoinedSequence.Iterator` | rethrows | Not Sendable| | `AsyncLazySequence` | non-throwing | Conditional | -| `AsyncLazySequence.Iterator` | non-throwing | Conditional | +| `AsyncLazySequence.Iterator` | non-throwing | Not Sendable| | `AsyncLimitBuffer` | non-throwing | Sendable | | `AsyncMerge2Sequence` | rethrows | Sendable | -| `AsyncMerge2Sequence.Iterator` | rethrows | Sendable | +| `AsyncMerge2Sequence.Iterator` | rethrows | Not Sendable| | `AsyncMerge3Sequence` | rethrows | Sendable | -| `AsyncMerge3Sequence.Iterator` | rethrows | Sendable | +| `AsyncMerge3Sequence.Iterator` | rethrows | Not Sendable| | `AsyncRemoveDuplicatesSequence` | rethrows | Conditional | -| `AsyncRemoveDuplicatesSequence.Iterator` | rethrows | Conditional | +| `AsyncRemoveDuplicatesSequence.Iterator` | rethrows | Not Sendable| | `AsyncThrottleSequence` | rethrows | Conditional | -| `AsyncThrottleSequence.Iterator` | rethrows | Conditional | +| `AsyncThrottleSequence.Iterator` | rethrows | Not Sendable| | `AsyncThrowingChannel` | throws | Sendable | -| `AsyncThrowingChannel.Iterator` | throws | Sendable | +| `AsyncThrowingChannel.Iterator` | throws | Not Sendable| | `AsyncThrowingExclusiveReductionsSequence` | throws | Conditional | -| `AsyncThrowingExclusiveReductionsSequence.Iterator` | throws | Conditional | +| `AsyncThrowingExclusiveReductionsSequence.Iterator` | throws | Not Sendable| | `AsyncThrowingInclusiveReductionsSequence` | throws | Conditional | -| `AsyncThrowingInclusiveReductionsSequence.Iterator` | throws | Conditional | +| `AsyncThrowingInclusiveReductionsSequence.Iterator` | throws | Not Sendable| | `AsyncTimerSequence` | non-throwing | Sendable | -| `AsyncTimerSequence.Iterator` | non-throwing | Sendable | +| `AsyncTimerSequence.Iterator` | non-throwing | Not Sendable| | `AsyncZip2Sequence` | rethrows | Sendable | -| `AsyncZip2Sequence.Iterator` | rethrows | Sendable | +| `AsyncZip2Sequence.Iterator` | rethrows | Not Sendable| | `AsyncZip3Sequence` | rethrows | Sendable | -| `AsyncZip3Sequence.Iterator` | rethrows | Sendable | +| `AsyncZip3Sequence.Iterator` | rethrows | Not Sendable| diff --git a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift index 2b43fae2..96fad74c 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift @@ -235,7 +235,6 @@ public struct AsyncBufferSequence wher } extension AsyncBufferSequence: Sendable where Base: Sendable { } -extension AsyncBufferSequence.Iterator: Sendable where Base: Sendable { } extension AsyncBufferSequence: AsyncSequence { public typealias Element = Buffer.Output diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index 8185a223..1c275a2c 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -95,4 +95,3 @@ extension AsyncChain3Sequence: AsyncSequence { } extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } -extension AsyncChain3Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift b/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift index 0cfacece..5b96bc9f 100644 --- a/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift @@ -69,4 +69,3 @@ extension AsyncSequence { } extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } -extension AsyncCompactedSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index a0998ef3..0d56c593 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -112,4 +112,3 @@ extension AsyncExclusiveReductionsSequence: AsyncSequence { } extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } -extension AsyncExclusiveReductionsSequence.Iterator: Sendable where Base.AsyncIterator: Sendable, Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index cf52cb86..12611c14 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -85,4 +85,3 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { } extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable { } -extension AsyncInclusiveReductionsSequence.Iterator: Sendable where Base.AsyncIterator: Sendable, Base.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift index d50d721e..8d8e1b5b 100644 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift @@ -100,5 +100,3 @@ extension AsyncInterspersedSequence: AsyncSequence { } extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } -extension AsyncInterspersedSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } -extension AsyncInterspersedSequence.Iterator.State: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index 492d703c..e401eea2 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -142,9 +142,3 @@ public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base extension AsyncJoinedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable, Base.AsyncIterator: Sendable, Base.Element.AsyncIterator: Sendable { } -extension AsyncJoinedSequence.Iterator: Sendable -where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable, Base.AsyncIterator: Sendable, Base.Element.AsyncIterator: Sendable { } -extension AsyncJoinedSequence.Iterator.State: Sendable -where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable, Base.AsyncIterator: Sendable, Base.Element.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncLazySequence.swift b/Sources/AsyncAlgorithms/AsyncLazySequence.swift index 9c2aa1e2..9a8abd77 100644 --- a/Sources/AsyncAlgorithms/AsyncLazySequence.swift +++ b/Sources/AsyncAlgorithms/AsyncLazySequence.swift @@ -67,4 +67,3 @@ public struct AsyncLazySequence: AsyncSequence { } extension AsyncLazySequence: Sendable where Base: Sendable { } -extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index 60f3aa42..5fcd095f 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -86,7 +86,6 @@ public struct AsyncRemoveDuplicatesSequence: AsyncSequence } extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } -extension AsyncRemoveDuplicatesSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } /// An asynchronous sequence that omits repeated elements by testing them with an error-throwing predicate. @@ -145,4 +144,3 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS } extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } -extension AsyncThrowingRemoveDuplicatesSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index ad590afa..4832b9ac 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -102,6 +102,3 @@ extension AsyncThrottleSequence: AsyncSequence { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence.Iterator: Sendable where Base.AsyncIterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index 303c21cb..4b12c2c3 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -119,4 +119,3 @@ extension AsyncThrowingExclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } -extension AsyncThrowingExclusiveReductionsSequence.Iterator: Sendable where Base.AsyncIterator: Sendable, Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 673d7c51..36e88fb5 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -89,4 +89,3 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable { } -extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable where Base.AsyncIterator: Sendable, Base.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index 795cfa4e..fe3b58f6 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -89,6 +89,3 @@ extension AsyncTimerSequence where C == SuspendingClock { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence: Sendable { } - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncTimerSequence.Iterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift b/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift index 50ede106..71c618b4 100644 --- a/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift @@ -52,4 +52,3 @@ extension GatedSequence: AsyncSequence { } extension GatedSequence: Sendable where Element: Sendable { } -extension GatedSequence.Iterator: Sendable where Element: Sendable { } From 8f5e8cee4239862ededa4adfaa9748aa7db26a9c Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 20 Oct 2022 19:49:13 +0100 Subject: [PATCH 070/149] Merge the zip2 and zip3 implementations (#221) # Motivation We recently merged https://github.com/apple/swift-async-algorithms/pull/201 which removed the `Sendable` constraint of the `AsyncIterator` from `zip`. There were some left over comments that we wanted to fix in a follow up. Furthermore, we also can merge the implementations of `zip2` and `zip3` relatively easily. The merging also aids us in understanding what we need from the variadic generics proposals and allows us to give feedback since `zip` is non-trivial to implement with variadic generics. Lastly, the merged state machine will be a good base for the overhaul of `combineLatest`. # Modification This PR merges the state machines from `zip2` and `zip3` into a single one. Furthermore, it addresses some of the open feedback from the last PR. # Result We now have a single state machine which is a good foundation for our changes to `combineLatest`. --- .../Zip/AsyncZip2Sequence.swift | 35 +- .../Zip/AsyncZip3Sequence.swift | 36 +- Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift | 212 ------- .../Zip/Zip2StateMachine.swift | 367 ------------ Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift | 252 -------- .../Zip/Zip3StateMachine.swift | 439 -------------- .../AsyncAlgorithms/Zip/ZipStateMachine.swift | 548 ++++++++++++++++++ Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 320 ++++++++++ 8 files changed, 921 insertions(+), 1288 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift delete mode 100644 Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift delete mode 100644 Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift delete mode 100644 Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Zip/ZipStorage.swift diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index 292a612d..0ef591ba 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 -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 @@ -34,21 +34,38 @@ where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: } public func makeAsyncIterator() -> AsyncIterator { - Iterator( - base1, - base2 - ) + Iterator(storage: .init(self.base1, self.base2, nil)) } public struct Iterator: AsyncIteratorProtocol { - let runtime: Zip2Runtime + final class InternalClass { + private let storage: ZipStorage - init(_ base1: Base1, _ base2: Base2) { - self.runtime = Zip2Runtime(base1, base2) + fileprivate init(storage: ZipStorage) { + self.storage = storage + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + guard let element = try await self.storage.next() else { + return nil + } + + return (element.0, element.1) + } + } + + let internalClass: InternalClass + + fileprivate init(storage: ZipStorage) { + self.internalClass = InternalClass(storage: storage) } public mutating func next() async rethrows -> Element? { - try await self.runtime.next() + try await self.internalClass.next() } } } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 4a52158e..43317aca 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -22,7 +22,7 @@ public func zip: AsyncSequence -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 { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public typealias AsyncIterator = Iterator @@ -37,22 +37,40 @@ where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: } public func makeAsyncIterator() -> AsyncIterator { - Iterator( - base1, - base2, - base3 + Iterator(storage: .init(self.base1, self.base2, self.base3) ) } public struct Iterator: AsyncIteratorProtocol { - let runtime: Zip3Runtime + final class InternalClass { + private let storage: ZipStorage - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.runtime = Zip3Runtime(base1, base2, base3) + fileprivate init(storage: ZipStorage) { + self.storage = storage + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + guard let element = try await self.storage.next() else { + return nil + } + + // This force unwrap is safe since there must be a third element. + return (element.0, element.1, element.2!) + } + } + + let internalClass: InternalClass + + fileprivate init(storage: ZipStorage) { + self.internalClass = InternalClass(storage: storage) } public mutating func next() async rethrows -> Element? { - try await self.runtime.next() + try await self.internalClass.next() } } } diff --git a/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift b/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift deleted file mode 100644 index 86e875f8..00000000 --- a/Sources/AsyncAlgorithms/Zip/Zip2Runtime.swift +++ /dev/null @@ -1,212 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -final class Zip2Runtime: Sendable -where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { - typealias ZipStateMachine = Zip2StateMachine - - private let stateMachine = ManagedCriticalState(ZipStateMachine()) - private let base1: Base1 - private let base2: Base2 - - init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } - - func next() async rethrows -> (Base1.Element, Base2.Element)? { - try await withTaskCancellationHandler { - let results = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let output = stateMachine.newDemandFromConsumer(suspendedDemand: continuation) - switch output { - case .startTask(let suspendedDemand): - // first iteration, we start one task per base to iterate over them - self.startTask(stateMachine: &stateMachine, suspendedDemand: suspendedDemand) - - case .resumeBases(let suspendedBases): - // bases can be iterated over for 1 iteration so their next value can be retrieved - suspendedBases.forEach { $0.resume() } - - case .terminate(let suspendedDemand): - // the async sequence is already finished, immediately resuming - suspendedDemand.resume(returning: nil) - } - } - } - - guard let results else { - return nil - } - - self.stateMachine.withCriticalRegion { stateMachine in - // acknowledging the consumption of the zipped values, so we can begin another iteration on the bases - stateMachine.demandIsFulfilled() - } - - return try (results.0._rethrowGet(), results.1._rethrowGet()) - } onCancel: { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - // clean the allocated resources and state - self.handle(rootTaskIsCancelledOutput: output) - } - } - - private func handle(rootTaskIsCancelledOutput: ZipStateMachine.RootTaskIsCancelledOutput) { - switch rootTaskIsCancelledOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func startTask( - stateMachine: inout ZipStateMachine, - suspendedDemand: ZipStateMachine.SuspendedDemand - ) { - let task = Task { - await withTaskGroup(of: Void.self) { group in - group.addTask { - var base1Iterator = self.base1.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { continuation in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase1(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element1 = try await base1Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base1HasProducedElement(element: element1) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - - group.addTask { - var base2Iterator = self.base2.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { continuation in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase2(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element2 = try await base2Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base2HasProducedElement(element: element2) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - } - } - stateMachine.taskIsStarted(task: task, suspendedDemand: suspendedDemand) - } - - private func handle(newLoopFromBaseOutput: ZipStateMachine.NewLoopFromBaseOutput) { - switch newLoopFromBaseOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let suspendedBase): - suspendedBase.resume() - } - } - - private func handle(baseHasProducedElementOutput: ZipStateMachine.BaseHasProducedElementOutput) { - switch baseHasProducedElementOutput { - case .none: - break - - case .resumeDemand(let suspendedDemand, let result1, let result2): - suspendedDemand?.resume(returning: (result1, result2)) - } - } - - private func handle(baseHasProducedFailureOutput: ZipStateMachine.BaseHasProducedFailureOutput) { - switch baseHasProducedFailureOutput { - case .none: - break - - case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let result1, let result2): - suspendedDemand?.resume(returning: (result1, result2)) - suspendedBases.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseIsFinishedOutput: ZipStateMachine.BaseIsFinishedOutput) { - switch baseIsFinishedOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - deinit { - // clean the allocated resources and state - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } -} diff --git a/Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift b/Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift deleted file mode 100644 index 19ebc11f..00000000 --- a/Sources/AsyncAlgorithms/Zip/Zip2StateMachine.swift +++ /dev/null @@ -1,367 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -struct Zip2StateMachine: Sendable -where Element1: Sendable, Element2: Sendable { - typealias SuspendedDemand = UnsafeContinuation<(Result, Result)?, Never> - - private enum State { - case initial - case awaitingDemandFromConsumer( - task: Task?, - suspendedBases: [UnsafeContinuation] - ) - case awaitingBaseResults( - task: Task?, - result1: Result?, - result2: Result?, - suspendedBases: [UnsafeContinuation], - suspendedDemand: SuspendedDemand? - ) - case finished - } - - private var state: State = .initial - - mutating func taskIsStarted( - task: Task, - suspendedDemand: SuspendedDemand - ) { - switch self.state { - case .initial: - self.state = .awaitingBaseResults( - task: task, - result1: nil, - result2: nil, - suspendedBases: [], - suspendedDemand: suspendedDemand - ) - - default: - preconditionFailure("Inconsistent state, the task cannot start while the state is other than initial") - } - } - - enum NewDemandFromConsumerOutput { - case resumeBases(suspendedBases: [UnsafeContinuation]) - case startTask(suspendedDemand: SuspendedDemand) - case terminate(suspendedDemand: SuspendedDemand) - } - - mutating func newDemandFromConsumer( - suspendedDemand: UnsafeContinuation<(Result, Result)?, Never> - ) -> NewDemandFromConsumerOutput { - switch self.state { - case .initial: - return .startTask(suspendedDemand: suspendedDemand) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .awaitingBaseResults(task: task, result1: nil, result2: nil, suspendedBases: [], suspendedDemand: suspendedDemand) - return .resumeBases(suspendedBases: suspendedBases) - - case .awaitingBaseResults: - preconditionFailure("Inconsistent state, a demand is already suspended") - - case .finished: - return .terminate(suspendedDemand: suspendedDemand) - } - } - - enum NewLoopFromBaseOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate(suspendedBase: UnsafeContinuation) - } - - mutating func newLoopFromBase1(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, var suspendedBases, let suspendedDemand): - precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") - if result1 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(suspendedBase: suspendedBase) - } - } - - mutating func newLoopFromBase2(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, var suspendedBases, let suspendedDemand): - precondition(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") - if result2 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(suspendedBase: suspendedBase) - } - } - - enum BaseHasProducedElementOutput { - case none - case resumeDemand( - suspendedDemand: SuspendedDemand?, - result1: Result, - result2: Result - ) - } - - mutating func base1HasProducedElement(element: Element1) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, _, let result2, let suspendedBases, let suspendedDemand): - if let result2 { - self.state = .awaitingBaseResults( - task: task, - result1: .success(element), - result2: result2, - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: .success(element), result2: result2) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: .success(element), - result2: nil, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .none - } - } - - mutating func base2HasProducedElement(element: Element2) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, let result1, _, let suspendedBases, let suspendedDemand): - if let result1 { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: .success(element), - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: .success(element)) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: nil, - result2: .success(element), - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .none - } - } - - enum BaseHasProducedFailureOutput { - case none - case resumeDemandAndTerminate( - task: Task?, - suspendedDemand: SuspendedDemand?, - suspendedBases: [UnsafeContinuation], - result1: Result, - result2: Result - ) - } - - mutating func baseHasProducedFailure(error: any Error) -> BaseHasProducedFailureOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .resumeDemandAndTerminate( - task: task, - suspendedDemand: suspendedDemand, - suspendedBases: suspendedBases, - result1: .failure(error), - result2: .failure(error) - ) - - case .finished: - return .none - } - } - - mutating func base2HasProducedFailure(error: Error) -> BaseHasProducedFailureOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .resumeDemandAndTerminate( - task: task, - suspendedDemand: suspendedDemand, - suspendedBases: suspendedBases, - result1: .failure(error), - result2: .failure(error) - ) - - case .finished: - return .none - } - } - - mutating func demandIsFulfilled() { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, results are not yet available to be acknowledged") - - case .awaitingBaseResults(let task, let result1, let result2, let suspendedBases, let suspendedDemand): - precondition(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") - precondition(result1 != nil && result2 != nil, "Inconsistent state, all results are not yet available to be acknowledged") - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - - case .finished: - break - } - } - - enum RootTaskIsCancelledOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [SuspendedDemand?]? - ) - } - - mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum BaseIsFinishedOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [SuspendedDemand?]? - ) - } - - mutating func baseIsFinished() -> BaseIsFinishedOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } -} diff --git a/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift b/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift deleted file mode 100644 index 63e83fda..00000000 --- a/Sources/AsyncAlgorithms/Zip/Zip3Runtime.swift +++ /dev/null @@ -1,252 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -final class Zip3Runtime: Sendable -where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { - typealias ZipStateMachine = Zip3StateMachine - - private let stateMachine = ManagedCriticalState(ZipStateMachine()) - private let base1: Base1 - private let base2: Base2 - private let base3: Base3 - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } - - func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element)? { - try await withTaskCancellationHandler { - let results = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let output = stateMachine.newDemandFromConsumer(suspendedDemand: continuation) - switch output { - case .startTask(let suspendedDemand): - // first iteration, we start one task per base to iterate over them - self.startTask(stateMachine: &stateMachine, suspendedDemand: suspendedDemand) - - case .resumeBases(let suspendedBases): - // bases can be iterated over for 1 iteration so their next value can be retrieved - suspendedBases.forEach { $0.resume() } - - case .terminate(let suspendedDemand): - // the async sequence is already finished, immediately resuming - suspendedDemand.resume(returning: nil) - } - } - } - - guard let results else { - return nil - } - - self.stateMachine.withCriticalRegion { stateMachine in - // acknowledging the consumption of the zipped values, so we can begin another iteration on the bases - stateMachine.demandIsFulfilled() - } - - return try (results.0._rethrowGet(), results.1._rethrowGet(), results.2._rethrowGet()) - } onCancel: { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - // clean the allocated resources and state - self.handle(rootTaskIsCancelledOutput: output) - } - } - - private func handle(rootTaskIsCancelledOutput: ZipStateMachine.RootTaskIsCancelledOutput) { - switch rootTaskIsCancelledOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func startTask( - stateMachine: inout ZipStateMachine, - suspendedDemand: ZipStateMachine.SuspendedDemand - ) { - let task = Task { - await withTaskGroup(of: Void.self) { group in - group.addTask { - var base1Iterator = self.base1.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { continuation in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase1(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element1 = try await base1Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base1HasProducedElement(element: element1) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - - group.addTask { - var base2Iterator = self.base2.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { continuation in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase2(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element2 = try await base2Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base2HasProducedElement(element: element2) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - - group.addTask { - var base3Iterator = self.base3.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { continuation in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase3(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element3 = try await base3Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base3HasProducedElement(element: element3) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - } - } - stateMachine.taskIsStarted(task: task, suspendedDemand: suspendedDemand) - } - - private func handle(newLoopFromBaseOutput: ZipStateMachine.NewLoopFromBaseOutput) { - switch newLoopFromBaseOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let suspendedBase): - suspendedBase.resume() - } - } - - private func handle(baseHasProducedElementOutput: ZipStateMachine.BaseHasProducedElementOutput) { - switch baseHasProducedElementOutput { - case .none: - break - - case .resumeDemand(let suspendedDemand, let result1, let result2, let result3): - suspendedDemand?.resume(returning: (result1, result2, result3)) - } - } - - private func handle(baseHasProducedFailureOutput: ZipStateMachine.BaseHasProducedFailureOutput) { - switch baseHasProducedFailureOutput { - case .none: - break - - case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let result1, let result2, let result3): - suspendedDemand?.resume(returning: (result1, result2, result3)) - suspendedBases.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseIsFinishedOutput: ZipStateMachine.BaseIsFinishedOutput) { - switch baseIsFinishedOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - deinit { - // clean the allocated resources and state - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } -} diff --git a/Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift b/Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift deleted file mode 100644 index 21faf292..00000000 --- a/Sources/AsyncAlgorithms/Zip/Zip3StateMachine.swift +++ /dev/null @@ -1,439 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -struct Zip3StateMachine: Sendable -where Element1: Sendable, Element2: Sendable, Element3: Sendable { - typealias SuspendedDemand = UnsafeContinuation<(Result, Result, Result)?, Never> - - private enum State { - case initial - case awaitingDemandFromConsumer( - task: Task?, - suspendedBases: [UnsafeContinuation] - ) - case awaitingBaseResults( - task: Task?, - result1: Result?, - result2: Result?, - result3: Result?, - suspendedBases: [UnsafeContinuation], - suspendedDemand: SuspendedDemand? - ) - case finished - } - - private var state: State = .initial - - mutating func taskIsStarted( - task: Task, - suspendedDemand: SuspendedDemand - ) { - switch self.state { - case .initial: - self.state = .awaitingBaseResults( - task: task, - result1: nil, - result2: nil, - result3: nil, - suspendedBases: [], - suspendedDemand: suspendedDemand - ) - - default: - preconditionFailure("Inconsistent state, the task cannot start while the state is other than initial") - } - } - - enum NewDemandFromConsumerOutput { - case resumeBases(suspendedBases: [UnsafeContinuation]) - case startTask(suspendedDemand: SuspendedDemand) - case terminate(suspendedDemand: SuspendedDemand) - } - - mutating func newDemandFromConsumer(suspendedDemand: SuspendedDemand) -> NewDemandFromConsumerOutput { - switch self.state { - case .initial: - return .startTask(suspendedDemand: suspendedDemand) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .awaitingBaseResults( - task: task, - result1: nil, - result2: nil, - result3: nil, - suspendedBases: [], - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: suspendedBases) - - case .awaitingBaseResults: - preconditionFailure("Inconsistent state, a demand is already suspended") - - case .finished: - return .terminate(suspendedDemand: suspendedDemand) - } - } - - enum NewLoopFromBaseOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate(suspendedBase: UnsafeContinuation) - } - - mutating func newLoopFromBase1(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): - precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - if result1 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(suspendedBase: suspendedBase) - } - } - - mutating func newLoopFromBase2(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): - precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - if result2 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(suspendedBase: suspendedBase) - } - } - - mutating func newLoopFromBase3(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): - precondition(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - if result3 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(suspendedBase: suspendedBase) - } - } - - enum BaseHasProducedElementOutput { - case none - case resumeDemand( - suspendedDemand: SuspendedDemand?, - result1: Result, - result2: Result, - result3: Result - ) - } - - mutating func base1HasProducedElement(element: Element1) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, _, let result2, let result3, let suspendedBases, let suspendedDemand): - if let result2, let result3 { - self.state = .awaitingBaseResults( - task: task, - result1: .success(element), - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: .success(element), result2: result2, result3: result3) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: .success(element), - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .none - } - } - - mutating func base2HasProducedElement(element: Element2) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, let result1, _, let result3, let suspendedBases, let suspendedDemand): - if let result1, let result3 { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: .success(element), - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: .success(element), result3: result3) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: .success(element), - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .none - } - } - - mutating func base3HasProducedElement(element: Element3) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, let result1, let result2, _, let suspendedBases, let suspendedDemand): - if let result1, let result2 { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: .success(element), - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: result2, result3: .success(element)) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: .success(element), - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .none - } - } - - enum BaseHasProducedFailureOutput { - case none - case resumeDemandAndTerminate( - task: Task?, - suspendedDemand: SuspendedDemand?, - suspendedBases: [UnsafeContinuation], - result1: Result, - result2: Result, - result3: Result - ) - } - - mutating func baseHasProducedFailure(error: any Error) -> BaseHasProducedFailureOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .resumeDemandAndTerminate( - task: task, - suspendedDemand: suspendedDemand, - suspendedBases: suspendedBases, - result1: .failure(error), - result2: .failure(error), - result3: .failure(error) - ) - - case .finished: - return .none - } - } - - mutating func demandIsFulfilled() { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer: - preconditionFailure("Inconsistent state, results are not yet available to be acknowledged") - - case .awaitingBaseResults(let task, let result1, let result2, let result3, let suspendedBases, let suspendedDemand): - precondition(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") - precondition(result1 != nil && result2 != nil && result3 != nil, "Inconsistent state, all results are not yet available to be acknowledged") - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - - case .finished: - break - } - } - - enum RootTaskIsCancelledOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [SuspendedDemand?]? - ) - } - - mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum BaseIsFinishedOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [SuspendedDemand?]? - ) - } - - mutating func baseIsFinished() -> BaseIsFinishedOutput { - switch self.state { - case .initial: - preconditionFailure("Inconsistent state, the task is not started") - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } -} diff --git a/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift b/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift new file mode 100644 index 00000000..d6b0c9ce --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift @@ -0,0 +1,548 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// State machine for zip +struct ZipStateMachine< + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>: Sendable where + Base1: Sendable, + Base2: Sendable, + Base3: Sendable, + Base1.Element: Sendable, + Base2.Element: Sendable, + Base3.Element: Sendable { + typealias DownstreamContinuation = UnsafeContinuation, Never> + + private enum State: Sendable { + /// Small wrapper for the state of an upstream sequence. + struct Upstream: Sendable { + /// The upstream continuation. + var continuation: UnsafeContinuation? + /// The produced upstream element. + var element: Element? + } + + /// The initial state before a call to `next` happened. + case initial(base1: Base1, base2: Base2, base3: Base3?) + + /// The state while we are waiting for downstream demand. + case waitingForDemand( + task: Task, + upstreams: (Upstream, Upstream, Upstream) + ) + + /// The state while we are consuming the upstream and waiting until we get a result from all upstreams. + case zipping( + task: Task, + upstreams: (Upstream, Upstream, Upstream), + downstreamContinuation: DownstreamContinuation + ) + + /// The state once one upstream sequences finished/threw 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 + } + + private var state: State + + private let numberOfUpstreamSequences: Int + + /// Initializes a new `StateMachine`. + init( + base1: Base1, + base2: Base2, + base3: Base3? + ) { + self.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 + /// the upstream continuations need to be resumed with a `CancellationError`. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch self.state { + case .initial: + // Nothing to do here. No demand was signalled until now + return .none + + case .zipping: + // 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 upstreams): + // 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 .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + 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") + } + } + + mutating func taskIsStarted( + task: Task, + downstreamContinuation: DownstreamContinuation + ) { + switch self.state { + case .initial: + // The user called `next` and we are starting the `Task` + // to consume the upstream sequences + self.state = .zipping( + task: task, + upstreams: (.init(), .init(), .init()), + downstreamContinuation: downstreamContinuation + ) + + case .zipping, .waitingForDemand, .finished: + // We only allow a single task 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 `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 + ) + } + + mutating func childTaskSuspended(baseIndex: Int, continuation: UnsafeContinuation) -> ChildTaskSuspendedAction? { + switch self.state { + case .initial: + // Child tasks are only created after we transitioned to `zipping` + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .waitingForDemand(let task, var upstreams): + self.state = .modifying + + switch baseIndex { + case 0: + upstreams.0.continuation = continuation + + case 1: + upstreams.1.continuation = continuation + + case 2: + upstreams.2.continuation = continuation + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)") + } + + self.state = .waitingForDemand( + task: task, + upstreams: upstreams + ) + + return .none + + case .zipping(let task, var upstreams, let downstreamContinuation): + // We are currently zipping. If we have a buffered element from the base + // 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 { + self.state = .modifying + upstreams.0.continuation = continuation + self.state = .zipping( + task: task, + upstreams: upstreams, + downstreamContinuation: downstreamContinuation + ) + return .none + } + + case 1: + if upstreams.1.element == nil { + return .resumeContinuation(upstreamContinuation: continuation) + } else { + self.state = .modifying + upstreams.1.continuation = continuation + self.state = .zipping( + task: task, + upstreams: upstreams, + downstreamContinuation: downstreamContinuation + ) + return .none + } + + case 2: + if upstreams.2.element == nil { + return .resumeContinuation(upstreamContinuation: continuation) + } else { + self.state = .modifying + upstreams.2.continuation = continuation + self.state = .zipping( + task: task, + upstreams: upstreams, + downstreamContinuation: downstreamContinuation + ) + return .none + } + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)") + } + + 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") + } + } + + /// Actions returned by `elementProduced()`. + enum ElementProducedAction { + /// Indicates that the downstream continuation should be resumed with the element. + case resumeContinuation( + downstreamContinuation: DownstreamContinuation, + result: Result<(Base1.Element, Base2.Element, Base3.Element?)?, Error> + ) + } + + mutating func elementProduced(_ result: (Base1.Element?, Base2.Element?, Base3.Element?)) -> ElementProducedAction? { + switch self.state { + case .initial: + // Child tasks that are producing elements are only created after we transitioned to `zipping` + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + + case .waitingForDemand: + // We are only issuing demand when we get signalled by the downstream. + // We should never receive an element when we are waiting for demand. + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + + case .zipping(let task, var upstreams, let downstreamContinuation): + self.state = .modifying + + switch result { + case (.some(let first), .none, .none): + precondition(upstreams.0.element == nil) + upstreams.0.element = first + + case (.none, .some(let second), .none): + precondition(upstreams.1.element == nil) + upstreams.1.element = second + + case (.none, .none, .some(let third)): + precondition(upstreams.2.element == nil) + upstreams.2.element = third + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + } + + // 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 { + // We got an element from each upstream so we can resume the downstream now + self.state = .waitingForDemand( + task: task, + upstreams: ( + .init(continuation: upstreams.0.continuation), + .init(continuation: upstreams.1.continuation), + .init(continuation: upstreams.2.continuation) + ) + ) + + return .resumeContinuation( + downstreamContinuation: downstreamContinuation, + result: .success((first, second, third)) + ) + + } else if let first = upstreams.0.element, + 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, + upstreams: ( + .init(continuation: upstreams.0.continuation), + .init(continuation: upstreams.1.continuation), + .init(continuation: upstreams.2.continuation) + ) + ) + + return .resumeContinuation( + downstreamContinuation: downstreamContinuation, + result: .success((first, second, nil)) + ) + } else { + // We are still waiting for one of the upstreams to produce an element + self.state = .zipping( + task: task, + upstreams: ( + .init(continuation: upstreams.0.continuation, element: upstreams.0.element), + .init(continuation: upstreams.1.continuation, element: upstreams.1.element), + .init(continuation: upstreams.2.continuation, element: upstreams.2.element) + ), + downstreamContinuation: downstreamContinuation + ) + + 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 `upstreamFinished()`. + enum UpstreamFinishedAction { + /// Indicates that the downstream continuation should be resumed with `nil` and + /// the task and the upstream continuations should be cancelled. + case resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: DownstreamContinuation, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func upstreamFinished() -> UpstreamFinishedAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .waitingForDemand: + // This can't happen. We are only issuing demand for a single element each time. + // There must never be outstanding demand to an upstream while we have no demand ourselves. + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .zipping(let task, let upstreams, let downstreamContinuation): + // One of our upstreams finished. We need to transition to finished ourselves now + // and resume the downstream continuation with nil. Furthermore, we need to cancel all of + // the upstream work. + self.state = .finished + + return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + 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 downstream continuation should be resumed with the `error` and + /// the task and the upstream continuations should be cancelled. + case resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: DownstreamContinuation, + error: Error, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func upstreamThrew(_ error: Error) -> UpstreamThrewAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .waitingForDemand: + // This can't happen. We are only issuing demand for a single element each time. + // There must never be outstanding demand to an upstream while we have no demand ourselves. + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .zipping(let task, let upstreams, let downstreamContinuation): + // One of our upstreams threw. We need to transition to finished ourselves now + // and resume the downstream continuation with the error. Furthermore, we need to cancel all of + // the upstream work. + self.state = .finished + + return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + error: error, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + 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: DownstreamContinuation, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that the task and the upstream continuations should be cancelled. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func cancelled() -> CancelledAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + + case .waitingForDemand(let task, let upstreams): + // The downstream task got cancelled so we need to cancel our upstream Task + // and resume all continuations. We can also transition to finished. + self.state = .finished + + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .zipping(let task, let upstreams, let downstreamContinuation): + // The downstream Task got cancelled so we need to cancel our upstream Task + // and resume all continuations. We can also transition to finished. + self.state = .finished + + return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .finished: + // We are already finished so nothing to do here: + self.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. + case startTask(Base1, Base2, Base3?) + case resumeUpstreamContinuations( + upstreamContinuation: [UnsafeContinuation] + ) + /// Indicates that the downstream continuation should be resumed with `nil`. + case resumeDownstreamContinuationWithNil(DownstreamContinuation) + } + + mutating func next(for continuation: DownstreamContinuation) -> NextAction { + switch self.state { + case .initial(let base1, let base2, let base3): + // 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(base1, base2, base3) + + case .zipping: + // 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, var upstreams): + // We got demand signalled now and can transition to zipping. + // 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 } + upstreams.0.continuation = nil + upstreams.1.continuation = nil + upstreams.2.continuation = nil + + self.state = .zipping( + task: task, + upstreams: upstreams, + downstreamContinuation: continuation + ) + + return .resumeUpstreamContinuations( + upstreamContinuation: upstreamContinuations + ) + + case .finished: + // We are already finished so we are just returning `nil` + return .resumeDownstreamContinuationWithNil(continuation) + + case .modifying: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift new file mode 100644 index 00000000..57ee1dc5 --- /dev/null +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -0,0 +1,320 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class ZipStorage: Sendable + where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { + typealias StateMachine = ZipStateMachine + + private let stateMachine: ManagedCriticalState + + init(_ base1: Base1, _ base2: Base2, _ base3: Base3?) { + self.stateMachine = .init(.init(base1: base1, base2: base2, base3: base3)) + } + + func iteratorDeinitialized() { + let action = self.stateMachine.withCriticalRegion { $0.iteratorDeinitialized() } + + switch action { + case .cancelTaskAndUpstreamContinuations( + let task, + let upstreamContinuation + ): + upstreamContinuation.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + + case .none: + break + } + } + + func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? { + try await withTaskCancellationHandler { + let result = await withUnsafeContinuation { continuation in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.next(for: continuation) + switch action { + case .startTask(let base1, let base2, let base3): + // first iteration, we start one child task per base to iterate over them + self.startTask( + stateMachine: &stateMachine, + base1: base1, + base2: base2, + base3: base3, + downStreamContinuation: continuation + ) + + case .resumeUpstreamContinuations(let upstreamContinuations): + // bases can be iterated over for 1 iteration so their next value can be retrieved + upstreamContinuations.forEach { $0.resume() } + + case .resumeDownstreamContinuationWithNil(let continuation): + // the async sequence is already finished, immediately resuming + continuation.resume(returning: .success(nil)) + } + } + } + + return try result._rethrowGet() + + } onCancel: { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.cancelled() + + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + case .none: + break + } + } + } + } + + private func startTask( + stateMachine: inout StateMachine, + base1: Base1, + base2: Base2, + base3: Base3?, + downStreamContinuation: StateMachine.DownstreamContinuation + ) { + // This creates a new `Task` that is iterating the upstream + // sequences. We must store it to cancel it at the right times. + let task = Task { + await withThrowingTaskGroup(of: Void.self) { group in + // For each upstream sequence we are adding a child task that + // is consuming the upstream sequence + group.addTask { + var base1Iterator = base1.makeAsyncIterator() + + 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 + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() + + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) + + case .none: + break + } + } + } + + if let element1 = try await base1Iterator.next() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced((element1, nil, nil)) + + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .none: + break + } + } + } else { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamFinished() + + switch action { + case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + + case .none: + break + } + } + } + } + } + + group.addTask { + var base1Iterator = base2.makeAsyncIterator() + + 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 + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() + + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) + + case .none: + break + } + } + } + + if let element2 = try await base1Iterator.next() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced((nil, element2, nil)) + + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .none: + break + } + } + } else { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamFinished() + + switch action { + case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + + case .none: + break + } + } + } + } + } + + if let base3 = base3 { + group.addTask { + var base1Iterator = base3.makeAsyncIterator() + + 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 + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() + + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) + + case .none: + break + } + } + } + + if let element3 = try await base1Iterator.next() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced((nil, nil, element3)) + + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .none: + break + } + } + } else { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamFinished() + + switch action { + case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + + case .none: + break + } + } + } + } + } + } + + do { + try await group.waitForAll() + } catch { + // One of the upstream sequences threw an error + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamThrew(error) + + switch action { + case .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() + } + } + } + + stateMachine.taskIsStarted(task: task, downstreamContinuation: downStreamContinuation) + } +} From ed0b086089f4e9ac76b3cb6138f578c25e661f34 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 20 Oct 2022 19:49:54 +0100 Subject: [PATCH 071/149] Remove Sendable from `AsyncBufferedByteIterator` (#220) # Motivation I raise on the pitch of `AsyncBufferedByteIterator` on the forums that I think the iterator must not be `Sendable`. The reasoning for this is twofold. First, an iterator is the connection of a consumer to the `AsyncSequence`; therefore, iterators should not be shared since it breaks that assumption. Secondly, the implementation of the `AsyncBufferedByteIterator` can be more straight forward since we don't have to check for unique ownership of the storage. # Modification Remove the `Sendable` conformances from `AsyncBufferedByteIterator`. --- .../AsyncBufferedByteIterator.swift | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift index 848ceeb4..9016b7af 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift @@ -39,7 +39,7 @@ /// } /// /// -public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { +public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 @usableFromInline var buffer: _AsyncBytesBuffer @@ -64,10 +64,13 @@ public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { } } +@available(*, unavailable) +extension AsyncBufferedByteIterator: Sendable { } + @frozen @usableFromInline -internal struct _AsyncBytesBuffer: @unchecked Sendable { +internal struct _AsyncBytesBuffer { @usableFromInline - final class Storage: Sendable { + final class Storage { fileprivate let buffer: UnsafeMutableRawBufferPointer init( @@ -85,7 +88,7 @@ internal struct _AsyncBytesBuffer: @unchecked Sendable { } } - @usableFromInline internal var storage: Storage + @usableFromInline internal let storage: Storage @usableFromInline internal var nextPointer: UnsafeRawPointer @usableFromInline internal var endPointer: UnsafeRawPointer @@ -110,24 +113,6 @@ internal struct _AsyncBytesBuffer: @unchecked Sendable { } try Task.checkCancellation() do { - // If two tasks have access to this iterator then the references on - // the storage will be non uniquely owned. This means that any reload - // must happen into its own fresh buffer. The consumption of those - // bytes between two tasks are inherently defined as potential - // duplication by the nature of sending that buffer across the two - // tasks - this means that the brief period in which they may be - // sharing non reloaded bytes is to be expected; basically in that - // edge case of making the iterator and sending that across to two - // places to iterate is asking for something bizzare and the answer - // should not be crash, but it definitely cannot be consistent. - // - // The unique ref check is here to prevent the potentials of a crashing - // scenario. - if !isKnownUniquelyReferenced(&storage) { - // The count is not mutated across invocations so the access is safe. - let capacity = storage.buffer.count - storage = Storage(capacity: capacity) - } let readSize: Int = try await readFunction(storage.buffer) if readSize == 0 { finished = true From ae26222e86305b12cab5579ce803645351065ec2 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 16 Nov 2022 16:49:50 +0000 Subject: [PATCH 072/149] Rework `combineLatest` to remove the `Sendable` constraint on the base iterator (#228) # Motivation This follows the recent rework of the `merge` and `zip` algorithms where we dropped the `Sendable` constraint on the `BaseX.AsyncIterator`. This PR now removes the same constraint on `combineLatest`. # Modification This PR overhauls the implementation of `combineLatest` with the goal of changing these things: - Drop the `Sendable` constraint of the base `AsyncIterator`s - Only create a single `Task` # Result `combineLatest` now doesn't require the base `AsyncIterator` to be `Sendable` --- .../AsyncCombineLatest2Sequence.swift | 309 -------- .../AsyncCombineLatest3Sequence.swift | 57 -- .../AsyncCombineLatest2Sequence.swift | 89 +++ .../AsyncCombineLatest3Sequence.swift | 99 +++ .../CombineLatestStateMachine.swift | 722 ++++++++++++++++++ .../CombineLatest/CombineLatestStorage.swift | 358 +++++++++ .../TestCombineLatest.swift | 2 - 7 files changed, 1268 insertions(+), 368 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncCombineLatest2Sequence.swift delete mode 100644 Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift create mode 100644 Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift create mode 100644 Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift create mode 100644 Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift diff --git a/Sources/AsyncAlgorithms/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/AsyncCombineLatest2Sequence.swift deleted file mode 100644 index 9db4d34a..00000000 --- a/Sources/AsyncAlgorithms/AsyncCombineLatest2Sequence.swift +++ /dev/null @@ -1,309 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// Creates an asynchronous sequence that combines the latest values from two `AsyncSequence` types -/// by emitting a tuple of the values. -public func combineLatest(_ base1: Base1, _ base2: Base2) -> AsyncCombineLatest2Sequence { - AsyncCombineLatest2Sequence(base1, base2) -} - -/// An `AsyncSequence` that combines the latest values produced from two asynchronous sequences into an asynchronous sequence of tuples. -public struct AsyncCombineLatest2Sequence: Sendable - where - Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { - let base1: Base1 - let base2: Base2 - - init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } -} - -extension AsyncCombineLatest2Sequence: AsyncSequence { - public typealias Element = (Base1.Element, Base2.Element) - - /// The iterator for a `AsyncCombineLatest2Sequence` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - enum Partial: Sendable { - case first(Result, Base1.AsyncIterator) - case second(Result, Base2.AsyncIterator) - } - - enum State { - case initial(Base1.AsyncIterator, Base2.AsyncIterator) - case idle(Base1.AsyncIterator, Base2.AsyncIterator, (Base1.Element, Base2.Element)) - case firstActiveSecondIdle(Task, Base2.AsyncIterator, (Base1.Element, Base2.Element)) - case firstIdleSecondActive(Base1.AsyncIterator, Task, (Base1.Element, Base2.Element)) - case firstTerminalSecondIdle(Base2.AsyncIterator, (Base1.Element, Base2.Element)) - case firstIdleSecondTerminal(Base1.AsyncIterator, (Base1.Element, Base2.Element)) - case terminal - } - - var state: State - - init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator) { - state = .initial(base1, base2) - } - - public mutating func next() async rethrows -> (Base1.Element, Base2.Element)? { - let task1: Task - let task2: Task - var current: (Base1.Element, Base2.Element) - - switch state { - case .initial(let iterator1, let iterator2): - func iteration( - _ group: inout TaskGroup, - _ value1: inout Base1.Element?, - _ value2: inout Base2.Element?, - _ iterator1: inout Base1.AsyncIterator?, - _ iterator2: inout Base2.AsyncIterator? - ) async -> Result<(Base1.Element, Base2.Element)?, Error>? { - guard let partial = await group.next() else { - return .success(nil) - } - switch partial { - case .first(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value1 = value - iterator1 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - case .second(let res, let iter): - switch res { - case .success(let value): - if let value = value { - value2 = value - iterator2 = iter - return nil - } else { - group.cancelAll() - return .success(nil) - } - case .failure(let error): - group.cancelAll() - return .failure(error) - } - } - } - - let (result, iter1, iter2) = await withTaskGroup(of: Partial.self) { group -> (Result<(Base1.Element, Base2.Element)?, Error>, Base1.AsyncIterator?, Base2.AsyncIterator?) in - group.addTask { - var iterator = iterator1 - do { - let value = try await iterator.next() - return .first(.success(value), iterator) - } catch { - return .first(.failure(error), iterator) - } - } - group.addTask { - var iterator = iterator2 - do { - let value = try await iterator.next() - return .second(.success(value), iterator) - } catch { - return .second(.failure(error), iterator) - } - } - - var res1: Base1.Element? - var res2: Base2.Element? - var iter1: Base1.AsyncIterator? - var iter2: Base2.AsyncIterator? - - if let result = await iteration(&group, &res1, &res2, &iter1, &iter2) { - return (result, nil, nil) - } - if let result = await iteration(&group, &res1, &res2, &iter1, &iter2) { - return (result, nil, nil) - } - guard let res1 = res1, let res2 = res2 else { - return (.success(nil), nil, nil) - } - - return (.success((res1, res2)), iter1, iter2) - } - do { - // make sure to get the result first just in case it has a failure embedded - guard let value = try result._rethrowGet() else { - state = .terminal - return nil - } - guard let iter1 = iter1, let iter2 = iter2 else { - state = .terminal - return nil - } - state = .idle(iter1, iter2, value) - return value - } catch { - state = .terminal - throw error - } - case .idle(let iterator1, let iterator2, let value): - task1 = Task { - var iterator = iterator1 - do { - let value = try await iterator.next() - return .first(.success(value), iterator) - } catch { - return .first(.failure(error), iterator) - } - } - task2 = Task { - var iterator = iterator2 - do { - let value = try await iterator.next() - return .second(.success(value), iterator) - } catch { - return .second(.failure(error), iterator) - } - } - current = value - case .firstActiveSecondIdle(let task, let iterator2, let value): - task1 = task - task2 = Task { - var iterator = iterator2 - do { - let value = try await iterator.next() - return .second(.success(value), iterator) - } catch { - return .second(.failure(error), iterator) - } - } - current = value - case .firstIdleSecondActive(let iterator1, let task, let value): - task1 = Task { - var iterator = iterator1 - do { - let value = try await iterator.next() - return .first(.success(value), iterator) - } catch { - return .first(.failure(error), iterator) - } - } - task2 = task - current = value - case .firstTerminalSecondIdle(var iterator, var current): - do { - guard let member = try await iterator.next() else { - state = .terminal - return nil - } - current.1 = member - state = .firstTerminalSecondIdle(iterator, current) - return current - } catch { - state = .terminal - throw error - } - case .firstIdleSecondTerminal(var iterator, var current): - do { - guard let member = try await iterator.next() else { - state = .terminal - return nil - } - current.0 = member - state = .firstIdleSecondTerminal(iterator, current) - return current - } catch { - state = .terminal - throw error - } - case .terminal: - return nil - } - switch await Task.select(task1, task2).value { - case .first(let result, let iterator): - switch result { - case .success(let member): - if let member = member { - current.0 = member - state = .firstIdleSecondActive(iterator, task2, current) - } else { - switch await task2.value { - case .first: - fatalError() - case .second(let result, let iterator): - switch result { - case .success(let member): - if let member = member { - current.1 = member - state = .firstTerminalSecondIdle(iterator, current) - return current - } else { - state = .terminal - return nil - } - case .failure: - state = .terminal - try result._rethrowError() - } - } - } - case .failure: - state = .terminal - task2.cancel() - try result._rethrowError() - } - case .second(let result, let iterator): - switch result { - case .success(let member): - if let member = member { - current.1 = member - state = .firstActiveSecondIdle(task1, iterator, current) - } else { - switch await task1.value { - case .first(let result, let iterator): - switch result { - case .success(let member): - if let member = member { - current.0 = member - state = .firstIdleSecondTerminal(iterator, current) - return current - } else { - state = .terminal - return nil - } - case .failure: - state = .terminal - try result._rethrowError() - } - case .second: - fatalError() - } - } - case .failure: - state = .terminal - task2.cancel() - try result._rethrowError() - } - } - return current - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator()) - } -} diff --git a/Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift deleted file mode 100644 index e182a435..00000000 --- a/Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift +++ /dev/null @@ -1,57 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// Creates an asynchronous sequence that combines the latest values from three `AsyncSequence` types -/// by emitting a tuple of the values. -public func combineLatest(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncCombineLatest3Sequence { - AsyncCombineLatest3Sequence(base1, base2, base3) -} - -/// An `AsyncSequence` that combines the latest values produced from three asynchronous sequences into an asynchronous sequence of tuples. -public struct AsyncCombineLatest3Sequence: Sendable - where - Base1: Sendable, Base2: Sendable, Base3: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { - let base1: Base1 - let base2: Base2 - let base3: Base3 - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } -} - -extension AsyncCombineLatest3Sequence: AsyncSequence { - public typealias Element = (Base1.Element, Base2.Element, Base3.Element) - - /// The iterator for a `AsyncCombineLatest3Sequence` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - var iterator: AsyncCombineLatest2Sequence, Base3>.Iterator - - init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator, _ base3: Base3.AsyncIterator) { - iterator = AsyncCombineLatest2Sequence, Base3>.Iterator(AsyncCombineLatest2Sequence.Iterator(base1, base2), base3) - } - - public mutating func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element)? { - guard let value = try await iterator.next() else { - return nil - } - return (value.0.0, value.0.1, value.1) - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator(), base3.makeAsyncIterator()) - } -} diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift new file mode 100644 index 00000000..8ff0c038 --- /dev/null +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Creates an asynchronous sequence that combines the latest values from two `AsyncSequence` types +/// by emitting a tuple of the values. ``combineLatest(_:_:)`` only emits a value whenever any of the base `AsyncSequence`s +/// emit a value (so long as each of the bases have emitted at least one value). +/// +/// Finishes: +/// ``combineLatest(_:_:)`` finishes when one of the bases finishes before emitting any value or +/// when all bases finished. +/// +/// Throws: +/// ``combineLatest(_:_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed +/// values will be dropped. +public func combineLatest< + Base1: AsyncSequence, + Base2: AsyncSequence +>(_ base1: Base1, _ base2: Base2) -> AsyncCombineLatest2Sequence where + Base1: Sendable, + Base1.Element: Sendable, + Base2: Sendable, + Base2.Element: Sendable { + AsyncCombineLatest2Sequence(base1, base2) +} + +/// An `AsyncSequence` that combines the latest values produced from two asynchronous sequences into an asynchronous sequence of tuples. +public struct AsyncCombineLatest2Sequence< + Base1: AsyncSequence, + Base2: AsyncSequence +>: AsyncSequence where + Base1: Sendable, + Base1.Element: Sendable, + Base2: Sendable, + Base2.Element: Sendable { + public typealias Element = (Base1.Element, Base2.Element) + public typealias AsyncIterator = Iterator + + let base1: Base1 + let base2: Base2 + + public init(_ base1: Base1, _ base2: Base2) { + self.base1 = base1 + self.base2 = base2 + } + + public func makeAsyncIterator() -> AsyncIterator { + Iterator(storage: .init(self.base1, self.base2, nil)) + } + + public struct Iterator: AsyncIteratorProtocol { + final class InternalClass { + private let storage: CombineLatestStorage + + fileprivate init(storage: CombineLatestStorage) { + self.storage = storage + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + guard let element = try await self.storage.next() else { + return nil + } + + return (element.0, element.1) + } + } + + let internalClass: InternalClass + + fileprivate init(storage: CombineLatestStorage) { + self.internalClass = InternalClass(storage: storage) + } + + public mutating func next() async rethrows -> Element? { + try await self.internalClass.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift new file mode 100644 index 00000000..0ba733e2 --- /dev/null +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Creates an asynchronous sequence that combines the latest values from three `AsyncSequence` types +/// by emitting a tuple of the values. ``combineLatest(_:_:_:)`` only emits a value whenever any of the base `AsyncSequence`s +/// emit a value (so long as each of the bases have emitted at least one value). +/// +/// Finishes: +/// ``combineLatest(_:_:_:)`` finishes when one of the bases finishes before emitting any value or +/// when all bases finished. +/// +/// Throws: +/// ``combineLatest(_:_:_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed +/// values will be dropped. +public func combineLatest< + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncCombineLatest3Sequence where + Base1: Sendable, + Base1.Element: Sendable, + Base2: Sendable, + Base2.Element: Sendable, + Base3: Sendable, + Base3.Element: Sendable { + AsyncCombineLatest3Sequence(base1, base2, base3) +} + +/// An `AsyncSequence` that combines the latest values produced from three asynchronous sequences into an asynchronous sequence of tuples. +public struct AsyncCombineLatest3Sequence< + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>: AsyncSequence 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 + + let base1: Base1 + let base2: Base2 + let base3: Base3 + + init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { + self.base1 = base1 + self.base2 = base2 + self.base3 = base3 + } + + public func makeAsyncIterator() -> AsyncIterator { + Iterator(storage: .init(self.base1, self.base2, self.base3) + ) + } + + public struct Iterator: AsyncIteratorProtocol { + final class InternalClass { + private let storage: CombineLatestStorage + + fileprivate init(storage: CombineLatestStorage) { + self.storage = storage + } + + deinit { + self.storage.iteratorDeinitialized() + } + + func next() async rethrows -> Element? { + guard let element = try await self.storage.next() else { + return nil + } + + // This force unwrap is safe since there must be a third element. + return (element.0, element.1, element.2!) + } + } + + let internalClass: InternalClass + + fileprivate init(storage: CombineLatestStorage) { + self.internalClass = InternalClass(storage: storage) + } + + public mutating func next() async rethrows -> Element? { + try await self.internalClass.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift new file mode 100644 index 00000000..38c8f5a9 --- /dev/null +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -0,0 +1,722 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import DequeModule + +/// State machine for combine latest +struct CombineLatestStateMachine< + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>: Sendable where + Base1: Sendable, + Base2: Sendable, + Base3: Sendable, + Base1.Element: Sendable, + Base2.Element: Sendable, + Base3.Element: Sendable { + typealias DownstreamContinuation = UnsafeContinuation, Never> + + private enum State: Sendable { + /// Small wrapper for the state of an upstream sequence. + struct Upstream: Sendable { + /// The upstream continuation. + var continuation: UnsafeContinuation? + /// The produced upstream element. + var element: Element? + /// Indicates wether the upstream finished/threw already + var isFinished: Bool + } + + /// The initial state before a call to `next` happened. + case initial(base1: Base1, base2: Base2, base3: Base3?) + + /// The state while we are waiting for downstream demand. + case waitingForDemand( + task: Task, + upstreams: (Upstream, Upstream, Upstream), + buffer: Deque<(Base1.Element, Base2.Element, Base3.Element?)> + ) + + /// The state while we are consuming the upstream and waiting until we get a result from all upstreams. + case combining( + task: Task, + upstreams: (Upstream, Upstream, Upstream), + downstreamContinuation: DownstreamContinuation, + buffer: Deque<(Base1.Element, Base2.Element, Base3.Element?)> + ) + + case upstreamsFinished( + buffer: Deque<(Base1.Element, Base2.Element, Base3.Element?)> + ) + + case upstreamThrew( + error: Error + ) + + /// The state once 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 + } + + private var state: State + + private let numberOfUpstreamSequences: Int + + /// Initializes a new `StateMachine`. + init( + base1: Base1, + base2: Base2, + base3: Base3? + ) { + self.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 + /// the upstream continuations need to be resumed with a `CancellationError`. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch self.state { + case .initial: + // Nothing to do here. No demand was signalled until now + return .none + + case .combining: + // 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 upstreams, _): + // 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 .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .upstreamThrew, .upstreamsFinished: + // The iterator was dropped so we can transition to finished now. + 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 + + case .modifying: + preconditionFailure("Invalid state") + } + } + + mutating func taskIsStarted( + task: Task, + downstreamContinuation: DownstreamContinuation + ) { + switch self.state { + case .initial: + // The user called `next` and we are starting the `Task` + // to consume the upstream sequences + self.state = .combining( + task: task, + upstreams: (.init(isFinished: false), .init(isFinished: false), .init(isFinished: false)), + downstreamContinuation: downstreamContinuation, + buffer: .init() + ) + + case .combining, .waitingForDemand, .upstreamThrew, .upstreamsFinished, .finished: + // We only allow a single task 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 `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 + ) + } + + mutating func childTaskSuspended(baseIndex: Int, continuation: UnsafeContinuation) -> ChildTaskSuspendedAction? { + switch self.state { + case .initial: + // Child tasks are only created after we transitioned to `zipping` + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + + case .upstreamsFinished: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .waitingForDemand(let task, var upstreams, let buffer): + self.state = .modifying + + switch baseIndex { + case 0: + upstreams.0.continuation = continuation + + case 1: + upstreams.1.continuation = continuation + + case 2: + upstreams.2.continuation = continuation + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended() with base index \(baseIndex)") + } + + self.state = .waitingForDemand( + task: task, + upstreams: upstreams, + buffer: buffer + ) + + return .none + + case .combining: + // We are currently combining and need to resume any upstream until we transition to waitingForDemand + + return .resumeContinuation(upstreamContinuation: continuation) + + case .upstreamThrew, .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") + } + } + + /// Actions returned by `elementProduced()`. + enum ElementProducedAction { + /// Indicates that the downstream continuation should be resumed with the element. + case resumeContinuation( + downstreamContinuation: DownstreamContinuation, + result: Result<(Base1.Element, Base2.Element, Base3.Element?)?, Error> + ) + } + + mutating func elementProduced(_ result: (Base1.Element?, Base2.Element?, Base3.Element?)) -> ElementProducedAction? { + print("upstream produced: \(result)") + switch self.state { + case .initial: + // Child tasks that are producing elements are only created after we transitioned to `zipping` + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + + case .upstreamsFinished: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .waitingForDemand(let task, var upstreams, var buffer): + // We got an element in late. This can happen since we race the upstreams. + // We have to store the new tuple in our buffer and remember the upstream states. + + self.state = .modifying + + switch result { + case (.some(let first), .none, .none): + buffer.append((first, upstreams.1.element!, upstreams.2.element)) + upstreams.0.element = first + + case (.none, .some(let second), .none): + buffer.append((upstreams.0.element!, second, upstreams.2.element)) + upstreams.1.element = second + + case (.none, .none, .some(let third)): + buffer.append((upstreams.0.element!, upstreams.1.element!, third)) + upstreams.2.element = third + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + } + + self.state = .waitingForDemand( + task: task, + upstreams: upstreams, + buffer: buffer + ) + + 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") + self.state = .modifying + + switch result { + case (.some(let first), .none, .none): + upstreams.0.element = first + + case (.none, .some(let second), .none): + upstreams.1.element = second + + case (.none, .none, .some(let third)): + upstreams.2.element = third + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received elementProduced()") + } + + // 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 { + // We got an element from each upstream so we can resume the downstream now + self.state = .waitingForDemand( + task: task, + upstreams: upstreams, + buffer: buffer + ) + + return .resumeContinuation( + downstreamContinuation: downstreamContinuation, + result: .success((first, second, third)) + ) + + } else if let first = upstreams.0.element, + 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, + upstreams: upstreams, + buffer: buffer + ) + + return .resumeContinuation( + downstreamContinuation: downstreamContinuation, + result: .success((first, second, nil)) + ) + } else { + // We are still waiting for one of the upstreams to produce an element + 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) + ), + downstreamContinuation: downstreamContinuation, + buffer: buffer + ) + + return .none + } + + case .upstreamThrew, .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 `upstreamFinished()`. + enum UpstreamFinishedAction { + /// Indicates 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: DownstreamContinuation, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func upstreamFinished(baseIndex: Int) -> UpstreamFinishedAction? { + print("upstream finished: \(baseIndex)") + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .upstreamsFinished: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") + + case .waitingForDemand(let task, var upstreams, let buffer): + // One of the upstreams finished. + + self.state = .modifying + + switch baseIndex { + case 0: + upstreams.0.isFinished = true + + case 1: + upstreams.1.isFinished = true + + case 2: + upstreams.2.isFinished = true + + default: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished() with base index \(baseIndex)") + } + + if upstreams.0.isFinished && upstreams.1.isFinished && upstreams.2.isFinished { + // All upstreams finished we can transition to either finished or upstreamsFinished now + if buffer.isEmpty { + self.state = .finished + } else { + self.state = .upstreamsFinished(buffer: buffer) + } + + return .cancelTaskAndUpstreamContinuations( + task: task, + 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 + if buffer.isEmpty { + self.state = .finished + } else { + self.state = .upstreamsFinished(buffer: buffer) + } + + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + } else { + self.state = .waitingForDemand( + task: task, + upstreams: upstreams, + buffer: buffer + ) + return .none + } + + case .combining(let task, var upstreams, let downstreamContinuation, let buffer): + // One of the upstreams finished. + + self.state = .modifying + + // We need to track if an empty upstream finished. + // If that happens we can transition to finish right away. + let emptyUpstreamFinished: Bool + switch baseIndex { + case 0: + upstreams.0.isFinished = true + emptyUpstreamFinished = upstreams.0.element == nil + + case 1: + upstreams.1.isFinished = true + emptyUpstreamFinished = upstreams.1.element == nil + + case 2: + upstreams.2.isFinished = true + emptyUpstreamFinished = upstreams.2.element == nil + + default: + 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. + if emptyUpstreamFinished { + // All upstreams finished + self.state = .finished + + return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + } else if upstreams.0.isFinished && upstreams.1.isFinished && upstreams.2.isFinished { + // All upstreams finished + self.state = .finished + + return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + 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 + self.state = .finished + + return .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + } else { + self.state = .combining( + task: task, + upstreams: upstreams, + downstreamContinuation: downstreamContinuation, + buffer: buffer + ) + return .none + } + + case .upstreamThrew, .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 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: DownstreamContinuation, + error: Error, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func upstreamThrew(_ error: Error) -> UpstreamThrewAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .upstreamsFinished: + preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + + case .waitingForDemand(let task, let upstreams, _): + // An upstream threw. We can cancel everything now and transition to finished. + // We just need to store the error for the next downstream demand + self.state = .upstreamThrew( + error: error + ) + + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .combining(let task, let upstreams, let downstreamContinuation, _): + // One of our upstreams threw. We need to transition to finished ourselves now + // and resume the downstream continuation with the error. Furthermore, we need to cancel all of + // the upstream work. + self.state = .finished + + return .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + error: error, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .upstreamThrew, .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: DownstreamContinuation, + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + /// Indicates that the task and the upstream continuations should be cancelled. + case cancelTaskAndUpstreamContinuations( + task: Task, + upstreamContinuations: [UnsafeContinuation] + ) + } + + mutating func cancelled() -> CancelledAction? { + switch self.state { + case .initial: + preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + + case .waitingForDemand(let task, let upstreams, _): + // The downstream task got cancelled so we need to cancel our upstream Task + // and resume all continuations. We can also transition to finished. + self.state = .finished + + return .cancelTaskAndUpstreamContinuations( + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .combining(let task, let upstreams, let downstreamContinuation, _): + // The downstream Task got cancelled so we need to cancel our upstream Task + // and resume all continuations. We can also transition to finished. + self.state = .finished + + return .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation: downstreamContinuation, + task: task, + upstreamContinuations: [upstreams.0.continuation, upstreams.1.continuation, upstreams.2.continuation].compactMap { $0 } + ) + + case .upstreamsFinished: + // We can transition to finished now + self.state = .finished + + return .none + + case .upstreamThrew, .finished: + // We are already finished so nothing to do here: + + 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. + case startTask(Base1, Base2, Base3?) + /// Indicates that all upstream continuations should be resumed. + case resumeUpstreamContinuations( + upstreamContinuation: [UnsafeContinuation] + ) + /// Indicates that the downstream continuation should be resumed with the result. + case resumeContinuation( + downstreamContinuation: DownstreamContinuation, + result: Result<(Base1.Element, Base2.Element, Base3.Element?)?, Error> + ) + /// Indicates that the downstream continuation should be resumed with `nil`. + case resumeDownstreamContinuationWithNil(DownstreamContinuation) + } + + mutating func next(for continuation: DownstreamContinuation) -> NextAction { + switch self.state { + case .initial(let base1, let base2, let base3): + // 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(base1, base2, base3) + + case .combining: + // 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, var upstreams, var buffer): + // We got demand signalled now we have to check if there is anything buffered. + // 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 } + upstreams.0.continuation = nil + upstreams.1.continuation = nil + upstreams.2.continuation = nil + + self.state = .combining( + task: task, + upstreams: upstreams, + downstreamContinuation: continuation, + buffer: buffer + ) + + return .resumeUpstreamContinuations( + upstreamContinuation: upstreamContinuations + ) + } + + 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 { + self.state = .finished + + return .resumeDownstreamContinuationWithNil(continuation) + } + + case .upstreamThrew(let error): + // One of the upstreams threw and we have to return this error now. + self.state = .finished + + return .resumeContinuation(downstreamContinuation: continuation, result: .failure(error)) + + case .finished: + // We are already finished so we are just returning `nil` + return .resumeDownstreamContinuationWithNil(continuation) + + case .modifying: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift new file mode 100644 index 00000000..716eea2b --- /dev/null +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -0,0 +1,358 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class CombineLatestStorage< + Base1: AsyncSequence, + Base2: AsyncSequence, + Base3: AsyncSequence +>: Sendable where + Base1: Sendable, + Base1.Element: Sendable, + Base2: Sendable, + Base2.Element: Sendable, + Base3: Sendable, + Base3.Element: Sendable { + typealias StateMachine = CombineLatestStateMachine + + private let stateMachine: ManagedCriticalState + + init(_ base1: Base1, _ base2: Base2, _ base3: Base3?) { + self.stateMachine = .init(.init(base1: base1, base2: base2, base3: base3)) + } + + func iteratorDeinitialized() { + let action = self.stateMachine.withCriticalRegion { $0.iteratorDeinitialized() } + + switch action { + case .cancelTaskAndUpstreamContinuations( + let task, + let upstreamContinuation + ): + upstreamContinuation.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + + case .none: + break + } + } + + func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? { + try await withTaskCancellationHandler { + let result = await withUnsafeContinuation { continuation in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.next(for: continuation) + switch action { + case .startTask(let base1, let base2, let base3): + // first iteration, we start one child task per base to iterate over them + self.startTask( + stateMachine: &stateMachine, + base1: base1, + base2: base2, + base3: base3, + downStreamContinuation: continuation + ) + + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .resumeUpstreamContinuations(let upstreamContinuations): + // bases can be iterated over for 1 iteration so their next value can be retrieved + upstreamContinuations.forEach { $0.resume() } + + case .resumeDownstreamContinuationWithNil(let continuation): + // the async sequence is already finished, immediately resuming + continuation.resume(returning: .success(nil)) + } + } + } + + print("Returning: \(result)") + return try result._rethrowGet() + + } onCancel: { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.cancelled() + + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + case .none: + break + } + } + } + } + + private func startTask( + stateMachine: inout StateMachine, + base1: Base1, + base2: Base2, + base3: Base3?, + downStreamContinuation: StateMachine.DownstreamContinuation + ) { + // This creates a new `Task` that is iterating the upstream + // sequences. We must store it to cancel it at the right times. + let task = Task { + await withThrowingTaskGroup(of: Void.self) { group in + // For each upstream sequence we are adding a child task that + // is consuming the upstream sequence + group.addTask { + var base1Iterator = base1.makeAsyncIterator() + + 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 + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() + + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) + + case .none: + break + } + } + } + + if let element1 = try await base1Iterator.next() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced((element1, nil, nil)) + + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .none: + break + } + } + } else { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamFinished(baseIndex: 0) + } + + switch action { + case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + break loop + + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + + case .none: + break loop + } + } + } + } + + group.addTask { + var base1Iterator = base2.makeAsyncIterator() + + 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 + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() + + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) + + case .none: + break + } + } + } + + if let element2 = try await base1Iterator.next() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced((nil, element2, nil)) + + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .none: + break + } + } + } else { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamFinished(baseIndex: 1) + } + + switch action { + case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + break loop + + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + + case .none: + break loop + } + } + } + } + + if let base3 = base3 { + group.addTask { + var base1Iterator = base3.makeAsyncIterator() + + 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 + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() + + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) + + case .none: + break + } + } + } + + if let element3 = try await base1Iterator.next() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced((nil, nil, element3)) + + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .none: + break + } + } + } else { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamFinished(baseIndex: 2) + } + + switch action { + case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .success(nil)) + break loop + + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + + case .none: + break loop + } + } + } + } + } + + do { + try await group.waitForAll() + } catch { + // One of the upstream sequences threw an error + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamThrew(error) + + switch action { + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let error, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: .failure(error)) + + case .none: + break + } + } + + group.cancelAll() + } + } + } + + stateMachine.taskIsStarted(task: task, downstreamContinuation: downStreamContinuation) + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index 659e535a..78e08ae7 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -19,7 +19,6 @@ final class TestCombineLatest2: XCTestCase { let sequence = combineLatest(a.async, b.async) let actual = await Array(sequence) XCTAssertGreaterThanOrEqual(actual.count, 3) - XCTAssertEqual(actual.first!, (1, "a")) } func test_throwing_combineLatest1() async { @@ -329,7 +328,6 @@ final class TestCombineLatest3: XCTestCase { let sequence = combineLatest(a.async, b.async, c.async) let actual = await Array(sequence) XCTAssertGreaterThanOrEqual(actual.count, 3) - XCTAssertEqual(actual.first!, (1, "a", 4)) } func test_ordering1() async { From 0174f58455661dce1b44ac882156a198c43ee02b Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 16 Nov 2022 10:15:26 -0800 Subject: [PATCH 073/149] Cleanup Sendable conformances and @preconcurrency imports to build as strict concurrency clean (#230) --- .../AsyncAlgorithms/AsyncBufferSequence.swift | 20 +- Sources/AsyncAlgorithms/AsyncChannel.swift | 8 +- .../AsyncCompactedSequence.swift | 2 +- .../AsyncInterspersedSequence.swift | 2 +- .../AsyncAlgorithms/AsyncJoinedSequence.swift | 2 +- .../AsyncRemoveDuplicatesSequence.swift | 7 +- .../AsyncThrowingChannel.swift | 8 +- .../AsyncCombineLatest2Sequence.swift | 2 +- .../AsyncCombineLatest3Sequence.swift | 2 +- .../Zip/AsyncZip2Sequence.swift | 2 +- .../Zip/AsyncZip3Sequence.swift | 2 +- .../Performance/ThroughputMeasurement.swift | 2 +- .../Support/ReportingSequence.swift | 2 +- .../TestAdjacentPairs.swift | 2 +- Tests/AsyncAlgorithmsTests/TestBuffer.swift | 208 +----------------- .../TestBufferedByteIterator.swift | 2 +- Tests/AsyncAlgorithmsTests/TestChain.swift | 2 +- Tests/AsyncAlgorithmsTests/TestChannel.swift | 2 +- .../TestCombineLatest.swift | 2 +- .../AsyncAlgorithmsTests/TestCompacted.swift | 2 +- .../TestInterspersed.swift | 2 +- Tests/AsyncAlgorithmsTests/TestJoin.swift | 2 +- Tests/AsyncAlgorithmsTests/TestLazy.swift | 2 +- .../TestManualClock.swift | 2 +- Tests/AsyncAlgorithmsTests/TestMerge.swift | 2 +- .../AsyncAlgorithmsTests/TestReductions.swift | 2 +- .../TestRemoveDuplicates.swift | 2 +- .../AsyncAlgorithmsTests/TestValidator.swift | 2 +- Tests/AsyncAlgorithmsTests/TestZip.swift | 2 +- 29 files changed, 46 insertions(+), 253 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift index 96fad74c..c079289c 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift @@ -198,7 +198,7 @@ public actor AsyncLimitBuffer: AsyncBuffer { } } -extension AsyncSequence where Element: Sendable { +extension AsyncSequence where Element: Sendable, Self: Sendable { /// Creates an asynchronous sequence that buffers elements using a buffer created from a supplied closure. /// /// Use the `buffer(_:)` method to account for `AsyncSequence` types that may produce elements faster @@ -224,7 +224,7 @@ extension AsyncSequence where Element: Sendable { } /// An `AsyncSequence` that buffers elements utilizing an `AsyncBuffer`. -public struct AsyncBufferSequence where Base.Element == Buffer.Input, Base.AsyncIterator: Sendable { +public struct AsyncBufferSequence where Base.Element == Buffer.Input { let base: Base let createBuffer: @Sendable () -> Buffer @@ -246,11 +246,11 @@ extension AsyncBufferSequence: AsyncSequence { let buffer: Buffer let state: AsyncBufferState - init(_ iterator: Base.AsyncIterator, buffer: Buffer, state: AsyncBufferState) { + init(_ base: Base, buffer: Buffer, state: AsyncBufferState) { self.buffer = buffer self.state = state task = Task { - var iter = iterator + var iter = base.makeAsyncIterator() do { while let item = try await iter.next() { await state.enqueue(item, buffer: buffer) @@ -279,21 +279,21 @@ extension AsyncBufferSequence: AsyncSequence { } enum State { - case idle(Base.AsyncIterator, @Sendable () -> Buffer) + case idle(Base, @Sendable () -> Buffer) case active(Active) } var state: State - init(_ iterator: Base.AsyncIterator, createBuffer: @Sendable @escaping () -> Buffer) { - state = .idle(iterator, createBuffer) + init(_ base: Base, createBuffer: @Sendable @escaping () -> Buffer) { + state = .idle(base, createBuffer) } public mutating func next() async rethrows -> Element? { switch state { - case .idle(let iterator, let createBuffer): + case .idle(let base, let createBuffer): let bufferState = AsyncBufferState() - let buffer = Active(iterator, buffer: createBuffer(), state: bufferState) + let buffer = Active(base, buffer: createBuffer(), state: bufferState) state = .active(buffer) return try await buffer.next() case .active(let buffer): @@ -303,6 +303,6 @@ extension AsyncBufferSequence: AsyncSequence { } public func makeAsyncIterator() -> Iterator { - Iterator(base.makeAsyncIterator(), createBuffer: createBuffer) + Iterator(base, createBuffer: createBuffer) } } diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index 5c4f5a47..9b4a0036 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import OrderedCollections +@preconcurrency import OrderedCollections /// A channel for sending elements from one task to another with back pressure. /// @@ -58,7 +58,7 @@ public final class AsyncChannel: AsyncSequence, Sendable { typealias Pending = ChannelToken?, Never>> typealias Awaiting = ChannelToken> - struct ChannelToken: Hashable { + struct ChannelToken: Hashable, Sendable { var generation: Int var continuation: Continuation? @@ -86,14 +86,14 @@ public final class AsyncChannel: AsyncSequence, Sendable { case cancelled } - enum Emission { + enum Emission : Sendable { case idle case pending(OrderedSet) case awaiting(OrderedSet) case finished } - struct State { + struct State : Sendable { var emission: Emission = .idle var generation = 0 } diff --git a/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift b/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift index 5b96bc9f..82d2c617 100644 --- a/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift @@ -68,4 +68,4 @@ extension AsyncSequence { } } -extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } +extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift index 8d8e1b5b..43f8d974 100644 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift @@ -99,4 +99,4 @@ extension AsyncInterspersedSequence: AsyncSequence { } } -extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } +extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift index cbcbbbe2..716f981e 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift @@ -91,4 +91,4 @@ public struct AsyncJoinedSequence: AsyncSequence where Base } extension AsyncJoinedSequence: Sendable -where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable, Base.AsyncIterator: Sendable, Base.Element.AsyncIterator: Sendable { } +where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index 5fcd095f..fd1d2192 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -85,9 +85,6 @@ public struct AsyncRemoveDuplicatesSequence: AsyncSequence } } -extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } - - /// An asynchronous sequence that omits repeated elements by testing them with an error-throwing predicate. public struct AsyncThrowingRemoveDuplicatesSequence: AsyncSequence { public typealias Element = Base.Element @@ -143,4 +140,6 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS } } -extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } + +extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 732b0906..6291a7b2 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import OrderedCollections +@preconcurrency import OrderedCollections /// An error-throwing channel for sending elements from on task to another with back pressure. /// @@ -61,7 +61,7 @@ public final class AsyncThrowingChannel: Asyn typealias Pending = ChannelToken?, Never>> typealias Awaiting = ChannelToken> - struct ChannelToken: Hashable { + struct ChannelToken: Hashable, Sendable { var generation: Int var continuation: Continuation? @@ -95,14 +95,14 @@ public final class AsyncThrowingChannel: Asyn case failed(Error) } - enum Emission { + enum Emission: Sendable { case idle case pending(OrderedSet) case awaiting(OrderedSet) case terminated(Termination) } - struct State { + struct State : Sendable { var emission: Emission = .idle var generation = 0 } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index 8ff0c038..c07bd099 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -35,7 +35,7 @@ public func combineLatest< public struct AsyncCombineLatest2Sequence< Base1: AsyncSequence, Base2: AsyncSequence ->: AsyncSequence where +>: AsyncSequence, Sendable where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift index 0ba733e2..a1c7e51a 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -39,7 +39,7 @@ public struct AsyncCombineLatest3Sequence< Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence ->: AsyncSequence where +>: AsyncSequence, Sendable where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index 0ef591ba..6fb341ac 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -20,7 +20,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 +public struct AsyncZip2Sequence: AsyncSequence, Sendable where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 43317aca..87474bae 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -21,7 +21,7 @@ public func zip: AsyncSequence +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 diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index f9356aae..16531d9b 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -11,7 +11,7 @@ import AsyncAlgorithms import Foundation -@preconcurrency import XCTest +import XCTest #if canImport(Darwin) public struct InfiniteAsyncSequence: AsyncSequence, Sendable { diff --git a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift index a03e6f89..1f351d69 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift @@ -43,7 +43,7 @@ final class ReportingSequence: Sequence, IteratorProtocol { } } -final class ReportingAsyncSequence: AsyncSequence, AsyncIteratorProtocol { +final class ReportingAsyncSequence: AsyncSequence, AsyncIteratorProtocol, @unchecked Sendable { enum Event: Equatable, CustomStringConvertible { case next case makeAsyncIterator diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 3bcd73cf..843624f6 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestAdjacentPairs: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index d6838203..cd989c41 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestBuffer: XCTestCase { @@ -225,212 +225,6 @@ final class TestBuffer: XCTestCase { XCTAssertEqual(data, collected) } - func test_multi_tasks() async { - var values = GatedSequence(Array(0 ... 10)) - let bufferSeq = values.buffer(policy: .unbounded) - var iter_ = bufferSeq.makeAsyncIterator() - - // Initiate the sequence's operation and creation of its Task and actor before sharing its iterator. - values.advance() - let _ = await iter_.next() - - let iter = iter_ - - let task1 = Task<[Int], Never> { - var result = [Int]() - var iter1 = iter - while let val = await iter1.next() { - result.append(val) - } - return result - } - - let task2 = Task<[Int], Never> { - var result = [Int]() - var iter2 = iter - while let val = await iter2.next() { - result.append(val) - } - return result - } - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - values.advance() - - let task1Results = await task1.value - let task2Results = await task2.value - - XCTAssertEqual(task1Results.sorted(), task1Results) - XCTAssertEqual(task2Results.sorted(), task2Results) - - let combined = (task1Results + task2Results).sorted() - XCTAssertEqual(combined, Array(1 ... 10)) - } - - func test_multi_tasks_error() async { - var values = GatedSequence(Array(0 ... 10)) - let mapSeq = values.map { try throwOn(7, $0) } - let bufferSeq = mapSeq.buffer(policy: .unbounded) - var iter_ = bufferSeq.makeAsyncIterator() - - // Initiate the sequence's operation and creation of its Task and actor before sharing its iterator. - values.advance() - let _ = try! await iter_.next() - - let iter = iter_ - - let task1 = Task<([Int], Error?), Never> { - var result = [Int]() - var err: Error? - var iter1 = iter - do { - while let val = try await iter1.next() { - result.append(val) - } - } catch { - err = error - } - return (result, err) - } - - let task2 = Task<([Int], Error?), Never> { - var result = [Int]() - var err: Error? - var iter2 = iter - do { - while let val = try await iter2.next() { - result.append(val) - } - } catch { - err = error - } - return (result, err) - } - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - - let task1Results = await task1.value - let task2Results = await task2.value - - XCTAssertEqual(task1Results.0.sorted(), task1Results.0) - XCTAssertEqual(task2Results.0.sorted(), task2Results.0) - - let combined = (task1Results.0 + task2Results.0).sorted() - XCTAssertEqual(combined, Array(1 ... 6)) - - XCTAssertEqual(1, [task1Results, task2Results].compactMap{ $0.1 }.count) - } - - func test_multi_tasks_delegateBufferError() async { - actor BufferDelegate: AsyncBuffer { - var buffer = [Int]() - var pushed = [Int]() - - func push(_ element: Int) async { - buffer.append(element) - pushed.append(element) - } - - func pop() async throws -> Int? { - if buffer.count > 0 { - let value = buffer.removeFirst() - if value == 7 { - throw Failure() - } - return value - } - return nil - } - } - let delegate = BufferDelegate() - - var values = GatedSequence(Array(0 ... 10)) - let bufferSeq = values.buffer { delegate } - var iter_ = bufferSeq.makeAsyncIterator() - - // Initiate the sequence's operation and creation of its Task and actor before sharing its iterator. - values.advance() - let _ = try! await iter_.next() - - let iter = iter_ - - let task1 = Task<([Int], Error?), Never> { - var result = [Int]() - var err: Error? - var iter1 = iter - do { - while let val = try await iter1.next() { - result.append(val) - } - } catch { - err = error - } - return (result, err) - } - - let task2 = Task<([Int], Error?), Never> { - var result = [Int]() - var err: Error? - var iter2 = iter - do { - while let val = try await iter2.next() { - result.append(val) - } - } catch { - err = error - } - return (result, err) - } - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - values.advance() - values.advance() - - try? await Task.sleep(nanoseconds: 100_000_000) - values.advance() - - let task1Results = await task1.value - let task2Results = await task2.value - - XCTAssertEqual(task1Results.0.sorted(), task1Results.0) - XCTAssertEqual(task2Results.0.sorted(), task2Results.0) - - let combined = (task1Results.0 + task2Results.0).sorted() - XCTAssertEqual(combined, Array(1 ... 6)) - - XCTAssertEqual(1, [task1Results, task2Results].compactMap{ $0.1 }.count) - } - func test_bufferingOldest() async { validate { "X-12- 34- 5 |" diff --git a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift index 9284cc67..ee6c5d12 100644 --- a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift +++ b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestBufferedByteIterator: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestChain.swift b/Tests/AsyncAlgorithmsTests/TestChain.swift index 2ef9fc85..b3be90a3 100644 --- a/Tests/AsyncAlgorithmsTests/TestChain.swift +++ b/Tests/AsyncAlgorithmsTests/TestChain.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestChain2: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index 2d8797fa..3d53fe3a 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestChannel: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index 78e08ae7..489252ae 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestCombineLatest2: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestCompacted.swift b/Tests/AsyncAlgorithmsTests/TestCompacted.swift index ddba2fa8..b82fe31e 100644 --- a/Tests/AsyncAlgorithmsTests/TestCompacted.swift +++ b/Tests/AsyncAlgorithmsTests/TestCompacted.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestCompacted: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/TestInterspersed.swift index 1fa6c386..79c93f73 100644 --- a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/TestInterspersed.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestInterspersed: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestJoin.swift b/Tests/AsyncAlgorithmsTests/TestJoin.swift index e545c1f0..04fd1772 100644 --- a/Tests/AsyncAlgorithmsTests/TestJoin.swift +++ b/Tests/AsyncAlgorithmsTests/TestJoin.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms extension Sequence where Element: Sequence, Element.Element: Equatable & Sendable { diff --git a/Tests/AsyncAlgorithmsTests/TestLazy.swift b/Tests/AsyncAlgorithmsTests/TestLazy.swift index a173d80f..1ef69d29 100644 --- a/Tests/AsyncAlgorithmsTests/TestLazy.swift +++ b/Tests/AsyncAlgorithmsTests/TestLazy.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestLazy: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestManualClock.swift b/Tests/AsyncAlgorithmsTests/TestManualClock.swift index cc4d2640..045ba2de 100644 --- a/Tests/AsyncAlgorithmsTests/TestManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/TestManualClock.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestManualClock: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index 76ce5344..ef72d1ac 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestMerge2: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestReductions.swift b/Tests/AsyncAlgorithmsTests/TestReductions.swift index 82a0132b..ad9cf5ac 100644 --- a/Tests/AsyncAlgorithmsTests/TestReductions.swift +++ b/Tests/AsyncAlgorithmsTests/TestReductions.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestReductions: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift index eb859d01..01ddf3ff 100644 --- a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift +++ b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestRemoveDuplicates: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestValidator.swift b/Tests/AsyncAlgorithmsTests/TestValidator.swift index 2b4a152d..9c5ef9c2 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidator.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidator.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestValidator: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index 80e8caf8..d27dd73d 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import XCTest +import XCTest import AsyncAlgorithms final class TestZip2: XCTestCase { From dd97b564ae7f5680d87edc66f642fa0361d16e2e Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Wed, 16 Nov 2022 13:19:45 -0500 Subject: [PATCH 074/149] tests: harmonize debounce availability (#223) --- Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index f1868b5b..9038500f 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -64,7 +64,7 @@ final class TestThroughput: XCTestCase { zip($0, $1, $2) } } - @available(macOS 13.0, *) + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func test_debounce() async { await measureSequenceThroughput(source: (1...).async) { $0.debounce(for: .zero, clock: ContinuousClock()) From 9abd029409149294459742960c9f20ac2078caea Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 16 Nov 2022 10:54:45 -0800 Subject: [PATCH 075/149] Ensure all imports of other modules are implementation only (#232) --- Sources/AsyncAlgorithms/AsyncChannel.swift | 2 +- Sources/AsyncAlgorithms/AsyncThrowingChannel.swift | 2 +- .../CombineLatest/CombineLatestStateMachine.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift index 9b4a0036..1b41c779 100644 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import OrderedCollections +@preconcurrency @_implementationOnly import OrderedCollections /// A channel for sending elements from one task to another with back pressure. /// diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift index 6291a7b2..8359287e 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@preconcurrency import OrderedCollections +@preconcurrency @_implementationOnly import OrderedCollections /// An error-throwing channel for sending elements from on task to another with back pressure. /// diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 38c8f5a9..d9928f75 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import DequeModule +@_implementationOnly import DequeModule /// State machine for combine latest struct CombineLatestStateMachine< diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index d1a4b4ec..2723d112 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import DequeModule +@_implementationOnly import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index 579e0743..c2b54eb1 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import DequeModule +@_implementationOnly import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge< diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index ba0f2940..57d43bde 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import DequeModule +@_implementationOnly import DequeModule /// The state machine for any of the `merge` operator. /// From 5656338b5d1d9a8d2e90b92a32bddefd7175285b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 28 Nov 2022 13:03:04 +0000 Subject: [PATCH 076/149] Remove `print` from implementation of some algorithms (#240) --- .../CombineLatest/CombineLatestStateMachine.swift | 2 -- .../AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index d9928f75..fb68f073 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -243,7 +243,6 @@ struct CombineLatestStateMachine< } mutating func elementProduced(_ result: (Base1.Element?, Base2.Element?, Base3.Element?)) -> ElementProducedAction? { - print("upstream produced: \(result)") switch self.state { case .initial: // Child tasks that are producing elements are only created after we transitioned to `zipping` @@ -375,7 +374,6 @@ struct CombineLatestStateMachine< } mutating func upstreamFinished(baseIndex: Int) -> UpstreamFinishedAction? { - print("upstream finished: \(baseIndex)") switch self.state { case .initial: preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index 716eea2b..31245579 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -75,7 +75,6 @@ final class CombineLatestStorage< } } - print("Returning: \(result)") return try result._rethrowGet() } onCancel: { From fb1537575840e88eaea418bcf01fae4c522eb994 Mon Sep 17 00:00:00 2001 From: Hayashi Tatsuya <62803132+swiftty@users.noreply.github.com> Date: Sat, 17 Dec 2022 03:09:34 +0900 Subject: [PATCH 077/149] Fix Source links in documentations (#234) --- .../AsyncAlgorithms.docc/Guides/CombineLatest.md | 2 +- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md | 2 +- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md | 2 +- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md index e0fd7f47..b481eaae 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/CombineLatest.md @@ -1,6 +1,6 @@ # Combine Latest -[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCombineLatest2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift) | +[[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. diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md index b193c313..e1a30faa 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md @@ -3,7 +3,7 @@ * Author(s): [Philippe Hausler](https://github.com/phausler) [ -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift) | +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestDebounce.swift) ] diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md index bed7c4bb..edc1842b 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md @@ -1,6 +1,6 @@ # Merge -[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Asyncmerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift) | +[[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. diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md index 2aa4f32e..e73e1fde 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md @@ -1,6 +1,6 @@ # Zip -[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift) | +[[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. From 7586d2060f7acbecde355e6e9b1aba9dd645f3dd Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 3 Jan 2023 09:30:31 -0800 Subject: [PATCH 078/149] Convert the guides for debounce and throttle into a proposal (#194) * Convert the guides for debounce and throttle into a proposal * Adjust Sendability of the iterator requirements --- Evolution/NNNN-rate-limits.md | 157 ++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Evolution/NNNN-rate-limits.md diff --git a/Evolution/NNNN-rate-limits.md b/Evolution/NNNN-rate-limits.md new file mode 100644 index 00000000..94bf6e89 --- /dev/null +++ b/Evolution/NNNN-rate-limits.md @@ -0,0 +1,157 @@ +# Rate Limiting + +* Proposal: [SAA-NNNN]() +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** +* Implementation: +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestDebounce.swift) +] +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestThrottle.swift) +] + +* Decision Notes: +* Bugs: + +## Introduction + +When events can potentially happen faster than the desired consumption rate, there are multiple ways to handle the situation. One approach is to only emit values after a given period of time of inactivity, or "quiescence", has elapsed. This algorithm is commonly referred to as debouncing. A very close reelativee is an apporach to emit values after a given period has elapsed. These emitted values can be reduced from the values encountered during the waiting period. This algorithm is commonly referred to as throttling. + +## Proposed Solution + +The debounce algorithm produces elements after a particular duration has passed between events. It transacts within a given tolerance applied to a clock. If values are produced by the base `AsyncSequence` during this quiet period, the debounce does not resume its next iterator until the period has elapsed with no values are produced or unless a terminal event is encountered. + +The interface for this algorithm is available on all `AsyncSequence` types where the base type, iterator, and element are `Sendable`, since this algorithm will inherently create tasks to manage their timing of events. A shorthand implementation will be offered where the clock is the `ContinuousClock`, which allows for easy construction with `Duration` values. + +```swift +extension AsyncSequence { + public func debounce( + for interval: C.Instant.Duration, + tolerance: C.Instant.Duration? = nil, + clock: C + ) -> AsyncDebounceSequence + + public func debounce( + for interval: Duration, + tolerance: Duration? = nil + ) -> AsyncDebounceSequence +} +``` + +This all boils down to a terse description of how to transform the asynchronous sequence over time. + +```swift +fastEvents.debounce(for: .seconds(1)) +``` + +In this case it transforms a potentially fast asynchronous sequence of events into one that waits for a window of 1 second with no events to elapse before emitting a value. + +The throttle algorithm produces elements such that at least a specific interval has elapsed between them. It transacts by measuring against a specific clock. If values are produced by the base `AsyncSequence` the throttle does not resume its next iterator until the period has elapsed or unless a terminal event is encountered. + +The interface for this algorithm is available on all `AsyncSequence` types. Unlike other algorithms like `debounce`, the throttle algorithm does not need to create additional tasks or require any sort of tolerance because the interval is just measured. A shorthand implementation will be offered in conjunction where the clock is the `ContinuousClock`, which allows for easy construction with `Duration` values. An additional shorthand is offered to reduce the values such that it provides a "latest" or "earliest" value, representing the leading or trailing edge of a throttled region of production of events. + +```swift +extension AsyncSequence { + public func throttle( + for interval: C.Instant.Duration, + clock: C, + reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced + ) -> AsyncThrottleSequence + + public func throttle( + for interval: Duration, + reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced + ) -> AsyncThrottleSequence + + public func throttle( + for interval: C.Instant.Duration, + clock: C, + latest: Bool = true + ) -> AsyncThrottleSequence + + public func throttle( + for interval: Duration, + latest: Bool = true + ) -> AsyncThrottleSequence +} +``` + +This all boils down to a terse description of how to transform the asynchronous sequence over time. + +```swift +fastEvents.throttle(for: .seconds(1)) +``` + +In this case, the throttle transforms a potentially fast asynchronous sequence of events into one that waits for a window of 1 second to elapse before emitting a value. + +## Detailed Design + +### Debounce + +The type that implements the algorithm for debounce emits the same element type as the base that it applies to. It also throws when the base type throws (and likewise does not throw when the base type does not throw). + +```swift +public struct AsyncDebounceSequence: Sendable + where Base.Element: Sendable, Base: Sendable { +} + +extension AsyncDebounceSequence: AsyncSequence { + public typealias Element = Base.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Base.Element? + } + + public func makeAsyncIterator() -> Iterator +} +``` + +Since the stored types comprising `AsyncDebounceSequence` must be `Sendable`; `AsyncDebounceSequence` is unconditionally always `Sendable`. It is worth noting that the iterators are not required to be Sendable. + +### Throttle + +The type that implements the algorithm for throttle emits the same element type as the base that it applies to. It also throws when the base type throws (and likewise does not throw when the base type does not throw). + +```swift +public struct AsyncThrottleSequence { +} + +extension AsyncThrottleSequence: AsyncSequence { + public typealias Element = Reduced + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Reduced? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncThrottleSequence: Sendable + where Base: Sendable, Element: Sendable { } +``` + +The `AsyncThrottleSequence` is conditionally `Sendable` if the base types comprising it are `Sendable`. + +The time in which events are measured are from the previous emission if present. If a duration has elapsed between the last emission and the point in time the throttle is measured then that duration is counted as elapsed. The first element is considered not throttled because no interval can be constructed from the start to the first element. + +## Alternatives Considered + +An alternative form of `debounce` could exist similar to the reductions of `throttle`, where a closure would be invoked for each value being set as the latest, and reducing a new value to produce for the debounce. + +It was considered to only provide the "latest" style APIs, however the reduction version grants more flexibility and can act as a funnel to the implementations of `latest`. + +## Credits/Inspiration + +The naming for debounce comes as a term of art; originally this term was inspired by electronic circuitry. When a physical switch closes a circuit it can easily have a "bouncing" behavior (also called chatter) that is caused by electrical contact resistance and the physical bounce of springs associated with switches. That phenomenon is often addressed with additional circuits to de-bounce (removing the bouncing) by ensuring a certain quiescence occurs. + +http://reactivex.io/documentation/operators/debounce.html + +https://developer.apple.com/documentation/combine/publishers/debounce/ + +http://reactivex.io/documentation/operators/sample.html + +https://developer.apple.com/documentation/combine/publishers/throttle/ + From 4ba079747d69d114dd83cf98d6ae6dc18d6bde74 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 3 Jan 2023 09:31:15 -0800 Subject: [PATCH 079/149] Transform the chunk and timer guides into a proposal (#231) --- Evolution/NNNN-chunk.md | 356 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 Evolution/NNNN-chunk.md diff --git a/Evolution/NNNN-chunk.md b/Evolution/NNNN-chunk.md new file mode 100644 index 00000000..f9540b95 --- /dev/null +++ b/Evolution/NNNN-chunk.md @@ -0,0 +1,356 @@ +# Chunked & Timer +* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-chunk.md) +* Author(s): [Kevin Perry](https://github.com/kperryua), [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** +* Implementation: [ +[By group](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift), +[On projection](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift), +[Count and signal](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChunksOfCountAndSignalSequence.swift) +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChunk.swift) +] +[ +[Timer](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncTimerSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestTimer.swift) +] +* Decision Notes: +* Bugs: + +## Introduction + +Grouping of values from an asynchronous sequence is often useful for tasks that involve writing those values efficiently or useful to handle specific structured data inputs. + +The groupings may be controlled by different ways but one most notable is to control them by regular intervals. Producing elements at regular intervals can be useful for composing with other algorithms. These can range from invoking code at specific times to using those regular intervals as a delimiter of events. There are other cases this exists in APIs however those do not currently interact with Swift concurrency. These existing APIs are ones like `Timer` or `DispatchTimer` but are bound to internal clocks that are not extensible. + +## Proposed Solution + +Chunking operations can be broken down into a few distinct categories: grouping according to a binary predicate used to determine whether consecutive elements belong to the same group, projecting an element's property to determine the element's chunk membership, by discrete count, by another signal asynchronous sequence which indicates when the chunk should be delimited, or by a combination of count and signal. + +To satisfy the specific grouping by inteervals we propose to add a new type; `AsyncTimerSequence` which utilizes the new `Clock`, `Instant` and `Duration` types. This allows the interaction of the timer to custom implementations of types adopting `Clock`. + +This asynchronous sequence will produce elements of the clock's `Instant` type after the interval has elapsed. That instant will be the `now` at the time that the sleep has resumed. For each invocation to `next()` the `AsyncTimerSequence.Iterator` will calculate the next deadline to resume and pass that and the tolerance to the clock. If at any point in time the task executing that iteration is cancelled the iteration will return `nil` from the call to `next()`. + +## Detailed Design + +### Grouping + +Group chunks are determined by passing two consecutive elements to a closure which tests whether they are in the same group. When the `AsyncChunkedByGroupSequence` iterator receives the first element from the base sequence, it will immediately be added to a group. When it receives the second item, it tests whether the previous item and the current item belong to the same group. If they are not in the same group, then the iterator emits the first item's group and a new group is created containing the second item. Items declared to be in the same group accumulate until a new group is declared, or the iterator finds the end of the base sequence. When the base sequence terminates, the final group is emitted. If the base sequence throws an error, `AsyncChunkedByGroupSequence` will rethrow that error immediately and discard any current group. + +```swift +extension AsyncSequence { + public func chunked( + into: Collected.Type, + by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool + ) -> AsyncChunkedByGroupSequence + where Collected.Element == Element + + public func chunked( + by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool + ) -> AsyncChunkedByGroupSequence +} +``` + +Consider an example where an asynchronous sequence emits the following values: `10, 20, 30, 10, 40, 40, 10, 20`. Given the chunked operation to be defined as follows: + +```swift +let chunks = numbers.chunked { $0 <= $1 } +for await numberChunk in chunks { + print(numberChunk) +} +``` + +That snippet will produce the following values: + +```swift +[10, 20, 30] +[10, 40, 40] +[10, 20] +``` + +While `Array` is the default type for chunks, thanks to the overload that takes a `RangeReplaceableCollection` type, the same sample can be chunked into instances of `ContiguousArray`, or any other `RangeReplaceableCollection` instead. + +```swift +let chunks = numbers.chunked(into: ContiguousArray.self) { $0 <= $1 } +for await numberChunk in chunks { + print(numberChunk) +} +``` + +That variant is the funnel method for the main implementation, which passes `[Element].self` in as the parameter. + +### Projection + +In some scenarios, chunks are determined not by comparing different elements, but by the element itself. This may be the case when the element has some sort of discriminator that can determine the chunk it belongs to. When two consecutive elements have different projections, the current chunk is emitted and a new chunk is created for the new element. + +When the `AsyncChunkedOnProjectionSequence`'s iterator receives `nil` from the base sequence, it emits the final chunk. When the base sequence throws an error, the iterator discards the current chunk and rethrows that error. + +Similarly to the `chunked(by:)` method this algorithm has an optional specification for the `RangeReplaceableCollection` which is used as the type of each chunk. + +```swift +extension AsyncSequence { + public func chunked( + into: Collected.Type, + on projection: @escaping @Sendable (Element) -> Subject + ) -> AsyncChunkedOnProjectionSequence + + public func chunked( + on projection: @escaping @Sendable (Element) -> Subject + ) -> AsyncChunkedOnProjectionSequence +} +``` + +The following example shows how a sequence of names can be chunked together by their first characters. + +```swift +let names = URL(fileURLWithPath: "/tmp/names.txt").lines +let groupedNames = names.chunked(on: \.first!) +for try await (firstLetter, names) in groupedNames { + print(firstLetter) + for name in names { + print(" ", name) + } +} +``` + +A special property of this kind of projection chunking is that when an asynchronous sequence's elements are known to be ordered, the output of the chunking asynchronous sequence is suitable for initializing dictionaries using the `AsyncSequence` initializer for `Dictionary`. This is because the projection can be easily designed to match the sorting characteristics and thereby guarantee that the output matches the pattern of an array of pairs of unique "keys" with the chunks as the "values". + +In the example above, if the names are known to be ordered then you can take advantage of the uniqueness of each "first character" projection to initialize a `Dictionary` like so: + +```swift +let names = URL(fileURLWithPath: "/tmp/names.txt").lines +let nameDirectory = try await Dictionary(uniqueKeysWithValues: names.chunked(on: \.first!)) +``` + +### Count or Signal + +Sometimes chunks are determined not by the elements themselves, but by external factors. This final category enables limiting chunks to a specific size and/or delimiting them by another asynchronous sequence which is referred to as a "signal". This particular chunking family is useful for scenarios where the elements are more efficiently processed as chunks than individual elements, regardless of their values. + +This family is broken down into two sub-families of methods: ones that employ a signal plus an optional count (which return an `AsyncChunksOfCountOrSignalSequence`), and the ones that only deal with counts (which return an `AsyncChunksOfCountSequence`). Both sub-families have `Collected` as their element type, or `Array` if unspecified. These sub-families have rethrowing behaviors; if the base `AsyncSequence` can throw then the chunks sequence can also throw. Likewise if the base `AsyncSequence` cannot throw then the chunks sequence also cannot throw. + +##### Count only + +```swift +extension AsyncSequence { + public func chunks( + ofCount count: Int, + into: Collected.Type + ) -> AsyncChunksOfCountSequence + where Collected.Element == Element + + public func chunks( + ofCount count: Int + ) -> AsyncChunksOfCountSequence +} +``` + +If a chunk size limit is specified via an `ofCount` parameter, the sequence will produce chunks of type `Collected` with at most the specified number of elements. When a chunk reaches the given size, the asynchronous sequence will emit it immediately. + +For example, an asynchronous sequence of `UInt8` bytes can be chunked into at most 1024-byte `Data` instances like so: + +```swift +let packets = bytes.chunks(ofCount: 1024, into: Data.self) +for try await packet in packets { + write(packet) +} +``` + +##### Signal only + +```swift +extension AsyncSequence { + public func chunked( + by signal: Signal, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence + where Collected.Element == Element + + public func chunked( + by signal: Signal + ) -> AsyncChunksOfCountOrSignalSequence + + public func chunked( + by timer: AsyncTimerSequence, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence> + where Collected.Element == Element + + public func chunked( + by timer: AsyncTimerSequence + ) -> AsyncChunksOfCountOrSignalSequence> +} +``` + +If a signal asynchronous sequence is specified, the chunking asynchronous sequence emits chunks whenever the signal emits. The signals element values are ignored. If the chunking asynchronous sequence hasn't accumulated any elements since its previous emission, then no value is emitted in response to the signal. + +Since time is a frequent method of signaling desired delineations of chunks, there is a pre-specialized set of overloads that take `AsyncTimerSequence`. These allow shorthand initialization by using `AsyncTimerSequence`'s static member initializers. + +As an example, an asynchronous sequence of log messages can be chunked into arrays of logs in four second segments like so: + +```swift +let fourSecondsOfLogs = logs.chunked(by: .repeating(every: .seconds(4))) +for await chunk in fourSecondsOfLogs { + send(chunk) +} +``` + +##### Count or Signal + +```swift +extension AsyncSequence { + public func chunks( + ofCount count: Int, + or signal: Signal, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence + where Collected.Element == Element + + public func chunks( + ofCount count: Int, + or signal: Signal + ) -> AsyncChunksOfCountOrSignalSequence + + public func chunked( + by timer: AsyncTimerSequence, + into: Collected.Type + ) -> AsyncChunksOfCountOrSignalSequence> + where Collected.Element == Element + + public func chunked( + by timer: AsyncTimerSequence + ) -> AsyncChunksOfCountOrSignalSequence> +} +``` + +If both count and signal are specified, the chunking asynchronous sequence emits chunks whenever *either* the chunk reaches the specified size *or* the signal asynchronous sequence emits. When a signal causes a chunk to be emitted, the accumulated element count is reset back to zero. When an `AsyncTimerSequence` is used as a signal, the timer is started from the moment `next()` is called for the first time on `AsyncChunksOfCountOrSignalSequence`'s iterator, and it emits on a regular cadence from that moment. Note that the scheduling of the timer's emission is unaffected by any chunks emitted based on count. + +Like the example above, this code emits up to 1024-byte `Data` instances, but a chunk will also be emitted every second. + +```swift +let packets = bytes.chunks(ofCount: 1024 or: .repeating(every: .seconds(1)), into: Data.self) +for try await packet in packets { + write(packet) +} +``` + +In any configuration of any of the chunking families, when the base asynchronous sequence terminates, one of two things will happen: 1) a partial chunk will be emitted, or 2) no chunk will be emitted (i.e. the iterator received no elements since the emission of the previous chunk). No elements from the base asynchronous sequence are ever discarded, except in the case of a thrown error. + +## Interfaces + +### Grouping + +```swift +public struct AsyncChunkedByGroupSequence: AsyncSequence + where Collected.Element == Base.Element { + public typealias Element = Collected + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Collected? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncChunkedByGroupSequence: Sendable + where Base: Sendable, Base.Element: Sendable { } + +extension AsyncChunkedByGroupSequence.Iterator: Sendable + where Base.AsyncIterator: Sendable, Base.Element: Sendable { } +``` + +### Projection + +```swift +public struct AsyncChunkedOnProjectionSequence: AsyncSequence where Collected.Element == Base.Element { + public typealias Element = (Subject, Collected) + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> (Subject, Collected)? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncChunkedOnProjectionSequence: Sendable + where Base: Sendable, Base.Element: Sendable { } +extension AsyncChunkedOnProjectionSequence.Iterator: Sendable + where Base.AsyncIterator: Sendable, Base.Element: Sendable, Subject: Sendable { } +``` + +### Count + +```swift +public struct AsyncChunksOfCountSequence: AsyncSequence + where Collected.Element == Base.Element { + public typealias Element = Collected + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Collected? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncChunksOfCountSequence : Sendable where Base : Sendable, Base.Element : Sendable { } +extension AsyncChunksOfCountSequence.Iterator : Sendable where Base.AsyncIterator : Sendable, Base.Element : Sendable { } + +``` + +### Count or Signal + +```swift +public struct AsyncChunksOfCountOrSignalSequence: AsyncSequence, Sendable + where + Collected.Element == Base.Element, + Base: Sendable, Signal: Sendable, + Base.AsyncIterator: Sendable, Signal.AsyncIterator: Sendable, + Base.Element: Sendable, Signal.Element: Sendable { + public typealias Element = Collected + + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async rethrows -> Collected? + } + + public func makeAsyncIterator() -> Iterator +} +``` + +### Timer + +```swift +public struct AsyncTimerSequence: AsyncSequence { + public typealias Element = C.Instant + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async -> C.Instant? + } + + public init( + interval: C.Instant.Duration, + tolerance: C.Instant.Duration? = nil, + clock: C + ) + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncTimerSequence where C == SuspendingClock { + public static func repeating(every interval: Duration, tolerance: Duration? = nil) -> AsyncTimerSequence +} + +extension AsyncTimerSequence: Sendable { } +``` + +## Alternatives Considered + +It was considered to make the chunked element to be an `AsyncSequence` instead of allowing collection into a `RangeReplaceableCollection` however it was determined that the throwing behavior of that would be complex to understand. If that hurdle could be overcome then that might be a future direction/consideration that would be worth exploring. + +Variants of `chunked(by:)` (grouping) and `chunked(on:)` (projection) methods could be added that take delimiting `Signal` and `AsyncTimerSequence` inputs similar to `chunked(byCount:or:)`. However, it was decided that such functionality was likely to be underutilized and not worth the complication to the already broad surface area of `chunked` methods. + +The naming of this family was considered to be `collect` which is used in APIs like `Combine`. This family of functions has distinct similarity to those APIs. + +## Credits/Inspiration + +This transformation function is a heavily inspired analog of the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Chunked.md) + +https://developer.apple.com/documentation/foundation/timer + +https://developer.apple.com/documentation/foundation/timer/timerpublisher From b6dff0060244063f8fff9f912d2c0a4ad610231c Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 3 Jan 2023 09:31:36 -0800 Subject: [PATCH 080/149] Transform the reductions and remove duplicates guides into a proposal (#233) --- Evolution/NNNN-reductions.md | 196 +++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 Evolution/NNNN-reductions.md diff --git a/Evolution/NNNN-reductions.md b/Evolution/NNNN-reductions.md new file mode 100644 index 00000000..d1d8ae5a --- /dev/null +++ b/Evolution/NNNN-reductions.md @@ -0,0 +1,196 @@ +# Reductions +* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-reductions.md) +* Author(s): [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** +* Implementation: [ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestReductions.swift) +] +* Decision Notes: +* Bugs: + +## Introduction + +The family of algorithms for reduce are useful for converting a sequence or asynchronous sequence into a single value, but that can elide important intermediate information. The _reductions_ algorithm is often called "scan", but this name does not convey its heritage to the family of reducing. + +There are two strategies that are usable for creating continuous reductions: exclusive reductions and inclusive reductions: + + * Exclusive reductions take a value and incorporate values into that initial value. A common example is reductions by appending to an array. + * Inclusive reductions transact only on the values provided. A common example is adding numbers. + + There are also common specializations of this algorithm that are worth offering as a shorthand. Particularly removing duplications that is a common task of reducing a sequence. When processing values over time it is definitely possible that the same value may occur in a row. When the distinctness of the presence value is not needed it is useful to consider the values over time that are differing from the last. Particularly this can be expressed as removing duplicate values either in the case as they are directly `Equatable` or by a predicate. + +## Proposed Solution + +Exclusive reductions come in two variants: transforming by application, or transformation via mutation. This replicates the same interface as `reduce(_:_:)` and `reduce(into:_:)`. Unlike the `reduce` algorithms, the `reductions` algorithm also comes in two flavors: throwing or non throwing transformations. + +```swift +extension AsyncSequence { + public func reductions( + _ initial: Result, + _ transform: @Sendable @escaping (Result, Element) async -> Result + ) -> AsyncExclusiveReductionsSequence + + public func reductions( + into initial: Result, + _ transform: @Sendable @escaping (inout Result, Element) async -> Void + ) -> AsyncExclusiveReductionsSequence +} + +extension AsyncSequence { + public func reductions( + _ initial: Result, + _ transform: @Sendable @escaping (Result, Element) async throws -> Result + ) -> AsyncThrowingExclusiveReductionsSequence + + public func reductions( + into initial: Result, + _ transform: @Sendable @escaping (inout Result, Element) async throws -> Void + ) -> AsyncThrowingExclusiveReductionsSequence +} +``` + +These APIs can be used to reduce an initial value progressively or reduce into an initial value via mutation. In practice, a common use case for reductions is to mutate a collection by appending values. + +```swift +characters.reductions(into: "") { $0.append($1) } +``` + +If the characters being produced asynchronously are `"a", "b", "c"`, then the iteration of the reductions is `"a", "ab", "abc"`. + +Inclusive reductions do not have an initial value and therefore do not need an additional variations beyond the throwing and non throwing flavors. + +```swift +extension AsyncSequence { + public func reductions( + _ transform: @Sendable @escaping (Element, Element) async -> Element + ) -> AsyncInclusiveReductionsSequence + + public func reductions( + _ transform: @Sendable @escaping (Element, Element) async throws -> Element + ) -> AsyncThrowingInclusiveReductionsSequence +} +``` + +This is often used for scenarios like a running tally or other similar cases. + +```swift +numbers.reductions { $0 + $1 } +``` + +In the above example, if the numbers are a sequence of `1, 2, 3, 4`, the produced values would be `1, 3, 6, 10`. + +The `removeDuplicates()` and `removeDuplicates(by:)` APIs serve this purpose of removing duplicate values that occur. These are special case optimizations in the family of the reductions APIs. These algorithms test against the previous value and if the latest iteration of the base `AsyncSequence` is the same as the last it invokes `next()` again. The resulting `AsyncRemoveDuplicatesSequence` will ensure that no duplicate values occur next to each other. This should not be confused with only emitting unique new values; where each value is tested against a collected set of values. + +```swift +extension AsyncSequence where Element: Equatable { + public func removeDuplicates() -> AsyncRemoveDuplicatesSequence +} + +extension AsyncSequence { + public func removeDuplicates( + by predicate: @escaping @Sendable (Element, Element) async -> Bool + ) -> AsyncRemoveDuplicatesSequence + + public func removeDuplicates( + by predicate: @escaping @Sendable (Element, Element) async throws -> Bool + ) -> AsyncThrowingRemoveDuplicatesSequence +} +``` + +The `removeDuplicates` family comes in three variants. One variant is conditional upon the `Element` type being `Equatable`. This variation is a shorthand for writing `.removeDuplicates { $0 == $1 }`. The next variation is the closure version that allows for custom predicates to be applied. This algorithm allows for the cases where the elements themselves may not be equatable but portions of the element may be compared. Lastly is the variation that allows for comparison when the comparison method may throw. + +## Detailed Design + +#### Reductions + +The exclusive reduction variants come in two distinct cases: non-throwing and throwing. These both have corresponding types to encompass that throwing behavior. + +For non-throwing exclusive reductions, the element type of the sequence is the result of the reduction transform. `AsyncExclusiveReductionsSequence` will throw if the base asynchronous sequence throws, and will not throw if the base does not throws. + +```swift +public struct AsyncExclusiveReductionsSequence { +} + +extension AsyncExclusiveReductionsSequence: AsyncSequence { + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncExclusiveReductionsSequence: Sendable + where Base: Sendable, Element: Sendable { } + +``` + +The sendability behavior of `AsyncExclusiveReductionsSequence` is such that when the base, base iterator, and element are `Sendable` then `AsyncExclusiveReductionsSequence` is `Sendable`. + +```swift +public struct AsyncThrowingExclusiveReductionsSequence { +} + +extension AsyncThrowingExclusiveReductionsSequence: AsyncSequence { + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async throws -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncThrowingExclusiveReductionsSequence: Sendable + where Base: Sendable, Element: Sendable { } + +``` + +#### Remove Duplicates + +In the cases where the `Element` type is `Equatable` or the non-trowing predicate variant these utilize the type `AsyncRemoveDuplicatesSequence`. The throwing predicate variant uses `AsyncThrowingRemoveDuplicatesSequence`. Both of these types are conditionally `Sendable` when the base, base element, and base iterator are `Sendable` + +The `AsyncRemoveDuplicatesSequence` will rethrow if the base asynchronous sequence throws and will not throw if the base asynchronous sequence does not throw. + +```swift +public struct AsyncRemoveDuplicatesSequence: AsyncSequence { + public typealias Element = Base.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } + + public func makeAsyncIterator() -> Iterator { + Iterator(iterator: base.makeAsyncIterator(), predicate: predicate) + } +} + +extension AsyncRemoveDuplicatesSequence: Sendable + where Base: Sendable, Base.Element: Sendable { } + +``` + +The `AsyncThrowingRemoveDuplicatesSequence` will rethrow if the base asynchronous sequence throws and still may throw if the base asynchronous sequence does not throw due to the predicate having the potential of throwing. + +```swift + +public struct AsyncThrowingRemoveDuplicatesSequence: AsyncSequence { + public typealias Element = Base.Element + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async throws -> Element? + } + + public func makeAsyncIterator() -> Iterator +} + +extension AsyncThrowingRemoveDuplicatesSequence: Sendable + where Base: Sendable, Base.Element: Sendable { } + +``` + +## Alternatives Considered + +One alternate name for `reductions` was to name it `scan`; however the naming from the Swift Algorithms package offers considerably more inference to the heritage of what family of functions this algorithm belongs to. + +## Credits/Inspiration + +This transformation function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md) From aed5422380244498344a036b8d94e27f370d9a22 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 3 Jan 2023 09:31:58 -0800 Subject: [PATCH 081/149] AsyncLazySequence proposal (#189) --- Evolution/NNNN-lazy.md | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 Evolution/NNNN-lazy.md diff --git a/Evolution/NNNN-lazy.md b/Evolution/NNNN-lazy.md new file mode 100644 index 00000000..f2d84580 --- /dev/null +++ b/Evolution/NNNN-lazy.md @@ -0,0 +1,48 @@ +# AsyncLazySequence + +* Proposal: [NNNN](NNNN-lazy.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Status: **Implemented** + +* Implementation: + [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncLazySequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift) + +## Introduction + +`AsyncLazySequence` 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 +``` + +This transformation can be useful to test operations specifically available on `AsyncSequence` but also is useful +to combine with other `AsyncSequence` types to provide well known sources of data. + +The `.async` property returns an `AsyncLazySequence` that is generic upon the base `Sequence` it was constructed from. + +```swift +extension Sequence { + public var async: AsyncLazySequence { get } +} + +public struct AsyncLazySequence: AsyncSequence { + ... +} + +extension AsyncLazySequence: Sendable where Base: Sendable { } +extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable { } +``` + +### Naming + +This property's and type's name match the naming approaches in the Swift standard library. The property is named with a +succinct name in inspiration from `.lazy`, and the type is named in reference to the lazy behavior of the constructed +`AsyncSequence`. + +## Effect on API resilience + +`AsyncLazySequence` has a trivial implementation and is marked as `@frozen` and `@inlinable`. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable. From 3f12423c47033a909fe217482b05e7ff979bea3e Mon Sep 17 00:00:00 2001 From: Dan Appel Date: Tue, 3 Jan 2023 15:15:34 -0800 Subject: [PATCH 082/149] Add precondition guards to pthread_mutex calls (#243) While in the current configuration lock/unlock shouldn't ever fail, it seems polite to guard against basic misuse in debug builds. Co-authored-by: Dan Appel --- Sources/AsyncAlgorithms/Locking.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 74396080..b23ba30f 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -37,7 +37,7 @@ internal struct Lock { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) #elseif canImport(Glibc) - pthread_mutex_init(platformLock, nil) + precondition(pthread_mutex_init(platformLock, nil) == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) InitializeSRWLock(platformLock) #endif @@ -45,7 +45,7 @@ internal struct Lock { fileprivate static func deinitialize(_ platformLock: PlatformLock) { #if canImport(Glibc) - pthread_mutex_destroy(platformLock) + precondition(pthread_mutex_destroy(platformLock) == 0, "pthread_mutex_destroy failed") #endif platformLock.deinitialize(count: 1) } @@ -64,7 +64,7 @@ internal struct Lock { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) #elseif canImport(Glibc) - pthread_mutex_unlock(platformLock) + precondition(pthread_mutex_unlock(platformLock) == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) #endif From e7b9b6039d36bef31b485516b6a877e02bc91b45 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 3 Jan 2023 15:24:40 -0800 Subject: [PATCH 083/149] Extract the result to test the preconditions seperate from the function calls for locking (#244) --- Sources/AsyncAlgorithms/Locking.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index b23ba30f..4e8e246c 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -37,7 +37,8 @@ internal struct Lock { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) #elseif canImport(Glibc) - precondition(pthread_mutex_init(platformLock, nil) == 0, "pthread_mutex_init failed") + let result = pthread_mutex_init(platformLock, nil) + precondition(result == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) InitializeSRWLock(platformLock) #endif @@ -45,7 +46,8 @@ internal struct Lock { fileprivate static func deinitialize(_ platformLock: PlatformLock) { #if canImport(Glibc) - precondition(pthread_mutex_destroy(platformLock) == 0, "pthread_mutex_destroy failed") + let result = pthread_mutex_destroy(platformLock) + precondition(result == 0, "pthread_mutex_destroy failed") #endif platformLock.deinitialize(count: 1) } @@ -64,7 +66,8 @@ internal struct Lock { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) #elseif canImport(Glibc) - precondition(pthread_mutex_unlock(platformLock) == 0, "pthread_mutex_unlock failed") + let result = pthread_mutex_unlock(platformLock) + precondition(result == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) #endif From a07a3f41b5165e73757de6a30dc8eec81a0e356f Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 10 Jan 2023 15:36:42 -0800 Subject: [PATCH 084/149] Rename AsyncLazySequence to AsyncSyncSequence per review feedback (#246) * Rename AsyncLazySequence to AsyncSyncSequence per review feedback * Rename and number the async proposal --- Evolution/NNNN-lazy.md => 0009-async.md | 18 +++++++++--------- .../AsyncAlgorithms.docc/Guides/Effects.md | 4 ++-- .../AsyncAlgorithms.docc/Guides/Lazy.md | 14 +++++++------- .../AsyncChunksOfCountOrSignalSequence.swift | 2 +- ...ySequence.swift => AsyncSyncSequence.swift} | 8 ++++---- Tests/AsyncAlgorithmsTests/TestJoin.swift | 14 +++++++------- 6 files changed, 30 insertions(+), 30 deletions(-) rename Evolution/NNNN-lazy.md => 0009-async.md (71%) rename Sources/AsyncAlgorithms/{AsyncLazySequence.swift => AsyncSyncSequence.swift} (89%) diff --git a/Evolution/NNNN-lazy.md b/0009-async.md similarity index 71% rename from Evolution/NNNN-lazy.md rename to 0009-async.md index f2d84580..897e57b8 100644 --- a/Evolution/NNNN-lazy.md +++ b/0009-async.md @@ -1,16 +1,16 @@ -# AsyncLazySequence +# AsyncSyncSequence * Proposal: [NNNN](NNNN-lazy.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** * Implementation: - [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncLazySequence.swift) | + [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) ## Introduction -`AsyncLazySequence` converts a non-asynchronous sequence into an asynchronous one. +`AsyncSyncSequence` converts a non-asynchronous sequence into an asynchronous one. This operation is available for all `Sequence` types. @@ -22,19 +22,19 @@ let characters = "abcde".async This transformation can be useful to test operations specifically available on `AsyncSequence` but also is useful to combine with other `AsyncSequence` types to provide well known sources of data. -The `.async` property returns an `AsyncLazySequence` that is generic upon the base `Sequence` it was constructed from. +The `.async` property returns an `AsyncSyncSequence` that is generic upon the base `Sequence` it was constructed from. ```swift extension Sequence { - public var async: AsyncLazySequence { get } + public var async: AsyncSyncSequence { get } } -public struct AsyncLazySequence: AsyncSequence { +public struct AsyncSyncSequence: AsyncSequence { ... } -extension AsyncLazySequence: Sendable where Base: Sendable { } -extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable { } +extension AsyncSyncSequence: Sendable where Base: Sendable { } +extension AsyncSyncSequence.Iterator: Sendable where Base.Iterator: Sendable { } ``` ### Naming @@ -45,4 +45,4 @@ succinct name in inspiration from `.lazy`, and the type is named in reference to ## Effect on API resilience -`AsyncLazySequence` has a trivial implementation and is marked as `@frozen` and `@inlinable`. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable. +`AsyncSyncSequence` has a trivial implementation and is marked as `@frozen` and `@inlinable`. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable. diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md index 5d7a5967..82830bb6 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md @@ -34,8 +34,8 @@ | `AsyncInterspersedSequence.Iterator` | rethrows | Not Sendable| | `AsyncJoinedSequence` | rethrows | Conditional | | `AsyncJoinedSequence.Iterator` | rethrows | Not Sendable| -| `AsyncLazySequence` | non-throwing | Conditional | -| `AsyncLazySequence.Iterator` | non-throwing | Not Sendable| +| `AsyncSyncSequence` | non-throwing | Conditional | +| `AsyncSyncSequence.Iterator` | non-throwing | Not Sendable| | `AsyncLimitBuffer` | non-throwing | Sendable | | `AsyncMerge2Sequence` | rethrows | Sendable | | `AsyncMerge2Sequence.Iterator` | rethrows | Not Sendable| diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md index 3254ab08..fe5dda9d 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md @@ -1,6 +1,6 @@ -# AsyncLazySequence +# AsyncSyncSequence -[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncLazySequence.swift) | +[[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. @@ -17,19 +17,19 @@ to combine with other `AsyncSequence` types to provide well known sources of dat ## Detailed Design -The `.async` property returns an `AsyncLazySequence` that is generic upon the base `Sequence` it was constructed from. +The `.async` property returns an `AsyncSyncSequence` that is generic upon the base `Sequence` it was constructed from. ```swift extension Sequence { - public var async: AsyncLazySequence { get } + public var async: AsyncSyncSequence { get } } -public struct AsyncLazySequence: AsyncSequence { +public struct AsyncSyncSequence: AsyncSequence { ... } -extension AsyncLazySequence: Sendable where Base: Sendable { } -extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable { } +extension AsyncSyncSequence: Sendable where Base: Sendable { } +extension AsyncSyncSequence.Iterator: Sendable where Base.Iterator: Sendable { } ``` ### Naming diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index 8e4ce7b5..3fce684d 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -70,7 +70,7 @@ public struct AsyncChunksOfCountOrSignalSequence typealias EitherMappedSignal = AsyncMapSequence - typealias ChainedBase = AsyncChain2Sequence> + typealias ChainedBase = AsyncChain2Sequence> typealias Merged = AsyncMerge2Sequence let count: Int? diff --git a/Sources/AsyncAlgorithms/AsyncLazySequence.swift b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift similarity index 89% rename from Sources/AsyncAlgorithms/AsyncLazySequence.swift rename to Sources/AsyncAlgorithms/AsyncSyncSequence.swift index 9a8abd77..6710df7c 100644 --- a/Sources/AsyncAlgorithms/AsyncLazySequence.swift +++ b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift @@ -14,8 +14,8 @@ extension Sequence { /// but on which operations, such as `map` and `filter`, are /// implemented asynchronously. @inlinable - public var async: AsyncLazySequence { - AsyncLazySequence(self) + public var async: AsyncSyncSequence { + AsyncSyncSequence(self) } } @@ -28,7 +28,7 @@ extension Sequence { /// This functions similarly to `LazySequence` by accessing elements sequentially /// in the iterator's `next()` method. @frozen -public struct AsyncLazySequence: AsyncSequence { +public struct AsyncSyncSequence: AsyncSequence { public typealias Element = Base.Element @frozen @@ -66,4 +66,4 @@ public struct AsyncLazySequence: AsyncSequence { } } -extension AsyncLazySequence: Sendable where Base: Sendable { } +extension AsyncSyncSequence: Sendable where Base: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestJoin.swift b/Tests/AsyncAlgorithmsTests/TestJoin.swift index 04fd1772..91de1ef9 100644 --- a/Tests/AsyncAlgorithmsTests/TestJoin.swift +++ b/Tests/AsyncAlgorithmsTests/TestJoin.swift @@ -13,8 +13,8 @@ import XCTest import AsyncAlgorithms extension Sequence where Element: Sequence, Element.Element: Equatable & Sendable { - func nestedAsync(throwsOn bad: Element.Element) -> AsyncLazySequence<[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 +22,7 @@ extension Sequence where Element: Sequence, Element.Element: Equatable & Sendabl } extension Sequence where Element: Sequence, Element.Element: Sendable { - var nestedAsync : AsyncLazySequence<[AsyncLazySequence]> { + var nestedAsync : AsyncSyncSequence<[AsyncSyncSequence]> { return self.map { $0.async }.async } } @@ -55,7 +55,7 @@ final class TestJoinedBySeparator: XCTestCase { } func test_join_empty() async { - let sequences = [AsyncLazySequence<[Int]>]().async + let sequences = [AsyncSyncSequence<[Int]>]().async var iterator = sequences.joined(separator: [-1, -2, -3].async).makeAsyncIterator() let expected = [Int]() var actual = [Int]() @@ -105,7 +105,7 @@ final class TestJoinedBySeparator: XCTestCase { } func test_cancellation() async { - let source : AsyncLazySequence<[AsyncLazySequence>]> = [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") @@ -158,7 +158,7 @@ final class TestJoined: XCTestCase { } func test_join_empty() async { - let sequences = [AsyncLazySequence<[Int]>]().async + let sequences = [AsyncSyncSequence<[Int]>]().async var iterator = sequences.joined().makeAsyncIterator() let expected = [Int]() var actual = [Int]() @@ -189,7 +189,7 @@ final class TestJoined: XCTestCase { } func test_cancellation() async { - let source : AsyncLazySequence<[AsyncLazySequence>]> = [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") From 0ebc8058fc12663699fd364f94193ffc7322840f Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Thu, 12 Jan 2023 18:40:37 +0100 Subject: [PATCH 085/149] channel: re-implement with a state machine (#235) --- Sources/AsyncAlgorithms/AsyncChannel.swift | 275 -------------- .../AsyncThrowingChannel.swift | 320 ---------------- .../Channels/AsyncChannel.swift | 60 +++ .../Channels/AsyncThrowingChannel.swift | 63 ++++ .../Channels/ChannelStateMachine.swift | 344 ++++++++++++++++++ .../Channels/ChannelStorage.swift | 149 ++++++++ .../Performance/TestThroughput.swift | 6 + .../Performance/ThroughputMeasurement.swift | 58 +++ Tests/AsyncAlgorithmsTests/TestChannel.swift | 339 ++++++----------- .../TestThrowingChannel.swift | 313 ++++++++++++++++ 10 files changed, 1112 insertions(+), 815 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncChannel.swift delete mode 100644 Sources/AsyncAlgorithms/AsyncThrowingChannel.swift create mode 100644 Sources/AsyncAlgorithms/Channels/AsyncChannel.swift create mode 100644 Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift create mode 100644 Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Channels/ChannelStorage.swift create mode 100644 Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift diff --git a/Sources/AsyncAlgorithms/AsyncChannel.swift b/Sources/AsyncAlgorithms/AsyncChannel.swift deleted file mode 100644 index 1b41c779..00000000 --- a/Sources/AsyncAlgorithms/AsyncChannel.swift +++ /dev/null @@ -1,275 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -@preconcurrency @_implementationOnly import OrderedCollections - -/// A channel for sending elements from one task to another with back pressure. -/// -/// The `AsyncChannel` class is intended to be used as a communication type between tasks, -/// particularly when one task produces values and another task consumes those values. The back -/// pressure applied by `send(_:)` via the suspension/resume ensures that -/// the production of values does not exceed the consumption of values from iteration. This method -/// suspends after enqueuing the event and is resumed when the next call to `next()` -/// on the `Iterator` is made, or when `finish()` is called from another Task. -/// As `finish()` induces a terminal state, there is no need for a back pressure management. -/// This function does not suspend and will finish all the pending iterations. -public final class AsyncChannel: AsyncSequence, Sendable { - /// The iterator for a `AsyncChannel` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - let channel: AsyncChannel - var active: Bool = true - - init(_ channel: AsyncChannel) { - self.channel = channel - } - - /// Await the next sent element or finish. - public mutating func next() async -> Element? { - guard active else { - return nil - } - - let generation = channel.establish() - let nextTokenStatus = ManagedCriticalState(.new) - - let value = await withTaskCancellationHandler { - await channel.next(nextTokenStatus, generation) - } onCancel: { [channel] in - channel.cancelNext(nextTokenStatus, generation) - } - - if let value { - return value - } else { - active = false - return nil - } - } - } - - typealias Pending = ChannelToken?, Never>> - typealias Awaiting = ChannelToken> - - struct ChannelToken: Hashable, Sendable { - var generation: Int - var continuation: Continuation? - - init(generation: Int, continuation: Continuation) { - self.generation = generation - self.continuation = continuation - } - - init(placeholder generation: Int) { - self.generation = generation - self.continuation = nil - } - - func hash(into hasher: inout Hasher) { - hasher.combine(generation) - } - - static func == (_ lhs: ChannelToken, _ rhs: ChannelToken) -> Bool { - return lhs.generation == rhs.generation - } - } - - enum ChannelTokenStatus: Equatable { - case new - case cancelled - } - - enum Emission : Sendable { - case idle - case pending(OrderedSet) - case awaiting(OrderedSet) - case finished - } - - struct State : Sendable { - var emission: Emission = .idle - var generation = 0 - } - - let state = ManagedCriticalState(State()) - - /// Create a new `AsyncChannel` given an element type. - public init(element elementType: Element.Type = Element.self) { } - - func establish() -> Int { - state.withCriticalRegion { state in - defer { state.generation &+= 1 } - return state.generation - } - } - - func cancelNext(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state in - let continuation: UnsafeContinuation? - - switch state.emission { - case .awaiting(var nexts): - continuation = nexts.remove(Awaiting(placeholder: generation))?.continuation - if nexts.isEmpty { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } - default: - continuation = nil - } - - nextTokenStatus.withCriticalRegion { status in - if status == .new { - status = .cancelled - } - } - - continuation?.resume(returning: nil) - } - } - - func next(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) async -> Element? { - return await withUnsafeContinuation { (continuation: UnsafeContinuation) in - var cancelled = false - var terminal = false - state.withCriticalRegion { state in - - if nextTokenStatus.withCriticalRegion({ $0 }) == .cancelled { - cancelled = true - } - - switch state.emission { - case .idle: - state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) - case .pending(var sends): - let send = sends.removeFirst() - if sends.count == 0 { - state.emission = .idle - } else { - state.emission = .pending(sends) - } - send.continuation?.resume(returning: continuation) - case .awaiting(var nexts): - nexts.updateOrAppend(Awaiting(generation: generation, continuation: continuation)) - state.emission = .awaiting(nexts) - case .finished: - terminal = true - } - } - - if cancelled || terminal { - continuation.resume(returning: nil) - } - } - } - - func cancelSend(_ sendTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state in - let continuation: UnsafeContinuation?, Never>? - - switch state.emission { - case .pending(var sends): - let send = sends.remove(Pending(placeholder: generation)) - if sends.isEmpty { - state.emission = .idle - } else { - state.emission = .pending(sends) - } - continuation = send?.continuation - default: - continuation = nil - } - - sendTokenStatus.withCriticalRegion { status in - if status == .new { - status = .cancelled - } - } - - continuation?.resume(returning: nil) - } - } - - func send(_ sendTokenStatus: ManagedCriticalState, _ generation: Int, _ element: Element) async { - let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state in - - if sendTokenStatus.withCriticalRegion({ $0 }) == .cancelled { - continuation.resume(returning: nil) - return - } - - switch state.emission { - case .idle: - state.emission = .pending([Pending(generation: generation, continuation: continuation)]) - case .pending(var sends): - sends.updateOrAppend(Pending(generation: generation, continuation: continuation)) - state.emission = .pending(sends) - case .awaiting(var nexts): - let next = nexts.removeFirst().continuation - if nexts.count == 0 { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } - continuation.resume(returning: next) - case .finished: - continuation.resume(returning: nil) - } - } - } - continuation?.resume(returning: element) - } - - /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made - /// or when a call to `finish()` is made from another Task. - /// If the channel is already finished then this returns immediately - /// If the task is cancelled, this function will resume. Other sending operations from other tasks will remain active. - public func send(_ element: Element) async { - let generation = establish() - let sendTokenStatus = ManagedCriticalState(.new) - - await withTaskCancellationHandler { - await send(sendTokenStatus, generation, element) - } onCancel: { [weak self] in - self?.cancelSend(sendTokenStatus, generation) - } - } - - /// Send a finish to all awaiting iterations. - /// All subsequent calls to `next(_:)` will resume immediately. - public func finish() { - state.withCriticalRegion { state in - - defer { state.emission = .finished } - - switch state.emission { - case .pending(let sends): - for send in sends { - send.continuation?.resume(returning: nil) - } - case .awaiting(let nexts): - for next in nexts { - next.continuation?.resume(returning: nil) - } - default: - break - } - } - - - } - - /// Create an `Iterator` for iteration of an `AsyncChannel` - public func makeAsyncIterator() -> Iterator { - return Iterator(self) - } -} diff --git a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift deleted file mode 100644 index 8359287e..00000000 --- a/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift +++ /dev/null @@ -1,320 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -@preconcurrency @_implementationOnly import OrderedCollections - -/// An error-throwing channel for sending elements from on task to another with back pressure. -/// -/// The `AsyncThrowingChannel` class is intended to be used as a communication types between tasks, -/// particularly when one task produces values and another task consumes those values. The back -/// pressure applied by `send(_:)` via suspension/resume ensures that the production of values does -/// not exceed the consumption of values from iteration. This method suspends after enqueuing the event -/// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called -/// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no need for a back pressure management. -/// Those functions do not suspend and will finish all the pending iterations. -public final class AsyncThrowingChannel: AsyncSequence, Sendable { - /// The iterator for an `AsyncThrowingChannel` instance. - public struct Iterator: AsyncIteratorProtocol, Sendable { - let channel: AsyncThrowingChannel - var active: Bool = true - - init(_ channel: AsyncThrowingChannel) { - self.channel = channel - } - - public mutating func next() async throws -> Element? { - guard active else { - return nil - } - - let generation = channel.establish() - let nextTokenStatus = ManagedCriticalState(.new) - - do { - let value = try await withTaskCancellationHandler { - try await channel.next(nextTokenStatus, generation) - } onCancel: { [channel] in - channel.cancelNext(nextTokenStatus, generation) - } - - if let value = value { - return value - } else { - active = false - return nil - } - } catch { - active = false - throw error - } - } - } - - typealias Pending = ChannelToken?, Never>> - typealias Awaiting = ChannelToken> - - struct ChannelToken: Hashable, Sendable { - var generation: Int - var continuation: Continuation? - - init(generation: Int, continuation: Continuation) { - self.generation = generation - self.continuation = continuation - } - - init(placeholder generation: Int) { - self.generation = generation - self.continuation = nil - } - - func hash(into hasher: inout Hasher) { - hasher.combine(generation) - } - - static func == (_ lhs: ChannelToken, _ rhs: ChannelToken) -> Bool { - return lhs.generation == rhs.generation - } - } - - - enum ChannelTokenStatus: Equatable { - case new - case cancelled - } - - enum Termination { - case finished - case failed(Error) - } - - enum Emission: Sendable { - case idle - case pending(OrderedSet) - case awaiting(OrderedSet) - case terminated(Termination) - } - - struct State : Sendable { - var emission: Emission = .idle - var generation = 0 - } - - let state = ManagedCriticalState(State()) - - public init(_ elementType: Element.Type = Element.self) { } - - func establish() -> Int { - state.withCriticalRegion { state in - defer { state.generation &+= 1 } - return state.generation - } - } - - func cancelNext(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state in - let continuation: UnsafeContinuation? - - switch state.emission { - case .awaiting(var nexts): - continuation = nexts.remove(Awaiting(placeholder: generation))?.continuation - if nexts.isEmpty { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } - default: - continuation = nil - } - - nextTokenStatus.withCriticalRegion { status in - if status == .new { - status = .cancelled - } - } - - continuation?.resume(returning: nil) - } - } - - func next(_ nextTokenStatus: ManagedCriticalState, _ generation: Int) async throws -> Element? { - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - var cancelled = false - var potentialTermination: Termination? - - state.withCriticalRegion { state in - - if nextTokenStatus.withCriticalRegion({ $0 }) == .cancelled { - cancelled = true - return - } - - switch state.emission { - case .idle: - state.emission = .awaiting([Awaiting(generation: generation, continuation: continuation)]) - case .pending(var sends): - let send = sends.removeFirst() - if sends.count == 0 { - state.emission = .idle - } else { - state.emission = .pending(sends) - } - send.continuation?.resume(returning: continuation) - case .awaiting(var nexts): - nexts.updateOrAppend(Awaiting(generation: generation, continuation: continuation)) - state.emission = .awaiting(nexts) - case .terminated(let termination): - potentialTermination = termination - state.emission = .terminated(.finished) - } - } - - if cancelled { - continuation.resume(returning: nil) - return - } - - switch potentialTermination { - case .none: - return - case .failed(let error): - continuation.resume(throwing: error) - return - case .finished: - continuation.resume(returning: nil) - return - } - } - } - - func cancelSend(_ sendTokenStatus: ManagedCriticalState, _ generation: Int) { - state.withCriticalRegion { state in - let continuation: UnsafeContinuation?, Never>? - - switch state.emission { - case .pending(var sends): - let send = sends.remove(Pending(placeholder: generation)) - if sends.isEmpty { - state.emission = .idle - } else { - state.emission = .pending(sends) - } - continuation = send?.continuation - default: - continuation = nil - } - - sendTokenStatus.withCriticalRegion { status in - if status == .new { - status = .cancelled - } - } - - continuation?.resume(returning: nil) - } - } - - func send(_ sendTokenStatus: ManagedCriticalState, _ generation: Int, _ element: Element) async { - let continuation: UnsafeContinuation? = await withUnsafeContinuation { continuation in - state.withCriticalRegion { state in - - if sendTokenStatus.withCriticalRegion({ $0 }) == .cancelled { - continuation.resume(returning: nil) - return - } - - switch state.emission { - case .idle: - state.emission = .pending([Pending(generation: generation, continuation: continuation)]) - case .pending(var sends): - sends.updateOrAppend(Pending(generation: generation, continuation: continuation)) - state.emission = .pending(sends) - case .awaiting(var nexts): - let next = nexts.removeFirst().continuation - if nexts.count == 0 { - state.emission = .idle - } else { - state.emission = .awaiting(nexts) - } - continuation.resume(returning: next) - case .terminated: - continuation.resume(returning: nil) - } - } - } - continuation?.resume(returning: element) - } - - func terminateAll(error: Failure? = nil) { - state.withCriticalRegion { state in - - let nextState: Emission - if let error = error { - nextState = .terminated(.failed(error)) - } else { - nextState = .terminated(.finished) - } - - switch state.emission { - case .idle: - state.emission = nextState - case .pending(let sends): - state.emission = nextState - for send in sends { - send.continuation?.resume(returning: nil) - } - case .awaiting(let nexts): - state.emission = nextState - if let error = error { - for next in nexts { - next.continuation?.resume(throwing: error) - } - } else { - for next in nexts { - next.continuation?.resume(returning: nil) - } - } - case .terminated: - break - } - } - } - - /// Send an element to an awaiting iteration. This function will resume when the next call to `next()` is made - /// or when a call to `finish()`/`fail(_:)` is made from another Task. - /// If the channel is already finished then this returns immediately - /// If the task is cancelled, this function will resume. Other sending operations from other tasks will remain active. - public func send(_ element: Element) async { - let generation = establish() - let sendTokenStatus = ManagedCriticalState(.new) - - await withTaskCancellationHandler { - await send(sendTokenStatus, generation, element) - } onCancel: { [weak self] in - self?.cancelSend(sendTokenStatus, generation) - } - } - - /// Send an error to all awaiting iterations. - /// All subsequent calls to `next(_:)` will resume immediately. - public func fail(_ error: Error) where Failure == Error { - terminateAll(error: error) - } - - /// Send a finish to all awaiting iterations. - /// All subsequent calls to `next(_:)` will resume immediately. - public func finish() { - terminateAll() - } - - public func makeAsyncIterator() -> Iterator { - return Iterator(self) - } -} diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift new file mode 100644 index 00000000..75becf2d --- /dev/null +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A channel for sending elements from one task to another with back pressure. +/// +/// The `AsyncChannel` class is intended to be used as a communication type between tasks, +/// particularly when one task produces values and another task consumes those values. The back +/// pressure applied by `send(_:)` via the suspension/resume ensures that +/// the production of values does not exceed the consumption of values from iteration. This method +/// suspends after enqueuing the event and is resumed when the next call to `next()` +/// on the `Iterator` is made, or when `finish()` is called from another Task. +/// As `finish()` induces a terminal state, there is no more need for a back pressure management. +/// This function does not suspend and will finish all the pending iterations. +public final class AsyncChannel: AsyncSequence, @unchecked Sendable { + public typealias Element = Element + public typealias AsyncIterator = Iterator + + let storage: ChannelStorage + + public init() { + self.storage = ChannelStorage() + } + + /// Sends an element to an awaiting iteration. This function will resume when the next call to `next()` is made + /// or when a call to `finish()` is made from another task. + /// If the channel is already finished then this returns immediately. + /// If the task is cancelled, this function will resume without sending the element. + /// Other sending operations from other tasks will remain active. + public func send(_ element: Element) async { + await self.storage.send(element: element) + } + + /// Immediately resumes all the suspended operations. + /// All subsequent calls to `next(_:)` will resume immediately. + public func finish() { + self.storage.finish() + } + + public func makeAsyncIterator() -> Iterator { + Iterator(storage: self.storage) + } + + public struct Iterator: AsyncIteratorProtocol { + let storage: ChannelStorage + + public mutating func next() async -> Element? { + // Although the storage can throw, its usage in the context of an `AsyncChannel` guarantees it cannot. + // There is no public way of sending a failure to it. + try! await self.storage.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift new file mode 100644 index 00000000..28de36ae --- /dev/null +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// An error-throwing channel for sending elements from on task to another with back pressure. +/// +/// The `AsyncThrowingChannel` class is intended to be used as a communication types between tasks, +/// particularly when one task produces values and another task consumes those values. The back +/// pressure applied by `send(_:)` via suspension/resume ensures that the production of values does +/// not exceed the consumption of values from iteration. This method suspends after enqueuing the event +/// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called +/// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. +/// Those functions do not suspend and will finish all the pending iterations. +public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { + public typealias Element = Element + public typealias AsyncIterator = Iterator + + let storage: ChannelStorage + + public init() { + self.storage = ChannelStorage() + } + + /// Sends an element to an awaiting iteration. This function will resume when the next call to `next()` is made + /// or when a call to `finish()` or `fail` is made from another task. + /// If the channel is already finished then this returns immediately. + /// If the task is cancelled, this function will resume without sending the element. + /// Other sending operations from other tasks will remain active. + public func send(_ element: Element) async { + await self.storage.send(element: element) + } + + /// Sends an error to all awaiting iterations. + /// All subsequent calls to `next(_:)` will resume immediately. + public func fail(_ error: Error) where Failure == Error { + self.storage.finish(error: error) + } + + /// Immediately resumes all the suspended operations. + /// All subsequent calls to `next(_:)` will resume immediately. + public func finish() { + self.storage.finish() + } + + public func makeAsyncIterator() -> Iterator { + Iterator(storage: self.storage) + } + + public struct Iterator: AsyncIteratorProtocol { + let storage: ChannelStorage + + public mutating func next() async throws -> Element? { + try await self.storage.next() + } + } +} diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift new file mode 100644 index 00000000..2972c754 --- /dev/null +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -0,0 +1,344 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +@_implementationOnly import OrderedCollections + +struct ChannelStateMachine: Sendable { + private struct SuspendedProducer: Hashable { + let id: UInt64 + let continuation: UnsafeContinuation? + let element: Element? + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + static func == (_ lhs: SuspendedProducer, _ rhs: SuspendedProducer) -> Bool { + return lhs.id == rhs.id + } + + static func placeHolder(id: UInt64) -> SuspendedProducer { + SuspendedProducer(id: id, continuation: nil, element: nil) + } + } + + private struct SuspendedConsumer: Hashable { + let id: UInt64 + let continuation: UnsafeContinuation? + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + static func == (_ lhs: SuspendedConsumer, _ rhs: SuspendedConsumer) -> Bool { + return lhs.id == rhs.id + } + + static func placeHolder(id: UInt64) -> SuspendedConsumer { + SuspendedConsumer(id: id, continuation: nil) + } + } + + private enum Termination { + case finished + case failed(Error) + } + + private enum State { + case channeling( + suspendedProducers: OrderedSet, + cancelledProducers: Set, + suspendedConsumers: OrderedSet, + cancelledConsumers: Set + ) + case terminated(Termination) + } + + private var state: State = .channeling(suspendedProducers: [], cancelledProducers: [], suspendedConsumers: [], cancelledConsumers: []) + + enum SendAction { + case resumeConsumer(continuation: UnsafeContinuation?) + case suspend + } + + 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) + } + } + + enum SendSuspendedAction { + case resumeProducer + case resumeProducerAndConsumer(continuation: UnsafeContinuation?) + } + + mutating func sendSuspended( + continuation: UnsafeContinuation, + element: Element, + 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: + return .resumeProducer + } + } + + enum SendCancelledAction { + case none + case resumeProducer(continuation: UnsafeContinuation?) + } + + 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) + } + + // 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 + } + } + + enum FinishAction { + case none + case resumeProducersAndConsumers( + producerSontinuations: [UnsafeContinuation?], + consumerContinuations: [UnsafeContinuation?] + ) + } + + 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) + } + } else { + self.state = .terminated(.finished) + } + return .resumeProducersAndConsumers( + producerSontinuations: suspendedProducers.map { $0.continuation }, + consumerContinuations: suspendedConsumers.map { $0.continuation } + ) + + case .terminated: + return .none + } + } + + enum NextAction { + case resumeProducer(continuation: UnsafeContinuation?, result: Result) + case suspend + } + + 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)) + } + } + + enum NextSuspendedAction { + case resumeConsumer(element: Element?) + case resumeConsumerWithError(error: Error) + case resumeProducerAndConsumer(continuation: UnsafeContinuation?, element: Element?) + } + + mutating func nextSuspended( + continuation: UnsafeContinuation, + 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): + return .resumeConsumer(element: nil) + + case .terminated(.failed(let error)): + self.state = .terminated(.finished) + return .resumeConsumerWithError(error: error) + } + } + + enum NextCancelledAction { + case none + case resumeConsumer(continuation: UnsafeContinuation?) + } + + 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) + } + + // 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 new file mode 100644 index 00000000..da398dbc --- /dev/null +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -0,0 +1,149 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +struct ChannelStorage: Sendable { + private let stateMachine: ManagedCriticalState> + private let ids = ManagedCriticalState(0) + + init() { + self.stateMachine = ManagedCriticalState(ChannelStateMachine()) + } + + func generateId() -> UInt64 { + self.ids.withCriticalRegion { ids in + defer { ids &+= 1 } + return ids + } + } + + func send(element: Element) async { + // check if a suspension is needed + let shouldExit = self.stateMachine.withCriticalRegion { stateMachine -> Bool in + let action = stateMachine.send() + + switch action { + case .suspend: + // the element has not been delivered because no consumer available, we must suspend + return false + case .resumeConsumer(let continuation): + continuation?.resume(returning: element) + return true + } + } + + if shouldExit { + return + } + + let producerID = self.generateId() + + await withTaskCancellationHandler { + // a suspension is needed + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) + + switch action { + case .none: + break + case .resumeProducer: + continuation.resume() + case .resumeProducerAndConsumer(let consumerContinuation): + continuation.resume() + consumerContinuation?.resume(returning: element) + } + } + } + } onCancel: { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.sendCancelled(producerID: producerID) + + switch action { + case .none: + break + case .resumeProducer(let continuation): + continuation?.resume() + } + } + } + } + + func finish(error: Failure? = nil) { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.finish(error: error) + + 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) } + } + } + } + } + + func next() async throws -> Element? { + let (shouldExit, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action = stateMachine.next() + + switch action { + case .suspend: + return (false, nil) + case .resumeProducer(let producerContinuation, let result): + producerContinuation?.resume() + return (true, result) + } + } + + if shouldExit { + return try result?._rethrowGet() + } + + let consumerID = self.generateId() + + return try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.nextSuspended( + continuation: continuation, + consumerID: consumerID + ) + + switch action { + case .none: + break + case .resumeConsumer(let element): + continuation.resume(returning: element) + case .resumeConsumerWithError(let error): + continuation.resume(throwing: error) + case .resumeProducerAndConsumer(let producerContinuation, let element): + producerContinuation?.resume() + continuation.resume(returning: element) + } + } + } + } onCancel: { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.nextCancelled(consumerID: consumerID) + + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } + } + } + } +} diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index 9038500f..834b7303 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -14,6 +14,12 @@ import AsyncAlgorithms #if canImport(Darwin) final class TestThroughput: XCTestCase { + func test_channel() async { + await measureChannelThroughput(output: 1) + } + func test_throwingChannel() async { + await measureThrowingChannelThroughput(output: 1) + } func test_chain2() async { await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { chain($0, $1) diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index 16531d9b..c8991bdd 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -56,6 +56,64 @@ final class _ThroughputMetric: NSObject, XCTMetric, @unchecked Sendable { } extension XCTestCase { + public func measureChannelThroughput(output: @escaping @autoclosure () -> Output) async { + let metric = _ThroughputMetric() + let sampleTime: Double = 0.1 + + measure(metrics: [metric]) { + let channel = AsyncChannel() + + let exp = self.expectation(description: "Finished") + let iterTask = Task { + var eventCount = 0 + for try await _ in channel { + eventCount += 1 + } + metric.eventCount = eventCount + exp.fulfill() + return eventCount + } + let sendTask = Task { + while !Task.isCancelled { + await channel.send(output()) + } + } + usleep(UInt32(sampleTime * Double(USEC_PER_SEC))) + iterTask.cancel() + sendTask.cancel() + self.wait(for: [exp], timeout: sampleTime * 2) + } + } + + public func measureThrowingChannelThroughput(output: @escaping @autoclosure () -> Output) async { + let metric = _ThroughputMetric() + let sampleTime: Double = 0.1 + + measure(metrics: [metric]) { + let channel = AsyncThrowingChannel() + + let exp = self.expectation(description: "Finished") + let iterTask = Task { + var eventCount = 0 + for try await _ in channel { + eventCount += 1 + } + metric.eventCount = eventCount + exp.fulfill() + return eventCount + } + let sendTask = Task { + while !Task.isCancelled { + await channel.send(output()) + } + } + usleep(UInt32(sampleTime * Double(USEC_PER_SEC))) + iterTask.cancel() + sendTask.cancel() + self.wait(for: [exp], timeout: sampleTime * 2) + } + } + public func measureSequenceThroughput( output: @autoclosure () -> Output, _ sequenceBuilder: (InfiniteAsyncSequence) -> S) async where S: Sendable { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index 3d53fe3a..b181bddd 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -13,265 +13,164 @@ import XCTest import AsyncAlgorithms final class TestChannel: XCTestCase { - func test_asyncChannel_delivers_values_when_two_producers_and_two_consumers() async { - let (sentFromProducer1, sentFromProducer2) = ("test1", "test2") - let expected = Set([sentFromProducer1, sentFromProducer2]) + func test_asyncChannel_delivers_elements_when_several_producers_and_several_consumers() async { + let sents = (1...10) + let expected = Set(sents) - let channel = AsyncChannel() - Task { - await channel.send(sentFromProducer1) - } - Task { - await channel.send(sentFromProducer2) - } - - let t: Task = Task { - var iterator = channel.makeAsyncIterator() - let value = await iterator.next() - return value - } - var iterator = channel.makeAsyncIterator() - - let (collectedFromConsumer1, collectedFromConsumer2) = (await t.value, await iterator.next()) - let collected = Set([collectedFromConsumer1, collectedFromConsumer2]) + // Given: an AsyncChannel + let sut = AsyncChannel() + // When: sending elements from tasks in a group + Task { + await withTaskGroup(of: Void.self) { group in + for sent in sents { + group.addTask { + await sut.send(sent) + } + } + } + } + + // When: receiving those elements from tasks in a group + let collected = await withTaskGroup(of: Int.self, returning: Set.self) { group in + for _ in sents { + group.addTask { + var iterator = sut.makeAsyncIterator() + let received = await iterator.next() + return received! + } + } + + var collected = Set() + for await element in group { + collected.update(with: element) + } + return collected + } + + // Then: all elements are sent and received XCTAssertEqual(collected, expected) } - - func test_asyncThrowingChannel_delivers_values_when_two_producers_and_two_consumers() async throws { - let (sentFromProducer1, sentFromProducer2) = ("test1", "test2") - let expected = Set([sentFromProducer1, sentFromProducer2]) - let channel = AsyncThrowingChannel() - Task { - await channel.send("test1") - } - Task { - await channel.send("test2") - } - - let t: Task = Task { - var iterator = channel.makeAsyncIterator() - let value = try await iterator.next() - return value - } - var iterator = channel.makeAsyncIterator() + func test_asyncChannel_resumes_producers_and_discards_additional_elements_when_finish_is_called() async { + // Given: an AsyncChannel + let sut = AsyncChannel() - let (collectedFromConsumer1, collectedFromConsumer2) = (try await t.value, try await iterator.next()) - let collected = Set([collectedFromConsumer1, collectedFromConsumer2]) - - XCTAssertEqual(collected, expected) - } - - func test_asyncThrowingChannel_throws_and_discards_additional_sent_values_when_fail_is_called() async { - let sendImmediatelyResumes = expectation(description: "Send immediately resumes after fail") - - let channel = AsyncThrowingChannel() - channel.fail(Failure()) - - var iterator = channel.makeAsyncIterator() - do { - let _ = try await iterator.next() - XCTFail("The AsyncThrowingChannel should have thrown") - } catch { - XCTAssertEqual(error as? Failure, Failure()) + // Given: 2 suspended send operations + let task1 = Task { + await sut.send(1) } - do { - let pastFailure = try await iterator.next() - XCTAssertNil(pastFailure) - } catch { - XCTFail("The AsyncThrowingChannel should not fail when failure has already been fired") + let task2 = Task { + await sut.send(2) } - await channel.send("send") - sendImmediatelyResumes.fulfill() - wait(for: [sendImmediatelyResumes], timeout: 1.0) - } - - func test_asyncChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called() async { - let channel = AsyncChannel() - let complete = ManagedCriticalState(false) - let finished = expectation(description: "finished") + // When: finishing the channel + sut.finish() - Task { - channel.finish() - complete.withCriticalRegion { $0 = true } - finished.fulfill() - } + // Then: the send operations are resumed + _ = await (task1.value, task2.value) - let valueFromConsumer1 = ManagedCriticalState(nil) - let valueFromConsumer2 = ManagedCriticalState(nil) + // When: sending an extra value + await sut.send(3) - let received = expectation(description: "received") - received.expectedFulfillmentCount = 2 + // Then: the operation and the iteration are immediately resumed + var collected = [Int]() + for await element in sut { + collected.append(element) + } + XCTAssertTrue(collected.isEmpty) + } - let pastEnd = expectation(description: "pastEnd") - pastEnd.expectedFulfillmentCount = 2 + func test_asyncChannel_resumes_consumers_when_finish_is_called() async { + // Given: an AsyncChannel + let sut = AsyncChannel() - Task { - var iterator = channel.makeAsyncIterator() - let ending = await iterator.next() - valueFromConsumer1.withCriticalRegion { $0 = ending } - received.fulfill() - let item = await iterator.next() - XCTAssertNil(item) - pastEnd.fulfill() + // Given: 2 suspended iterations + let task1 = Task { + var iterator = sut.makeAsyncIterator() + return await iterator.next() } - Task { - var iterator = channel.makeAsyncIterator() - let ending = await iterator.next() - valueFromConsumer2.withCriticalRegion { $0 = ending } - received.fulfill() - let item = await iterator.next() - XCTAssertNil(item) - pastEnd.fulfill() + let task2 = Task { + var iterator = sut.makeAsyncIterator() + return await iterator.next() } - - wait(for: [finished, received], timeout: 1.0) - XCTAssertTrue(complete.withCriticalRegion { $0 }) - XCTAssertEqual(valueFromConsumer1.withCriticalRegion { $0 }, nil) - XCTAssertEqual(valueFromConsumer2.withCriticalRegion { $0 }, nil) + // When: finishing the channel + sut.finish() - wait(for: [pastEnd], timeout: 1.0) - let additionalSend = expectation(description: "additional send") - Task { - await channel.send("test") - additionalSend.fulfill() - } - wait(for: [additionalSend], timeout: 1.0) - } + // Then: the iterations are resumed with nil values + let (collected1, collected2) = await (task1.value, task2.value) + XCTAssertNil(collected1) + XCTAssertNil(collected2) - func test_asyncThrowingChannel_ends_alls_iterators_and_discards_additional_sent_values_when_finish_is_called() async { - let channel = AsyncThrowingChannel() - let complete = ManagedCriticalState(false) - let finished = expectation(description: "finished") - - Task { - channel.finish() - complete.withCriticalRegion { $0 = true } - finished.fulfill() - } + // When: requesting a next value + var iterator = sut.makeAsyncIterator() + let pastEnd = await iterator.next() - let valueFromConsumer1 = ManagedCriticalState(nil) - let valueFromConsumer2 = ManagedCriticalState(nil) + // Then: the past end is nil + XCTAssertNil(pastEnd) + } - let received = expectation(description: "received") - received.expectedFulfillmentCount = 2 + func test_asyncChannel_resumes_producer_when_task_is_cancelled() async { + let send1IsResumed = expectation(description: "The first send operation is resumed") - let pastEnd = expectation(description: "pastEnd") - pastEnd.expectedFulfillmentCount = 2 + // Given: an AsyncChannel + let sut = AsyncChannel() - Task { - var iterator = channel.makeAsyncIterator() - let ending = try await iterator.next() - valueFromConsumer1.withCriticalRegion { $0 = ending } - received.fulfill() - let item = try await iterator.next() - XCTAssertNil(item) - pastEnd.fulfill() + // Given: 2 suspended send operations + let task1 = Task { + await sut.send(1) + send1IsResumed.fulfill() } - Task { - var iterator = channel.makeAsyncIterator() - let ending = try await iterator.next() - valueFromConsumer2.withCriticalRegion { $0 = ending } - received.fulfill() - let item = try await iterator.next() - XCTAssertNil(item) - pastEnd.fulfill() + let task2 = Task { + await sut.send(2) } - wait(for: [finished, received], timeout: 1.0) + // When: cancelling the first task + task1.cancel() - XCTAssertTrue(complete.withCriticalRegion { $0 }) - XCTAssertEqual(valueFromConsumer1.withCriticalRegion { $0 }, nil) - XCTAssertEqual(valueFromConsumer2.withCriticalRegion { $0 }, nil) + // Then: the first sending operation is resumed + wait(for: [send1IsResumed], timeout: 1.0) - wait(for: [pastEnd], timeout: 1.0) - let additionalSend = expectation(description: "additional send") - Task { - await channel.send("test") - additionalSend.fulfill() - } - wait(for: [additionalSend], timeout: 1.0) - } - - func test_asyncChannel_ends_iterator_when_task_is_cancelled() async { - let channel = AsyncChannel() - let ready = expectation(description: "ready") - let task: Task = Task { - var iterator = channel.makeAsyncIterator() - ready.fulfill() - return await iterator.next() - } - wait(for: [ready], timeout: 1.0) - task.cancel() - let value = await task.value - XCTAssertNil(value) - } + // When: collecting elements + var iterator = sut.makeAsyncIterator() + let collected = await iterator.next() - func test_asyncThrowingChannel_ends_iterator_when_task_is_cancelled() async throws { - let channel = AsyncThrowingChannel() - let ready = expectation(description: "ready") - let task: Task = Task { - var iterator = channel.makeAsyncIterator() - ready.fulfill() - return try await iterator.next() - } - wait(for: [ready], timeout: 1.0) - task.cancel() - let value = try await task.value - XCTAssertNil(value) + // Then: the second operation resumes and the iteration receives the element + _ = await task2.value + XCTAssertEqual(collected, 2) } - - func test_asyncChannel_resumes_send_when_task_is_cancelled_and_continue_remaining_send_tasks() async { - let channel = AsyncChannel() - let notYetDone = expectation(description: "not yet done") - notYetDone.isInverted = true - let done = expectation(description: "done") - let task = Task { - await channel.send(1) - notYetDone.fulfill() - done.fulfill() - } - - Task { - await channel.send(2) - } - wait(for: [notYetDone], timeout: 0.1) - task.cancel() - wait(for: [done], timeout: 1.0) + func test_asyncChannel_resumes_consumer_when_task_is_cancelled() async { + // Given: an AsyncChannel + let sut = AsyncChannel() - var iterator = channel.makeAsyncIterator() - let received = await iterator.next() - XCTAssertEqual(received, 2) - } - - func test_asyncThrowingChannel_resumes_send_when_task_is_cancelled_and_continue_remaining_send_tasks() async throws { - let channel = AsyncThrowingChannel() - let notYetDone = expectation(description: "not yet done") - notYetDone.isInverted = true - let done = expectation(description: "done") - let task = Task { - await channel.send(1) - notYetDone.fulfill() - done.fulfill() + // Given: 2 suspended iterations + let task1 = Task { + var iterator = sut.makeAsyncIterator() + return await iterator.next() } - Task { - await channel.send(2) + let task2 = Task { + var iterator = sut.makeAsyncIterator() + return await iterator.next() } - wait(for: [notYetDone], timeout: 0.1) - task.cancel() - wait(for: [done], timeout: 1.0) + // When: cancelling the first task + task1.cancel() + + // Then: the iteration is resumed with a nil element + let collected1 = await task1.value + XCTAssertNil(collected1) + + // When: sending an element + await sut.send(1) - var iterator = channel.makeAsyncIterator() - let received = try await iterator.next() - XCTAssertEqual(received, 2) + // Then: the second iteration is resumed with the element + let collected2 = await task2.value + XCTAssertEqual(collected2, 1) } } diff --git a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift new file mode 100644 index 00000000..7dd60f18 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift @@ -0,0 +1,313 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import AsyncAlgorithms + +final class TestThrowingChannel: XCTestCase { + func test_asyncThrowingChannel_delivers_elements_when_several_producers_and_several_consumers() async throws { + let sents = (1...10) + let expected = Set(sents) + + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // When: sending elements from tasks in a group + Task { + await withTaskGroup(of: Void.self) { group in + for sent in sents { + group.addTask { + await sut.send(sent) + } + } + } + } + + // When: receiving those elements from tasks in a group + let collected = try await withThrowingTaskGroup(of: Int.self, returning: Set.self) { group in + for _ in sents { + group.addTask { + var iterator = sut.makeAsyncIterator() + let received = try await iterator.next() + return received! + } + } + + var collected = Set() + for try await element in group { + collected.update(with: element) + } + return collected + } + + // Then: all elements are sent and received + XCTAssertEqual(collected, expected) + } + + func test_asyncThrowingChannel_resumes_producers_and_discards_additional_elements_when_finish_is_called() async throws { + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: 2 suspended send operations + let task1 = Task { + await sut.send(1) + } + + let task2 = Task { + await sut.send(2) + } + + // When: finishing the channel + sut.finish() + + // Then: the send operations are resumed + _ = await (task1.value, task2.value) + + // When: sending an extra value + await sut.send(3) + + // Then: the operation and the iteration are immediately resumed + var collected = [Int]() + for try await element in sut { + collected.append(element) + } + XCTAssertTrue(collected.isEmpty) + } + + func test_asyncThrowingChannel_resumes_producers_and_discards_additional_elements_when_fail_is_called() async throws { + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: 2 suspended send operations + let task1 = Task { + await sut.send(1) + } + + let task2 = Task { + await sut.send(2) + } + + // When: failing the channel + sut.fail(Failure()) + + // Then: the send operations are resumed + _ = await (task1.value, task2.value) + + // When: sending an extra value + await sut.send(3) + + // Then: the send operation is resumed + // Then: the iteration is resumed with a failure + var collected = [Int]() + do { + for try await element in sut { + collected.append(element) + } + } catch { + XCTAssertTrue(collected.isEmpty) + XCTAssertEqual(error as? Failure, Failure()) + } + + // When: requesting a next value + var iterator = sut.makeAsyncIterator() + let pastFailure = try await iterator.next() + + // Then: the past failure is nil + XCTAssertNil(pastFailure) + } + + func test_asyncThrowingChannel_resumes_consumers_when_finish_is_called() async throws { + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: 2 suspended iterations + let task1 = Task { + var iterator = sut.makeAsyncIterator() + return try await iterator.next() + } + + let task2 = Task { + var iterator = sut.makeAsyncIterator() + return try await iterator.next() + } + + + // When: finishing the channel + sut.finish() + + // Then: the iterations are resumed with nil values + let (collected1, collected2) = try await (task1.value, task2.value) + XCTAssertNil(collected1) + XCTAssertNil(collected2) + + // When: requesting a next value + var iterator = sut.makeAsyncIterator() + let pastEnd = try await iterator.next() + + // Then: the past end is nil + XCTAssertNil(pastEnd) + } + + func test_asyncThrowingChannel_resumes_consumer_when_fail_is_called() async throws { + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: suspended iteration + let task = Task { + var iterator = sut.makeAsyncIterator() + + do { + _ = try await iterator.next() + XCTFail("We expect the above call to throw") + } catch { + XCTAssertEqual(error as? Failure, Failure()) + } + + return try await iterator.next() + } + + // When: failing the channel + sut.fail(Failure()) + + // Then: the iterations are resumed with the error and the next element is nil + do { + let collected = try await task.value + XCTAssertNil(collected) + } catch { + XCTFail("The task should not fail, the past failure element should be nil, not a failure.") + } + } + + func test_asyncThrowingChannel_resumes_consumers_when_fail_is_called() async throws { + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: 2 suspended iterations + let task1 = Task { + var iterator = sut.makeAsyncIterator() + return try await iterator.next() + } + + let task2 = Task { + var iterator = sut.makeAsyncIterator() + return try await iterator.next() + } + + // When: failing the channel + sut.fail(Failure()) + + // Then: the iterations are resumed with the error + do { + _ = try await task1.value + } catch { + XCTAssertEqual(error as? Failure, Failure()) + } + + do { + _ = try await task2.value + } catch { + XCTAssertEqual(error as? Failure, Failure()) + } + + // When: requesting a next value + var iterator = sut.makeAsyncIterator() + let pastFailure = try await iterator.next() + + // Then: the past failure is nil + XCTAssertNil(pastFailure) + } + + func test_asyncThrowingChannel_resumes_consumer_with_error_when_already_failed() async throws { + // Given: an AsyncThrowingChannel that is failed + let sut = AsyncThrowingChannel() + sut.fail(Failure()) + + var iterator = sut.makeAsyncIterator() + + // When: requesting the next element + do { + _ = try await iterator.next() + } catch { + // Then: the iteration is resumed with the error + XCTAssertEqual(error as? Failure, Failure()) + } + + // When: requesting the next element past failure + do { + let pastFailure = try await iterator.next() + // Then: the past failure is nil + XCTAssertNil(pastFailure) + } catch { + XCTFail("The past failure should not throw") + } + } + + func test_asyncThrowingChannel_resumes_producer_when_task_is_cancelled() async throws { + let send1IsResumed = expectation(description: "The first send operation is resumed") + + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: 2 suspended send operations + let task1 = Task { + await sut.send(1) + send1IsResumed.fulfill() + } + + let task2 = Task { + await sut.send(2) + } + + // When: cancelling the first task + task1.cancel() + + // Then: the first sending operation is resumed + wait(for: [send1IsResumed], timeout: 1.0) + + // When: collecting elements + var iterator = sut.makeAsyncIterator() + let collected = try await iterator.next() + + // Then: the second operation resumes and the iteration receives the element + _ = await task2.value + XCTAssertEqual(collected, 2) + } + + func test_asyncThrowingChannel_resumes_consumer_when_task_is_cancelled() async throws { + // Given: an AsyncThrowingChannel + let sut = AsyncThrowingChannel() + + // Given: 2 suspended iterations + let task1 = Task { + var iterator = sut.makeAsyncIterator() + return try await iterator.next() + } + + let task2 = Task { + var iterator = sut.makeAsyncIterator() + return try await iterator.next() + } + + // When: cancelling the first task + task1.cancel() + + // Then: the first iteration is resumed with a nil element + let collected1 = try await task1.value + XCTAssertNil(collected1) + + // When: sending an element + await sut.send(1) + + // Then: the second iteration is resumed with the element + let collected2 = try await task2.value + XCTAssertEqual(collected2, 1) + } +} From bf0ec24bded364130e93df31b841a3f59860e125 Mon Sep 17 00:00:00 2001 From: ga eun kim Date: Thu, 2 Feb 2023 00:20:40 +0900 Subject: [PATCH 086/149] fix iterator variable name on zip (#251) --- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index 57ee1dc5..d9fb3a82 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -165,7 +165,7 @@ final class ZipStorage Date: Mon, 6 Feb 2023 17:05:07 +0000 Subject: [PATCH 087/149] Tolerate cancelling before we create our task (#250) # Motivation We switched the point in time when we create tasks to the first call to `next()`. This had the side effect that we can now be cancelled before we even created our task. Which by itself is fine but we didn't handle it in the state machines. # Modification Adapt the state machines of `debounce`, `zip`, `merge` and `combineLatest` to handle cancellation before `next()`. # Result We shouldn't run into these preconditions anymore. --- .../CombineLatest/CombineLatestStateMachine.swift | 4 +++- .../Debounce/DebounceStateMachine.swift | 5 ++--- Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift | 8 +++++--- Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift | 4 +++- Tests/AsyncAlgorithmsTests/TestCombineLatest.swift | 10 ++++++++++ Tests/AsyncAlgorithmsTests/TestDebounce.swift | 11 +++++++++++ Tests/AsyncAlgorithmsTests/TestMerge.swift | 10 ++++++++++ Tests/AsyncAlgorithmsTests/TestZip.swift | 10 ++++++++++ 8 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index fb68f073..34f4dead 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -585,7 +585,9 @@ struct CombineLatestStateMachine< mutating func cancelled() -> CancelledAction? { switch self.state { case .initial: - preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + state = .finished + + return .none case .waitingForDemand(let task, let upstreams, _): // The downstream task got cancelled so we need to cancel our upstream Task diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index cd6cc1a8..bd111ab1 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -583,9 +583,8 @@ struct DebounceStateMachine { mutating func cancelled() -> CancelledAction? { switch self.state { case .initial: - // Since we are transitioning to `merging` before we return from `makeAsyncIterator` - // this can never happen - preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + state = .finished + return .none case .waitingForDemand: // We got cancelled before we event got any demand. This can happen if a cancelled task diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index 57d43bde..e3bb59e4 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -465,9 +465,11 @@ struct MergeStateMachine< mutating func cancelled() -> CancelledAction { switch state { case .initial: - // Since we are transitioning to `merging` before we return from `makeAsyncIterator` - // this can never happen - preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + // 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 diff --git a/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift b/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift index d6b0c9ce..475457c6 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift @@ -460,7 +460,9 @@ struct ZipStateMachine< mutating func cancelled() -> CancelledAction? { switch self.state { case .initial: - preconditionFailure("Internal inconsistency current state \(self.state) and received cancelled()") + state = .finished + + return .none case .waitingForDemand(let task, let upstreams): // The downstream task got cancelled so we need to cancel our upstream Task diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index 489252ae..f33258c9 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -318,6 +318,16 @@ final class TestCombineLatest2: XCTestCase { task.cancel() wait(for: [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() + } } final class TestCombineLatest3: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestDebounce.swift b/Tests/AsyncAlgorithmsTests/TestDebounce.swift index d82105bc..5d296054 100644 --- a/Tests/AsyncAlgorithmsTests/TestDebounce.swift +++ b/Tests/AsyncAlgorithmsTests/TestDebounce.swift @@ -68,4 +68,15 @@ final class TestDebounce: XCTestCase { 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") } + + 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/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index ef72d1ac..0bcb3479 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -191,6 +191,16 @@ final class TestMerge2: XCTestCase { task.cancel() wait(for: [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() + } } final class TestMerge3: XCTestCase { diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index d27dd73d..25bc9ec7 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -158,6 +158,16 @@ final class TestZip2: XCTestCase { task.cancel() wait(for: [finished], timeout: 1.0) } + + 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() + } } final class TestZip3: XCTestCase { From 9cfed92b026c524674ed869a4ff2dcfdeedf8a2a Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Fri, 10 Feb 2023 20:27:06 +0100 Subject: [PATCH 088/149] buffer: implement with custom storages (#239) --- .../AsyncAlgorithms/AsyncBufferSequence.swift | 308 -------------- .../Buffer/AsyncBufferSequence.swift | 131 ++++++ .../Buffer/BoundedBufferStateMachine.swift | 310 ++++++++++++++ .../Buffer/BoundedBufferStorage.swift | 142 +++++++ .../Buffer/UnboundedBufferStateMachine.swift | 250 +++++++++++ .../Buffer/UnboundedBufferStorage.swift | 114 +++++ .../Performance/TestThroughput.swift | 20 + Tests/AsyncAlgorithmsTests/TestBuffer.swift | 392 ++++++++++-------- 8 files changed, 1190 insertions(+), 477 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/AsyncBufferSequence.swift create mode 100644 Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift create mode 100644 Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift create mode 100644 Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift create mode 100644 Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift diff --git a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/AsyncBufferSequence.swift deleted file mode 100644 index c079289c..00000000 --- a/Sources/AsyncAlgorithms/AsyncBufferSequence.swift +++ /dev/null @@ -1,308 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -actor AsyncBufferState { - enum TerminationState: Sendable, CustomStringConvertible { - case running - case baseFailure(Error) // An error from the base sequence has occurred. We need to process any buffered items before throwing the error. We can rely on it not emitting any more items. - case baseTermination - case terminal - - var description: String { - switch self { - case .running: return "running" - case .baseFailure: return "base failure" - case .baseTermination: return "base termination" - case .terminal: return "terminal" - } - } - } - - var pending = [UnsafeContinuation, Never>]() - var terminationState = TerminationState.running - - init() { } - - func drain(buffer: Buffer) async where Buffer.Input == Input, Buffer.Output == Output { - guard pending.count > 0 else { - return - } - - do { - if let value = try await buffer.pop() { - pending.removeFirst().resume(returning: .success(value)) - } else { - switch terminationState { - case .running: - // There's no value to report, because it was probably grabbed by next() before we could grab it. The pending continuation was either resumed by next() directly, or will be by a future enqueued value or base termination/failure. - break - case .baseFailure(let error): - // Now that there are no more items in the buffer, we can finally report the base sequence's error and enter terminal state. - pending.removeFirst().resume(returning: .failure(error)) - self.terminate() - case .terminal, .baseTermination: - self.terminate() - } - } - } catch { - // Errors thrown by the buffer immediately terminate the sequence. - pending.removeFirst().resume(returning: .failure(error)) - self.terminate() - } - } - - func enqueue(_ item: Input, buffer: Buffer) async where Buffer.Input == Input, Buffer.Output == Output { - await buffer.push(item) - await drain(buffer: buffer) - } - - func fail(_ error: Error, buffer: Buffer) async where Buffer.Input == Input, Buffer.Output == Output { - terminationState = .baseFailure(error) - await drain(buffer: buffer) - } - - func finish(buffer: Buffer) async where Buffer.Input == Input, Buffer.Output == Output { - if case .running = terminationState { - terminationState = .baseTermination - } - await drain(buffer: buffer) - } - - func terminate() { - terminationState = .terminal - let oldPending = pending - pending = [] - for continuation in oldPending { - continuation.resume(returning: .success(nil)) - } - } - - func next(buffer: Buffer) async throws -> Buffer.Output? where Buffer.Input == Input, Buffer.Output == Output { - if case .terminal = terminationState { - return nil - } - - do { - while let value = try await buffer.pop() { - if let continuation = pending.first { - pending.removeFirst() - continuation.resume(returning: .success(value)) - } else { - return value - } - } - } catch { - // Errors thrown by the buffer immediately terminate the sequence. - self.terminate() - throw error - } - - switch terminationState { - case .running: - break - case .baseFailure(let error): - self.terminate() - throw error - case .baseTermination, .terminal: - self.terminate() - return nil - } - - let result: Result = await withUnsafeContinuation { continuation in - pending.append(continuation) - } - return try result._rethrowGet() - } -} - -/// An asynchronous buffer storage actor protocol used for buffering -/// elements to an `AsyncBufferSequence`. -@rethrows -public protocol AsyncBuffer: Actor { - associatedtype Input: Sendable - associatedtype Output: Sendable - - /// Push an element to enqueue to the buffer - func push(_ element: Input) async - - /// Pop an element from the buffer. - /// - /// Implementors of `pop()` may throw. In cases where types - /// throw from this function, that throwing behavior contributes to - /// the rethrowing characteristics of `AsyncBufferSequence`. - func pop() async throws -> Output? -} - -/// A buffer that limits pushed items by a certain count. -public actor AsyncLimitBuffer: AsyncBuffer { - /// A policy for buffering elements to an `AsyncLimitBuffer` - public enum Policy: Sendable { - /// A policy for no bounding limit of pushed elements. - case unbounded - /// A policy for limiting to a specific number of oldest values. - case bufferingOldest(Int) - /// A policy for limiting to a specific number of newest values. - case bufferingNewest(Int) - } - - var buffer = [Element]() - let policy: Policy - - init(policy: Policy) { - // limits should always be greater than 0 items - switch policy { - case .bufferingNewest(let limit): - precondition(limit > 0) - case .bufferingOldest(let limit): - precondition(limit > 0) - default: break - } - self.policy = policy - } - - /// Push an element to enqueue to the buffer. - public func push(_ element: Element) async { - switch policy { - case .unbounded: - buffer.append(element) - case .bufferingOldest(let limit): - if buffer.count < limit { - buffer.append(element) - } - case .bufferingNewest(let limit): - if buffer.count < limit { - // there is space available - buffer.append(element) - } else { - // no space is available and this should make some room - buffer.removeFirst() - buffer.append(element) - } - } - } - - /// Pop an element from the buffer. - public func pop() async -> Element? { - guard buffer.count > 0 else { - return nil - } - return buffer.removeFirst() - } -} - -extension AsyncSequence where Element: Sendable, Self: Sendable { - /// Creates an asynchronous sequence that buffers elements using a buffer created from a supplied closure. - /// - /// Use the `buffer(_:)` method to account for `AsyncSequence` types that may produce elements faster - /// than they are iterated. The `createBuffer` closure returns a backing buffer for storing elements and dealing with - /// behavioral characteristics of the `buffer(_:)` algorithm. - /// - /// - Parameter createBuffer: A closure that constructs a new `AsyncBuffer` actor to store buffered values. - /// - Returns: An asynchronous sequence that buffers elements using the specified `AsyncBuffer`. - public func buffer(_ createBuffer: @Sendable @escaping () -> Buffer) -> AsyncBufferSequence where Buffer.Input == Element { - AsyncBufferSequence(self, createBuffer: createBuffer) - } - - /// Creates an asynchronous sequence that buffers elements using a specific policy to limit the number of - /// elements that are buffered. - /// - /// - Parameter policy: A limiting policy behavior on the buffering behavior of the `AsyncBufferSequence` - /// - Returns: An asynchronous sequence that buffers elements up to a given limit. - public func buffer(policy limit: AsyncLimitBuffer.Policy) -> AsyncBufferSequence> { - buffer { - AsyncLimitBuffer(policy: limit) - } - } -} - -/// An `AsyncSequence` that buffers elements utilizing an `AsyncBuffer`. -public struct AsyncBufferSequence where Base.Element == Buffer.Input { - let base: Base - let createBuffer: @Sendable () -> Buffer - - init(_ base: Base, createBuffer: @Sendable @escaping () -> Buffer) { - self.base = base - self.createBuffer = createBuffer - } -} - -extension AsyncBufferSequence: Sendable where Base: Sendable { } - -extension AsyncBufferSequence: AsyncSequence { - public typealias Element = Buffer.Output - - /// The iterator for a `AsyncBufferSequence` instance. - public struct Iterator: AsyncIteratorProtocol { - struct Active { - var task: Task? - let buffer: Buffer - let state: AsyncBufferState - - init(_ base: Base, buffer: Buffer, state: AsyncBufferState) { - self.buffer = buffer - self.state = state - task = Task { - var iter = base.makeAsyncIterator() - do { - while let item = try await iter.next() { - await state.enqueue(item, buffer: buffer) - } - await state.finish(buffer: buffer) - } catch { - await state.fail(error, buffer: buffer) - } - } - } - - func next() async rethrows -> Element? { - let result: Result = await withTaskCancellationHandler { - do { - let value = try await state.next(buffer: buffer) - return .success(value) - } catch { - task?.cancel() - return .failure(error) - } - } onCancel: { - task?.cancel() - } - return try result._rethrowGet() - } - } - - enum State { - case idle(Base, @Sendable () -> Buffer) - case active(Active) - } - - var state: State - - init(_ base: Base, createBuffer: @Sendable @escaping () -> Buffer) { - state = .idle(base, createBuffer) - } - - public mutating func next() async rethrows -> Element? { - switch state { - case .idle(let base, let createBuffer): - let bufferState = AsyncBufferState() - let buffer = Active(base, buffer: createBuffer(), state: bufferState) - state = .active(buffer) - return try await buffer.next() - case .active(let buffer): - return try await buffer.next() - } - } - } - - public func makeAsyncIterator() -> Iterator { - Iterator(base, createBuffer: createBuffer) - } -} diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift new file mode 100644 index 00000000..8049a1a0 --- /dev/null +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// 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 where Self: Sendable { + /// Creates an asynchronous sequence that buffers elements. + /// + /// The buffering behaviour is dictated by the policy: + /// - bounded: will buffer elements until the limit is reached. Then it will suspend the upstream async sequence. + /// - unbounded: will buffer elements without limit. + /// - bufferingNewest: will buffer elements until the limit is reached. Then it will discard the oldest elements. + /// - bufferingOldest: will buffer elements until the limit is reached. Then it will discard the newest elements. + /// + /// - Parameter policy: A policy that drives the behaviour of the ``AsyncBufferSequence`` + /// - Returns: An asynchronous sequence that buffers elements up to a given limit. + public func buffer( + policy: AsyncBufferSequencePolicy + ) -> AsyncBufferSequence { + AsyncBufferSequence(base: self, policy: policy) + } +} + +/// A policy dictating the buffering behaviour of an ``AsyncBufferSequence`` +public struct AsyncBufferSequencePolicy: Sendable { + enum _Policy { + case bounded(Int) + case unbounded + case bufferingNewest(Int) + case bufferingOldest(Int) + } + + let policy: _Policy + + /// A policy for buffering elements until the limit is reached. + /// Then consumption of the upstream `AsyncSequence` will be paused until elements are consumed from the buffer. + /// If the limit is zero then no buffering policy is applied. + public static func bounded(_ limit: Int) -> Self { + precondition(limit >= 0, "The limit should be positive or equal to 0.") + return Self(policy: .bounded(limit)) + } + + /// A policy for buffering elements without limit. + public static var unbounded: Self { + return Self(policy: .unbounded) + } + + /// A policy for buffering elements until the limit is reached. + /// After the limit is reached and a new element is produced by the upstream, the oldest buffered element will be discarded. + /// If the limit is zero then no buffering policy is applied. + public static func bufferingLatest(_ limit: Int) -> Self { + precondition(limit >= 0, "The limit should be positive or equal to 0.") + return Self(policy: .bufferingNewest(limit)) + } + + /// A policy for buffering elements until the limit is reached. + /// After the limit is reached and a new element is produced by the upstream, the latest buffered element will be discarded. + /// If the limit is zero then no buffering policy is applied. + public static func bufferingOldest(_ limit: Int) -> Self { + precondition(limit >= 0, "The limit should be positive or equal to 0.") + return Self(policy: .bufferingOldest(limit)) + } +} + +/// An `AsyncSequence` that buffers elements in regard to a policy. +public struct AsyncBufferSequence: AsyncSequence { + enum StorageType { + case transparent(Base.AsyncIterator) + case bounded(storage: BoundedBufferStorage) + case unbounded(storage: UnboundedBufferStorage) + } + + public typealias Element = Base.Element + public typealias AsyncIterator = Iterator + + let base: Base + let policy: AsyncBufferSequencePolicy + + public init( + base: Base, + policy: AsyncBufferSequencePolicy + ) { + self.base = base + self.policy = policy + } + + 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))) + } + return Iterator(storageType: storageType) + } + + public struct Iterator: AsyncIteratorProtocol { + var storageType: StorageType + + 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() + } + } + } +} + +extension AsyncBufferSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncBufferSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift new file mode 100644 index 00000000..a5c50a61 --- /dev/null +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -0,0 +1,310 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import DequeModule + +struct BoundedBufferStateMachine { + typealias Element = Base.Element + typealias SuspendedProducer = UnsafeContinuation + typealias SuspendedConsumer = UnsafeContinuation?, Never> + + private enum State { + case initial(base: Base) + case buffering( + task: Task, + buffer: Deque>, + suspendedProducer: SuspendedProducer?, + suspendedConsumer: SuspendedConsumer? + ) + case modifying + case finished(buffer: Deque>) + } + + private var state: State + private let limit: Int + + init(base: Base, limit: Int) { + self.state = .initial(base: base) + self.limit = limit + } + + var task: Task? { + switch self.state { + 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 .modifying: + preconditionFailure("Invalid state.") + + case .finished: + preconditionFailure("Invalid state.") + } + } + + mutating func shouldSuspendProducer() -> Bool { + switch state { + case .initial: + preconditionFailure("Invalid state. The task should already by 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(_, _, .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 .modifying: + preconditionFailure("Invalid state.") + + case .finished: + return false + } + } + + enum ProducerSuspendedAction { + case none + case resumeProducer + } + + mutating func producerSuspended(continuation: SuspendedProducer) -> ProducerSuspendedAction { + switch self.state { + case .initial: + preconditionFailure("Invalid state. The task should already by 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 + } + } + + enum ElementProducedAction { + case none + case resumeConsumer(continuation: SuspendedConsumer, result: Result) + } + + 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, .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(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 + } + } + + enum FinishAction { + case none + case resumeConsumer( + continuation: UnsafeContinuation?, Never>? + ) + } + + 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, .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 + } + } + + enum NextAction { + case startTask(base: Base) + case suspend + case returnResult(producerContinuation: UnsafeContinuation?, result: Result?) + } + + 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) + + 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) + } + } + + enum NextSuspendedAction { + case none + case returnResult(producerContinuation: UnsafeContinuation?, result: Result?) + } + + 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, .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) + + 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) + } + } + + enum InterruptedAction { + case none + case resumeProducerAndConsumer( + task: Task, + producerContinuation: UnsafeContinuation?, + consumerContinuation: UnsafeContinuation?, Never>? + ) + } + + 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 + } + } +} diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift new file mode 100644 index 00000000..c00360e1 --- /dev/null +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -0,0 +1,142 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class BoundedBufferStorage: Sendable where Base: Sendable { + private let stateMachine: ManagedCriticalState> + + init(base: Base, limit: Int) { + self.stateMachine = ManagedCriticalState(BoundedBufferStateMachine(base: base, limit: limit)) + } + + func next() async -> Result? { + return await withTaskCancellationHandler { + let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action = stateMachine.next() + switch action { + case .startTask(let base): + self.startTask(stateMachine: &stateMachine, base: base) + return (true, nil) + case .suspend: + return (true, nil) + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + return (false, result) + } + } + + if !shouldSuspend { + return result + } + + return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.nextSuspended(continuation: continuation) + switch action { + case .none: + break + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + continuation.resume(returning: result) + } + } + } + } onCancel: { + self.interrupted() + } + } + + private func startTask( + stateMachine: inout BoundedBufferStateMachine, + base: Base + ) { + let task = Task { + do { + var iterator = base.makeAsyncIterator() + + loop: while true { + let shouldSuspend = self.stateMachine.withCriticalRegion { stateMachine -> Bool in + return stateMachine.shouldSuspendProducer() + } + + if shouldSuspend { + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.producerSuspended(continuation: continuation) + + switch action { + case .none: + break + case .resumeProducer: + continuation.resume() + } + } + } + } + + guard let element = try await iterator.next() else { + // the upstream is finished + break loop + } + + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced(element: element) + switch action { + case .none: + break + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) + } + } + } + + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.finish(error: nil) + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } + } + } catch { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.finish(error: error) + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) + } + } + } + } + + stateMachine.taskStarted(task: task) + } + + func interrupted() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.interrupted() + switch action { + case .none: + break + case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): + task.cancel() + producerContinuation?.resume() + consumerContinuation?.resume(returning: nil) + } + } + } + + deinit { + self.interrupted() + } +} diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift new file mode 100644 index 00000000..b163619a --- /dev/null +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -0,0 +1,250 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import DequeModule + +struct UnboundedBufferStateMachine { + typealias Element = Base.Element + typealias SuspendedConsumer = UnsafeContinuation?, Never> + + enum Policy { + case unlimited + case bufferingNewest(Int) + case bufferingOldest(Int) + } + + private enum State { + case initial(base: Base) + case buffering( + task: Task, + buffer: Deque>, + suspendedConsumer: SuspendedConsumer? + ) + case modifying + case finished(buffer: Deque>) + } + + private var state: State + private let policy: Policy + + init(base: Base, policy: Policy) { + self.state = .initial(base: base) + self.policy = policy + } + + var task: Task? { + switch self.state { + 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 .buffering: + preconditionFailure("Invalid state.") + + case .modifying: + preconditionFailure("Invalid state.") + + case .finished: + preconditionFailure("Invalid state.") + } + } + + enum ElementProducedAction { + case none + case resumeConsumer( + continuation: SuspendedConsumer, + result: Result + ) + } + + 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(element)) + case .bufferingNewest(let limit): + if buffer.count >= limit { + _ = buffer.popFirst() + } + buffer.append(.success(element)) + case .bufferingOldest(let limit): + if buffer.count < limit { + buffer.append(.success(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 + } + } + + enum FinishAction { + case none + case resumeConsumer(continuation: SuspendedConsumer?) + } + + 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 + } + } + + enum NextAction { + case startTask(base: Base) + case suspend + case returnResult(Result?) + } + + 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) + + 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) + } + } + + enum NextSuspendedAction { + case none + case resumeConsumer(Result?) + } + + 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) + + 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) + } + } + + enum InterruptedAction { + case none + case resumeConsumer(task: Task, continuation: SuspendedConsumer?) + } + + 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 + } + } +} diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift new file mode 100644 index 00000000..59b02810 --- /dev/null +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +final class UnboundedBufferStorage: Sendable where Base: Sendable { + private let stateMachine: ManagedCriticalState> + + init(base: Base, policy: UnboundedBufferStateMachine.Policy) { + self.stateMachine = ManagedCriticalState(UnboundedBufferStateMachine(base: base, policy: policy)) + } + + func next() async -> Result? { + return await withTaskCancellationHandler { + + let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action = stateMachine.next() + switch action { + case .startTask(let base): + self.startTask(stateMachine: &stateMachine, base: base) + return (true, nil) + case .suspend: + return (true, nil) + case .returnResult(let result): + return (false, result) + } + } + + if !shouldSuspend { + return result + } + + return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.nextSuspended(continuation: continuation) + switch action { + case .none: + break + case .resumeConsumer(let result): + continuation.resume(returning: result) + } + } + } + } onCancel: { + self.interrupted() + } + } + + private func startTask( + stateMachine: inout UnboundedBufferStateMachine, + base: Base + ) { + let task = Task { + do { + for try await element in base { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.elementProduced(element: element) + switch action { + case .none: + break + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) + } + } + } + + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.finish(error: nil) + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } + } + } catch { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.finish(error: error) + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) + } + } + } + } + + stateMachine.taskStarted(task: task) + } + + func interrupted() { + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.interrupted() + switch action { + case .none: + break + case .resumeConsumer(let task, let continuation): + task.cancel() + continuation?.resume(returning: nil) + } + } + } + + deinit { + self.interrupted() + } +} diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index 834b7303..d8003ca8 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -20,6 +20,26 @@ final class TestThroughput: XCTestCase { func test_throwingChannel() async { await measureThrowingChannelThroughput(output: 1) } + func test_buffer_bounded() async { + await measureSequenceThroughput(output: 1) { + $0.buffer(policy: .bounded(5)) + } + } + func test_buffer_unbounded() async { + await measureSequenceThroughput(output: 1) { + $0.buffer(policy: .unbounded) + } + } + func test_buffer_bufferingNewest() async { + await measureSequenceThroughput(output: 1) { + $0.buffer(policy: .bufferingLatest(5)) + } + } + func test_buffer_bufferingOldest() async { + await measureSequenceThroughput(output: 1) { + $0.buffer(policy: .bufferingOldest(5)) + } + } func test_chain2() async { await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { chain($0, $1) diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index cd989c41..f8ef8a3c 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -13,263 +13,317 @@ import XCTest import AsyncAlgorithms final class TestBuffer: XCTestCase { - func test_buffering() async { - var gated = GatedSequence([1, 2, 3, 4, 5]) - let sequence = gated.buffer(policy: .unbounded) - var iterator = sequence.makeAsyncIterator() - - gated.advance() + func test_given_a_base_sequence_when_buffering_with_unbounded_then_the_buffer_is_filled_in() async { + // Given + var base = GatedSequence([1, 2, 3, 4, 5]) + let buffered = base.buffer(policy: .unbounded) + var iterator = buffered.makeAsyncIterator() + + // When + base.advance() + + // Then var value = await iterator.next() XCTAssertEqual(value, 1) - gated.advance() - gated.advance() - gated.advance() + + // When + base.advance() + base.advance() + base.advance() + + // Then value = await iterator.next() XCTAssertEqual(value, 2) + value = await iterator.next() XCTAssertEqual(value, 3) + value = await iterator.next() XCTAssertEqual(value, 4) - gated.advance() - gated.advance() + + // When + base.advance() + base.advance() + + // Then value = await iterator.next() XCTAssertEqual(value, 5) value = await iterator.next() XCTAssertEqual(value, nil) - value = await iterator.next() - XCTAssertEqual(value, nil) + + let pastEnd = await iterator.next() + XCTAssertEqual(value, pastEnd) } - func test_buffering_withError() async { + func test_given_a_failable_base_sequence_when_buffering_with_unbounded_then_the_failure_is_forwarded() async { + // Given var gated = GatedSequence([1, 2, 3, 4, 5, 6, 7]) - let gated_map = gated.map { try throwOn(3, $0) } - let sequence = gated_map.buffer(policy: .unbounded) - var iterator = sequence.makeAsyncIterator() + let base = gated.map { try throwOn(3, $0) } + let buffered = base.buffer(policy: .unbounded) + var iterator = buffered.makeAsyncIterator() + // When gated.advance() + + // Then var value = try! await iterator.next() XCTAssertEqual(value, 1) + + // When gated.advance() gated.advance() gated.advance() + + // Then value = try! await iterator.next() XCTAssertEqual(value, 2) + // When gated.advance() gated.advance() gated.advance() gated.advance() + + // Then do { value = try await iterator.next() XCTFail("next() should have thrown.") - } catch { } + } catch { + XCTAssert(error is Failure) + } - value = try! await iterator.next() - XCTAssertNil(value) + var pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) } - - func test_buffer_delegation() async { - actor BufferDelegate: AsyncBuffer { - var buffer = [Int]() - var pushed = [Int]() - - func push(_ element: Int) async { - buffer.append(element) - pushed.append(element) - } - - func pop() async -> Int? { - if buffer.count > 0 { - return buffer.removeFirst() + + func test_given_a_base_sequence_when_bufferingOldest_then_the_policy_is_applied() async { + validate { + "X-12- 34- 5 |" + $0.inputs[0].buffer(policy: .bufferingOldest(2)) + "X,,,[1,],,[2,],[3,][5,]|" + } + } + + func test_given_a_base_sequence_when_bufferingOldest_with_0_limit_then_the_policy_is_transparent() async { + validate { + "X-12- 34- 5 |" + $0.inputs[0].buffer(policy: .bufferingOldest(0)) + "X-12- 34- 5 |" + } + } + + func test_given_a_base_sequence_when_bufferingOldest_at_slow_pace_then_no_element_is_dropped() async { + validate { + "X-12 3 4 5 |" + $0.inputs[0].buffer(policy: .bufferingOldest(2)) + "X,,[1,][2,][3,][45]|" + } + } + + func test_given_a_failable_base_sequence_when_bufferingOldest_then_the_failure_is_forwarded() async { + validate { + "X-12345^" + $0.inputs[0].buffer(policy: .bufferingOldest(2)) + "X,,,,,,[12^]" + } + } + + func test_given_a_base_sequence_when_bufferingNewest_then_the_policy_is_applied() async { + validate { + "X-12- 34 -5|" + $0.inputs[0].buffer(policy: .bufferingLatest(2)) + "X,,,[1,],,[3,],[4,][5,]|" + } + } + + func test_given_a_base_sequence_when_bufferingNewest_with_limit_0_then_the_policy_is_transparent() async { + validate { + "X-12- 34 -5|" + $0.inputs[0].buffer(policy: .bufferingLatest(0)) + "X-12- 34 -5|" + } + } + + func test_given_a_base_sequence_when_bufferingNewest_at_slow_pace_then_no_element_is_dropped() async { + validate { + "X-12 3 4 5 |" + $0.inputs[0].buffer(policy: .bufferingLatest(2)) + "X,,[1,][2,][3,][45]|" + } + } + + func test_given_a_failable_base_sequence_when_bufferingNewest_then_the_failure_is_forwarded() async { + validate { + "X-12345^" + $0.inputs[0].buffer(policy: .bufferingLatest(2)) + "X,,,,,,[45^]" + } + } + + 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) + + let finished = expectation(description: "finished") + let iterated = expectation(description: "iterated") + + let task = Task { + var firstIteration = false + for await _ in buffered { + if !firstIteration { + firstIteration = true + iterated.fulfill() } - return nil } + finished.fulfill() } - let delegate = BufferDelegate() - var gated = GatedSequence([1, 2, 3, 4, 5]) - let sequence = gated.buffer { - delegate - } - var iterator = sequence.makeAsyncIterator() - - gated.advance() + // ensure the task actually starts + wait(for: [iterated], timeout: 1.0) + + // When + task.cancel() + + // Then + wait(for: [finished], timeout: 1.0) + } + + func test_given_a_base_sequence_when_buffering_with_bounded_then_the_buffer_is_filled_in_and_suspends() async { + // Gicen + var base = GatedSequence([1, 2, 3, 4, 5]) + let buffered = base.buffer(policy: .bounded(2)) + var iterator = buffered.makeAsyncIterator() + + // When + base.advance() + + // Then var value = await iterator.next() - var pushed = await delegate.pushed - XCTAssertEqual(pushed, [1]) XCTAssertEqual(value, 1) - gated.advance() - gated.advance() - gated.advance() + + // When + base.advance() + base.advance() + base.advance() + + // Then value = await iterator.next() XCTAssertEqual(value, 2) value = await iterator.next() - pushed = await delegate.pushed XCTAssertEqual(value, 3) value = await iterator.next() - pushed = await delegate.pushed - XCTAssertEqual(pushed, [1, 2, 3, 4]) XCTAssertEqual(value, 4) - gated.advance() - gated.advance() + + // When + base.advance() + base.advance() + + // Then value = await iterator.next() - pushed = await delegate.pushed - XCTAssertEqual(pushed, [1, 2, 3, 4, 5]) XCTAssertEqual(value, 5) value = await iterator.next() XCTAssertEqual(value, nil) - value = await iterator.next() - XCTAssertEqual(value, nil) - } - - func test_delegatedBuffer_withError() async { - actor BufferDelegate: AsyncBuffer { - var buffer = [Int]() - var pushed = [Int]() - - func push(_ element: Int) async { - buffer.append(element) - pushed.append(element) - } - func pop() async throws -> Int? { - if buffer.count > 0 { - let value = buffer.removeFirst() - if value == 3 { - throw Failure() - } - return value - } - return nil - } - } - let delegate = BufferDelegate() + let pastEnd = await iterator.next() + XCTAssertEqual(value, pastEnd) + } + func test_given_a_failable_base_sequence_when_buffering_with_bounded_then_the_failure_is_forwarded() async { + // Given var gated = GatedSequence([1, 2, 3, 4, 5, 6, 7]) - let sequence = gated.buffer { delegate } - var iterator = sequence.makeAsyncIterator() + let base = gated.map { try throwOn(3, $0) } + let buffered = base.buffer(policy: .bounded(5)) + var iterator = buffered.makeAsyncIterator() + // When gated.advance() + + // Then var value = try! await iterator.next() XCTAssertEqual(value, 1) + + // When gated.advance() gated.advance() gated.advance() + + // Then value = try! await iterator.next() XCTAssertEqual(value, 2) + // When gated.advance() gated.advance() gated.advance() gated.advance() + + // Then do { value = try await iterator.next() XCTFail("next() should have thrown.") - } catch { } + } catch { + XCTAssert(error is Failure) + } - value = try! await iterator.next() - XCTAssertNil(value) + var pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) + pastFailure = try! await iterator.next() + XCTAssertNil(pastFailure) - value = try! await iterator.next() - XCTAssertNil(value) - } - - func test_byteBuffer() async { - actor ByteBuffer: AsyncBuffer { - var buffer: [UInt8]? - - func push(_ element: UInt8) async { - if buffer == nil { - buffer = [UInt8]() - } - buffer?.append(element) - } - - func pop() async -> [UInt8]? { - defer { buffer = nil } - return buffer - } - } - - var data = Data() - for _ in 0..<4096 { - data.append(UInt8.random(in: 0.. Date: Wed, 15 Feb 2023 23:32:12 +0100 Subject: [PATCH 089/149] evolution: add buffer (#252) --- Evolution/0009-buffer.md | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 Evolution/0009-buffer.md diff --git a/Evolution/0009-buffer.md b/Evolution/0009-buffer.md new file mode 100644 index 00000000..0e2b2fd4 --- /dev/null +++ b/Evolution/0009-buffer.md @@ -0,0 +1,114 @@ +# Buffer + +* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-buffer.md) +* Author(s): [Thibault Wittemberg](https://github.com/twittemb) +* Status: **Implemented** +* Implementation: [ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBuffer.swift) +] +* Decision Notes: +* Bugs: + +## Introduction + +Buffering is a technique that balances supply and demand by temporarily storing elements to even out fluctuations in production and consumption rates. `AsyncStream` facilitates this process by allowing you to control the size of the buffer and the strategy for handling elements that exceed that size. However, this approach may not be suitable for all situations, and it doesn't provide a way to adapt other `AsyncSequence` types to incorporate buffering. + +This proposal presents a new type that addresses these more advanced requirements and offers a comprehensive solution for buffering in asynchronous sequences. + +## Motivation + +As an `AsyncSequence` operates as a pull system, the production of elements is directly tied to the demand expressed by the consumer. The slower the consumer is in requesting elements, the slower the production of these elements will be. This can negatively impact the software that produces the elements, as demonstrated in the following example. + +Consider an `AsyncSequence` that reads and returns a line from a file every time a new element is requested. To ensure exclusive access to the file, a lock is maintained while reading. Ideally, the lock should be held for as short a duration as possible to allow other processes to access the file. However, if the consumer is slow in processing received lines or has a fluctuating pace, the lock will be maintained for an extended period, reducing the performance of the system. + +To mitigate this issue, a buffer operator can be employed to streamline the consumption of the `AsyncSequence`. This operator allows an internal iteration of the `AsyncSequence` independently from the consumer. Each element would then be made available for consumption by using a queuing mechanism. + +By applying the buffer operator to the previous example, the file can be read as efficiently as possible, allowing the lock to be released in a timely manner. This results in improved performance, as the consumer can consume elements at its own pace without negatively affecting the system. The buffer operator ensures that the production of elements is not limited by the pace of the consumer, allowing both the producer and consumer to operate at optimal levels. + +## Proposed Solution + +We propose to extend `AsyncSequence` with a `buffer()` operator. This operator will return an `AsyncBuffereSequence` that wraps the source `AsyncSequence` and handle the buffering mechanism. + +This operator will accept an `AsyncBufferSequencePolicy`. The policy will dictate the behaviour in case of a buffer overflow. + +As of now we propose 4 different behaviours: + +```swift +public struct AsyncBufferSequencePolicy: Sendable { + public static func bounded(_ limit: Int) + public static var unbounded + public static func bufferingLatest(_ limit: Int) + public static func bufferingOldest(_ limit: Int) +} +``` + +And the public API of `AsyncBuffereSequence` will be: + +```swift +extension AsyncSequence where Self: Sendable { + public func buffer( + policy: AsyncBufferSequencePolicy + ) -> AsyncBufferSequence { + AsyncBufferSequence(base: self, policy: policy) + } +} + +public struct AsyncBufferSequence: AsyncSequence { + public typealias Element = Base.Element + + public func makeAsyncIterator() -> Iterator + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } +} + +extension AsyncBufferSequence: Sendable where Base: Sendable { } +``` + +## Notes on Sendable + +Since all buffering means that the base asynchronous sequence must be iterated independently of the consumption (to resolve the production versus consumption issue) the base `AsyncSequence` needs to be able to be sent across task boundaries (the iterator does not need this requirement). + +## Detailed Design + +The choice of the buffering policy is made through an enumeration whose values are related to a storage type. To date, two types of storage are implemented: + +- `BoundedBufferStorage` backing the `AsyncBufferSequencePolicy.bounded(_:)` policy, +- `UnboundedBufferStorage` backing the `AsyncBufferSequencePolicy.unbounded`, `AsyncBufferSequencePolicy.bufferingNewest(_:)` and `AsyncBufferSequencePolicy.bufferingLatest(_:)` policies. + +Both storage types rely on a Mealy state machine stored in a `ManagedCriticalState`. They drive the mutations of the internal buffer, while ensuring that concurrent access to the state are safe. + +### BoundedBufferStorage + +`BoundedBufferStorage` is instantiated with the upstream `AsyncSequence` and a buffer maximum size. Upon the first call to the `next()` method, a task is spawned to support the iteration over this `AsyncSequence`. The iteration retrieves elements and adds them to an internal buffer as long as the buffer limit has not been reached. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case, the iteration is temporarily suspended until additional space becomes available. + +### UnboundedBufferStorage + +`UnboundedBufferStorage` is instantiated with the upstream `AsyncSequence` and a buffering policy. Upon the first call to the `next()` method, a task is spawned to support the iteration over this `AsyncSequence`. + +From there the behaviour will depend on the buffering policy. + +#### Unbounded +If the policy is `unbounded`, the iteration retrieves elements and adds them to an internal buffer until the upstream `AsyncSequence` finishes or fails. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. + +#### BufferingLatest +If the policy is `bufferingLatest(_:)`, the iteration retrieves elements and adds them to an internal buffer. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case the oldest buffered elements will be removed from the buffer and the latest ones will be added. + +#### BufferingOldest +If the policy is `bufferingOldest(_:)`, the iteration retrieves elements and adds them to an internal buffer. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case the latest element will be discarded and never added to the buffer. + +### Terminal events and cancellation + +Terminal events from the upstream `AsyncSequence`, such as normal completion or failure, are delayed, ensuring that the consumer will receive all the elements before receiving the termination. + +If the consuming `Task` is cancelled, so will be the `Task` supporting the iteration of the upstream `AsyncSequence`. + +## Alternatives Considered + +The buffering mechanism was originally thought to rely on an open implementation of an `AsyncBuffer` protocol, which would be constrained to an `Actor` conformance. It was meant for developers to be able to provide their own implementation of a buffering algorithm. This buffer was designed to control the behavior of elements when pushed and popped, in an isolated manner. + +A default implementation was provided to offers a queuing strategy for buffering elements, with options for unbounded, oldest-first, or newest-first buffering. + +This implementation was eventually discarded due to the potentially higher cost of calling isolated functions on an `Actor`, compared to using a low-level locking mechanism like the one employed in other operators through the use of the `ManagedCriticalState`. From 7e450054733d2335647d9ca7889540606b1b1166 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 28 Feb 2023 08:35:03 +0000 Subject: [PATCH 090/149] Switch from `group.waitForAll()` to `group.next()` (#254) # Motivation Swift 5.8 is including a change to how `group.waitForAll()` is working. It now properly waits for all tasks to finish even if one of the tasks throws. We have used `group.waitForAll()` in multiple places and need to change this code accordingly. # Modification Switch code from `group.waitForAll()` to `group.next()`. # Result This fixes a few stuck tests that we have seen when running against development snapshots. --- .../CombineLatestStateMachine.swift | 3 +- .../CombineLatest/CombineLatestStorage.swift | 54 ++++++++-------- .../Debounce/DebounceStateMachine.swift | 4 +- .../Debounce/DebounceStorage.swift | 64 ++++++++++--------- .../AsyncAlgorithms/Merge/MergeStorage.swift | 63 +++++++++--------- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 48 +++++++------- 6 files changed, 118 insertions(+), 118 deletions(-) diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 34f4dead..71d0507a 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -530,7 +530,8 @@ struct CombineLatestStateMachine< preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") case .upstreamsFinished: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + // We need to tolerate multiple upstreams failing + return .none case .waitingForDemand(let task, let upstreams, _): // An upstream threw. We can cancel everything now and transition to finished. diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index 31245579..bc32cee8 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -319,35 +319,33 @@ final class CombineLatestStorage< } } - do { - try await group.waitForAll() - } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - - switch action { - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let error, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .none: - break - } - } + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamThrew(error) + switch action { + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let error, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + downstreamContinuation.resume(returning: .failure(error)) + case .none: + break + } + } - group.cancelAll() + group.cancelAll() + } } } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index bd111ab1..98c23287 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -390,8 +390,8 @@ struct DebounceStateMachine { preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") case .upstreamFailure: - // The upstream already failed so it should never have throw again. - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + // We need to tolerate multiple upstreams failing + return .none case .waitingForDemand(let task, .none, let clockContinuation, .none): // We don't have any buffered element so we can just go ahead diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 21e1ddf6..cd1bb515 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -254,39 +254,41 @@ final class DebounceStorage: @unchecked Sendable } } - do { - try await group.waitForAll() - } catch { - // The upstream sequence threw an error - let action = self.stateMachine.withCriticalRegion { $0.upstreamThrew(error) } + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + self.stateMachine.withCriticalRegion { stateMachine in + let action = stateMachine.upstreamThrew(error) + switch action { + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + let downstreamContinuation, + let error, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) - switch action { - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - let downstreamContinuation, - let error, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .cancelTaskAndClockContinuation( - let task, - let clockContinuation - ): - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - - case .none: - break - } + task.cancel() + + downstreamContinuation.resume(returning: .failure(error)) - group.cancelAll() + case .cancelTaskAndClockContinuation( + let task, + let clockContinuation + ): + clockContinuation?.resume(throwing: CancellationError()) + task.cancel() + case .none: + break + } + } + + group.cancelAll() + } } } } diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index de4c72b8..7a83ad8b 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -404,40 +404,39 @@ final class MergeStorage< } } } - - do { - try await group.waitForAll() - } catch { + + while !group.isEmpty { + do { + try await group.next() + } catch { // One of the upstream sequences threw an error - let action = self.lock.withLock { - self.stateMachine.upstreamThrew(error) - } - - switch action { - case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - error, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - - task.cancel() - - downstreamContinuation.resume(throwing: error) - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - - task.cancel() - - case .none: - break + let action = self.lock.withLock { + self.stateMachine.upstreamThrew(error) + } + switch action { + case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + error, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + + downstreamContinuation.resume(throwing: error) + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + case .none: + break + } + group.cancelAll() } - - group.cancelAll() } } } diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index d9fb3a82..f997e681 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -286,31 +286,31 @@ final class ZipStorage Date: Thu, 9 Mar 2023 10:40:27 +0000 Subject: [PATCH 091/149] Package.swift: make package name consistent (#255) Rest of SwiftPM packages provided by Apple follow a dash-case naming scheme with `swift-` prefix. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4f716a85..a1562d76 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( - name: "AsyncAlgorithms", + name: "swift-async-algorithms", platforms: [ .macOS("10.15"), .iOS("13.0"), From 9d83fff5b8cdf4dedaf006d0c72bb93595815126 Mon Sep 17 00:00:00 2001 From: elmetal Date: Wed, 10 May 2023 21:05:56 +0900 Subject: [PATCH 092/149] [Channel] Fix Source links in documentation (#264) --- .../AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md index 19eed41c..dc2c2e66 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md @@ -3,8 +3,8 @@ * Author(s): [Philippe Hausler](https://github.com/phausler) [ -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChannel.swift), -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift) | +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift), +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChannel.swift) ] From 0c00af2372d340a152d6cf5cd359d927daaaf64e Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 10 May 2023 16:10:31 +0400 Subject: [PATCH 093/149] Clean up old evolution proposals (#258) # Motivation Our old proposals had a couple of stale links and were not all aligned. This cleans them up. --- Evolution/0001-zip.md | 10 ++++------ Evolution/0002-merge.md | 10 ++++------ Evolution/0003-compacted.md | 4 ++-- Evolution/0004-joined.md | 2 +- Evolution/0005-adjacent-pairs.md | 2 +- Evolution/0006-combineLatest.md | 10 ++++------ Evolution/0007-chain.md | 2 +- Evolution/0008-bytes.md | 4 ++-- 0009-async.md => Evolution/0009-async.md | 2 +- Evolution/{0009-buffer.md => 0010-buffer.md} | 4 ++-- 10 files changed, 22 insertions(+), 28 deletions(-) rename 0009-async.md => Evolution/0009-async.md (94%) rename Evolution/{0009-buffer.md => 0010-buffer.md} (98%) diff --git a/Evolution/0001-zip.md b/Evolution/0001-zip.md index 1ba48205..2e1885c5 100644 --- a/Evolution/0001-zip.md +++ b/Evolution/0001-zip.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0001](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0001-zip.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestZip.swift)] * Decision Notes: * Bugs: @@ -45,8 +45,7 @@ public func zip: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -59,8 +58,7 @@ public struct AsyncZip2Sequence: Sen public struct AsyncZip3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0002-merge.md b/Evolution/0002-merge.md index ff15f23d..b3dacfc5 100644 --- a/Evolution/0002-merge.md +++ b/Evolution/0002-merge.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0002](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0002-merge.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Asyncmerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)] * Decision Notes: * Bugs: @@ -46,8 +46,7 @@ public struct AsyncMerge2Sequence: S where Base1.Element == Base2.Element, Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = Base1.Element public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncMerge3Sequence: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncCombineLatest2Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0007-chain.md b/Evolution/0007-chain.md index 8df4804f..df2aa7c1 100644 --- a/Evolution/0007-chain.md +++ b/Evolution/0007-chain.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0007](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0007-chain.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] diff --git a/Evolution/0008-bytes.md b/Evolution/0008-bytes.md index 76e2f3a0..50e2cf28 100644 --- a/Evolution/0008-bytes.md +++ b/Evolution/0008-bytes.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0008](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0008-bytes.md) * Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] @@ -33,7 +33,7 @@ struct AsyncBytes: AsyncSequence { ## Detailed Design ```swift -public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { +public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 public init( diff --git a/0009-async.md b/Evolution/0009-async.md similarity index 94% rename from 0009-async.md rename to Evolution/0009-async.md index 897e57b8..a29984ab 100644 --- a/0009-async.md +++ b/Evolution/0009-async.md @@ -1,6 +1,6 @@ # AsyncSyncSequence -* Proposal: [NNNN](NNNN-lazy.md) +* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-async.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** diff --git a/Evolution/0009-buffer.md b/Evolution/0010-buffer.md similarity index 98% rename from Evolution/0009-buffer.md rename to Evolution/0010-buffer.md index 0e2b2fd4..da56def7 100644 --- a/Evolution/0009-buffer.md +++ b/Evolution/0010-buffer.md @@ -1,8 +1,8 @@ # Buffer -* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-buffer.md) +* Proposal: [SAA-0010](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0010-buffer.md) * Author(s): [Thibault Wittemberg](https://github.com/twittemb) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [ [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBuffer.swift) From b3394663fd8ad9c701cbfcffcaedb64afc7e92be Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 10 May 2023 18:18:41 +0400 Subject: [PATCH 094/149] Add Interspersed proposal (#259) * Clean up old evolution proposals # Motivation Our old proposals had a couple of stale links and were not all aligned. This cleans them up. * Add Interspersed proposal # Motivation Add a new evolution proposal for the interspersed algorithm. --- Evolution/0011-interspersed.md | 135 ++++++++++++++++++ .../AsyncInterspersedSequence.swift | 22 ++- .../{ => Interspersed}/TestInterspersed.swift | 39 ++--- 3 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 Evolution/0011-interspersed.md rename Sources/AsyncAlgorithms/{ => Interspersed}/AsyncInterspersedSequence.swift (81%) rename Tests/AsyncAlgorithmsTests/{ => Interspersed}/TestInterspersed.swift (71%) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md new file mode 100644 index 00000000..79cc02f2 --- /dev/null +++ b/Evolution/0011-interspersed.md @@ -0,0 +1,135 @@ +# Feature name + +* Proposal: [SAA-0011](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0011-interspersed.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Review Manager: [Franz Busch](https://github.com/FranzBusch) +* Status: **Implemented** + +* Implementation: + [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift) + +## Motivation + +A common transformation that is applied to async sequences is to intersperse the elements with +a separator element. + +## Proposed solution + +We propose to add a new method on `AsyncSequence` that allows to intersperse +a separator between each emitted element. This proposed API looks like this + +```swift +extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameter separator: The value to insert in between each of this async + /// sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, separator: separator) + } +} +``` + +## Detailed design + +The bulk of the implementation of the new `interspersed` method is inside the new +`AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence +inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand +to the base iterator. +There is one special case that we have to call out. When the base async sequence throws +then `AsyncInterspersedSequence.Iterator.next()` will return the separator first and then rethrow the error. + +Below is the implementation of the `AsyncInterspersedSequence`. +```swift +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncInterspersedSequence { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Base.Element + + @usableFromInline + internal init(_ base: Base, separator: Base.Element) { + self.base = base + self.separator = separator + } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start + case element(Result) + case separator + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Base.Element + + @usableFromInline + internal var state = State.start + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { + self.iterator = iterator + self.separator = separator + } + + public mutating func next() async rethrows -> Base.Element? { + // After the start, the state flips between element and separator. Before + // returning a separator, a check is made for the next element as a + // separator is only returned between two elements. The next element is + // stored to allow it to be returned in the next iteration. However, if + // the checking the next element throws, the separator is emitted before + // rethrowing that error. + switch state { + case .start: + state = .separator + return try await iterator.next() + case .separator: + do { + guard let next = try await iterator.next() else { return nil } + state = .element(.success(next)) + } catch { + state = .element(.failure(error)) + } + return separator + case .element(let result): + state = .separator + return try result._rethrowGet() + } + } + } + + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.AsyncIterator { + AsyncIterator(base.makeAsyncIterator(), separator: separator) + } +} +``` diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift similarity index 81% rename from Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift rename to Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 43f8d974..7dd08c53 100644 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -10,10 +10,21 @@ //===----------------------------------------------------------------------===// extension AsyncSequence { - /// Returns an asynchronous sequence containing elements of this asynchronous sequence with - /// the given separator inserted in between each element. + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. /// - /// Any value of the asynchronous sequence's element type can be used as the separator. + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` /// /// - Parameter separator: The value to insert in between each of this async /// sequence’s elements. @@ -95,8 +106,11 @@ extension AsyncInterspersedSequence: AsyncSequence { @inlinable public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) + Iterator(base.makeAsyncIterator(), separator: separator) } } extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift similarity index 71% rename from Tests/AsyncAlgorithmsTests/TestInterspersed.swift rename to Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 79c93f73..2f351ab0 100644 --- a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -66,26 +66,33 @@ final class TestInterspersed: XCTestCase { func test_cancellation() async { let source = Indefinite(value: "test") let sequence = source.async.interspersed(with: "sep") - let finished = expectation(description: "finished") - let iterated = expectation(description: "iterated") - let task = Task { + let lockStepChannel = AsyncChannel() - var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() - iterated.fulfill() + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() - while let _ = await iterator.next() { } + // Information the parent task that we are consuming + await lockStepChannel.send(()) - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) + while let _ = await iterator.next() { } - finished.fulfill() + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + + // Information the parent task that we finished consuming + await lockStepChannel.send(()) + } + + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } + + // Now we cancel the child + group.cancelAll() + + // Waiting until the child task finished consuming + _ = await lockStepChannel.first { _ in true } } - // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) - // cancellation should ensure the loop finishes - // without regards to the remaining underlying sequence - task.cancel() - wait(for: [finished], timeout: 1.0) } } From ac9309b2ac88ccb6e96c8add8c4e943cf439cc48 Mon Sep 17 00:00:00 2001 From: Kazumasa Shimomura Date: Fri, 23 Jun 2023 04:54:24 +0900 Subject: [PATCH 095/149] Rename `downStream` to `downstream` (#263) --- .../CombineLatest/CombineLatestStorage.swift | 6 +++--- .../AsyncAlgorithms/Debounce/DebounceStateMachine.swift | 8 ++++---- Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift | 4 ++-- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index bc32cee8..d3b67404 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -58,7 +58,7 @@ final class CombineLatestStorage< base1: base1, base2: base2, base3: base3, - downStreamContinuation: continuation + downstreamContinuation: continuation ) case .resumeContinuation(let downstreamContinuation, let result): @@ -108,7 +108,7 @@ final class CombineLatestStorage< base1: Base1, base2: Base2, base3: Base3?, - downStreamContinuation: StateMachine.DownstreamContinuation + downstreamContinuation: StateMachine.DownstreamContinuation ) { // This creates a new `Task` that is iterating the upstream // sequences. We must store it to cancel it at the right times. @@ -350,6 +350,6 @@ final class CombineLatestStorage< } } - stateMachine.taskIsStarted(task: task, downstreamContinuation: downStreamContinuation) + stateMachine.taskIsStarted(task: task, downstreamContinuation: downstreamContinuation) } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index 98c23287..b75d2f3e 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -517,8 +517,8 @@ struct DebounceStateMachine { /// Actions returned by `clockSleepFinished()`. enum ClockSleepFinishedAction { /// Indicates that the downstream continuation should be resumed with the given element. - case resumeDownStreamContinuation( - downStreamContinuation: UnsafeContinuation, Never>, + case resumeDownstreamContinuation( + downstreamContinuation: UnsafeContinuation, Never>, element: Element ) } @@ -547,8 +547,8 @@ struct DebounceStateMachine { bufferedElement: nil ) - return .resumeDownStreamContinuation( - downStreamContinuation: downstreamContinuation, + return .resumeDownstreamContinuation( + downstreamContinuation: downstreamContinuation, element: currentElement.element ) } else { diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index cd1bb515..40c30634 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -238,8 +238,8 @@ final class DebounceStorage: @unchecked Sendable let action = self.stateMachine.withCriticalRegion { $0.clockSleepFinished() } switch action { - case .resumeDownStreamContinuation(let downStreamContinuation, let element): - downStreamContinuation.resume(returning: .success(element)) + case .resumeDownstreamContinuation(let downstreamContinuation, let element): + downstreamContinuation.resume(returning: .success(element)) case .none: break diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index f997e681..7d971a78 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -49,7 +49,7 @@ final class ZipStorage Date: Fri, 23 Jun 2023 06:40:09 -0700 Subject: [PATCH 096/149] Audit pass on inline and initialization (#271) --- .../AsyncInclusiveReductionsSequence.swift | 2 +- .../AsyncThrowingInclusiveReductionsSequence.swift | 2 +- Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift | 8 ++++---- .../CombineLatest/AsyncCombineLatest2Sequence.swift | 2 +- .../AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift | 2 +- .../Interspersed/AsyncInterspersedSequence.swift | 4 ++-- Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index 12611c14..0dab7cb0 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -58,7 +58,7 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { internal let transform: @Sendable (Base.Element, Base.Element) async -> Base.Element @inlinable - internal init( + init( _ iterator: Base.AsyncIterator, transform: @Sendable @escaping (Base.Element, Base.Element) async -> Base.Element ) { diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 36e88fb5..2a03304f 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -57,7 +57,7 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { internal let transform: @Sendable (Base.Element, Base.Element) async throws -> Base.Element @inlinable - internal init( + init( _ iterator: Base.AsyncIterator, transform: @Sendable @escaping (Base.Element, Base.Element) async throws -> Base.Element ) { diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index 8049a1a0..817615a4 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -70,7 +70,7 @@ public struct AsyncBufferSequencePolicy: Sendable { /// An `AsyncSequence` that buffers elements in regard to a policy. public struct AsyncBufferSequence: AsyncSequence { - enum StorageType { + enum StorageType { case transparent(Base.AsyncIterator) case bounded(storage: BoundedBufferStorage) case unbounded(storage: UnboundedBufferStorage) @@ -82,7 +82,7 @@ public struct AsyncBufferSequence: AsyncSequence let base: Base let policy: AsyncBufferSequencePolicy - public init( + init( base: Base, policy: AsyncBufferSequencePolicy ) { @@ -91,7 +91,7 @@ public struct AsyncBufferSequence: AsyncSequence } public func makeAsyncIterator() -> Iterator { - let storageType: StorageType + let storageType: StorageType switch self.policy.policy { case .bounded(...0), .bufferingNewest(...0), .bufferingOldest(...0): storageType = .transparent(self.base.makeAsyncIterator()) @@ -108,7 +108,7 @@ public struct AsyncBufferSequence: AsyncSequence } public struct Iterator: AsyncIteratorProtocol { - var storageType: StorageType + var storageType: StorageType public mutating func next() async rethrows -> Element? { switch self.storageType { diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index c07bd099..f8fd86cc 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -46,7 +46,7 @@ public struct AsyncCombineLatest2Sequence< let base1: Base1 let base2: Base2 - public init(_ base1: Base1, _ base2: Base2) { + init(_ base1: Base1, _ base2: Base2) { self.base1 = base1 self.base2 = base2 } diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index 5ae17f14..e2f8b7a9 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -41,7 +41,7 @@ public struct AsyncDebounceSequence: Sendable whe /// - interval: The interval to debounce. /// - tolerance: The tolerance of the clock. /// - clock: The clock. - public init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { + init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { self.base = base self.interval = interval self.tolerance = tolerance diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 7dd08c53..18398f9f 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -45,7 +45,7 @@ public struct AsyncInterspersedSequence { internal let separator: Base.Element @usableFromInline - internal init(_ base: Base, separator: Base.Element) { + init(_ base: Base, separator: Base.Element) { self.base = base self.separator = separator } @@ -73,7 +73,7 @@ extension AsyncInterspersedSequence: AsyncSequence { internal var state = State.start @usableFromInline - internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { + init(_ iterator: Base.AsyncIterator, separator: Base.Element) { self.iterator = iterator self.separator = separator } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 2723d112..c1a45ba3 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -40,7 +40,7 @@ public struct AsyncMerge2Sequence< /// - Parameters: /// - base1: The first upstream ``Swift/AsyncSequence``. /// - base2: The second upstream ``Swift/AsyncSequence``. - public init( + init( _ base1: Base1, _ base2: Base2 ) { diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index c2b54eb1..6f5abf13 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -49,7 +49,7 @@ public struct AsyncMerge3Sequence< /// - base1: The first upstream ``Swift/AsyncSequence``. /// - base2: The second upstream ``Swift/AsyncSequence``. /// - base3: The third upstream ``Swift/AsyncSequence``. - public init( + init( _ base1: Base1, _ base2: Base2, _ base3: Base3 From 936a68d01218f5ddeaacbef63fdb45af6c20c360 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 23 Jun 2023 14:47:25 +0100 Subject: [PATCH 097/149] [AsyncInterspersedSequence] Integrate review feedback (#267) * Integrate review feedback This integrates all of the feedback from the review thread. Here is a quick summary: - Change the trailing separator behaviour. We are no longer returning a separator before we are forwarding the error - Add a synchronous and asynchronous closure based `interspersed` method. - Support interspersing every n elements * Add AsyncThrowingInterspersedSequence * Update examples --- Evolution/0011-interspersed.md | 348 ++++++++++--- .../AsyncInterspersedSequence.swift | 484 +++++++++++++++--- .../Interspersed/TestInterspersed.swift | 226 +++++--- 3 files changed, 829 insertions(+), 229 deletions(-) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md index 79cc02f2..cfc99737 100644 --- a/Evolution/0011-interspersed.md +++ b/Evolution/0011-interspersed.md @@ -17,33 +17,134 @@ a separator element. ## Proposed solution We propose to add a new method on `AsyncSequence` that allows to intersperse -a separator between each emitted element. This proposed API looks like this +a separator between every n emitted element. This proposed API looks like this ```swift -extension AsyncSequence { - /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting - /// the given separator between each element. - /// - /// Any value of this asynchronous sequence's element type can be used as the separator. - /// - /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: - /// - /// ``` - /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") - /// for await element in interspersed { - /// print(element) - /// } - /// // Prints "A" "-" "B" "-" "C" - /// ``` - /// - /// - Parameter separator: The value to insert in between each of this async - /// sequence’s elements. - /// - Returns: The interspersed asynchronous sequence of elements. - @inlinable - public func interspersed(with separator: Element) -> AsyncInterspersedSequence { - AsyncInterspersedSequence(self, separator: separator) - } +public extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } } ``` @@ -53,83 +154,166 @@ The bulk of the implementation of the new `interspersed` method is inside the ne `AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand to the base iterator. -There is one special case that we have to call out. When the base async sequence throws -then `AsyncInterspersedSequence.Iterator.next()` will return the separator first and then rethrow the error. Below is the implementation of the `AsyncInterspersedSequence`. ```swift /// An asynchronous sequence that presents the elements of a base asynchronous sequence of /// elements with a separator between each of those elements. public struct AsyncInterspersedSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let separator: Base.Element - - @usableFromInline - internal init(_ base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } -} + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } -extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element + @usableFromInline + internal let base: Base - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline - internal enum State { - case start - case element(Result) - case separator - } + internal let separator: Separator @usableFromInline - internal var iterator: Base.AsyncIterator + internal let every: Int @usableFromInline - internal let separator: Base.Element + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } @usableFromInline - internal var state = State.start + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } @usableFromInline - internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) - public mutating func next() async rethrows -> Base.Element? { - // After the start, the state flips between element and separator. Before - // returning a separator, a check is made for the next element as a - // separator is only returned between two elements. The next element is - // stored to allow it to be returned in the next iteration. However, if - // the checking the next element throws, the separator is emitted before - // rethrowing that error. - switch state { - case .start: - state = .separator - return try await iterator.next() - case .separator: - do { - guard let next = try await iterator.next() else { return nil } - state = .element(.success(next)) - } catch { - state = .element(.failure(error)) - } - return separator - case .element(let result): - state = .separator - return try result._rethrowGet() - } + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async rethrows -> Base.Element? { + // After the start, the state flips between element and separator. Before + // returning a separator, a check is made for the next element as a + // separator is only returned between two elements. The next element is + // stored to allow it to be returned in the next iteration. However, if + // the checking the next element throws, the separator is emitted before + // rethrowing that error. + switch state { + case var .start(element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if every == 1 { + state = .separator + } else { + state = .element(1) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + state = .start(element) + switch separator { + case let .element(element): + return element + + case let .syncClosure(closure): + return closure() + + case let .asyncClosure(closure): + return await closure() + } + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case let .element(count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if every == newCount { + state = .separator + } else { + state = .element(newCount) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .finished: + return nil + } + } } - } - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.AsyncIterator { - AsyncIterator(base.makeAsyncIterator(), separator: separator) - } + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { + Iterator(base.makeAsyncIterator(), every: every, separator: separator) + } } ``` diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 18398f9f..9932e77e 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -10,107 +10,435 @@ //===----------------------------------------------------------------------===// extension AsyncSequence { - /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting - /// the given separator between each element. - /// - /// Any value of this asynchronous sequence's element type can be used as the separator. - /// - /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: - /// - /// ``` - /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") - /// for await element in interspersed { - /// print(element) - /// } - /// // Prints "A" "-" "B" "-" "C" - /// ``` - /// - /// - Parameter separator: The value to insert in between each of this async - /// sequence’s elements. - /// - Returns: The interspersed asynchronous sequence of elements. - @inlinable - public func interspersed(with separator: Element) -> AsyncInterspersedSequence { - AsyncInterspersedSequence(self, separator: separator) - } + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } } /// An asynchronous sequence that presents the elements of a base asynchronous sequence of /// elements with a separator between each of those elements. public struct AsyncInterspersedSequence { - @usableFromInline - internal let base: Base + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } + + @usableFromInline + internal let base: Base - @usableFromInline - internal let separator: Base.Element + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int - @usableFromInline - init(_ base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } + @usableFromInline + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } } extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async rethrows -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .element(let element): + return element - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct Iterator: AsyncIteratorProtocol { + case .syncClosure(let closure): + return closure() + + case .asyncClosure(let closure): + return await closure() + } + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncThrowingInterspersedSequence { @usableFromInline - internal enum State { - case start - case element(Result) - case separator + internal enum Separator { + case syncClosure(@Sendable () throws -> Element) + case asyncClosure(@Sendable () async throws -> Element) } @usableFromInline - internal var iterator: Base.AsyncIterator + internal let base: Base + + @usableFromInline + internal let separator: Separator @usableFromInline - internal let separator: Base.Element + internal let every: Int @usableFromInline - internal var state = State.start + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } @usableFromInline - init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator - } - - public mutating func next() async rethrows -> Base.Element? { - // After the start, the state flips between element and separator. Before - // returning a separator, a check is made for the next element as a - // separator is only returned between two elements. The next element is - // stored to allow it to be returned in the next iteration. However, if - // the checking the next element throws, the separator is emitted before - // rethrowing that error. - switch state { - case .start: - state = .separator - return try await iterator.next() - case .separator: - do { - guard let next = try await iterator.next() else { return nil } - state = .element(.success(next)) - } catch { - state = .element(.failure(error)) - } - return separator - case .element(let result): - state = .separator - return try result._rethrowGet() - } - } - } - - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) - } + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } } -extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncThrowingInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async throws -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .syncClosure(let closure): + return try closure() + + case .asyncClosure(let closure): + return try await closure() + } + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} + +extension AsyncThrowingInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncThrowingInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) extension AsyncInterspersedSequence.Iterator: Sendable {} +@available(*, unavailable) +extension AsyncThrowingInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 2f351ab0..6e09e84d 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -9,90 +9,178 @@ // //===----------------------------------------------------------------------===// -import XCTest import AsyncAlgorithms +import XCTest final class TestInterspersed: XCTestCase { - func test_interspersed() async { - let source = [1, 2, 3, 4, 5] - let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) + func test_interspersed() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_empty() async { - let source = [Int]() - let expected = [Int]() - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) + + func test_interspersed_every() async { + let source = [1, 2, 3, 4, 5, 6, 7, 8] + let expected = [1, 2, 3, 0, 4, 5, 6, 0, 7, 8] + let sequence = source.async.interspersed(every: 3, with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_with_throwing_upstream() async { - let source = [1, 2, 3, -1, 4, 5] - let expected = [1, 0, 2, 0, 3, 0] - var actual = [Int]() - let sequence = source.async.map { - try throwOn(-1, $0) - }.interspersed(with: 0) - - var iterator = sequence.makeAsyncIterator() - do { - while let item = try await iterator.next() { - actual.append(item) - } - XCTFail() - } catch { - XCTAssertEqual(Failure(), error as? Failure) + + func test_interspersed_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: { 0 }) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = try! await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = source.async.interspersed(with: "sep") - let lockStepChannel = AsyncChannel() - - await withTaskGroup(of: Void.self) { group in - group.addTask { + + func test_interspersed_async_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed { + try! await Task.sleep(nanoseconds: 1000) + return 0 + } + var actual = [Int]() var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } - // Information the parent task that we are consuming - await lockStepChannel.send(()) + func test_interspersed_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed(with: { throw Failure() }) - while let _ = await iterator.next() { } + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_async_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed { + try await Task.sleep(nanoseconds: 1000) + throw Failure() + } + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_empty() async { + let source = [Int]() + let expected = [Int]() + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } let pastEnd = await iterator.next() XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_with_throwing_upstream() async { + let source = [1, 2, 3, -1, 4, 5] + let expected = [1, 0, 2, 0, 3] + var actual = [Int]() + let sequence = source.async.map { + try throwOn(-1, $0) + }.interspersed(with: 0) + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_cancellation() async { + let source = Indefinite(value: "test") + let sequence = source.async.interspersed(with: "sep") + let lockStepChannel = AsyncChannel() + + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() + + // Information the parent task that we are consuming + await lockStepChannel.send(()) + + while let _ = await iterator.next() {} + + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - // Information the parent task that we finished consuming - await lockStepChannel.send(()) - } + // Information the parent task that we finished consuming + await lockStepChannel.send(()) + } - // Waiting until the child task started consuming - _ = await lockStepChannel.first { _ in true } + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } - // Now we cancel the child - group.cancelAll() + // Now we cancel the child + group.cancelAll() - // Waiting until the child task finished consuming - _ = await lockStepChannel.first { _ in true } + // Waiting until the child task finished consuming + _ = await lockStepChannel.first { _ in true } + } } - } } From 156bb4bf495789bf1f13b78cfa741d193a2f4700 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 23 Jun 2023 12:22:23 -0700 Subject: [PATCH 098/149] Reduce the overall warnings from tests (#274) * Migrate from semaphore style testing waits to async test fulfillment * Reduce warnings for missing hashable and comparable conformances per availability * Reduce warnings for executor (still one known warning) --- Sources/AsyncSequenceValidation/Clock.swift | 4 ++++ Sources/AsyncSequenceValidation/Test.swift | 6 ++++++ .../TestAdjacentPairs.swift | 5 +++-- Tests/AsyncAlgorithmsTests/TestBuffer.swift | 8 ++++---- .../TestBufferedByteIterator.swift | 4 ++-- Tests/AsyncAlgorithmsTests/TestChain.swift | 8 ++++---- Tests/AsyncAlgorithmsTests/TestChannel.swift | 2 +- .../TestCombineLatest.swift | 18 ++++++++--------- .../AsyncAlgorithmsTests/TestCompacted.swift | 4 ++-- Tests/AsyncAlgorithmsTests/TestJoin.swift | 8 ++++---- Tests/AsyncAlgorithmsTests/TestLazy.swift | 4 ++-- .../TestManualClock.swift | 6 +++--- Tests/AsyncAlgorithmsTests/TestMerge.swift | 8 ++++---- .../AsyncAlgorithmsTests/TestReductions.swift | 4 ++-- .../TestRemoveDuplicates.swift | 4 ++-- .../TestThrowingChannel.swift | 2 +- .../AsyncAlgorithmsTests/TestValidator.swift | 20 +++++++++---------- Tests/AsyncAlgorithmsTests/TestZip.swift | 8 ++++---- 18 files changed, 67 insertions(+), 56 deletions(-) diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index 6f33d15a..e76f5aa9 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -132,3 +132,7 @@ extension AsyncSequenceValidationDiagram.Clock: TestClock { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram.Clock: Clock { } + +// placeholders to avoid warnings +extension AsyncSequenceValidationDiagram.Clock.Instant: Hashable { } +extension AsyncSequenceValidationDiagram.Clock.Instant: Comparable { } diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index 35177cf2..0275cc11 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -68,6 +68,12 @@ extension AsyncSequenceValidationDiagram { struct Context { final class ClockExecutor: SerialExecutor { + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + func enqueue(_ job: __owned ExecutorJob) { + job.runSynchronously(on: asUnownedSerialExecutor()) + } + + @available(*, deprecated) // known deprecation warning func enqueue(_ job: UnownedJob) { job._runSynchronously(on: asUnownedSerialExecutor()) } diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 843624f6..4fea0ef5 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -88,10 +88,11 @@ final class TestAdjacentPairs: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index f8ef8a3c..7bb45f89 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -190,13 +190,13 @@ final class TestBuffer: XCTestCase { finished.fulfill() } // ensure the task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // When task.cancel() // Then - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_given_a_base_sequence_when_buffering_with_bounded_then_the_buffer_is_filled_in_and_suspends() async { @@ -310,13 +310,13 @@ final class TestBuffer: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // When task.cancel() // Then - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_given_a_base_sequence_when_bounded_with_limit_0_then_the_policy_is_transparent() async { diff --git a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift index ee6c5d12..2c8841d8 100644 --- a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift +++ b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift @@ -199,10 +199,10 @@ final class TestBufferedByteIterator: XCTestCase { } } } - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChain.swift b/Tests/AsyncAlgorithmsTests/TestChain.swift index b3be90a3..6bb2cb55 100644 --- a/Tests/AsyncAlgorithmsTests/TestChain.swift +++ b/Tests/AsyncAlgorithmsTests/TestChain.swift @@ -87,13 +87,13 @@ final class TestChain2: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } @@ -192,12 +192,12 @@ final class TestChain3: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index b181bddd..b5d28cd9 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -133,7 +133,7 @@ final class TestChannel: XCTestCase { task1.cancel() // Then: the first sending operation is resumed - wait(for: [send1IsResumed], timeout: 1.0) + await fulfillment(of: [send1IsResumed], timeout: 1.0) // When: collecting elements var iterator = sut.makeAsyncIterator() diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index f33258c9..cb0f7324 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -84,7 +84,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) } @@ -126,7 +126,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) } @@ -168,7 +168,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) } @@ -210,7 +210,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) } @@ -250,7 +250,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(validator.failure as? Failure, Failure()) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b")]) } @@ -290,7 +290,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(validator.failure as? Failure, Failure()) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a")]) } @@ -312,11 +312,11 @@ final class TestCombineLatest2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_combineLatest_when_cancelled() async { @@ -389,7 +389,7 @@ final class TestCombineLatest3: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)]) } diff --git a/Tests/AsyncAlgorithmsTests/TestCompacted.swift b/Tests/AsyncAlgorithmsTests/TestCompacted.swift index b82fe31e..3e2e5198 100644 --- a/Tests/AsyncAlgorithmsTests/TestCompacted.swift +++ b/Tests/AsyncAlgorithmsTests/TestCompacted.swift @@ -83,10 +83,10 @@ final class TestCompacted: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestJoin.swift b/Tests/AsyncAlgorithmsTests/TestJoin.swift index 91de1ef9..c60b71da 100644 --- a/Tests/AsyncAlgorithmsTests/TestJoin.swift +++ b/Tests/AsyncAlgorithmsTests/TestJoin.swift @@ -122,11 +122,11 @@ final class TestJoinedBySeparator: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } @@ -206,10 +206,10 @@ final class TestJoined: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestLazy.swift b/Tests/AsyncAlgorithmsTests/TestLazy.swift index 1ef69d29..3ff8c5c1 100644 --- a/Tests/AsyncAlgorithmsTests/TestLazy.swift +++ b/Tests/AsyncAlgorithmsTests/TestLazy.swift @@ -121,12 +121,12 @@ final class TestLazy: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestManualClock.swift b/Tests/AsyncAlgorithmsTests/TestManualClock.swift index 045ba2de..15cc97bb 100644 --- a/Tests/AsyncAlgorithmsTests/TestManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/TestManualClock.swift @@ -29,7 +29,7 @@ final class TestManualClock: XCTestCase { clock.advance() XCTAssertFalse(state.withCriticalRegion { $0 }) clock.advance() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } @@ -51,7 +51,7 @@ final class TestManualClock: XCTestCase { XCTAssertFalse(state.withCriticalRegion { $0 }) clock.advance() task.cancel() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } @@ -73,7 +73,7 @@ final class TestManualClock: XCTestCase { } XCTAssertFalse(state.withCriticalRegion { $0 }) task.cancel() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index 0bcb3479..c8d5e1ce 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -185,11 +185,11 @@ final class TestMerge2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_merge_when_cancelled() async { @@ -509,11 +509,11 @@ final class TestMerge3: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } // MARK: - IteratorInitialized diff --git a/Tests/AsyncAlgorithmsTests/TestReductions.swift b/Tests/AsyncAlgorithmsTests/TestReductions.swift index ad9cf5ac..24e7e4e4 100644 --- a/Tests/AsyncAlgorithmsTests/TestReductions.swift +++ b/Tests/AsyncAlgorithmsTests/TestReductions.swift @@ -217,10 +217,10 @@ final class TestReductions: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift index 01ddf3ff..932585b0 100644 --- a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift +++ b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift @@ -95,10 +95,10 @@ final class TestRemoveDuplicates: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift index 7dd60f18..6110c884 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift @@ -270,7 +270,7 @@ final class TestThrowingChannel: XCTestCase { task1.cancel() // Then: the first sending operation is resumed - wait(for: [send1IsResumed], timeout: 1.0) + await fulfillment(of: [send1IsResumed], timeout: 1.0) // When: collecting elements var iterator = sut.makeAsyncIterator() diff --git a/Tests/AsyncAlgorithmsTests/TestValidator.swift b/Tests/AsyncAlgorithmsTests/TestValidator.swift index 9c5ef9c2..67d2f37e 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidator.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidator.swift @@ -24,7 +24,7 @@ final class TestValidator: XCTestCase { } XCTAssertFalse(state.withCriticalRegion { $0 }) gate.open() - wait(for: [entered], timeout: 1.0) + await fulfillment(of: [entered], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } @@ -52,18 +52,18 @@ final class TestValidator: XCTestCase { } finished.fulfill() } - wait(for: [started], timeout: 1.0) + await fulfillment(of: [started], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, []) gated.advance() - wait(for: [expectations[0]], timeout: 1.0) + await fulfillment(of: [expectations[0]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) gated.advance() - wait(for: [expectations[1]], timeout: 1.0) + await fulfillment(of: [expectations[1]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2]) gated.advance() - wait(for: [expectations[2]], timeout: 1.0) + await fulfillment(of: [expectations[2]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2, 3]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_gatedSequence_throwing() async { @@ -93,14 +93,14 @@ final class TestValidator: XCTestCase { } finished.fulfill() } - wait(for: [started], timeout: 1.0) + await fulfillment(of: [started], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, []) gated.advance() - wait(for: [expectations[0]], timeout: 1.0) + await fulfillment(of: [expectations[0]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) gated.advance() XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) XCTAssertEqual(failure.withCriticalRegion { $0 as? Failure }, Failure()) } @@ -131,7 +131,7 @@ final class TestValidator: XCTestCase { XCTAssertEqual(value, [2, 3, 4]) a.advance() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [2, 3, 4]) } diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index 25bc9ec7..4c2fb229 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -152,11 +152,11 @@ final class TestZip2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_zip_when_cancelled() async { @@ -370,10 +370,10 @@ final class TestZip3: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } From a42b72ad84a963a15b88c69becfb569bf35f3d2d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Fri, 23 Jun 2023 14:21:34 -0700 Subject: [PATCH 099/149] Audit pass on Sendable conformances and requirements (#272) * Audit pass on Sendable conformances and requirements * A slightly cleaner mark for Sendability of OrderedSet * Remove flags for strict mode --- Package.swift | 2 +- .../Buffer/BoundedBufferStateMachine.swift | 5 ++++- .../Buffer/UnboundedBufferStateMachine.swift | 5 ++++- Sources/AsyncAlgorithms/Channels/AsyncChannel.swift | 2 +- .../Channels/AsyncThrowingChannel.swift | 2 +- .../Channels/ChannelStateMachine.swift | 11 +++++++---- Sources/AsyncAlgorithms/Channels/ChannelStorage.swift | 2 +- 7 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index a1562d76..56417e84 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3"))], + dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], targets: [ .target( name: "AsyncAlgorithms", diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index a5c50a61..b863bc50 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -16,7 +16,7 @@ struct BoundedBufferStateMachine { typealias SuspendedProducer = UnsafeContinuation typealias SuspendedConsumer = UnsafeContinuation?, Never> - private enum State { + fileprivate enum State { case initial(base: Base) case buffering( task: Task, @@ -308,3 +308,6 @@ struct BoundedBufferStateMachine { } } } + +extension BoundedBufferStateMachine: Sendable where Base: Sendable { } +extension BoundedBufferStateMachine.State: Sendable where Base: Sendable { } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index b163619a..a43c2023 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -21,7 +21,7 @@ struct UnboundedBufferStateMachine { case bufferingOldest(Int) } - private enum State { + fileprivate enum State { case initial(base: Base) case buffering( task: Task, @@ -248,3 +248,6 @@ struct UnboundedBufferStateMachine { } } } + +extension UnboundedBufferStateMachine: Sendable where Base: Sendable { } +extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index 75becf2d..8035d06a 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -19,7 +19,7 @@ /// on the `Iterator` is made, or when `finish()` is called from another Task. /// As `finish()` induces a terminal state, there is no more need for a back pressure management. /// This function does not suspend and will finish all the pending iterations. -public final class AsyncChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncChannel: AsyncSequence, @unchecked Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index 28de36ae..2fc48dfe 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -18,7 +18,7 @@ /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. /// Those functions do not suspend and will finish all the pending iterations. -public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index 2972c754..e823e5f7 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -10,8 +10,11 @@ //===----------------------------------------------------------------------===// @_implementationOnly import OrderedCollections -struct ChannelStateMachine: Sendable { - private struct SuspendedProducer: Hashable { +// NOTE: this is only marked as unchecked since the swift-collections tag is before auditing for Sendable +extension OrderedSet: @unchecked Sendable where Element: Sendable { } + +struct ChannelStateMachine: Sendable { + private struct SuspendedProducer: Hashable, Sendable { let id: UInt64 let continuation: UnsafeContinuation? let element: Element? @@ -29,7 +32,7 @@ struct ChannelStateMachine: Sendable { } } - private struct SuspendedConsumer: Hashable { + private struct SuspendedConsumer: Hashable, Sendable { let id: UInt64 let continuation: UnsafeContinuation? @@ -51,7 +54,7 @@ struct ChannelStateMachine: Sendable { case failed(Error) } - private enum State { + private enum State: Sendable { case channeling( suspendedProducers: OrderedSet, cancelledProducers: Set, diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index da398dbc..12b5ba72 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -8,7 +8,7 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -struct ChannelStorage: Sendable { +struct ChannelStorage: Sendable { private let stateMachine: ManagedCriticalState> private let ids = ManagedCriticalState(0) From 2ace7010e9c1a75f036a266131e3642192a62c35 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 23 Jun 2023 22:21:48 +0100 Subject: [PATCH 100/149] Add docker files for CI (#270) --- .swiftformat | 25 ++++++++++++++++++ docker/Dockerfile | 24 +++++++++++++++++ docker/docker-compose.2004.57.yaml | 22 ++++++++++++++++ docker/docker-compose.2004.58.yaml | 21 +++++++++++++++ docker/docker-compose.2204.main.yaml | 21 +++++++++++++++ docker/docker-compose.yaml | 39 ++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 .swiftformat create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.2004.57.yaml create mode 100644 docker/docker-compose.2004.58.yaml create mode 100644 docker/docker-compose.2204.main.yaml create mode 100644 docker/docker-compose.yaml diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..3eb557ea --- /dev/null +++ b/.swiftformat @@ -0,0 +1,25 @@ +# file options + +--swiftversion 5.7 +--exclude .build + +# format options + +--self insert +--patternlet inline +--ranges nospace +--stripunusedargs unnamed-only +--ifdef no-indent +--extensionacl on-declarations +--disable typeSugar # https://github.com/nicklockwood/SwiftFormat/issues/636 +--disable andOperator +--disable wrapMultilineStatementBraces +--disable enumNamespaces +--disable redundantExtensionACL +--disable redundantReturn +--disable preferKeyPath +--disable sortedSwitchCases +--disable hoistAwait +--disable hoistTry + +# rules diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..e592f92f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,24 @@ +ARG swift_version=5.7 +ARG ubuntu_version=focal +ARG base_image=swift:$swift_version-$ubuntu_version +FROM $base_image +# needed to do again after FROM due to docker limitation +ARG swift_version +ARG ubuntu_version + +# set as UTF-8 +RUN apt-get update && apt-get install -y locales locales-all +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +# tools +RUN mkdir -p $HOME/.tools +RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile + +# swiftformat (until part of the toolchain) + +ARG swiftformat_version=0.51.12 +RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format +RUN cd $HOME/.tools/swift-format && swift build -c release +RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/docker-compose.2004.57.yaml b/docker/docker-compose.2004.57.yaml new file mode 100644 index 00000000..19c52d83 --- /dev/null +++ b/docker/docker-compose.2004.57.yaml @@ -0,0 +1,22 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:20.04-5.7 + build: + args: + ubuntu_version: "focal" + swift_version: "5.7" + + build: + image: swift-async-algorithms:20.04-5.7 + + test: + image: swift-async-algorithms:20.04-5.7 + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:20.04-5.7 diff --git a/docker/docker-compose.2004.58.yaml b/docker/docker-compose.2004.58.yaml new file mode 100644 index 00000000..56d83dfc --- /dev/null +++ b/docker/docker-compose.2004.58.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:20.04-5.8 + build: + args: + base_image: "swiftlang/swift:nightly-5.8-focal" + + build: + image: swift-async-algorithms:20.04-5.8 + + test: + image: swift-async-algorithms:20.04-5.8 + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:20.04-5.8 diff --git a/docker/docker-compose.2204.main.yaml b/docker/docker-compose.2204.main.yaml new file mode 100644 index 00000000..f28e21d2 --- /dev/null +++ b/docker/docker-compose.2204.main.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:22.04-main + build: + args: + base_image: "swiftlang/swift:nightly-main-jammy" + + build: + image: swift-async-algorithms:22.04-main + + test: + image: swift-async-algorithms:22.04-main + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:22.04-main diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..8d1d9a33 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,39 @@ +# this file is not designed to be run directly +# instead, use the docker-compose.. files +# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.2004.56.yaml run test +version: "3" + +services: + runtime-setup: + image: swift-async-algorithms:default + build: + context: . + dockerfile: Dockerfile + + common: &common + image: swift-async-algorithms:default + depends_on: [runtime-setup] + volumes: + - ~/.ssh:/root/.ssh + - ..:/code:z + working_dir: /code + + soundness: + <<: *common + command: /bin/bash -xcl "swift -version && uname -a && ./scripts/soundness.sh" + + build: + <<: *common + environment: [] + command: /bin/bash -cl "swift build" + + test: + <<: *common + depends_on: [runtime-setup] + command: /bin/bash -xcl "swift $${SWIFT_TEST_VERB-test} $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}" + + # util + + shell: + <<: *common + entrypoint: /bin/bash From adb12bfcccaa040778c905c5a50da9d9367fd0db Mon Sep 17 00:00:00 2001 From: 0xpablo Date: Sat, 24 Jun 2023 00:54:35 +0200 Subject: [PATCH 101/149] Add WebAssembly support (#273) --- Sources/AsyncAlgorithms/Locking.swift | 2 ++ Sources/AsyncSequenceValidation/TaskDriver.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 4e8e246c..e1ed2626 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -24,6 +24,8 @@ internal struct Lock { typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK +#else + typealias Primitive = Int #endif typealias PlatformLock = UnsafeMutablePointer diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 9f45c1f6..69c8fe5c 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -50,8 +50,12 @@ final class TaskDriver { } func start() { +#if canImport(Darwin) || canImport(Glibc) pthread_create(&thread, nil, start_thread, Unmanaged.passRetained(self).toOpaque()) +#elseif canImport(WinSDK) +#error("TODO: Port TaskDriver threading to windows") +#endif } func run() { From 07a0c1ee08e90dd15b05d45a3ead10929c0b7ec5 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 11 Jul 2023 15:18:10 +0200 Subject: [PATCH 102/149] Remove copy and paste within MergeStorage (#275) --- .../AsyncAlgorithms/Merge/MergeStorage.swift | 348 +++++------------- 1 file changed, 94 insertions(+), 254 deletions(-) diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index 7a83ad8b..42712cae 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -147,262 +147,12 @@ final class MergeStorage< // sequences. We must store it to cancel it at the right times. let task = Task { await withThrowingTaskGroup(of: Void.self) { group in - // For each upstream sequence we are adding a child task that - // is consuming the upstream sequence - group.addTask { - var iterator1 = base1.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element1 = try await iterator1.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element1) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } - } - - // Copy from the above just using the base2 sequence - group.addTask { - var iterator2 = base2.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element2 = try await iterator2.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element2) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } - } + self.iterateAsyncSequence(base1, in: &group) + self.iterateAsyncSequence(base2, in: &group) // Copy from the above just using the base3 sequence if let base3 = base3 { - group.addTask { - var iterator3 = base3.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element3 = try await iterator3.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element3) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } - } + self.iterateAsyncSequence(base3, in: &group) } while !group.isEmpty { @@ -444,5 +194,95 @@ final class MergeStorage< // We need to inform our state machine that we started the Task stateMachine.taskStarted(task) } -} + private func iterateAsyncSequence( + _ base: AsyncSequence, + in taskGroup: inout ThrowingTaskGroup + ) where AsyncSequence.Element == Base1.Element { + // For each upstream sequence we are adding a child task that + // is consuming the upstream sequence + taskGroup.addTask { + var iterator = base.makeAsyncIterator() + + // This is our upstream consumption loop + loop: while true { + // We are creating a continuation before requesting the next + // element from upstream. This continuation is only resumed + // if the downstream consumer called `next` to signal his demand. + try await withUnsafeThrowingContinuation { continuation in + let action = self.lock.withLock { + self.stateMachine.childTaskSuspended(continuation) + } + + switch action { + case let .resumeContinuation(continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) + + case let .resumeContinuationWithError(continuation, error): + // This happens if another upstream already failed or if + // the task got cancelled. + continuation.resume(throwing: error) + + case .none: + break + } + } + + // We got signalled from the downstream that we have demand so let's + // request a new element from the upstream + if let element1 = try await iterator.next() { + let action = self.lock.withLock { + self.stateMachine.elementProduced(element1) + } + + switch action { + case let .resumeContinuation(continuation, element): + // We had an outstanding demand and where the first + // upstream to produce an element so we can forward it to + // the downstream + continuation.resume(returning: element) + + case .none: + break + } + + } else { + // The upstream returned `nil` which indicates that it finished + let action = self.lock.withLock { + self.stateMachine.upstreamFinished() + } + + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. + switch action { + case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + downstreamContinuation.resume(returning: nil) + + break loop + + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + + break loop + case .none: + + break loop + } + } + } + } + } +} From df46b4c235819d4d0bd421ec28ab4b6cc2243ef8 Mon Sep 17 00:00:00 2001 From: Freya Alminde <72786+freysie@users.noreply.github.com> Date: Mon, 24 Jul 2023 17:42:27 +0200 Subject: [PATCH 103/149] Fix typo in Guides/Chunked.md (#277) --- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md index 2ddacc5f..94389220 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md @@ -213,7 +213,7 @@ If both count and signal are specified, the chunking asynchronous sequence emits Like the example above, this code emits up to 1024-byte `Data` instances, but a chunk will also be emitted every second. ```swift -let packets = bytes.chunks(ofCount: 1024 or: .repeating(every: .seconds(1)), into: Data.self) +let packets = bytes.chunks(ofCount: 1024, or: .repeating(every: .seconds(1)), into: Data.self) for try await packet in packets { write(packet) } From f5d5fb6483e7ea664bd402b9c379179934622f58 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 27 Jul 2023 22:16:37 +0100 Subject: [PATCH 104/149] Mark all iterators as non-`Sendable` (#280) --- .../AsyncAdjacentPairsSequence.swift | 46 +++++++++--------- .../AsyncAlgorithms.docc/Guides/Timer.md | 2 +- .../AsyncAlgorithms/AsyncChain3Sequence.swift | 3 ++ .../AsyncChunkedByGroupSequence.swift | 4 +- .../AsyncChunkedOnProjectionSequence.swift | 4 +- .../AsyncChunksOfCountOrSignalSequence.swift | 3 ++ .../AsyncChunksOfCountSequence.swift | 3 ++ .../AsyncCompactedSequence.swift | 37 ++++++++------- .../AsyncExclusiveReductionsSequence.swift | 3 ++ .../AsyncInclusiveReductionsSequence.swift | 3 ++ .../AsyncJoinedBySeparatorSequence.swift | 3 ++ .../AsyncAlgorithms/AsyncJoinedSequence.swift | 3 ++ .../AsyncRemoveDuplicatesSequence.swift | 6 +++ .../AsyncAlgorithms/AsyncSyncSequence.swift | 3 ++ .../AsyncThrottleSequence.swift | 3 ++ ...cThrowingExclusiveReductionsSequence.swift | 3 ++ ...cThrowingInclusiveReductionsSequence.swift | 14 ++++++ .../AsyncAlgorithms/AsyncTimerSequence.swift | 3 ++ .../Channels/AsyncChannel.swift | 3 ++ .../Channels/AsyncThrowingChannel.swift | 3 ++ .../AsyncCombineLatest2Sequence.swift | 3 ++ .../AsyncCombineLatest3Sequence.swift | 3 ++ .../Debounce/AsyncDebounceSequence.swift | 9 ++-- .../Merge/AsyncMerge2Sequence.swift | 9 ++-- .../Merge/AsyncMerge3Sequence.swift | 9 ++-- .../AsyncAlgorithms/PartialIteration.swift | 47 ------------------- .../Zip/AsyncZip2Sequence.swift | 3 ++ .../Zip/AsyncZip3Sequence.swift | 3 ++ 28 files changed, 138 insertions(+), 100 deletions(-) delete mode 100644 Sources/AsyncAlgorithms/PartialIteration.swift diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 2a37f918..b1a0a156 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -9,6 +9,29 @@ // //===----------------------------------------------------------------------===// +extension AsyncSequence { + /// An `AsyncSequence` that iterates over the adjacent pairs of the original + /// original `AsyncSequence`. + /// + /// ``` + /// for await (first, second) in (1...5).async.adjacentPairs() { + /// print("First: \(first), Second: \(second)") + /// } + /// + /// // First: 1, Second: 2 + /// // First: 2, Second: 3 + /// // First: 3, Second: 4 + /// // First: 4, Second: 5 + /// ``` + /// + /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements + /// or the original `AsyncSequence`. + @inlinable + public func adjacentPairs() -> AsyncAdjacentPairsSequence { + AsyncAdjacentPairsSequence(self) + } +} + /// An `AsyncSequence` that iterates over the adjacent pairs of the original /// `AsyncSequence`. @frozen @@ -60,29 +83,6 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { } } -extension AsyncSequence { - /// An `AsyncSequence` that iterates over the adjacent pairs of the original - /// original `AsyncSequence`. - /// - /// ``` - /// for await (first, second) in (1...5).async.adjacentPairs() { - /// print("First: \(first), Second: \(second)") - /// } - /// - /// // First: 1, Second: 2 - /// // First: 2, Second: 3 - /// // First: 3, Second: 4 - /// // First: 4, Second: 5 - /// ``` - /// - /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements - /// or the original `AsyncSequence`. - @inlinable - public func adjacentPairs() -> AsyncAdjacentPairsSequence { - AsyncAdjacentPairsSequence(self) - } -} - extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable { } @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md index 8228a4e2..1419d68b 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md @@ -42,7 +42,7 @@ extension AsyncTimerSequence: Sendable { } extension AsyncTimerSequence.Iterator: Sendable { } ``` -Since all the types comprising `AsyncTimerSequence` and it's `Iterator` are `Sendable` these types are also `Sendable`. +Since all the types comprising `AsyncTimerSequence` are `Sendable` these types are also `Sendable`. ## Credits/Inspiration diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index 1c275a2c..e88e3584 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -95,3 +95,6 @@ extension AsyncChain3Sequence: AsyncSequence { } extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } + +@available(*, unavailable) +extension AsyncChain3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index 26faafe5..0ce5d199 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -117,4 +117,6 @@ public struct AsyncChunkedByGroupSequence() -> AsyncCompactedSequence + where Element == Unwrapped? { + AsyncCompactedSequence(self) + } +} + /// An `AsyncSequence` that iterates over every non-nil element from the original /// `AsyncSequence`. @frozen @@ -50,22 +66,7 @@ public struct AsyncCompactedSequence: AsyncSequenc } } -extension AsyncSequence { - /// Returns a new `AsyncSequence` that iterates over every non-nil element from the - /// original `AsyncSequence`. - /// - /// Produces the same result as `c.compactMap { $0 }`. - /// - /// - Returns: An `AsyncSequence` where the element is the unwrapped original - /// element and iterates over every non-nil element from the original - /// `AsyncSequence`. - /// - /// Complexity: O(1) - @inlinable - public func compacted() -> AsyncCompactedSequence - where Element == Unwrapped? { - AsyncCompactedSequence(self) - } -} - extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncCompactedSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index 0d56c593..cef05359 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -112,3 +112,6 @@ extension AsyncExclusiveReductionsSequence: AsyncSequence { } extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncExclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index 0dab7cb0..ca907b80 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -85,3 +85,6 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { } extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncInclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index e401eea2..515d8a8e 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -142,3 +142,6 @@ public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base extension AsyncJoinedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable { } + +@available(*, unavailable) +extension AsyncJoinedSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index fd1d2192..0f45e21d 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -143,3 +143,9 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncRemoveDuplicatesSequence.Iterator: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingRemoveDuplicatesSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift index 6710df7c..70a6637b 100644 --- a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift @@ -67,3 +67,6 @@ public struct AsyncSyncSequence: AsyncSequence { } extension AsyncSyncSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncSyncSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index 4832b9ac..ae2b1db4 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -102,3 +102,6 @@ extension AsyncThrottleSequence: AsyncSequence { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncThrottleSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index 4b12c2c3..1cb49d8b 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -119,3 +119,6 @@ extension AsyncThrowingExclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingExclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 2a03304f..7779a842 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -1,3 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. @@ -89,3 +100,6 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index fe3b58f6..f3a06fc0 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -89,3 +89,6 @@ extension AsyncTimerSequence where C == SuspendingClock { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence: Sendable { } + +@available(*, unavailable) +extension AsyncTimerSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index 8035d06a..f59b6b5f 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -58,3 +58,6 @@ public final class AsyncChannel: AsyncSequence, @unchecked Se } } } + +@available(*, unavailable) +extension AsyncChannel.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index 2fc48dfe..eaa55fcc 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -61,3 +61,6 @@ public final class AsyncThrowingChannel: Asyn } } } + +@available(*, unavailable) +extension AsyncThrowingChannel.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index f8fd86cc..fa68acf7 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -87,3 +87,6 @@ public struct AsyncCombineLatest2Sequence< } } } + +@available(*, unavailable) +extension AsyncCombineLatest2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift index a1c7e51a..4353c0b0 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -97,3 +97,6 @@ public struct AsyncCombineLatest3Sequence< } } } + +@available(*, unavailable) +extension AsyncCombineLatest3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index e2f8b7a9..888d4f42 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -53,20 +53,20 @@ public struct AsyncDebounceSequence: Sendable whe extension AsyncDebounceSequence: AsyncSequence { public typealias Element = Base.Element - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = DebounceStorage( base: self.base, interval: self.interval, tolerance: self.tolerance, clock: self.clock ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncDebounceSequence { - public struct AsyncIterator: AsyncIteratorProtocol { + public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -97,3 +97,6 @@ extension AsyncDebounceSequence { } } } + +@available(*, unavailable) +extension AsyncDebounceSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index c1a45ba3..2de482c8 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -50,18 +50,18 @@ public struct AsyncMerge2Sequence< } extension AsyncMerge2Sequence: AsyncSequence { - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, base2: base2, base3: nil ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } extension AsyncMerge2Sequence { - public struct AsyncIterator: AsyncIteratorProtocol { + public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -92,3 +92,6 @@ extension AsyncMerge2Sequence { } } } + +@available(*, unavailable) +extension AsyncMerge2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index 6f5abf13..8cafcd95 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -61,18 +61,18 @@ public struct AsyncMerge3Sequence< } extension AsyncMerge3Sequence: AsyncSequence { - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, base2: base2, base3: base3 ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } public extension AsyncMerge3Sequence { - struct AsyncIterator: AsyncIteratorProtocol { + struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -103,3 +103,6 @@ public extension AsyncMerge3Sequence { } } } + +@available(*, unavailable) +extension AsyncMerge3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/PartialIteration.swift b/Sources/AsyncAlgorithms/PartialIteration.swift deleted file mode 100644 index 1404ddc2..00000000 --- a/Sources/AsyncAlgorithms/PartialIteration.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Async Algorithms open source project -// -// Copyright (c) 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -enum PartialIteration: CustomStringConvertible { - case idle(Iterator) - case pending(Task) - case terminal - - var description: String { - switch self { - case .idle: return "idle" - case .pending: return "pending" - case .terminal: return "terminal" - } - } - - mutating func resolve(_ result: Result, _ iterator: Iterator) rethrows -> Iterator.Element? { - do { - guard let value = try result._rethrowGet() else { - self = .terminal - return nil - } - self = .idle(iterator) - return value - } catch { - self = .terminal - throw error - } - } - - mutating func cancel() { - if case .pending(let task) = self { - task.cancel() - } - self = .terminal - } -} - -extension PartialIteration: Sendable where Iterator: Sendable, Iterator.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index 6fb341ac..34e42913 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -69,3 +69,6 @@ public struct AsyncZip2Sequence: Asy } } } + +@available(*, unavailable) +extension AsyncZip2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 87474bae..513dc27a 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -74,3 +74,6 @@ public struct AsyncZip3Sequence Date: Tue, 1 Aug 2023 16:09:34 +0100 Subject: [PATCH 105/149] Fix more `Sendable` warnings and flaky test (#281) # Motivation We still had some `Sendable` warnings left under strict Concurrency checking. # Modification This PR fixes a bunch of `Sendable` warnings but we still have some left in the validation tests. Additionally, I fixed a flaky test. --- Package.swift | 25 +++++++-- Package@swift-5.7.swift | 51 +++++++++++++++++++ .../AsyncAlgorithms/Merge/MergeStorage.swift | 2 +- .../Interspersed/TestInterspersed.swift | 7 +-- .../Performance/ThroughputMeasurement.swift | 4 +- Tests/AsyncAlgorithmsTests/TestChunk.swift | 2 + 6 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 Package@swift-5.7.swift diff --git a/Package.swift b/Package.swift index 56417e84..36c4e078 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 5.8 import PackageDescription @@ -20,18 +20,33 @@ let package = Package( targets: [ .target( name: "AsyncAlgorithms", - dependencies: [.product(name: "Collections", package: "swift-collections")] + dependencies: [.product(name: "Collections", package: "swift-collections")], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] ), .target( name: "AsyncSequenceValidation", - dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .systemLibrary(name: "_CAsyncSequenceValidationSupport"), .target( name: "AsyncAlgorithms_XCTest", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), ] ) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift new file mode 100644 index 00000000..56417e84 --- /dev/null +++ b/Package@swift-5.7.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 5.6 + +import PackageDescription + +let package = Package( + name: "swift-async-algorithms", + platforms: [ + .macOS("10.15"), + .iOS("13.0"), + .tvOS("13.0"), + .watchOS("6.0") + ], + products: [ + .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), + .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), + .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), + .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), + ], + dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], + targets: [ + .target( + name: "AsyncAlgorithms", + dependencies: [.product(name: "Collections", package: "swift-collections")] + ), + .target( + name: "AsyncSequenceValidation", + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + .systemLibrary(name: "_CAsyncSequenceValidationSupport"), + .target( + name: "AsyncAlgorithms_XCTest", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + .testTarget( + name: "AsyncAlgorithmsTests", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + ] +) + +#if canImport(Darwin) +import Darwin +let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil +#elseif canImport(Glibc) +import Glibc +let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil +#else +let buildingDocs = false +#endif + +// Only require the docc plugin when building documentation +package.dependencies += buildingDocs ? [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), +] : [] diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index 42712cae..9dedee76 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -198,7 +198,7 @@ final class MergeStorage< private func iterateAsyncSequence( _ base: AsyncSequence, in taskGroup: inout ThrowingTaskGroup - ) where AsyncSequence.Element == Base1.Element { + ) where AsyncSequence.Element == Base1.Element, AsyncSequence: Sendable { // For each upstream sequence we are adding a child task that // is consuming the upstream sequence taskGroup.addTask { diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 6e09e84d..e51a4817 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -166,10 +166,6 @@ final class TestInterspersed: XCTestCase { while let _ = await iterator.next() {} - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - - // Information the parent task that we finished consuming await lockStepChannel.send(()) } @@ -179,8 +175,7 @@ final class TestInterspersed: XCTestCase { // Now we cancel the child group.cancelAll() - // Waiting until the child task finished consuming - _ = await lockStepChannel.first { _ in true } + await group.waitForAll() } } } diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index c8991bdd..db223f3e 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -56,7 +56,7 @@ final class _ThroughputMetric: NSObject, XCTMetric, @unchecked Sendable { } extension XCTestCase { - public func measureChannelThroughput(output: @escaping @autoclosure () -> Output) async { + public func measureChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 @@ -85,7 +85,7 @@ extension XCTestCase { } } - public func measureThrowingChannelThroughput(output: @escaping @autoclosure () -> Output) async { + public func measureThrowingChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 4970cdc0..eee61247 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -13,10 +13,12 @@ import XCTest import AsyncSequenceValidation import AsyncAlgorithms +@Sendable func sumCharacters(_ array: [String]) -> String { return "\(array.reduce(into: 0) { $0 = $0 + Int($1)! })" } +@Sendable func concatCharacters(_ array: [String]) -> String { return array.joined() } From e639e5c8896de0c1ded10d70e2571f165596b556 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 15 Aug 2023 15:40:57 -0700 Subject: [PATCH 106/149] Ensure tests work in deployments that host as swift 5.7 (#285) --- .../Performance/TestThroughput.swift | 5 ++- .../Support/Asserts.swift | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index d8003ca8..4ea06e06 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -90,11 +90,12 @@ final class TestThroughput: XCTestCase { zip($0, $1, $2) } } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func test_debounce() async { + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { await measureSequenceThroughput(source: (1...).async) { - $0.debounce(for: .zero, clock: ContinuousClock()) + $0.debounce(for: .zero, clock: ContinuousClock()) } + } } } #endif diff --git a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift index c9cdb968..d891cf91 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift @@ -165,3 +165,47 @@ internal func XCTAssertThrowsError( verify(error) } } + +class WaiterDelegate: NSObject, XCTWaiterDelegate { + let state: ManagedCriticalState?> = ManagedCriticalState(nil) + + init(_ continuation: UnsafeContinuation) { + state.withCriticalRegion { $0 = continuation } + } + + func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { + resume() + } + + func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { + resume() + } + + func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { + resume() + } + + func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { + + } + + func resume() { + let continuation = state.withCriticalRegion { continuation in + defer { continuation = nil } + return continuation + } + continuation?.resume() + } +} + +extension XCTestCase { + @_disfavoredOverload + func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async { + return await withUnsafeContinuation { continuation in + let delegate = WaiterDelegate(continuation) + let waiter = XCTWaiter(delegate: delegate) + waiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder) + delegate.resume() + } + } +} From b0ec4694d2046165d0365a64cee682bf4ca2d9ae Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 18 Aug 2023 19:19:58 +0100 Subject: [PATCH 107/149] Remove the validation packages from public products (#287) # Motivation The current validation testing library has a bunch of warnings and we haven't put it through a proper API review yet. Since we are preparing for a 1.0.0, I think it is best if we remove the products for the validation targets for now and take some time to make sure the interfaces are taken through an API review. # Modification Remove products for the validation targets. --- Package.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Package.swift b/Package.swift index 36c4e078..afc3bcaa 100644 --- a/Package.swift +++ b/Package.swift @@ -12,9 +12,6 @@ let package = Package( ], products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), - .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), - .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), - .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], targets: [ From 4a1fb99f0089a9d9db07859bcad55b4a77e3c3dd Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 22 Aug 2023 16:09:28 -0700 Subject: [PATCH 108/149] Rework availability for executor to avoid warnings (#288) --- Sources/AsyncSequenceValidation/Job.swift | 2 +- Sources/AsyncSequenceValidation/Test.swift | 47 +++++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncSequenceValidation/Job.swift b/Sources/AsyncSequenceValidation/Job.swift index 44dedbc8..461af50d 100644 --- a/Sources/AsyncSequenceValidation/Job.swift +++ b/Sources/AsyncSequenceValidation/Job.swift @@ -19,6 +19,6 @@ struct Job: Hashable, @unchecked Sendable { } func execute() { - _swiftJobRun(unsafeBitCast(job, to: UnownedJob.self), AsyncSequenceValidationDiagram.Context.executor.asUnownedSerialExecutor()) + _swiftJobRun(unsafeBitCast(job, to: UnownedJob.self), AsyncSequenceValidationDiagram.Context.unownedExecutor) } } diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index 0275cc11..8dc86832 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -67,25 +67,62 @@ extension AsyncSequenceValidationDiagram { } struct Context { +#if swift(<5.9) final class ClockExecutor: SerialExecutor { - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + func enqueue(_ job: UnownedJob) { + job._runSynchronously(on: self.asUnownedSerialExecutor()) + } + + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + private static let _executor = ClockExecutor() + + static var unownedExecutor: UnownedSerialExecutor { + _executor.asUnownedSerialExecutor() + } +#else + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + final class ClockExecutor_5_9: SerialExecutor { func enqueue(_ job: __owned ExecutorJob) { job.runSynchronously(on: asUnownedSerialExecutor()) } - @available(*, deprecated) // known deprecation warning + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + final class ClockExecutor_Pre5_9: SerialExecutor { + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(*, deprecated, message: "Implement 'enqueue(_: __owned ExecutorJob)' instead") func enqueue(_ job: UnownedJob) { - job._runSynchronously(on: asUnownedSerialExecutor()) + job._runSynchronously(on: self.asUnownedSerialExecutor()) } - + func asUnownedSerialExecutor() -> UnownedSerialExecutor { UnownedSerialExecutor(ordinary: self) } } + private static let _executor: AnyObject = { + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + return ClockExecutor_5_9() + } else { + return ClockExecutor_Pre5_9() + } + }() + + static var unownedExecutor: UnownedSerialExecutor { + (_executor as! any SerialExecutor).asUnownedSerialExecutor() + } +#endif + static var clock: Clock? - static let executor = ClockExecutor() + static var driver: TaskDriver? From 281e27c5d0c19bf31feb95aa31cc2ae146684f75 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 19 Sep 2023 17:23:38 +0200 Subject: [PATCH 109/149] Fix `chunks(countOf: 1)` (#293) --- Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift | 4 ++++ Tests/AsyncAlgorithmsTests/TestChunk.swift | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift index 9e1c4959..0ebafb4b 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift @@ -49,6 +49,10 @@ public struct AsyncChunksOfCountSequence String { } final class TestChunk: XCTestCase { + func test_count_one() { + validate { + "ABCDE|" + $0.inputs[0].chunks(ofCount: 1).map(concatCharacters) + "ABCDE|" + } + } + func test_signal_equalChunks() { validate { "ABC- DEF- GHI- |" From 6dfbfd5e49a33c024ea7963dd79c634720342518 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 21 Sep 2023 11:07:08 +0200 Subject: [PATCH 110/149] Remove background correction in `AsyncTimerSequence` (#289) # Motivation Currently, the `AsyncTimerSequence` is trying to correct for when an application becomes suspended and the timer might fire multiple times once the application gets foregrounded again. However, this is already handled by the `Clock` types themselves. The `SuspendingClock` is correcting for suspension of the app whereas the `ContinuousClock` is not. Additionally, this was not only hit by background an application but by just calling `Task.sleep` in the for-await loop that is consuming the sequence. # Modification This removes the part of the code in `AsyncTimerSequence` which corrected for suspension of the application. --- .../AsyncAlgorithms/AsyncTimerSequence.swift | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index f3a06fc0..dcfc878b 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -13,59 +13,49 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct AsyncTimerSequence: AsyncSequence { public typealias Element = C.Instant - + /// The iterator for an `AsyncTimerSequence` instance. public struct Iterator: AsyncIteratorProtocol { var clock: C? let interval: C.Instant.Duration let tolerance: C.Instant.Duration? var last: C.Instant? - + init(interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { self.clock = clock self.interval = interval self.tolerance = tolerance } - - func nextDeadline(_ clock: C) -> C.Instant { - let now = clock.now - let last = self.last ?? now - let next = last.advanced(by: interval) - if next < now { - return last.advanced(by: interval * Int(((next.duration(to: now)) / interval).rounded(.up))) - } else { - return next - } - } - + public mutating func next() async -> C.Instant? { - guard let clock = clock else { + guard let clock = self.clock else { return nil } - let next = nextDeadline(clock) + + let next = (self.last ?? clock.now).advanced(by: self.interval) do { - try await clock.sleep(until: next, tolerance: tolerance) + try await clock.sleep(until: next, tolerance: self.tolerance) } catch { self.clock = nil return nil } let now = clock.now - last = next + self.last = next return now } } - + let clock: C let interval: C.Instant.Duration let tolerance: C.Instant.Duration? - + /// Create an `AsyncTimerSequence` with a given repeating interval. public init(interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) { self.clock = clock self.interval = interval self.tolerance = tolerance } - + public func makeAsyncIterator() -> Iterator { Iterator(interval: interval, tolerance: tolerance, clock: clock) } From c889832c6499eeba5b31d142861f6478bec55308 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 21 Sep 2023 02:09:43 -0700 Subject: [PATCH 111/149] Add a proposal for AsyncChannel (#216) --- Evolution/NNNN-channel.md | 86 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 Evolution/NNNN-channel.md diff --git a/Evolution/NNNN-channel.md b/Evolution/NNNN-channel.md new file mode 100644 index 00000000..09256190 --- /dev/null +++ b/Evolution/NNNN-channel.md @@ -0,0 +1,86 @@ +# Channel + +* Author(s): [Philippe Hausler](https://github.com/phausler) + +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChannel.swift), +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChannel.swift) +] + +## Introduction + +`AsyncStream` introduced a mechanism to send buffered elements from a context that doesn't use Swift concurrency into one that does. That design only addressed a portion of the potential use cases; the missing portion was the back pressure excerpted across two concurrency domains. + +## Proposed Solution + +To achieve a system that supports back pressure and allows for the communication of more than one value from one task to another we are introducing a new type, the _channel_. The channel will be a reference-type asynchronous sequence with an asynchronous sending capability that awaits the consumption of iteration. Each value sent by the channel will await the consumption of that value by iteration. That awaiting behavior will allow for the affordance of back pressure applied from the consumption site to be transmitted to the production site. This means that the rate of production cannot exceed the rate of consumption, and that the rate of consumption cannot exceed the rate of production. Sending a terminal event to the channel will instantly resume all pending operations for every producers and consumers. + +## Detailed Design + +Similar to the `AsyncStream` and `AsyncThrowingStream` types, the type for sending elements via back pressure will come in two versions. These two versions will account for the throwing nature or non-throwing nature of the elements being produced. + +Each type will have functions to send elements and to send terminal events. + +```swift +public final class AsyncChannel: AsyncSequence, Sendable { + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async -> Element? + } + + public init(element elementType: Element.Type = Element.self) + + public func send(_ element: Element) async + public func finish() + + public func makeAsyncIterator() -> Iterator +} + +public final class AsyncThrowingChannel: AsyncSequence, Sendable { + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async throws -> Element? + } + + public init(element elementType: Element.Type = Element.self, failure failureType: Failure.Type = Failure.self) + + public func send(_ element: Element) async + public func fail(_ error: Error) where Failure == Error + public func finish() + + public func makeAsyncIterator() -> Iterator +} +``` + +Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` via the suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. This method suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` or `fail(_:)` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, or by throwing an error, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. The calls to `send(:)` and `next()` will immediately resume when their supporting task is cancelled, other operations from other tasks will remain active. + +```swift +let channel = AsyncChannel() +Task { + while let resultOfLongCalculation = doLongCalculations() { + await channel.send(resultOfLongCalculation) + } + channel.finish() +} + +for await calculationResult in channel { + print(calculationResult) +} +``` + +The example above uses a task to perform intense calculations; each of which are sent to the other task via the `send(_:)` method. That call to `send(_:)` returns when the next iteration of the channel is invoked. + +## Alternatives Considered + +The use of the name "subject" was considered, due to its heritage as a name for a sync-to-async adapter type. + +It was considered to make `AsyncChannel` and `AsyncThrowingChannel` actors, however due to the cancellation internals it would imply that these types would need to create new tasks to handle cancel events. The advantages of an actor in this particular case did not outweigh the impact of adjusting the implementations to be actors. + +## Future Directions + +`AsyncChannel` and `AsyncThrowingChannel` are just the prominent members of the channel-like behavior algorithms. It is reasonable to have as its own distinct type a buffering channel that provides more [tuned back pressure per a given buffer of elements](https://forums.swift.org/t/asyncchannel-should-we-allow-to-buffer/60876). These other members of the same category of algorithm should be considered on their own as distinct proposals. + +## Credits/Inspiration + +`AsyncChannel` and `AsyncThrowingChannel` was heavily inspired from `Subject` but with the key difference that it uses Swift concurrency to apply back pressure. + +https://developer.apple.com/documentation/combine/subject/ From 220f86f7925430f0a932925b5631fc822b4c25ec Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Thu, 21 Sep 2023 08:18:20 -0700 Subject: [PATCH 112/149] Ensure the last element of reduction in the throttle is emitted and use appropriate delay (#292) --- .../AsyncThrottleSequence.swift | 11 +++++++++- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 22 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index ae2b1db4..a8eec469 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -81,7 +81,16 @@ extension AsyncThrottleSequence: AsyncSequence { let start = last ?? clock.now repeat { guard let element = try await base.next() else { - return nil + if reduced != nil, let last { + // ensure the rate of elements never exceeds the given interval + let amount = interval - last.duration(to: clock.now) + if amount > .zero { + try? await clock.sleep(for: amount) + } + } + // the last value is unable to have any subsequent + // values so always return the last reduction + return reduced } let reduction = await reducing(reduced, element) let now = clock.now diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index 72c90f65..4e0d3898 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -72,7 +72,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) - "a--d--g--j-|" + "a--d--g--j--[k|]" } } @@ -81,7 +81,7 @@ final class TestThrottle: XCTestCase { validate { "abcdefghijk|" $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) - "a--b--e--h-|" + "a--b--e--h--[k|]" } } @@ -138,4 +138,22 @@ final class TestThrottle: XCTestCase { "-a---c---e---g---i---k-|" } } + + func test_trailing_delay_without_latest() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "abcdefghijkl|" + $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) + "a--b--e--h--[k|]" + } + } + + func test_trailing_delay_with_latest() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "abcdefghijkl|" + $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: true) + "a--d--g--j--[l|]" + } + } } From f56556930fc677096331565242024158fd7235b7 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 21 Sep 2023 17:19:00 +0200 Subject: [PATCH 113/149] Remove majority of `@unchecked Sendable` usages (#295) # Motivation `@unchecked Sendable` is a great way to make a type `Sendable` while it is not really `Sendable` this is rarely useful and we should rather use conditional `@unchecked Sendable` annotations such as the one on `ManagedCriticalState` # Modification This PR removes all `@unchecked Sendable` in the main algorithm target except the one on `Merge` since we are doing manual locking there. # Result No more `@unchecked Sendable` usage. --- Sources/AsyncAlgorithms/Channels/AsyncChannel.swift | 2 +- Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift | 2 +- .../AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift | 6 +++--- Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift | 4 ++-- Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index f59b6b5f..026281de 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -19,7 +19,7 @@ /// on the `Iterator` is made, or when `finish()` is called from another Task. /// As `finish()` induces a terminal state, there is no more need for a back pressure management. /// This function does not suspend and will finish all the pending iterations. -public final class AsyncChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index eaa55fcc..63cbf50d 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -18,7 +18,7 @@ /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. /// Those functions do not suspend and will finish all the pending iterations. -public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncThrowingChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index 888d4f42..c57b2c42 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -13,14 +13,14 @@ extension AsyncSequence { /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed by using a specified Clock. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence where Self: Sendable { + public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { AsyncDebounceSequence(self, interval: interval, tolerance: tolerance, clock: clock) } /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence where Self: Sendable { + public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { self.debounce(for: interval, tolerance: tolerance, clock: .continuous) } } @@ -28,7 +28,7 @@ extension AsyncSequence { /// An `AsyncSequence` that emits the latest element after a given quiescence period /// has elapsed. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct AsyncDebounceSequence: Sendable where Base: Sendable { +public struct AsyncDebounceSequence: Sendable where Base.Element: Sendable { private let base: Base private let clock: C private let interval: C.Instant.Duration diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index b75d2f3e..5fb89451 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -10,10 +10,10 @@ //===----------------------------------------------------------------------===// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct DebounceStateMachine { +struct DebounceStateMachine: Sendable where Base.Element: Sendable { typealias Element = Base.Element - private enum State { + private enum State: Sendable { /// The initial state before a call to `next` happened. case initial(base: Base) diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 40c30634..1c223143 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class DebounceStorage: @unchecked Sendable where Base: Sendable { +final class DebounceStorage: Sendable where Base.Element: Sendable { typealias Element = Base.Element /// The state machine protected with a lock. From 260e19822105061bdf021ff01f401c104851143b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 21 Sep 2023 17:20:02 +0200 Subject: [PATCH 114/149] Add support for the Swift package index (#297) # Motivation We had some scripts around to generate docs for this package but all of our other packages are now providing documentation on the Swift package index. # Modification This removes the old script to generate documentation and adds an `.spi.yml` file. Furthermore, I removed the conditional dependency on the docc plugin which we are unconditionally depending on in most of our other packages like NIO. --- .spi.yml | 4 ++ Package.swift | 20 ++------ Package@swift-5.7.swift | 20 ++------ bin/update-gh-pages-documentation-site | 71 -------------------------- 4 files changed, 12 insertions(+), 103 deletions(-) create mode 100644 .spi.yml delete mode 100755 bin/update-gh-pages-documentation-site diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..2a779cf6 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [AsyncAlgorithms] diff --git a/Package.swift b/Package.swift index afc3bcaa..2932e199 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,10 @@ let package = Package( products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], targets: [ .target( name: "AsyncAlgorithms", @@ -46,18 +49,3 @@ let package = Package( ), ] ) - -#if canImport(Darwin) -import Darwin -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#elseif canImport(Glibc) -import Glibc -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#else -let buildingDocs = false -#endif - -// Only require the docc plugin when building documentation -package.dependencies += buildingDocs ? [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), -] : [] diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 56417e84..7c488af0 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -16,7 +16,10 @@ let package = Package( .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], targets: [ .target( name: "AsyncAlgorithms", @@ -34,18 +37,3 @@ let package = Package( dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), ] ) - -#if canImport(Darwin) -import Darwin -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#elseif canImport(Glibc) -import Glibc -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#else -let buildingDocs = false -#endif - -// Only require the docc plugin when building documentation -package.dependencies += buildingDocs ? [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), -] : [] diff --git a/bin/update-gh-pages-documentation-site b/bin/update-gh-pages-documentation-site deleted file mode 100755 index 0c1e59aa..00000000 --- a/bin/update-gh-pages-documentation-site +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -# -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2022 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See https://swift.org/LICENSE.txt for license information -# See https://swift.org/CONTRIBUTORS.txt for Swift project authors -# -# Updates the GitHub Pages documentation site thats published from the 'docs' -# subdirectory in the 'gh-pages' branch of this repository. -# -# This script should be run by someone with commit access to the 'gh-pages' branch -# at a regular frequency so that the documentation content on the GitHub Pages site -# is up-to-date with the content in this repo. -# - -export BUILDING_FOR_DOCUMENTATION_GENERATION=1 - -set -eu - -# A `realpath` alternative using the default C implementation. -filepath() { - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -SWIFT_ASYNC_ALGORITHMS_ROOT="$(dirname $(dirname $(filepath $0)))" - -ASYNC_ALGORITHMS_BUILD_DIR="$SWIFT_ASYNC_ALGORITHMS_ROOT"/.build/async-algorithms-gh-pages-build - -# Set current directory to the repository root -cd "$SWIFT_ASYNC_ALGORITHMS_ROOT" - -# Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory -git fetch -git worktree add --checkout gh-pages origin/gh-pages - -# Pretty print DocC JSON output so that it can be consistently diffed between commits -export DOCC_JSON_PRETTYPRINT="YES" - -# Generate documentation for the 'AsyncAlgorithms' target and output it -# to the /docs subdirectory in the gh-pages worktree directory. -swift package \ - --allow-writing-to-directory "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" \ - generate-documentation \ - --target AsyncAlgorithms \ - --disable-indexing \ - --transform-for-static-hosting \ - --hosting-base-path swift-async-algorithms \ - --output-path "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" - -# Save the current commit we've just built documentation from in a variable -CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` - -# Commit and push our changes to the gh-pages branch -cd gh-pages -git add docs - -if [ -n "$(git status --porcelain)" ]; then - echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin." - git commit -m "Update GitHub Pages documentation site to $CURRENT_COMMIT_HASH" - git push origin HEAD:gh-pages -else - # No changes found, nothing to commit. - echo "No documentation changes found." -fi - -# Delete the git worktree we created -cd .. -git worktree remove gh-pages From 0a3866daecd74737869f70fe43c4ac543fa89089 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 23 Sep 2023 18:16:32 +0200 Subject: [PATCH 115/149] Require 5.8 for SPI --- .spi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.spi.yml b/.spi.yml index 2a779cf6..2c794bc9 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,3 +2,4 @@ version: 1 builder: configs: - documentation_targets: [AsyncAlgorithms] + swift_version: 5.8 From 6360ca0344f058fb048c5597447f3d44f0329296 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 27 Sep 2023 16:19:00 +0100 Subject: [PATCH 116/149] Remove `@_implementationOnly` usage (#294) # Motivation We added `@_implementationOnly` a little while ago to hide the dependency on the `DequeModule`. However, with recent compilers this produces a warning since `@_implementationOnly` is only intended to be used in resilient libraries. # Modification This PR removes the usage of `@_implementationOnly` --- .../AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift | 2 +- .../Buffer/UnboundedBufferStateMachine.swift | 2 +- Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift | 2 +- .../CombineLatest/CombineLatestStateMachine.swift | 2 +- Sources/AsyncAlgorithms/Locking.swift | 6 +++--- Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift | 2 +- Sources/AsyncSequenceValidation/TaskDriver.swift | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index b863bc50..9ca7a993 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule struct BoundedBufferStateMachine { typealias Element = Base.Element diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index a43c2023..de5d37ae 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule struct UnboundedBufferStateMachine { typealias Element = Base.Element diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index e823e5f7..2c8b1b92 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -8,7 +8,7 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -@_implementationOnly import OrderedCollections +import OrderedCollections // NOTE: this is only marked as unchecked since the swift-collections tag is before auditing for Sendable extension OrderedSet: @unchecked Sendable where Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 71d0507a..5217e8de 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// State machine for combine latest struct CombineLatestStateMachine< diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index e1ed2626..952b13c8 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -10,11 +10,11 @@ //===----------------------------------------------------------------------===// #if canImport(Darwin) -@_implementationOnly import Darwin +import Darwin #elseif canImport(Glibc) -@_implementationOnly import Glibc +import Glibc #elseif canImport(WinSDK) -@_implementationOnly import WinSDK +import WinSDK #endif internal struct Lock { diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 2de482c8..9f82ed98 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index 8cafcd95..d5576694 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge< diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index e3bb59e4..bb832ada 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// The state machine for any of the `merge` operator. /// diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 69c8fe5c..ed128193 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -12,9 +12,9 @@ import _CAsyncSequenceValidationSupport #if canImport(Darwin) -@_implementationOnly import Darwin +import Darwin #elseif canImport(Glibc) -@_implementationOnly import Glibc +import Glibc #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") #endif From 8cfdf03b518ad8651044eb734615d557ac23359d Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 27 Sep 2023 16:26:01 +0100 Subject: [PATCH 117/149] Fix 5.7/5.8 build errors (#298) # Motivation Currently, this repo fails to build on Swift 5.7 and 5.8 since we were using clock APIs that were only available on 5.9. --- Sources/AsyncAlgorithms/AsyncThrottleSequence.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index a8eec469..f515fe6a 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -85,7 +85,7 @@ extension AsyncThrottleSequence: AsyncSequence { // ensure the rate of elements never exceeds the given interval let amount = interval - last.duration(to: clock.now) if amount > .zero { - try? await clock.sleep(for: amount) + try? await clock.sleep(until: clock.now.advanced(by: amount), tolerance: nil) } } // the last value is unable to have any subsequent From cb417003f962f9de3fc7852c1b735a1f1152a89a Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 27 Sep 2023 16:35:58 +0100 Subject: [PATCH 118/149] Make throttle underscored (#296) # Motivation During the discussion in https://github.com/apple/swift-async-algorithms/issues/248 it became clear that the semantics of `throttle` are not 100% figured out yet. # Modification This PR is making `throttle` an underscored API for the upcoming 1.0.0 release. This gives us more time to investigate what exact semantics we want to have. --- .../AsyncThrottleSequence.swift | 24 +++++++------- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 32 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index f515fe6a..4dbc1e48 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -12,20 +12,20 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> AsyncThrottleSequence { - AsyncThrottleSequence(self, interval: interval, clock: clock, reducing: reducing) + public func _throttle(for interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> _AsyncThrottleSequence { + _AsyncThrottleSequence(self, interval: interval, clock: clock, reducing: reducing) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: Duration, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> AsyncThrottleSequence { - throttle(for: interval, clock: .continuous, reducing: reducing) + public func _throttle(for interval: Duration, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: .continuous, reducing: reducing) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: C.Instant.Duration, clock: C, latest: Bool = true) -> AsyncThrottleSequence { - throttle(for: interval, clock: clock) { previous, element in + public func _throttle(for interval: C.Instant.Duration, clock: C, latest: Bool = true) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: clock) { previous, element in if latest { return element } else { @@ -36,14 +36,14 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: Duration, latest: Bool = true) -> AsyncThrottleSequence { - throttle(for: interval, clock: .continuous, latest: latest) + public func _throttle(for interval: Duration, latest: Bool = true) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: .continuous, latest: latest) } } /// A rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct AsyncThrottleSequence { +public struct _AsyncThrottleSequence { let base: Base let interval: C.Instant.Duration let clock: C @@ -58,7 +58,7 @@ public struct AsyncThrottleSequence { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence: AsyncSequence { +extension _AsyncThrottleSequence: AsyncSequence { public typealias Element = Reduced /// The iterator for an `AsyncThrottleSequence` instance. @@ -110,7 +110,7 @@ extension AsyncThrottleSequence: AsyncSequence { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } +extension _AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } @available(*, unavailable) -extension AsyncThrottleSequence.Iterator: Sendable { } +extension _AsyncThrottleSequence.Iterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index 4e0d3898..d7b60db3 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -17,7 +17,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(0), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(0), clock: $0.clock) "abcdefghijk|" } } @@ -26,7 +26,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(0), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(0), clock: $0.clock, latest: false) "abcdefghijk|" } } @@ -35,7 +35,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock) "abcdefghijk|" } } @@ -44,7 +44,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock, latest: false) "abcdefghijk|" } } @@ -53,7 +53,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "a-c-e-g-i-k|" } } @@ -62,7 +62,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock, latest: false) "a-b-d-f-h-j|" } } @@ -71,7 +71,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock) "a--d--g--j--[k|]" } } @@ -80,7 +80,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: false) "a--b--e--h--[k|]" } } @@ -89,7 +89,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdef^hijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "a-c-e-^" } } @@ -98,7 +98,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdef^hijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock, latest: false) "a-b-d-^" } } @@ -107,7 +107,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock) "-a-b-c-d-e-f-g-h-i-j-k-|" } } @@ -116,7 +116,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "-a-b-c-d-e-f-g-h-i-j-k-|" } } @@ -125,7 +125,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "--a--b--c--d--e--f--g|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "--a--b--c--d--e--f--g|" } } @@ -134,7 +134,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock) "-a---c---e---g---i---k-|" } } @@ -143,7 +143,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijkl|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: false) "a--b--e--h--[k|]" } } @@ -152,7 +152,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijkl|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: true) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: true) "a--d--g--j--[l|]" } } From 5bbdcc1d37990cca524852c5a3924c02fa2da983 Mon Sep 17 00:00:00 2001 From: Artem Gavrilik <73897254+GavrilikArt@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:42:24 +0200 Subject: [PATCH 119/149] Fix typo in BoundedBufferStateMachine (#301) --- .../Buffer/BoundedBufferStateMachine.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index 9ca7a993..5c99d3d7 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -64,7 +64,7 @@ struct BoundedBufferStateMachine { mutating func shouldSuspendProducer() -> Bool { switch state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(_, let buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -95,7 +95,7 @@ struct BoundedBufferStateMachine { mutating func producerSuspended(continuation: SuspendedProducer) -> ProducerSuspendedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, let buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -132,7 +132,7 @@ struct BoundedBufferStateMachine { mutating func elementProduced(element: Element) -> ElementProducedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, var buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -170,7 +170,7 @@ struct BoundedBufferStateMachine { mutating func finish(error: Error?) -> FinishAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(_, var buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -245,7 +245,7 @@ struct BoundedBufferStateMachine { mutating func nextSuspended(continuation: SuspendedConsumer) -> NextSuspendedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, let buffer, .none, .none) where buffer.isEmpty: // we are idle, we confirm the suspension of the consumer From da4e36f86544cdf733a40d59b3a2267e3a7bbf36 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 17 Nov 2023 16:20:14 +0000 Subject: [PATCH 120/149] Fix potential deadlocks when resuming a continuation while holding a lock (#303) * Fix potential deadlocks in zip * Fix debounce * Fix combineLatest * Fix Channel * Fix buffer --- .../Buffer/BoundedBufferStorage.swift | 129 ++++---- .../Buffer/UnboundedBufferStorage.swift | 101 +++--- .../Channels/ChannelStorage.swift | 145 +++++---- .../CombineLatest/CombineLatestStorage.swift | 223 +++++++------ .../Debounce/DebounceStorage.swift | 138 +++++---- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 293 +++++++++--------- 6 files changed, 553 insertions(+), 476 deletions(-) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index c00360e1..f83a37fa 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -18,34 +18,47 @@ final class BoundedBufferStorage: Sendable where Base: Send func next() async -> Result? { return await withTaskCancellationHandler { - let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action: BoundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next() switch action { case .startTask(let base): self.startTask(stateMachine: &stateMachine, base: base) - return (true, nil) + return nil + case .suspend: - return (true, nil) - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - return (false, result) + return action + case .returnResult: + return action } } - if !shouldSuspend { - return result + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .suspend: + break + + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + return result + + case .none: + break } return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended(continuation: continuation) - switch action { - case .none: - break - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended(continuation: continuation) + } + switch action { + case .none: + break + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + continuation.resume(returning: result) } } } onCancel: { @@ -68,15 +81,15 @@ final class BoundedBufferStorage: Sendable where Base: Send if shouldSuspend { await withUnsafeContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.producerSuspended(continuation: continuation) - - switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.producerSuspended(continuation: continuation) + } + + switch action { + case .none: + break + case .resumeProducer: + continuation.resume() } } } @@ -86,35 +99,35 @@ final class BoundedBufferStorage: Sendable where Base: Send break loop } - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced(element: element) - switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced(element: element) } - } - - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: nil) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } + + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: nil) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } catch { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) - switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -123,16 +136,16 @@ final class BoundedBufferStorage: Sendable where Base: Send } func interrupted() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.interrupted() - switch action { - case .none: - break - case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): - task.cancel() - producerContinuation?.resume() - consumerContinuation?.resume(returning: nil) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.interrupted() + } + switch action { + case .none: + break + case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): + task.cancel() + producerContinuation?.resume() + consumerContinuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift index 59b02810..b63b261f 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -19,32 +19,41 @@ final class UnboundedBufferStorage: Sendable where Base: Se func next() async -> Result? { return await withTaskCancellationHandler { - let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action: UnboundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next() switch action { case .startTask(let base): self.startTask(stateMachine: &stateMachine, base: base) - return (true, nil) + return nil case .suspend: - return (true, nil) - case .returnResult(let result): - return (false, result) + return action + case .returnResult: + return action } } - if !shouldSuspend { - return result + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + case .suspend: + break + case .returnResult(let result): + return result + case .none: + break } return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended(continuation: continuation) - switch action { - case .none: - break - case .resumeConsumer(let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended(continuation: continuation) + } + switch action { + case .none: + break + case .resumeConsumer(let result): + continuation.resume(returning: result) } } } onCancel: { @@ -59,35 +68,35 @@ final class UnboundedBufferStorage: Sendable where Base: Se let task = Task { do { for try await element in base { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced(element: element) - switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced(element: element) } - } - - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: nil) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } + + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: nil) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } catch { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) - switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -96,15 +105,15 @@ final class UnboundedBufferStorage: Sendable where Base: Se } func interrupted() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.interrupted() - switch action { - case .none: - break - case .resumeConsumer(let task, let continuation): - task.cancel() - continuation?.resume(returning: nil) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.interrupted() + } + switch action { + case .none: + break + case .resumeConsumer(let task, let continuation): + task.cancel() + continuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index 12b5ba72..0fb67818 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -25,21 +25,17 @@ struct ChannelStorage: Sendable { func send(element: Element) async { // check if a suspension is needed - let shouldExit = self.stateMachine.withCriticalRegion { stateMachine -> Bool in - let action = stateMachine.send() - - switch action { - case .suspend: - // the element has not been delivered because no consumer available, we must suspend - return false - case .resumeConsumer(let continuation): - continuation?.resume(returning: element) - return true - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.send() } - if shouldExit { - return + switch action { + case .suspend: + break + + case .resumeConsumer(let continuation): + continuation?.resume(returning: element) + return } let producerID = self.generateId() @@ -47,103 +43,100 @@ struct ChannelStorage: Sendable { await withTaskCancellationHandler { // a suspension is needed await withUnsafeContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) - - switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - case .resumeProducerAndConsumer(let consumerContinuation): - continuation.resume() - consumerContinuation?.resume(returning: element) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) } - } - } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.sendCancelled(producerID: producerID) switch action { case .none: break - case .resumeProducer(let continuation): - continuation?.resume() + case .resumeProducer: + continuation.resume() + case .resumeProducerAndConsumer(let consumerContinuation): + continuation.resume() + consumerContinuation?.resume(returning: element) } } - } - } - - func finish(error: Failure? = nil) { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) + } onCancel: { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.sendCancelled(producerID: producerID) + } switch action { case .none: break - case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): - producerContinuations.forEach { $0?.resume() } - if let error { - consumerContinuations.forEach { $0?.resume(throwing: error) } - } else { - consumerContinuations.forEach { $0?.resume(returning: nil) } - } + case .resumeProducer(let continuation): + continuation?.resume() } } } - func next() async throws -> Element? { - let (shouldExit, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in - let action = stateMachine.next() + func finish(error: Failure? = nil) { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } - switch action { - case .suspend: - return (false, nil) - case .resumeProducer(let producerContinuation, let result): - producerContinuation?.resume() - return (true, result) - } + switch action { + case .none: + break + case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): + producerContinuations.forEach { $0?.resume() } + if let error { + consumerContinuations.forEach { $0?.resume(throwing: error) } + } else { + consumerContinuations.forEach { $0?.resume(returning: nil) } + } } + } - if shouldExit { - return try result?._rethrowGet() + func next() async throws -> Element? { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.next() + } + + switch action { + case .suspend: + break + + case .resumeProducer(let producerContinuation, let result): + producerContinuation?.resume() + return try result._rethrowGet() } let consumerID = self.generateId() return try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended( + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended( continuation: continuation, consumerID: consumerID ) - - switch action { - case .none: - break - case .resumeConsumer(let element): - continuation.resume(returning: element) - case .resumeConsumerWithError(let error): - continuation.resume(throwing: error) - case .resumeProducerAndConsumer(let producerContinuation, let element): - producerContinuation?.resume() - continuation.resume(returning: element) - } } - } - } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextCancelled(consumerID: consumerID) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let element): + continuation.resume(returning: element) + case .resumeConsumerWithError(let error): + continuation.resume(throwing: error) + case .resumeProducerAndConsumer(let producerContinuation, let element): + producerContinuation?.resume() + continuation.resume(returning: element) } } + } onCancel: { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextCancelled(consumerID: consumerID) + } + + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } } } diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index d3b67404..0d97adea 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -48,7 +48,7 @@ final class CombineLatestStorage< func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? { try await withTaskCancellationHandler { let result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in + let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next(for: continuation) switch action { case .startTask(let base1, let base2, let base3): @@ -60,45 +60,65 @@ final class CombineLatestStorage< base3: base3, downstreamContinuation: continuation ) + return nil - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + case .resumeContinuation: + return action - case .resumeUpstreamContinuations(let upstreamContinuations): - // bases can be iterated over for 1 iteration so their next value can be retrieved - upstreamContinuations.forEach { $0.resume() } + case .resumeUpstreamContinuations: + return action - case .resumeDownstreamContinuationWithNil(let continuation): - // the async sequence is already finished, immediately resuming - continuation.resume(returning: .success(nil)) + case .resumeDownstreamContinuationWithNil: + return action } } + + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .resumeUpstreamContinuations(let upstreamContinuations): + // bases can be iterated over for 1 iteration so their next value can be retrieved + upstreamContinuations.forEach { $0.resume() } + + case .resumeDownstreamContinuationWithNil(let continuation): + // the async sequence is already finished, immediately resuming + continuation.resume(returning: .success(nil)) + + case .none: + break + } } return try result._rethrowGet() } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.cancelled() + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.cancelled() + } - switch action { - case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() - downstreamContinuation.resume(returning: .success(nil)) + downstreamContinuation.resume(returning: .success(nil)) - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() - case .none: - break - } + case .none: + break } } } @@ -124,33 +144,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element1 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((element1, nil, nil)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((element1, nil, nil)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -191,33 +211,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element2 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((nil, element2, nil)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((nil, element2, nil)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -259,33 +279,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element3 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((nil, nil, element3)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((nil, nil, element3)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -323,28 +343,29 @@ final class CombineLatestStorage< do { try await group.next() } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - switch action { - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let error, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - downstreamContinuation.resume(returning: .failure(error)) - case .none: - break - } - } + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) + } + + switch action { + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let error, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + downstreamContinuation.resume(returning: .failure(error)) + case .none: + break + } - group.cancelAll() + group.cancelAll() } } } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 1c223143..1839e334 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -55,36 +55,59 @@ final class DebounceStorage: Sendable // We always suspend since we can never return an element right away let result: Result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { - let action = $0.next(for: continuation) - - switch action { - case .startTask(let base): - self.startTask( - stateMachine: &$0, - base: base, - downstreamContinuation: continuation - ) - - case .resumeUpstreamContinuation(let upstreamContinuation): - // This is signalling the upstream task that is consuming the upstream - // sequence to signal demand. - upstreamContinuation?.resume(returning: ()) - - case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): - // This is signalling the upstream task that is consuming the upstream - // sequence to signal demand and start the clock task. - upstreamContinuation?.resume(returning: ()) - clockContinuation?.resume(returning: deadline) - - case .resumeDownstreamContinuationWithNil(let continuation): - continuation.resume(returning: .success(nil)) - - case .resumeDownstreamContinuationWithError(let continuation, let error): - continuation.resume(returning: .failure(error)) - } + let action: DebounceStateMachine.NextAction? = self.stateMachine.withCriticalRegion { + let action = $0.next(for: continuation) + + switch action { + case .startTask(let base): + self.startTask( + stateMachine: &$0, + base: base, + downstreamContinuation: continuation + ) + return nil + + case .resumeUpstreamContinuation: + return action + + case .resumeUpstreamAndClockContinuation: + return action + + case .resumeDownstreamContinuationWithNil: + return action + + case .resumeDownstreamContinuationWithError: + return action } - } + } + + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .resumeUpstreamContinuation(let upstreamContinuation): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand. + upstreamContinuation?.resume(returning: ()) + + case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand and start the clock task. + upstreamContinuation?.resume(returning: ()) + clockContinuation?.resume(returning: deadline) + + case .resumeDownstreamContinuationWithNil(let continuation): + continuation.resume(returning: .success(nil)) + + case .resumeDownstreamContinuationWithError(let continuation, let error): + continuation.resume(returning: .failure(error)) + + case .none: + break + } + } return try result._rethrowGet() } onCancel: { @@ -258,37 +281,38 @@ final class DebounceStorage: Sendable do { try await group.next() } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - switch action { - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - let downstreamContinuation, - let error, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .cancelTaskAndClockContinuation( - let task, - let clockContinuation - ): - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - case .none: - break - } + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) } - group.cancelAll() + switch action { + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + let downstreamContinuation, + let error, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) + + task.cancel() + + downstreamContinuation.resume(returning: .failure(error)) + + case .cancelTaskAndClockContinuation( + let task, + let clockContinuation + ): + clockContinuation?.resume(throwing: CancellationError()) + task.cancel() + case .none: + break + } } + + group.cancelAll() } } } diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index 7d971a78..93a3466c 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -39,7 +39,7 @@ final class ZipStorage (Base1.Element, Base2.Element, Base3.Element?)? { try await withTaskCancellationHandler { let result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in + let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next(for: continuation) switch action { case .startTask(let base1, let base2, let base3): @@ -51,42 +51,59 @@ final class ZipStorage Date: Wed, 24 Jan 2024 06:28:26 -0500 Subject: [PATCH 121/149] Update README.md (#306) Missing Version Number --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2324a873..c49c9c6c 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ To use the `AsyncAlgorithms` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: ```swift -.package(url: "https://github.com/apple/swift-async-algorithms"), +.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), ``` Include `"AsyncAlgorithms"` as a dependency for your executable target: From d162617838265e2804f0fc427bed4398f5b1c08b Mon Sep 17 00:00:00 2001 From: Eric Horacek Date: Wed, 28 Feb 2024 04:47:12 -0800 Subject: [PATCH 122/149] Depend on specific products from swift-collections (#307) * Only depend on OrderedCollections from swift-collections Before this change, once consumers update to SwiftCollections 1.1.0 they'll pull in all of the new collection dependencies (HashTree, BitCollections, etc.). In our case, this increased our binary size by ~1MB. To fix this, we switch to only depending on what we need. * Add DequeModule dependency --- Package.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2932e199..3e8a9389 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,10 @@ let package = Package( targets: [ .target( name: "AsyncAlgorithms", - dependencies: [.product(name: "Collections", package: "swift-collections")], + dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections"), + ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency=complete"), ] From 46b4464735ae57635482a86217272427964c1fee Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 4 Apr 2024 20:46:16 +0100 Subject: [PATCH 123/149] Fix some strict concurrency warnings (#310) # Motivation There were a few new strict concurrency warnings that this PR fixes. --- Package.swift | 2 +- .../Buffer/BoundedBufferStateMachine.swift | 17 ++++++++------ .../Buffer/UnboundedBufferStateMachine.swift | 23 +++++++++++-------- .../Channels/ChannelStateMachine.swift | 3 --- Sources/AsyncAlgorithms/UnsafeTransfer.swift | 19 +++++++++++++++ 5 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 Sources/AsyncAlgorithms/UnsafeTransfer.swift diff --git a/Package.swift b/Package.swift index 3e8a9389..04121ef6 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index 5c99d3d7..d6008ad9 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -16,16 +16,19 @@ struct BoundedBufferStateMachine { typealias SuspendedProducer = UnsafeContinuation typealias SuspendedConsumer = UnsafeContinuation?, Never> + // We are using UnsafeTransfer here since we have to get the elements from the task + // into the consumer task. This is a transfer but we cannot prove this to the compiler at this point + // since next is not marked as transferring the return value. fileprivate enum State { case initial(base: Base) case buffering( task: Task, - buffer: Deque>, + buffer: Deque, Error>>, suspendedProducer: SuspendedProducer?, suspendedConsumer: SuspendedConsumer? ) case modifying - case finished(buffer: Deque>) + case finished(buffer: Deque, Error>>) } private var state: State @@ -139,7 +142,7 @@ struct BoundedBufferStateMachine { // 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(element)) + buffer.append(.success(.init(element))) self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) return .none @@ -218,7 +221,7 @@ struct BoundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) - return .returnResult(producerContinuation: suspendedProducer, result: result) + return .returnResult(producerContinuation: suspendedProducer, result: result.map { $0.wrapped }) case .buffering(_, _, _, .some): preconditionFailure("Invalid states. There is already a suspended consumer.") @@ -233,7 +236,7 @@ struct BoundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .finished(buffer: buffer) - return .returnResult(producerContinuation: nil, result: result) + return .returnResult(producerContinuation: nil, result: result.map { $0.wrapped }) } } @@ -257,7 +260,7 @@ struct BoundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .buffering(task: task, buffer: buffer, suspendedProducer: nil, suspendedConsumer: nil) - return .returnResult(producerContinuation: suspendedProducer, result: result) + return .returnResult(producerContinuation: suspendedProducer, result: result.map { $0.wrapped }) case .buffering(_, _, _, .some): preconditionFailure("Invalid states. There is already a suspended consumer.") @@ -272,7 +275,7 @@ struct BoundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .finished(buffer: buffer) - return .returnResult(producerContinuation: nil, result: result) + return .returnResult(producerContinuation: nil, result: result.map { $0.wrapped }) } } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index de5d37ae..be19b58b 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -21,15 +21,18 @@ struct UnboundedBufferStateMachine { case bufferingOldest(Int) } + // We are using UnsafeTransfer here since we have to get the elements from the task + // into the consumer task. This is a transfer but we cannot prove this to the compiler at this point + // since next is not marked as transferring the return value. fileprivate enum State { case initial(base: Base) case buffering( task: Task, - buffer: Deque>, + buffer: Deque, Error>>, suspendedConsumer: SuspendedConsumer? ) case modifying - case finished(buffer: Deque>) + case finished(buffer: Deque, Error>>) } private var state: State @@ -84,15 +87,15 @@ struct UnboundedBufferStateMachine { self.state = .modifying switch self.policy { case .unlimited: - buffer.append(.success(element)) + buffer.append(.success(.init(element))) case .bufferingNewest(let limit): if buffer.count >= limit { _ = buffer.popFirst() } - buffer.append(.success(element)) + buffer.append(.success(.init(element))) case .bufferingOldest(let limit): if buffer.count < limit { - buffer.append(.success(element)) + buffer.append(.success(.init(element))) } } self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) @@ -170,7 +173,7 @@ struct UnboundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) - return .returnResult(result) + return .returnResult(result.map { $0.wrapped }) case .modifying: preconditionFailure("Invalid state.") @@ -182,7 +185,7 @@ struct UnboundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .finished(buffer: buffer) - return .returnResult(result) + return .returnResult(result.map { $0.wrapped }) } } @@ -208,7 +211,7 @@ struct UnboundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) - return .resumeConsumer(result) + return .resumeConsumer(result.map { $0.wrapped }) case .modifying: preconditionFailure("Invalid state.") @@ -220,7 +223,7 @@ struct UnboundedBufferStateMachine { self.state = .modifying let result = buffer.popFirst()! self.state = .finished(buffer: buffer) - return .resumeConsumer(result) + return .resumeConsumer(result.map { $0.wrapped }) } } @@ -251,3 +254,5 @@ struct UnboundedBufferStateMachine { extension UnboundedBufferStateMachine: Sendable where Base: Sendable { } extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable { } + + diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index 2c8b1b92..920f6056 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -10,9 +10,6 @@ //===----------------------------------------------------------------------===// import OrderedCollections -// NOTE: this is only marked as unchecked since the swift-collections tag is before auditing for Sendable -extension OrderedSet: @unchecked Sendable where Element: Sendable { } - struct ChannelStateMachine: Sendable { private struct SuspendedProducer: Hashable, Sendable { let id: UInt64 diff --git a/Sources/AsyncAlgorithms/UnsafeTransfer.swift b/Sources/AsyncAlgorithms/UnsafeTransfer.swift new file mode 100644 index 00000000..c8bfca12 --- /dev/null +++ b/Sources/AsyncAlgorithms/UnsafeTransfer.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A wrapper struct to unconditionally to transfer an non-Sendable value. +struct UnsafeTransfer: @unchecked Sendable { + let wrapped: Element + + init(_ wrapped: Element) { + self.wrapped = wrapped + } +} From 6ae9a051f76b81cc668305ceed5b0e0a7fd93d20 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 11 Apr 2024 06:41:45 +0900 Subject: [PATCH 124/149] Add support for `SWIFTCI_USE_LOCAL_DEPS` convention (#311) To use this package in utils/build-script pipeline --- Package.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 04121ef6..4132319a 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,6 @@ let package = Package( products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), ], - dependencies: [ - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - ], targets: [ .target( name: "AsyncAlgorithms", @@ -52,3 +48,14 @@ let package = Package( ), ] ) + +if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ] +} else { + package.dependencies += [ + .package(path: "../swift-collections"), + ] +} From e83857ca55e3a37d6833c2f067ad6f8b392a1b52 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 6 Aug 2024 11:26:55 +0100 Subject: [PATCH 125/149] Add Musl import, error if unrecognised platform (#325) * Add Musl import, error if unrecognised platform * Add #else #error for all platform checks --- Sources/AsyncAlgorithms/Locking.swift | 22 ++++++++++++++----- .../AsyncSequenceValidation/TaskDriver.swift | 12 ++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 952b13c8..a016d10f 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -13,19 +13,23 @@ import Darwin #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(WinSDK) import WinSDK +#else +#error("Unsupported platform") #endif internal struct Lock { #if canImport(Darwin) typealias Primitive = os_unfair_lock -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK #else - typealias Primitive = Int + #error("Unsupported platform") #endif typealias PlatformLock = UnsafeMutablePointer @@ -38,16 +42,18 @@ internal struct Lock { fileprivate static func initialize(_ platformLock: PlatformLock) { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) let result = pthread_mutex_init(platformLock, nil) precondition(result == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) InitializeSRWLock(platformLock) +#else + #error("Unsupported platform") #endif } fileprivate static func deinitialize(_ platformLock: PlatformLock) { -#if canImport(Glibc) +#if canImport(Glibc) || canImport(Musl) let result = pthread_mutex_destroy(platformLock) precondition(result == 0, "pthread_mutex_destroy failed") #endif @@ -57,21 +63,25 @@ internal struct Lock { fileprivate static func lock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_lock(platformLock) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) +#else + #error("Unsupported platform") #endif } fileprivate static func unlock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) let result = pthread_mutex_unlock(platformLock) precondition(result == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) ReleaseSRWLockExclusive(platformLock) +#else + #error("Unsupported platform") #endif } diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index ed128193..639557d0 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -15,8 +15,12 @@ import _CAsyncSequenceValidationSupport import Darwin #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") +#else +#error("Unsupported platform") #endif #if canImport(Darwin) @@ -24,7 +28,7 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return nil } -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw!).takeRetainedValue().run() return nil @@ -38,7 +42,7 @@ final class TaskDriver { let queue: WorkQueue #if canImport(Darwin) var thread: pthread_t? -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) var thread = pthread_t() #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") @@ -50,7 +54,7 @@ final class TaskDriver { } func start() { -#if canImport(Darwin) || canImport(Glibc) +#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) pthread_create(&thread, nil, start_thread, Unmanaged.passRetained(self).toOpaque()) #elseif canImport(WinSDK) @@ -68,7 +72,7 @@ final class TaskDriver { func join() { #if canImport(Darwin) pthread_join(thread!, nil) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Musl) pthread_join(thread, nil) #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") From 2503842d68e4282fcc77070791a4c6698b19ab54 Mon Sep 17 00:00:00 2001 From: finagolfin Date: Wed, 7 Aug 2024 15:48:51 +0530 Subject: [PATCH 126/149] Use Bionic module from new Android overlay in Swift 6 instead (#326) The new module and overlay were merged into Swift 6 in swiftlang/swift#74758. --- Sources/AsyncAlgorithms/Locking.swift | 12 +++++++----- Sources/AsyncSequenceValidation/TaskDriver.swift | 15 +++++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index a016d10f..6fc1d090 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -17,6 +17,8 @@ import Glibc import Musl #elseif canImport(WinSDK) import WinSDK +#elseif canImport(Bionic) +import Bionic #else #error("Unsupported platform") #endif @@ -24,7 +26,7 @@ import WinSDK internal struct Lock { #if canImport(Darwin) typealias Primitive = os_unfair_lock -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK @@ -42,7 +44,7 @@ internal struct Lock { fileprivate static func initialize(_ platformLock: PlatformLock) { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_init(platformLock, nil) precondition(result == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) @@ -53,7 +55,7 @@ internal struct Lock { } fileprivate static func deinitialize(_ platformLock: PlatformLock) { -#if canImport(Glibc) || canImport(Musl) +#if canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_destroy(platformLock) precondition(result == 0, "pthread_mutex_destroy failed") #endif @@ -63,7 +65,7 @@ internal struct Lock { fileprivate static func lock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_lock(platformLock) -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) @@ -75,7 +77,7 @@ internal struct Lock { fileprivate static func unlock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) let result = pthread_mutex_unlock(platformLock) precondition(result == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 639557d0..50ed45ff 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -17,6 +17,8 @@ import Darwin import Glibc #elseif canImport(Musl) import Musl +#elseif canImport(Bionic) +import Bionic #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") #else @@ -28,11 +30,16 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return nil } -#elseif canImport(Glibc) || canImport(Musl) +#elseif (canImport(Glibc) && !os(Android)) || canImport(Musl) func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw!).takeRetainedValue().run() return nil } +#elseif os(Android) +func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + Unmanaged.fromOpaque(raw).takeRetainedValue().run() + return UnsafeMutableRawPointer(bitPattern: 0xdeadbee)! +} #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") #endif @@ -42,7 +49,7 @@ final class TaskDriver { let queue: WorkQueue #if canImport(Darwin) var thread: pthread_t? -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) var thread = pthread_t() #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") @@ -54,7 +61,7 @@ final class TaskDriver { } func start() { -#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) +#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_create(&thread, nil, start_thread, Unmanaged.passRetained(self).toOpaque()) #elseif canImport(WinSDK) @@ -72,7 +79,7 @@ final class TaskDriver { func join() { #if canImport(Darwin) pthread_join(thread!, nil) -#elseif canImport(Glibc) || canImport(Musl) +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_join(thread, nil) #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") From 5c8bd186f48c16af0775972700626f0b74588278 Mon Sep 17 00:00:00 2001 From: Mason Kim <59835351+qwerty3345@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:15:02 +0900 Subject: [PATCH 127/149] Fix a few typos (#329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DOCS] fix typo: precicely → precisely * [DOCS] fix typo: ever → every * [DOCS] fix typo: AsyncBufferSequence, relative --- Evolution/0006-combineLatest.md | 2 +- Evolution/0010-buffer.md | 4 ++-- Evolution/0011-interspersed.md | 6 +++--- Evolution/NNNN-rate-limits.md | 2 +- .../Interspersed/AsyncInterspersedSequence.swift | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Evolution/0006-combineLatest.md b/Evolution/0006-combineLatest.md index 584b068a..bcdf6a84 100644 --- a/Evolution/0006-combineLatest.md +++ b/Evolution/0006-combineLatest.md @@ -13,7 +13,7 @@ ## Introduction -Similar to the `zip` algorithm there is a need to combine the latest values from multiple input asynchronous sequences. Since `AsyncSequence` augments the concept of sequence with the characteristic of time it means that the composition of elements may not just be pairwise emissions but instead be temporal composition. This means that it is useful to emit a new tuple _when_ a value is produced. The `combineLatest` algorithm provides precicely that. +Similar to the `zip` algorithm there is a need to combine the latest values from multiple input asynchronous sequences. Since `AsyncSequence` augments the concept of sequence with the characteristic of time it means that the composition of elements may not just be pairwise emissions but instead be temporal composition. This means that it is useful to emit a new tuple _when_ a value is produced. The `combineLatest` algorithm provides precisely that. ## Detailed Design diff --git a/Evolution/0010-buffer.md b/Evolution/0010-buffer.md index da56def7..e77f6d81 100644 --- a/Evolution/0010-buffer.md +++ b/Evolution/0010-buffer.md @@ -28,7 +28,7 @@ By applying the buffer operator to the previous example, the file can be read as ## Proposed Solution -We propose to extend `AsyncSequence` with a `buffer()` operator. This operator will return an `AsyncBuffereSequence` that wraps the source `AsyncSequence` and handle the buffering mechanism. +We propose to extend `AsyncSequence` with a `buffer()` operator. This operator will return an `AsyncBufferSequence` that wraps the source `AsyncSequence` and handle the buffering mechanism. This operator will accept an `AsyncBufferSequencePolicy`. The policy will dictate the behaviour in case of a buffer overflow. @@ -43,7 +43,7 @@ public struct AsyncBufferSequencePolicy: Sendable { } ``` -And the public API of `AsyncBuffereSequence` will be: +And the public API of `AsyncBufferSequence` will be: ```swift extension AsyncSequence where Self: Sendable { diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md index cfc99737..27e5dbc1 100644 --- a/Evolution/0011-interspersed.md +++ b/Evolution/0011-interspersed.md @@ -178,7 +178,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .element(separator) self.every = every @@ -186,7 +186,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .syncClosure(separator) self.every = every @@ -194,7 +194,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .asyncClosure(separator) self.every = every diff --git a/Evolution/NNNN-rate-limits.md b/Evolution/NNNN-rate-limits.md index 94bf6e89..e78c60d1 100644 --- a/Evolution/NNNN-rate-limits.md +++ b/Evolution/NNNN-rate-limits.md @@ -18,7 +18,7 @@ ## Introduction -When events can potentially happen faster than the desired consumption rate, there are multiple ways to handle the situation. One approach is to only emit values after a given period of time of inactivity, or "quiescence", has elapsed. This algorithm is commonly referred to as debouncing. A very close reelativee is an apporach to emit values after a given period has elapsed. These emitted values can be reduced from the values encountered during the waiting period. This algorithm is commonly referred to as throttling. +When events can potentially happen faster than the desired consumption rate, there are multiple ways to handle the situation. One approach is to only emit values after a given period of time of inactivity, or "quiescence", has elapsed. This algorithm is commonly referred to as debouncing. A very close relative is an approach to emit values after a given period has elapsed. These emitted values can be reduced from the values encountered during the waiting period. This algorithm is commonly referred to as throttling. ## Proposed Solution diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 9932e77e..78ef20d3 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -157,7 +157,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .element(separator) self.every = every @@ -165,7 +165,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .syncClosure(separator) self.every = every @@ -173,7 +173,7 @@ public struct AsyncInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .asyncClosure(separator) self.every = every @@ -310,7 +310,7 @@ public struct AsyncThrowingInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .syncClosure(separator) self.every = every @@ -318,7 +318,7 @@ public struct AsyncThrowingInterspersedSequence { @usableFromInline internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { - precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + precondition(every > 0, "Separators can only be interspersed every 1+ elements") self.base = base self.separator = .asyncClosure(separator) self.every = every From 4c3ea81f81f0a25d0470188459c6d4bf20cf2f97 Mon Sep 17 00:00:00 2001 From: orobio Date: Wed, 9 Oct 2024 10:47:15 +0200 Subject: [PATCH 128/149] Fix memory leak in Lock (#331) --- Sources/AsyncAlgorithms/Locking.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 6fc1d090..4265bdfd 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -95,6 +95,7 @@ internal struct Lock { func deinitialize() { Lock.deinitialize(platformLock) + platformLock.deallocate() } func lock() { From ba33a225e9645a91923ba114e639e5d47318ba79 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 10:53:01 +0100 Subject: [PATCH 129/149] Format rules --- .swift-format | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ .swiftformat | 25 ---------------------- 2 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 .swift-format delete mode 100644 .swiftformat 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 From d5b49aab174cc66bb0f7df26e9f5e1cc3cf8dccc Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 17:24:58 +0100 Subject: [PATCH 130/149] Apply formatting --- Package.swift | 18 +- Package@swift-5.7.swift | 11 +- .../AsyncAdjacentPairsSequence.swift | 4 +- .../AsyncBufferedByteIterator.swift | 20 +- .../AsyncAlgorithms/AsyncChain2Sequence.swift | 23 +- .../AsyncAlgorithms/AsyncChain3Sequence.swift | 31 +- .../AsyncChunkedByGroupSequence.swift | 29 +- .../AsyncChunkedOnProjectionSequence.swift | 28 +- .../AsyncChunksOfCountOrSignalSequence.swift | 69 +- .../AsyncChunksOfCountSequence.swift | 18 +- .../AsyncCompactedSequence.swift | 14 +- .../AsyncExclusiveReductionsSequence.swift | 50 +- .../AsyncInclusiveReductionsSequence.swift | 10 +- .../AsyncJoinedBySeparatorSequence.swift | 103 +- .../AsyncAlgorithms/AsyncJoinedSequence.swift | 58 +- .../AsyncRemoveDuplicatesSequence.swift | 25 +- .../AsyncAlgorithms/AsyncSyncSequence.swift | 21 +- .../AsyncThrottleSequence.swift | 69 +- ...cThrowingExclusiveReductionsSequence.swift | 66 +- ...cThrowingInclusiveReductionsSequence.swift | 10 +- .../AsyncAlgorithms/AsyncTimerSequence.swift | 15 +- .../Buffer/AsyncBufferSequence.swift | 40 +- .../Buffer/BoundedBufferStateMachine.swift | 380 ++--- .../Buffer/BoundedBufferStorage.swift | 94 +- .../Buffer/UnboundedBufferStateMachine.swift | 286 ++-- .../Buffer/UnboundedBufferStorage.swift | 82 +- .../Channels/AsyncChannel.swift | 2 +- .../Channels/AsyncThrowingChannel.swift | 2 +- .../Channels/ChannelStateMachine.swift | 361 ++--- .../Channels/ChannelStorage.swift | 84 +- .../AsyncCombineLatest2Sequence.swift | 14 +- .../AsyncCombineLatest3Sequence.swift | 17 +- .../CombineLatestStateMachine.swift | 151 +- .../CombineLatest/CombineLatestStorage.swift | 56 +- .../Debounce/AsyncDebounceSequence.swift | 133 +- .../Debounce/DebounceStateMachine.swift | 1363 +++++++++-------- .../Debounce/DebounceStorage.swift | 531 +++---- Sources/AsyncAlgorithms/Dictionary.swift | 15 +- .../AsyncInterspersedSequence.swift | 734 ++++----- Sources/AsyncAlgorithms/Locking.swift | 98 +- .../Merge/AsyncMerge2Sequence.swift | 124 +- .../Merge/AsyncMerge3Sequence.swift | 143 +- .../Merge/MergeStateMachine.swift | 1197 ++++++++------- .../AsyncAlgorithms/Merge/MergeStorage.swift | 501 +++--- Sources/AsyncAlgorithms/Rethrow.swift | 5 +- Sources/AsyncAlgorithms/SetAlgebra.swift | 2 +- Sources/AsyncAlgorithms/UnsafeTransfer.swift | 8 +- .../Zip/AsyncZip2Sequence.swift | 4 +- .../Zip/AsyncZip3Sequence.swift | 17 +- .../AsyncAlgorithms/Zip/ZipStateMachine.swift | 84 +- Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 61 +- .../ValidationTest.swift | 55 +- .../AsyncSequenceValidationDiagram.swift | 115 +- Sources/AsyncSequenceValidation/Clock.swift | 64 +- Sources/AsyncSequenceValidation/Event.swift | 34 +- .../AsyncSequenceValidation/Expectation.swift | 40 +- Sources/AsyncSequenceValidation/Input.swift | 40 +- Sources/AsyncSequenceValidation/Job.swift | 4 +- .../SourceLocation.swift | 4 +- .../AsyncSequenceValidation/TaskDriver.swift | 51 +- Sources/AsyncSequenceValidation/Test.swift | 175 ++- Sources/AsyncSequenceValidation/Theme.swift | 6 +- .../AsyncSequenceValidation/WorkQueue.swift | 95 +- .../Interspersed/TestInterspersed.swift | 298 ++-- .../Performance/ThroughputMeasurement.swift | 106 +- .../Support/Asserts.swift | 127 +- .../Support/Failure.swift | 2 +- Tests/AsyncAlgorithmsTests/Support/Gate.swift | 8 +- .../Support/GatedSequence.swift | 12 +- .../Support/Indefinite.swift | 4 +- .../Support/ManualClock.swift | 118 +- .../Support/ReportingSequence.swift | 16 +- .../Support/Validator.swift | 25 +- .../Support/ViolatingSequence.swift | 12 +- .../TestAdjacentPairs.swift | 4 +- Tests/AsyncAlgorithmsTests/TestBuffer.swift | 10 +- .../TestBufferedByteIterator.swift | 34 +- Tests/AsyncAlgorithmsTests/TestChain.swift | 14 +- Tests/AsyncAlgorithmsTests/TestChunk.swift | 54 +- .../TestCombineLatest.swift | 128 +- .../AsyncAlgorithmsTests/TestCompacted.swift | 8 +- Tests/AsyncAlgorithmsTests/TestDebounce.swift | 57 +- .../AsyncAlgorithmsTests/TestDictionary.swift | 12 +- Tests/AsyncAlgorithmsTests/TestJoin.swift | 12 +- Tests/AsyncAlgorithmsTests/TestLazy.swift | 24 +- .../TestManualClock.swift | 4 +- Tests/AsyncAlgorithmsTests/TestMerge.swift | 139 +- .../TestRangeReplaceableCollection.swift | 8 +- .../AsyncAlgorithmsTests/TestReductions.swift | 22 +- .../TestRemoveDuplicates.swift | 18 +- .../AsyncAlgorithmsTests/TestSetAlgebra.swift | 6 +- Tests/AsyncAlgorithmsTests/TestThrottle.swift | 110 +- .../TestThrowingChannel.swift | 5 +- Tests/AsyncAlgorithmsTests/TestTimer.swift | 22 +- .../TestValidationTests.swift | 111 +- .../AsyncAlgorithmsTests/TestValidator.swift | 16 +- Tests/AsyncAlgorithmsTests/TestZip.swift | 14 +- 97 files changed, 5081 insertions(+), 4443 deletions(-) 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/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..b060bdee 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -31,10 +31,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 +44,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 +84,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..c9f14e64 100644 --- a/Sources/AsyncAlgorithms/Dictionary.swift +++ b/Sources/AsyncAlgorithms/Dictionary.swift @@ -24,10 +24,11 @@ extension Dictionary { /// `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 +48,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 +61,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 +75,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..7adb0600 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. 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..1876b97c 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. 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() } } From ba30f2051bd62efe11e37bf6b4273545a8effce8 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 11:43:25 +0100 Subject: [PATCH 131/149] Documentation --- .../AsyncAlgorithms.docc/Guides/BufferedBytes.md | 4 ++-- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md | 2 ++ .../AsyncAlgorithms.docc/Guides/CombineLatest.md | 4 ++-- .../AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Effects.md | 4 ++++ .../AsyncAlgorithms.docc/Guides/Intersperse.md | 4 ++-- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md | 4 ++-- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md | 4 ++-- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Zip.md | 4 ++-- Sources/AsyncAlgorithms/Dictionary.swift | 2 -- Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift | 2 +- Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift | 2 +- 11 files changed, 20 insertions(+), 16 deletions(-) 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..a2de67e6 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md @@ -1,10 +1,10 @@ # AsyncSyncSequence +v + [[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 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/Dictionary.swift b/Sources/AsyncAlgorithms/Dictionary.swift index c9f14e64..629a7712 100644 --- a/Sources/AsyncAlgorithms/Dictionary.swift +++ b/Sources/AsyncAlgorithms/Dictionary.swift @@ -20,8 +20,6 @@ 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 diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 7adb0600..a705c4a9 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -25,7 +25,7 @@ where 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 diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index 1876b97c..f4a15edf 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -28,7 +28,7 @@ where 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, From 552856a85cb66d2fb32d7d2aefb3382c79bb61c1 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 11:43:34 +0100 Subject: [PATCH 132/149] License headers --- .license_header_template | 10 +++++ .licenseignore | 40 +++++++++++++++++++ .../AsyncInclusiveReductionsSequence.swift | 11 +++++ 3 files changed, 61 insertions(+) create mode 100644 .license_header_template create mode 100644 .licenseignore 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/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index b060bdee..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. From 5878737018ec7bcd84a9a6ea9421c8763932a5ba Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 11:44:17 +0100 Subject: [PATCH 133/149] Language check --- Evolution/0000-swift-async-algorithms-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 08bb1bb0143395e3ea2c2882563322ae6257cf79 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 11:45:15 +0100 Subject: [PATCH 134/149] Fix yaml check and remove docker compose --- .yamllint.yml | 7 +++++ docker/Dockerfile | 24 ----------------- docker/docker-compose.2004.57.yaml | 22 ---------------- docker/docker-compose.2004.58.yaml | 21 --------------- docker/docker-compose.2204.main.yaml | 21 --------------- docker/docker-compose.yaml | 39 ---------------------------- 6 files changed, 7 insertions(+), 127 deletions(-) create mode 100644 .yamllint.yml delete mode 100644 docker/Dockerfile delete mode 100644 docker/docker-compose.2004.57.yaml delete mode 100644 docker/docker-compose.2004.58.yaml delete mode 100644 docker/docker-compose.2204.main.yaml delete mode 100644 docker/docker-compose.yaml 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/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 From 9fcf11df15e1ff0f9e8ba17cb619824b04d5965a Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Mar 2025 14:04:05 +0100 Subject: [PATCH 135/149] Fix doc paste mistake --- Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md index a2de67e6..48f8cfbf 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md @@ -1,12 +1,10 @@ # AsyncSyncSequence -v +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)] -This operation is available for all `Sequence` types. - ```swift let numbers = [1, 2, 3, 4].async let characters = "abcde".async From 57b0814edf94b60af49541dea0eed9cd8716f9dd Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Sat, 29 Mar 2025 01:49:13 -0700 Subject: [PATCH 136/149] [CI] Add support for GitHub Actions (#344) --- .github/workflows/pull_request.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..1ed694ad --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,15 @@ +name: Pull request + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Swift Async Algorithms" From 042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 22 Apr 2025 08:07:37 +0100 Subject: [PATCH 137/149] Move platform requirements to availability annotations (#348) * Move platform requirements to availability annotations Adding or raising the deployment platforms in the package manifest is a SemVer major breaking change as consumers must also add or raise their deployment platforms. This is a known limitation of SwiftPM. Unforunately this means that it's very difficult for non-leaf packages to adopt packages which declare their platforms in the manifest. Doing so puts the brakes on adoption and ecosystem growth. For 'core' packages like this one availability constraints should be expressed on declarations rather than in the manifest. This patch adds equivalent availability annotations to declarations across the package and removes platforms from the package manifest. * Address naming feedback --- Package.swift | 45 ++++++++++++++----- .../AsyncAdjacentPairsSequence.swift | 4 ++ .../AsyncBufferedByteIterator.swift | 2 + .../AsyncAlgorithms/AsyncChain2Sequence.swift | 6 +++ .../AsyncAlgorithms/AsyncChain3Sequence.swift | 6 +++ .../AsyncChunkedByGroupSequence.swift | 5 +++ .../AsyncChunkedOnProjectionSequence.swift | 5 +++ .../AsyncChunksOfCountOrSignalSequence.swift | 6 +++ .../AsyncChunksOfCountSequence.swift | 9 ++-- .../AsyncCompactedSequence.swift | 4 ++ .../AsyncExclusiveReductionsSequence.swift | 8 ++++ .../AsyncInclusiveReductionsSequence.swift | 7 +++ .../AsyncJoinedBySeparatorSequence.swift | 4 ++ .../AsyncAlgorithms/AsyncJoinedSequence.swift | 4 ++ .../AsyncRemoveDuplicatesSequence.swift | 9 ++++ .../AsyncAlgorithms/AsyncSyncSequence.swift | 4 ++ .../AsyncThrottleSequence.swift | 1 + ...cThrowingExclusiveReductionsSequence.swift | 8 ++++ ...cThrowingInclusiveReductionsSequence.swift | 7 +++ .../Buffer/AsyncBufferSequence.swift | 5 +++ .../Buffer/BoundedBufferStateMachine.swift | 3 ++ .../Buffer/BoundedBufferStorage.swift | 1 + .../Buffer/UnboundedBufferStateMachine.swift | 3 ++ .../Buffer/UnboundedBufferStorage.swift | 1 + .../Channels/AsyncChannel.swift | 1 + .../Channels/AsyncThrowingChannel.swift | 1 + .../Channels/ChannelStateMachine.swift | 1 + .../Channels/ChannelStorage.swift | 1 + .../AsyncCombineLatest2Sequence.swift | 2 + .../AsyncCombineLatest3Sequence.swift | 2 + .../CombineLatestStateMachine.swift | 1 + .../CombineLatest/CombineLatestStorage.swift | 1 + .../Debounce/AsyncDebounceSequence.swift | 1 + Sources/AsyncAlgorithms/Dictionary.swift | 4 ++ .../AsyncInterspersedSequence.swift | 18 ++++++++ .../Merge/AsyncMerge2Sequence.swift | 6 +++ .../Merge/AsyncMerge3Sequence.swift | 6 +++ .../Merge/MergeStateMachine.swift | 1 + .../AsyncAlgorithms/Merge/MergeStorage.swift | 1 + .../RangeReplaceableCollection.swift | 1 + Sources/AsyncAlgorithms/SetAlgebra.swift | 1 + .../Zip/AsyncZip2Sequence.swift | 2 + .../Zip/AsyncZip3Sequence.swift | 2 + .../AsyncAlgorithms/Zip/ZipStateMachine.swift | 1 + Sources/AsyncAlgorithms/Zip/ZipStorage.swift | 1 + .../ValidationTest.swift | 4 ++ .../AsyncSequenceValidationDiagram.swift | 1 + Sources/AsyncSequenceValidation/Clock.swift | 6 +++ Sources/AsyncSequenceValidation/Event.swift | 1 + .../AsyncSequenceValidation/Expectation.swift | 1 + Sources/AsyncSequenceValidation/Input.swift | 1 + Sources/AsyncSequenceValidation/Job.swift | 1 + .../AsyncSequenceValidation/TaskDriver.swift | 4 ++ Sources/AsyncSequenceValidation/Test.swift | 3 ++ Sources/AsyncSequenceValidation/Theme.swift | 3 ++ .../AsyncSequenceValidation/WorkQueue.swift | 1 + 56 files changed, 225 insertions(+), 13 deletions(-) diff --git a/Package.swift b/Package.swift index 1177d22d..4a99d5f8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,15 +1,40 @@ // swift-tools-version: 5.8 import PackageDescription +import CompilerPluginSupport + +// Availability Macros +let availabilityTags = [Availability("AsyncAlgorithms")] +let versionNumbers = ["1.0"] + +// Availability Macro Utilities +enum OSAvailability: String { + // This should match the package's deployment target + case initialIntroduction = "macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" + case pending = "macOS 9999, iOS 9999, tvOS 9999, watchOS 9999" + // Use 10000 for future availability to avoid compiler magic around + // the 9999 version number but ensure it is greater than 9999 + case future = "macOS 10000, iOS 10000, tvOS 10000, watchOS 10000" +} + +struct Availability { + let name: String + let osAvailability: OSAvailability + + init(_ name: String, availability: OSAvailability = .initialIntroduction) { + self.name = name + self.osAvailability = availability + } +} + +let availabilityMacros: [SwiftSetting] = versionNumbers.flatMap { version in + availabilityTags.map { + .enableExperimentalFeature("AvailabilityMacro=\($0.name) \(version):\($0.osAvailability.rawValue)") + } +} let package = Package( name: "swift-async-algorithms", - platforms: [ - .macOS("10.15"), - .iOS("13.0"), - .tvOS("13.0"), - .watchOS("6.0"), - ], products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]) ], @@ -20,14 +45,14 @@ let package = Package( .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "DequeModule", package: "swift-collections"), ], - swiftSettings: [ + swiftSettings: availabilityMacros + [ .enableExperimentalFeature("StrictConcurrency=complete") ] ), .target( name: "AsyncSequenceValidation", dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], - swiftSettings: [ + swiftSettings: availabilityMacros + [ .enableExperimentalFeature("StrictConcurrency=complete") ] ), @@ -35,14 +60,14 @@ let package = Package( .target( name: "AsyncAlgorithms_XCTest", dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], - swiftSettings: [ + swiftSettings: availabilityMacros + [ .enableExperimentalFeature("StrictConcurrency=complete") ] ), .testTarget( name: "AsyncAlgorithmsTests", dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], - swiftSettings: [ + swiftSettings: availabilityMacros + [ .enableExperimentalFeature("StrictConcurrency=complete") ] ), diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 0ba5e90d..25dbd49f 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// An `AsyncSequence` that iterates over the adjacent pairs of the original /// original `AsyncSequence`. @@ -26,6 +27,7 @@ extension AsyncSequence { /// /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements /// or the original `AsyncSequence`. + @available(AsyncAlgorithms 1.0, *) @inlinable public func adjacentPairs() -> AsyncAdjacentPairsSequence { AsyncAdjacentPairsSequence(self) @@ -34,6 +36,7 @@ extension AsyncSequence { /// An `AsyncSequence` that iterates over the adjacent pairs of the original /// `AsyncSequence`. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncAdjacentPairsSequence: AsyncSequence { public typealias Element = (Base.Element, Base.Element) @@ -83,6 +86,7 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift index 4d696e26..62530261 100644 --- a/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift +++ b/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift @@ -39,6 +39,7 @@ /// } /// /// +@available(AsyncAlgorithms 1.0, *) public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 @usableFromInline var buffer: _AsyncBytesBuffer @@ -67,6 +68,7 @@ public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { @available(*, unavailable) extension AsyncBufferedByteIterator: Sendable {} +@available(AsyncAlgorithms 1.0, *) @frozen @usableFromInline internal struct _AsyncBytesBuffer { @usableFromInline diff --git a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift index d0e70250..ddd4a038 100644 --- a/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift @@ -17,6 +17,7 @@ /// - s2: The second asynchronous sequence. /// - Returns: An asynchronous sequence that iterates first over the elements of `s1`, and /// then over the elements of `s2`. +@available(AsyncAlgorithms 1.0, *) @inlinable public func chain( _ s1: Base1, @@ -26,6 +27,7 @@ public func chain( } /// A concatenation of two asynchronous sequences with the same element type. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncChain2Sequence where Base1.Element == Base2.Element { @usableFromInline @@ -41,10 +43,12 @@ public struct AsyncChain2Sequence wh } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChain2Sequence: AsyncSequence { public typealias Element = Base1.Element /// The iterator for a `AsyncChain2Sequence` instance. + @available(AsyncAlgorithms 1.0, *) @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline @@ -76,12 +80,14 @@ extension AsyncChain2Sequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator()) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index ec6d68ae..045bf832 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -18,6 +18,7 @@ /// - s3: The third asynchronous sequence. /// - 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` +@available(AsyncAlgorithms 1.0, *) @inlinable public func chain( _ s1: Base1, @@ -28,6 +29,7 @@ public func chain where Base1.Element == Base2.Element, Base1.Element == Base3.Element { @@ -48,10 +50,12 @@ where Base1.Element == Base2.Element, Base1.Element == Base3.Element { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChain3Sequence: AsyncSequence { public typealias Element = Base1.Element /// The iterator for a `AsyncChain3Sequence` instance. + @available(AsyncAlgorithms 1.0, *) @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline @@ -93,12 +97,14 @@ extension AsyncChain3Sequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator(), base3.makeAsyncIterator()) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index a0e8b446..0e1e99cf 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -9,9 +9,11 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` /// type by testing if elements belong in the same group. + @available(AsyncAlgorithms 1.0, *) @inlinable public func chunked( into: Collected.Type, @@ -21,6 +23,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that creates chunks by testing if elements belong in the same group. + @available(AsyncAlgorithms 1.0, *) @inlinable public func chunked( by belongInSameGroup: @escaping @Sendable (Element, Element) -> Bool @@ -51,6 +54,7 @@ extension AsyncSequence { /// // [10, 20, 30] /// // [10, 40, 40] /// // [10, 20] +@available(AsyncAlgorithms 1.0, *) public struct AsyncChunkedByGroupSequence: AsyncSequence where Collected.Element == Base.Element { public typealias Element = Collected @@ -121,6 +125,7 @@ where Collected.Element == Base.Element { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChunkedByGroupSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift index b98f587c..a9c02fba 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift @@ -9,8 +9,10 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type on the uniqueness of a given subject. + @available(AsyncAlgorithms 1.0, *) @inlinable public func chunked( into: Collected.Type, @@ -20,6 +22,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that creates chunks on the uniqueness of a given subject. + @available(AsyncAlgorithms 1.0, *) @inlinable public func chunked( on projection: @escaping @Sendable (Element) -> Subject @@ -29,6 +32,7 @@ extension AsyncSequence { } /// An `AsyncSequence` that chunks on a subject when it differs from the last element. +@available(AsyncAlgorithms 1.0, *) public struct AsyncChunkedOnProjectionSequence< Base: AsyncSequence, Subject: Equatable, @@ -104,6 +108,7 @@ public struct AsyncChunkedOnProjectionSequence< } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChunkedOnProjectionSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift index 6c68cbdb..731440e2 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountOrSignalSequence.swift @@ -9,8 +9,10 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type of a given count or when a signal `AsyncSequence` produces an element. + @available(AsyncAlgorithms 1.0, *) public func chunks( ofCount count: Int, or signal: Signal, @@ -20,6 +22,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that creates chunks of a given count or when a signal `AsyncSequence` produces an element. + @available(AsyncAlgorithms 1.0, *) public func chunks( ofCount count: Int, or signal: Signal @@ -28,6 +31,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type when a signal `AsyncSequence` produces an element. + @available(AsyncAlgorithms 1.0, *) public func chunked( by signal: Signal, into: Collected.Type @@ -36,6 +40,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that creates chunks when a signal `AsyncSequence` produces an element. + @available(AsyncAlgorithms 1.0, *) public func chunked(by signal: Signal) -> AsyncChunksOfCountOrSignalSequence { chunked(by: signal, into: [Element].self) } @@ -78,6 +83,7 @@ extension AsyncSequence { } /// An `AsyncSequence` that chunks elements into collected `RangeReplaceableCollection` instances by either count or a signal from another `AsyncSequence`. +@available(AsyncAlgorithms 1.0, *) public struct AsyncChunksOfCountOrSignalSequence< Base: AsyncSequence, Collected: RangeReplaceableCollection, diff --git a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift index 70e429ff..71b6c4c8 100644 --- a/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift @@ -9,8 +9,10 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` of a given count. + @available(AsyncAlgorithms 1.0, *) @inlinable public func chunks( ofCount count: Int, @@ -20,6 +22,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that creates chunks of a given count. + @available(AsyncAlgorithms 1.0, *) @inlinable public func chunks(ofCount count: Int) -> AsyncChunksOfCountSequence { chunks(ofCount: count, into: [Element].self) @@ -27,6 +30,7 @@ extension AsyncSequence { } /// An `AsyncSequence` that chunks elements into `RangeReplaceableCollection` instances of at least a given count. +@available(AsyncAlgorithms 1.0, *) public struct AsyncChunksOfCountSequence: AsyncSequence where Collected.Element == Base.Element { public typealias Element = Collected @@ -89,8 +93,7 @@ where Collected.Element == Base.Element { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncChunksOfCountSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension AsyncChunksOfCountSequence.Iterator: Sendable where Base.AsyncIterator: Sendable, Base.Element: Sendable {} - -@available(*, unavailable) -extension AsyncChunksOfCountSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift b/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift index afd08fc7..7717dd1a 100644 --- a/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Returns a new `AsyncSequence` that iterates over every non-nil element from the /// original `AsyncSequence`. @@ -18,6 +19,7 @@ extension AsyncSequence { /// - Returns: An `AsyncSequence` where the element is the unwrapped original /// element and iterates over every non-nil element from the original /// `AsyncSequence`. + @available(AsyncAlgorithms 1.0, *) @inlinable public func compacted() -> AsyncCompactedSequence where Element == Unwrapped? { @@ -27,6 +29,7 @@ extension AsyncSequence { /// An `AsyncSequence` that iterates over every non-nil element from the original /// `AsyncSequence`. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncCompactedSequence: AsyncSequence where Base.Element == Element? { @@ -66,6 +69,7 @@ where Base.Element == Element? { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index a6de1f25..582317c1 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given closure. @@ -22,6 +23,7 @@ extension AsyncSequence { /// the next element in the receiving asynchronous sequence, which it returns. /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. + @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( _ initial: Result, @@ -45,6 +47,7 @@ extension AsyncSequence { /// previous result instead of returning a value. /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. + @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( into initial: Result, @@ -56,6 +59,7 @@ extension AsyncSequence { /// An asynchronous sequence of applying a transform to the element of an asynchronous sequence and the /// previously transformed result. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncExclusiveReductionsSequence { @usableFromInline @@ -75,8 +79,10 @@ public struct AsyncExclusiveReductionsSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncExclusiveReductionsSequence: AsyncSequence { /// The iterator for an `AsyncExclusiveReductionsSequence` instance. + @available(AsyncAlgorithms 1.0, *) @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline @@ -113,12 +119,14 @@ extension AsyncExclusiveReductionsSequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeAsyncIterator(), initial: initial, transform: transform) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index 377918e9..d9049cbc 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given closure. @@ -28,6 +29,7 @@ extension AsyncSequence { /// result and the next element in the receiving sequence, and returns /// the result. /// - Returns: An asynchronous sequence of the reduced elements. + @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( _ transform: @Sendable @escaping (Element, Element) async -> Element @@ -38,6 +40,7 @@ extension AsyncSequence { /// An asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using a given closure. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncInclusiveReductionsSequence { @usableFromInline @@ -53,10 +56,12 @@ public struct AsyncInclusiveReductionsSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncInclusiveReductionsSequence: AsyncSequence { public typealias Element = Base.Element /// The iterator for an `AsyncInclusiveReductionsSequence` instance. + @available(AsyncAlgorithms 1.0, *) @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline @@ -89,12 +94,14 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeAsyncIterator(), transform: transform) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index 05e78c3f..13195436 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -9,8 +9,10 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence where Element: AsyncSequence { /// Concatenate an `AsyncSequence` of `AsyncSequence` elements with a separator. + @available(AsyncAlgorithms 1.0, *) @inlinable public func joined( separator: Separator @@ -20,6 +22,7 @@ extension AsyncSequence where Element: AsyncSequence { } /// An `AsyncSequence` that concatenates `AsyncSequence` elements with a separator. +@available(AsyncAlgorithms 1.0, *) public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base.Element: AsyncSequence, Separator.Element == Base.Element.Element { public typealias Element = Base.Element.Element @@ -143,6 +146,7 @@ where Base.Element: AsyncSequence, Separator.Element == Base.Element.Element { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncJoinedBySeparatorSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable, Separator: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift index cf069959..e03d45d5 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift @@ -9,8 +9,10 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence where Element: AsyncSequence { /// Concatenate an `AsyncSequence` of `AsyncSequence` elements + @available(AsyncAlgorithms 1.0, *) @inlinable public func joined() -> AsyncJoinedSequence { return AsyncJoinedSequence(self) @@ -18,6 +20,7 @@ extension AsyncSequence where Element: AsyncSequence { } /// An `AsyncSequence` that concatenates`AsyncSequence` elements +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncJoinedSequence: AsyncSequence where Base.Element: AsyncSequence { public typealias Element = Base.Element.Element @@ -90,6 +93,7 @@ public struct AsyncJoinedSequence: AsyncSequence where Base } } +@available(AsyncAlgorithms 1.0, *) extension AsyncJoinedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable {} diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index 3b63c64d..d492cdbf 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -9,8 +9,10 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence where Element: Equatable { /// Creates an asynchronous sequence that omits repeated elements. + @available(AsyncAlgorithms 1.0, *) public func removeDuplicates() -> AsyncRemoveDuplicatesSequence { AsyncRemoveDuplicatesSequence(self) { lhs, rhs in lhs == rhs @@ -18,8 +20,10 @@ extension AsyncSequence where Element: Equatable { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Creates an asynchronous sequence that omits repeated elements by testing them with a predicate. + @available(AsyncAlgorithms 1.0, *) public func removeDuplicates( by predicate: @escaping @Sendable (Element, Element) async -> Bool ) -> AsyncRemoveDuplicatesSequence { @@ -27,6 +31,7 @@ extension AsyncSequence { } /// Creates an asynchronous sequence that omits repeated elements by testing them with an error-throwing predicate. + @available(AsyncAlgorithms 1.0, *) public func removeDuplicates( by predicate: @escaping @Sendable (Element, Element) async throws -> Bool ) -> AsyncThrowingRemoveDuplicatesSequence { @@ -35,6 +40,7 @@ extension AsyncSequence { } /// An asynchronous sequence that omits repeated elements by testing them with a predicate. +@available(AsyncAlgorithms 1.0, *) public struct AsyncRemoveDuplicatesSequence: AsyncSequence { public typealias Element = Base.Element @@ -90,6 +96,7 @@ public struct AsyncRemoveDuplicatesSequence: AsyncSequence } /// An asynchronous sequence that omits repeated elements by testing them with an error-throwing predicate. +@available(AsyncAlgorithms 1.0, *) public struct AsyncThrowingRemoveDuplicatesSequence: AsyncSequence { public typealias Element = Base.Element @@ -144,7 +151,9 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS } } +@available(AsyncAlgorithms 1.0, *) extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift index 49cfac7a..54c9713a 100644 --- a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift @@ -9,10 +9,12 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension Sequence { /// An asynchronous sequence containing the same elements as this sequence, /// but on which operations, such as `map` and `filter`, are /// implemented asynchronously. + @available(AsyncAlgorithms 1.0, *) @inlinable public var async: AsyncSyncSequence { AsyncSyncSequence(self) @@ -27,6 +29,7 @@ extension Sequence { /// /// This functions similarly to `LazySequence` by accessing elements sequentially /// in the iterator's `next()` method. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncSyncSequence: AsyncSequence { public typealias Element = Base.Element @@ -65,6 +68,7 @@ public struct AsyncSyncSequence: AsyncSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncSyncSequence: Sendable where Base: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index 6b5e617d..98ed1682 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) 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, *) diff --git a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index cb22708b..35ea3ef5 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. @@ -23,6 +24,7 @@ extension AsyncSequence { /// the result. If the closure throws an error, the sequence throws. /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. + @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( _ initial: Result, @@ -47,6 +49,7 @@ extension AsyncSequence { /// error, the sequence throws. /// - Returns: An asynchronous sequence of the initial value followed by the reduced /// elements. + @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( into initial: Result, @@ -58,6 +61,7 @@ extension AsyncSequence { /// An asynchronous sequence of applying an error-throwing transform to the element of /// an asynchronous sequence and the previously transformed result. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncThrowingExclusiveReductionsSequence { @usableFromInline @@ -81,8 +85,10 @@ public struct AsyncThrowingExclusiveReductionsSequence Iterator { Iterator(base.makeAsyncIterator(), initial: initial, transform: transform) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 4ba2d81f..3df0a33c 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. @@ -27,6 +28,7 @@ extension AsyncSequence { /// result and the next element in the receiving sequence. If the closure /// throws an error, the sequence throws. /// - Returns: An asynchronous sequence of the reduced elements. + @available(AsyncAlgorithms 1.0, *) @inlinable public func reductions( _ transform: @Sendable @escaping (Element, Element) async throws -> Element @@ -37,6 +39,7 @@ extension AsyncSequence { /// An asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using a given error-throwing closure. +@available(AsyncAlgorithms 1.0, *) @frozen public struct AsyncThrowingInclusiveReductionsSequence { @usableFromInline @@ -52,10 +55,12 @@ public struct AsyncThrowingInclusiveReductionsSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { public typealias Element = Base.Element /// The iterator for an `AsyncThrowingInclusiveReductionsSequence` instance. + @available(AsyncAlgorithms 1.0, *) @frozen public struct Iterator: AsyncIteratorProtocol { @usableFromInline @@ -93,12 +98,14 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(base.makeAsyncIterator(), transform: transform) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index 5361a233..9a0794cc 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence where Self: Sendable { /// Creates an asynchronous sequence that buffers elements. /// @@ -20,6 +21,7 @@ extension AsyncSequence where Self: Sendable { /// /// - Parameter policy: A policy that drives the behaviour of the ``AsyncBufferSequence`` /// - Returns: An asynchronous sequence that buffers elements up to a given limit. + @available(AsyncAlgorithms 1.0, *) public func buffer( policy: AsyncBufferSequencePolicy ) -> AsyncBufferSequence { @@ -28,6 +30,7 @@ extension AsyncSequence where Self: Sendable { } /// A policy dictating the buffering behaviour of an ``AsyncBufferSequence`` +@available(AsyncAlgorithms 1.0, *) public struct AsyncBufferSequencePolicy: Sendable { enum _Policy { case bounded(Int) @@ -69,6 +72,7 @@ public struct AsyncBufferSequencePolicy: Sendable { } /// An `AsyncSequence` that buffers elements in regard to a policy. +@available(AsyncAlgorithms 1.0, *) public struct AsyncBufferSequence: AsyncSequence { enum StorageType { case transparent(Base.AsyncIterator) @@ -125,6 +129,7 @@ public struct AsyncBufferSequence: AsyncSequence } } +@available(AsyncAlgorithms 1.0, *) extension AsyncBufferSequence: Sendable where Base: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index e6a1f324..57821717 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -11,6 +11,7 @@ import DequeModule +@available(AsyncAlgorithms 1.0, *) struct BoundedBufferStateMachine { typealias Element = Base.Element typealias SuspendedProducer = UnsafeContinuation @@ -322,5 +323,7 @@ struct BoundedBufferStateMachine { } } +@available(AsyncAlgorithms 1.0, *) extension BoundedBufferStateMachine: Sendable where Base: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension BoundedBufferStateMachine.State: Sendable where Base: Sendable {} diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index 4ccc1928..ce89cd5d 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) final class BoundedBufferStorage: Sendable where Base: Sendable { private let stateMachine: ManagedCriticalState> diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index 2ba5b45b..d253ed6c 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -11,6 +11,7 @@ import DequeModule +@available(AsyncAlgorithms 1.0, *) struct UnboundedBufferStateMachine { typealias Element = Base.Element typealias SuspendedConsumer = UnsafeContinuation?, Never> @@ -252,5 +253,7 @@ struct UnboundedBufferStateMachine { } } +@available(AsyncAlgorithms 1.0, *) extension UnboundedBufferStateMachine: Sendable where Base: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable {} diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift index b8a6ac24..219b5f50 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) final class UnboundedBufferStorage: Sendable where Base: Sendable { private let stateMachine: ManagedCriticalState> diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index a7c5d384..c94ab57b 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -19,6 +19,7 @@ /// on the `Iterator` is made, or when `finish()` is called from another Task. /// As `finish()` induces a terminal state, there is no more need for a back pressure management. /// This function does not suspend and will finish all the pending iterations. +@available(AsyncAlgorithms 1.0, *) public final class AsyncChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index e84a94c5..622cdc4d 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -18,6 +18,7 @@ /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. /// Those functions do not suspend and will finish all the pending iterations. +@available(AsyncAlgorithms 1.0, *) public final class AsyncThrowingChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index dad46297..ee7c49ef 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -10,6 +10,7 @@ //===----------------------------------------------------------------------===// import OrderedCollections +@available(AsyncAlgorithms 1.0, *) struct ChannelStateMachine: Sendable { private struct SuspendedProducer: Hashable, Sendable { let id: UInt64 diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index 585d9c5f..ad180fac 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -8,6 +8,7 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) struct ChannelStorage: Sendable { private let stateMachine: ManagedCriticalState> private let ids = ManagedCriticalState(0) diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index fab5772e..a37cbb07 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -20,6 +20,7 @@ /// Throws: /// ``combineLatest(_:_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed /// values will be dropped. +@available(AsyncAlgorithms 1.0, *) public func combineLatest< Base1: AsyncSequence, Base2: AsyncSequence @@ -34,6 +35,7 @@ where } /// An `AsyncSequence` that combines the latest values produced from two asynchronous sequences into an asynchronous sequence of tuples. +@available(AsyncAlgorithms 1.0, *) public struct AsyncCombineLatest2Sequence< Base1: AsyncSequence, Base2: AsyncSequence diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift index 3152827f..b0aa29bb 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -20,6 +20,7 @@ /// Throws: /// ``combineLatest(_:_:_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed /// values will be dropped. +@available(AsyncAlgorithms 1.0, *) public func combineLatest< Base1: AsyncSequence, Base2: AsyncSequence, @@ -37,6 +38,7 @@ where } /// An `AsyncSequence` that combines the latest values produced from three asynchronous sequences into an asynchronous sequence of tuples. +@available(AsyncAlgorithms 1.0, *) public struct AsyncCombineLatest3Sequence< Base1: AsyncSequence, Base2: AsyncSequence, diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index aae12b87..2de5c72f 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -12,6 +12,7 @@ import DequeModule /// State machine for combine latest +@available(AsyncAlgorithms 1.0, *) struct CombineLatestStateMachine< Base1: AsyncSequence, Base2: AsyncSequence, diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index 18012832..d1dbecec 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) final class CombineLatestStorage< Base1: AsyncSequence, Base2: AsyncSequence, diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index 286b7aa2..b784cf08 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed by using a specified Clock. diff --git a/Sources/AsyncAlgorithms/Dictionary.swift b/Sources/AsyncAlgorithms/Dictionary.swift index 629a7712..6ed3a71f 100644 --- a/Sources/AsyncAlgorithms/Dictionary.swift +++ b/Sources/AsyncAlgorithms/Dictionary.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension Dictionary { /// Creates a new dictionary from the key-value pairs in the given asynchronous sequence. /// @@ -21,6 +22,7 @@ extension Dictionary { /// - Parameter keysAndValues: An asynchronous sequence of key-value pairs to use for /// the new dictionary. Every key in `keysAndValues` must be unique. /// - Precondition: The sequence must not have duplicate keys. + @available(AsyncAlgorithms 1.0, *) @inlinable public init(uniqueKeysWithValues keysAndValues: S) async rethrows where S.Element == (Key, Value) { @@ -45,6 +47,7 @@ extension Dictionary { /// keys that are encountered. The closure returns the desired value for /// the final dictionary, or throws an error if building the dictionary /// can't proceed. + @available(AsyncAlgorithms 1.0, *) @inlinable public init( _ keysAndValues: S, @@ -72,6 +75,7 @@ extension Dictionary { /// - values: An asynchronous sequence of values to group into a dictionary. /// - keyForValue: A closure that returns a key for each element in /// `values`. + @available(AsyncAlgorithms 1.0, *) @inlinable public init(grouping values: S, by keyForValue: (S.Element) async throws -> Key) async rethrows where Value == [S.Element] { diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index b755950a..3bd52186 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequence { /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting /// the given separator between each element. @@ -30,6 +31,7 @@ extension AsyncSequence { /// - 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. + @available(AsyncAlgorithms 1.0, *) @inlinable public func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { AsyncInterspersedSequence(self, every: every, separator: separator) @@ -55,6 +57,7 @@ extension AsyncSequence { /// - 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. + @available(AsyncAlgorithms 1.0, *) @inlinable public func interspersed( every: Int = 1, @@ -83,6 +86,7 @@ extension AsyncSequence { /// - 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. + @available(AsyncAlgorithms 1.0, *) @inlinable public func interspersed( every: Int = 1, @@ -111,6 +115,7 @@ extension AsyncSequence { /// - 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. + @available(AsyncAlgorithms 1.0, *) @inlinable public func interspersed( every: Int = 1, @@ -139,6 +144,7 @@ extension AsyncSequence { /// - 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. + @available(AsyncAlgorithms 1.0, *) @inlinable public func interspersed( every: Int = 1, @@ -150,6 +156,7 @@ extension AsyncSequence { /// An asynchronous sequence that presents the elements of a base asynchronous sequence of /// elements with a separator between each of those elements. +@available(AsyncAlgorithms 1.0, *) public struct AsyncInterspersedSequence { @usableFromInline internal enum Separator { @@ -192,10 +199,12 @@ public struct AsyncInterspersedSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncInterspersedSequence: AsyncSequence { public typealias Element = Base.Element /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + @available(AsyncAlgorithms 1.0, *) public struct Iterator: AsyncIteratorProtocol { @usableFromInline internal enum State { @@ -293,6 +302,7 @@ extension AsyncInterspersedSequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) @@ -301,6 +311,7 @@ extension AsyncInterspersedSequence: AsyncSequence { /// An asynchronous sequence that presents the elements of a base asynchronous sequence of /// elements with a separator between each of those elements. +@available(AsyncAlgorithms 1.0, *) public struct AsyncThrowingInterspersedSequence { @usableFromInline internal enum Separator { @@ -334,10 +345,12 @@ public struct AsyncThrowingInterspersedSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingInterspersedSequence: AsyncSequence { public typealias Element = Base.Element /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + @available(AsyncAlgorithms 1.0, *) public struct Iterator: AsyncIteratorProtocol { @usableFromInline internal enum State { @@ -432,16 +445,21 @@ extension AsyncThrowingInterspersedSequence: AsyncSequence { } } + @available(AsyncAlgorithms 1.0, *) @inlinable public func makeAsyncIterator() -> Iterator { Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) } } +@available(AsyncAlgorithms 1.0, *) extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension AsyncInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +@available(AsyncAlgorithms 1.0, *) extension AsyncThrowingInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index a705c4a9..482db1bf 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -12,6 +12,7 @@ import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences +@available(AsyncAlgorithms 1.0, *) public func merge( _ base1: Base1, _ base2: Base2 @@ -26,6 +27,7 @@ where } /// An `AsyncSequence` that takes two upstream `AsyncSequence`s and combines their elements. +@available(AsyncAlgorithms 1.0, *) public struct AsyncMerge2Sequence< Base1: AsyncSequence, Base2: AsyncSequence @@ -55,7 +57,9 @@ where } } +@available(AsyncAlgorithms 1.0, *) extension AsyncMerge2Sequence: AsyncSequence { + @available(AsyncAlgorithms 1.0, *) public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, @@ -66,7 +70,9 @@ extension AsyncMerge2Sequence: AsyncSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncMerge2Sequence { + @available(AsyncAlgorithms 1.0, *) public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index f4a15edf..ec1960fb 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -12,6 +12,7 @@ import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences +@available(AsyncAlgorithms 1.0, *) public func merge< Base1: AsyncSequence, Base2: AsyncSequence, @@ -29,6 +30,7 @@ where } /// An `AsyncSequence` that takes three upstream `AsyncSequence`s and combines their elements. +@available(AsyncAlgorithms 1.0, *) public struct AsyncMerge3Sequence< Base1: AsyncSequence, Base2: AsyncSequence, @@ -65,7 +67,9 @@ where } } +@available(AsyncAlgorithms 1.0, *) extension AsyncMerge3Sequence: AsyncSequence { + @available(AsyncAlgorithms 1.0, *) public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, @@ -76,7 +80,9 @@ extension AsyncMerge3Sequence: AsyncSequence { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncMerge3Sequence { + @available(AsyncAlgorithms 1.0, *) public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index 24b574ec..a32c59c3 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -15,6 +15,7 @@ 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. +@available(AsyncAlgorithms 1.0, *) struct MergeStateMachine< Base1: AsyncSequence, Base2: AsyncSequence, diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index c7332dda..64051fa9 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) final class MergeStorage< Base1: AsyncSequence, Base2: AsyncSequence, diff --git a/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift b/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift index fedcf3bd..dcaf2e0d 100644 --- a/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift +++ b/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension RangeReplaceableCollection { /// Creates a new instance of a collection containing the elements of an asynchronous sequence. /// diff --git a/Sources/AsyncAlgorithms/SetAlgebra.swift b/Sources/AsyncAlgorithms/SetAlgebra.swift index 14f885db..97392954 100644 --- a/Sources/AsyncAlgorithms/SetAlgebra.swift +++ b/Sources/AsyncAlgorithms/SetAlgebra.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension SetAlgebra { /// Creates a new set from an asynchronous sequence of items. /// diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index fb24e88b..6e0550b9 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -11,6 +11,7 @@ /// Creates an asynchronous sequence that concurrently awaits values from two `AsyncSequence` types /// and emits a tuple of the values. +@available(AsyncAlgorithms 1.0, *) public func zip( _ base1: Base1, _ base2: Base2 @@ -20,6 +21,7 @@ public func zip( /// An asynchronous sequence that concurrently awaits values from two `AsyncSequence` types /// and emits a tuple of the values. +@available(AsyncAlgorithms 1.0, *) public struct AsyncZip2Sequence: AsyncSequence, Sendable where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 68c261a2..2c6bc9fc 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -11,6 +11,7 @@ /// Creates an asynchronous sequence that concurrently awaits values from three `AsyncSequence` types /// and emits a tuple of the values. +@available(AsyncAlgorithms 1.0, *) public func zip( _ base1: Base1, _ base2: Base2, @@ -21,6 +22,7 @@ public func zip: AsyncSequence, Sendable where diff --git a/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift b/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift index 9c634d47..cb01a05c 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStateMachine.swift @@ -10,6 +10,7 @@ //===----------------------------------------------------------------------===// /// State machine for zip +@available(AsyncAlgorithms 1.0, *) struct ZipStateMachine< Base1: AsyncSequence, Base2: AsyncSequence, diff --git a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift index 551d4ada..786a6844 100644 --- a/Sources/AsyncAlgorithms/Zip/ZipStorage.swift +++ b/Sources/AsyncAlgorithms/Zip/ZipStorage.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) final class ZipStorage: Sendable where Base1: Sendable, diff --git a/Sources/AsyncAlgorithms_XCTest/ValidationTest.swift b/Sources/AsyncAlgorithms_XCTest/ValidationTest.swift index 7f1b326c..fa15e792 100644 --- a/Sources/AsyncAlgorithms_XCTest/ValidationTest.swift +++ b/Sources/AsyncAlgorithms_XCTest/ValidationTest.swift @@ -33,6 +33,7 @@ extension XCTestCase { #endif } + @available(AsyncAlgorithms 1.0, *) func validate( theme: Theme, expectedFailures: Set, @@ -77,6 +78,7 @@ extension XCTestCase { } } + @available(AsyncAlgorithms 1.0, *) func validate( expectedFailures: Set, @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, @@ -86,6 +88,7 @@ extension XCTestCase { validate(theme: .ascii, expectedFailures: expectedFailures, build, file: file, line: line) } + @available(AsyncAlgorithms 1.0, *) public func validate( theme: Theme, @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, @@ -95,6 +98,7 @@ extension XCTestCase { validate(theme: theme, expectedFailures: [], build, file: file, line: line) } + @available(AsyncAlgorithms 1.0, *) public func validate( @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, file: StaticString = #file, diff --git a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift index 88d74045..1cf15192 100644 --- a/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift +++ b/Sources/AsyncSequenceValidation/AsyncSequenceValidationDiagram.swift @@ -12,6 +12,7 @@ import _CAsyncSequenceValidationSupport @resultBuilder +@available(AsyncAlgorithms 1.0, *) public struct AsyncSequenceValidationDiagram: Sendable { public struct Component { var component: T diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index d9ebab3d..33ad9d0c 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -11,6 +11,7 @@ import AsyncAlgorithms +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { public struct Clock { let queue: WorkQueue @@ -33,6 +34,7 @@ public protocol TestInstant: Equatable { associatedtype Duration } +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram.Clock { public struct Step: DurationProtocol, Hashable, CustomStringConvertible { internal var rawValue: Int @@ -127,16 +129,20 @@ extension AsyncSequenceValidationDiagram.Clock { } } +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram.Clock.Instant: TestInstant {} @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram.Clock.Instant: InstantProtocol {} +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram.Clock: TestClock {} @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncSequenceValidationDiagram.Clock: Clock {} // placeholders to avoid warnings +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram.Clock.Instant: Hashable {} +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram.Clock.Instant: Comparable {} diff --git a/Sources/AsyncSequenceValidation/Event.swift b/Sources/AsyncSequenceValidation/Event.swift index a0fe887a..24fcb5e6 100644 --- a/Sources/AsyncSequenceValidation/Event.swift +++ b/Sources/AsyncSequenceValidation/Event.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { struct Failure: Error, Equatable {} diff --git a/Sources/AsyncSequenceValidation/Expectation.swift b/Sources/AsyncSequenceValidation/Expectation.swift index 89040ef2..627b6e3e 100644 --- a/Sources/AsyncSequenceValidation/Expectation.swift +++ b/Sources/AsyncSequenceValidation/Expectation.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { public struct ExpectationResult: Sendable { public struct Event: Sendable { diff --git a/Sources/AsyncSequenceValidation/Input.swift b/Sources/AsyncSequenceValidation/Input.swift index ad587751..ebf1b246 100644 --- a/Sources/AsyncSequenceValidation/Input.swift +++ b/Sources/AsyncSequenceValidation/Input.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { public struct Specification: Sendable { public let specification: String diff --git a/Sources/AsyncSequenceValidation/Job.swift b/Sources/AsyncSequenceValidation/Job.swift index 0a081870..7f57d72f 100644 --- a/Sources/AsyncSequenceValidation/Job.swift +++ b/Sources/AsyncSequenceValidation/Job.swift @@ -11,6 +11,7 @@ import _CAsyncSequenceValidationSupport +@available(AsyncAlgorithms 1.0, *) struct Job: Hashable, @unchecked Sendable { let job: JobRef diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 80ad44cd..bd7df453 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -26,16 +26,19 @@ import Bionic #endif #if canImport(Darwin) +@available(AsyncAlgorithms 1.0, *) func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return nil } #elseif (canImport(Glibc) && !os(Android)) || canImport(Musl) +@available(AsyncAlgorithms 1.0, *) func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw!).takeRetainedValue().run() return nil } #elseif os(Android) +@available(AsyncAlgorithms 1.0, *) func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return UnsafeMutableRawPointer(bitPattern: 0xdeadbee)! @@ -44,6 +47,7 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { #error("TODO: Port TaskDriver threading to windows") #endif +@available(AsyncAlgorithms 1.0, *) final class TaskDriver { let work: (TaskDriver) -> Void let queue: WorkQueue diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index b19f6e5a..c096859c 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -13,12 +13,14 @@ import _CAsyncSequenceValidationSupport import AsyncAlgorithms @_silgen_name("swift_job_run") +@available(AsyncAlgorithms 1.0, *) @usableFromInline internal func _swiftJobRun( _ job: UnownedJob, _ executor: UnownedSerialExecutor ) +@available(AsyncAlgorithms 1.0, *) public protocol AsyncSequenceValidationTest: Sendable { var inputs: [AsyncSequenceValidationDiagram.Specification] { get } var output: AsyncSequenceValidationDiagram.Specification { get } @@ -31,6 +33,7 @@ public protocol AsyncSequenceValidationTest: Sendable { ) async throws } +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { struct Test: AsyncSequenceValidationTest, @unchecked Sendable where Operation.Element == String { diff --git a/Sources/AsyncSequenceValidation/Theme.swift b/Sources/AsyncSequenceValidation/Theme.swift index fc20eeea..0fbbdb5a 100644 --- a/Sources/AsyncSequenceValidation/Theme.swift +++ b/Sources/AsyncSequenceValidation/Theme.swift @@ -9,18 +9,21 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) public protocol AsyncSequenceValidationTheme { func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token func description(for token: AsyncSequenceValidationDiagram.Token) -> String } +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationTheme where Self == AsyncSequenceValidationDiagram.ASCIITheme { public static var ascii: AsyncSequenceValidationDiagram.ASCIITheme { return AsyncSequenceValidationDiagram.ASCIITheme() } } +@available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { public enum Token: Sendable { case step diff --git a/Sources/AsyncSequenceValidation/WorkQueue.swift b/Sources/AsyncSequenceValidation/WorkQueue.swift index 82d56e24..db794e3b 100644 --- a/Sources/AsyncSequenceValidation/WorkQueue.swift +++ b/Sources/AsyncSequenceValidation/WorkQueue.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +@available(AsyncAlgorithms 1.0, *) struct WorkQueue: Sendable { enum Item: CustomStringConvertible, Comparable { case blocked(Token, AsyncSequenceValidationDiagram.Clock.Instant, UnsafeContinuation) From 3997ce3255fcc40d1dd143e1bf73378413a06224 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 2 Jul 2025 17:09:32 +0200 Subject: [PATCH 138/149] Remove the DocC plugin as a dependency (#354) When DocC was introduced the DocC plugin was useful for building documentation locally. Nowadays both Xcode and VSCode have built-in support to generate this without the need for the plugin. This PR removes the direct dependency on the plugin. --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4a99d5f8..c97097b4 100644 --- a/Package.swift +++ b/Package.swift @@ -77,7 +77,6 @@ let package = Package( if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ] } else { package.dependencies += [ From 6dcb3acddde5bdcdf923eb480c2cd0bb57cfc737 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Mon, 11 Aug 2025 14:51:05 -0700 Subject: [PATCH 139/149] Simplify the version definitions and add a new definition for 1.1 (#359) --- Package.swift | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/Package.swift b/Package.swift index c97097b4..b34c676a 100644 --- a/Package.swift +++ b/Package.swift @@ -4,34 +4,11 @@ import PackageDescription import CompilerPluginSupport // Availability Macros -let availabilityTags = [Availability("AsyncAlgorithms")] -let versionNumbers = ["1.0"] -// Availability Macro Utilities -enum OSAvailability: String { - // This should match the package's deployment target - case initialIntroduction = "macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" - case pending = "macOS 9999, iOS 9999, tvOS 9999, watchOS 9999" - // Use 10000 for future availability to avoid compiler magic around - // the 9999 version number but ensure it is greater than 9999 - case future = "macOS 10000, iOS 10000, tvOS 10000, watchOS 10000" -} - -struct Availability { - let name: String - let osAvailability: OSAvailability - - init(_ name: String, availability: OSAvailability = .initialIntroduction) { - self.name = name - self.osAvailability = availability - } -} - -let availabilityMacros: [SwiftSetting] = versionNumbers.flatMap { version in - availabilityTags.map { - .enableExperimentalFeature("AvailabilityMacro=\($0.name) \(version):\($0.osAvailability.rawValue)") - } -} +let availabilityMacros: [SwiftSetting] = [ + .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0"), + .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), +] let package = Package( name: "swift-async-algorithms", From c5373933395acd89d490680e983d589963733d3a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 2 Sep 2025 16:10:59 +0100 Subject: [PATCH 140/149] Add `enable_wasm_sdk_build: true` to `pull_request.yml` (#363) `swift-async-algorithms` should be built for Wasm as one of the officially supported platforms on CI to prevent possible future regressions. --- .github/workflows/pull_request.yml | 4 ++++ Sources/AsyncAlgorithms/Locking.swift | 12 +++++++----- Sources/AsyncSequenceValidation/TaskDriver.swift | 10 ++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1ed694ad..855cedbb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,6 +8,10 @@ jobs: tests: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_wasm_sdk_build: true + wasm_sdk_build_command: swift build -Xcc -D_WASI_EMULATED_PTHREAD + soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index d87ef76d..7376c68e 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -19,6 +19,8 @@ import Musl import WinSDK #elseif canImport(Bionic) import Bionic +#elseif canImport(wasi_pthread) +import wasi_pthread #else #error("Unsupported platform") #endif @@ -26,7 +28,7 @@ import Bionic internal struct Lock { #if canImport(Darwin) typealias Primitive = os_unfair_lock - #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK @@ -44,7 +46,7 @@ internal struct Lock { fileprivate static func initialize(_ platformLock: PlatformLock) { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) - #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) let result = pthread_mutex_init(platformLock, nil) precondition(result == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) @@ -55,7 +57,7 @@ internal struct Lock { } fileprivate static func deinitialize(_ platformLock: PlatformLock) { - #if canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #if canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) let result = pthread_mutex_destroy(platformLock) precondition(result == 0, "pthread_mutex_destroy failed") #endif @@ -65,7 +67,7 @@ internal struct Lock { fileprivate static func lock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_lock(platformLock) - #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) @@ -77,7 +79,7 @@ internal struct Lock { fileprivate static func unlock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) - #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) let result = pthread_mutex_unlock(platformLock) precondition(result == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index bd7df453..106274cf 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -19,6 +19,8 @@ import Glibc import Musl #elseif canImport(Bionic) import Bionic +#elseif canImport(wasi_pthread) +import wasi_pthread #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") #else @@ -31,7 +33,7 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw).takeRetainedValue().run() return nil } -#elseif (canImport(Glibc) && !os(Android)) || canImport(Musl) +#elseif (canImport(Glibc) && !os(Android)) || canImport(Musl) || canImport(wasi_pthread) @available(AsyncAlgorithms 1.0, *) func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { Unmanaged.fromOpaque(raw!).takeRetainedValue().run() @@ -51,7 +53,7 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { final class TaskDriver { let work: (TaskDriver) -> Void let queue: WorkQueue - #if canImport(Darwin) + #if canImport(Darwin) || canImport(wasi_pthread) var thread: pthread_t? #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) var thread = pthread_t() @@ -65,7 +67,7 @@ final class TaskDriver { } func start() { - #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) pthread_create( &thread, nil, @@ -85,7 +87,7 @@ final class TaskDriver { } func join() { - #if canImport(Darwin) + #if canImport(Darwin) || canImport(wasi_pthread) pthread_join(thread!, nil) #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) pthread_join(thread, nil) From 18e23984811a5ca9673e8aa3a6ef37b17d9c5e82 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Mon, 8 Sep 2025 09:38:39 -0700 Subject: [PATCH 141/149] Update the project to address concurrency failures for 6.2 build modes. (#362) * Correct build failures while updating to 6.2 swift build modes * Add a package definition for 5.8 builds * Add some brief explanation for the alteration of the Element in the buffer to an UnsafeTransfer * Fix broken merge for wasi threads --- Package.swift | 2 +- Package@swift-5.8.swift | 62 +++++++++++++++++++ .../Buffer/AsyncBufferSequence.swift | 12 +++- .../Buffer/BoundedBufferStateMachine.swift | 10 +-- .../Buffer/BoundedBufferStorage.swift | 15 ++--- .../Buffer/UnboundedBufferStateMachine.swift | 6 +- .../Buffer/UnboundedBufferStorage.swift | 14 ++--- Sources/AsyncSequenceValidation/Clock.swift | 2 +- .../AsyncSequenceValidation/TaskDriver.swift | 10 +-- Sources/AsyncSequenceValidation/Test.swift | 10 +-- .../_CAsyncSequenceValidationSupport.h | 4 +- 11 files changed, 110 insertions(+), 37 deletions(-) create mode 100644 Package@swift-5.8.swift diff --git a/Package.swift b/Package.swift index b34c676a..c82f577f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 6.2 import PackageDescription import CompilerPluginSupport diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift new file mode 100644 index 00000000..b34c676a --- /dev/null +++ b/Package@swift-5.8.swift @@ -0,0 +1,62 @@ +// swift-tools-version: 5.8 + +import PackageDescription +import CompilerPluginSupport + +// Availability Macros + +let availabilityMacros: [SwiftSetting] = [ + .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0"), + .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), +] + +let package = Package( + name: "swift-async-algorithms", + products: [ + .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]) + ], + targets: [ + .target( + name: "AsyncAlgorithms", + dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections"), + ], + swiftSettings: availabilityMacros + [ + .enableExperimentalFeature("StrictConcurrency=complete") + ] + ), + .target( + name: "AsyncSequenceValidation", + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], + swiftSettings: availabilityMacros + [ + .enableExperimentalFeature("StrictConcurrency=complete") + ] + ), + .systemLibrary(name: "_CAsyncSequenceValidationSupport"), + .target( + name: "AsyncAlgorithms_XCTest", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], + swiftSettings: availabilityMacros + [ + .enableExperimentalFeature("StrictConcurrency=complete") + ] + ), + .testTarget( + name: "AsyncAlgorithmsTests", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], + swiftSettings: availabilityMacros + [ + .enableExperimentalFeature("StrictConcurrency=complete") + ] + ), + ] +) + +if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), + ] +} else { + package.dependencies += [ + .package(path: "../swift-collections") + ] +} diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index 9a0794cc..adbee9f7 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -74,6 +74,14 @@ public struct AsyncBufferSequencePolicy: Sendable { /// An `AsyncSequence` that buffers elements in regard to a policy. @available(AsyncAlgorithms 1.0, *) public struct AsyncBufferSequence: AsyncSequence { + // Internal implementation note: + // This type origianlly had no requirement that the element is actually Sendable. However, + // that is technically an implementation detail hole in the safety of the system, it needs + // to specify that the element is actually Sendable since the draining mechanism passes + // through the isolation that is in nature sending but cannot be marked as such for the + // isolated next method. + // In practice the users of this type are safe from isolation crossing since the Element + // is as sendable as it is required by the base sequences the buffer is constructed from. enum StorageType { case transparent(Base.AsyncIterator) case bounded(storage: BoundedBufferStorage) @@ -121,9 +129,9 @@ public struct AsyncBufferSequence: AsyncSequence self.storageType = .transparent(iterator) return element case .bounded(let storage): - return try await storage.next()?._rethrowGet() + return try await storage.next().wrapped?._rethrowGet() case .unbounded(let storage): - return try await storage.next()?._rethrowGet() + return try await storage.next().wrapped?._rethrowGet() } } } diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index 57821717..10cb4079 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -15,7 +15,7 @@ import DequeModule struct BoundedBufferStateMachine { typealias Element = Base.Element typealias SuspendedProducer = UnsafeContinuation - typealias SuspendedConsumer = UnsafeContinuation?, Never> + typealias SuspendedConsumer = UnsafeContinuation?>, Never> // We are using UnsafeTransfer here since we have to get the elements from the task // into the consumer task. This is a transfer but we cannot prove this to the compiler at this point @@ -137,7 +137,7 @@ struct BoundedBufferStateMachine { enum ElementProducedAction { case none - case resumeConsumer(continuation: SuspendedConsumer, result: Result) + case resumeConsumer(continuation: SuspendedConsumer, result: UnsafeTransfer?>) } mutating func elementProduced(element: Element) -> ElementProducedAction { @@ -161,7 +161,7 @@ struct BoundedBufferStateMachine { // 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)) + return .resumeConsumer(continuation: suspendedConsumer, result: UnsafeTransfer(.success(element))) case .buffering(_, _, .some, _): preconditionFailure("Invalid state. There should not be a suspended producer.") @@ -177,7 +177,7 @@ struct BoundedBufferStateMachine { enum FinishAction { case none case resumeConsumer( - continuation: UnsafeContinuation?, Never>? + continuation: UnsafeContinuation?>, Never>? ) } @@ -295,7 +295,7 @@ struct BoundedBufferStateMachine { case resumeProducerAndConsumer( task: Task, producerContinuation: UnsafeContinuation?, - consumerContinuation: UnsafeContinuation?, Never>? + consumerContinuation: UnsafeContinuation?>, Never>? ) } diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index ce89cd5d..10eaa923 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -17,7 +17,7 @@ final class BoundedBufferStorage: Sendable where Base: Send self.stateMachine = ManagedCriticalState(BoundedBufferStateMachine(base: base, limit: limit)) } - func next() async -> Result? { + func next() async -> UnsafeTransfer?> { return await withTaskCancellationHandler { let action: BoundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in @@ -45,14 +45,14 @@ final class BoundedBufferStorage: Sendable where Base: Send case .returnResult(let producerContinuation, let result): producerContinuation?.resume() - return result + return UnsafeTransfer(result) case .none: break } return await withUnsafeContinuation { - (continuation: UnsafeContinuation?, Never>) in + (continuation: UnsafeContinuation?>, Never>) in let action = self.stateMachine.withCriticalRegion { stateMachine in stateMachine.nextSuspended(continuation: continuation) } @@ -61,7 +61,7 @@ final class BoundedBufferStorage: Sendable where Base: Send break case .returnResult(let producerContinuation, let result): producerContinuation?.resume() - continuation.resume(returning: result) + continuation.resume(returning: UnsafeTransfer(result)) } } } onCancel: { @@ -109,6 +109,7 @@ final class BoundedBufferStorage: Sendable where Base: Send case .none: break case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } @@ -120,7 +121,7 @@ final class BoundedBufferStorage: Sendable where Base: Send case .none: break case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + continuation?.resume(returning: UnsafeTransfer(nil)) } } catch { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -130,7 +131,7 @@ final class BoundedBufferStorage: Sendable where Base: Send case .none: break case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) + continuation?.resume(returning: UnsafeTransfer(Result.failure(error))) } } } @@ -148,7 +149,7 @@ final class BoundedBufferStorage: Sendable where Base: Send case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): task.cancel() producerContinuation?.resume() - consumerContinuation?.resume(returning: nil) + consumerContinuation?.resume(returning: UnsafeTransfer(nil)) } } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index d253ed6c..67464ad6 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -14,7 +14,7 @@ import DequeModule @available(AsyncAlgorithms 1.0, *) struct UnboundedBufferStateMachine { typealias Element = Base.Element - typealias SuspendedConsumer = UnsafeContinuation?, Never> + typealias SuspendedConsumer = UnsafeContinuation?>, Never> enum Policy { case unlimited @@ -73,7 +73,7 @@ struct UnboundedBufferStateMachine { case none case resumeConsumer( continuation: SuspendedConsumer, - result: Result + result: UnsafeTransfer?> ) } @@ -108,7 +108,7 @@ struct UnboundedBufferStateMachine { self.state = .buffering(task: task, buffer: buffer, suspendedConsumer: nil) return .resumeConsumer( continuation: suspendedConsumer, - result: .success(element) + result: UnsafeTransfer(.success(element)) ) case .modifying: diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift index 219b5f50..a3f5aac6 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -17,7 +17,7 @@ final class UnboundedBufferStorage: Sendable where Base: Se self.stateMachine = ManagedCriticalState(UnboundedBufferStateMachine(base: base, policy: policy)) } - func next() async -> Result? { + func next() async -> UnsafeTransfer?> { return await withTaskCancellationHandler { let action: UnboundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { @@ -42,13 +42,13 @@ final class UnboundedBufferStorage: Sendable where Base: Se case .suspend: break case .returnResult(let result): - return result + return UnsafeTransfer(result) case .none: break } return await withUnsafeContinuation { - (continuation: UnsafeContinuation?, Never>) in + (continuation: UnsafeContinuation?>, Never>) in let action = self.stateMachine.withCriticalRegion { stateMachine in stateMachine.nextSuspended(continuation: continuation) } @@ -56,7 +56,7 @@ final class UnboundedBufferStorage: Sendable where Base: Se case .none: break case .resumeConsumer(let result): - continuation.resume(returning: result) + continuation.resume(returning: UnsafeTransfer(result)) } } } onCancel: { @@ -89,7 +89,7 @@ final class UnboundedBufferStorage: Sendable where Base: Se case .none: break case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + continuation?.resume(returning: UnsafeTransfer(nil)) } } catch { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -99,7 +99,7 @@ final class UnboundedBufferStorage: Sendable where Base: Se case .none: break case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) + continuation?.resume(returning: UnsafeTransfer(Result.failure(error))) } } } @@ -116,7 +116,7 @@ final class UnboundedBufferStorage: Sendable where Base: Se break case .resumeConsumer(let task, let continuation): task.cancel() - continuation?.resume(returning: nil) + continuation?.resume(returning: UnsafeTransfer(nil)) } } diff --git a/Sources/AsyncSequenceValidation/Clock.swift b/Sources/AsyncSequenceValidation/Clock.swift index 33ad9d0c..492e0bbc 100644 --- a/Sources/AsyncSequenceValidation/Clock.swift +++ b/Sources/AsyncSequenceValidation/Clock.swift @@ -13,7 +13,7 @@ import AsyncAlgorithms @available(AsyncAlgorithms 1.0, *) extension AsyncSequenceValidationDiagram { - public struct Clock { + public struct Clock: Sendable { let queue: WorkQueue init(queue: WorkQueue) { diff --git a/Sources/AsyncSequenceValidation/TaskDriver.swift b/Sources/AsyncSequenceValidation/TaskDriver.swift index 106274cf..336164e4 100644 --- a/Sources/AsyncSequenceValidation/TaskDriver.swift +++ b/Sources/AsyncSequenceValidation/TaskDriver.swift @@ -50,18 +50,18 @@ func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { #endif @available(AsyncAlgorithms 1.0, *) -final class TaskDriver { - let work: (TaskDriver) -> Void +final class TaskDriver: Sendable { + let work: @Sendable (TaskDriver) -> Void let queue: WorkQueue #if canImport(Darwin) || canImport(wasi_pthread) - var thread: pthread_t? + nonisolated(unsafe) var thread: pthread_t? #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) - var thread = pthread_t() + nonisolated(unsafe) var thread = pthread_t() #elseif canImport(WinSDK) #error("TODO: Port TaskDriver threading to windows") #endif - init(queue: WorkQueue, _ work: @escaping (TaskDriver) -> Void) { + init(queue: WorkQueue, _ work: @Sendable @escaping (TaskDriver) -> Void) { self.queue = queue self.work = work } diff --git a/Sources/AsyncSequenceValidation/Test.swift b/Sources/AsyncSequenceValidation/Test.swift index c096859c..69cde581 100644 --- a/Sources/AsyncSequenceValidation/Test.swift +++ b/Sources/AsyncSequenceValidation/Test.swift @@ -122,7 +122,7 @@ extension AsyncSequenceValidationDiagram { } } - private static let _executor: AnyObject = { + private static let _executor: AnyObject & Sendable = { guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else { return ClockExecutor_Pre5_9() } @@ -134,13 +134,13 @@ extension AsyncSequenceValidationDiagram { } #endif - static var clock: Clock? + nonisolated(unsafe) static var clock: Clock? - static var driver: TaskDriver? + nonisolated(unsafe) static var driver: TaskDriver? - static var currentJob: Job? + nonisolated(unsafe) static var currentJob: Job? - static var specificationFailures = [ExpectationFailure]() + nonisolated(unsafe) static var specificationFailures = [ExpectationFailure]() } enum ActualResult { diff --git a/Sources/_CAsyncSequenceValidationSupport/_CAsyncSequenceValidationSupport.h b/Sources/_CAsyncSequenceValidationSupport/_CAsyncSequenceValidationSupport.h index 9780327c..df0ccecd 100644 --- a/Sources/_CAsyncSequenceValidationSupport/_CAsyncSequenceValidationSupport.h +++ b/Sources/_CAsyncSequenceValidationSupport/_CAsyncSequenceValidationSupport.h @@ -241,8 +241,10 @@ typedef struct _Job* JobRef; +#define NONISOLATED_UNSAFE __attribute__((swift_attr("nonisolated(unsafe)"))) + typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)(JobRef _Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -SWIFT_CC(swift) void (* _Nullable swift_task_enqueueGlobal_hook)( +NONISOLATED_UNSAFE SWIFT_CC(swift) void (* _Nullable swift_task_enqueueGlobal_hook)( JobRef _Nonnull job, swift_task_enqueueGlobal_original _Nonnull original); From ca38935fc0bc60fd4524890961653173985ed7ee Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Mon, 8 Sep 2025 16:23:41 -0700 Subject: [PATCH 142/149] Clear up CI issues and testing failures (#365) * Conditionalize the testing targets for non-windows targets to avoid the TaskDriver requirements * Some driveby formatting fixes and remove the freebsd target since that is not supported in swiftpm yet * Rework validation import conditions to dependency conditionals * Exclude 5.9 workflows and enable macOS testing scheme for CI * Additional ifdefing out non windows targets for sequence diagram testing and more formatting fixes --- .github/workflows/pull_request.yml | 2 + Package.swift | 46 ++++++++++++++++--- Package@swift-5.8.swift | 44 ++++++++++++++++-- .../Buffer/AsyncBufferSequence.swift | 2 +- .../Buffer/BoundedBufferStorage.swift | 1 - Tests/AsyncAlgorithmsTests/TestBuffer.swift | 4 ++ Tests/AsyncAlgorithmsTests/TestChunk.swift | 5 ++ Tests/AsyncAlgorithmsTests/TestDebounce.swift | 2 + Tests/AsyncAlgorithmsTests/TestMerge.swift | 4 ++ Tests/AsyncAlgorithmsTests/TestThrottle.swift | 2 + Tests/AsyncAlgorithmsTests/TestTimer.swift | 5 ++ .../TestValidationTests.swift | 5 ++ 12 files changed, 110 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 855cedbb..115ea4e4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,6 +9,8 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: + linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]" + windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]" enable_wasm_sdk_build: true wasm_sdk_build_command: swift build -Xcc -D_WASI_EMULATED_PTHREAD diff --git a/Package.swift b/Package.swift index c82f577f..45c5abca 100644 --- a/Package.swift +++ b/Package.swift @@ -3,11 +3,13 @@ import PackageDescription import CompilerPluginSupport -// Availability Macros - let availabilityMacros: [SwiftSetting] = [ - .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0"), - .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), + .enableExperimentalFeature( + "AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" + ), + .enableExperimentalFeature( + "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" + ), ] let package = Package( @@ -43,7 +45,39 @@ let package = Package( ), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], + dependencies: [ + .target(name: "AsyncAlgorithms"), + .target( + name: "AsyncSequenceValidation", + condition: .when(platforms: [ + .macOS, + .iOS, + .tvOS, + .watchOS, + .visionOS, + .macCatalyst, + .android, + .linux, + .openbsd, + .wasi, + ]) + ), + .target( + name: "AsyncAlgorithms_XCTest", + condition: .when(platforms: [ + .macOS, + .iOS, + .tvOS, + .watchOS, + .visionOS, + .macCatalyst, + .android, + .linux, + .openbsd, + .wasi, + ]) + ), + ], swiftSettings: availabilityMacros + [ .enableExperimentalFeature("StrictConcurrency=complete") ] @@ -53,7 +87,7 @@ let package = Package( if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0") ] } else { package.dependencies += [ diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index b34c676a..8161e475 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -6,8 +6,12 @@ import CompilerPluginSupport // Availability Macros let availabilityMacros: [SwiftSetting] = [ - .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0"), - .enableExperimentalFeature("AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0"), + .enableExperimentalFeature( + "AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" + ), + .enableExperimentalFeature( + "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" + ), ] let package = Package( @@ -43,7 +47,39 @@ let package = Package( ), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], + dependencies: [ + .target(name: "AsyncAlgorithms"), + .target( + name: "AsyncSequenceValidation", + condition: .when(platforms: [ + .macOS, + .iOS, + .tvOS, + .watchOS, + .visionOS, + .macCatalyst, + .android, + .linux, + .openbsd, + .wasi, + ]) + ), + .target( + name: "AsyncAlgorithms_XCTest", + condition: .when(platforms: [ + .macOS, + .iOS, + .tvOS, + .watchOS, + .visionOS, + .macCatalyst, + .android, + .linux, + .openbsd, + .wasi, + ]) + ), + ], swiftSettings: availabilityMacros + [ .enableExperimentalFeature("StrictConcurrency=complete") ] @@ -53,7 +89,7 @@ let package = Package( if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0") ] } else { package.dependencies += [ diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index adbee9f7..a966405e 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -80,7 +80,7 @@ public struct AsyncBufferSequence: AsyncSequence // to specify that the element is actually Sendable since the draining mechanism passes // through the isolation that is in nature sending but cannot be marked as such for the // isolated next method. - // In practice the users of this type are safe from isolation crossing since the Element + // In practice the users of this type are safe from isolation crossing since the Element // is as sendable as it is required by the base sequences the buffer is constructed from. enum StorageType { case transparent(Base.AsyncIterator) diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index 10eaa923..d9ae62ad 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -109,7 +109,6 @@ final class BoundedBufferStorage: Sendable where Base: Send case .none: break case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) } } diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index 83026953..42dd2f1a 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -108,6 +108,7 @@ final class TestBuffer: XCTestCase { XCTAssertNil(pastFailure) } + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) func test_given_a_base_sequence_when_bufferingOldest_then_the_policy_is_applied() async { validate { "X-12- 34- 5 |" @@ -171,6 +172,7 @@ final class TestBuffer: XCTestCase { "X,,,,,,[45^]" } } + #endif func test_given_a_buffered_with_unbounded_sequence_when_cancelling_consumer_then_the_iteration_finishes_and_the_base_is_cancelled() @@ -325,6 +327,7 @@ final class TestBuffer: XCTestCase { await fulfillment(of: [finished], timeout: 1.0) } + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) func test_given_a_base_sequence_when_bounded_with_limit_0_then_the_policy_is_transparent() async { validate { "X-12- 34 -5|" @@ -332,4 +335,5 @@ final class TestBuffer: XCTestCase { "X-12- 34 -5|" } } + #endif } diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 8cd5e8e8..b9a5956b 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -10,6 +10,9 @@ //===----------------------------------------------------------------------===// import XCTest + +#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) + import AsyncSequenceValidation import AsyncAlgorithms @@ -334,3 +337,5 @@ final class TestChunk: XCTestCase { } } } + +#endif diff --git a/Tests/AsyncAlgorithmsTests/TestDebounce.swift b/Tests/AsyncAlgorithmsTests/TestDebounce.swift index 317f0bca..94dae286 100644 --- a/Tests/AsyncAlgorithmsTests/TestDebounce.swift +++ b/Tests/AsyncAlgorithmsTests/TestDebounce.swift @@ -13,6 +13,7 @@ import XCTest import AsyncAlgorithms final class TestDebounce: XCTestCase { + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) 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") @@ -67,6 +68,7 @@ final class TestDebounce: XCTestCase { "----|" } } + #endif func test_Rethrows() async throws { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index a2779e3d..03650ec4 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -170,6 +170,7 @@ final class TestMerge2: XCTestCase { XCTAssertEqual(collected.intersection(expected), expected) } + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) func test_merge_makes_sequence_with_ordered_elements_when_sources_follow_a_timeline() { validate { "a-c-e-g-|" @@ -178,6 +179,7 @@ final class TestMerge2: XCTestCase { "abcdefgh|" } } + #endif func test_merge_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") @@ -508,6 +510,7 @@ final class TestMerge3: XCTestCase { XCTAssertEqual(collected.intersection(expected), expected) } + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) func test_merge_makes_sequence_with_ordered_elements_when_sources_follow_a_timeline() { validate { "a---e---|" @@ -517,6 +520,7 @@ final class TestMerge3: XCTestCase { "abcdefgh|" } } + #endif func test_merge_finishes_when_iteration_task_is_cancelled() async { let source1 = Indefinite(value: "test1") diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index bf027233..0d21cd5a 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -13,6 +13,7 @@ import XCTest import AsyncAlgorithms final class TestThrottle: XCTestCase { + #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) 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") @@ -188,4 +189,5 @@ final class TestThrottle: XCTestCase { "a--d--g--j--[l|]" } } + #endif } diff --git a/Tests/AsyncAlgorithmsTests/TestTimer.swift b/Tests/AsyncAlgorithmsTests/TestTimer.swift index b54647ca..05160097 100644 --- a/Tests/AsyncAlgorithmsTests/TestTimer.swift +++ b/Tests/AsyncAlgorithmsTests/TestTimer.swift @@ -10,6 +10,9 @@ //===----------------------------------------------------------------------===// import XCTest + +#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) + import AsyncAlgorithms import AsyncSequenceValidation @@ -57,3 +60,5 @@ final class TestTimer: XCTestCase { } } } + +#endif diff --git a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift index b4b646f2..f64dd2c5 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidationTests.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidationTests.swift @@ -10,6 +10,9 @@ //===----------------------------------------------------------------------===// import XCTest + +#if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) || canImport(wasi_pthread) + import AsyncAlgorithms import AsyncSequenceValidation @testable import AsyncAlgorithms_XCTest @@ -354,3 +357,5 @@ struct LaggingAsyncSequence: AsyncSequence { self.clock = clock } } + +#endif From 70c36ce9a1143709ba7a4cf764b6a5303ced23d4 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 10 Sep 2025 10:00:31 -0700 Subject: [PATCH 143/149] Share algorithm (#357) This is an implementation of share; an algorithm for allowing AsyncSequence types to be iterated by multiple consumers. In addition this cleans up the project to be a starting point for the 1.1 release - it fixes the continuous integration support and configurations but has the cost of only supporting 3 active releases (6.0, 6.1, and 6.2). Previous releases are only going to have legacy support via older released versions. * Fix the remaining todo on hard cancellation vs soft cancellation * Update Sources/AsyncAlgorithms/AsyncShareSequence.swift Co-authored-by: Jamie <2119834+jamieQ@users.noreply.github.com> * Update the implementation of share to handle sendability requirements, address some edge cases and update to the latest version of the discussion around buffering behaviors as well as adding some documentation and commentary * Add some first drafts at unit tests for verifying share behaviors * Cleanup some of the documentation and address feedback for implementation details * Fix merge damage by restoring the changes from @jamieQ * Fix up some availability issues w.r.t. sendable metatypes * Add a preamble to SendableMetatypes.swift * Add a fallback for next (for pre 6.2 swift's) * Slight adjustments for older compiler builds and expose the sendable metatypes since those are part of documentation * Update formatting and gate availability for older compilers * Speculative fix for pre 6.1 compiler crashes by removing the sending keyword * Revert "Speculative fix for pre 6.1 compiler crashes by removing the sending keyword" This reverts commit 875fa81bc69276f88138e28d325c2bc3e185b62e. * Speculative fix for pre 6.1 compiler crashes by removing the sending keyword, and correct sendable metatype shim availability * Remove block comment for formatting check * Take a more conservative approach and roll back to only supporting share on 6.2 * Only test share on 6.2 or newer * roll back to older sendable metatype constraints * yet another stab at 5.10 visionOS guards * Formatting pass * Disable 5.10 builds and leave those to older releases * Ignore swift-version files to avoid local swiftly toolchain selection from being required by others * Renumber the proposals and copy share into a guide * Add a link from the primary doc interface to share --------- Co-authored-by: Jamie <2119834+jamieQ@users.noreply.github.com> --- .github/workflows/pull_request.yml | 2 +- .gitignore | 3 +- .../{NNNN-channel.md => 0012-channel.md} | 0 Evolution/{NNNN-chunk.md => 0013-chunk.md} | 4 +- ...NNN-rate-limits.md => 0014-rate-limits.md} | 4 +- ...{NNNN-reductions.md => 0015-reductions.md} | 4 +- Evolution/0016-share.md | 197 +++++ Package.swift | 12 +- .../AsyncAlgorithms.docc/AsyncAlgorithms.md | 1 + .../AsyncAlgorithms.docc/Guides/Share.md | 194 +++++ .../AsyncAlgorithms/AsyncShareSequence.swift | 726 ++++++++++++++++++ Sources/AsyncAlgorithms/Locking.swift | 4 + .../Support/GatedSequence.swift | 10 + Tests/AsyncAlgorithmsTests/TestShare.swift | 583 ++++++++++++++ 14 files changed, 1734 insertions(+), 10 deletions(-) rename Evolution/{NNNN-channel.md => 0012-channel.md} (100%) rename Evolution/{NNNN-chunk.md => 0013-chunk.md} (99%) rename Evolution/{NNNN-rate-limits.md => 0014-rate-limits.md} (98%) rename Evolution/{NNNN-reductions.md => 0015-reductions.md} (98%) create mode 100644 Evolution/0016-share.md create mode 100644 Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Share.md create mode 100644 Sources/AsyncAlgorithms/AsyncShareSequence.swift create mode 100644 Tests/AsyncAlgorithmsTests/TestShare.swift diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 115ea4e4..341d6ca4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]" + linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]]" windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]" enable_wasm_sdk_build: true wasm_sdk_build_command: swift build -Xcc -D_WASI_EMULATED_PTHREAD diff --git a/.gitignore b/.gitignore index ec575d9e..695a8c0e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc .swiftpm -Package.resolved \ No newline at end of file +Package.resolved +.swift-version \ No newline at end of file diff --git a/Evolution/NNNN-channel.md b/Evolution/0012-channel.md similarity index 100% rename from Evolution/NNNN-channel.md rename to Evolution/0012-channel.md diff --git a/Evolution/NNNN-chunk.md b/Evolution/0013-chunk.md similarity index 99% rename from Evolution/NNNN-chunk.md rename to Evolution/0013-chunk.md index f9540b95..6d2b52bd 100644 --- a/Evolution/NNNN-chunk.md +++ b/Evolution/0013-chunk.md @@ -1,7 +1,7 @@ # Chunked & Timer -* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-chunk.md) +* Proposal: [SAA-0013](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0013-chunk.md) * Author(s): [Kevin Perry](https://github.com/kperryua), [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [ [By group](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift), [On projection](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift), diff --git a/Evolution/NNNN-rate-limits.md b/Evolution/0014-rate-limits.md similarity index 98% rename from Evolution/NNNN-rate-limits.md rename to Evolution/0014-rate-limits.md index e78c60d1..b7babcd3 100644 --- a/Evolution/NNNN-rate-limits.md +++ b/Evolution/0014-rate-limits.md @@ -1,8 +1,8 @@ # Rate Limiting -* Proposal: [SAA-NNNN]() +* Proposal: [SAA-0014](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0014-rate-limits.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [ [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncDebounceSequence.swift) | diff --git a/Evolution/NNNN-reductions.md b/Evolution/0015-reductions.md similarity index 98% rename from Evolution/NNNN-reductions.md rename to Evolution/0015-reductions.md index d1d8ae5a..6fca3430 100644 --- a/Evolution/NNNN-reductions.md +++ b/Evolution/0015-reductions.md @@ -1,7 +1,7 @@ # Reductions -* Proposal: [SAA-NNNN](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/NNNN-reductions.md) +* Proposal: [SAA-0015](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0015-reductions.md) * Author(s): [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [ [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestReductions.swift) diff --git a/Evolution/0016-share.md b/Evolution/0016-share.md new file mode 100644 index 00000000..0be1a7f9 --- /dev/null +++ b/Evolution/0016-share.md @@ -0,0 +1,197 @@ +# Share +* Proposal: [SAA-0015](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0016-share.md) +* Author(s): [Philippe Hausler](https://github.com/phausler) +* Status: **Accepted** +* Implementation: [ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncShareSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestShare.swift) +] +* Decision Notes: +* Bugs: + + +## Introduction + +Many of the AsyncSequence adopting types only permit a one singular consumption. However there are many times that the same produced values are useful in more than one place. Out of that mechanism there are a few approaches to share, distribute, and broadcast those values. This proposal will focus on one concept; sharing. Sharing is where each consumption independently can make forward progress and get the same values but do not replay from the beginning of time. + +## Motivation + +There are many potential usages for the sharing concept of AsyncSequences. + +One such example is the case where a source of data as an asynchronous sequence needs to be consumed by updating UI, logging, and additionally a network connection. This particular case does not matter on which uses but instead that those uses are independent of each other. It would not be expected for networking to block or delay the updates to UI, nor should logging. This example case also illustrates that the isolation of each side might be different and that some of the sides may not tolerate coalescing or dropping values. + +There are many other use cases that have been requested for this family of algorithms. Since the release of AsyncAlgorithms it has perhaps been the most popularly requested set of behaviors as additions to the package. + +## Proposed solution + +AsyncAlgorithms will introduce a new extension function on AsyncSequence that will provide a shareable asynchronous sequence that will produce the same values upon iteration from multiple instances of it's AsyncIterator. Those iterations can take place in multiple isolations. + +When values from a differing isolation cannot be coalesced, the two options available are either awaiting (an exertion of back-pressure across the sequences) or buffering (an internal back-pressure to a buffer). Replaying the values from the beginning of the creation of the sequence is a distinctly different behavior that should be considered a different use case. This then leaves the behavioral characteristic of this particular operation of share as; sharing a buffer of values started from the initialization of a new iteration of the sequence. Control over that buffer should then have options to determine the behavior, similar to how AsyncStream allows that control. It should have options to be unbounded, buffering the oldest count of elements, or buffering the newest count of elements. + +It is critical to identify that this is one algorithm in the family of algorithms for sharing values. It should not attempt to solve all behavioral requirements but instead serve a common set of them that make cohesive sense together. This proposal is not mutually exclusive to the other algorithms in the sharing family. + +## Detailed design + +A new extension will be added to return a `Sendable` `AsyncSequence`. This extension will take a buffering policy to identify how the buffer will be handled when iterations do not consume at the same rate. + +The `Sendable` annotation identifies to the developer that this sequence can be shared and stored in an existental `any`. + +```swift +extension AsyncSequence where Element: Sendable { + public func share( + bufferingPolicy: AsyncBufferSequencePolicy = .bounded(1) + ) -> some AsyncSequence & Sendable +} +``` + +The buffer internally to the share algorithm will only extend back to the furthest element available but there will only be a singular buffer shared across all iterators. This ensures that with the application of the buffering policy the storage size is as minimal as possible while still allowing all iterations to avoid dropping values and keeping the memory usage in check. The signature reuses the existing `AsyncBufferSequencePolicy` type to specify the behavior around buffering either responding to how it should limit emitting to the buffer or what should happen when the buffer is exceeded. + +## Runtime Behavior + +The runtime behaviors fall into a few categories; ordering, iteration isolation, cancellation, and lifetimes. To understand the beahviors there are a terms useful to define. Each creation of the AsyncIterator of the sequence and invocation of next will be referred to a side of the share iteration. The back pressure to the system to fetch a new element or termination is refered to as demand. The limit which is the pending gate for awaiting until the buffer has been serviced used for the `AsyncBufferSequencePolicy.bounded(_ : Int)` policy. The last special definition is that of the extent which is specifically in this case the lifetime of the asynchronous sequence itself. + +When the underlying type backing the share algorithm is constructed a new extent is created; this is used for tracking the reference lifetime under the hood and is used to both house the iteration but also to identify the point at which no more sides can be constructed. When no more sides can be constructed and no sides are left to iterate then the backing iteration is canceled. This prevents any un-referenced task backing the iteration to not be leaked by the algorith itself. + +That construction then creates an initial shared state and buffer. No task is started initially; it is only upon the first demand that the task backing the iteration is started; this means on the first call to next a task is spun up servicing all potential sides. The order of which the sides are serviced is not specified and cannot be relied upon, however the order of delivery within a side is always guarenteed to be ordered. The singular task servicing the iteration will be the only place holding any sort of iterator from the base `AsyncSequence`; so that iterator is isolated and not sent from one isolation to another. That iteration first awaits any limit availability and then awaits for a demand given by a side. After-which it then awaits an element or terminal event from the iterator and enqueues the elements to the buffer. + +The buffer itself is only held in one location, each side however has a cursor index into that buffer and when values are consumed it adjusts the indexes accordingly; leaving the buffer usage only as big as the largest deficit. This means that new sides that are started post initial start up will not have a "replay" effect; that is a similar but distinct algorithm and is not addressed by this proposal. Any buffer size sensitive systems that wish to adjust behavior should be aware that specifying a policy is a suggested step. However in common usage similar to other such systems servicing desktop and mobile applications the common behavior is often unbounded. Alternatively desktop or mobile applications will often want `.bounded(1)` since that enforces the slowest consumption to drive the forward progress at most 1 buffered element. All of the use cases have a reasonable default of `.bounded(1)`; mobile, deskop, and server side uses. Leaving this as the default parameter keeps the progressive disclosure of the beahviors - such that the easiest thing to write is correct for all uses, and then more advanced control can be adjusted by passing in a specific policy. This default argument diverges slightly from AsyncStream, but follows a similar behavior to that of Combine's `share`. + +As previously stated, the isolation of the iteration of the upstream/base AsyncSequence is to a detached task, this ensures that individual sides can have independent cancellation. Those cancellations will have the effect of remvoing that side from the shared iteration and cleaning up accordingly (including adjusting the trimming of the internal buffer). + +Representing concurrent access is difficult to express all potential examples but there are a few cases included with this proposal to illustrate some of the behaviors. If a more comprehensive behavioral analysis is needed, it is strongly suggested to try out the pending pull request to identify how specific behaviors work. Please keep in mind that the odering between tasks is not specified, only the order within one side of iteration. + +Practically this all means that a given iteration may be "behind" another and can eventually catch up (provided it is within the buffer limit). + +```swift +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .unbounded) + +let t1 = Task { + for await element in exampleSource { + if element == 0 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 1", element) + } +} + +let t2 = Task { + for await element in exampleSource { + if element == 3 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 2", element) + } +} + +await t1.value +await t2.value + +``` + +This example will print a possible ordering of the following: + +``` +Task 2 0 +Task 2 1 +Task 2 2 +Task 1 0 +Task 2 3 +Task 2 4 +Task 1 1 +Task 1 2 +Task 1 3 +Task 1 4 +``` + +The order of the interleaving of the prints are not guaranteed; however the order of the elements per iteration is. Likewise in this buffering case it is guaranteed that all values are represented in the output. + +If the creation were instead altered to the following: + +```swift +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .bufferingLatest(2)) +``` + +The output would print the possible ordering of: + +``` +Task 2 0 +Task 2 1 +Task 2 2 +Task 1 0 +Task 2 4 +Task 1 3 +Task 1 4 +``` + +Some values are dropped due to the buffering policy, but eventually they reach consistency. Which similarly works for the following: + +``` +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .bufferingOldest(2)) +``` + +``` +Task 2 0 +Task 2 1 +Task 2 2 +Task 1 0 +Task 2 4 +Task 1 1 +Task 1 2 +``` + +However in this particular case the newest values are the dropped elements. + +The `.bounded(N)` policy enforces consumption to prevent any side from being beyond a given amount away from other sides' consumption. + +```swift +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .bounded(1)) + +let t1 = Task { + for await element in exampleSource { + if element == 0 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 1", element) + } +} + +let t2 = Task { + for await element in exampleSource { + if element == 3 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 2", element) + } +} + +await t1.value +await t2.value +``` + +Will have a potential ordering output of: + +``` +Task 2 0 +Task 2 1 +Task 1 0 +Task 1 1 +Task 2 2 +Task 1 2 +Task 1 3 +Task 1 4 +Task 2 3 +Task 2 4 +``` + +In that example output Task 2 can get element 0 and 1 but must await until task 1 has caught up to the specified buffering. This limit means that no additional iteration (and no values are then dropped) is made until the buffer count is below the specified value. + + +## Effect on API resilience + +This is an additive API and no existing systems are changed, however it will introduce a few new types that will need to be maintained as ABI interfaces. Since the intent of this is to provide a mechanism to store AsyncSequences to a shared context the type must be exposed as ABI (for type sizing). + +## Alternatives considered + +It has been considered that the buffering policy would be nested inside the `AsyncShareSequence` type. However since this seems to be something that will be useful for other types it makes sense to use an existing type from a top level type. However if it is determined that a general form of a buffering policy would require additional behaviors this might be a debatable placement to move back to an interior type similar to AsyncStream. + + diff --git a/Package.swift b/Package.swift index 45c5abca..c6a2894c 100644 --- a/Package.swift +++ b/Package.swift @@ -3,12 +3,20 @@ import PackageDescription import CompilerPluginSupport +let AsyncAlgorithms_v1_0 = "AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" +#if compiler(>=6.0) && swift(>=6.0) // 5.10 doesnt support visionOS availability +let AsyncAlgorithms_v1_1 = + "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" +#else +let AsyncAlgorithms_v1_1 = "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0" +#endif + let availabilityMacros: [SwiftSetting] = [ .enableExperimentalFeature( - "AvailabilityMacro=AsyncAlgorithms 1.0:macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" + AsyncAlgorithms_v1_0 ), .enableExperimentalFeature( - "AvailabilityMacro=AsyncAlgorithms 1.1:macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" + AsyncAlgorithms_v1_1 ), ] diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md index 1f18bc02..8461d28a 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md @@ -30,6 +30,7 @@ This package has three main goals: - - - +- - - - diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Share.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Share.md new file mode 100644 index 00000000..76610671 --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Share.md @@ -0,0 +1,194 @@ +# Share + +* Author(s): [Philippe Hausler](https://github.com/phausler) + +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncShareSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestShare.swift) +] + +## Introduction + +Many of the AsyncSequence adopting types only permit a one singular consumption. However there are many times that the same produced values are useful in more than one place. Out of that mechanism there are a few approaches to share, distribute, and broadcast those values. This proposal will focus on one concept; sharing. Sharing is where each consumption independently can make forward progress and get the same values but do not replay from the beginning of time. + +## Motivation + +There are many potential usages for the sharing concept of AsyncSequences. + +One such example is the case where a source of data as an asynchronous sequence needs to be consumed by updating UI, logging, and additionally a network connection. This particular case does not matter on which uses but instead that those uses are independent of each other. It would not be expected for networking to block or delay the updates to UI, nor should logging. This example case also illustrates that the isolation of each side might be different and that some of the sides may not tolerate coalescing or dropping values. + +There are many other use cases that have been requested for this family of algorithms. Since the release of AsyncAlgorithms it has perhaps been the most popularly requested set of behaviors as additions to the package. + +## Proposed solution + +AsyncAlgorithms will introduce a new extension function on AsyncSequence that will provide a shareable asynchronous sequence that will produce the same values upon iteration from multiple instances of it's AsyncIterator. Those iterations can take place in multiple isolations. + +When values from a differing isolation cannot be coalesced, the two options available are either awaiting (an exertion of back-pressure across the sequences) or buffering (an internal back-pressure to a buffer). Replaying the values from the beginning of the creation of the sequence is a distinctly different behavior that should be considered a different use case. This then leaves the behavioral characteristic of this particular operation of share as; sharing a buffer of values started from the initialization of a new iteration of the sequence. Control over that buffer should then have options to determine the behavior, similar to how AsyncStream allows that control. It should have options to be unbounded, buffering the oldest count of elements, or buffering the newest count of elements. + +It is critical to identify that this is one algorithm in the family of algorithms for sharing values. It should not attempt to solve all behavioral requirements but instead serve a common set of them that make cohesive sense together. This proposal is not mutually exclusive to the other algorithms in the sharing family. + +## Detailed design + +A new extension will be added to return a `Sendable` `AsyncSequence`. This extension will take a buffering policy to identify how the buffer will be handled when iterations do not consume at the same rate. + +The `Sendable` annotation identifies to the developer that this sequence can be shared and stored in an existental `any`. + +```swift +extension AsyncSequence where Element: Sendable { + public func share( + bufferingPolicy: AsyncBufferSequencePolicy = .bounded(1) + ) -> some AsyncSequence & Sendable +} +``` + +The buffer internally to the share algorithm will only extend back to the furthest element available but there will only be a singular buffer shared across all iterators. This ensures that with the application of the buffering policy the storage size is as minimal as possible while still allowing all iterations to avoid dropping values and keeping the memory usage in check. The signature reuses the existing `AsyncBufferSequencePolicy` type to specify the behavior around buffering either responding to how it should limit emitting to the buffer or what should happen when the buffer is exceeded. + +## Runtime Behavior + +The runtime behaviors fall into a few categories; ordering, iteration isolation, cancellation, and lifetimes. To understand the beahviors there are a terms useful to define. Each creation of the AsyncIterator of the sequence and invocation of next will be referred to a side of the share iteration. The back pressure to the system to fetch a new element or termination is refered to as demand. The limit which is the pending gate for awaiting until the buffer has been serviced used for the `AsyncBufferSequencePolicy.bounded(_ : Int)` policy. The last special definition is that of the extent which is specifically in this case the lifetime of the asynchronous sequence itself. + +When the underlying type backing the share algorithm is constructed a new extent is created; this is used for tracking the reference lifetime under the hood and is used to both house the iteration but also to identify the point at which no more sides can be constructed. When no more sides can be constructed and no sides are left to iterate then the backing iteration is canceled. This prevents any un-referenced task backing the iteration to not be leaked by the algorith itself. + +That construction then creates an initial shared state and buffer. No task is started initially; it is only upon the first demand that the task backing the iteration is started; this means on the first call to next a task is spun up servicing all potential sides. The order of which the sides are serviced is not specified and cannot be relied upon, however the order of delivery within a side is always guarenteed to be ordered. The singular task servicing the iteration will be the only place holding any sort of iterator from the base `AsyncSequence`; so that iterator is isolated and not sent from one isolation to another. That iteration first awaits any limit availability and then awaits for a demand given by a side. After-which it then awaits an element or terminal event from the iterator and enqueues the elements to the buffer. + +The buffer itself is only held in one location, each side however has a cursor index into that buffer and when values are consumed it adjusts the indexes accordingly; leaving the buffer usage only as big as the largest deficit. This means that new sides that are started post initial start up will not have a "replay" effect; that is a similar but distinct algorithm and is not addressed by this proposal. Any buffer size sensitive systems that wish to adjust behavior should be aware that specifying a policy is a suggested step. However in common usage similar to other such systems servicing desktop and mobile applications the common behavior is often unbounded. Alternatively desktop or mobile applications will often want `.bounded(1)` since that enforces the slowest consumption to drive the forward progress at most 1 buffered element. All of the use cases have a reasonable default of `.bounded(1)`; mobile, deskop, and server side uses. Leaving this as the default parameter keeps the progressive disclosure of the beahviors - such that the easiest thing to write is correct for all uses, and then more advanced control can be adjusted by passing in a specific policy. This default argument diverges slightly from AsyncStream, but follows a similar behavior to that of Combine's `share`. + +As previously stated, the isolation of the iteration of the upstream/base AsyncSequence is to a detached task, this ensures that individual sides can have independent cancellation. Those cancellations will have the effect of remvoing that side from the shared iteration and cleaning up accordingly (including adjusting the trimming of the internal buffer). + +Representing concurrent access is difficult to express all potential examples but there are a few cases included with this proposal to illustrate some of the behaviors. If a more comprehensive behavioral analysis is needed, it is strongly suggested to try out the pending pull request to identify how specific behaviors work. Please keep in mind that the odering between tasks is not specified, only the order within one side of iteration. + +Practically this all means that a given iteration may be "behind" another and can eventually catch up (provided it is within the buffer limit). + +```swift +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .unbounded) + +let t1 = Task { + for await element in exampleSource { + if element == 0 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 1", element) + } +} + +let t2 = Task { + for await element in exampleSource { + if element == 3 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 2", element) + } +} + +await t1.value +await t2.value + +``` + +This example will print a possible ordering of the following: + +``` +Task 2 0 +Task 2 1 +Task 2 2 +Task 1 0 +Task 2 3 +Task 2 4 +Task 1 1 +Task 1 2 +Task 1 3 +Task 1 4 +``` + +The order of the interleaving of the prints are not guaranteed; however the order of the elements per iteration is. Likewise in this buffering case it is guaranteed that all values are represented in the output. + +If the creation were instead altered to the following: + +```swift +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .bufferingLatest(2)) +``` + +The output would print the possible ordering of: + +``` +Task 2 0 +Task 2 1 +Task 2 2 +Task 1 0 +Task 2 4 +Task 1 3 +Task 1 4 +``` + +Some values are dropped due to the buffering policy, but eventually they reach consistency. Which similarly works for the following: + +``` +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .bufferingOldest(2)) +``` + +``` +Task 2 0 +Task 2 1 +Task 2 2 +Task 1 0 +Task 2 4 +Task 1 1 +Task 1 2 +``` + +However in this particular case the newest values are the dropped elements. + +The `.bounded(N)` policy enforces consumption to prevent any side from being beyond a given amount away from other sides' consumption. + +```swift +let exampleSource = [0, 1, 2, 3, 4].async.share(bufferingPolicy: .bounded(1)) + +let t1 = Task { + for await element in exampleSource { + if element == 0 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 1", element) + } +} + +let t2 = Task { + for await element in exampleSource { + if element == 3 { + try? await Task.sleep(for: .seconds(1)) + } + print("Task 2", element) + } +} + +await t1.value +await t2.value +``` + +Will have a potential ordering output of: + +``` +Task 2 0 +Task 2 1 +Task 1 0 +Task 1 1 +Task 2 2 +Task 1 2 +Task 1 3 +Task 1 4 +Task 2 3 +Task 2 4 +``` + +In that example output Task 2 can get element 0 and 1 but must await until task 1 has caught up to the specified buffering. This limit means that no additional iteration (and no values are then dropped) is made until the buffer count is below the specified value. + + +## Effect on API resilience + +This is an additive API and no existing systems are changed, however it will introduce a few new types that will need to be maintained as ABI interfaces. Since the intent of this is to provide a mechanism to store AsyncSequences to a shared context the type must be exposed as ABI (for type sizing). + +## Alternatives considered + +It has been considered that the buffering policy would be nested inside the `AsyncShareSequence` type. However since this seems to be something that will be useful for other types it makes sense to use an existing type from a top level type. However if it is determined that a general form of a buffering policy would require additional behaviors this might be a debatable placement to move back to an interior type similar to AsyncStream. + + diff --git a/Sources/AsyncAlgorithms/AsyncShareSequence.swift b/Sources/AsyncAlgorithms/AsyncShareSequence.swift new file mode 100644 index 00000000..6c76a4d1 --- /dev/null +++ b/Sources/AsyncAlgorithms/AsyncShareSequence.swift @@ -0,0 +1,726 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.2) + +import Synchronization +import DequeModule + +@available(AsyncAlgorithms 1.1, *) +extension AsyncSequence +where Element: Sendable, Self: SendableMetatype, AsyncIterator: SendableMetatype { + /// Creates a shared async sequence that allows multiple concurrent iterations over a single source. + /// + /// The `share` method transforms an async sequence into a shareable sequence that can be safely + /// iterated by multiple concurrent tasks. This is useful when you want to broadcast elements from + /// a single source to multiple consumers without duplicating work or creating separate iterations. + /// + /// Each element from the source sequence is delivered to all active iterators. + /// Elements are buffered according to the specified buffering policy to handle timing differences + /// between consumers. + /// + /// The base sequence is iterated in it's own task to ensure that cancellation is not polluted from + /// one side of iteration to another. + /// + /// ## Example Usage + /// + /// ```swift + /// let numbers = [1, 2, 3, 4, 5].share.map { + /// try? await Task.sleep(for: .seconds(1)) + /// return $0 + /// } + /// + /// let shared = numbers.share() + /// + /// // Multiple tasks can iterate concurrently + /// let consumer1 = Task { + /// for await value in shared { + /// print("Consumer 1: \(value)") + /// } + /// } + /// + /// let consumer2 = Task { + /// for await value in shared { + /// print("Consumer 2: \(value)") + /// } + /// } + /// + /// await consumer1.value + /// await consumer2.value + /// ``` + /// + /// - Parameter bufferingPolicy: The policy controlling how elements are enqueued to the shared buffer. Defaults to `.bounded(1)`. + /// - `.bounded(n)`: Limits the buffer to `n` elements, applying backpressure to the source when that limit is reached + /// - `.bufferingOldest(n)`: Keeps the oldest `n` elements, discarding newer ones when full + /// - `.bufferingNewest(n)`: Keeps the newest `n` elements, discarding older ones when full + /// - `.unbounded`: Allows unlimited buffering (use with caution) + /// + /// - Returns: A sendable async sequence that can be safely shared across multiple concurrent tasks. + /// + public func share( + bufferingPolicy: AsyncBufferSequencePolicy = .bounded(1) + ) -> some AsyncSequence & Sendable { + // The iterator is transferred to the isolation of the iterating task + // this has to be done "unsafely" since we cannot annotate the transfer + // however since iterating an AsyncSequence types twice has been defined + // as invalid and one creation of the iterator is virtually a consuming + // operation so this is safe at runtime. + // The general principal of `.share()` is to provide a mecahnism for non- + // shared AsyncSequence types to be shared. The parlance for those is + // that the base AsyncSequence type is not Sendable. If the iterator + // is not marked as `nonisolated(unsafe)` the compiler will claim that + // the value is "Capture of 'iterator' with non-Sendable type 'Self.AsyncIterator' in a '@Sendable' closure;" + // Since the closure returns a disconnected non-sendable value there is no + // distinct problem here and the compiler just needs to be informed + // that the diagnostic is overly pessimistic. + nonisolated(unsafe) let iterator = makeAsyncIterator() + return AsyncShareSequence( + { + iterator + }, + bufferingPolicy: bufferingPolicy + ) + } +} + +// An async sequence that enables safe concurrent sharing of a single source sequence. +// +// `AsyncShareSequence` wraps a base async sequence and allows multiple concurrent iterators +// to consume elements from the same source. It handles all the complexity of coordinating +// between multiple consumers, buffering elements, and managing the lifecycle of the underlying +// iteration. +// +// ## Key Features +// +// **Single Source Iteration**: The base sequence's iterator is created and consumed only once +// **Concurrent Safe**: Multiple tasks can safely iterate simultaneously +// **Configurable Buffering**: Supports various buffering strategies for different use cases +// **Automatic Cleanup**: Properly manages resources and cancellation across all consumers +// +// ## Internal Architecture +// +// The implementation uses several key components: +// `Side`: Represents a single consumer's iteration state +// `Iteration`: Coordinates all consumers and manages the shared buffer +// `Extent`: Manages the overall lifecycle and cleanup +// +// This type is typically not used directly; instead, use the `share()` method on any +// async sequence that meets the sendability requirements. +@available(AsyncAlgorithms 1.1, *) +struct AsyncShareSequence: Sendable +where Base.Element: Sendable, Base: SendableMetatype, Base.AsyncIterator: SendableMetatype { + // Represents a single consumer's connection to the shared sequence. + // + // Each iterator of the shared sequence creates its own `Side` instance, which tracks + // that consumer's position in the shared buffer and manages its continuation for + // async iteration. The `Side` automatically registers itself with the central + // `Iteration` coordinator and cleans up when deallocated. + // + // ## Lifecycle + // + // **Creation**: Automatically registers with the iteration coordinator + // **Usage**: Tracks buffer position and manages async continuations + // **Cleanup**: Automatically unregisters and cancels pending operations on deinit + final class Side { + // Tracks the state of a single consumer's iteration. + // + // - `continuation`: The continuation waiting for the next element (nil if not waiting) + // - `position`: The consumer's current position in the shared buffer + struct State { + var continuation: UnsafeContinuation, Never>? + var position = 0 + + // Creates a new state with the position adjusted by the given offset. + // + // This is used when the shared buffer is trimmed to maintain correct + // relative positioning for this consumer. + // + // - Parameter adjustment: The number of positions to subtract from the current position + // - Returns: A new `State` with the adjusted position + func offset(_ adjustment: Int) -> State { + State(continuation: continuation, position: position - adjustment) + } + } + + let iteration: Iteration + let id: Int + + init(_ iteration: Iteration) { + self.iteration = iteration + id = iteration.registerSide() + } + + deinit { + iteration.unregisterSide(id) + } + + func next(isolation actor: isolated (any Actor)?) async throws(Failure) -> Element? { + try await iteration.next(isolation: actor, id: id) + } + } + + // The central coordinator that manages the shared iteration state. + // + // `Iteration` is responsible for: + // Managing the single background task that consumes the source sequence + // Coordinating between multiple consumer sides + // Buffering elements according to the specified policy + // Handling backpressure and flow control + // Managing cancellation and cleanup + // + // ## Thread Safety + // + // All operations are synchronized using a `Mutex` to ensure thread-safe access + // to the shared state across multiple concurrent consumers. + final class Iteration: Sendable { + // Represents the state of the background task that consumes the source sequence. + // + // The iteration task goes through several states during its lifecycle: + // `pending`: Initial state, holds the factory to create the iterator + // `starting`: Transitional state while the task is being created + // `running`: Active state with a running background task + // `cancelled`: Terminal state when the iteration has been cancelled + enum IteratingTask { + case pending(@Sendable () -> sending Base.AsyncIterator) + case starting + case running(Task) + case cancelled + + var isStarting: Bool { + switch self { + case .starting: true + default: false + } + } + + func cancel() { + switch self { + case .running(let task): + task.cancel() + default: + break + } + } + } + // The complete shared state for coordinating all aspects of the shared iteration. + // + // This state is protected by a mutex and contains all the information needed + // to coordinate between multiple consumers, manage buffering, and control + // the background iteration task. + struct State: Sendable { + // Defines how elements are stored and potentially discarded in the shared buffer. + // + // `unbounded`: Store all elements without limit (may cause memory growth) + // `bufferingOldest(Int)`: Keep only the oldest N elements, ignore newer ones when full + // `bufferingNewest(Int)`: Keep only the newest N elements, discard older ones when full + enum StoragePolicy: Sendable { + case unbounded + case bufferingOldest(Int) + case bufferingNewest(Int) + } + + var generation = 0 + var sides = [Int: Side.State]() + var iteratingTask: IteratingTask + private(set) var buffer = Deque() + private(set) var finished = false + private(set) var failure: Failure? + var cancelled = false + var limit: UnsafeContinuation? + var demand: UnsafeContinuation? + + let storagePolicy: StoragePolicy + + init( + _ iteratorFactory: @escaping @Sendable () -> sending Base.AsyncIterator, + bufferingPolicy: AsyncBufferSequencePolicy + ) { + self.iteratingTask = .pending(iteratorFactory) + switch bufferingPolicy.policy { + case .bounded: self.storagePolicy = .unbounded + case .bufferingOldest(let bound): self.storagePolicy = .bufferingOldest(bound) + case .bufferingNewest(let bound): self.storagePolicy = .bufferingNewest(bound) + case .unbounded: self.storagePolicy = .unbounded + } + } + + // Removes elements from the front of the buffer that all consumers have already processed. + // + // This method finds the minimum position across all active consumers and removes + // that many elements from the front of the buffer. It then adjusts all consumer + // positions to account for the removed elements, maintaining their relative positions. + // + // This optimization prevents the buffer from growing indefinitely when all consumers + // are keeping pace with each other. + mutating func trimBuffer() { + if let minimumIndex = sides.values.map({ $0.position }).min(), minimumIndex > 0 { + buffer.removeFirst(minimumIndex) + sides = sides.mapValues { + $0.offset(minimumIndex) + } + } + } + + // Private state machine transitions for the emission of a given value. + // + // This method ensures the continuations are properly consumed when emitting values + // and returns those continuations for resumption. + private mutating func _emit( + _ value: T, + limit: Int + ) -> (T, UnsafeContinuation?, UnsafeContinuation?, Bool) { + let belowLimit = buffer.count < limit || limit == 0 + defer { + if belowLimit { + self.limit = nil + } + demand = nil + } + guard case .cancelled = iteratingTask else { + return (value, belowLimit ? self.limit : nil, demand, false) + } + return (value, belowLimit ? self.limit : nil, demand, true) + } + + // Internal state machine transitions for the emission of a given value. + // + // This method ensures the continuations are properly consumed when emitting values + // and returns those continuations for resumption. + // + // If no limit is specified it interprets that as an unbounded limit. + mutating func emit( + _ value: T, + limit: Int? + ) -> (T, UnsafeContinuation?, UnsafeContinuation?, Bool) { + return _emit(value, limit: limit ?? .max) + } + + // Adds an element to the buffer according to the configured storage policy. + // + // The behavior depends on the storage policy: + // **Unbounded**: Always appends the element + // **Buffering Oldest**: Appends only if under the limit, otherwise ignores the element + // **Buffering Newest**: Appends if under the limit, otherwise removes the oldest and appends + // + // - Parameter element: The element to add to the buffer + mutating func enqueue(_ element: Element) { + let count = buffer.count + + switch storagePolicy { + case .unbounded: + buffer.append(element) + case .bufferingOldest(let limit): + if count < limit { + buffer.append(element) + } + case .bufferingNewest(let limit): + if count < limit { + buffer.append(element) + } else if count > 0 { + buffer.removeFirst() + buffer.append(element) + } + } + } + + mutating func finish() { + finished = true + } + + mutating func fail(_ error: Failure) { + finished = true + failure = error + } + } + + let state: Mutex + let limit: Int? + + init( + _ iteratorFactory: @escaping @Sendable () -> sending Base.AsyncIterator, + bufferingPolicy: AsyncBufferSequencePolicy + ) { + state = Mutex(State(iteratorFactory, bufferingPolicy: bufferingPolicy)) + switch bufferingPolicy.policy { + case .bounded(let limit): + self.limit = limit + default: + self.limit = nil + } + } + + func cancel() { + let (task, limitContinuation, demand, cancelled) = state.withLock { + state -> (IteratingTask?, UnsafeContinuation?, UnsafeContinuation?, Bool) in + guard state.sides.count == 0 else { + state.cancelled = true + return state.emit(nil, limit: limit) + } + defer { + state.iteratingTask = .cancelled + state.cancelled = true + } + return state.emit(state.iteratingTask, limit: limit) + } + task?.cancel() + limitContinuation?.resume(returning: cancelled) + demand?.resume() + } + + func registerSide() -> Int { + state.withLock { state in + defer { state.generation += 1 } + state.sides[state.generation] = Side.State() + return state.generation + } + } + + func unregisterSide(_ id: Int) { + let (side, continuation, cancelled, iteratingTaskToCancel) = state.withLock { + state -> (Side.State?, UnsafeContinuation?, Bool, IteratingTask?) in + let side = state.sides.removeValue(forKey: id) + state.trimBuffer() + let cancelRequested = state.sides.count == 0 && state.cancelled + guard let limit, state.buffer.count < limit else { + guard case .cancelled = state.iteratingTask else { + defer { + if cancelRequested { + state.iteratingTask = .cancelled + } + } + return (side, nil, false, cancelRequested ? state.iteratingTask : nil) + } + return (side, nil, true, nil) + } + defer { state.limit = nil } + guard case .cancelled = state.iteratingTask else { + defer { + if cancelRequested { + state.iteratingTask = .cancelled + } + } + return (side, state.limit, false, cancelRequested ? state.iteratingTask : nil) + } + return (side, state.limit, true, nil) + } + if let continuation { + continuation.resume(returning: cancelled) + } + if let side { + side.continuation?.resume(returning: .success(nil)) + } + if let iteratingTaskToCancel { + iteratingTaskToCancel.cancel() + } + } + + func iterate() async -> Bool { + if let limit { + let cancelled = await withUnsafeContinuation { (continuation: UnsafeContinuation) in + let (resume, cancelled) = state.withLock { state -> (UnsafeContinuation?, Bool) in + guard state.buffer.count >= limit else { + assert(state.limit == nil) + guard case .cancelled = state.iteratingTask else { + return (continuation, false) + } + return (continuation, true) + } + state.limit = continuation + guard case .cancelled = state.iteratingTask else { + return (nil, false) + } + return (nil, true) + } + if let resume { + resume.resume(returning: cancelled) + } + } + if cancelled { + return false + } + } + + // await a demand + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + let hasPendingDemand = state.withLock { state in + for (_, side) in state.sides { + if side.continuation != nil { + return true + } + } + state.demand = continuation + return false + } + if hasPendingDemand { + continuation.resume() + } + } + return state.withLock { state in + switch state.iteratingTask { + case .cancelled: + return false + default: + return true + } + } + } + + func cancel(id: Int) { + unregisterSide(id) // doubly unregistering is idempotent but has a side effect of emitting nil if present + } + + struct Resumption { + let continuation: UnsafeContinuation, Never> + let result: Result + + func resume() { + continuation.resume(returning: result) + } + } + + func emit(_ result: Result) { + let (resumptions, limitContinuation, demandContinuation, cancelled) = state.withLock { + state -> ([Resumption], UnsafeContinuation?, UnsafeContinuation?, Bool) in + var resumptions = [Resumption]() + switch result { + case .success(let element): + if let element { + state.enqueue(element) + } else { + state.finish() + } + case .failure(let failure): + state.fail(failure) + } + for (id, side) in state.sides { + if let continuation = side.continuation { + if side.position < state.buffer.count { + resumptions.append(Resumption(continuation: continuation, result: .success(state.buffer[side.position]))) + state.sides[id]?.position += 1 + state.sides[id]?.continuation = nil + } else if state.finished { + state.sides[id]?.continuation = nil + if let failure = state.failure { + resumptions.append(Resumption(continuation: continuation, result: .failure(failure))) + } else { + resumptions.append(Resumption(continuation: continuation, result: .success(nil))) + } + } + } + } + state.trimBuffer() + return state.emit(resumptions, limit: limit) + } + + if let limitContinuation { + limitContinuation.resume(returning: cancelled) + } + if let demandContinuation { + demandContinuation.resume() + } + for resumption in resumptions { + resumption.resume() + } + } + + private func nextIteration( + _ id: Int + ) async -> Result.Element?, AsyncShareSequence.Failure> { + return await withTaskCancellationHandler { + await withUnsafeContinuation { continuation in + let (res, limitContinuation, demandContinuation, cancelled) = state.withLock { + state -> ( + Result?, UnsafeContinuation?, UnsafeContinuation?, Bool + ) in + guard let side = state.sides[id] else { + return state.emit(.success(nil), limit: limit) + } + if side.position < state.buffer.count { + // There's an element available at this position + let element = state.buffer[side.position] + state.sides[id]?.position += 1 + state.trimBuffer() + return state.emit(.success(element), limit: limit) + } else { + // Position is beyond the buffer + if let failure = state.failure { + return state.emit(.failure(failure), limit: limit) + } else if state.finished { + return state.emit(.success(nil), limit: limit) + } else { + state.sides[id]?.continuation = continuation + return state.emit(nil, limit: limit) + } + } + } + if let limitContinuation { + limitContinuation.resume(returning: cancelled) + } + if let demandContinuation { + demandContinuation.resume() + } + if let res { + continuation.resume(returning: res) + } + } + } onCancel: { + cancel(id: id) + } + } + + private func iterationLoop(factory: @Sendable () -> sending Base.AsyncIterator) async { + var iterator = factory() + do { + while await iterate() { + if let element = try await iterator.next() { + emit(.success(element)) + } else { + emit(.success(nil)) + } + } + } catch { + emit(.failure(error as! Failure)) + } + } + + func next(isolation actor: isolated (any Actor)?, id: Int) async throws(Failure) -> Element? { + let (factory, cancelled) = state.withLock { state -> ((@Sendable () -> sending Base.AsyncIterator)?, Bool) in + switch state.iteratingTask { + case .pending(let factory): + state.iteratingTask = .starting + return (factory, false) + case .cancelled: + return (nil, true) + default: + return (nil, false) + } + } + if cancelled { return nil } + if let factory { + let task: Task + // for the fancy dance of availability and canImport see the comment on the next check for details + #if swift(>=6.2) + if #available(macOS 26.0, iOS 26.0, tvOS 26.0, visionOS 26.0, *) { + task = Task(name: "Share Iteration") { [factory, self] in + await iterationLoop(factory: factory) + } + } else { + task = Task.detached(name: "Share Iteration") { [factory, self] in + await iterationLoop(factory: factory) + } + } + #else + task = Task.detached { [factory, self] in + await iterationLoop(factory: factory) + } + #endif + // Known Issue: there is a very small race where the task may not get a priority escalation during startup + // this unfortuantely cannot be avoided since the task should ideally not be formed within the critical + // region of the state. Since that could lead to potential deadlocks in low-core-count systems. + // That window is relatively small and can be revisited if a suitable proof of safe behavior can be + // determined. + state.withLock { state in + precondition(state.iteratingTask.isStarting) + state.iteratingTask = .running(task) + } + } + + // withTaskPriorityEscalationHandler is only available for the '26 releases and the 6.2 version of + // the _Concurrency library. This menas for Darwin based OSes we have to have a fallback at runtime, + // and for non-darwin OSes we need to verify against the ability to import that version. + // Using this priority escalation means that the base task can avoid being detached. + // + // This is disabled for now until the 9999 availability is removed from `withTaskPriorityEscalationHandler` + #if false // TODO: remove when this is resolved + guard #available(macOS 26.0, iOS 26.0, tvOS 26.0, visionOS 26.0, *) else { + return try await nextIteration(id).get() + } + return try await withTaskPriorityEscalationHandler { + return await nextIteration(id) + } onPriorityEscalated: { old, new in + let task = state.withLock { state -> Task? in + switch state.iteratingTask { + case .running(let task): + return task + default: + return nil + } + } + task?.escalatePriority(to: new) + }.get() + #else + return try await nextIteration(id).get() + #endif + + } + } + + // Manages the lifecycle of the shared iteration. + // + // `Extent` serves as the ownership boundary for the shared sequence. When the + // `AsyncShareSequence` itself is deallocated, the `Extent` ensures that the + // background iteration task is properly cancelled and all resources are cleaned up. + // + // This design allows multiple iterators to safely reference the same underlying + // iteration coordinator while ensuring proper cleanup when the shared sequence + // is no longer needed. + final class Extent: Sendable { + let iteration: Iteration + + init( + _ iteratorFactory: @escaping @Sendable () -> sending Base.AsyncIterator, + bufferingPolicy: AsyncBufferSequencePolicy + ) { + iteration = Iteration(iteratorFactory, bufferingPolicy: bufferingPolicy) + } + + deinit { + iteration.cancel() + } + } + + let extent: Extent + + init( + _ iteratorFactory: @escaping @Sendable () -> sending Base.AsyncIterator, + bufferingPolicy: AsyncBufferSequencePolicy + ) { + extent = Extent(iteratorFactory, bufferingPolicy: bufferingPolicy) + } +} + +@available(AsyncAlgorithms 1.1, *) +extension AsyncShareSequence: AsyncSequence { + typealias Element = Base.Element + typealias Failure = Base.Failure + + struct Iterator: AsyncIteratorProtocol { + let side: Side + + init(_ iteration: Iteration) { + side = Side(iteration) + } + + mutating func next() async rethrows -> Element? { + try await side.next(isolation: nil) + } + + mutating func next(isolation actor: isolated (any Actor)?) async throws(Failure) -> Element? { + try await side.next(isolation: actor) + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(extent.iteration) + } +} + +#endif diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 7376c68e..a733ad87 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -153,6 +153,10 @@ struct ManagedCriticalState { return try critical(&header.pointee) } } + + func withLock(_ critical: (inout State) throws -> R) rethrows -> R { + return try withCriticalRegion(critical) + } } extension ManagedCriticalState: @unchecked Sendable where State: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift b/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift index 9167a4c1..8283f6e8 100644 --- a/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift @@ -10,6 +10,7 @@ //===----------------------------------------------------------------------===// public struct GatedSequence { + public typealias Failure = Never let elements: [Element] let gates: [Gate] var index = 0 @@ -44,6 +45,15 @@ extension GatedSequence: AsyncSequence { await gate.enter() return element } + + public mutating func next(isolation actor: isolated (any Actor)?) async throws(Never) -> Element? { + guard gatedElements.count > 0 else { + return nil + } + let (element, gate) = gatedElements.removeFirst() + await gate.enter() + return element + } } public func makeAsyncIterator() -> Iterator { diff --git a/Tests/AsyncAlgorithmsTests/TestShare.swift b/Tests/AsyncAlgorithmsTests/TestShare.swift new file mode 100644 index 00000000..ac817536 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestShare.swift @@ -0,0 +1,583 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.2) + +import XCTest +import AsyncAlgorithms +import Synchronization + +@available(macOS 15.0, *) +final class TestShare: XCTestCase { + + // MARK: - Basic Functionality Tests + + func test_share_delivers_elements_to_multiple_consumers() async { + let source = [1, 2, 3, 4, 5] + let shared = source.async.share() + let gate1 = Gate() + let gate2 = Gate() + + async let consumer1 = Task.detached { + var results = [Int]() + var iterator = shared.makeAsyncIterator() + gate1.open() + await gate2.enter() + while let value = await iterator.next(isolation: nil) { + results.append(value) + } + return results + } + + async let consumer2 = Task.detached { + var results = [Int]() + var iterator = shared.makeAsyncIterator() + gate2.open() + await gate1.enter() + while let value = await iterator.next(isolation: nil) { + results.append(value) + } + return results + } + let results1 = await consumer1.value + let results2 = await consumer2.value + + XCTAssertEqual(results1, [1, 2, 3, 4, 5]) + XCTAssertEqual(results2, [1, 2, 3, 4, 5]) + } + + func test_share_with_single_consumer() async { + let source = [1, 2, 3, 4, 5] + let shared = source.async.share() + + var results = [Int]() + for await value in shared { + results.append(value) + } + + XCTAssertEqual(results, [1, 2, 3, 4, 5]) + } + + func test_share_with_empty_source() async { + let source = [Int]() + let shared = source.async.share() + + var results = [Int]() + for await value in shared { + results.append(value) + } + + XCTAssertEqual(results, []) + } + + // MARK: - Buffering Policy Tests + + func test_share_with_bounded_buffering() async { + var gated = GatedSequence([1, 2, 3, 4, 5]) + let shared = gated.share(bufferingPolicy: .bounded(2)) + + let results1 = Mutex([Int]()) + let results2 = Mutex([Int]()) + let gate1 = Gate() + let gate2 = Gate() + + let consumer1 = Task { + var iterator = shared.makeAsyncIterator() + gate1.open() + await gate2.enter() + // Consumer 1 reads first element + if let value = await iterator.next(isolation: nil) { + results1.withLock { $0.append(value) } + } + // Delay to allow consumer 2 to get ahead + try? await Task.sleep(for: .milliseconds(10)) + // Continue reading + while let value = await iterator.next(isolation: nil) { + results1.withLock { $0.append(value) } + } + } + + let consumer2 = Task { + var iterator = shared.makeAsyncIterator() + gate2.open() + await gate1.enter() + // Consumer 2 reads all elements quickly + while let value = await iterator.next(isolation: nil) { + results2.withLock { $0.append(value) } + } + } + + // Advance the gated sequence to make elements available + gated.advance() // 1 + gated.advance() // 2 + gated.advance() // 3 + gated.advance() // 4 + gated.advance() // 5 + + await consumer1.value + await consumer2.value + + // Both consumers should receive all elements + XCTAssertEqual(results1.withLock { $0 }.sorted(), [1, 2, 3, 4, 5]) + XCTAssertEqual(results2.withLock { $0 }.sorted(), [1, 2, 3, 4, 5]) + } + + func test_share_with_unbounded_buffering() async { + let source = [1, 2, 3, 4, 5] + let shared = source.async.share(bufferingPolicy: .unbounded) + + let results1 = Mutex([Int]()) + let results2 = Mutex([Int]()) + let gate1 = Gate() + let gate2 = Gate() + + let consumer1 = Task { + var iterator = shared.makeAsyncIterator() + gate2.open() + await gate1.enter() + while let value = await iterator.next(isolation: nil) { + results1.withLock { $0.append(value) } + // Add some delay to consumer 1 + try? await Task.sleep(for: .milliseconds(1)) + } + } + + let consumer2 = Task { + var iterator = shared.makeAsyncIterator() + gate1.open() + await gate2.enter() + while let value = await iterator.next(isolation: nil) { + results2.withLock { $0.append(value) } + } + } + + await consumer1.value + await consumer2.value + + XCTAssertEqual(results1.withLock { $0 }, [1, 2, 3, 4, 5]) + XCTAssertEqual(results2.withLock { $0 }, [1, 2, 3, 4, 5]) + } + + func test_share_with_bufferingLatest_buffering() async { + var gated = GatedSequence([1, 2, 3, 4, 5]) + let shared = gated.share(bufferingPolicy: .bufferingLatest(2)) + + let fastResults = Mutex([Int]()) + let slowResults = Mutex([Int]()) + let gate1 = Gate() + let gate2 = Gate() + + let fastConsumer = Task.detached { + var iterator = shared.makeAsyncIterator() + gate2.open() + await gate1.enter() + while let value = await iterator.next(isolation: nil) { + fastResults.withLock { $0.append(value) } + } + } + + let slowConsumer = Task.detached { + var iterator = shared.makeAsyncIterator() + gate1.open() + await gate2.enter() + // Read first element immediately + if let value = await iterator.next(isolation: nil) { + slowResults.withLock { $0.append(value) } + } + // Add significant delay to let buffer fill up and potentially overflow + try? await Task.sleep(for: .milliseconds(50)) + // Continue reading remaining elements + while let value = await iterator.next(isolation: nil) { + slowResults.withLock { $0.append(value) } + } + } + + // Release all elements quickly to test buffer overflow behavior + gated.advance() // 1 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 2 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 3 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 4 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 5 + + await fastConsumer.value + await slowConsumer.value + + let slowResultsArray = slowResults.withLock { $0 } + + // Slow consumer should get the first element plus the latest elements in buffer + // With bufferingLatest(2), when buffer overflows, older elements are discarded + XCTAssertTrue(slowResultsArray.count >= 1, "Should have at least the first element") + XCTAssertEqual(slowResultsArray.first, 1, "Should start with first element") + + // Due to bufferingLatest policy, the slow consumer should favor newer elements + // It may miss some middle elements but should get the latest ones + let receivedSet = Set(slowResultsArray) + XCTAssertTrue(receivedSet.isSubset(of: Set([1, 2, 3, 4, 5]))) + + // With bufferingLatest, we expect the slow consumer to get newer elements + // when it finally catches up after the delay + if slowResultsArray.count > 1 { + let laterElements = Set(slowResultsArray.dropFirst()) + // Should have received some of the later elements (4, 5) due to bufferingLatest + XCTAssertTrue( + laterElements.contains(4) || laterElements.contains(5) || laterElements.contains(3), + "BufferingLatest should favor keeping newer elements" + ) + } + } + + func test_share_with_bufferingOldest_buffering() async { + var gated = GatedSequence([1, 2, 3, 4, 5]) + let shared = gated.share(bufferingPolicy: .bufferingOldest(2)) + + let fastResults = Mutex([Int]()) + let slowResults = Mutex([Int]()) + let gate1 = Gate() + let gate2 = Gate() + + let fastConsumer = Task { + var iterator = shared.makeAsyncIterator() + gate2.open() + await gate1.enter() + while let value = await iterator.next(isolation: nil) { + fastResults.withLock { $0.append(value) } + } + } + + let slowConsumer = Task { + var iterator = shared.makeAsyncIterator() + gate1.open() + await gate2.enter() + // Read first element immediately + if let value = await iterator.next(isolation: nil) { + slowResults.withLock { $0.append(value) } + } + // Add significant delay to let buffer fill up and potentially overflow + try? await Task.sleep(for: .milliseconds(50)) + // Continue reading remaining elements + while let value = await iterator.next(isolation: nil) { + slowResults.withLock { $0.append(value) } + } + } + + // Release all elements quickly to test buffer overflow behavior + gated.advance() // 1 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 2 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 3 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 4 + try? await Task.sleep(for: .milliseconds(5)) + gated.advance() // 5 + + await fastConsumer.value + await slowConsumer.value + + let slowResultsArray = slowResults.withLock { $0 } + + // Slow consumer should get the first element plus the oldest elements that fit in buffer + // With bufferingOldest(2), when buffer overflows, newer elements are ignored + XCTAssertTrue(slowResultsArray.count >= 1, "Should have at least the first element") + XCTAssertEqual(slowResultsArray.first, 1, "Should start with first element") + + // Due to bufferingOldest policy, the slow consumer should favor older elements + let receivedSet = Set(slowResultsArray) + XCTAssertTrue(receivedSet.isSubset(of: Set([1, 2, 3, 4, 5]))) + + // With bufferingOldest, when the buffer is full, newer elements are ignored + // So the slow consumer should be more likely to receive earlier elements + if slowResultsArray.count > 1 { + let laterElements = Array(slowResultsArray.dropFirst()) + // Should have received earlier elements due to bufferingOldest policy + // Elements 4 and 5 are less likely to be received since they're newer + let hasEarlierElements = laterElements.contains(2) || laterElements.contains(3) + let hasLaterElements = laterElements.contains(4) && laterElements.contains(5) + + // BufferingOldest should favor keeping older elements when buffer is full + // So we should be more likely to see earlier elements than later ones + XCTAssertTrue( + hasEarlierElements || !hasLaterElements, + "BufferingOldest should favor keeping older elements over newer ones" + ) + } + } + + // MARK: - Cancellation Tests + + func test_share_cancellation_of_single_consumer() async { + let shared = Indefinite(value: 42).async.share() + + let finished = expectation(description: "finished") + let iterated = expectation(description: "iterated") + + let task = Task { + var firstIteration = false + for await _ in shared { + if !firstIteration { + firstIteration = true + iterated.fulfill() + } + } + finished.fulfill() + } + + // Wait for the task to start iterating + await fulfillment(of: [iterated], timeout: 1.0) + + // Cancel the task + task.cancel() + + // Verify the task finishes + await fulfillment(of: [finished], timeout: 1.0) + } + + func test_share_cancellation_with_multiple_consumers() async { + let shared = Indefinite(value: 42).async.share() + + let consumer1Finished = expectation(description: "consumer1Finished") + let consumer2Finished = expectation(description: "consumer2Finished") + let consumer1Iterated = expectation(description: "consumer1Iterated") + let consumer2Iterated = expectation(description: "consumer2Iterated") + + let consumer1 = Task { + var firstIteration = false + for await _ in shared { + if !firstIteration { + firstIteration = true + consumer1Iterated.fulfill() + } + } + consumer1Finished.fulfill() + } + + let consumer2 = Task { + var firstIteration = false + for await _ in shared { + if !firstIteration { + firstIteration = true + consumer2Iterated.fulfill() + } + } + consumer2Finished.fulfill() + } + + // Wait for both consumers to start + await fulfillment(of: [consumer1Iterated, consumer2Iterated], timeout: 1.0) + + // Cancel only consumer1 + consumer1.cancel() + + // Consumer1 should finish + await fulfillment(of: [consumer1Finished], timeout: 1.0) + + // Consumer2 should still be running, so cancel it too + consumer2.cancel() + await fulfillment(of: [consumer2Finished], timeout: 1.0) + } + + func test_share_cancellation_cancels_source_when_no_consumers() async { + let source = Indefinite(value: 1).async + let shared = source.share() + + let finished = expectation(description: "finished") + let iterated = expectation(description: "iterated") + + let task = Task { + var iterator = shared.makeAsyncIterator() + if await iterator.next(isolation: nil) != nil { + iterated.fulfill() + } + // Task will be cancelled here, so iteration should stop + while await iterator.next(isolation: nil) != nil { + // Continue iterating until cancelled + } + finished.fulfill() + } + + await fulfillment(of: [iterated], timeout: 1.0) + task.cancel() + await fulfillment(of: [finished], timeout: 1.0) + } + + // MARK: - Error Handling Tests + + func test_share_propagates_errors_to_all_consumers() async { + let source = [1, 2, 3, 4, 5].async.map { value in + if value == 3 { + throw TestError.failure + } + return value + } + let shared = source.share() + + let consumer1Results = Mutex([Int]()) + let consumer2Results = Mutex([Int]()) + let consumer1Error = Mutex(nil) + let consumer2Error = Mutex(nil) + let gate1 = Gate() + let gate2 = Gate() + + let consumer1 = Task { + do { + var iterator = shared.makeAsyncIterator() + gate2.open() + await gate1.enter() + while let value = try await iterator.next() { + consumer1Results.withLock { $0.append(value) } + } + } catch { + consumer1Error.withLock { $0 = error } + } + } + + let consumer2 = Task { + do { + var iterator = shared.makeAsyncIterator() + gate1.open() + await gate2.enter() + while let value = try await iterator.next() { + consumer2Results.withLock { $0.append(value) } + } + } catch { + consumer2Error.withLock { $0 = error } + } + } + + await consumer1.value + await consumer2.value + + // Both consumers should receive the first two elements + XCTAssertEqual(consumer1Results.withLock { $0 }, [1, 2]) + XCTAssertEqual(consumer2Results.withLock { $0 }, [1, 2]) + + // Both consumers should receive the error + XCTAssertTrue(consumer1Error.withLock { $0 is TestError }) + XCTAssertTrue(consumer2Error.withLock { $0 is TestError }) + } + + // MARK: - Timing and Race Condition Tests + + func test_share_with_late_joining_consumer() async { + var gated = GatedSequence([1, 2, 3, 4, 5]) + let shared = gated.share(bufferingPolicy: .unbounded) + + let earlyResults = Mutex([Int]()) + let lateResults = Mutex([Int]()) + + // Start early consumer + let earlyConsumer = Task { + var iterator = shared.makeAsyncIterator() + while let value = await iterator.next(isolation: nil) { + earlyResults.withLock { $0.append(value) } + } + } + + // Advance some elements + gated.advance() // 1 + gated.advance() // 2 + + // Give early consumer time to consume + try? await Task.sleep(for: .milliseconds(10)) + + // Start late consumer + let lateConsumer = Task { + var iterator = shared.makeAsyncIterator() + while let value = await iterator.next(isolation: nil) { + lateResults.withLock { $0.append(value) } + } + } + + // Advance remaining elements + gated.advance() // 3 + gated.advance() // 4 + gated.advance() // 5 + + await earlyConsumer.value + await lateConsumer.value + + // Early consumer gets all elements + XCTAssertEqual(earlyResults.withLock { $0 }, [1, 2, 3, 4, 5]) + // Late consumer only gets elements from when it joined + XCTAssertTrue(lateResults.withLock { $0.count <= 5 }) + } + + func test_share_iterator_independence() async { + let source = [1, 2, 3, 4, 5] + let shared = source.async.share() + + var iterator1 = shared.makeAsyncIterator() + var iterator2 = shared.makeAsyncIterator() + + // Both iterators should independently get the same elements + let value1a = await iterator1.next(isolation: nil) + let value2a = await iterator2.next(isolation: nil) + + let value1b = await iterator1.next(isolation: nil) + let value2b = await iterator2.next(isolation: nil) + + XCTAssertEqual(value1a, 1) + XCTAssertEqual(value2a, 1) + XCTAssertEqual(value1b, 2) + XCTAssertEqual(value2b, 2) + } + + // MARK: - Memory and Resource Management Tests + + func test_share_cleans_up_when_all_consumers_finish() async { + let source = [1, 2, 3] + let shared = source.async.share() + + var results = [Int]() + for await value in shared { + results.append(value) + } + + XCTAssertEqual(results, [1, 2, 3]) + + // Create a new iterator after the sequence finished + var newIterator = shared.makeAsyncIterator() + let value = await newIterator.next(isolation: nil) + XCTAssertNil(value) // Should return nil since source is exhausted + } + + func test_share_multiple_sequential_consumers() async { + let source = [1, 2, 3, 4, 5] + let shared = source.async.share(bufferingPolicy: .unbounded) + + // First consumer + var results1 = [Int]() + for await value in shared { + results1.append(value) + } + + // Second consumer (starting after first finished) + var results2 = [Int]() + for await value in shared { + results2.append(value) + } + + XCTAssertEqual(results1, [1, 2, 3, 4, 5]) + XCTAssertEqual(results2, []) // Should be empty since source is exhausted + } +} + +// MARK: - Helper Types + +private enum TestError: Error, Equatable { + case failure +} + +#endif From daf3c0d7237267c5f63d12bd844cc5e7e65f10bf Mon Sep 17 00:00:00 2001 From: Janosch Hildebrand Date: Wed, 1 Oct 2025 17:43:45 +0200 Subject: [PATCH 144/149] Fix typo in doc comment for share (#370) --- Sources/AsyncAlgorithms/AsyncShareSequence.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncAlgorithms/AsyncShareSequence.swift b/Sources/AsyncAlgorithms/AsyncShareSequence.swift index 6c76a4d1..ebe3492d 100644 --- a/Sources/AsyncAlgorithms/AsyncShareSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncShareSequence.swift @@ -33,7 +33,7 @@ where Element: Sendable, Self: SendableMetatype, AsyncIterator: SendableMetatype /// ## Example Usage /// /// ```swift - /// let numbers = [1, 2, 3, 4, 5].share.map { + /// let numbers = [1, 2, 3, 4, 5].async.map { /// try? await Task.sleep(for: .seconds(1)) /// return $0 /// } From 26111a6fb73ce448a41579bbdb12bdebd66672f1 Mon Sep 17 00:00:00 2001 From: Changmin Lee Date: Tue, 7 Oct 2025 01:28:20 +0900 Subject: [PATCH 145/149] Fix broken documentation links in Interspersed guide (#371) --- .../AsyncAlgorithms.docc/Guides/Intersperse.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md index fbe775d1..537ab27e 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md @@ -2,8 +2,8 @@ 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)] +[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift)] ```swift let numbers = [1, 2, 3].async.interspersed(with: 0) From 24bd5acc81f6675a3dba2a30b57a31704a7bfd85 Mon Sep 17 00:00:00 2001 From: Melissa Kilby Date: Mon, 27 Oct 2025 22:09:46 +0000 Subject: [PATCH 146/149] chore: restrict GitHub workflow permissions - future-proof (#373) Signed-off-by: Melissa Kilby --- .github/workflows/pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 341d6ca4..c6c6e1fd 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,5 +1,8 @@ name: Pull request +permissions: + contents: read + on: pull_request: types: [opened, reopened, synchronize] From 4686d2edd30840292c6c2b3da0fe9b614e914c00 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 19 Nov 2025 00:56:41 +0100 Subject: [PATCH 147/149] Add `MultiProducerSingleConsumerChannel` (#305) * Add `AsyncBackpressuredStream` proposal and implementation # Motivation The pitch to add external backpressure support to the standard libraries `AsyncStream` got returned for revision since there are larger open questions around `AsyncSequence`. However, having external backpressure in a source asynchronous sequence is becoming more and more important. # Modification This PR adds a modified proposal and implementation that brings the Swift Evolution proposal over to Swift Async Algorithms. * Update proposal and implementation * Update proposal * Add example project * Formatting * Fix Swift 6.0 build * Future direction for ~Copyable elements * Apply formatting * Fix CI * Move to 6.1 and update proposal * Guard tests * Minor edits to the proposal * Fix revision order * FIxup setOnTerminationCallback * Address review feedback * Rename to `MultiProducerSingleConsumerAsyncChannel` * Allow one termination callback per source. * Fix all sendable warnings * Remove unbounded strategy, rename copy -> makeAdditionalSource, rename asyncSequence() -> elements(), and move enqueue and cancel to the `CallbackHandle` previously called `CallbackToken` * Remove Example and fix docs and fix format * fixes grammar and some word order issues * Remove unnecessary consume --------- Co-authored-by: Joe Heck --- .editorconfig | 8 + .github/workflows/pull_request.yml | 1 + ...-mutli-producer-single-consumer-channel.md | 838 ++++++++ .../AsyncAlgorithms/Internal/_TinyArray.swift | 329 ++++ ...rSingleConsumerAsyncChannel+Internal.swift | 1747 +++++++++++++++++ ...tiProducerSingleConsumerAsyncChannel.swift | 637 ++++++ ...ducerSingleConsumerAsyncChannelTests.swift | 1128 +++++++++++ .../Support/ManualExecutor.swift | 30 + 8 files changed, 4718 insertions(+) create mode 100644 .editorconfig create mode 100644 Evolution/0016-mutli-producer-single-consumer-channel.md create mode 100644 Sources/AsyncAlgorithms/Internal/_TinyArray.swift create mode 100644 Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift create mode 100644 Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift create mode 100644 Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift create mode 100644 Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8b4c83bb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c6c6e1fd..e4804029 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,3 +22,4 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift Async Algorithms" + format_check_container_image: "swiftlang/swift:nightly-6.1-noble" # Needed since 6.0.x doesn't support sending keyword diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md new file mode 100644 index 00000000..b870513a --- /dev/null +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -0,0 +1,838 @@ +# MultiProducerSingleConsumerAsyncChannel + +* Proposal: [SAA-0016](0016-multi-producer-single-consumer-channel.md) +* Authors: [Franz Busch](https://github.com/FranzBusch) +* Status: **Implemented** + +## Revision +- 2023/12/18: Migrate proposal from Swift Evolution to Swift Async Algorithms. +- 2023/12/19: Add element size dependent strategy +- 2024/05/19: Rename to multi producer single consumer channel +- 2024/05/28: Add unbounded strategy +- 2025/03/24: Adopt `~Copyable` for correct semantics and better performance. + +## Introduction + +[SE-0314](https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md) +introduced new `Async[Throwing]Stream` types which act as root asynchronous +sequences. These two types allow bridging from synchronous callbacks such as +delegates to an asynchronous sequence. This proposal adds a new root primitive +with the goal of modeling asynchronous multi-producer-single-consumer systems. + +## Motivation + +After using the `AsyncSequence` protocol, the `Async[Throwing]Stream` types, and +the `Async[Throwing]Channel` types extensively over the past years, we learned +that there is a gap in the ecosystem for a type that provides strict +multi-producer-single-consumer guarantees with external backpressure support. +Additionally, any stream/channel like type needs to have a clear definition +about the following behaviors: + +1. Backpressure +2. Multi/single consumer support +3. Downstream consumer termination +4. Upstream producer termination + +The below sections are providing a detailed explanation of each of those. + +### Backpressure + +In general, backpressure is the mechanism that prevents a fast producer from +overwhelming a slow consumer. It helps the stability of the overall system by +regulating the flow of data between different components. Additionally, it +allows us to put an upper bound on the resource consumption of a system. In reality, +backpressure is used in almost all networked applications. + +In Swift, asynchronous sequences also have the concept of internal backpressure. +This is modeled by the pull-based implementation where a consumer has to call +`next` on the `AsyncIterator`. In this model, there is no way for a consumer to +overwhelm a producer since the producer controls the rate of pulling elements. + +However, the internal backpressure of an asynchronous sequence isn't the only +backpressure in play. There is also the source backpressure that is producing +the actual elements. For a backpressured system, it is important that every +component of such a system is aware of the backpressure of its consumer and its +producer. + +Let's take a quick look at how our current root asynchronous sequences are handling +this. + +`Async[Throwing]Stream` aims to support backpressure by providing a configurable +buffer and returning `Async[Throwing]Stream.Continuation.YieldResult` which +contains the current buffer depth from the `yield()` method. However, only +providing the current buffer depth on `yield()` is not enough to bridge a +backpressured system into an asynchronous sequence since this can only be used +as a "stop" signal, but we are missing a signal to indicate resuming the +production. The only viable backpressure strategy that can be implemented with +the current API is a timed backoff where we stop producing for some period of +time and then speculatively produce again. This is a very inefficient pattern +that produces high latencies and inefficient use of resources. + +`Async[Throwing]Channel` is a multi-producer-multi-consumer channel that only +supports asynchronous producers. Additionally, the backpressure strategy is +fixed by a buffer size of 1 element per producer. + +We are currently lacking a type that supports a configurable backpressure +strategy and both asynchronous and synchronous producers. + +### Multi/single consumer support + +The `AsyncSequence` protocol itself makes no assumptions about whether the +implementation supports multiple consumers or not. This allows the creation of +unicast and multicast asynchronous sequences. The difference between a unicast +and multicast asynchronous sequence is if they allow multiple iterators to be +created. `AsyncStream` does support the creation of multiple iterators and it +does handle multiple consumers correctly. On the other hand, +`AsyncThrowingStream` also supports multiple iterators but does `fatalError` +when more than one iterator has to suspend. The original proposal states: + +> As with any sequence, iterating over an AsyncStream multiple times, or +creating multiple iterators and iterating over them separately, may produce an +unexpected series of values. + +While that statement leaves room for any behavior, we learned that a clear distinction +of behavior for root asynchronous sequences is beneficial; especially when it comes to +how transformation algorithms are applied on top. + +### Downstream consumer termination + +Downstream consumer termination allows the producer to notify the consumer that +no more values are going to be produced. `Async[Throwing]Stream` does support +this by calling the `finish()` or `finish(throwing:)` methods of the +`Async[Throwing]Stream.Continuation`. However, `Async[Throwing]Stream` does not +handle the case that the `Continuation` may be `deinit`ed before one of the +finish methods is called. This currently leads to async streams that never +terminate. + +### Upstream producer termination + +Upstream producer termination is the inverse of downstream consumer termination, +where the producer is notified once the consumption has terminated. Currently, +`Async[Throwing]Stream` does expose the `onTermination` property on the +`Continuation`. The `onTermination` closure is invoked once the consumer has +terminated. The consumer can terminate in four separate cases: + +1. The asynchronous sequence was `deinit`ed and no iterator was created. +2. The iterator was `deinit`ed and the asynchronous sequence is unicast. +3. The consuming task is canceled. +4. The asynchronous sequence returned `nil` or threw. + +`Async[Throwing]Stream` currently invokes `onTermination` in all cases; however, +since `Async[Throwing]Stream` supports multiple consumers (as discussed in the +`Multi/single consumer support` section), a single consumer task being canceled +leads to the termination of all consumers. This is not expected from multicast +asynchronous sequences in general. + +## Proposed solution + +The above motivation lays out the expected behaviors for any consumer/producer +system and compares them to the behaviors of `Async[Throwing]Stream` and +`Async[Throwing]Channel`. + +This section proposes a new type called `MultiProducerSingleConsumerAsyncChannel` +that implements all of the above-mentioned behaviors. Importantly, this proposed +solution is taking advantage of `~Copyable` types to model the +multi-producer-single-consumer behavior. While the current `AsyncSequence` +protocols are not supporting `~Copyable` types, we provide a way to convert the +proposed channel to an asynchronous sequence. This leaves us room to support any +potential future asynchronous streaming protocol that supports `~Copyable`. + +### Creating a MultiProducerSingleConsumerAsyncChannel + +You can create an `MultiProducerSingleConsumerAsyncChannel` instance using the +`makeChannel(of:backpressureStrategy:)` method. This method returns you the +channel and the source. The source can be used to send new values to the +asynchronous channel. The new API specifically provides a +multi-producer/single-consumer pattern. + +```swift +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) + +// The channel and source can be extracted from the returned type +let channel = consume channelAndSource.channel +let source = consume channelAndSource.source +``` + +The new proposed APIs offer two different backpressure strategies: +- Watermark: Using a low and high watermark. +- Unbounded: Unbounded buffering of the channel. **Only** use this if the + production is limited through some other means. + +The source is used to send values to the channel. It provides different APIs for +synchronous and asynchronous producers. All of the APIs are relaying the +backpressure of the channel. The synchronous multi-step APIs are the foundation +for all other APIs. Below is an example of how it can be used: + +```swift +do { + let sendResult = try source.send(contentsOf: sequence) + + switch sendResult { + case .produceMore: + // Trigger more production in the underlying system + + case .enqueueCallback(let callbackHandle): + // There are enough values in the channel already. We need to enqueue + // a callback to get notified when we should produce more. + callbackHandle.enqueueCallback(onProduceMore: { result in + switch result { + case .success: + // Trigger more production in the underlying system + case .failure(let error): + // Terminate the underlying producer + } + }) + } +} catch { + // `send(contentsOf:)` throws if the channel already terminated +} +``` + +The above API offers the most control and highest performance when bridging a +synchronous producer to a `MultiProducerSingleConsumerAsyncChannel`. First, you have +to send values using the `send(contentsOf:)` which returns a `SendResult`. The +result either indicates that more values should be produced or that a callback +should be enqueued by calling the `enqueueCallback(onProduceMore:)` method. +This callback is invoked once the backpressure strategy +decides that more values should be produced. This API aims to offer the most +flexibility with the greatest performance. The callback only has to be allocated +in the case where the producer needs to pause production. + +Additionally, the above API is the building block for some higher-level and +easier-to-use APIs to send values to the channel. Below is an +example of the two higher-level APIs. + +```swift +// Writing new values and providing a callback when to produce more +try source.send(contentsOf: sequence, onProduceMore: { result in + switch result { + case .success: + // Trigger more production + case .failure(let error): + // Terminate the underlying producer + } +}) + +// This method suspends until more values should be produced +try await source.send(contentsOf: sequence) +``` + +With the above APIs, we should be able to effectively bridge any system into a +`MultiProducerSingleConsumerAsyncChannel` regardless of whether the system is callback-based, +blocking, or asynchronous. + +### Multi producer + +To support multiple producers, the source offers a `copy` method to produce a new +source. The source is returned `sending`, so it is in a disconnected isolation +region from the original source, allowing it to be passed into a different isolation +region to concurrently produce elements. + +```swift +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) +) +var channel = consume channelAndSource.channel +var source1 = consume channelAndSource.source +var source2 = source1.makeAdditionalSource() + +group.addTask { + try await source1.send(1) +} + +group.addTask() { + try await source2.send(2) +} + +print(await channel.next()) // Prints either 1 or 2 depending on which child task runs first +print(await channel.next()) // Prints either 1 or 2 depending on which child task runs first +``` + +### Downstream consumer termination + +> When reading the next two examples of termination behavior, keep in mind +that the newly proposed APIs are providing a strict single consumer channel. + +Calling `finish()` terminates the downstream consumer. Below is an example of +this: + +```swift +// Termination through calling finish +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source + +try await source.send(1) +source.finish() + +print(await channel.next()) // Prints Optional(1) +print(await channel.next()) // Prints nil +``` + +If the channel has a failure type it can also be finished with an error. + +```swift +// Termination through calling finish +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: SomeError.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source + +try await source.send(1) +source.finish(throwing: SomeError) + +print(try await channel.next()) // Prints Optional(1) +print(try await channel.next()) // Throws SomeError +``` + +The other way to terminate the consumer is by deiniting the source. This has the +same effect as calling `finish()`. Since the source is a `~Copyable` type, this +will happen automatically when the source is last used or explicitly consumed. + +```swift +// Termination through deiniting the source +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source + +try await source.send(1) +_ = consume source // Explicitly consume the source + +print(await channel.next()) // Prints Optional(1) +print(await channel.next()) // Prints nil +``` + +### Upstream producer termination + +The producer will get notified about termination through the `onTerminate` +callback. Termination of the producer happens in the following scenarios: + +```swift +// Termination through task cancellation +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source +source.setOnTerminationCallback { print("Terminated") } + +let task = Task { + await channel.next() +} +task.cancel() // Prints Terminated +``` + +```swift +// Termination through deiniting the channel +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source +source.setOnTerminationCallback { print("Terminated") } +_ = consume channel // Prints Terminated +``` + +```swift +// Termination through finishing the source and consuming the last element +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source +source.setOnTerminationCallback { print("Terminated") } + +_ = try await source.send(1) +source.finish() + +print(await channel.next()) // Prints Optional(1) +await channel.next() // Prints Terminated +``` + +```swift +// Termination through deiniting the last source and consuming the last element +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source1 = consume channelAndSource.source +var source2 = source1.makeAdditionalSource() +source1.setOnTerminationCallback { print("Terminated") } + +_ = try await source1.send(1) +_ = consume source1 +_ = try await source2.send(2) + +print(await channel.next()) // Prints Optional(1) +print(await channel.next()) // Prints Optional(2) +_ = consume source2 +await channel.next() // Prints Terminated +``` + +Similar to the downstream consumer termination, trying to send more elements after the +producer has been terminated will result in an error thrown from the send methods. + +## Detailed design + +```swift +#if compiler(>=6.1) +/// An error that is thrown from the various `send` methods of the +/// ``MultiProducerSingleConsumerAsyncChannel/Source``. +/// +/// This error is thrown when the channel is already finished when +/// trying to send new elements to the source. +public struct MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError: Error { } + +/// A multi-producer single-consumer channel. +/// +/// The ``MultiProducerSingleConsumerAsyncChannel`` provides a ``MultiProducerSingleConsumerAsyncChannel/Source`` to +/// send values to the channel. The channel supports different back pressure strategies to control the +/// buffering and demand. The channel will buffer values until its backpressure strategy decides that the +/// producer have to wait. +/// +/// This channel is also suitable for the single-producer single-consumer use-case +/// +/// ## Using a MultiProducerSingleConsumerAsyncChannel +/// +/// To use a ``MultiProducerSingleConsumerAsyncChannel`` you have to create a new channel with its source first by calling +/// the ``MultiProducerSingleConsumerAsyncChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. +/// Afterwards, you can pass the source to the producer and the channel to the consumer. +/// +/// ``` +/// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( +/// of: Int.self, +/// backpressureStrategy: .watermark(low: 2, high: 4) +/// ) +/// +/// // The channel and source can be extracted from the returned type +/// let channel = consume channelAndSource.channel +/// let source = consume channelAndSource.source +/// ``` +/// +/// ### Asynchronous producing +/// +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerAsyncChannel/Source/send(_:)-8eo96`` +/// and ``MultiProducerSingleConsumerAsyncChannel/Source/send(contentsOf:)``. Backpressure results in calls +/// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. +/// +/// ``` +/// try await withThrowingTaskGroup(of: Void.self) { group in +/// group.addTask { +/// try await source.send(1) +/// try await source.send(2) +/// try await source.send(3) +/// } +/// +/// for await element in channel { +/// print(element) +/// } +/// } +/// ``` +/// +/// ### Synchronous produceing +/// +/// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, +/// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. +/// +/// ```swift +/// do { +/// let sendResult = try source.send(contentsOf: sequence) +/// +/// switch sendResult { +/// case .produceMore: +/// // Trigger more production in the underlying system +/// +/// case .enqueueCallback(let callbackHandle): +/// // There are enough values in the channel already. We need to enqueue +/// // a callback to get notified when we should produce more. +/// callbackHandle.enqueueCallback(onProduceMore: { result in +/// switch result { +/// case .success: +/// // Trigger more production in the underlying system +/// case .failure(let error): +/// // Terminate the underlying producer +/// } +/// }) +/// } +/// } catch { +/// // `send(contentsOf:)` throws if the channel already terminated +/// } +/// ``` +/// +/// ### Multiple producers +/// +/// To support multiple producers the source offers a ``Source/makeAdditionalSource()`` method to produce a new source. +/// +/// ### Terminating the production of values +/// +/// The consumer can be terminated through multiple ways: +/// - Calling ``Source/finish(throwing:)``. +/// - Deiniting all sources. +/// +/// In both cases, if there are still elements buffered by the channel, then the consumer will receive +/// all buffered elements. Afterwards it will be terminated. +/// +/// ### Observing termination of the consumer +/// +/// When the consumer stops consumption by either deiniting the channel or the task calling ``next(isolation:)`` +/// getting cancelled, the source will get notified about the termination if a termination callback has been set +/// before by calling ``Source/setOnTerminationCallback(_:)``. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MultiProducerSingleConsumerAsyncChannel: ~Copyable { + /// A struct containing the initialized channel and source. + /// + /// This struct can be deconstructed by consuming the individual + /// components from it. + /// + /// ```swift + /// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + /// of: Int.self, + /// backpressureStrategy: .watermark(low: 5, high: 10) + /// ) + /// var channel = consume channelAndSource.channel + /// var source = consume channelAndSource.source + /// ``` + @frozen + public struct ChannelAndStream : ~Copyable { + /// The channel. + public var channel: MultiProducerSingleConsumerAsyncChannel + /// The source. + public var source: Source + } + + /// Initializes a new ``MultiProducerSingleConsumerAsyncChannel`` and an ``MultiProducerSingleConsumerAsyncChannel/Source``. + /// + /// - Parameters: + /// - elementType: The element type of the channel. + /// - failureType: The failure type of the channel. + /// - backpressureStrategy: The backpressure strategy that the channel should use. + /// - Returns: A tuple containing the channel and its source. The source should be passed to the + /// producer while the channel should be passed to the consumer. + public static func makeChannel( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Never.self, + backpressureStrategy: Source.BackpressureStrategy + ) -> ChannelAndStream + + /// Returns the next element. + /// + /// If this method returns `nil` it indicates that no further values can ever + /// be returned. The channel automatically closes when all sources have been deinited. + /// + /// If there are no elements and the channel has not been finished yet, this method will + /// suspend until an element is send to the channel. + /// + /// If the task calling this method is cancelled this method will return `nil`. + /// + /// - Parameter isolation: The callers isolation. + /// - Returns: The next buffered element. + public func next(isolation: isolated (any Actor)? = #isolation) async throws(Failure) -> Element? +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerAsyncChannel { + /// A struct to send values to the channel. + /// + /// Use this source to provide elements to the channel by calling one of the `send` methods. + public struct Source: ~Copyable, Sendable { + /// A struct representing the backpressure of the channel. + public struct BackpressureStrategy: Sendable { + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + public static func watermark(low: Int, high: Int) -> BackpressureStrategy + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. + /// + /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when + /// it is consumed from the channel, so it is recommended to provide a function that runs in constant time. + public static func watermark(low: Int, high: Int, waterLevelForElement: @escaping @Sendable (borrowing Element) -> Int) -> BackpressureStrategy + + /// An unbounded backpressure strategy. + /// + /// - Important: Only use this strategy if the production of elements is limited through some other mean. Otherwise + /// an unbounded backpressure strategy can result in infinite memory usage and cause + /// your process to run out of memory. + public static func unbounded() -> BackpressureStrategy + } + + /// A type that indicates the result of sending elements to the source. + public enum SendResult: ~Copyable, Sendable { + /// A handle that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this handle to enqueue a callback by calling the ``CallbackHandle/enqueueCallback(onProduceMore:)`` method. + /// + /// - Important: ``CallbackHandle/enqueueCallback(onProduceMore:)`` and ``CallbackHandle/cancelCallback()`` must + /// only be called once. + public struct CallbackHandle: Sendable, Hashable { + /// Enqueues a callback that will be invoked once more elements should be produced. + /// + /// - Important: Calling enqueue more than once is **not allowed**. + /// + /// - Parameters: + /// - onProduceMore: The callback which gets invoked once more elements should be produced. + @inlinable + public mutating func enqueueCallback( + onProduceMore: sending @escaping (Result) -> Void + ) + + /// Cancel an enqueued callback. + /// + /// - Note: This methods supports being called before ``enqueueCallback(onProduceMore:)`` is called. + /// + /// - Important: Calling enqueue more than once is **not allowed**. + @inlinable + public mutating func cancelCallback() + } + + /// Indicates that more elements should be produced and send to the source. + case produceMore + + /// Indicates that a callback should be enqueued. + case enqueueCallback(CallbackHandle) + } + + /// A callback to invoke when the channel finished. + /// + /// This is called after the last element has been consumed by the channel. + public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) + + /// Creates a new source which can be used to send elements to the channel concurrently. + /// + /// The channel will only automatically be finished if all existing sources have been deinited. + /// + /// - Returns: A new source for sending elements to the channel. + public mutating func makeAdditionalSource() -> Source + + /// Sends new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter sequence: The elements to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + public mutating func send( + contentsOf sequence: consuming sending S + ) throws -> SendResult where Element == S.Element, S: Sequence + + /// Send the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter element: The element to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + public mutating func send(_ element: sending consuming Element) throws -> SendResult + + /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(contentsOf:onProduceMore:)``. + public mutating func send( + contentsOf sequence: consuming sending S, + onProduceMore: @escaping @Sendable (Result) -> Void + ) where Element == S.Element, S: Sequence + + /// Sends the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - element: The element to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(_:onProduceMore:)``. + public mutating func send( + _ element: consuming sending Element, + onProduceMore: @escaping @Sendable (Result + ) -> Void) + + /// Send new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + public mutating func send( + contentsOf sequence: consuming sending S + ) async throws where Element == S.Element, S: Sequence + + /// Send new element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - element: The element to send to the channel. + public mutating func send(_ element: consuming sending Element) async throws + + /// Send the elements of the asynchronous sequence to the channel. + /// + /// This method returns once the provided asynchronous sequence or the channel finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + public mutating func send( + contentsOf sequence: consuming sending S + ) async throws where Element: Sendable, Element == S.Element, S: Sendable, S: AsyncSequence + + /// Indicates that the production terminated. + /// + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerAsyncChannel/next(isolation:)`` will return + /// `nil` or throw an error. + /// + /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept + /// new elements. + /// + /// - Parameters: + /// - error: The error to throw, or `nil`, to finish normally. + public consuming func finish(throwing error: Failure? = nil) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerAsyncChannel { + /// Converts the channel to an asynchronous sequence for consumption. + /// + /// - Important: The returned asynchronous sequence only supports a single iterator to be created and + /// will fatal error at runtime on subsequent calls to `makeAsyncIterator`. + public consuming func asyncSequence() -> some (AsyncSequence & Sendable) +} +``` + +## Comparison to other root asynchronous primitives + +### swift-async-algorithm: AsyncChannel + +The `AsyncChannel` is a multi-consumer/multi-producer root asynchronous sequence +which can be used to communicate between two tasks. It only offers asynchronous +production APIs and has an effective buffer of one per producer. This means that +any producer will be suspended until its value has been consumed. `AsyncChannel` +can handle multiple consumers and resumes them in FIFO order. + +### swift-nio: NIOAsyncSequenceProducer + +The NIO team has created their own root asynchronous sequence with the goal to +provide a high-performance sequence that can be used to bridge a NIO `Channel` +inbound stream into Concurrency. The `NIOAsyncSequenceProducer` is a highly +generic and fully inlinable type and quite unwieldy to use. This proposal is +heavily inspired by the learnings from this type but tries to create a more +flexible and easier-to-use API that fits into the standard library. + +## Future directions + +### Adaptive backpressure strategy + +The high/low watermark strategy is common in networking code; however, there are +other strategies such as an adaptive strategy that we could offer in the future. +An adaptive strategy regulates the backpressure based on the rate of +consumption and production. With the proposed new APIs, we can easily add further +strategies. + +### Support `~Copyable` elements + +In the future, we can extend the channel to support `~Copyable` elements. We +only need an underlying buffer primitive that can hold `~Copyable` types, and the +continuations need to support `~Copyable` elements as well. By making the +channel not directly conform to `AsyncSequence`, we can support this down the +road. + +## Alternatives considered + +### Provide an `onTermination` callback to the factory method + +During development of the new APIs, I first tried to provide the `onTermination` +callback in the `makeChannel` method. However, that showed significant usability +problems in scenarios where one wants to store the source in a type and +reference `self` in the `onTermination` closure at the same time; hence, I kept +the current pattern of setting the `onTermination` closure on the source. + +### Provide a `onConsumerCancellation` callback + +During the pitch phase, it was raised that we should provide a +`onConsumerCancellation` callback which gets invoked once the asynchronous +channel notices that the consuming task got cancelled. This callback could be +used to customize how cancellation is handled by the channel, e.g. one could +imagine writing a few more elements to the channel before finishing it. Right now, +the channel immediately returns `nil` or throws a `CancellationError` when it +notices cancellation. This proposal decided not to provide this customization +because it opens up the possibility that asynchronous channels are not terminating +when implemented incorrectly. Additionally, asynchronous sequences are not the +only place where task cancellation leads to an immediate error being thrown i.e. +`Task.sleep()` does the same. Hence, the value of the asynchronous not +terminating immediately brings little value when the next call in the iterating +task might throw. However, the implementation is flexible enough to add this in +the future, and we can just default it to the current behaviour. + +### Create a custom type for the `Result` of the `onProduceMore` callback + +The `onProducerMore` callback takes a `Result` which is used to +indicate if the producer should produce more or if the asynchronous channel +finished. We could introduce a new type for this, but the proposal decided +against it since it effectively is a result type. + +### Use an initializer instead of factory methods + +Instead of providing a `makeChannel` factory method, we could use an initializer +approach that takes a closure which gets the `Source` passed into. A similar API +has been offered with the `Continuation` based approach and +[SE-0388](https://github.com/apple/swift-evolution/blob/main/proposals/0388-async-stream-factory.md) +introduced new factory methods to solve some of the usability ergonomics with +the initializer based APIs. + +### Provide the type on older compilers + +To achieve maximum performance, the implementation is using `~Copyable` extensively. +On Swift versions before 6.1, there is a https://github.com/swiftlang/swift/issues/78048 when using; hence, this type +is only usable with Swift 6.1 and later compilers. + +## Acknowledgements + +- [Johannes Weiss](https://github.com/weissi) - For making me aware how +important this problem is and providing great ideas on how to shape the API. +- [Philippe Hausler](https://github.com/phausler) - For helping me designing the +APIs and continuously providing feedback +- [George Barnett](https://github.com/glbrntt) - For providing extensive code +reviews and testing the implementation. +- [Si Beaumont](https://github.com/simonjbeaumont) - For implementing the element size dependent strategy diff --git a/Sources/AsyncAlgorithms/Internal/_TinyArray.swift b/Sources/AsyncAlgorithms/Internal/_TinyArray.swift new file mode 100644 index 00000000..4d3e64a2 --- /dev/null +++ b/Sources/AsyncAlgorithms/Internal/_TinyArray.swift @@ -0,0 +1,329 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// ``_TinyArray`` is a ``RandomAccessCollection`` optimised to store zero or one ``Element``. +/// It supports arbitrary many elements but if only up to one ``Element`` is stored it does **not** allocate separate storage on the heap +/// and instead stores the ``Element`` inline. +@usableFromInline +struct _TinyArray { + @usableFromInline + enum Storage { + case one(Element) + case arbitrary([Element]) + } + + @usableFromInline + var storage: Storage +} + +// MARK: - TinyArray "public" interface + +extension _TinyArray: Equatable where Element: Equatable {} +extension _TinyArray: Hashable where Element: Hashable {} +extension _TinyArray: Sendable where Element: Sendable {} + +extension _TinyArray: RandomAccessCollection { + @usableFromInline + typealias Element = Element + + @usableFromInline + typealias Index = Int + + @inlinable + subscript(position: Int) -> Element { + get { + self.storage[position] + } + set { + self.storage[position] = newValue + } + } + + @inlinable + var startIndex: Int { + self.storage.startIndex + } + + @inlinable + var endIndex: Int { + self.storage.endIndex + } +} + +extension _TinyArray { + @inlinable + init(_ elements: some Sequence) { + self.storage = .init(elements) + } + + @inlinable + init() { + self.storage = .init() + } + + @inlinable + mutating func append(_ newElement: Element) { + self.storage.append(newElement) + } + + @inlinable + mutating func append(contentsOf newElements: some Sequence) { + self.storage.append(contentsOf: newElements) + } + + @discardableResult + @inlinable + mutating func remove(at index: Int) -> Element { + self.storage.remove(at: index) + } + + @inlinable + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + try self.storage.removeAll(where: shouldBeRemoved) + } + + @inlinable + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + try self.storage.sort(by: areInIncreasingOrder) + } +} + +// MARK: - TinyArray.Storage "private" implementation + +extension _TinyArray.Storage: Equatable where Element: Equatable { + @inlinable + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.one(let lhs), .one(let rhs)): + return lhs == rhs + case (.arbitrary(let lhs), .arbitrary(let rhs)): + // we don't use lhs.elementsEqual(rhs) so we can hit the fast path from Array + // if both arrays share the same underlying storage: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1775 + return lhs == rhs + + case (.one(let element), .arbitrary(let array)), + (.arbitrary(let array), .one(let element)): + guard array.count == 1 else { + return false + } + return element == array[0] + + } + } +} +extension _TinyArray.Storage: Hashable where Element: Hashable { + @inlinable + func hash(into hasher: inout Hasher) { + // same strategy as Array: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1801 + hasher.combine(count) + for element in self { + hasher.combine(element) + } + } +} +extension _TinyArray.Storage: Sendable where Element: Sendable {} + +extension _TinyArray.Storage: RandomAccessCollection { + @inlinable + subscript(position: Int) -> Element { + get { + switch self { + case .one(let element): + guard position == 0 else { + fatalError("index \(position) out of bounds") + } + return element + case .arbitrary(let elements): + return elements[position] + } + } + set { + switch self { + case .one: + guard position == 0 else { + fatalError("index \(position) out of bounds") + } + self = .one(newValue) + case .arbitrary(var elements): + elements[position] = newValue + self = .arbitrary(elements) + } + } + } + + @inlinable + var startIndex: Int { + 0 + } + + @inlinable + var endIndex: Int { + switch self { + case .one: return 1 + case .arbitrary(let elements): return elements.endIndex + } + } +} + +extension _TinyArray.Storage { + @inlinable + init(_ elements: some Sequence) { + var iterator = elements.makeIterator() + guard let firstElement = iterator.next() else { + self = .arbitrary([]) + return + } + guard let secondElement = iterator.next() else { + // newElements just contains a single element + // and we hit the fast path + self = .one(firstElement) + return + } + + var elements: [Element] = [] + elements.reserveCapacity(elements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + while let nextElement = iterator.next() { + elements.append(nextElement) + } + self = .arbitrary(elements) + } + + @inlinable + init() { + self = .arbitrary([]) + } + + @inlinable + mutating func append(_ newElement: Element) { + self.append(contentsOf: CollectionOfOne(newElement)) + } + + @inlinable + mutating func append(contentsOf newElements: some Sequence) { + switch self { + case .one(let firstElement): + var iterator = newElements.makeIterator() + guard let secondElement = iterator.next() else { + // newElements is empty, nothing to do + return + } + var elements: [Element] = [] + elements.reserveCapacity(1 + newElements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + elements.appendRemainingElements(from: &iterator) + self = .arbitrary(elements) + + case .arbitrary(var elements): + if elements.isEmpty { + // if `self` is currently empty and `newElements` just contains a single + // element, we skip allocating an array and set `self` to `.one(firstElement)` + var iterator = newElements.makeIterator() + guard let firstElement = iterator.next() else { + // newElements is empty, nothing to do + return + } + guard let secondElement = iterator.next() else { + // newElements just contains a single element + // and we hit the fast path + self = .one(firstElement) + return + } + elements.reserveCapacity(elements.count + newElements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + elements.appendRemainingElements(from: &iterator) + self = .arbitrary(elements) + + } else { + elements.append(contentsOf: newElements) + self = .arbitrary(elements) + } + + } + } + + @discardableResult + @inlinable + mutating func remove(at index: Int) -> Element { + switch self { + case .one(let oldElement): + guard index == 0 else { + fatalError("index \(index) out of bounds") + } + self = .arbitrary([]) + return oldElement + + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + return elements.remove(at: index) + + } + } + + @inlinable + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + switch self { + case .one(let oldElement): + if try shouldBeRemoved(oldElement) { + self = .arbitrary([]) + } + + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + return try elements.removeAll(where: shouldBeRemoved) + + } + } + + @inlinable + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + switch self { + case .one: + // a collection of just one element is always sorted, nothing to do + break + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + + try elements.sort(by: areInIncreasingOrder) + } + } +} + +extension Array { + @inlinable + mutating func appendRemainingElements(from iterator: inout some IteratorProtocol) { + while let nextElement = iterator.next() { + append(nextElement) + } + } +} diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift new file mode 100644 index 00000000..ef2567dd --- /dev/null +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift @@ -0,0 +1,1747 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.1) +import DequeModule +import Synchronization + +@available(AsyncAlgorithms 1.1, *) +extension MultiProducerSingleConsumerAsyncChannel { + @usableFromInline + enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { + @usableFromInline + struct _Watermark: Sendable, CustomStringConvertible { + /// The low watermark where demand should start. + @usableFromInline + let _low: Int + + /// The high watermark where demand should be stopped. + @usableFromInline + let _high: Int + + /// The current watermark level. + @usableFromInline + var _currentWatermark: Int = 0 + + /// A closure that can be used to calculate the watermark impact of a single element + @usableFromInline + let _waterLevelForElement: (@Sendable (borrowing Element) -> Int)? + + @usableFromInline + var description: String { + "watermark(\(self._currentWatermark))" + } + + init(low: Int, high: Int, waterLevelForElement: (@Sendable (borrowing Element) -> Int)?) { + precondition(low <= high) + self._low = low + self._high = high + self._waterLevelForElement = waterLevelForElement + } + + @inlinable + mutating func didSend(elements: Deque>.SubSequence) -> Bool { + if let waterLevelForElement = self._waterLevelForElement { + for element in elements { + // This force-unwrap is safe since the element must not be taken + // from the box yet + self._currentWatermark += waterLevelForElement(element.wrapped!) + } + } else { + self._currentWatermark += elements.count + } + precondition(self._currentWatermark >= 0) + // We are demanding more until we reach the high watermark + return self._currentWatermark < self._high + } + + @inlinable + mutating func didConsume(element: SendableConsumeOnceBox) -> Bool { + if let waterLevelForElement = self._waterLevelForElement { + // This force-unwrap is safe since the element must not be taken + // from the box yet + self._currentWatermark -= waterLevelForElement(element.wrapped!) + } else { + self._currentWatermark -= 1 + } + precondition(self._currentWatermark >= 0) + // We start demanding again once we are below the low watermark + return self._currentWatermark < self._low + } + } + + /// A watermark based strategy. + case watermark(_Watermark) + + @usableFromInline + var description: String { + switch consume self { + case .watermark(let strategy): + return strategy.description + } + } + + @inlinable + mutating func didSend(elements: Deque>.SubSequence) -> Bool { + switch consume self { + case .watermark(var strategy): + let result = strategy.didSend(elements: elements) + self = .watermark(strategy) + return result + } + } + + @inlinable + mutating func didConsume(element: SendableConsumeOnceBox) -> Bool { + switch consume self { + case .watermark(var strategy): + let result = strategy.didConsume(element: element) + self = .watermark(strategy) + return result + } + } + } +} + +@available(AsyncAlgorithms 1.1, *) +extension MultiProducerSingleConsumerAsyncChannel { + @usableFromInline + final class _Storage: Sendable { + @usableFromInline + let _stateMachine: Mutex<_StateMachine> + + @inlinable + init( + backpressureStrategy: _InternalBackpressureStrategy + ) { + self._stateMachine = Mutex<_StateMachine>(_StateMachine(backpressureStrategy: backpressureStrategy)) + } + + func setOnTerminationCallback( + sourceID: UInt64, + callback: (@Sendable () -> Void)? + ) { + let action = self._stateMachine.withLock { + $0.setOnTerminationCallback(sourceID: sourceID, callback: callback) + } + + switch action { + case .callOnTermination(let onTermination): + onTermination() + case .none: + break + } + } + + func channelDeinitialized() { + let action = self._stateMachine.withLock { + $0.channelDeinitialized() + } + + switch action { + case .callOnTerminations(let onTerminations): + onTerminations.forEach { $0.1() } + + case .failProducersAndCallOnTerminations(let producerContinuations, let onTerminations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + onTerminations.forEach { $0.1() } + + case .none: + break + } + } + + func sequenceInitialized() { + self._stateMachine.withLock { + $0.sequenceInitialized() + } + } + + func sequenceDeinitialized() { + let action = self._stateMachine.withLock { + $0.sequenceDeinitialized() + } + + switch action { + case .callOnTerminations(let onTerminations): + onTerminations.forEach { $0.1() } + + case .failProducersAndCallOnTerminations(let producerContinuations, let onTerminations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + onTerminations.forEach { $0.1() } + + case .none: + break + } + } + + func iteratorInitialized() { + self._stateMachine.withLock { + $0.iteratorInitialized() + } + } + + func iteratorDeinitialized() { + let action = self._stateMachine.withLock { + $0.iteratorDeinitialized() + } + + switch action { + case .callOnTerminations(let onTerminations): + onTerminations.forEach { $0.1() } + + case .failProducersAndCallOnTerminations(let producerContinuations, let onTerminations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + onTerminations.forEach { $0.1() } + + case .none: + break + } + } + + func sourceInitialized() -> UInt64 { + self._stateMachine.withLock { + $0.sourceInitialized() + } + } + + func sourceDeinitialized() { + let action = self._stateMachine.withLock { + $0.sourceDeinitialized() + } + + switch action { + case .resumeConsumerAndCallOnTerminations(let consumerContinuation, let failure, let onTerminations): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) + } + + onTerminations.forEach { $0.1() } + + case .none: + break + } + } + + @inlinable + func send( + contentsOf sequence: sending some Sequence + ) throws -> MultiProducerSingleConsumerAsyncChannel.Source.SendResult { + // We need to move the value into an optional since we don't have call-once + // closures in the Swift yet. + var optionalSequence = Optional(sequence) + let action = self._stateMachine.withLock { + let sequence = optionalSequence.takeSending()! + return $0.send(sequence) + } + + switch action { + case .returnProduceMore: + return .produceMore + + case .returnEnqueue(let callbackToken): + return .enqueueCallback(.init(id: callbackToken, storage: self)) + + case .resumeConsumerAndReturnProduceMore(let continuation, let element): + continuation.resume(returning: element.take()) + return .produceMore + + case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): + continuation.resume(returning: element.take()) + return .enqueueCallback(.init(id: callbackToken, storage: self)) + + case .throwFinishedError: + throw MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError() + } + } + + @inlinable + func enqueueProducer( + callbackToken: UInt64, + continuation: UnsafeContinuation + ) { + let action = self._stateMachine.withLock { + $0.enqueueContinuation(callbackToken: callbackToken, continuation: continuation) + } + + switch action { + case .resumeProducer(let continuation): + continuation.resume() + + case .resumeProducerWithError(let continuation, let error): + continuation.resume(throwing: error) + + case .none: + break + } + } + + @inlinable + func enqueueProducer( + callbackToken: UInt64, + onProduceMore: sending @escaping (Result) -> Void + ) { + // We need to move the value into an optional since we don't have call-once + // closures in the Swift yet. + var optionalOnProduceMore = Optional(onProduceMore) + let action = self._stateMachine.withLock { + let onProduceMore = optionalOnProduceMore.takeSending()! + return $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + + switch action { + case .resumeProducer(let onProduceMore): + onProduceMore(Result.success(())) + + case .resumeProducerWithError(let onProduceMore, let error): + onProduceMore(Result.failure(error)) + + case .none: + break + } + } + + @inlinable + func cancelProducer( + callbackToken: UInt64 + ) { + let action = self._stateMachine.withLock { + $0.cancelProducer(callbackToken: callbackToken) + } + + switch action { + case .resumeProducerWithCancellationError(let onProduceMore): + switch onProduceMore { + case .closure(let onProduceMore): + onProduceMore(.failure(CancellationError())) + case .continuation(let continuation): + continuation.resume(throwing: CancellationError()) + } + + case .none: + break + } + } + + @inlinable + func finish(_ failure: Failure?) { + let action = self._stateMachine.withLock { + $0.finish(failure) + } + + switch action { + case .callOnTerminations(let onTerminations): + onTerminations.forEach { $0.1() } + + case .resumeConsumerAndCallOnTerminations(let consumerContinuation, let failure, let onTerminations): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) + } + + onTerminations.forEach { $0.1() } + + case .resumeProducers(let producerContinuations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + + case .none: + break + } + } + + @inlinable + func next(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { + let action = self._stateMachine.withLock { + $0.next() + } + + switch action { + case .returnElement(let element): + return element.take() + + case .returnElementAndResumeProducers(let element, let producerContinuations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.success(())) + case .continuation(let continuation): + continuation.resume() + } + } + + return element.take() + + case .returnFailureAndCallOnTerminations(let failure, let onTerminations): + onTerminations.forEach { $0.1() } + switch failure { + case .some(let error): + throw error + + case .none: + return nil + } + + case .returnNil: + return nil + + case .suspendTask: + return try await self.suspendNext() + } + } + + @inlinable + func suspendNext(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + let action = self._stateMachine.withLock { + $0.suspendNext(continuation: continuation) + } + + switch action { + case .resumeConsumerWithElement(let continuation, let element): + continuation.resume(returning: element.take()) + + case .resumeConsumerWithElementAndProducers( + let continuation, + let element, + let producerContinuations + ): + continuation.resume(returning: element.take()) + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.success(())) + case .continuation(let continuation): + continuation.resume() + } + } + + case .resumeConsumerWithFailureAndCallOnTerminations( + let continuation, + let failure, + let onTerminations + ): + switch failure { + case .some(let error): + continuation.resume(throwing: error) + + case .none: + continuation.resume(returning: nil) + } + onTerminations.forEach { $0.1() } + + case .resumeConsumerWithNil(let continuation): + continuation.resume(returning: nil) + + case .none: + break + } + } + } onCancel: { + let action = self._stateMachine.withLock { + $0.cancelNext() + } + + switch action { + case .resumeConsumerWithNilAndCallOnTerminations(let continuation, let onTerminations): + continuation.resume(returning: nil) + onTerminations.forEach { $0.1() } + + case .failProducersAndCallOnTerminations(let producerContinuations, let onTerminations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + onTerminations.forEach { $0.1() } + + case .none: + break + } + } + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerAsyncChannel._Storage { + /// The state machine of the channel. + @usableFromInline + struct _StateMachine: ~Copyable { + /// The state machine's current state. + @usableFromInline + var _state: _State + + @usableFromInline + init( + backpressureStrategy: MultiProducerSingleConsumerAsyncChannel._InternalBackpressureStrategy + ) { + self._state = .channeling( + .init( + backpressureStrategy: backpressureStrategy, + iteratorInitialized: false, + sequenceInitialized: false, + buffer: .init(), + producerContinuations: .init(), + cancelledAsyncProducers: .init(), + hasOutstandingDemand: true, + activeProducers: 0, + nextCallbackTokenID: 0, + nextSourceID: 0 + ) + ) + } + + @inlinable + init(state: consuming _State) { + self._state = state + } + + /// Actions returned by `sourceDeinitialized()`. + @usableFromInline + enum SetOnTerminationCallback { + /// Indicates that `onTermination` should be called. + case callOnTermination( + @Sendable () -> Void + ) + } + @inlinable + mutating func setOnTerminationCallback( + sourceID: UInt64, + callback: (@Sendable () -> Void)? + ) -> SetOnTerminationCallback? { + switch consume self._state { + case .channeling(var channeling): + if let callback { + if let index = channeling.onTerminations.firstIndex(where: { $0.0 == sourceID }) { + channeling.onTerminations[index] = (sourceID, callback) + } else { + channeling.onTerminations.append((sourceID, callback)) + } + } else { + channeling.onTerminations.removeAll(where: { $0.0 == sourceID }) + } + + self = .init(state: .channeling(channeling)) + return .none + + case .sourceFinished(var sourceFinished): + if let callback { + if let index = sourceFinished.onTerminations.firstIndex(where: { $0.0 == sourceID }) { + sourceFinished.onTerminations[index] = (sourceID, callback) + } else { + sourceFinished.onTerminations.append((sourceID, callback)) + } + } else { + sourceFinished.onTerminations.removeAll(where: { $0.0 == sourceID }) + } + + self = .init(state: .sourceFinished(sourceFinished)) + return .none + + case .finished(let finished): + self = .init(state: .finished(finished)) + + guard let callback else { + return .none + } + return .callOnTermination(callback) + } + } + + @inlinable + mutating func sourceInitialized() -> UInt64 { + switch consume self._state { + case .channeling(var channeling): + channeling.activeProducers += 1 + let sourceID = channeling.nextSourceID() + self = .init(state: .channeling(channeling)) + return sourceID + + case .sourceFinished(var sourceFinished): + let sourceID = sourceFinished.nextSourceID() + self = .init(state: .sourceFinished(sourceFinished)) + return sourceID + + case .finished(let finished): + self = .init(state: .finished(finished)) + return .max // We use max to indicate that this is finished + } + } + + /// Actions returned by `sourceDeinitialized()`. + @usableFromInline + enum SourceDeinitialized { + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination`s should be called. + case resumeConsumerAndCallOnTerminations( + consumerContinuation: UnsafeContinuation, + failure: Failure?, + onTerminations: _TinyArray<(UInt64, @Sendable () -> Void)> + ) + } + + @inlinable + mutating func sourceDeinitialized() -> SourceDeinitialized? { + switch consume self._state { + case .channeling(var channeling): + channeling.activeProducers -= 1 + + guard channeling.activeProducers == 0 else { + // We still have more producers + self = .init(state: .channeling(channeling)) + + return nil + } + // This was the last producer so we can transition to source finished now + + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we are just going to mark + // the source as finished. + self = .init( + state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: nil, + onTerminations: channeling.onTerminations, + nextSourceID: channeling._nextSourceID + ) + ) + ) + + return nil + } + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(channeling.buffer.isEmpty, "Expected an empty buffer") + + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .resumeConsumerAndCallOnTerminations( + consumerContinuation: consumerContinuation, + failure: nil, + onTerminations: channeling.onTerminations + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished, finishing again has no effect. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(var finished): + finished.sourceFinished = true + self = .init(state: .finished(finished)) + return .none + } + } + + @inlinable + mutating func sequenceInitialized() { + switch consume self._state { + case .channeling(var channeling): + channeling.sequenceInitialized = true + self = .init(state: .channeling(channeling)) + + case .sourceFinished(var sourceFinished): + sourceFinished.sequenceInitialized = true + self = .init(state: .sourceFinished(sourceFinished)) + + case .finished(var finished): + finished.sequenceInitialized = true + self = .init(state: .finished(finished)) + } + } + + /// Actions returned by `sequenceDeinitialized()`. + @usableFromInline + enum ChannelOrSequenceDeinitializedAction { + /// Indicates that `onTermination`s should be called. + case callOnTerminations(_TinyArray<(UInt64, @Sendable () -> Void)>) + /// Indicates that all producers should be failed and `onTermination`s should be called. + case failProducersAndCallOnTerminations( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + _TinyArray<(UInt64, @Sendable () -> Void)> + ) + } + + @inlinable + mutating func sequenceDeinitialized() -> ChannelOrSequenceDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + guard channeling.iteratorInitialized else { + precondition(channeling.sequenceInitialized, "Sequence was not initialized") + // No iterator was created so we can transition to finished right away. + self = .init( + state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: false + ) + ) + ) + + return .failProducersAndCallOnTerminations( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTerminations + ) + } + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + self = .init(state: .channeling(channeling)) + + return .none + + case .sourceFinished(let sourceFinished): + guard sourceFinished.iteratorInitialized else { + precondition(sourceFinished.sequenceInitialized, "Sequence was not initialized") + // No iterator was created so we can transition to finished right away. + self = .init( + state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: true + ) + ) + ) + + return .callOnTerminations(sourceFinished.onTerminations) + } + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } + + @inlinable + mutating func channelDeinitialized() -> ChannelOrSequenceDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + guard channeling.sequenceInitialized else { + // No async sequence was created so we can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .failProducersAndCallOnTerminations( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTerminations + ) + } + // An async sequence was created so we need to ignore this deinit + self = .init(state: .channeling(channeling)) + return nil + + case .sourceFinished(let sourceFinished): + guard sourceFinished.sequenceInitialized else { + // No async sequence was created so we can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .callOnTerminations(sourceFinished.onTerminations) + } + // An async sequence was created so we need to ignore this deinit + self = .init(state: .sourceFinished(sourceFinished)) + return nil + + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } + + @inlinable + mutating func iteratorInitialized() { + switch consume self._state { + case .channeling(var channeling): + if channeling.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + channeling.iteratorInitialized = true + self = .init(state: .channeling(channeling)) + } + + case .sourceFinished(var sourceFinished): + if sourceFinished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + sourceFinished.iteratorInitialized = true + self = .init(state: .sourceFinished(sourceFinished)) + } + + case .finished(let finished): + if finished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: finished.sourceFinished + ) + ) + ) + } + } + } + + /// Actions returned by `iteratorDeinitialized()`. + @usableFromInline + enum IteratorDeinitializedAction { + /// Indicates that `onTermination`s should be called. + case callOnTerminations(_TinyArray<(UInt64, @Sendable () -> Void)>) + /// Indicates that all producers should be failed and `onTermination`s should be called. + case failProducersAndCallOnTerminations( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + _TinyArray<(UInt64, @Sendable () -> Void)> + ) + } + + @inlinable + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + if channeling.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: false + ) + ) + ) + + return .failProducersAndCallOnTerminations( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTerminations + ) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") + } + + case .sourceFinished(let sourceFinished): + if sourceFinished.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: true + ) + ) + ) + + return .callOnTerminations(sourceFinished.onTerminations) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") + } + + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } + + /// Actions returned by `send()`. + @usableFromInline + enum SendAction: ~Copyable { + /// Indicates that the producer should be notified to produce more. + case returnProduceMore + /// Indicates that the producer should be suspended to stop producing. + case returnEnqueue( + callbackToken: UInt64 + ) + /// Indicates that the consumer should be resumed and the producer should be notified to produce more. + case resumeConsumerAndReturnProduceMore( + continuation: UnsafeContinuation, + element: SendableConsumeOnceBox + ) + /// Indicates that the consumer should be resumed and the producer should be suspended. + case resumeConsumerAndReturnEnqueue( + continuation: UnsafeContinuation, + element: SendableConsumeOnceBox, + callbackToken: UInt64 + ) + /// Indicates that the producer has been finished. + case throwFinishedError + } + + @inlinable + mutating func send(_ sequence: sending some Sequence) -> sending SendAction { + switch consume self._state { + case .channeling(var channeling): + // We have an element and can resume the continuation + let bufferEndIndexBeforeAppend = channeling.buffer.endIndex + channeling.buffer + .append( + contentsOf: sequence.map { element in + // This is actually safe but there is no way for us to express this + // The sequence is send to us so all elements must be + // in a disconnected region. We just need to extract them once from + // the sequence. + nonisolated(unsafe) let disconnectedElement = element + return SendableConsumeOnceBox(wrapped: disconnectedElement) + } + ) + var shouldProduceMore = channeling.backpressureStrategy.didSend( + elements: channeling.buffer[bufferEndIndexBeforeAppend...] + ) + channeling.hasOutstandingDemand = shouldProduceMore + + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we just buffer the elements + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + guard let callbackToken else { + return .returnProduceMore + } + return .returnEnqueue(callbackToken: callbackToken) + } + guard let element = channeling.buffer.popFirst() else { + // We got a send of an empty sequence. We just tolerate this. + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + guard let callbackToken else { + return .returnProduceMore + } + return .returnEnqueue(callbackToken: callbackToken) + } + // This is actually safe but we can't express it right now. + // The element is taken from the deque once we and initally send it into + // the Deque. + nonisolated(unsafe) let disconnectedElement = element + // We need to tell the back pressure strategy that we consumed + shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore + + // We got a consumer continuation and an element. We can resume the consumer now + channeling.consumerContinuation = nil + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + guard let callbackToken else { + return .resumeConsumerAndReturnProduceMore( + continuation: consumerContinuation, + element: disconnectedElement + ) + } + return .resumeConsumerAndReturnEnqueue( + continuation: consumerContinuation, + element: disconnectedElement, + callbackToken: callbackToken + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished we are dropping the elements. + self = .init(state: .sourceFinished(sourceFinished)) + + return .throwFinishedError + + case .finished(let finished): + // If the source has finished we are dropping the elements. + self = .init(state: .finished(finished)) + + return .throwFinishedError + } + } + + /// Actions returned by `enqueueProducer()`. + @usableFromInline + enum EnqueueProducerAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer((Result) -> Void) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError((Result) -> Void, Error) + } + + @inlinable + mutating func enqueueProducer( + callbackToken: UInt64, + onProduceMore: sending @escaping (Result) -> Void + ) -> EnqueueProducerAction? { + switch consume self._state { + case .channeling(var channeling): + if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { + // Our producer got marked as cancelled. + channeling.cancelledAsyncProducers.remove(at: index) + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithError(onProduceMore, CancellationError()) + } else if channeling.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + self = .init(state: .channeling(channeling)) + + return .resumeProducer(onProduceMore) + } else { + channeling.suspendedProducers.append((callbackToken, .closure(onProduceMore))) + self = .init(state: .channeling(channeling)) + + return .none + } + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + + /// Actions returned by `enqueueContinuation()`. + @usableFromInline + enum EnqueueContinuationAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer(UnsafeContinuation) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError(UnsafeContinuation, Error) + } + + @inlinable + mutating func enqueueContinuation( + callbackToken: UInt64, + continuation: UnsafeContinuation + ) -> EnqueueContinuationAction? { + switch consume self._state { + case .channeling(var channeling): + if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { + // Our producer got marked as cancelled. + channeling.cancelledAsyncProducers.remove(at: index) + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithError(continuation, CancellationError()) + } else if channeling.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + self = .init(state: .channeling(channeling)) + + return .resumeProducer(continuation) + } else { + channeling.suspendedProducers.append((callbackToken, .continuation(continuation))) + self = .init(state: .channeling(channeling)) + + return .none + } + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) + } + } + + /// Actions returned by `cancelProducer()`. + @usableFromInline + enum CancelProducerAction { + /// Indicates that the producer should be notified about cancellation. + case resumeProducerWithCancellationError(_MultiProducerSingleConsumerSuspendedProducer) + } + + @inlinable + mutating func cancelProducer( + callbackToken: UInt64 + ) -> CancelProducerAction? { + switch consume self._state { + case .channeling(var channeling): + guard let index = channeling.suspendedProducers.firstIndex(where: { $0.0 == callbackToken }) else { + // The task that sends was cancelled before sending elements so the cancellation handler + // got invoked right away + channeling.cancelledAsyncProducers.append(callbackToken) + self = .init(state: .channeling(channeling)) + + return .none + } + // We have an enqueued producer that we need to resume now + let continuation = channeling.suspendedProducers.remove(at: index).1 + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithCancellationError(continuation) + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .none + } + } + + /// Actions returned by `finish()`. + @usableFromInline + enum FinishAction { + /// Indicates that `onTermination`s should be called. + case callOnTerminations(_TinyArray<(UInt64, @Sendable () -> Void)>) + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination`s should be called. + case resumeConsumerAndCallOnTerminations( + consumerContinuation: UnsafeContinuation, + failure: Failure?, + onTerminations: _TinyArray<(UInt64, @Sendable () -> Void)> + ) + /// Indicates that the producers should be resumed with an error. + case resumeProducers( + producerContinuations: _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + } + + @inlinable + mutating func finish(_ failure: Failure?) -> FinishAction? { + switch consume self._state { + case .channeling(let channeling): + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we are just going to mark + // the source as finished and terminate the current suspended producers. + self = .init( + state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: failure, + onTerminations: channeling.onTerminations, + nextSourceID: channeling._nextSourceID + ) + ) + ) + + return .resumeProducers( + producerContinuations: .init(channeling.suspendedProducers.lazy.map { $0.1 }) + ) + } + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(channeling.buffer.isEmpty, "Expected an empty buffer") + + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .resumeConsumerAndCallOnTerminations( + consumerContinuation: consumerContinuation, + failure: failure, + onTerminations: channeling.onTerminations + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished, finishing again has no effect. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(var finished): + finished.sourceFinished = true + self = .init(state: .finished(finished)) + return .none + } + } + + /// Actions returned by `next()`. + @usableFromInline + enum NextAction { + /// Indicates that the element should be returned to the caller. + case returnElement(SendableConsumeOnceBox) + /// Indicates that the element should be returned to the caller and that all producers should be called. + case returnElementAndResumeProducers( + SendableConsumeOnceBox, + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + /// Indicates that the `Failure` should be returned to the caller and that `onTermination`s should be called. + case returnFailureAndCallOnTerminations( + Failure?, + _TinyArray<(UInt64, @Sendable () -> Void)> + ) + /// Indicates that the `nil` should be returned to the caller. + case returnNil + /// Indicates that the `Task` of the caller should be suspended. + case suspendTask + } + + @inlinable + mutating func next() -> NextAction { + switch consume self._state { + case .channeling(var channeling): + guard channeling.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") + } + + guard let element = channeling.buffer.popFirst() else { + // There is nothing in the buffer to fulfil the demand so we need to suspend. + // We are not interacting with the backpressure strategy here because + // we are doing this inside `suspendNext` + self = .init(state: .channeling(channeling)) + + return .suspendTask + } + // We have an element to fulfil the demand right away. + let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore + + guard shouldProduceMore else { + // We don't have any new demand, so we can just return the element. + self = .init(state: .channeling(channeling)) + + return .returnElement(element) + } + // There is demand and we have to resume our producers + let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) + channeling.suspendedProducers.removeAll(keepingCapacity: true) + self = .init(state: .channeling(channeling)) + + return .returnElementAndResumeProducers(element, producers) + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + guard let element = sourceFinished.buffer.popFirst() else { + // We are returning the queued failure now and can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .returnFailureAndCallOnTerminations( + sourceFinished.failure, + sourceFinished.onTerminations + ) + } + self = .init(state: .sourceFinished(sourceFinished)) + + return .returnElement(element) + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .returnNil + } + } + + /// Actions returned by `suspendNext()`. + @usableFromInline + enum SuspendNextAction: ~Copyable { + /// Indicates that the consumer should be resumed. + case resumeConsumerWithElement(UnsafeContinuation, SendableConsumeOnceBox) + /// Indicates that the consumer and all producers should be resumed. + case resumeConsumerWithElementAndProducers( + UnsafeContinuation, + SendableConsumeOnceBox, + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + /// Indicates that the consumer should be resumed with the failure and that `onTermination`s should be called. + case resumeConsumerWithFailureAndCallOnTerminations( + UnsafeContinuation, + Failure?, + _TinyArray<(UInt64, @Sendable () -> Void)> + ) + /// Indicates that the consumer should be resumed with `nil`. + case resumeConsumerWithNil(UnsafeContinuation) + } + + @inlinable + mutating func suspendNext(continuation: UnsafeContinuation) -> SuspendNextAction? { + switch consume self._state { + case .channeling(var channeling): + guard channeling.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") + } + + // We have to check here again since we might have a producer interleave next and suspendNext + guard let element = channeling.buffer.popFirst() else { + // There is nothing in the buffer to fulfil the demand so we to store the continuation. + channeling.consumerContinuation = continuation + self = .init(state: .channeling(channeling)) + + return .none + } + + // We have an element to fulfil the demand right away. + let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore + + guard shouldProduceMore else { + // We don't have any new demand, so we can just return the element. + self = .init(state: .channeling(channeling)) + + return .resumeConsumerWithElement(continuation, element) + } + // There is demand and we have to resume our producers + let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) + channeling.suspendedProducers.removeAll(keepingCapacity: true) + self = .init(state: .channeling(channeling)) + + return .resumeConsumerWithElementAndProducers( + continuation, + element, + producers + ) + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + guard let element = sourceFinished.buffer.popFirst() else { + // We are returning the queued failure now and can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .resumeConsumerWithFailureAndCallOnTerminations( + continuation, + sourceFinished.failure, + sourceFinished.onTerminations + ) + } + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeConsumerWithElement(continuation, element) + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .resumeConsumerWithNil(continuation) + } + } + + /// Actions returned by `cancelNext()`. + @usableFromInline + enum CancelNextAction { + /// Indicates that the continuation should be resumed with nil, the producers should be finished and call onTerminations. + case resumeConsumerWithNilAndCallOnTerminations( + UnsafeContinuation, + _TinyArray<(UInt64, @Sendable () -> Void)> + ) + /// Indicates that the producers should be finished and call onTerminations. + case failProducersAndCallOnTerminations( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + _TinyArray<(UInt64, @Sendable () -> Void)> + ) + } + + @inlinable + mutating func cancelNext() -> CancelNextAction? { + switch consume self._state { + case .channeling(let channeling): + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: false + ) + ) + ) + + guard let consumerContinuation = channeling.consumerContinuation else { + return .failProducersAndCallOnTerminations( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTerminations + ) + } + precondition( + channeling.suspendedProducers.isEmpty, + "Internal inconsistency. Unexpected producer continuations." + ) + return .resumeConsumerWithNilAndCallOnTerminations( + consumerContinuation, + channeling.onTerminations + ) + + case .sourceFinished(let sourceFinished): + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .none + } + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerAsyncChannel._Storage._StateMachine { + @usableFromInline + enum _State: ~Copyable { + @usableFromInline + struct Channeling: ~Copyable { + /// The backpressure strategy. + @usableFromInline + var backpressureStrategy: MultiProducerSingleConsumerAsyncChannel._InternalBackpressureStrategy + + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool + + /// The onTermination callbacks. + @usableFromInline + var onTerminations: _TinyArray<(UInt64, @Sendable () -> Void)> + + /// The buffer of elements. + @usableFromInline + var buffer: Deque> + + /// The optional consumer continuation. + @usableFromInline + var consumerContinuation: UnsafeContinuation? + + /// The producer continuations. + @usableFromInline + var suspendedProducers: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)> + + /// The producers that have been cancelled. + @usableFromInline + var cancelledAsyncProducers: Deque + + /// Indicates if we currently have outstanding demand. + @usableFromInline + var hasOutstandingDemand: Bool + + /// The number of active producers. + @usableFromInline + var activeProducers: UInt64 + + /// The next callback token. + @usableFromInline + var nextCallbackTokenID: UInt64 + + /// The source ID. + @usableFromInline + var _nextSourceID: UInt64 + + var description: String { + "backpressure:\(self.backpressureStrategy.description) iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) consumerContinuation:\(self.consumerContinuation == nil) producerContinuations:\(self.suspendedProducers.count) cancelledProducers:\(self.cancelledAsyncProducers.count) hasOutstandingDemand:\(self.hasOutstandingDemand)" + } + + @inlinable + init( + backpressureStrategy: MultiProducerSingleConsumerAsyncChannel._InternalBackpressureStrategy, + iteratorInitialized: Bool, + sequenceInitialized: Bool, + onTerminations: _TinyArray<(UInt64, @Sendable () -> Void)> = .init(), + buffer: Deque>, + consumerContinuation: UnsafeContinuation? = nil, + producerContinuations: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)>, + cancelledAsyncProducers: Deque, + hasOutstandingDemand: Bool, + activeProducers: UInt64, + nextCallbackTokenID: UInt64, + nextSourceID: UInt64 + ) { + self.backpressureStrategy = backpressureStrategy + self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized + self.onTerminations = onTerminations + self.buffer = buffer + self.consumerContinuation = consumerContinuation + self.suspendedProducers = producerContinuations + self.cancelledAsyncProducers = cancelledAsyncProducers + self.hasOutstandingDemand = hasOutstandingDemand + self.activeProducers = activeProducers + self.nextCallbackTokenID = nextCallbackTokenID + self._nextSourceID = nextSourceID + } + + /// Generates the next callback token. + @inlinable + mutating func nextCallbackToken() -> UInt64 { + let id = self.nextCallbackTokenID + self.nextCallbackTokenID += 1 + return id + } + + /// Generates the next source ID. + @inlinable + mutating func nextSourceID() -> UInt64 { + let id = self._nextSourceID + self._nextSourceID += 1 + return id + } + } + + @usableFromInline + struct SourceFinished: ~Copyable { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool + + /// The buffer of elements. + @usableFromInline + var buffer: Deque> + + /// The failure that should be thrown after the last element has been consumed. + @usableFromInline + var failure: Failure? + + /// The onTermination callbacks. + @usableFromInline + var onTerminations: _TinyArray<(UInt64, @Sendable () -> Void)> + + /// The source ID. + @usableFromInline + var _nextSourceID: UInt64 + + var description: String { + "iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) failure:\(self.failure == nil)" + } + + @inlinable + init( + iteratorInitialized: Bool, + sequenceInitialized: Bool, + buffer: Deque>, + failure: Failure? = nil, + onTerminations: _TinyArray<(UInt64, @Sendable () -> Void)> = .init(), + nextSourceID: UInt64 + ) { + self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized + self.buffer = buffer + self.failure = failure + self.onTerminations = onTerminations + self._nextSourceID = nextSourceID + } + + /// Generates the next source ID. + @inlinable + mutating func nextSourceID() -> UInt64 { + let id = self._nextSourceID + self._nextSourceID += 1 + return id + } + } + + @usableFromInline + struct Finished: ~Copyable { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool + + /// Indicates if the source was finished. + @usableFromInline + var sourceFinished: Bool + + var description: String { + "iteratorInitialized:\(self.iteratorInitialized) sourceFinished:\(self.sourceFinished)" + } + + @inlinable + init( + iteratorInitialized: Bool, + sequenceInitialized: Bool, + sourceFinished: Bool + ) { + self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized + self.sourceFinished = sourceFinished + } + } + + /// The state once either any element was sent or `next()` was called. + case channeling(Channeling) + + /// The state once the underlying source signalled that it is finished. + case sourceFinished(SourceFinished) + + /// The state once there can be no outstanding demand. This can happen if: + /// 1. The iterator was deinited + /// 2. The underlying source finished and all buffered elements have been consumed + case finished(Finished) + + @usableFromInline + var description: String { + switch self { + case .channeling(let channeling): + return "channeling \(channeling.description)" + case .sourceFinished(let sourceFinished): + return "sourceFinished \(sourceFinished.description)" + case .finished(let finished): + return "finished \(finished.description)" + } + } + } +} + +@available(AsyncAlgorithms 1.1, *) +@usableFromInline +enum _MultiProducerSingleConsumerSuspendedProducer { + case closure((Result) -> Void) + case continuation(UnsafeContinuation) +} + +extension Optional where Wrapped: ~Copyable { + @usableFromInline + mutating func takeSending() -> sending Self { + let result = consume self + self = nil + return result + } +} + +@usableFromInline +struct SendableConsumeOnceBox { + @usableFromInline + var wrapped: Optional + + @inlinable + init(wrapped: consuming sending Wrapped) { + self.wrapped = .some(wrapped) + } + + @inlinable + consuming func take() -> sending Wrapped { + return self.wrapped.takeSending()! + } +} +#endif diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift new file mode 100644 index 00000000..131946fd --- /dev/null +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift @@ -0,0 +1,637 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.1) +/// An error that is thrown from the various `send` methods of the +/// ``MultiProducerSingleConsumerAsyncChannel/Source``. +/// +/// This error is thrown when the channel is already finished when +/// trying to send new elements to the source. +@available(AsyncAlgorithms 1.1, *) +public struct MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError: Error { + @usableFromInline + init() {} +} + +/// A multi-producer single-consumer channel. +/// +/// The ``MultiProducerSingleConsumerAsyncChannel`` provides a ``MultiProducerSingleConsumerAsyncChannel/Source`` to +/// send values to the channel. The channel supports different back pressure strategies to control the +/// buffering and demand. The channel will buffer values until its backpressure strategy decides that the +/// producer have to wait. +/// +/// This channel is also suitable for the single-producer single-consumer use-case +/// +/// ## Using a MultiProducerSingleConsumerAsyncChannel +/// +/// To use a ``MultiProducerSingleConsumerAsyncChannel`` you have to create a new channel with its source first by calling +/// the ``MultiProducerSingleConsumerAsyncChannel/makeChannel(of:throwing:backpressureStrategy:)`` method. +/// Afterwards, you can pass the source to the producer and the channel to the consumer. +/// +/// ``` +/// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( +/// of: Int.self, +/// backpressureStrategy: .watermark(low: 2, high: 4) +/// ) +/// +/// // The channel and source can be extracted from the returned type +/// let channel = consume channelAndSource.channel +/// let source = consume channelAndSource.source +/// ``` +/// +/// ### Asynchronous producing +/// +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerAsyncChannel/Source/send(contentsOf:)-1h7t9`` +/// and ``MultiProducerSingleConsumerAsyncChannel/Source/send(_:)->()``. Backpressure results in calls +/// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. +/// +/// ``` +/// try await withThrowingTaskGroup(of: Void.self) { group in +/// group.addTask { +/// try await source.send(1) +/// try await source.send(2) +/// try await source.send(3) +/// } +/// +/// for await element in channel { +/// print(element) +/// } +/// } +/// ``` +/// +/// ### Synchronous produceing +/// +/// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, +/// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. +/// +/// ```swift +/// do { +/// let sendResult = try source.send(contentsOf: sequence) +/// +/// switch sendResult { +/// case .produceMore: +/// // Trigger more production in the underlying system +/// +/// case .enqueueCallback(let callbackHandle): +/// // There are enough values in the channel already. We need to enqueue +/// // a callback to get notified when we should produce more. +/// callbackHandle.enqueueCallback(onProduceMore: { result in +/// switch result { +/// case .success: +/// // Trigger more production in the underlying system +/// case .failure(let error): +/// // Terminate the underlying producer +/// } +/// }) +/// } +/// } catch { +/// // `send(contentsOf:)` throws if the channel already terminated +/// } +/// ``` +/// +/// ### Multiple producers +/// +/// To support multiple producers the source offers a ``Source/makeAdditionalSource()`` method to produce a new source. +/// +/// ### Terminating the production of values +/// +/// The consumer can be terminated through multiple ways: +/// - Calling ``Source/finish(throwing:)``. +/// - Deiniting all sources. +/// +/// In both cases, if there are still elements buffered by the channel, then the consumer will receive +/// all buffered elements. Afterwards it will be terminated. +/// +/// ### Observing termination of the consumer +/// +/// When the consumer stops consumption by either deiniting the channel or the task calling ``next(isolation:)`` +/// getting cancelled, the source will get notified about the termination if a termination callback has been set +/// before by calling ``Source/setOnTerminationCallback(_:)``. +@available(AsyncAlgorithms 1.1, *) +public struct MultiProducerSingleConsumerAsyncChannel: ~Copyable { + /// The backing storage. + @usableFromInline + let storage: _Storage + + /// A struct containing the initialized channel and source. + /// + /// This struct can be deconstructed by consuming the individual + /// components from it. + /// + /// ```swift + /// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + /// of: Int.self, + /// backpressureStrategy: .watermark(low: 5, high: 10) + /// ) + /// var channel = consume channelAndSource.channel + /// var source = consume channelAndSource.source + /// ``` + @frozen + public struct ChannelAndStream: ~Copyable { + /// The channel. + @usableFromInline + var channel: MultiProducerSingleConsumerAsyncChannel? + + /// Takes and returns the channel. + /// + /// - Important: If this is called more than once it will result in a runtime crash. + @inlinable + public mutating func takeChannel() -> sending MultiProducerSingleConsumerAsyncChannel { + return self.channel.takeSending()! + } + /// The source. + public var source: Source + + init( + channel: consuming MultiProducerSingleConsumerAsyncChannel, + source: consuming Source + ) { + self.channel = .some(channel) + self.source = source + } + } + + /// Initializes a new ``MultiProducerSingleConsumerAsyncChannel`` and an ``MultiProducerSingleConsumerAsyncChannel/Source``. + /// + /// - Parameters: + /// - elementType: The element type of the channel. + /// - failureType: The failure type of the channel. + /// - backpressureStrategy: The backpressure strategy that the channel should use. + /// - Returns: A struct containing the channel and its source. The source should be passed to the + /// producer while the channel should be passed to the consumer. + public static func makeChannel( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Never.self, + backpressureStrategy: Source.BackpressureStrategy + ) -> sending ChannelAndStream { + let storage = _Storage( + backpressureStrategy: backpressureStrategy.internalBackpressureStrategy + ) + let source = Source(storage: storage) + + return .init(channel: .init(storage: storage), source: source) + } + + init(storage: _Storage) { + self.storage = storage + } + + deinit { + self.storage.channelDeinitialized() + } + + /// Returns the next element. + /// + /// If this method returns `nil` it indicates that no further values can ever + /// be returned. The channel automatically closes when all sources have been deinited. + /// + /// If there are no elements and the channel has not been finished yet, this method will + /// suspend until an element is send to the channel. + /// + /// If the task calling this method is cancelled this method will return `nil`. + /// + /// - Parameter isolation: The callers isolation. + /// - Returns: The next buffered element. + @inlinable + public mutating func next( + isolation: isolated (any Actor)? = #isolation + ) async throws(Failure) -> Element? { + do { + return try await self.storage.next() + } catch { + // This force-cast is safe since we only allow closing the source with this failure + // We only need this force cast since continuations don't support typed throws yet. + throw error as! Failure + } + } +} + +@available(AsyncAlgorithms 1.1, *) +extension MultiProducerSingleConsumerAsyncChannel { + /// A struct to send values to the channel. + /// + /// Use this source to provide elements to the channel by calling one of the `send` methods. + public struct Source: ~Copyable { + /// A struct representing the backpressure of the channel. + public struct BackpressureStrategy: Sendable { + var internalBackpressureStrategy: _InternalBackpressureStrategy + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + public static func watermark(low: Int, high: Int) -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .watermark( + .init(low: low, high: high, waterLevelForElement: nil) + ) + ) + } + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. + /// + /// - Important: `waterLevelForElement` will be called during a lock on each element when it is written into the source and when + /// it is consumed from the channel, so it must be side-effect free and at best constant in time. + public static func watermark( + low: Int, + high: Int, + waterLevelForElement: @escaping @Sendable (borrowing Element) -> Int + ) -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .watermark( + .init(low: low, high: high, waterLevelForElement: waterLevelForElement) + ) + ) + } + } + + /// A type that indicates the result of sending elements to the source. + public enum SendResult: ~Copyable, Sendable { + /// A handle that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this handle to enqueue a callback by calling the ``CallbackHandle/enqueueCallback(onProduceMore:)`` method. + /// + /// - Important: ``CallbackHandle/enqueueCallback(onProduceMore:)`` and ``CallbackHandle/cancelCallback()`` must + /// only be called once. + public struct CallbackHandle: Sendable, Hashable { + @usableFromInline + let _id: UInt64 + + @usableFromInline + let _storage: _Storage + + @usableFromInline + init(id: UInt64, storage: _Storage) { + self._id = id + self._storage = storage + } + + /// Enqueues a callback that will be invoked once more elements should be produced. + /// + /// - Important: Calling enqueue more than once is **not allowed**. + /// + /// - Parameters: + /// - onProduceMore: The callback which gets invoked once more elements should be produced. + @inlinable + public mutating func enqueueCallback( + onProduceMore: sending @escaping (Result) -> Void + ) { + self._storage.enqueueProducer(callbackToken: self._id, onProduceMore: onProduceMore) + } + + /// Cancel an enqueued callback. + /// + /// - Note: This methods supports being called before ``enqueueCallback(onProduceMore:)`` is called. + /// + /// - Important: Calling enqueue more than once is **not allowed**. + @inlinable + public mutating func cancelCallback() { + self._storage.cancelProducer(callbackToken: self._id) + } + + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs._id == rhs._id + } + + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(self._id) + } + } + + /// Indicates that more elements should be produced and send to the source. + case produceMore + + /// Indicates that a callback should be enqueued. + case enqueueCallback(CallbackHandle) + } + + @usableFromInline + let _storage: _Storage + + @usableFromInline + let _id: UInt64 + + internal init( + storage: _Storage + ) { + self._storage = storage + self._id = self._storage.sourceInitialized() + } + + deinit { + self._storage.sourceDeinitialized() + } + + /// Sets a callback to invoke when the channel terminated. + /// + /// This is called after the last element has been consumed by the channel. + /// If the channel has already terminated this callback is called immediately. + /// + /// - Important: Only one termination callback can be set per source. Setting a callback if + /// a previous one has been set will override the previous one. + public func setOnTerminationCallback(_ callback: (@Sendable () -> Void)?) { + self._storage.setOnTerminationCallback(sourceID: self._id, callback: callback) + } + + /// Creates a new source which can be used to send elements to the channel concurrently. + /// + /// The channel will only automatically be finished if all existing sources have been deinited. + /// + /// - Returns: A new source for sending elements to the channel. + public mutating func makeAdditionalSource() -> sending Self { + .init(storage: self._storage) + } + + /// Sends new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter sequence: The elements to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + public mutating func send( + contentsOf sequence: consuming sending S + ) throws -> SendResult where Element == S.Element, S: Sequence, Element: Copyable { + try self._storage.send(contentsOf: sequence) + } + + /// Send the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter element: The element to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + public mutating func send(_ element: consuming sending Element) throws -> SendResult { + try self._storage.send(contentsOf: CollectionOfOne(element)) + } + + /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(contentsOf:onProduceMore:)``. + @inlinable + public mutating func send( + contentsOf sequence: consuming sending S, + onProduceMore: @escaping @Sendable (Result) -> Void + ) where Element == S.Element, S: Sequence, Element: Copyable { + do { + let sendResult = try self.send(contentsOf: sequence) + + switch consume sendResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(var callbackHandle): + callbackHandle.enqueueCallback(onProduceMore: onProduceMore) + } + } catch { + onProduceMore(.failure(error)) + } + } + + /// Sends the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - element: The element to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(_:onProduceMore:)``. + @inlinable + public mutating func send( + _ element: consuming sending Element, + onProduceMore: @escaping @Sendable (Result) -> Void + ) { + do { + let sendResult = try self.send(element) + + switch consume sendResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(var callbackHandle): + callbackHandle.enqueueCallback(onProduceMore: onProduceMore) + } + } catch { + onProduceMore(.failure(error)) + } + } + + /// Send new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating func send( + contentsOf sequence: consuming sending S + ) async throws where Element == S.Element, S: Sequence, Element: Copyable { + let syncSend: (sending S, inout Self) throws -> SendResult = { try $1.send(contentsOf: $0) } + let sendResult = try syncSend(sequence, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackToken): + let id = callbackToken._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) + } + } + } + + /// Send new element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - element: The element to send to the channel. + @inlinable + public mutating func send(_ element: consuming sending Element) async throws { + let syncSend: (consuming sending Element, inout Self) throws -> SendResult = { try $1.send($0) } + let sendResult = try syncSend(element, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackHandle): + let id = callbackHandle._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) + } + } + } + + /// Send the elements of the asynchronous sequence to the channel. + /// + /// This method returns once the provided asynchronous sequence or the channel finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating func send(contentsOf sequence: consuming sending S) async throws + where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { + for try await element in sequence { + try await self.send(contentsOf: CollectionOfOne(element)) + } + } + + /// Indicates that the production terminated. + /// + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerAsyncChannel/next(isolation:)`` will return + /// `nil` or throw an error. + /// + /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept + /// new elements. + /// + /// - Parameters: + /// - error: The error to throw, or `nil`, to finish normally. + @inlinable + public consuming func finish(throwing error: Failure? = nil) { + self._storage.finish(error) + } + } +} + +@available(AsyncAlgorithms 1.1, *) +extension MultiProducerSingleConsumerAsyncChannel where Element: Copyable { + struct ChannelAsyncSequence: AsyncSequence { + @usableFromInline + final class _Backing: Sendable { + @usableFromInline + let storage: MultiProducerSingleConsumerAsyncChannel._Storage + + init(storage: MultiProducerSingleConsumerAsyncChannel._Storage) { + self.storage = storage + self.storage.sequenceInitialized() + } + + deinit { + self.storage.sequenceDeinitialized() + } + } + + @usableFromInline + let _backing: _Backing + + public func makeAsyncIterator() -> Self.Iterator { + .init(storage: self._backing.storage) + } + } + + /// Converts the channel to an asynchronous sequence for consumption. + /// + /// - Important: The returned asynchronous sequence only supports a single iterator to be created and + /// will fatal error at runtime on subsequent calls to `makeAsyncIterator`. + public consuming func elements() -> some (AsyncSequence & Sendable) { + ChannelAsyncSequence(_backing: .init(storage: self.storage)) + } +} + +@available(AsyncAlgorithms 1.1, *) +extension MultiProducerSingleConsumerAsyncChannel.ChannelAsyncSequence where Element: Copyable { + struct Iterator: AsyncIteratorProtocol { + @usableFromInline + final class _Backing { + @usableFromInline + let storage: MultiProducerSingleConsumerAsyncChannel._Storage + + init(storage: MultiProducerSingleConsumerAsyncChannel._Storage) { + self.storage = storage + self.storage.iteratorInitialized() + } + + deinit { + self.storage.iteratorDeinitialized() + } + } + + @usableFromInline + let _backing: _Backing + + init(storage: MultiProducerSingleConsumerAsyncChannel._Storage) { + self._backing = .init(storage: storage) + } + + @inlinable + mutating func next() async throws -> Element? { + do { + return try await self._backing.storage.next(isolation: nil) + } catch { + throw error as! Failure + } + } + + @inlinable + mutating func next( + isolation actor: isolated (any Actor)? = #isolation + ) async throws(Failure) -> Element? { + do { + return try await self._backing.storage.next(isolation: actor) + } catch { + throw error as! Failure + } + } + } +} + +@available(AsyncAlgorithms 1.1, *) +extension MultiProducerSingleConsumerAsyncChannel.ChannelAsyncSequence: Sendable {} +#endif diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift new file mode 100644 index 00000000..ead13dee --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift @@ -0,0 +1,1128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.1) +import AsyncAlgorithms +import XCTest + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class MultiProducerSingleConsumerAsyncChannelTests: XCTestCase { + // MARK: - sourceDeinitialized + + func testSourceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { + didTerminate = true + } + + group.addTask(executorPreference: manualExecutor) { + _ = await channel.next() + } + + withExtendedLifetime(source) {} + _ = consume source + XCTAssertFalse(didTerminate) + manualExecutor.run() + _ = try await group.next() + XCTAssertTrue(didTerminate) + } + } + + func testSourceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { + didTerminate = true + } + + group.addTask(executorPreference: manualExecutor) { + _ = await channel.next() + } + manualExecutor.run() + XCTAssertFalse(didTerminate) + + withExtendedLifetime(source) {} + _ = consume source + XCTAssertTrue(didTerminate) + manualExecutor.run() + _ = try await group.next() + } + } + + func testSourceDeinitialized_whenMultipleSources() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + var channel = channelAndSource.takeChannel() + var source1 = consume channelAndSource.source + var source2 = source1.makeAdditionalSource() + nonisolated(unsafe) var didTerminate1 = false + nonisolated(unsafe) var didTerminate2 = false + source1.setOnTerminationCallback { + didTerminate1 = true + } + source2.setOnTerminationCallback { + didTerminate2 = true + } + + _ = try await source1.send(1) + XCTAssertFalse(didTerminate1) + XCTAssertFalse(didTerminate2) + _ = consume source1 + XCTAssertFalse(didTerminate1) + XCTAssertFalse(didTerminate2) + _ = try await source2.send(2) + XCTAssertFalse(didTerminate1) + XCTAssertFalse(didTerminate2) + + _ = await channel.next() + XCTAssertFalse(didTerminate1) + XCTAssertFalse(didTerminate2) + _ = await channel.next() + XCTAssertFalse(didTerminate1) + XCTAssertFalse(didTerminate2) + _ = consume source2 + _ = await channel.next() + XCTAssertTrue(didTerminate1) + XCTAssertTrue(didTerminate2) + } + + func testSourceDeinitialized_whenSourceFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + var source: MultiProducerSingleConsumerAsyncChannel.Source? = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + try await source?.send(1) + try await source?.send(2) + source?.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator = Optional.some(channel.elements().makeAsyncIterator()) + _ = try await iterator?.next() + + _ = await onTerminationIterator.next() + + _ = try await iterator?.next() + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenFinished() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let source: MultiProducerSingleConsumerAsyncChannel.Source? = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + source?.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + _ = channel.elements().makeAsyncIterator() + + _ = await onTerminationIterator.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + // MARK: Channel deinitialized + + func testChannelDeinitialized() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + XCTAssertFalse(didTerminate) + _ = consume channel + XCTAssertTrue(didTerminate) + } + + // MARK: - sequenceDeinitialized + + func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let asyncSequence = channel.elements() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + group.addTask(executorPreference: manualExecutor) { + _ = await asyncSequence.first { _ in true } + } + + withExtendedLifetime(source) {} + _ = consume source + XCTAssertFalse(didTerminate) + manualExecutor.run() + _ = try await group.next() + XCTAssertTrue(didTerminate) + } + } + + func testSequenceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let asyncSequence = channel.elements() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + group.addTask(executorPreference: manualExecutor) { + _ = await asyncSequence.first { _ in true } + } + manualExecutor.run() + XCTAssertFalse(didTerminate) + + withExtendedLifetime(source) {} + _ = consume source + XCTAssertTrue(didTerminate) + manualExecutor.run() + _ = try await group.next() + } + } + + // MARK: - iteratorInitialized + + func testIteratorInitialized_whenInitial() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + _ = consume channelAndSource.source + + _ = channel.elements().makeAsyncIterator() + } + + func testIteratorInitialized_whenChanneling() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + try await source.send(1) + + var iterator = channel.elements().makeAsyncIterator() + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + } + + func testIteratorInitialized_whenSourceFinished() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + try await source.send(1) + source.finish(throwing: nil) + + var iterator = channel.elements().makeAsyncIterator() + let element1 = await iterator.next(isolation: nil) + XCTAssertEqual(element1, 1) + let element2 = await iterator.next(isolation: nil) + XCTAssertNil(element2) + } + + func testIteratorInitialized_whenFinished() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + + source.finish(throwing: nil) + + var iterator = channel.elements().makeAsyncIterator() + let element = await iterator.next(isolation: nil) + XCTAssertNil(element) + } + + // MARK: - iteratorDeinitialized + + func testIteratorDeinitialized_whenInitial() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator = Optional.some(channel.elements().makeAsyncIterator()) + iterator = nil + _ = await iterator?.next(isolation: nil) + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenChanneling() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + try await source.send(1) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator = Optional.some(channel.elements().makeAsyncIterator()) + iterator = nil + _ = await iterator?.next(isolation: nil) + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenSourceFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + try await source.send(1) + source.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator = Optional.some(channel.elements().makeAsyncIterator()) + iterator = nil + _ = await iterator?.next(isolation: nil) + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + source.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator = Optional.some(channel.elements().makeAsyncIterator()) + iterator = nil + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenChanneling_andSuspendedProducer() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel: MultiProducerSingleConsumerAsyncChannel? = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + var iterator = channel?.elements().makeAsyncIterator() + channel = nil + + _ = try { try source.send(1) }() + + do { + try await withCheckedThrowingContinuation { continuation in + source.send(1) { result in + continuation.resume(with: result) + } + + iterator = nil + } + } catch { + XCTAssertTrue(error is MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError) + } + + _ = try await iterator?.next() + } + + // MARK: - write + + func testWrite_whenInitial() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + try await source.send(1) + + var iterator = channel.elements().makeAsyncIterator() + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + } + + func testWrite_whenChanneling_andNoConsumer() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + try await source.send(1) + try await source.send(2) + + var iterator = channel.elements().makeAsyncIterator() + let element1 = await iterator.next(isolation: nil) + XCTAssertEqual(element1, 1) + let element2 = await iterator.next(isolation: nil) + XCTAssertEqual(element2, 2) + } + + func testWrite_whenChanneling_andSuspendedConsumer() async throws { + try await withThrowingTaskGroup(of: Int?.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + group.addTask { + await channel.next() + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + try await source.send(1) + let element = try await group.next() + XCTAssertEqual(element, 1) + } + } + + func testWrite_whenChanneling_andSuspendedConsumer_andEmptySequence() async throws { + try await withThrowingTaskGroup(of: Int?.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + group.addTask { + await channel.next() + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + try await source.send(contentsOf: []) + try await source.send(contentsOf: [1]) + let element = try await group.next() + XCTAssertEqual(element, 1) + } + } + + func testWrite_whenSourceFinished() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.takeChannel() + var source1 = consume channelAndSource.source + var source2 = source1.makeAdditionalSource() + + try await source1.send(1) + source1.finish() + do { + try await source2.send(1) + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError) + } + let element1 = await channel.next() + XCTAssertEqual(element1, 1) + let element2 = await channel.next() + XCTAssertNil(element2) + } + + func testWrite_whenConcurrentProduction() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.takeChannel() + var source1 = consume channelAndSource.source + var source2 = Optional.some(source1.makeAdditionalSource()) + + let manualExecutor1 = ManualTaskExecutor() + group.addTask(executorPreference: manualExecutor1) { + try await source1.send(1) + } + + let manualExecutor2 = ManualTaskExecutor() + group.addTask(executorPreference: manualExecutor2) { + var source2 = source2.take()! + try await source2.send(2) + source2.finish() + } + + manualExecutor1.run() + let element1 = await channel.next() + XCTAssertEqual(element1, 1) + + manualExecutor2.run() + let element2 = await channel.next() + XCTAssertEqual(element2, 2) + + let element3 = await channel.next() + XCTAssertNil(element3) + } + } + + // MARK: - enqueueProducer + + func testEnqueueProducer_whenChanneling_andAndCancelled() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.send(1) + + let writeResult = try { try source.send(2) }() + + switch consume writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(var callbackHandle): + callbackHandle.cancelCallback() + + callbackHandle.enqueueCallback { result in + producerSource.yield(with: result) + } + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = await channel.next() + XCTAssertEqual(element, 1) + } + + func testEnqueueProducer_whenChanneling_andAndCancelled_andAsync() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + try await source.send(1) + + group.addTask { + try await source.send(2) + } + + group.cancelAll() + do { + try await group.next() + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = await channel.next() + XCTAssertEqual(element, 1) + } + } + + func testEnqueueProducer_whenChanneling_andInterleaving() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + var iterator = channel.elements().makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.send(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(var callbackHandle): + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + + callbackHandle.enqueueCallback { result in + producerSource.yield(with: result) + } + } + + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + func testEnqueueProducer_whenChanneling_andSuspending() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + var iterator = channel.elements().makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.send(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(var callbackHandle): + callbackHandle.enqueueCallback { result in + producerSource.yield(with: result) + } + } + + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + // MARK: - cancelProducer + + func testCancelProducer_whenChanneling() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.send(1) + + let writeResult = try { try source.send(2) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(var callbackHandle): + callbackHandle.enqueueCallback { result in + producerSource.yield(with: result) + } + + callbackHandle.cancelCallback() + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = await channel.next() + XCTAssertEqual(element, 1) + } + + // MARK: - finish + + func testFinish_whenChanneling_andConsumerSuspended() async throws { + try await withThrowingTaskGroup(of: Int?.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + var channel = channelAndSource.takeChannel() + var source: MultiProducerSingleConsumerAsyncChannel.Source? = consume channelAndSource.source + + group.addTask { + while let element = await channel.next() { + if element == 2 { + return element + } + } + return nil + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + source?.finish(throwing: nil) + source = nil + + let element = try await group.next() + XCTAssertEqual(element, .some(nil)) + } + } + + func testFinish_whenInitial() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndSource.takeChannel() + let source = consume channelAndSource.source + + source.finish(throwing: CancellationError()) + + do { + for try await _ in channel.elements() {} + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + } + + // MARK: - Backpressure + + func testBackpressure() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await source.send(contentsOf: [1]) + } + } + + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.elements().makeAsyncIterator() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + group.cancelAll() + } + } + + func testBackpressureSync() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await withCheckedThrowingContinuation { continuation in + source.send(contentsOf: [1]) { result in + continuation.resume(with: result) + } + } + } + } + + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.elements().makeAsyncIterator() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + group.cancelAll() + } + } + + func testWatermarkWithCustomCoount() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: [Int].self, + backpressureStrategy: .watermark(low: 2, high: 4, waterLevelForElement: { $0.count }) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + var iterator = channel.elements().makeAsyncIterator() + + try await source.send([1, 1, 1]) + + _ = await iterator.next(isolation: nil) + + try await source.send([1, 1, 1]) + + _ = await iterator.next(isolation: nil) + } + + func testWatermarWithLotsOfElements() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + // This test should in the future use a custom task executor to schedule to avoid sending + // 1000 elements. + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.takeChannel() + var source: MultiProducerSingleConsumerAsyncChannel.Source! = consume channelAndSource.source + + group.addTask { + var source = source.take()! + for i in 0...10000 { + try await source.send(i) + } + source.finish() + } + + let asyncSequence = channel.elements() + + group.addTask { + var sum = 0 + for try await element in asyncSequence { + sum += element + } + } + } + } + + func testThrowsError() async throws { + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + try await source.send(1) + try await source.send(2) + source.finish(throwing: CancellationError()) + + var elements = [Int]() + var iterator = channel.elements().makeAsyncIterator() + + do { + while let element = try await iterator.next() { + elements.append(element) + } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + XCTAssertEqual(elements, [1, 2]) + } + + let element = try await iterator.next() + XCTAssertNil(element) + } + + func testAsyncSequenceWrite() async throws { + let (stream, continuation) = AsyncStream.makeStream() + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + var channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + continuation.yield(1) + continuation.yield(2) + continuation.finish() + + try await source.send(contentsOf: stream) + source.finish(throwing: nil) + + let elements = await channel.collect() + XCTAssertEqual(elements, [1, 2]) + } + + // MARK: NonThrowing + + func testNonThrowing() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + var channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.takeChannel() + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await source.send(contentsOf: [1]) + } + } + + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.elements().makeAsyncIterator() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + group.cancelAll() + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerAsyncChannel { + /// Collect all elements in the sequence into an array. + fileprivate mutating func collect() async throws(Failure) -> [Element] { + var elements = [Element]() + while let element = try await self.next() { + elements.append(element) + } + return elements + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerAsyncChannel.Source.SendResult { + func assertIsProducerMore() { + switch self { + case .produceMore: + return () + + case .enqueueCallback: + XCTFail("Expected produceMore") + } + } + + func assertIsEnqueueCallback() { + switch self { + case .produceMore: + XCTFail("Expected enqueueCallback") + + case .enqueueCallback: + return () + } + } +} + +extension Optional where Wrapped: ~Copyable { + fileprivate mutating func take() -> Self { + let result = consume self + self = nil + return result + } +} +#endif diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift new file mode 100644 index 00000000..b07fb835 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.0) +import DequeModule +import Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class ManualTaskExecutor: TaskExecutor { + private let jobs = Mutex>(.init()) + + func enqueue(_ job: UnownedJob) { + self.jobs.withLock { $0.append(job) } + } + + func run() { + while let job = self.jobs.withLock({ $0.popFirst() }) { + job.runSynchronously(on: self.asUnownedTaskExecutor()) + } + } +} +#endif From 2773d4125311133a2f705ec374c363a935069d45 Mon Sep 17 00:00:00 2001 From: Joseph Heck Date: Wed, 19 Nov 2025 13:22:29 -0800 Subject: [PATCH 148/149] This removes the build constraint at Swift Package Index from Swift 5.8 so that (#377) symbols that are only available in later versions (for example, MultiProducerSingleConsumerAsyncChannel) are reflected in the hosted documentation. --- .spi.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.spi.yml b/.spi.yml index 2c794bc9..2a779cf6 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,4 +2,3 @@ version: 1 builder: configs: - documentation_targets: [AsyncAlgorithms] - swift_version: 5.8 From 6c050d5ef8e1aa6342528460db614e9770d7f804 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 1 Dec 2025 20:18:00 +0100 Subject: [PATCH 149/149] Make `MPSCAsyncChannel` source methods `nonisolated(nonsending)` (#380) `nonisolated(nonsending)` is a 6.2 language feature that allows the inheritance of the callers isolation. The new `MPSCAsyncChannel.Source` send methods should adopt this to avoid unnecessary isolation hops. --- .github/workflows/pull_request.yml | 2 +- ...tiProducerSingleConsumerAsyncChannel.swift | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e4804029..8325fa8e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,4 +22,4 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift Async Algorithms" - format_check_container_image: "swiftlang/swift:nightly-6.1-noble" # Needed since 6.0.x doesn't support sending keyword + format_check_container_image: "swiftlang/swift:nightly-main-noble" # Needed due to https://github.com/swiftlang/swift-format/issues/1081 diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift index 131946fd..acc9a72a 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift @@ -445,6 +445,95 @@ extension MultiProducerSingleConsumerAsyncChannel { } } + #if compiler(>=6.2) + /// Send new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating nonisolated(nonsending) func send( + contentsOf sequence: consuming sending S + ) async throws where Element == S.Element, S: Sequence, Element: Copyable { + let syncSend: (sending S, inout Self) throws -> SendResult = { try $1.send(contentsOf: $0) } + let sendResult = try syncSend(sequence, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackToken): + let id = callbackToken._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) + } + } + } + + /// Send new element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - element: The element to send to the channel. + @inlinable + public mutating nonisolated(nonsending) func send(_ element: consuming sending Element) async throws { + let syncSend: (consuming sending Element, inout Self) throws -> SendResult = { try $1.send($0) } + let sendResult = try syncSend(element, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackHandle): + let id = callbackHandle._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) + } + } + } + + /// Send the elements of the asynchronous sequence to the channel. + /// + /// This method returns once the provided asynchronous sequence or the channel finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating nonisolated(nonsending) func send(contentsOf sequence: consuming sending S) async throws + where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { + for try await element in sequence { + try await self.send(contentsOf: CollectionOfOne(element)) + } + } + #else /// Send new elements to the channel. /// /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the @@ -532,6 +621,7 @@ extension MultiProducerSingleConsumerAsyncChannel { try await self.send(contentsOf: CollectionOfOne(element)) } } + #endif /// Indicates that the production terminated. ///