Design Priorities for the Swift 6 Language Mode

In that case this issue might also be interesting!

Is there a list of all breaking changes currently being considered for Swift 6? If not, will it be published any time soon?

5 Likes

We were just talking about global actor inference in another thread, and it was suggested we bring the suggestion over here:

I would like to propose removing all global-actor inference in Swift 6.

Global actor inference has an extremely complex set of rules, including some extremely surprising ones (such as the "property wrappers" one that is the original subject of that thread). It creates "spooky action at a distance" where conforming to a new protocol, or using a new property wrapper, might completely change the global actor of a type (and all its members).

This kind of source-breaking change might be rough on consumers of frameworks like UIKit, but I believe that it will lead to better understanding of the actual actor isolation requirements of the code that people are writing, as well as fewer hidden surprises and gotchas.

3 Likes

Given our guidance that source-breaking changes need to be targeted, not sweeping, I think it's fair to say that removing all global actor inference would be exceedingly unlikely.

However, the thread you linked to discusses a specific inference rule and makes particular arguments about why it in particular merits reconsideration even as it's source breaking; perhaps someone who's familiar with those points might enlighten the community about it here, or otherwise those interested can pop over to the other thread to learn more.

1 Like

Sure, I'm happy to summarize here, since I made the original post. In brief:

This struct is not actor-isolated:

struct MyView: View {
  @State private var x = 0

  var body: some View {
    Text("\(x)")
  }
}

This struct is actor-isolated (to the main actor):

struct MyView: View {
  @StateObject private var model = SomeModel()

  var body: some View {
    Text("\(model.x)")
  }
}

Simply changing @State to @StateObject changed the actor isolation of the entire containing type. It's because struct StateObject is declared @MainActor, but struct State is not. Applying an actor-isolated property wrapper to a property implicitly propagates that isolation up to the containing type, which in turn propagates it to all sibling declarations of the property (including all other properties and functions within the type).

I made a more complete argument in the other thread, so I'll just summarize here. This upward propagation of actor isolation from a property to the containing type is not well justified by SE-0316 where it was introduced, and has been the source of a lot of confusion (to mention just a couple recent examples). It even inhibits the adoption of @MainActor annotations in some places, because the actor-isolation becomes "viral", spreading further than the author intended. Nobody has commented in favor of the current behavior; the confusion and surprise is universal in all comments.

Changing this behavior (no longer propagating the actor isolation to the containing type) would be source-breaking, but there may be ways to mitigate that breakage for particular frameworks (e.g. SwiftUI). But since it's source-breaking, it would need to happen in a new language mode, so Swift 6 would be the right time to do it.

27 Likes

Would it be appropriate to open an evolution proposal now for things that might change in Swift 6? Or should that wait, since we're still firmly in Swift 5.8 development?

1 Like

Concerning data-race safety by default, one thing currently missing in Swift's concurrency support is interop with other systems that handle their own multithreading environment, like Combine, or RxSwift.

For example, both Combine and RxSwift have a way to subscribe to events on the main thread. Getting a closure marked @MainActor is easy, but to be able call it without error or warning, the place calling it also has to be statically @MainActor. Retrofitting that in an environment managing threads dynamically it is not easy. I guess the easiest would probably to have Swift provide something like @unchecked @MainActor, or a MainActor.runInline { ... } that aborts if not already on the main actor. Currently you can probably hack something using @MainActor(unsafe) but it's not its intended use case, and the (unsafe) part of it is supposed to be ignored by Swift 6.

(By the way Task { @MainActor in ... } does not run the code directly but queues it to be run later so it won't work here, and most of the time you are not in an async context so you cannot await anything inline.)

Closures passed to DispatchQueue.main are considered @MainActor so I thought there was already a solution for this problem, but looking at Swift's source code I realised the handling of DispatchQueue.main was just a hack in the compiler. Combine is an Apple technology so if needed I'm sure you could always also add special support for it in the compiler, but something that could be used by third parties would be really useful. I'm sure most people using RxSwift or something similar would like to not to have to migrate out of it to fully migrate to Swift 6 (the library used needing modifications is fine of course).

3 Likes

Yes, it’s fine to make new evolution proposals now.

1 Like

Swift 6 sounds like a good opportunity to do something about the fact that Decimal loses precision unexpectedly when initiated using a literal since all literals with a period in them pass through a binary floating point stage which is not what one expects when specifying a number that is decimal in its nature.

7 Likes

I don't think that would need a whole new language mode. When we have a design and implementation for decimal literals, I would say we should adopt it even in older language modes, since it should have the same behavior as today for binary floats, and the new behavior would be a bug fix for existing code that uses literal syntax with decimal types.

13 Likes

I agree, I would just hate to see it not being implemented because it would be source-breaking due to a "space bar heating" aspect.

Please do create a proposal for this. I think you have a strong case for removing this inference based on its practical impacts, and it needs to go through the process.

Doug

9 Likes

Yeah, I’ve been working on a proposal. I hope to have a pitch up sometime next week, and I already have an initial implementation. Thanks for the feedback!

24 Likes

I hope to have a pitch up sometime next week

Pitch has been posted here.

9 Likes

This section of the original post clearly encourages developers to opt-in to "upcoming" features early on. SE-0362 lists 6 feature identifiers, but if I understood it correctly, all future proposals, including the recently accepted SE-0384 will have to follow this unified approach and provide a "feature identifier". But I can't find one mentioned anywhere in SE-0384, instead, it introduces a "frontend flag" named -enable-import-objc-forward-declarations.

While this might just be an oversight, my more general question is:

How are developers supposed to easily discover all available features that are opt-in during Swift 5.x and will be turned on in Swift 6 for a "piecemeal adoption"?

Practically speaking, I think it's important that there is a place with an overview of all of these feature flags with a quick summary what they are about and maybe a link to the full proposal. Is there already such a place or Swift command or something else I can point people to / use myself?

6 Likes

Currently you need Swift 5.8 (most recent Xcode beta) to use this feature. It will probably be better documented once 5.8 is out. For now you could look them up in swift/Features.def at main · apple/swift · GitHub

I don’t think the SE-0384 feature you mention is currently supported by the new upcoming features option.

If you play with this, SPM has support under .swiftSettings to add upcoming or experimental features in the package if you change the package version to 5.8.

2 Likes

This should clearly be done with a feature identifier, and we'll look into fixing this before it's released. Thanks for pointing it out!

As for listing out all the feature identifiers, yeah, that's something we've been looking into for the website.

5 Likes

Just to round-trip on this: the Language Workgroup has summarily revised SE-0384 to use an SE-0362 feature flag instead of an ad hoc flag.

5 Likes

I checked the file, but something seems off: I can only find these lines in there, but there's no mention of StrictConcurrency which was explicitly mentioned in SE-0362 to be available as an "upcoming feature". The same goes for ImplicitOpenExistentials. Am I missing something?

2 Likes

One more source-breaking change that I'd like to mention in case I forget: if you switch over a non-frozen enum and forget to include a catch-all clause, that is currently only a warning (and a trap at runtime if it ever encounters an added case) rather than a compiler error:

Example:

func test(_ x: FloatingPointRoundingRule) {
    switch x {
    // ^ warning: switch covers known cases, but 'FloatingPointRoundingRule' may have additional unknown values, possibly added in future versions
    // ^ note: handle unknown values using "@unknown default"
    case .up,
         .down,
         .toNearestOrEven,
         .toNearestOrAwayFromZero,
         .towardZero,
         .awayFromZero:
      print("foo")
    }
}

Godbolt

It seems the original goal of SE-0192 - Handling Future Enum Cases was for this to be an error, but the rollout was considered too aggressive for Swift 5.0, so it was softened to a warning + runtime trap:

Honestly, I hope enough time has passed that we can make this change in a Swift 5.x update, but if not, let's please finally do it in Swift 6.

CC @jrose

22 Likes