diff --git a/stdlib/public/Concurrency/CheckedContinuation.swift b/stdlib/public/Concurrency/CheckedContinuation.swift index 2873aee15fa3e..66952e0796ea1 100644 --- a/stdlib/public/Concurrency/CheckedContinuation.swift +++ b/stdlib/public/Concurrency/CheckedContinuation.swift @@ -290,12 +290,31 @@ extension CheckedContinuation { /// - SeeAlso: `withCheckedThrowingContinuation(function:_:)` /// - SeeAlso: `withUnsafeContinuation(function:_:)` /// - SeeAlso: `withUnsafeThrowingContinuation(function:_:)` +@available(SwiftStdlib 5.1, *) +@export(implementation) +@_silgen_name("$ss25withCheckedContinuation_X8function_xSS_yScCyxs5NeverOGXEtYalF") // The `_X` suffix is only here to avoid silgen_name clash with original _unsafeInheritExecutor ABI +public nonisolated(nonsending) func withCheckedContinuation( + function: String = #function, + _ body: (CheckedContinuation) -> Void +) async -> sending T { + return await Builtin.withUnsafeContinuation { + let unsafeContinuation = unsafe UnsafeContinuation($0) + return body(unsafe CheckedContinuation(continuation: unsafeContinuation, + function: function)) + } +} + @inlinable @available(SwiftStdlib 5.1, *) #if !$Embedded @backDeployed(before: SwiftStdlib 6.0) #endif -public func withCheckedContinuation( +@abi(func withCheckedContinuation( + isolation: isolated (any Actor)?, + function: String, + _ body: (CheckedContinuation) -> Void +) async -> sending T) +public func _isolatedParam_withCheckedContinuation( isolation: isolated (any Actor)? = #isolation, function: String = #function, _ body: (CheckedContinuation) -> Void @@ -325,7 +344,6 @@ public func _unsafeInheritExecutor_withCheckedContinuation( } } - /// Invokes the passed in closure with a checked continuation for the current task. /// /// The body of the closure executes synchronously on the calling task, and once it returns @@ -354,12 +372,36 @@ public func _unsafeInheritExecutor_withCheckedContinuation( /// - SeeAlso: `withCheckedContinuation(function:_:)` /// - SeeAlso: `withUnsafeContinuation(function:_:)` /// - SeeAlso: `withUnsafeThrowingContinuation(function:_:)` +@available(SwiftStdlib 5.1, *) +@export(implementation) +@_silgen_name("$ss33withCheckedThrowingContinuation_X8function_xSS_yScCyxs5Error_pGXEtYaKlF") // The `_X` suffix is only here to avoid silgen_name clash with original _unsafeInheritExecutor ABI +public nonisolated(nonsending) func withCheckedThrowingContinuation( + function: String = #function, + _ body: (CheckedContinuation) -> Void +) async throws -> sending T { + // ABI NOTE: Interestingly enough the 'nonisolated(nonsending)' version of this func has the same ABI + // as the initial implementation, that was '@_unsafeInheritExecutor'. So we can remove the "ABI shim" + // as the current correct impl and the previous impl are both one and the same. + // + // We need to keep around the isolated parameter ABI for compatibility though. + return try await Builtin.withUnsafeThrowingContinuation { + let unsafeContinuation = unsafe UnsafeContinuation($0) + return body(unsafe CheckedContinuation(continuation: unsafeContinuation, + function: function)) + } +} + @inlinable @available(SwiftStdlib 5.1, *) #if !$Embedded @backDeployed(before: SwiftStdlib 6.0) #endif -public func withCheckedThrowingContinuation( +@abi(func withCheckedThrowingContinuation( + isolation: isolated (any Actor)?, + function: String, + _ body: (CheckedContinuation) -> Void +) async throws -> sending T) +func _isolatedParam_withCheckedThrowingContinuation( isolation: isolated (any Actor)? = #isolation, function: String = #function, _ body: (CheckedContinuation) -> Void diff --git a/stdlib/public/Concurrency/DiscardingTaskGroup.swift b/stdlib/public/Concurrency/DiscardingTaskGroup.swift index bfb8246da2d86..0cfbe98c52430 100644 --- a/stdlib/public/Concurrency/DiscardingTaskGroup.swift +++ b/stdlib/public/Concurrency/DiscardingTaskGroup.swift @@ -69,12 +69,38 @@ import Swift /// /// - SeeAlso: ``TaskGroup`` /// - SeeAlso: ``withThrowingDiscardingTaskGroup(returning:body:)`` +@available(SwiftStdlib 5.9, *) +@export(implementation) +public nonisolated(nonsending) func withDiscardingTaskGroup( + returning returnType: GroupResult.Type = GroupResult.self, + body: nonisolated(nonsending) (inout DiscardingTaskGroup) async -> GroupResult +) async -> GroupResult { + let flags = taskGroupCreateFlags( + discardResults: true + ) + + let _group = Builtin.createTaskGroupWithFlags(flags, Void.self) + var group = DiscardingTaskGroup(group: _group) + defer { Builtin.destroyTaskGroup(_group) } + + let result = await body(&group) + + try! await group.awaitAllRemainingTasksNonsending() // try!-safe, cannot throw since this is a non throwing group + + return result +} + @available(SwiftStdlib 5.9, *) #if !hasFeature(Embedded) @backDeployed(before: SwiftStdlib 6.0) #endif @inlinable -public func withDiscardingTaskGroup( +@abi(func withDiscardingTaskGroup( + returning returnType: GroupResult.Type, + isolation: isolated (any Actor)?, + body: (inout DiscardingTaskGroup) async -> GroupResult +) async -> GroupResult) +func _isolatedParameter_withDiscardingTaskGroup( returning returnType: GroupResult.Type = GroupResult.self, isolation: isolated (any Actor)? = #isolation, body: (inout DiscardingTaskGroup) async -> GroupResult @@ -182,6 +208,11 @@ public struct DiscardingTaskGroup { /// Await all the remaining tasks on this group. /// /// - Throws: The first error that was encountered by this group. + @export(implementation) + internal nonisolated(nonsending) mutating func awaitAllRemainingTasksNonsending() async throws { + let _: Void? = try await _taskGroupWaitAll(group: _group, bodyError: nil) + } + @usableFromInline internal mutating func awaitAllRemainingTasks() async throws { let _: Void? = try await _taskGroupWaitAll(group: _group, bodyError: nil) @@ -336,15 +367,50 @@ extension DiscardingTaskGroup: Sendable { } /// } /// } /// ``` +@available(SwiftStdlib 5.9, *) +@export(implementation) +public nonisolated(nonsending) func withThrowingDiscardingTaskGroup( + returning returnType: GroupResult.Type = GroupResult.self, + body: nonisolated(nonsending) (inout ThrowingDiscardingTaskGroup) async throws -> GroupResult +) async throws -> GroupResult { + let flags = taskGroupCreateFlags( + discardResults: true + ) + + let _group = Builtin.createTaskGroupWithFlags(flags, Void.self) + var group = ThrowingDiscardingTaskGroup(group: _group) + defer { Builtin.destroyTaskGroup(_group) } + + let result: GroupResult + do { + result = try await body(&group) + } catch { + group.cancelAll() + + try await group.awaitAllRemainingTasksNonisolated(bodyError: error) + + throw error + } + + try await group.awaitAllRemainingTasksNonisolated(bodyError: nil) + + return result +} + @available(SwiftStdlib 5.9, *) #if !hasFeature(Embedded) @backDeployed(before: SwiftStdlib 6.0) #endif @inlinable -public func withThrowingDiscardingTaskGroup( - returning returnType: GroupResult.Type = GroupResult.self, - isolation: isolated (any Actor)? = #isolation, - body: (inout ThrowingDiscardingTaskGroup) async throws -> GroupResult +@abi(func withThrowingDiscardingTaskGroup( + returning returnType: GroupResult.Type, + isolation: isolated (any Actor)?, + body: (inout ThrowingDiscardingTaskGroup) async throws -> GroupResult +) async throws -> GroupResult) +public func _isolatedParam_withThrowingDiscardingTaskGroup( + returning returnType: GroupResult.Type = GroupResult.self, + isolation: isolated (any Actor)? = #isolation, + body: (inout ThrowingDiscardingTaskGroup) async throws -> GroupResult ) async throws -> GroupResult { let flags = taskGroupCreateFlags( discardResults: true @@ -474,6 +540,11 @@ public struct ThrowingDiscardingTaskGroup { } /// Await all the remaining tasks on this group. + @export(implementation) + internal nonisolated(nonsending) mutating func awaitAllRemainingTasksNonisolated(bodyError: Error?) async throws { + let _: Void? = try await _taskGroupWaitAll(group: _group, bodyError: bodyError) + } + @usableFromInline internal mutating func awaitAllRemainingTasks(bodyError: Error?) async throws { let _: Void? = try await _taskGroupWaitAll(group: _group, bodyError: bodyError) diff --git a/stdlib/public/Concurrency/PartialAsyncTask.swift b/stdlib/public/Concurrency/PartialAsyncTask.swift index 821d134796da3..964195d4a00de 100644 --- a/stdlib/public/Concurrency/PartialAsyncTask.swift +++ b/stdlib/public/Concurrency/PartialAsyncTask.swift @@ -887,10 +887,9 @@ internal func _resumeUnsafeThrowingContinuationWithError( /// - SeeAlso: `withCheckedContinuation(function:_:)` /// - SeeAlso: `withCheckedThrowingContinuation(function:_:)` @available(SwiftStdlib 5.1, *) -@_alwaysEmitIntoClient +@export(implementation) @unsafe -public func withUnsafeContinuation( - isolation: isolated (any Actor)? = #isolation, +public nonisolated(nonsending) func withUnsafeContinuation( _ fn: (UnsafeContinuation) -> Void ) async -> sending T { return await Builtin.withUnsafeContinuation { @@ -924,10 +923,9 @@ public func withUnsafeContinuation( /// - SeeAlso: `withCheckedContinuation(function:_:)` /// - SeeAlso: `withCheckedThrowingContinuation(function:_:)` @available(SwiftStdlib 5.1, *) -@_alwaysEmitIntoClient +@export(implementation) @unsafe -public func withUnsafeThrowingContinuation( - isolation: isolated (any Actor)? = #isolation, +public nonisolated(nonsending) func withUnsafeThrowingContinuation( _ fn: (UnsafeContinuation) -> Void ) async throws -> sending T { return try await Builtin.withUnsafeThrowingContinuation { diff --git a/stdlib/public/Concurrency/Task+TaskExecutor.swift b/stdlib/public/Concurrency/Task+TaskExecutor.swift index ca04d74c63afe..a016c0bf31fc3 100644 --- a/stdlib/public/Concurrency/Task+TaskExecutor.swift +++ b/stdlib/public/Concurrency/Task+TaskExecutor.swift @@ -208,18 +208,22 @@ extension UnsafeCurrentTask { // ==== Runtime --------------------------------------------------------------- @available(SwiftStdlib 6.0, *) +@usableFromInline @_silgen_name("swift_task_getPreferredTaskExecutor") internal func _getPreferredUnownedTaskExecutor() -> Builtin.Executor +@usableFromInline typealias TaskExecutorPreferenceStatusRecord = UnsafeRawPointer @available(SwiftStdlib 6.0, *) +@usableFromInline @_silgen_name("swift_task_pushTaskExecutorPreference") internal func _pushTaskExecutorPreference(_ executor: Builtin.Executor) -> TaskExecutorPreferenceStatusRecord @available(SwiftStdlib 6.0, *) @_silgen_name("swift_task_popTaskExecutorPreference") +@usableFromInline internal func _popTaskExecutorPreference( record: TaskExecutorPreferenceStatusRecord ) diff --git a/stdlib/public/Concurrency/TaskCancellation.swift b/stdlib/public/Concurrency/TaskCancellation.swift index 5c24671fb1310..6866ac6ae8ad9 100644 --- a/stdlib/public/Concurrency/TaskCancellation.swift +++ b/stdlib/public/Concurrency/TaskCancellation.swift @@ -74,10 +74,25 @@ import Swift /// Therefore, if a cancellation handler must acquire a lock, other code should /// not cancel tasks or resume continuations while holding that lock. @available(SwiftStdlib 5.1, *) -#if !$Embedded -@backDeployed(before: SwiftStdlib 6.0) -#endif -public func withTaskCancellationHandler( +@export(implementation) +public nonisolated(nonsending) func withTaskCancellationHandler( + operation: nonisolated(nonsending) () async throws -> T, + onCancel handler: @Sendable () -> Void +) async rethrows -> T { + // unconditionally add the cancellation record to the task. + // if the task was already cancelled, it will be executed right away. + let record = unsafe _taskAddCancellationHandler(handler: handler) + defer { unsafe _taskRemoveCancellationHandler(record: record) } + + return try await operation() +} + +// Note: Deprecated version which would still hop if we did not close over an `isolated` parameter +// with the operation closure. Instead, we should do what the docs of this method promise - and not hop at all, +// by using the new nonisolated(nonsending) +@available(SwiftStdlib 5.1, *) +@_silgen_name("$ss27withTaskCancellationHandler9operation8onCancel9isolationxxyYaKXE_yyYbXEScA_pSgYitYaKlF") +public func _isolatedParam_withTaskCancellationHandler( operation: () async throws -> T, onCancel handler: @Sendable () -> Void, isolation: isolated (any Actor)? = #isolation diff --git a/stdlib/public/Concurrency/TaskGroup.swift b/stdlib/public/Concurrency/TaskGroup.swift index 501404094c993..5bdca5448255b 100644 --- a/stdlib/public/Concurrency/TaskGroup.swift +++ b/stdlib/public/Concurrency/TaskGroup.swift @@ -46,11 +46,43 @@ import Swift /// /// - SeeAlso: ``TaskGroup`` @available(SwiftStdlib 5.1, *) +@export(implementation) +public nonisolated(nonsending) func withTaskGroup( + of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self, + returning returnType: GroupResult.Type = GroupResult.self, + body: nonisolated(nonsending) (inout TaskGroup) async -> GroupResult +) async -> GroupResult { + #if compiler(>=5.5) && $BuiltinTaskGroupWithArgument + + let _group = Builtin.createTaskGroup(ChildTaskResult.self) + var group = TaskGroup(group: _group) + + // Run the withTaskGroup body. + let result = await body(&group) + + await group.awaitAllRemainingTasksNonsending() + + Builtin.destroyTaskGroup(_group) + return result + + #else + fatalError("Swift compiler is incompatible with this SDK version") + #endif +} + +// Overload with isolated parameter for ABI compatibility. +@available(SwiftStdlib 5.1, *) #if !hasFeature(Embedded) @backDeployed(before: SwiftStdlib 6.0) #endif @inlinable -public func withTaskGroup( +@abi(func withTaskGroup( + of childTaskResultType: ChildTaskResult.Type, + returning returnType: GroupResult.Type, + isolation: isolated (any Actor)?, + body: (inout TaskGroup) async -> GroupResult +) async -> GroupResult) +func _isolatedParameter_withTaskGroup( of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self, returning returnType: GroupResult.Type = GroupResult.self, isolation: isolated (any Actor)? = #isolation, @@ -107,6 +139,8 @@ public func _unsafeInheritExecutor_withTaskGroup( #endif } +// ==== ThrowingTaskGroup ----------------------------------------------------- + /// Starts a new scope that can contain a dynamic number of throwing child tasks. /// /// A group *always* waits for all of its child tasks @@ -172,11 +206,52 @@ public func _unsafeInheritExecutor_withTaskGroup( /// - SeeAlso: ``ThrowingTaskGroup`` /// - SeeAlso: ``ThrowingDiscardingTaskGroup`` @available(SwiftStdlib 5.1, *) +@export(implementation) +public nonisolated(nonsending) func withThrowingTaskGroup( + of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self, + returning returnType: GroupResult.Type = GroupResult.self, + body: nonisolated(nonsending) (inout ThrowingTaskGroup) async throws -> GroupResult +) async rethrows -> GroupResult { + #if compiler(>=5.5) && $BuiltinTaskGroupWithArgument + + let _group = Builtin.createTaskGroup(ChildTaskResult.self) + var group = ThrowingTaskGroup(group: _group) + + do { + // Run the withTaskGroup body. + let result = try await body(&group) + + await group.awaitAllRemainingTasksNonsending() + Builtin.destroyTaskGroup(_group) + + return result + } catch { + group.cancelAll() + + await group.awaitAllRemainingTasksNonsending() + Builtin.destroyTaskGroup(_group) + + throw error + } + + #else + fatalError("Swift compiler is incompatible with this SDK version") + #endif +} + +// Overload with isolated parameter for ABI compatibility. +@available(SwiftStdlib 5.1, *) #if !hasFeature(Embedded) @backDeployed(before: SwiftStdlib 6.0) #endif @inlinable -public func withThrowingTaskGroup( +@abi(func withThrowingTaskGroup( + of childTaskResultType: ChildTaskResult.Type, + returning returnType: GroupResult.Type, + isolation: isolated (any Actor)?, + body: (inout ThrowingTaskGroup) async throws -> GroupResult +) async rethrows -> GroupResult) +func _isolatedParameter_withThrowingTaskGroup( of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self, returning returnType: GroupResult.Type = GroupResult.self, isolation: isolated (any Actor)? = #isolation, @@ -424,10 +499,9 @@ public struct TaskGroup { /// /// - Returns: The value returned by the next child task that completes. @available(SwiftStdlib 5.1, *) - #if !hasFeature(Embedded) - @backDeployed(before: SwiftStdlib 6.0) - #endif - public mutating func next(isolation: isolated (any Actor)? = #isolation) async -> ChildTaskResult? { + @export(implementation) + @abi(mutating func nextNonisolatedNonsending() async -> ChildTaskResult?) + public nonisolated(nonsending) mutating func next() async -> ChildTaskResult? { // try!-safe because this function only exists for Failure == Never, // and as such, it is impossible to spawn a throwing child task. return try! await _taskGroupWaitNext(group: _group) // !-safe cannot throw, we're a non-throwing TaskGroup @@ -435,32 +509,48 @@ public struct TaskGroup { @available(SwiftStdlib 5.1, *) @_disfavoredOverload - public mutating func next() async -> ChildTaskResult? { + @usableFromInline + @abi(mutating func next(isolation: isolated (any Actor)?) async -> ChildTaskResult?) + mutating func _isolatedParam_next(isolation: isolated (any Actor)? = #isolation) async -> ChildTaskResult? { + // try!-safe because this function only exists for Failure == Never, + // and as such, it is impossible to spawn a throwing child task. + return try! await _taskGroupWaitNext(group: _group) // !-safe cannot throw, we're a non-throwing TaskGroup + } + + @available(SwiftStdlib 5.1, *) + @usableFromInline + @_disfavoredOverload + @abi(mutating func next() async -> ChildTaskResult?) + mutating func _noParam_next() async -> ChildTaskResult? { // try!-safe because this function only exists for Failure == Never, // and as such, it is impossible to spawn a throwing child task. return try! await _taskGroupWaitNext(group: _group) // !-safe cannot throw, we're a non-throwing TaskGroup } /// Await all of the pending tasks added this group. + @available(SwiftStdlib 5.1, *) + @export(implementation) + internal nonisolated(nonsending) mutating func awaitAllRemainingTasksNonsending() async { + while let _ = await next() {} + } + @usableFromInline @available(SwiftStdlib 5.1, *) - #if !hasFeature(Embedded) - @backDeployed(before: SwiftStdlib 6.0) - #endif + @available(*, deprecated, message: "Prefer nonisolated(nonsending) version in order to avoid un-necessary task enqueues.") internal mutating func awaitAllRemainingTasks(isolation: isolated (any Actor)? = #isolation) async { - while let _ = await next(isolation: isolation) {} + while let _ = await _isolatedParam_next(isolation: nil) {} } @usableFromInline @available(SwiftStdlib 5.1, *) internal mutating func awaitAllRemainingTasks() async { - while let _ = await next(isolation: nil) {} + while let _ = await _isolatedParam_next(isolation: nil) {} } /// Wait for all of the group's remaining tasks to complete. - @_alwaysEmitIntoClient - public mutating func waitForAll(isolation: isolated (any Actor)? = #isolation) async { - await awaitAllRemainingTasks(isolation: isolation) + @export(implementation) + public nonisolated(nonsending) mutating func waitForAll() async { + await awaitAllRemainingTasksNonsending() } /// A Boolean value that indicates whether the group has any remaining tasks. @@ -585,11 +675,25 @@ public struct ThrowingTaskGroup { } /// Await all the remaining tasks on this group. + @export(implementation) + @available(SwiftStdlib 5.1, *) + internal nonisolated(nonsending) mutating func awaitAllRemainingTasksNonsending() async { + while true { + do { + guard let _ = try await next() else { + return + } + } catch {} + } + } + @usableFromInline @available(SwiftStdlib 5.1, *) #if !hasFeature(Embedded) @backDeployed(before: SwiftStdlib 6.0) #endif + @_disfavoredOverload + // @available(*, deprecated, message: "Prefer nonsending(nonisolated) version in order to avoid un-necessary task enqueues.") internal mutating func awaitAllRemainingTasks(isolation: isolated (any Actor)? = #isolation) async { while true { do { @@ -721,22 +825,49 @@ public struct ThrowingTaskGroup { /// - Throws: The error thrown by the next child task that completes. /// /// - SeeAlso: `nextResult()` + @available(SwiftStdlib 5.1, *) + @export(implementation) + // FIXME: doesn't work to rename and the AEIC conflicts with the existing method: @abi(mutating func nextNonisolatedNonsending() async throws -> ChildTaskResult?) + @_silgen_name("$sScg6next_XxSgyYaKF") // _X to avoid conflict with existing ABI, even though AEIC does not have ABI + public nonisolated(nonsending) mutating func next() async throws -> ChildTaskResult? { + return try await _taskGroupWaitNext(group: _group) + } + @available(SwiftStdlib 5.1, *) #if !hasFeature(Embedded) @backDeployed(before: SwiftStdlib 6.0) #endif - public mutating func next(isolation: isolated (any Actor)? = #isolation) async throws -> ChildTaskResult? { + @usableFromInline + @_disfavoredOverload + @abi(mutating func next(isolation: isolated (any Actor)?) async throws -> ChildTaskResult?) + mutating func next(isolation: isolated (any Actor)? = #isolation) async throws -> ChildTaskResult? { return try await _taskGroupWaitNext(group: _group) } @available(SwiftStdlib 5.1, *) @_disfavoredOverload - public mutating func next() async throws -> ChildTaskResult? { + @usableFromInline + @_silgen_name("$sScg4nextxSgyYaKF") + mutating func _noParam_next() async throws -> ChildTaskResult? { return try await _taskGroupWaitNext(group: _group) } - @_silgen_name("$sScg10nextResults0B0Oyxq_GSgyYaKF") + @available(SwiftStdlib 5.1, *) + @export(implementation) + mutating nonisolated(nonsending) func nextResultNonisolatedNonsending() async throws -> Result? { + do { + guard let success: ChildTaskResult = try await _taskGroupWaitNext(group: _group) else { + return nil + } + + return .success(success) + } catch { + return .failure(error as! Failure) // as!-safe, because we are only allowed to throw Failure (Error) + } + } + @usableFromInline + @abi(mutating func nextResult() async throws -> Result?) mutating func nextResultForABI() async throws -> Result? { do { guard let success: ChildTaskResult = try await _taskGroupWaitNext(group: _group) else { @@ -897,7 +1028,11 @@ extension TaskGroup: AsyncSequence { /// /// - Returns: The value returned by the next child task that completes, /// or `nil` if there are no remaining child tasks, - public mutating func next() async -> Element? { + @_disfavoredOverload + @export(implementation) + // FIXME: cannot use @abi(mutating func next() async -> Element?) + @_silgen_name("$sScG8IteratorV6next_XxSgyYaF") // _X only to avoid name clash with next() even though this func has no ABI + public nonisolated(nonsending) mutating func next() async -> Element? { guard !finished else { return nil } guard let element = await group.next() else { finished = true @@ -906,23 +1041,26 @@ extension TaskGroup: AsyncSequence { return element } - /// Advances to and returns the result of the next child task. - /// - /// The elements returned from this method - /// appear in the order that the tasks *completed*, - /// not in the order that those tasks were added to the task group. - /// After this method returns `nil`, - /// this iterator is guaranteed to never produce more values. - /// - /// For more information about the iteration order and semantics, - /// see `TaskGroup.next()`. - /// - /// - Returns: The value returned by the next child task that completes, - /// or `nil` if there are no remaining child tasks, + @available(SwiftStdlib 5.1, *) + @_disfavoredOverload + @usableFromInline + @_silgen_name("$sScG8IteratorV4nextxSgyYaF") // FIXME: cannot use to rename @abi(mutating func next() async -> Element?) + mutating func _noParam_next() async -> Element? { + guard !finished else { return nil } + guard let element = await group.next() else { + finished = true + return nil + } + return element + } + + // Implements protocol requirement @available(SwiftStdlib 6.0, *) + @_disfavoredOverload + @abi(mutating func next(isolation actor: isolated (any Actor)?) async -> Element?) public mutating func next(isolation actor: isolated (any Actor)?) async -> Element? { guard !finished else { return nil } - guard let element = await group.next(isolation: actor) else { + guard let element = await group._isolatedParam_next(isolation: actor) else { finished = true return nil } @@ -1009,7 +1147,30 @@ extension ThrowingTaskGroup: AsyncSequence { /// /// - Returns: The value returned by the next child task that completes, /// or `nil` if there are no remaining child tasks, - public mutating func next() async throws -> Element? { + @available(SwiftStdlib 5.1, *) // since we're witnessing the next() without isolated parameter + @export(implementation) + // FIXME: would want to use @abi here + @_silgen_name("$sScg8IteratorV6next_XxSgyYaKF") // _X just to avoid name clash with $sScg8IteratorV4nextxSgyYaKF, this does not introduce ABI + public nonisolated(nonsending) mutating func next() async throws(Failure) -> Element? { + guard !finished else { return nil } + do { + guard let element = try await group.next() else { + finished = true + return nil + } + return element + } catch { + finished = true + throw error as! Failure + } + } + + @available(SwiftStdlib 5.1, *) + @_disfavoredOverload + @usableFromInline + // FIXME: cannot use abi here since we need to rename @abi(mutating func next() async -> Element?) + @_silgen_name("$sScg8IteratorV4nextxSgyYaKF") + mutating func _noParam_next() async throws -> Element? { guard !finished else { return nil } do { guard let element = try await group.next() else { @@ -1023,22 +1184,9 @@ extension ThrowingTaskGroup: AsyncSequence { } } - /// Advances to and returns the result of the next child task. - /// - /// The elements returned from this method - /// appear in the order that the tasks *completed*, - /// not in the order that those tasks were added to the task group. - /// After this method returns `nil`, - /// this iterator is guaranteed to never produce more values. - /// - /// For more information about the iteration order and semantics, - /// see `ThrowingTaskGroup.next()` - /// - /// - Throws: The error thrown by the next child task that completes. - /// - /// - Returns: The value returned by the next child task that completes, - /// or `nil` if there are no remaining child tasks, + // Implements protocol requirement @available(SwiftStdlib 6.0, *) + @_disfavoredOverload public mutating func next(isolation actor: isolated (any Actor)?) async throws(Failure) -> Element? { guard !finished else { return nil } do { @@ -1052,7 +1200,7 @@ extension ThrowingTaskGroup: AsyncSequence { throw error as! Failure } } - + public mutating func cancel() { finished = true group.cancelAll() diff --git a/stdlib/public/Concurrency/TaskLocal.swift b/stdlib/public/Concurrency/TaskLocal.swift index 256627880e974..b20861e402717 100644 --- a/stdlib/public/Concurrency/TaskLocal.swift +++ b/stdlib/public/Concurrency/TaskLocal.swift @@ -200,11 +200,28 @@ public final class TaskLocal: Sendable, CustomStringConvertible /// /// If the value is a reference type, it will be retained for the duration of /// the operation closure. + @discardableResult + @available(SwiftStdlib 5.1, *) + @export(implementation) + public nonisolated(nonsending) func withValue(_ valueDuringOperation: Value, + operation: nonisolated(nonsending) () async throws -> R, + file: String = #fileID, line: UInt = #line) async rethrows -> R { + return try await withValueNonisolatedNonsendingImpl( + valueDuringOperation, + operation: operation, + file: file, line: line) + } + @inlinable @discardableResult @available(SwiftStdlib 5.1, *) - @backDeployed(before: SwiftStdlib 6.0) - public func withValue(_ valueDuringOperation: Value, + @available(*, deprecated, message: "Replaced by nonisolated(nonsending) version") + @abi(func withValue(_ valueDuringOperation: Value, + operation: () async throws -> R, + isolation: isolated (any Actor)?, + file: String, line: UInt) async rethrows -> R + ) + internal func _isolatedParam_withValue(_ valueDuringOperation: Value, operation: () async throws -> R, isolation: isolated (any Actor)? = #isolation, file: String = #fileID, line: UInt = #line) async rethrows -> R { @@ -251,6 +268,18 @@ public final class TaskLocal: Sendable, CustomStringConvertible /// to swift_task_de/alloc for the copy as follows: /// - withValue contains the compiler-emitted calls swift_task_de/alloc. /// - withValueImpl contains the calls to Builtin.taskLocalValuePush/Pop + @discardableResult + @export(implementation) + internal nonisolated(nonsending) + func withValueNonisolatedNonsendingImpl(_ valueDuringOperation: __owned Value, + operation: nonisolated(nonsending) () async throws -> R, + file: String = #fileID, line: UInt = #line) async rethrows -> R { + _taskLocalValuePush(key: key, value: consume valueDuringOperation) + defer { _taskLocalValuePop() } + + return try await operation() + } + @inlinable @discardableResult @available(SwiftStdlib 5.1, *) diff --git a/stdlib/public/Observation/Sources/Observation/Observations.swift b/stdlib/public/Observation/Sources/Observation/Observations.swift index 3aa6d91ba56c6..ef883f3f75de7 100644 --- a/stdlib/public/Observation/Sources/Observation/Observations.swift +++ b/stdlib/public/Observation/Sources/Observation/Observations.swift @@ -89,7 +89,7 @@ public struct Observations: AsyncSequence, Se // install a willChange continuation into the set of continuations // this must take a locally unique id (to the active calls of next) static func willChange(isolation iterationIsolation: isolated (any Actor)? = #isolation, state: _ManagedCriticalState, id: Int) async { - return await withUnsafeContinuation(isolation: iterationIsolation) { continuation in + return await withUnsafeContinuation { continuation in state.withCriticalRegion { state in defer { state.dirty = false } switch state.continuations[id] { @@ -236,11 +236,11 @@ public struct Observations: AsyncSequence, Se // back to the trailing edges of the mutations. In short, this enables the transactionality bounded by the // isolation of the mutation. await withTaskCancellationHandler(operation: { - await State.willChange(isolation: iterationIsolation, state: state, id: id) + await State.willChange(state: state, id: id) }, onCancel: { // ensure to clean out our continuation uon cancellation State.cancel(state, id: id) - }, isolation: iterationIsolation) + }) return try await trackEmission(isolation: iterationIsolation, state: state, id: id) } } catch { diff --git a/test/Concurrency/Runtime/cancellation_handler_operation_does_not_hop.swift b/test/Concurrency/Runtime/cancellation_handler_operation_does_not_hop.swift new file mode 100644 index 0000000000000..f4fdf8261f5d8 --- /dev/null +++ b/test/Concurrency/Runtime/cancellation_handler_operation_does_not_hop.swift @@ -0,0 +1,70 @@ +// RUN: %target-run-simple-swift( -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s +// REQUIRES: concurrency +// REQUIRES: executable_test + +// REQUIRES: concurrency_runtime +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: freestanding + +actor Canceller { + var hello: String = "checking..." + + func testFunc() async { + await withTaskCancellationHandler { + self.assertIsolated("wat in \(#function)!") + print("testFunc.withTaskCancellationHandler") // CHECK: testFunc.withTaskCancellationHandler + self.hello = "done!" + } onCancel: { + // noop + } + + // just a simple check to see we executed the closure + + await globalTestFunc() + } +} +func globalTestFunc(isolation: isolated (any Actor)? = #isolation) async { + isolation!.assertIsolated("wat in \(#function)!") + await withTaskCancellationHandler { + isolation!.assertIsolated("wat in \(#function)!") + print("globalTestFunc.withTaskCancellationHandler") // CHECK: globalTestFunc.withTaskCancellationHandler + } onCancel: { + // noop + } +} + +@MainActor +func testMainActor() async { + MainActor.preconditionIsolated("Expected main actor") + await withTaskCancellationHandler { + MainActor.preconditionIsolated("expected MainActor") + } onCancel: { + // noop + } +} + +// FIXME: rdar://155313349 - nonisolated(nonsending) closure does not pick up isolated parameter when the closure is throwing +func testMainActorIsolated(isolation: isolated (any Actor)? = #isolation) async { + return // FIXME: until rdar://155313349 is fixed + + isolation!.preconditionIsolated("Expected main actor") + MainActor.preconditionIsolated("Expected main actor") + await withTaskCancellationHandler { + print("_unsafeInheritExecutor_withTaskCancellationHandler") + MainActor.preconditionIsolated("expected MainActor") + } onCancel: { + // noop + } +} + +_ = await Canceller().testFunc() + +_ = await Task { @MainActor in + await testMainActor() +}.value + +_ = await Task { @MainActor in + await testMainActorIsolated() +}.value + +print("done") // CHECK: done \ No newline at end of file diff --git a/test/Concurrency/Runtime/must_not_hop_TaskLocal_withValue.swift b/test/Concurrency/Runtime/must_not_hop_TaskLocal_withValue.swift new file mode 100644 index 0000000000000..c3f076bc79b8d --- /dev/null +++ b/test/Concurrency/Runtime/must_not_hop_TaskLocal_withValue.swift @@ -0,0 +1,142 @@ +// RUN: %target-run-simple-swift( -O -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s --dump-input=always +// REQUIRES: concurrency +// REQUIRES: executable_test + +// REQUIRES: concurrency_runtime +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: freestanding +// REQUIRES: libdispatch +// REQUIRES: synchronization + +import Synchronization + + + +if #available(SwiftStdlib 6.0, *) { + print("=== foo() async") + print("---------------------------------------") + await foo() +} + +// CHECK: === foo() async +// CHECK-NEXT: --------------------------------------- +// We hop to the task executor: +// CHECK-NEXT: [executor][task-executor] Enqueue (1) + +// CHECK-NEXT: foo - withTaskExecutorPreference + +// CHECK: foo - withTaskExecutorPreference - TL.withValue +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// TODO: could we eliminate this hop-back? +// CHECK-NEXT: [executor][task-executor] Enqueue (2) +// CHECK-NEXT: foo - withTaskExecutorPreference - TL.withValue done + +// CHECK: foo - withTaskExecutorPreference - TL.withValue throwing +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: foo - withTaskExecutorPreference - TL.withValue throwing done +// CHECK-NEXT: [executor][task-executor] Enqueue (3) + +// CHECK: foo - withTaskExecutorPreference done + +// CHECK: == Make: actor Foo +// CHECK-NEXT: --------------------------------------- +// CHECK-NEXT: [executor][actor-executor] Enqueue (1) +// CHECK-NEXT: actor.foo + +// CHECK: actor.foo - TL.withValue +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - TL.withValue done + +// CHECK: actor.foo - TL.withValue throwing +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - TL.withValue throwing done + +// No more enqueues are expected afterwards +// CHECK-NOT: [executor] + +@TaskLocal +var myTaskLocal: String = "" + +nonisolated(nonsending) func someFunc() async throws { + print("nonisolated(nonsending) someFunc() async") +} + +@available(SwiftStdlib 6.0, *) +@concurrent +func foo() async { + await withTaskExecutorPreference(AssertExactEnqueueCountExecutor(maxEnqueues: 8, name: "task-executor")) { + print("foo - withTaskExecutorPreference") + + await $myTaskLocal.withValue("value") { + print("foo - withTaskExecutorPreference - TL.withValue") + try? await someFunc() + } + print("foo - withTaskExecutorPreference - TL.withValue done") + + try! await $myTaskLocal.withValue("value") { + print("foo - withTaskExecutorPreference - TL.withValue throwing") + try await someFunc() + } + print("foo - withTaskExecutorPreference - TL.withValue throwing done") + } + print("foo - withTaskExecutorPreference done") + + print("== Make: actor Foo") + print("---------------------------------------") + await Foo().foo() +} + +@available(SwiftStdlib 6.0, *) +actor Foo { + let exec = AssertExactEnqueueCountExecutor(maxEnqueues: 8, name: "actor-executor") + + nonisolated var unownedExecutor: UnownedSerialExecutor { + self.exec.asUnownedSerialExecutor() + } + + func foo() async { + print("actor.foo") + + await $myTaskLocal.withValue("value") { + print("actor.foo - TL.withValue") + try? await someFunc() + } + print("actor.foo - TL.withValue done") + + try! await $myTaskLocal.withValue("value") { + print("actor.foo - TL.withValue throwing") + try await someFunc() + } + print("actor.foo - TL.withValue throwing done") + print("actor.foo done") + } +} + +@available(SwiftStdlib 6.0, *) +final class AssertExactEnqueueCountExecutor: TaskExecutor, SerialExecutor { + let maxEnqueues: Int + let enqueueCount: Atomic + + let name: String + + init(maxEnqueues: Int, name: String) { + self.maxEnqueues = maxEnqueues + self.enqueueCount = .init(0) + self.name = name + } + + public func enqueue(_ job: consuming ExecutorJob) { + let newEnqueueValue = self.enqueueCount.add(1, ordering: .relaxed).newValue + if newEnqueueValue > self.maxEnqueues { + fatalError("Got unexpected enqueue (\(newEnqueueValue)), in: \(self.name)") + } + print("[executor][\(self.name)] Enqueue (\(newEnqueueValue))") + job.runSynchronously(on: self.asUnownedSerialExecutor()) + } + + public func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } +} + +print("done") // CHECK: done diff --git a/test/Concurrency/Runtime/must_not_hop_withContinuation.swift b/test/Concurrency/Runtime/must_not_hop_withContinuation.swift new file mode 100644 index 0000000000000..c7cb8e58d8226 --- /dev/null +++ b/test/Concurrency/Runtime/must_not_hop_withContinuation.swift @@ -0,0 +1,168 @@ +// RUN: %target-run-simple-swift( -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s --dump-input=always +// REQUIRES: concurrency +// REQUIRES: executable_test + +// REQUIRES: concurrency_runtime +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: freestanding +// REQUIRES: libdispatch +// REQUIRES: synchronization + +import Synchronization + + +if #available(SwiftStdlib 6.0, *) { + print("=== foo() async") + print("---------------------------------------") + await foo() +} + +// CHECK: === foo() async +// CHECK-NEXT: --------------------------------------- +// We hop to the task executor: +// CHECK-NEXT: [executor][task-executor] Enqueue (1) + +// CHECK-NEXT: foo - withTaskExecutorPreference + +// CHECK: foo - withTaskExecutorPreference - withCheckedContinuation +// CHECK-NEXT: foo - withTaskExecutorPreference - withCheckedContinuation done + +// CHECK: foo - withTaskExecutorPreference - withUnsafeContinuation +// CHECK-NEXT: foo - withTaskExecutorPreference - withUnsafeContinuation done + +// CHECK: foo - withTaskExecutorPreference - withCheckedThrowingContinuation +// CHECK-NEXT: foo - withTaskExecutorPreference - withCheckedThrowingContinuation done + +// CHECK: foo - withTaskExecutorPreference - withUnsafeThrowingContinuation +// CHECK-NEXT: foo - withTaskExecutorPreference - withUnsafeThrowingContinuation done + +// By checking that this is the second enqueue here, +// we check that there was no stray enqueues between with... invocations: +// CHECK-NEXT: [executor][task-executor] Enqueue (2) + +// CHECK-NEXT: foo - withTaskExecutorPreference done + +// CHECK-NEXT: == Make: actor Foo +// CHECK-NEXT: --------------------------------------- +// CHECK-NEXT: [executor][actor-executor] Enqueue (1) +// CHECK-NEXT: actor.foo + +// CHECK: actor.foo - withCheckedContinuation +// CHECK-NEXT: actor.foo - withCheckedContinuation done + +// CHECK: actor.foo - withUnsafeContinuation +// CHECK-NEXT: actor.foo - withUnsafeContinuation done + +// CHECK: actor.foo - withCheckedThrowingContinuation +// CHECK-NEXT: actor.foo - withCheckedThrowingContinuation done + +// CHECK: actor.foo - withUnsafeThrowingContinuation +// CHECK-NEXT: actor.foo - withUnsafeThrowingContinuation done +// CHECK-NEXT: actor.foo done + +// No more enqueues are expected afterwards +// CHECK-NOT: [executor] + +@available(SwiftStdlib 6.0, *) +@concurrent +func foo() async { + await withTaskExecutorPreference(AssertExactEnqueueCountExecutor(maxEnqueues: 2, name: "task-executor")) { + print("foo - withTaskExecutorPreference") + await withCheckedContinuation { cont in + print("foo - withTaskExecutorPreference - withCheckedContinuation") + cont.resume() + } + print("foo - withTaskExecutorPreference - withCheckedContinuation done") + + await withUnsafeContinuation { cont in + print("foo - withTaskExecutorPreference - withUnsafeContinuation") + cont.resume() + } + print("foo - withTaskExecutorPreference - withUnsafeContinuation done") + + try! await withCheckedThrowingContinuation { cont in + print("foo - withTaskExecutorPreference - withCheckedThrowingContinuation") + cont.resume() + } + print("foo - withTaskExecutorPreference - withCheckedThrowingContinuation done") + + try! await withUnsafeThrowingContinuation { cont in + print("foo - withTaskExecutorPreference - withUnsafeThrowingContinuation") + cont.resume() + } + print("foo - withTaskExecutorPreference - withUnsafeThrowingContinuation done") + } + print("foo - withTaskExecutorPreference done") + + print("== Make: actor Foo") + print("---------------------------------------") + await Foo().foo() +} + +@available(SwiftStdlib 6.0, *) +actor Foo { + let exec = AssertExactEnqueueCountExecutor(maxEnqueues: 2, name: "actor-executor") + + nonisolated var unownedExecutor: UnownedSerialExecutor { + self.exec.asUnownedSerialExecutor() + } + + func foo() async { + print("actor.foo") + + await withCheckedContinuation { cont in + print("actor.foo - withCheckedContinuation") + cont.resume() + } + print("actor.foo - withCheckedContinuation done") + + await withUnsafeContinuation { cont in + print("actor.foo - withUnsafeContinuation") + cont.resume() + } + print("actor.foo - withUnsafeContinuation done") + + try! await withCheckedThrowingContinuation { cont in + print("actor.foo - withCheckedThrowingContinuation") + cont.resume() + } + print("actor.foo - withCheckedThrowingContinuation done") + + try! await withUnsafeThrowingContinuation { cont in + print("actor.foo - withUnsafeThrowingContinuation") + cont.resume() + } + print("actor.foo - withUnsafeThrowingContinuation done") + + print("actor.foo done") + } +} + +@available(SwiftStdlib 6.0, *) +final class AssertExactEnqueueCountExecutor: TaskExecutor, SerialExecutor { + let maxEnqueues: Int + let enqueueCount: Atomic + + let name: String + + init(maxEnqueues: Int, name: String) { + self.maxEnqueues = maxEnqueues + self.enqueueCount = .init(0) + self.name = name + } + + public func enqueue(_ job: consuming ExecutorJob) { + let newEnqueueValue = self.enqueueCount.add(1, ordering: .relaxed).newValue + if newEnqueueValue > self.maxEnqueues { + fatalError("Got unexpected enqueue (\(newEnqueueValue)), in: \(self.name)") + } + print("[executor][\(self.name)] Enqueue (\(newEnqueueValue))") + job.runSynchronously(on: self.asUnownedSerialExecutor()) + } + + public func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } +} + +print("done") // CHECK: done diff --git a/test/Concurrency/Runtime/must_not_hop_withTaskCancellationHandler.swift b/test/Concurrency/Runtime/must_not_hop_withTaskCancellationHandler.swift new file mode 100644 index 0000000000000..83252877bb224 --- /dev/null +++ b/test/Concurrency/Runtime/must_not_hop_withTaskCancellationHandler.swift @@ -0,0 +1,127 @@ +// RUN: %target-run-simple-swift( -O -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s --dump-input=always +// REQUIRES: concurrency +// REQUIRES: executable_test + +// REQUIRES: concurrency_runtime +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: freestanding +// REQUIRES: libdispatch +// REQUIRES: synchronization + +import Synchronization + +if #available(SwiftStdlib 6.0, *) { + print("=== foo() async") + print("---------------------------------------") + await foo() +} + +// CHECK: === foo() async +// CHECK-NEXT: --------------------------------------- +// We hop to the task executor: +// CHECK-NEXT: [executor][task-executor] Enqueue (1) +// CHECK-NEXT: foo - withTaskExecutorPreference + +// CHECK-NEXT: foo - withTaskExecutorPreference - withTaskCancellationHandler +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: foo - withTaskExecutorPreference - withTaskCancellationHandler (after someFunc) +// CHECK-NEXT: [executor][task-executor] Enqueue (2) +// CHECK-NEXT: foo - withTaskExecutorPreference - withTaskCancellationHandler done +// CHECK-NEXT: [executor][task-executor] Enqueue (3) +// CHECK-NEXT: foo - withTaskExecutorPreference done + +// CHECK: == Make: actor Foo +// CHECK-NEXT: --------------------------------------- + +// Hop onto the actor executor: +// CHECK-NEXT: [executor][actor-executor] Enqueue (1) +// CHECK-NEXT: actor.foo + +// Crucially, there must not be a hop before entering the operation: +// CHECK-NEXT: actor.foo - withTaskCancellationHandler +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - withTaskCancellationHandler (after someFunc) +// CHECK-NEXT: [executor][actor-executor] Enqueue (2) +// CHECK-NEXT: actor.foo - withTaskCancellationHandler done + +// No more enqueues are expected afterwards +// CHECK-NOT: [executor] + +nonisolated(nonsending) func someFunc() async throws { + print("nonisolated(nonsending) someFunc() async") +} + +@available(SwiftStdlib 6.0, *) +@concurrent +func foo() async { + await withTaskExecutorPreference(AssertExactEnqueueCountExecutor(maxEnqueues: 3, name: "task-executor")) { + print("foo - withTaskExecutorPreference") + + await withTaskCancellationHandler { + print("foo - withTaskExecutorPreference - withTaskCancellationHandler") + try? await someFunc() + print("foo - withTaskExecutorPreference - withTaskCancellationHandler (after someFunc)") + } onCancel: { + fatalError("Should not be cancelled") + } + print("foo - withTaskExecutorPreference - withTaskCancellationHandler done") + } + print("foo - withTaskExecutorPreference done") + + print("== Make: actor Foo") + print("---------------------------------------") + await Foo().foo() +} + +@available(SwiftStdlib 6.0, *) +actor Foo { + let exec = AssertExactEnqueueCountExecutor(maxEnqueues: 3, name: "actor-executor") + + nonisolated var unownedExecutor: UnownedSerialExecutor { + self.exec.asUnownedSerialExecutor() + } + + func foo() async { + print("actor.foo") + + await withTaskCancellationHandler { + print("actor.foo - withTaskCancellationHandler") + try? await someFunc() + print("actor.foo - withTaskCancellationHandler (after someFunc)") + } onCancel: { + fatalError("Should not be cancelled") + } + print("actor.foo - withTaskCancellationHandler done") + } +} + +@available(SwiftStdlib 6.0, *) +final class AssertExactEnqueueCountExecutor: TaskExecutor, SerialExecutor { + let maxEnqueues: Int + let enqueueCount: Atomic + + let name: String + + init(maxEnqueues: Int, name: String) { + self.maxEnqueues = maxEnqueues + self.enqueueCount = .init(0) + self.name = name + } + + public func enqueue(_ job: consuming ExecutorJob) { + let newEnqueueValue = self.enqueueCount.add(1, ordering: .relaxed).newValue + if newEnqueueValue > self.maxEnqueues { + fatalError("Got unexpected enqueue (\(newEnqueueValue)), in: \(self.name)") + } + print("[executor][\(self.name)] Enqueue (\(newEnqueueValue))") + job.runSynchronously( + isolatedTo: self.asUnownedSerialExecutor(), + taskExecutor: self.asUnownedTaskExecutor()) + } + + public func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } +} + +print("done") // CHECK: done diff --git a/test/Concurrency/Runtime/must_not_hop_withTaskGroup.swift b/test/Concurrency/Runtime/must_not_hop_withTaskGroup.swift new file mode 100644 index 0000000000000..c8acf5ee19c1b --- /dev/null +++ b/test/Concurrency/Runtime/must_not_hop_withTaskGroup.swift @@ -0,0 +1,200 @@ +// RUN: %target-run-simple-swift( -O -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s --dump-input=always +// REQUIRES: concurrency +// REQUIRES: executable_test + +// REQUIRES: concurrency_runtime +// UNSUPPORTED: back_deployment_runtime +// UNSUPPORTED: freestanding +// REQUIRES: libdispatch +// REQUIRES: synchronization + +import Synchronization + +if #available(SwiftStdlib 6.0, *) { + print("=== foo() async") + print("---------------------------------------") + await foo() +} + +// CHECK: === foo() async +// CHECK-NEXT: --------------------------------------- +// We hop to the task executor: +// CHECK-NEXT: [executor][task-executor] Enqueue (1) + +// CHECK-NEXT: foo - withTaskExecutorPreference + +// CHECK-NEXT: foo - withTaskExecutorPreference - withTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: foo - withTaskExecutorPreference - withTaskGroup (after someFunc) +// CHECK-NEXT: foo - withTaskExecutorPreference - withTaskGroup done + +// CHECK-NEXT: foo - withTaskExecutorPreference - withThrowingTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: foo - withTaskExecutorPreference - withThrowingTaskGroup (after someFunc) +// CHECK-NEXT: foo - withTaskExecutorPreference - withThrowingTaskGroup done + +// CHECK-NEXT: foo - withTaskExecutorPreference - withDiscardingTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: foo - withTaskExecutorPreference - withDiscardingTaskGroup (after someFunc) +// CHECK-NEXT: [executor][task-executor] Enqueue (2) +// CHECK-NEXT: foo - withTaskExecutorPreference - withDiscardingTaskGroup done + +// CHECK-NEXT: foo - withTaskExecutorPreference - withThrowingDiscardingTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: foo - withTaskExecutorPreference - withThrowingDiscardingTaskGroup (after someFunc) +// CHECK-NEXT: [executor][task-executor] Enqueue (3) +// CHECK-NEXT: foo - withTaskExecutorPreference - withThrowingDiscardingTaskGroup done + +// CHECK-NEXT: [executor][task-executor] Enqueue (4) +// CHECK-NEXT: foo - withTaskExecutorPreference done + +// CHECK: == Make: actor Foo +// CHECK-NEXT: --------------------------------------- + +// Hop onto the actor executor: +// CHECK-NEXT: [executor][actor-executor] Enqueue (1) +// CHECK-NEXT: actor.foo + +// CHECK-NEXT: actor.foo - withTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - withTaskGroup (after someFunc) +// CHECK-NEXT: actor.foo - withTaskGroup done + +// CHECK-NEXT: actor.foo - withThrowingTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - withThrowingTaskGroup (after someFunc) +// CHECK-NEXT: actor.foo - withThrowingTaskGroup done + +// CHECK-NEXT: actor.foo - withDiscardingTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - withDiscardingTaskGroup (after someFunc) +// CHECK-NEXT: [executor][actor-executor] Enqueue (2) +// CHECK-NEXT: actor.foo - withDiscardingTaskGroup done + +// CHECK-NEXT: actor.foo - withThrowingDiscardingTaskGroup +// CHECK-NEXT: nonisolated(nonsending) someFunc() async +// CHECK-NEXT: actor.foo - withThrowingDiscardingTaskGroup (after someFunc) +// TODO: can we reduce the number of enqueues here? +// CHECK-NEXT: [executor][actor-executor] Enqueue (3) +// CHECK-NEXT: [executor][actor-executor] Enqueue (4) +// CHECK-NEXT: actor.foo - withThrowingDiscardingTaskGroup done + +// No more enqueues are expected afterwards +// CHECK-NOT: [executor] + +nonisolated(nonsending) func someFunc() async throws { + print("nonisolated(nonsending) someFunc() async") +} + +@available(SwiftStdlib 6.0, *) +@concurrent +func foo() async { + await withTaskExecutorPreference(AssertExactEnqueueCountExecutor(maxEnqueues: 8, name: "task-executor")) { + print("foo - withTaskExecutorPreference") + + await withTaskGroup(of: Void.self) { group in + print("foo - withTaskExecutorPreference - withTaskGroup") + try? await someFunc() + print("foo - withTaskExecutorPreference - withTaskGroup (after someFunc)") + } + print("foo - withTaskExecutorPreference - withTaskGroup done") + + try! await withThrowingTaskGroup(of: Void.self) { group in + print("foo - withTaskExecutorPreference - withThrowingTaskGroup") + try await someFunc() + print("foo - withTaskExecutorPreference - withThrowingTaskGroup (after someFunc)") + } + print("foo - withTaskExecutorPreference - withThrowingTaskGroup done") + + await withDiscardingTaskGroup { group in + print("foo - withTaskExecutorPreference - withDiscardingTaskGroup") + try? await someFunc() + print("foo - withTaskExecutorPreference - withDiscardingTaskGroup (after someFunc)") + } + print("foo - withTaskExecutorPreference - withDiscardingTaskGroup done") + + try! await withThrowingDiscardingTaskGroup { group in + print("foo - withTaskExecutorPreference - withThrowingDiscardingTaskGroup") + try await someFunc() + print("foo - withTaskExecutorPreference - withThrowingDiscardingTaskGroup (after someFunc)") + } + print("foo - withTaskExecutorPreference - withThrowingDiscardingTaskGroup done") + } + print("foo - withTaskExecutorPreference done") + + print("== Make: actor Foo") + print("---------------------------------------") + await Foo().foo() +} + +@available(SwiftStdlib 6.0, *) +actor Foo { + let exec = AssertExactEnqueueCountExecutor(maxEnqueues: 8, name: "actor-executor") + + nonisolated var unownedExecutor: UnownedSerialExecutor { + self.exec.asUnownedSerialExecutor() + } + + func foo() async { + print("actor.foo") + + await withTaskGroup(of: Void.self) { group in + print("actor.foo - withTaskGroup") + try? await someFunc() + print("actor.foo - withTaskGroup (after someFunc)") + } + print("actor.foo - withTaskGroup done") + + try! await withThrowingTaskGroup(of: Void.self) { group in + print("actor.foo - withThrowingTaskGroup") + try await someFunc() + print("actor.foo - withThrowingTaskGroup (after someFunc)") + } + print("actor.foo - withThrowingTaskGroup done") + + await withDiscardingTaskGroup { group in + print("actor.foo - withDiscardingTaskGroup") + try? await someFunc() + print("actor.foo - withDiscardingTaskGroup (after someFunc)") + } + print("actor.foo - withDiscardingTaskGroup done") + + try! await withThrowingDiscardingTaskGroup { group in + print("actor.foo - withThrowingDiscardingTaskGroup") + try await someFunc() + print("actor.foo - withThrowingDiscardingTaskGroup (after someFunc)") + } + print("actor.foo - withThrowingDiscardingTaskGroup done") + } +} + +@available(SwiftStdlib 6.0, *) +final class AssertExactEnqueueCountExecutor: TaskExecutor, SerialExecutor { + let maxEnqueues: Int + let enqueueCount: Atomic + + let name: String + + init(maxEnqueues: Int, name: String) { + self.maxEnqueues = maxEnqueues + self.enqueueCount = .init(0) + self.name = name + } + + public func enqueue(_ job: consuming ExecutorJob) { + let newEnqueueValue = self.enqueueCount.add(1, ordering: .relaxed).newValue + if newEnqueueValue > self.maxEnqueues { + fatalError("Got unexpected enqueue (\(newEnqueueValue)), in: \(self.name)") + } + print("[executor][\(self.name)] Enqueue (\(newEnqueueValue))") + job.runSynchronously( + isolatedTo: self.asUnownedSerialExecutor(), + taskExecutor: self.asUnownedTaskExecutor()) + } + + public func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } +} + +print("done") // CHECK: done diff --git a/test/Concurrency/async_task_groups.swift b/test/Concurrency/async_task_groups.swift index cca22c125c268..d4345182c326c 100644 --- a/test/Concurrency/async_task_groups.swift +++ b/test/Concurrency/async_task_groups.swift @@ -193,7 +193,7 @@ extension Collection where Self: Sendable, Element: Sendable, Self.Index: Sendab var i = self.startIndex var submitted = 0 - func submitNext() async throws { + nonisolated(nonsending) func submitNext() async throws { // The reason that we emit an error here is b/c we capture the var box // to i and that is task isolated. This is the region isolation version // of the 'escaping closure captures non-escaping parameter' error. diff --git a/test/api-digester/stability-concurrency-abi.test b/test/api-digester/stability-concurrency-abi.test index 8e8e24505f24f..c23382c8581c4 100644 --- a/test/api-digester/stability-concurrency-abi.test +++ b/test/api-digester/stability-concurrency-abi.test @@ -79,22 +79,10 @@ Protocol Actor has added inherited protocol SendableMetatype Protocol Executor has added inherited protocol SendableMetatype Protocol SerialExecutor has added inherited protocol SendableMetatype -// #isolated adoption in with...Continuation -// api-digester is not aware of silgen_name trickery we do to keep this ABI compatible -Func withCheckedContinuation(function:_:) has parameter 0 type change from Swift.String to (any _Concurrency.Actor)? -Func withCheckedContinuation(function:_:) has parameter 1 type change from (_Concurrency.CheckedContinuation<τ_0_0, Swift.Never>) -> () to Swift.String -Func withCheckedThrowingContinuation(function:_:) has been renamed to Func withCheckedThrowingContinuation(isolation:function:_:) -Func withCheckedThrowingContinuation(function:_:) has mangled name changing from '_Concurrency.withCheckedThrowingContinuation(function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async throws -> A' to '_Concurrency.withCheckedThrowingContinuation(isolation: isolated Swift.Optional, function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async throws -> A' -Func withCheckedThrowingContinuation(function:_:) has parameter 0 type change from Swift.String to (any _Concurrency.Actor)? -Func withCheckedThrowingContinuation(function:_:) has parameter 1 type change from (_Concurrency.CheckedContinuation<τ_0_0, any Swift.Error>) -> () to Swift.String - // #isolation adoption for cancellation handlers; old APIs are kept ABI compatible -Func withTaskCancellationHandler(operation:onCancel:) has been renamed to Func withTaskCancellationHandler(operation:onCancel:isolation:) -Func withTaskCancellationHandler(operation:onCancel:) has mangled name changing from '_Concurrency.withTaskCancellationHandler(operation: () async throws -> A, onCancel: @Sendable () -> ()) async throws -> A' to '_Concurrency.withTaskCancellationHandler(operation: () async throws -> A, onCancel: @Sendable () -> (), isolation: isolated Swift.Optional) async throws -> A' - -// #isolated was adopted and the old methods kept: $ss31withCheckedThrowingContinuation8function_xSS_yScCyxs5Error_pGXEtYaKlF -Func withCheckedContinuation(function:_:) has been renamed to Func withCheckedContinuation(isolation:function:_:) -Func withCheckedContinuation(function:_:) has mangled name changing from '_Concurrency.withCheckedContinuation(function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async -> A' to '_Concurrency.withCheckedContinuation(isolation: isolated Swift.Optional, function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async -> A' +// but ABI checker does not understand the silgen_names on the ABi-compat APIs +Func withTaskCancellationHandler(operation:onCancel:) has mangled name changing from '_Concurrency.withTaskCancellationHandler(operation: () async throws -> A, onCancel: @Sendable () -> ()) async throws -> A' to '_Concurrency._isolatedParam_withTaskCancellationHandler(operation: () async throws -> A, onCancel: @Sendable () -> (), isolation: isolated Swift.Optional) async throws -> A' +Func withTaskCancellationHandler(operation:onCancel:) has been renamed to Func _isolatedParam_withTaskCancellationHandler(operation:onCancel:isolation:) // AsyncStream.init(unfolding:onCancel:) uses @_silgen_name to preserve mangling after adding @preconcurrency. Constructor AsyncStream.init(unfolding:onCancel:) has mangled name changing from 'Swift.AsyncStream.init(unfolding: () async -> Swift.Optional, onCancel: Swift.Optional<@Sendable () -> ()>) -> Swift.AsyncStream' to 'Swift.AsyncStream.init(unfolding: () async -> Swift.Optional, onCancel: Swift.Optional<() -> ()>) -> Swift.AsyncStream' @@ -114,22 +102,56 @@ Func SerialExecutor.enqueue(_:) has been added as a protocol requirement Func Executor.enqueue(_:) is a new API without '@available' // Adopt #isolation in TaskLocal.withValue APIs -Func TaskLocal.withValue(_:operation:file:line:) has been renamed to Func withValue(_:operation:isolation:file:line:) +// Update: Moved back to public API being nonisolated(nonsending) based, so no explicit 'isolated:' parameter anymore +// Func TaskLocal.withValue(_:operation:file:line:) has been renamed to Func withValue(_:operation:isolation:file:line:) Func TaskLocal.withValue(_:operation:file:line:) has mangled name changing from '_Concurrency.TaskLocal.withValue(_: A, operation: () async throws -> A1, file: Swift.String, line: Swift.UInt) async throws -> A1' to '_Concurrency.TaskLocal.withValue(_: A, operation: () throws -> A1, file: Swift.String, line: Swift.UInt) throws -> A1' -Func TaskLocal.withValue(_:operation:file:line:) has mangled name changing from '_Concurrency.TaskLocal.withValue(_: A, operation: () throws -> A1, file: Swift.String, line: Swift.UInt) throws -> A1' to '_Concurrency.TaskLocal.withValue(_: A, operation: () async throws -> A1, isolation: isolated Swift.Optional, file: Swift.String, line: Swift.UInt) async throws -> A1' +// Func TaskLocal.withValue(_:operation:file:line:) has mangled name changing from '_Concurrency.TaskLocal.withValue(_: A, operation: () throws -> A1, file: Swift.String, line: Swift.UInt) throws -> A1' to '_Concurrency.TaskLocal.withValue(_: A, operation: () async throws -> A1, isolation: isolated Swift.Optional, file: Swift.String, line: Swift.UInt) async throws -> A1' Func TaskLocal.withValue(_:operation:file:line:) has parameter 1 type change from () async throws -> τ_1_0 to () throws -> τ_1_0 Func TaskLocal.withValue(_:operation:file:line:) has parameter 1 type change from () throws -> τ_1_0 to () async throws -> τ_1_0 Func TaskLocal.withValue(_:operation:file:line:) has parameter 2 type change from Swift.String to (any _Concurrency.Actor)? Func TaskLocal.withValue(_:operation:file:line:) has parameter 3 type change from Swift.UInt to Swift.String -Func withTaskGroup(of:returning:body:) has parameter 2 type change from (inout _Concurrency.TaskGroup<τ_0_0>) async -> τ_0_1 to (any _Concurrency.Actor)? -Func withThrowingTaskGroup(of:returning:body:) has been renamed to Func withThrowingTaskGroup(of:returning:isolation:body:) -Func withThrowingTaskGroup(of:returning:body:) has mangled name changing from '_Concurrency.withThrowingTaskGroup(of: A.Type, returning: B.Type, body: (inout Swift.ThrowingTaskGroup) async throws -> B) async throws -> B' to '_Concurrency.withThrowingTaskGroup(of: A.Type, returning: B.Type, isolation: isolated Swift.Optional, body: (inout Swift.ThrowingTaskGroup) async throws -> B) async throws -> B' -Func withThrowingTaskGroup(of:returning:body:) has parameter 2 type change from (inout _Concurrency.ThrowingTaskGroup<τ_0_0, any Swift.Error>) async throws -> τ_0_1 to (any _Concurrency.Actor)? -Func withTaskGroup(of:returning:body:) has been renamed to Func withTaskGroup(of:returning:isolation:body:) -Func withTaskGroup(of:returning:body:) has mangled name changing from '_Concurrency.withTaskGroup(of: A.Type, returning: B.Type, body: (inout Swift.TaskGroup) async -> B) async -> B' to '_Concurrency.withTaskGroup(of: A.Type, returning: B.Type, isolation: isolated Swift.Optional, body: (inout Swift.TaskGroup) async -> B) async -> B' +// Update, moved to nonisolated(nonsending) +Func TaskLocal.withValue(_:operation:file:line:) has been renamed to Func withValueImpl(_:operation:isolation:file:line:) +Func TaskLocal.withValue(_:operation:file:line:) has mangled name changing from '_Concurrency.TaskLocal.withValue(_: A, operation: () throws -> A1, file: Swift.String, line: Swift.UInt) throws -> A1' to '_Concurrency.TaskLocal.withValueImpl(_: __owned A, operation: () async throws -> A1, isolation: isolated Swift.Optional, file: Swift.String, line: Swift.UInt) async throws -> A1' +Func TaskLocal.withValue(_:operation:file:line:) has parameter 0 changing from Default to Owned +Func TaskLocal.withValue(_:operation:file:line:) has removed default argument from parameter 2 Func pthread_main_np() is a new API without '@available' +// TaskGroups adopting nonisolated(nonsending), ABI checker doesn't fully understand ABI compat pattern we're doing to keep old ABI + +// API digester is not able to track ABI compat trickery we do when adopting nonisolated(nonsending), +// on occasion we do break source compatibility by removing the `isolation:` parameter as well: + +Func TaskGroup.Iterator.next() has been renamed to Func next(isolation:) +Func TaskGroup.Iterator.next() has mangled name changing from 'Swift.TaskGroup.Iterator.next() async -> Swift.Optional' to 'Swift.TaskGroup.Iterator.next(isolation: isolated Swift.Optional) async -> Swift.Optional' + +Func TaskGroup.next() has been renamed to Func _noParam_next() +Func TaskGroup.next() has mangled name changing from 'Swift.TaskGroup.next() async -> Swift.Optional' to 'Swift.TaskGroup._noParam_next() async -> Swift.Optional' + +Func ThrowingTaskGroup.Iterator.next() has been renamed to Func next(isolation:) +Func ThrowingTaskGroup.Iterator.next() has mangled name changing from 'Swift.ThrowingTaskGroup.Iterator.next() async throws -> Swift.Optional' to 'Swift.ThrowingTaskGroup.Iterator.next(isolation: isolated Swift.Optional) async throws(B) -> Swift.Optional' +Func ThrowingTaskGroup.next() has been renamed to Func next(isolation:) +Func ThrowingTaskGroup.next() has mangled name changing from 'Swift.ThrowingTaskGroup.next() async throws -> Swift.Optional' to 'Swift.ThrowingTaskGroup.next(isolation: isolated Swift.Optional) async throws -> Swift.Optional' + +Func withCheckedContinuation(function:_:) has been renamed to Func _isolatedParam_withCheckedContinuation(isolation:function:_:) +Func withCheckedContinuation(function:_:) has mangled name changing from '_Concurrency.withCheckedContinuation(function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async -> A' to '_Concurrency._isolatedParam_withCheckedContinuation(isolation: isolated Swift.Optional, function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async -> A' +Func withCheckedContinuation(function:_:) has parameter 0 type change from Swift.String to (any _Concurrency.Actor)? +Func withCheckedContinuation(function:_:) has parameter 1 type change from (_Concurrency.CheckedContinuation<τ_0_0, Swift.Never>) -> () to Swift.String + +Func withCheckedThrowingContinuation(function:_:) has been renamed to Func _isolatedParam_withCheckedThrowingContinuation(isolation:function:_:) +Func withCheckedThrowingContinuation(function:_:) has mangled name changing from '_Concurrency.withCheckedThrowingContinuation(function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async throws -> A' to '_Concurrency._isolatedParam_withCheckedThrowingContinuation(isolation: isolated Swift.Optional, function: Swift.String, _: (Swift.CheckedContinuation) -> ()) async throws -> A' +Func withCheckedThrowingContinuation(function:_:) has parameter 0 type change from Swift.String to (any _Concurrency.Actor)? +Func withCheckedThrowingContinuation(function:_:) has parameter 1 type change from (_Concurrency.CheckedContinuation<τ_0_0, any Swift.Error>) -> () to Swift.String + +Func withTaskGroup(of:returning:body:) has parameter 2 type change from (inout _Concurrency.TaskGroup<τ_0_0>) async -> τ_0_1 to (any _Concurrency.Actor)? +Func withTaskGroup(of:returning:body:) has been renamed to Func _isolatedParameter_withTaskGroup(of:returning:isolation:body:) +Func withTaskGroup(of:returning:body:) has mangled name changing from '_Concurrency.withTaskGroup(of: A.Type, returning: B.Type, body: (inout Swift.TaskGroup) async -> B) async -> B' to '_Concurrency._isolatedParameter_withTaskGroup(of: A.Type, returning: B.Type, isolation: isolated Swift.Optional, body: (inout Swift.TaskGroup) async -> B) async -> B' + +Func withThrowingTaskGroup(of:returning:body:) has been renamed to Func _isolatedParameter_withThrowingTaskGroup(of:returning:isolation:body:) +Func withThrowingTaskGroup(of:returning:body:) has mangled name changing from '_Concurrency.withThrowingTaskGroup(of: A.Type, returning: B.Type, body: (inout Swift.ThrowingTaskGroup) async throws -> B) async throws -> B' to '_Concurrency._isolatedParameter_withThrowingTaskGroup(of: A.Type, returning: B.Type, isolation: isolated Swift.Optional, body: (inout Swift.ThrowingTaskGroup) async throws -> B) async throws -> B' +Func withThrowingTaskGroup(of:returning:body:) has parameter 2 type change from (inout _Concurrency.ThrowingTaskGroup<τ_0_0, any Swift.Error>) async throws -> τ_0_1 to (any _Concurrency.Actor)? + // *** DO NOT DISABLE OR XFAIL THIS TEST. *** (See comment above.)