Swift 6: Understanding Behavior of Local Static Variables in Concurrency

I'd like to understand why this program behaves as it does and why there are no concurrency-related warnings or errors. This is using the Swift 6.0 that comes with Xcode 16.0 Beta 4 (16A521). Any insights would be appreciated.

% swift --version
swift-driver version: 1.112.3 Apple Swift version 6.0 (swiftlang-6.0.0.6.8 clang-1600.0.23.1)
Target: arm64-apple-macosx14.0
% cat > test.swift
final class C {
  func foo(_ label: String) {
    enum S {
      static var count = 0
    }
    print(label, "| count:", S.count)
    S.count += 1
  }
}

func test() async throws {
  let c = C()
  Task {
    c.foo("A")
  }
  c.foo("B")
  c.foo("C")
  try await Task.sleep(for: .seconds(1))
}

for _ in 0..<4 {
  try! await test()
  print("--")
}
% swiftc test.swift && ./test   
A | count: 0
B | count: 0
C | count: 2
--
B | count: 3
C | count: 4
A | count: 5
--
B | count: 6
C | count: 7
A | count: 6
--
B | count: 9
C | count: 10
A | count: 9
--

For reference, here's what happens when changing `class` to `actor`.
% cat > test.swift
final actor C {
  func foo(_ label: String) {
    enum S {
      static var count = 0
    }
    print(label, "| count:", S.count)
    S.count += 1
  }
}

func test() async throws {
  let c = C()
  Task {
    await c.foo("A")
  }
  await c.foo("B")
  await c.foo("C")
  try await Task.sleep(for: .seconds(1))
}

for _ in 0..<4 {
  try! await test()
  print("--")
}
 % swiftc test.swift && ./test
B | count: 0
A | count: 1
C | count: 2
--
B | count: 3
A | count: 4
C | count: 5
--
B | count: 6
A | count: 7
C | count: 8
--
B | count: 9
A | count: 10
C | count: 11
--
1 Like

You compile it in Swift 5 mode, not 6, and with any concurrency checks turned on whatsoever. If you run it with swiftc test.swift -swift-version 6 it won't compile, as it should.

2 Likes

Oh :person_facepalming:, I assumed the Swift 6.0 compiler would use -swift-version 6 implicitly, since it's Swift 6.0 ...

1 Like

-swift-version is a bit misguiding and there is an ongoing thread about its renaming. It is a mode, that guards source-breaking changes that come with 6th version, so that code can be gradually migrated, and even with latest compiler defaults to pre-6 behaviour when it comes to concurrency checks.

You can use SPM and run your test code as part of a package with swift-tools-version: 6.0 set in Package.swift, then it will compile everything in Swift 6 mode by default.

1 Like

I noticed that Xcode (16.0 Beta 4) sets the Swift Language Version build setting to Swift 5 for a newly created project.

And there's also e.g. these:


Has this always been the case, that you have to opt into breaking changes of Swift version X even when you are using Swift version X?

Will it be the same in Xcode 16.0 GM?

I assumed these flags were something we should use only while still using Swift 5, in order to prepare for the breaking changes in Swift 6.

I can’t recall such major breaking changes, so that’s pretty much the first time I think. It's just that adopting new concurrency might be not easy for projects with full checks on. Having defaults to throw potentially hundreds of errors might be off-putting on adoption.

These feature options guard some of concurrency checks, so that project can turn them on gradually, adopting it step-by-step. And Xcode behaviour I'd expect to remain mostly the same as it in betas on that.

Here is a nice wwdc session on the topic of migration: Migrate your app to Swift 6 - WWDC24 - Videos - Apple Developer

Thanks!