diff --git a/Sources/AsyncSequenceReader/AsyncBufferedIterator.swift b/Sources/AsyncSequenceReader/AsyncBufferedIterator.swift index e9c8d06..76950b1 100644 --- a/Sources/AsyncSequenceReader/AsyncBufferedIterator.swift +++ b/Sources/AsyncSequenceReader/AsyncBufferedIterator.swift @@ -16,7 +16,7 @@ public struct AsyncBufferedIterator: AsyncI /// /// - Note:Ideally, this should be implemented using some sort of cyclical buffer like a Deque, but in practice, it will only ever have one entry. @usableFromInline - var unconsumedBuffer: [BaseIterator.Element] = [] + var unconsumedBuffer: [Result] = [] @usableFromInline init(_ baseIterator: BaseIterator) { @@ -32,7 +32,7 @@ public struct AsyncBufferedIterator: AsyncI @inlinable public mutating func next(isolation actor: isolated (any Actor)? = #isolation) async rethrows -> BaseIterator.Element? { guard unconsumedBuffer.isEmpty else { - return unconsumedBuffer.removeFirst() + return try unconsumedBuffer.removeFirst().get() } return try await baseIterator._nextIsolated() @@ -41,13 +41,24 @@ public struct AsyncBufferedIterator: AsyncI /// Read ahead, and store the value for later, or throw if the base iterator also throws. /// - Returns: The read-ahead value. @usableFromInline - mutating func nextUnconsumed(isolation actor: isolated (any Actor)? = #isolation) async rethrows -> BaseIterator.Element? { - let next = try await baseIterator._nextIsolated() - if let value = next { - unconsumedBuffer.append(value) + mutating func nextUnconsumed(isolation actor: isolated (any Actor)? = #isolation) async -> Result? { + /// Prevent a compiler crash catching a re-throwing result by casting it first. + func _preventCompilerCrashCacthingError(_ actor: isolated (any Actor)? = #isolation) async throws -> Element? { + try await baseIterator._nextIsolated() } - return next + do { + let next = try await _preventCompilerCrashCacthingError() + if let value = next { + unconsumedBuffer.append(.success(value)) + return .success(value) + } + + return nil + } catch { + unconsumedBuffer.append(.failure(error)) + return .failure(error) + } } /// Returns if the iterator has more elements to consume. @@ -55,12 +66,12 @@ public struct AsyncBufferedIterator: AsyncI /// If it does, the iterator saves the elements, and will deliver them immediately on the next call to `next()` /// - Returns: A Bool indicating if there is more to consume or not. @inlinable - public mutating func hasMoreData(isolation actor: isolated (any Actor)? = #isolation) async rethrows -> Bool { + public mutating func hasMoreData(isolation actor: isolated (any Actor)? = #isolation) async -> Bool { guard unconsumedBuffer.isEmpty else { return true } - return try await nextUnconsumed() != nil + return await nextUnconsumed() != nil } } diff --git a/Sources/AsyncSequenceReader/AsyncIteratorMapSequence.swift b/Sources/AsyncSequenceReader/AsyncIteratorMapSequence.swift index 3dbaf44..8689378 100644 --- a/Sources/AsyncSequenceReader/AsyncIteratorMapSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncIteratorMapSequence.swift @@ -196,7 +196,7 @@ extension AsyncIteratorMapSequence: AsyncSequence { /// This iterator calls `next()` on its (wrapped) base iterator, and stores the result; if this call returns `nil`, `next()` returns `nil`. Otherwise, `next()` returns the result of calling the transforming closure on the received element. If calling the transformation throws an error, the sequence ends and `next()` rethrows the error. @inlinable public mutating func next(isolation actor: isolated (any Actor)? = #isolation) async throws -> Transformed? { - guard !encounteredError, try await baseIterator.hasMoreData(isolation: actor) else { + guard !encounteredError, await baseIterator.hasMoreData(isolation: actor) else { return nil } do { @@ -220,7 +220,7 @@ extension AsyncIteratorMapSequence: AsyncSequence { /// This iterator calls `next()` on its (wrapped) base iterator, and stores the result; if this call returns `nil`, `next()` returns `nil`. Otherwise, `next()` returns the result of calling the transforming closure on the received element. @inlinable public mutating func next(isolation actor: isolated (any Actor)? = #isolation) async rethrows -> Transformed? where TransformFailure == Never { - guard try await baseIterator.hasMoreData(isolation: actor) else { + guard await baseIterator.hasMoreData(isolation: actor) else { return nil } #if compiler(>=6.2) diff --git a/Sources/AsyncSequenceReader/AsyncReadSequence.swift b/Sources/AsyncSequenceReader/AsyncReadSequence.swift index 2f19896..74ec61c 100644 --- a/Sources/AsyncSequenceReader/AsyncReadSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadSequence.swift @@ -25,13 +25,16 @@ extension AsyncIteratorProtocol { /// - Parameter sequenceTransform: A transformation that accepts a sequence that can be read from, or stopped prematurely by returning `nil`. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Parameter readSequenceFactory: A factory to create a suitable ``AsyncReadSequence`` that will determine the logical bounds of the transformation within the receiving iterator. /// - Returns: A transformed value read from the iterator, or `nil` if there were no values left to read. - public mutating func transform( - with sequenceTransform: sending (sending ReadSequence) async throws -> Transformed, + public mutating func transform< + Transformed, ReadSequence: AsyncReadSequence, + TransformFailure: Error + >( + with sequenceTransform: sending (sending ReadSequence) async throws(TransformFailure) -> Transformed, readSequenceFactory: (inout AsyncBufferedIterator) -> ReadSequence - ) async throws -> Transformed? where ReadSequence.BaseIterator == Self { + ) async throws(TransformFailure) -> Transformed? where ReadSequence.BaseIterator == Self { var results: Transformed? = nil var wrappedIterator = AsyncBufferedIterator(self) - if try await wrappedIterator.hasMoreData() { + if await wrappedIterator.hasMoreData() { nonisolated(unsafe) let readSequence = readSequenceFactory(&wrappedIterator) results = try await sequenceTransform(readSequence) wrappedIterator = readSequence.baseIterator @@ -51,12 +54,15 @@ extension AsyncBufferedIterator { /// - Parameter sequenceTransform: A transformation that accepts a sequence that can be read from, or stopped prematurely by returning `nil`. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Parameter readSequenceFactory: A factory to create a suitable ``AsyncReadSequence`` that will determine the logical bounds of the transformation within the receiving iterator. /// - Returns: A transformed value read from the iterator, or `nil` if there were no values left to read. - public mutating func transform( - with sequenceTransform: sending (sending ReadSequence) async throws -> Transformed, + public mutating func transform< + Transformed, ReadSequence: AsyncReadSequence, + TransformFailure: Error + >( + with sequenceTransform: sending (sending ReadSequence) async throws(TransformFailure) -> Transformed, readSequenceFactory: (inout Self) -> ReadSequence - ) async throws -> Transformed? where ReadSequence.BaseIterator == BaseIterator { + ) async throws(TransformFailure) -> Transformed? where ReadSequence.BaseIterator == BaseIterator { var results: Transformed? = nil - if try await self.hasMoreData() { + if await self.hasMoreData() { nonisolated(unsafe) let readSequence = readSequenceFactory(&self) results = try await sequenceTransform(readSequence) self = readSequence.baseIterator diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift index aa4be9d..e05beff 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift @@ -182,10 +182,13 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - public mutating func collect( + public mutating func collect< + Transformed, + TransformFailure: Error + >( upToIncluding termination: Element, - sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws -> Transformed - ) async throws -> Transformed? where Element: Equatable { + sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? where Element: Equatable { try await collect(upToIncluding: [termination], sequenceTransform: sequenceTransform) } @@ -221,17 +224,17 @@ extension AsyncIteratorProtocol { /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. public mutating func collect< TerminationCollection: Collection, - Transformed + Transformed, + TransformFailure: Error >( upToIncluding termination: TerminationCollection, - sequenceTransform: sending (AsyncReadUpToElementsSequence) async throws -> Transformed - ) async throws -> Transformed? where Element: Equatable { + sequenceTransform: sending (AsyncReadUpToElementsSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? where Element: Equatable { try await transform(with: sequenceTransform) { .init($0, termination: termination) } } } -extension AsyncBufferedIterator { - +extension AsyncBufferedIterator where Element: Equatable { /// Collect elements into a sequence until the termination sequence is encountered, and transform it using the provided closure. /// /// - Note: It is up to the caller to verify if the termination sequence was encountered or not, which can easily be done by checking `result.suffix(termination.count).elementsEqual(termination)`. @@ -265,7 +268,7 @@ extension AsyncBufferedIterator { public mutating func collect( upToIncluding termination: Element, sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws -> Transformed - ) async throws -> Transformed? where Element: Equatable { + ) async rethrows -> Transformed? { try await collect(upToIncluding: [termination], sequenceTransform: sequenceTransform) } @@ -305,7 +308,7 @@ extension AsyncBufferedIterator { >( upToIncluding termination: TerminationCollection, sequenceTransform: sending (AsyncReadUpToElementsSequence) async throws -> Transformed - ) async throws -> Transformed? where Element: Equatable { + ) async rethrows -> Transformed? { try await transform(with: sequenceTransform) { .init($0, termination: termination) } } } diff --git a/Tests/AsyncSequenceReaderTests/AsyncBufferedIteratorTests.swift b/Tests/AsyncSequenceReaderTests/AsyncBufferedIteratorTests.swift index 43387b4..6cdb69c 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncBufferedIteratorTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncBufferedIteratorTests.swift @@ -100,19 +100,19 @@ import Testing #expect(try await bufferedIterator.next() == 2) #expect(try await bufferedIterator.next() == 3) #expect(try await bufferedIterator.next() == 4) - #expect(try await bufferedIterator.hasMoreData() == true) - #expect(try await bufferedIterator.hasMoreData() == true) - #expect(try await bufferedIterator.hasMoreData() == true) + #expect(await bufferedIterator.hasMoreData() == true) + #expect(await bufferedIterator.hasMoreData() == true) + #expect(await bufferedIterator.hasMoreData() == true) #expect(try await bufferedIterator.next() == 5) #expect(try await bufferedIterator.nonIsolatedNext() == 6) #expect(try await bufferedIterator.next() == 7) #expect(try await bufferedIterator.next() == 8) - #expect(try await bufferedIterator.hasMoreData() == true) + #expect(await bufferedIterator.hasMoreData() == true) #expect(try await bufferedIterator.next() == 9) - #expect(try await bufferedIterator.hasMoreData() == false) + #expect(await bufferedIterator.hasMoreData() == false) #expect(try await bufferedIterator.next() == nil) - #expect(try await bufferedIterator.hasMoreData() == false) - #expect(try await bufferedIterator.hasMoreData() == false) + #expect(await bufferedIterator.hasMoreData() == false) + #expect(await bufferedIterator.hasMoreData() == false) #expect(try await bufferedIterator.next() == nil) } } diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift index 0469bbe..d7acf21 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift @@ -338,87 +338,87 @@ import Testing let inputSequence = "apple, orange, banana, kiwi, kumquat, pear, pineapple" let results = inputSequence.iteratorMap { iterator -> String? in - try await iterator.collect(upToIncluding: ", ") { sequence -> String in + await iterator.collect(upToIncluding: ", ") { sequence -> String in await sequence.reduce(into: "") { $0.append($1) } } } var resultsIterator = results.makeAsyncIterator() - #expect(try await resultsIterator.next() == "apple, ") - #expect(try await resultsIterator.next() == "orange, ") - #expect(try await resultsIterator.next() == "banana, ") - #expect(try await resultsIterator.next() == "kiwi, ") - #expect(try await resultsIterator.next() == "kumquat, ") - #expect(try await resultsIterator.next() == "pear, ") - #expect(try await resultsIterator.next() == "pineapple") - #expect(try await resultsIterator.next() == nil) + #expect(await resultsIterator.next() == "apple, ") + #expect(await resultsIterator.next() == "orange, ") + #expect(await resultsIterator.next() == "banana, ") + #expect(await resultsIterator.next() == "kiwi, ") + #expect(await resultsIterator.next() == "kumquat, ") + #expect(await resultsIterator.next() == "pear, ") + #expect(await resultsIterator.next() == "pineapple") + #expect(await resultsIterator.next() == nil) } @Test func trapsIfBufferedIteratorTransformingUpToIncludingTerminationIsEmpty() async throws { let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { var inputSequence = AsyncBufferedIterator(AnyReadableSequence(" ").makeAsyncIterator()) - let _ = try await inputSequence.collect(upToIncluding: "") { sequence -> String in + let _ = await inputSequence.collect(upToIncluding: "") { sequence -> String in await sequence.reduce(into: "") { $0.append($1) } } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:331: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:334: Precondition failed: termination must not be empty") #endif } @Test func rawIteratorTransformingUpToIncluding() async throws { var iterator = AnyReadableSequence("apple orange banana kiwi kumquat pear pineapple").makeAsyncIterator() - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "apple ") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "orange ") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "banana ") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "kiwi ") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "kumquat ") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "pear ") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "pineapple") - #expect(try await iterator.collect(upToIncluding: " " as Character) { sequence in + #expect(await iterator.collect(upToIncluding: " " as Character) { sequence in await sequence.reduce(into: "") { $0.append($1) } } == nil) } @Test func rawIteratorTransformingUpToIncludingSequence() async throws { var iterator = AnyReadableSequence("apple, orange, banana, kiwi, kumquat, pear, pineapple").makeAsyncIterator() - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "apple, ") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "orange, ") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "banana, ") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "kiwi, ") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "kumquat, ") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "pear, ") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == "pineapple") - #expect(try await iterator.collect(upToIncluding: ", ") { sequence in + #expect(await iterator.collect(upToIncluding: ", ") { sequence in await sequence.reduce(into: "") { $0.append($1) } } == nil) } @@ -426,12 +426,12 @@ import Testing @Test func trapsIfTransformingUpToIncludingTerminationIsEmpty() async throws { let result = await #expect(processExitsWith: .failure, observing: [\.standardErrorContent]) { var inputSequence = AnyReadableSequence(" ").makeAsyncIterator() - let _ = try await inputSequence.collect(upToIncluding: "") { sequence -> String in + let _ = await inputSequence.collect(upToIncluding: "") { sequence -> String in await sequence.reduce(into: "") { $0.append($1) } } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:331: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:334: Precondition failed: termination must not be empty") #endif } } diff --git a/Tests/AsyncSequenceReaderTests/AsyncSequenceReaderTests.swift b/Tests/AsyncSequenceReaderTests/AsyncSequenceReaderTests.swift index fa06152..c6e0439 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncSequenceReaderTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncSequenceReaderTests.swift @@ -232,7 +232,7 @@ import Testing } var iterator = AsyncBufferedIterator(testStream.makeAsyncIterator()) - #expect(try await iterator.transform(with: { sequence in + #expect(await iterator.transform(with: { sequence in Issue.record("Transformation should never be called!") }, readSequenceFactory: { iterator in Issue.record("Factory should never be called!") @@ -244,7 +244,7 @@ import Testing let testStream = TestSequence(base: []) var iterator = testStream.makeAsyncIterator() - #expect(try await iterator.transform(with: { sequence in + #expect(await iterator.transform(with: { sequence in Issue.record("Transformation should never be called!") }, readSequenceFactory: { iterator in Issue.record("Factory should never be called!") @@ -258,7 +258,7 @@ import Testing } var iterator = testStream.makeAsyncIterator() - #expect(try await iterator.transform(with: { sequence in + #expect(await iterator.transform(with: { sequence in Issue.record("Transformation should never be called!") }, readSequenceFactory: { iterator in Issue.record("Factory should never be called!") @@ -273,7 +273,7 @@ import Testing } var iterator = AsyncBufferedIterator(testStream.makeAsyncIterator()) - let result = try await iterator.transform(with: { sequence in + let result = await iterator.transform(with: { sequence in return "" }, readSequenceFactory: { iterator in AsyncSequenceReader(iterator) { await $0.next() } @@ -287,14 +287,14 @@ import Testing let testStream = TestSequence(base: ["A"]) var iterator = testStream.makeAsyncIterator() - let _ = try await iterator.transform(with: { sequence in + let _ = await iterator.transform(with: { sequence in return "" }, readSequenceFactory: { iterator in AsyncSequenceReader(iterator) { await $0.next() } }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadSequence.swift:39: Precondition failed: A transform was requested, but the sequence was left in a state where the next value will never be read (fix: Use AnyReadableSequence(iterator) instead of calling `.transform` on `iterator` directly)") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadSequence.swift:42: Precondition failed: A transform was requested, but the sequence was left in a state where the next value will never be read (fix: Use AnyReadableSequence(iterator) instead of calling `.transform` on `iterator` directly)") #endif } @@ -306,14 +306,14 @@ import Testing } var iterator = testStream.makeAsyncIterator() - let _ = try await iterator.transform(with: { sequence in + let _ = await iterator.transform(with: { sequence in return "" }, readSequenceFactory: { iterator in return AsyncSequenceReader(iterator) { await $0.next() } }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadSequence.swift:39: Precondition failed: A transform was requested, but the sequence was left in a state where the next value will never be read (fix: Use AnyReadableSequence(iterator) instead of calling `.transform` on `iterator` directly)") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadSequence.swift:42: Precondition failed: A transform was requested, but the sequence was left in a state where the next value will never be read (fix: Use AnyReadableSequence(iterator) instead of calling `.transform` on `iterator` directly)") #endif } }