Dynamically checking isolation

Requirements:

  1. Data structure that combines a key-path to a property isolated to a global actor with a closure isolated to the same global actor.
  2. Data structure should be sendable.
  3. Key path and closure can be used synchronously after dynamically checking that current isolation is the one they are isolated to.
  4. The same data structure should work with different actors (open set).
  5. It should be possible to create instance of the data structure without being isolated to the right actor.

I was able to solve the first 4 with the following approach:

// Non-sendable
struct Info {
    var keyPath: PartialKeyPath<Foo>
    var action: () -> Void
}

public struct IsolatedBox<T>: @unchecked Sendable {
    public let isolation: any Actor
    private var value: T

    public init(_ value: T, isolation: isolated any Actor = #isolation) {
        self.isolation = isolation
        self.value = value
    }

    public func open(isolation: isolated any Actor = #isolation) -> T? {
        guard self.isolation === isolation else { return nil }
        return value
    }
}

But to construct IsolatedBox I need to be in the correct isolation, which contradicts with the 5th requirement.

I was thinking that maybe @isolated(any) closures could help:

var infoMakers: [@isolated(any) @Sendable () -> IsolatedBox<Info>] = {
    { @MainActor in 
        Info(\.mainProperty) { ... }
    },
    { @AnotherActor in
       Info(\.anotherProperty) { ... }
    }
}

So far, so good - I can create closures from any isolation, and when they get called, I will get the right information associated to the actor. But how do I actually call the closure?

func doIt(for keyPath: PartialKeyPath<Foo>, isolation: isolated any Actor = #isolation) {
    for infoMaker in infoMakers {
        if infoMaker.isolation === isolation {
           let infoBox = infoMaker() // error: call to @isolated(any) let 'infoMaker' in a synchronous actor-isolated context
           let info = infoBox.open()! // isolation already checked above
           if info.keyPath == keyPath {
               info.action()
           }
        }
    }
}

I've tried using assumeIsolated - it did not work:

infoMaker.isolation!.assumeIsolated { _ in
    infoMaker() // error: call to @isolated(any) let 'infoMaker' in a synchronous actor-isolated context
}

Checking the isolation property of an @isolated(any) closure doesn't help the compiler know whether you're able to call it. I'm pretty sure calls to @isolated(any) closures must be awaited, and I believe (though I'm not certain on this part) the check against the current isolation is automatically done at runtime to avoid the suspension if no actor hop is actually needed.

Can you pass an instance of some actor instead of using the default argument?

public init(_ value: T, isolation: isolated any Actor = #isolation) { ... }

e.g.,

public struct Acting {
  public let actor: any Actor
  init(_ actor: any Actor) {
    self.actor = actor
  }
  
  func on(isolation: isolated any Actor = #isolation) -> Bool {
    self.actor === isolation
  }
  public static func demo() {
    let me = Self(MainActor.shared) // passing instance
    Task { @MainActor in
      if me.on() {
        print("on Main")
      } else {
        print("not on Main")
      }
      return true
    }
  }
}

Key path to global-actor isolated property can be formed only in code isolated to that actor.

I hoped there is an escape hatch for this. Something with semantics of “call synchronously or crash”.

Digging into implementation of the @isolated(any), I see that they seem to have the same ABI as a regular closures. So bit-casting to a regular closure should work. But obviously I'd prefer something less hacky.