A few @isolated(any) function conversion questions

inspired by the discussion surfaced here, i wanted to highlight some of the specific language regarding @isolated(any) function conversions from the relevant evolution document. focusing on the third bullet point from the 'Conversions' section, it states:

Let F and G be function types, and let F' and G' be the corresponding function types with any isolation specifier removed (including but not limited to @isolated(any). If either F or G specifies @isolated(any) then a value of type F can be converted to type G if a value of type F' could be converted to type G' and the following conditions apply:

  • <snip>
  • <snip>
  • If only F specifies @isolated(any), then G must be an async function type. G may have any isolation specifier, but it will be ignored and the function will run with the isolation of the original value. The arguments and result must be sendable across an isolation boundary. It is unspecified whether the task will dynamically suspend when calling or returning from the resulting value.

the requirement that G be an async function type makes sense โ€“ in order to preserve the original function's isolation, async entry when it is called must be possible. however, i do not follow the subsequent statements regarding conversions to an arbitrary isolation being allowable. my reading of the text implies something like this would be permissible:

let f: @isolated(any) @Sendable () async -> Void = {}
let g: @MainActor @Sendable () async -> Void = f

however, this conversion currently does not work and produces an error. the statement

G may have any isolation specifier, but it will be ignored and the function will run with the isolation of the original value.

seems confusing โ€“ why would ignoring the isolation be okay? is the idea that an intermediary function value could automatically 'box' the original function to do whatever 'isolation hopping' is needed? overall i'm a bit confused about what that paragraph is trying to convey exactly. cc @hborla & @John_McCall โ€“ any insights you may have would be appreciated!

4 Likes

Yes, exactly. The way I think about it is that the isolation of G isn't ignored, but that G is an async function that can just switch to whatever isolation is needed to call a function of type F. In your example

let f: @isolated(any) @Sendable () async -> Void = {}
let g: @MainActor @Sendable () async -> Void = f

This conversion is fine because we can convert f to @MainActor @Sendable () async -> Void like this:

let g: @MainActor @Sendable () async -> Void =  {
  await f()
}

It's a compiler bug that the conversion produces an error. The conversion is fine, but the compiler does need to make sure the argument and result types conform to Sendable. For what it's worth, I attempted to elaborate on the async function conversion rules in SE-0461 here: swift-evolution/proposals/0461-async-function-isolation.md at main ยท swiftlang/swift-evolution ยท GitHub. They can be a little difficult to reason through :slight_smile:

1 Like