Skip to content

Commit 8ea9c53

Browse files
committed
Add Sendable annotations to SKSupport
1 parent 0d90601 commit 8ea9c53

File tree

6 files changed

+54
-31
lines changed

6 files changed

+54
-31
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ let package = Package(
197197
"LanguageServerProtocol",
198198
"LSPLogging",
199199
],
200-
exclude: ["CMakeLists.txt"]
200+
exclude: ["CMakeLists.txt"],
201+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
201202
),
202203

203204
.testTarget(

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import Dispatch
1414

1515
/// An abstract connection, allow messages to be sent to a (potentially remote) `MessageHandler`.
16-
public protocol Connection: AnyObject {
16+
public protocol Connection: AnyObject, Sendable {
1717

1818
/// Send a notification without a reply.
1919
func send(_ notification: some NotificationType)
@@ -61,7 +61,9 @@ public protocol MessageHandler: AnyObject {
6161
/// conn.send(...) // handled by server
6262
/// conn.close()
6363
/// ```
64-
public final class LocalConnection {
64+
///
65+
/// - Note: Unchecked sendable conformance because shared state is guarded by `queue`.
66+
public final class LocalConnection: @unchecked Sendable {
6567

6668
enum State {
6769
case ready, started, closed

Sources/SKSupport/AsyncQueue.swift

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fileprivate extension NSLock {
3434
}
3535

3636
/// A type that is able to track dependencies between tasks.
37-
public protocol DependencyTracker {
37+
public protocol DependencyTracker: Sendable {
3838
/// Whether the task described by `self` needs to finish executing before
3939
/// `other` can start executing.
4040
func isDependency(of other: Self) -> Bool
@@ -48,29 +48,47 @@ public struct Serial: DependencyTracker {
4848
}
4949
}
5050

51-
/// A queue that allows the execution of asynchronous blocks of code.
52-
public final class AsyncQueue<TaskMetadata: DependencyTracker> {
53-
private struct PendingTask {
54-
/// The task that is pending.
55-
let task: any AnyTask
51+
private struct PendingTask<TaskMetadata: Sendable>: Sendable {
52+
/// The task that is pending.
53+
let task: any AnyTask
5654

57-
let metadata: TaskMetadata
55+
let metadata: TaskMetadata
5856

59-
/// A unique value used to identify the task. This allows tasks to get
60-
/// removed from `pendingTasks` again after they finished executing.
61-
let id: UUID
62-
}
57+
/// A unique value used to identify the task. This allows tasks to get
58+
/// removed from `pendingTasks` again after they finished executing.
59+
let id: UUID
60+
}
6361

62+
/// A list of pending tasks that can be sent across actor boundaries and is guarded by a lock.
63+
///
64+
/// - Note: Unchecked sendable because the tasks are being protected by a lock.
65+
private class PendingTasks<TaskMetadata: Sendable>: @unchecked Sendable {
6466
/// Lock guarding `pendingTasks`.
65-
private let pendingTasksLock = NSLock()
67+
private let lock = NSLock()
6668

6769
/// Pending tasks that have not finished execution yet.
68-
private var pendingTasks = [PendingTask]()
70+
///
71+
/// - Important: This must only be accessed while `lock` has been acquired.
72+
private var tasks: [PendingTask<TaskMetadata>] = []
6973

70-
public init() {
71-
self.pendingTasksLock.name = "AsyncQueue"
74+
init() {
75+
self.lock.name = "AsyncQueue"
7276
}
7377

78+
/// Capture a lock and execute the closure, which may modify the pending tasks.
79+
func withLock<T>(_ body: (_ pendingTasks: inout [PendingTask<TaskMetadata>]) throws -> T) rethrows -> T {
80+
try lock.withLock {
81+
try body(&tasks)
82+
}
83+
}
84+
}
85+
86+
/// A queue that allows the execution of asynchronous blocks of code.
87+
public final class AsyncQueue<TaskMetadata: DependencyTracker> {
88+
private var pendingTasks: PendingTasks<TaskMetadata> = PendingTasks()
89+
90+
public init() {}
91+
7492
/// Schedule a new closure to be executed on the queue.
7593
///
7694
/// If this is a serial queue, all previously added tasks are guaranteed to
@@ -108,13 +126,13 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker> {
108126
) -> Task<Success, any Error> {
109127
let id = UUID()
110128

111-
return pendingTasksLock.withLock {
129+
return pendingTasks.withLock { tasks in
112130
// Build the list of tasks that need to finished execution before this one
113131
// can be executed
114-
let dependencies: [PendingTask] = pendingTasks.filter { $0.metadata.isDependency(of: metadata) }
132+
let dependencies: [PendingTask] = tasks.filter { $0.metadata.isDependency(of: metadata) }
115133

116134
// Schedule the task.
117-
let task = Task {
135+
let task = Task { [pendingTasks] in
118136
// IMPORTANT: The only throwing call in here must be the call to
119137
// operation. Otherwise the assumption that the task will never throw
120138
// if `operation` does not throw, which we are making in `async` does
@@ -125,14 +143,14 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker> {
125143

126144
let result = try await operation()
127145

128-
pendingTasksLock.withLock {
129-
pendingTasks.removeAll(where: { $0.id == id })
146+
pendingTasks.withLock { tasks in
147+
tasks.removeAll(where: { $0.id == id })
130148
}
131149

132150
return result
133151
}
134152

135-
pendingTasks.append(PendingTask(task: task, metadata: metadata, id: id))
153+
tasks.append(PendingTask(task: task, metadata: metadata, id: id))
136154

137155
return task
138156
}

Sources/SKSupport/AsyncUtils.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Foundation
1515
/// Wrapper around a task that allows multiple clients to depend on the task's value.
1616
///
1717
/// If all of the dependents are cancelled, the underlying task is cancelled as well.
18-
public actor RefCountedCancellableTask<Success> {
18+
public actor RefCountedCancellableTask<Success: Sendable> {
1919
public let task: Task<Success, Error>
2020

2121
/// The number of clients that depend on the task's result and that are not cancelled.
@@ -85,7 +85,7 @@ public extension Task {
8585
/// cancelled, `cancel` is invoked with the handle that `operation` provided.
8686
public func withCancellableCheckedThrowingContinuation<Handle, Result>(
8787
_ operation: (_ continuation: CheckedContinuation<Result, any Error>) -> Handle,
88-
cancel: (Handle) -> Void
88+
cancel: @Sendable (Handle) -> Void
8989
) async throws -> Result {
9090
let handleWrapper = ThreadSafeBox<Handle?>(initialValue: nil)
9191

@@ -117,11 +117,11 @@ public func withCancellableCheckedThrowingContinuation<Handle, Result>(
117117
)
118118
}
119119

120-
extension Collection {
120+
extension Collection where Element: Sendable {
121121
/// Transforms all elements in the collection concurrently and returns the transformed collection.
122-
public func concurrentMap<TransformedElement>(
122+
public func concurrentMap<TransformedElement: Sendable>(
123123
maxConcurrentTasks: Int = ProcessInfo.processInfo.processorCount,
124-
_ transform: @escaping (Element) async -> TransformedElement
124+
_ transform: @escaping @Sendable (Element) async -> TransformedElement
125125
) async -> [TransformedElement] {
126126
let indexedResults = await withTaskGroup(of: (index: Int, element: TransformedElement).self) { taskGroup in
127127
var indexedResults: [(index: Int, element: TransformedElement)] = []

Sources/SKSupport/ThreadSafeBox.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ extension NSLock {
2222
}
2323

2424
/// A thread safe container that contains a value of type `T`.
25-
public class ThreadSafeBox<T> {
25+
///
26+
/// - Note: Unchecked sendable conformance because value is guarded by a lock.
27+
public class ThreadSafeBox<T>: @unchecked Sendable {
2628
/// Lock guarding `_value`.
2729
private let lock = NSLock()
2830

Sources/SKSupport/dlopen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public final class DLHandle {
5757
}
5858
}
5959

60-
public struct DLOpenFlags: RawRepresentable, OptionSet {
60+
public struct DLOpenFlags: RawRepresentable, OptionSet, Sendable {
6161

6262
#if !os(Windows)
6363
public static let lazy: DLOpenFlags = DLOpenFlags(rawValue: RTLD_LAZY)

0 commit comments

Comments
 (0)