Task for non-copyable and/or non-sendable types

I'm trying to come up with a design for a Task-like primitive for non-sendable and/or non-copyable types, but it's pretty hard to make the compiler happy.
I've made an implementation that produces no warnings but it's suboptimal, to put it mildly.

Here is a simplified version
final class NonCopyableValueBox<T: ~Copyable> {
  private var value: T?

  init(value: consuming T) {
    self.value = consume value
  }

  func take() -> T? {
    let value = value.take()
    return value
  }
}

actor Future<T: Sendable> {
  enum State {
    case pending([CheckedContinuation<T, Never>])
    case fulfilled(T)
  }

  private var state = State.pending([])

  func get() async -> T {
    switch state {
    case .pending(var continuations):
      return await withCheckedContinuation { continuation in
        continuations.append(continuation)
        state = .pending(continuations)
      }
    case .fulfilled(let value):
      return value
    }
  }

  func fulfill(_ value: T) {
    switch state {
    case .fulfilled:
      assertionFailure()
    case .pending(let continuations):
      state = .fulfilled(value)
      for continuation in continuations {
        continuation.resume(returning: value)
      }
    }
  }
}

public struct NCTask<Success: ~Copyable>: ~Copyable {
  private let task: Task<Void, Never>
  private let future: Future<CheckedContinuation<NonCopyableValueBox<Success>, any Error>>

  public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async throws -> sending Success) {
    let future = Future<CheckedContinuation<NonCopyableValueBox<Success>, any Error>>()
    self.future = future
    task = Task(priority: priority, operation: { [operation] in
      do {
        let value = try await operation()
        let continuation = await future.get()
        continuation.resume(returning: NonCopyableValueBox(value: value))
      } catch {
        let continuation = await future.get()
        continuation.resume(throwing: error)
      }
    })
  }

  consuming func value() async throws -> sending Success {
    let box = try await withCheckedThrowingContinuation { [future] continuation in
      Task {
        await future.fulfill(continuation)
      }
    }
    guard let value = box.take() else {
      fatalError()
    }
    return value
  }
}

I feel like it should be possible to express the same simpler, something like this:

public struct NCTask<Success: ~Copyable>: ~Copyable {
  actor Value {
    var value: Success?

    init(value: consuming sending Success) {
      self.value = consume value
    }

    func take() -> sending Success? {
      value.take()
    }
  }

  private let task: Task<Value, any Error>

  public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async throws -> sending Success) {
    task = Task(priority: priority) {
      return Value(value: try await operation())
    }
  }

  public consuming func value() async throws -> sending Success {
    try await task.value.take()!
  }
}

This code however produces several warnings:

  1. return Value(value: try await operation()) - @isolated(any) value of type 'Success' passed as a strongly transferred parameter; later accesses could race
    I can silence it by splitting it into two lines and I can't understand why?
      let value = try await operation()
      return Value(value: value) // it's fine
    
  2. value.take() - Returning 'self'-isolated 'self.value' as a 'sending' result risks causing data races
  3. try await task.value.take()! - Returning a 'self'-isolated 'Optional<Success>' value as a 'sending' result risks causing data races

Any ideas on how to satisfy the compiler? Or maybe someone can suggest a better implementation?

1 Like

The Swift 6.0.x compiler has some issues with closures that use sending return. I immediately thought to try a 6.1 snapshot to see if was any better, but unfortunately this code currently causes it to crash.

Yep, I narrowed the scope of the issue down to the combination of @isolated(any) and sending and filed a bug: Swift 6.1 demangler issue with @isolated(any) closures returning 'sending' · Issue #78582 · swiftlang/swift · GitHub

@ktoso Have you considered extending Task to support something like that?

1 Like