Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MainActor isolation does not pass to nested functions defined within a closure #73301

Open
adysart opened this issue Apr 26, 2024 · 3 comments
Open
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. concurrency Feature: umbrella label for concurrency language features

Comments

@adysart
Copy link

adysart commented Apr 26, 2024

Description

I'm not 100% certain this is actually bug, it could be my lack of understanding how MainActor isolation is inferred. If you declare a nested function on a MainActor isolated type, but do so inside a closure, it seems that the nested function does not inherit MainActor isolation. I've attached a minimal reproducing case.

The closure passed to UIDeferredMenuElement.uncached() is not annotated with @MainActor, although UIDeferredMenuElement itself is.

@MainActor open class UIDeferredMenuElement : UIMenuElement {
    open class func uncached(_ elementProvider: @escaping (@escaping ([UIMenuElement]) -> Void) -> Void) -> Self
}

I honestly do not know if MainActor isolation carries over to this static method argument.

Note that accessing state on ViewController (which is MainActor isolated) from within this closure does not raise an error.

Explicitly annotating the bar() nested function with @MainActor resolves the compiler error.

Reproduction

final class ViewController: UIViewController {
  private let viewModel = ViewModel()

  private func makeMenu() -> UIDeferredMenuElement {
    UIDeferredMenuElement.uncached({ _ in
      func bar() {
        print(viewModel.value) // ❌ Main actor-isolated property 'value' can not be referenced from a non-isolated context
      }

      bar()

      print(self.viewModel.value)
    })
  }

}

@MainActor class ViewModel {
  var value = "hello"
}

Expected behavior

I'm not certain, but I believe this nested function should automatically inherit MainActor isolation from its containing type, and thus no compiler error should be raised.

Environment

Xcode 15.2 (15C500b)
Test project with deployment target of iOS 17.0
SWIFT_STRICT_CONCURRENCY=complete

Additional information

No response

@adysart adysart added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels labels Apr 26, 2024
@hborla hborla added concurrency Feature: umbrella label for concurrency language features and removed triage needed This issue needs more specific labels labels Apr 26, 2024
@hborla
Copy link
Member

hborla commented Apr 26, 2024

I honestly do not know if MainActor isolation carries over to this static method argument.

It doesn't! The reason why the closure is treated as @MainActor-isolated is because the closure type is not Sendable. If the closure is not Sendable, the assumption is that it cannot escape the isolation domain it's formed in (at least not without violating actor isolation rules, which is diagnosed if you're building with -strict-concurrency=complete or -swift-version 6). So, it's correct that the closure is treated as @MainActor-isolated even though that's not explicit anywhere.

I agree with you that a local function that isn't otherwise annotated as nonisolated or @Sendable should have the same behavior, because unless it's marked with @Sendable, it also cannot escape the context it's formed in. It's not different than forming a closure!

@adysart
Copy link
Author

adysart commented Apr 27, 2024

Thank you @hborla! That does make sense how a non-Sendable closure would be inferred to be MainActor isolated.

Regarding the nested function:

It's not different than forming a closure!

What's interesting here is that if I construct a closure along side the nested function, it does not raise an error:

  private func makeMenu() -> UIDeferredMenuElement {
    UIDeferredMenuElement.uncached({ _ in
      func bar() {
        print(self.viewModel.value) // ❌ Main actor-isolated property 'value' can not be referenced from a non-isolated context
      }

      let baz: () -> Void = {
        print(self.viewModel.value) // No error here
      }

      bar()
      baz()

      print(self.viewModel.value)
    })
  }

So maybe this is actually a bug with how nested functions inherit actor isolation?

@hborla
Copy link
Member

hborla commented Apr 27, 2024

Right, I'm saying that it's conceptually the same as forming a closure, and I think they should behave the same. I agree that there's a bug with the isolation of the local function in your example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. concurrency Feature: umbrella label for concurrency language features
Projects
None yet
Development

No branches or pull requests

2 participants