From 0f896c5c37531bf1416949daf4985f5f7e271b0a Mon Sep 17 00:00:00 2001 From: Alex Waldron Date: Fri, 26 Dec 2025 11:33:54 -0500 Subject: [PATCH 1/2] Implement next(isolation:) in AsyncAdjacentPairsSequence The next(isolation:) method allows the iterator to inherit the caller's isolation context, avoiding unnecessary isolation hops. --- .../AsyncAdjacentPairsSequence.swift | 14 ++++++++++ .../TestAdjacentPairs.swift | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 25dbd49f..cc7307de 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -65,6 +65,20 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { self.base = base } + @available(AsyncAlgorithms 1.1, *) + mutating public func next(isolation actor: isolated (any Actor)?) async throws(Base.Failure) -> (Base.Element, Base.Element)? { + if previousElement == nil { + previousElement = try await base.next(isolation: actor) + } + + guard let previous = previousElement, let next = try await base.next(isolation: actor) else { + return nil + } + + previousElement = next + return (previous, next) + } + @inlinable public mutating func next() async rethrows -> (Base.Element, Base.Element)? { if previousElement == nil { diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 9b6fdf3c..22cd147d 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -11,6 +11,7 @@ import XCTest import AsyncAlgorithms +import Observation final class TestAdjacentPairs: XCTestCase { func test_adjacentPairs_produces_tuples_of_adjacent_values_of_original_element() async { @@ -95,4 +96,29 @@ final class TestAdjacentPairs: XCTestCase { task.cancel() await fulfillment(of: [finished], timeout: 1.0) } + + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) + @MainActor func test_adjacentPairs_respects_immediate() async { + let testObservable = TestObservable() + let observations = Observations { testObservable.prop } + + let iterated = expectation(description: "iterates once") + + // with `Task.immediate`, the first element in the adjacent pair should be populated immediately + let t = Task.immediate { + for await (previous, current) in observations.adjacentPairs() { + XCTAssertEqual(previous, 1) + XCTAssertEqual(current, 2) + iterated.fulfill() + } + } + testObservable.prop = 2 + await fulfillment(of: [iterated], timeout: 1.0) + t.cancel() + } +} + +@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) +@MainActor @Observable private final class TestObservable { + var prop = 1 } From 425dba6fb2fdfc878db7f9efa6130e62f17dd4c1 Mon Sep 17 00:00:00 2001 From: Alex Waldron Date: Tue, 14 Apr 2026 08:16:58 -0400 Subject: [PATCH 2/2] Remove Observations dependency from TestAdjacentPairs --- .../AsyncAdjacentPairsSequence.swift | 4 ++- .../Support/ReportingSequence.swift | 10 +++++++ .../TestAdjacentPairs.swift | 30 +++++++++++-------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index cc7307de..99056c34 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -66,7 +66,9 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { } @available(AsyncAlgorithms 1.1, *) - mutating public func next(isolation actor: isolated (any Actor)?) async throws(Base.Failure) -> (Base.Element, Base.Element)? { + mutating public func next( + isolation actor: isolated (any Actor)? + ) async throws(Base.Failure) -> (Base.Element, Base.Element)? { if previousElement == nil { previousElement = try await base.next(isolation: actor) } diff --git a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift index 57d81836..d8ee7093 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift @@ -46,11 +46,13 @@ final class ReportingSequence: Sequence, IteratorProtocol { final class ReportingAsyncSequence: AsyncSequence, AsyncIteratorProtocol, @unchecked Sendable { enum Event: Equatable, CustomStringConvertible { case next + case nextIsolation case makeAsyncIterator var description: String { switch self { case .next: return "next()" + case .nextIsolation: return "next(isolation:)" case .makeAsyncIterator: return "makeAsyncIterator()" } } @@ -63,6 +65,14 @@ final class ReportingAsyncSequence: AsyncSequence, AsyncItera self.elements = elements } + func next(isolation actor: isolated (any Actor)?) async -> Element? { + events.append(.nextIsolation) + guard elements.count > 0 else { + return nil + } + return elements.removeFirst() + } + func next() async -> Element? { events.append(.next) guard elements.count > 0 else { diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 22cd147d..6adb4def 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -11,7 +11,6 @@ import XCTest import AsyncAlgorithms -import Observation final class TestAdjacentPairs: XCTestCase { func test_adjacentPairs_produces_tuples_of_adjacent_values_of_original_element() async { @@ -98,27 +97,32 @@ final class TestAdjacentPairs: XCTestCase { } @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) - @MainActor func test_adjacentPairs_respects_immediate() async { - let testObservable = TestObservable() - let observations = Observations { testObservable.prop } - + @MainActor + func test_adjacentPairs_next_with_nil_isolation_produces_correct_pairs() async throws { + let reportingSequence = ReportingAsyncSequence([1, 2]) + let adjacentPairs = reportingSequence.adjacentPairs() let iterated = expectation(description: "iterates once") - // with `Task.immediate`, the first element in the adjacent pair should be populated immediately let t = Task.immediate { - for await (previous, current) in observations.adjacentPairs() { + for await (previous, current) in adjacentPairs { XCTAssertEqual(previous, 1) XCTAssertEqual(current, 2) iterated.fulfill() } } - testObservable.prop = 2 + + XCTAssert(reportingSequence.elements.isEmpty, "With a proper implementation of next(isolaton:), elements should be immediately consumed") + let nonIsolatedNextCalled = reportingSequence.events.contains { + switch $0 { + case .next: + true + default: + false + } + } + XCTAssertFalse(nonIsolatedNextCalled, "Next without isolation should not be called with a fully isolated pipeline") + await fulfillment(of: [iterated], timeout: 1.0) t.cancel() } } - -@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) -@MainActor @Observable private final class TestObservable { - var prop = 1 -}