[Pitch] Stop inferring actor isolation based on property wrapper usage

Following favorable discussion here and here in the Evolution forums, I'm happy to present a proposal to stop inferring a type's actor isolation based on its use of property wrappers. Concretely, given this example from SE-0316 (Global Actors), CounterView would no longer infer actor isolation in Swift 6.

@propertyWrapper
struct UIUpdating<Wrapped> {
  @MainActor var wrappedValue: Wrapped
}

// Swift 5: CounterView infers @MainActor from use of @UIUpdating
// Swift 6: no global actor inferred for CounterView
struct CounterView { 
  @UIUpdating var intValue: Int = 0
}

The full proposal can be found here and you can find an initial pass at implementation here. I'd love to hear your feedback!

28 Likes

To clarify, wouldn't the property wrapper UIUpdating also itself no longer infer actor isolation given your pitch?


Regarding the possibility of warning in Swift 5 mode:

  • Are the spurious warnings an implementation limitation, or is there something fundamental to the design that prevents emitting warnings only when the behavior will actually differ in Swift 6?
  • If desired, it'd be possible to disable global actor inference by adding a second property with a different, contradictory actor isolation, yes? If so, then at worst, one could add a @Foo var _void: Void = () to achieve the same effect?

That's true; the type UIUpdating itself would no longer be isolated. (UIUpdating itself was never actor-isolated. See correction below.) However, CounterView.intValue would still be @MainActor-isolated, because property declarations still infer their own isolation based on the property wrappers applied to that declaration. That inference just wouldn't bubble up to CounterView itself.

I believe it's rare that someone directly interacts with an instance of a property wrapper. But if you were doing something like this:

let x = UIUpdating(wrappedValue: 3)
print(x.wrappedValue)

the call to init would no longer still not be isolated, though the access of x.wrappedValue would.


I started going down the road of only warning when the behavior would actually change. However, I quickly realized that this would produce a warning on every SwiftUI view that uses @ObservedObject or @StateObject, because those views are currently isolated, and under this proposal will no longer be isolated. That could be mitigated if it were coordinated with an update to SwiftUI that makes SwiftUI.View itself MainActor-isolated, but that may come with other trade-offs that I cannot evaluate. Both of those property wrappers are used very commonly; this would be a very wide-spread introduction of warnings for pretty much all SwiftUI users. That's a lot of warnings, and would need a pretty high bar of justification in my opinion.

Since the argument of this pitch is that this inferred isolation is often unintentional, it may not be problematic for the isolation of the type to change. It's not clear to me that a warning is warranted in that situation. But I'm open to discussion!

Another possible route I considered would be to add details to errors produced in the Swift 6 mode, such that when an actor-isolation error occurs, we could check whether it would have been accepted under the Swift 5 rules, and if so suggest adding isolation to the containing type in that case. But even then, that feels overly broad; I think it would make more sense to suggest just adding isolation more locally to the error (i.e. annotating the containing function with @MainActor, rather than the whole type). And the compiler already produces that suggestion.

Yes, that would work. However, adding that via a fix-it suggestion from the compiler would be difficult, because we'd need to add a new declaration of an unused private global actor for every case where this happened.

That seems unlikely to be the right solution for an automated suggestion, so I probably wouldn't want to implement that as a compiler-suggested fix-it. But users could certainly do that on their own.

Would this mean that instance methods of UIUpdating are no longer allowed to be marked nonisolated?

Instance methods of UIUpdating would be non-isolated by default, but you could still explicitly mark them as such if desired.

Actually, I was wrong. UIUpdating is already not actor-isolated, even in Swift 5. @MainActor var wrappedValue does not have a property wrapper on the property declaration, so the upward propagation from property wrappers does not apply. Simply applying a global actor to a property has never granted isolation to the entire type.

This inconsistency between @MainActor var x and @UIUpdating var x is another issue resolved by this proposal.

4 Likes

Oh I recently bumped into exactly this documentation and was totally confused on why it was the case. It definitely seems wrong to me to infer entire type's isolation from some PW.

+1

Any chance this could also be fixed in some upcoming Swift 5.X version?

I'd love to say yes, but because it has the potential to be source breaking, I don't think it could be enabled by default for Swift 5.x. I think it should be possible to add it as an opt-in flag, though, in the same way many of the other Swift 6 concurrency features have opt-in flags available in Swift 5.

I'll look into that.

2 Likes

Thanks for taking the time to create a proposal for this.

I am definitely +1 for this.

I also would really appreciate a way to get this behavior also in Swift 5 (opt in flag seems like a good solution to me)

I've updated my implementation to also accept a compiler flag. You would be able to do this:

swift
  -enable-upcoming-feature DisableActorInferenceFromPropertyWrapperUsage
  my_file.swift

It's a mouthful (or… a keyboardful?), but I'm struggling to come up with a more concise name. Suggestions welcome!

4 Likes

It's good enough! ;)

How would that apply to usage in an Xcode app project?

I believe you'd add -enable-upcoming-feature DisableActorInferenceFromPropertyWrapperUsage to the Other Swift Flags field, though I don't currently have a full toolchain where I could try that myself.