Trying to understand Concurrency Checking Modes

Hi everyone.

I am planning out the incremental migration process of a project to the new concurrency system. I've read through SE-0337 and started experimenting with @preconcurrency and -warn-concurrency and here are my results:

Sample 1

// Xcode 13.4; Other swift flags: -Xfrontend -warn-concurrency

@preconcurrency @MainActor
class SomeViewModel {
    let value1 = 0
    var value2 = 0
    func getValue2() -> Int { value2 }
    func setValue2(_ newValue: Int) { value2 = newValue }
}

func doSomething(with viewModel: SomeViewModel) {
    _ = viewModel.value1
    _ = viewModel.value2 // ERROR: Property 'value2' isolated to global actor 'MainActor' can not be referenced from this synchronous context
    _ = viewModel.getValue2()
    viewModel.setValue2(3)
}

My understanding here is that doSomething should be using Strict Checking mode because it's in the top level scope and -warn-concurrency is being used. From the proposal:

The top level scope's concurrency checking mode is:

  • Strict when the module is being compiled in Swift 6 mode or later, when the -warn-concurrency flag is used with an earlier language mode, or when the file being parsed is a module interface.
  • Minimal otherwise.

Sample 2

// Xcode 13.4; Other swift flags:

@preconcurrency @MainActor
class SomeViewModel {
    let value1 = 0
    var value2 = 0
    func getValue2() -> Int { value2 }
    func setValue2(_ newValue: Int) { value2 = newValue }
}

func doSomething(with viewModel: SomeViewModel) {
    // no warnings or erros here
    _ = viewModel.value1
    _ = viewModel.value2
    _ = viewModel.getValue2()
    viewModel.setValue2(3)
}

func wrappingAsync() async {
    func doSomethingElse(with viewModel: SomeViewModel) {
        _ = viewModel.value1
        _ = viewModel.value2 // ERROR: Property 'value2' isolated to global actor 'MainActor' can not be referenced from this synchronous context
        _ = viewModel.getValue2() // ERROR: Call to main actor-isolated instance method 'getValue2()' in a synchronous nonisolated context
        viewModel.setValue2(3) // ERROR: Call to main actor-isolated instance method 'getValue2()' in a synchronous nonisolated context
    }
}

In this example, my understanding is that the top level scope would be using Minimal Checking mode (we're in Swift 5.6 without the -warn-concurrency flag). However wrappingAsync() should be using Strict Checking because it is an async function, and then doSomethingElse should also be using Strict Checking because its parent scope is in strict checking mode.

A child scope's concurrency checking mode is:

  • Strict if the parent's concurrency checking mode is Minimal and any of the following conditions is true of the child scope:
    • ...
    • It is a function, method, initializer, accessor, variable, or subscript which is marked async or @Sendable .
    • ...
  • Otherwise, the same as the parent scope's.

Questions

  1. Am I missing something obvious? E.g. some flag or perhaps my understanding of scopes is wrong?
  2. In Sample 1, why are getValue2 and setVaue2 allowed, even though we are in Strict Checking mode. Or conversely, why is direct access to value2 disallowed, even though it can be easily circumvented with getter/setter functions.
  3. Shouldn't @preconcurrency actually be ignored in Sample 1, given that it is in Strict Checking mode. Removing @preconcurrency resulted in the same set of errors as in Sample 2.
  4. In case both of those samples work as expected, if hypothetically Swift 6 was available right now and I used it in those samples, which set of errors should I expect - just the ones where value2 is being accessed directly, or also the ones on getValue2 and setVaue2?

Thanks!

Martin

3 Likes

Here's a shorter example exposing what appears to be a bug in the -warn-concurrency interaction with @preconcurrency annotations:

// Xcode 13.4; Other swift flags: -Xfrontend -warn-concurrency

@MainActor
class MainActorViewModel {
    var value = 0
    func getValue() -> Int { value }
}

@preconcurrency @MainActor
class UnsafeMainActorViewModel {
    var value = 0
    func getValue() -> Int { value }
}

func doSomething(unsafeViewModel: UnsafeMainActorViewModel, viewModel: MainActorViewModel) {
    _ = unsafeViewModel.value // Error: Property 'value' isolated to global actor 'MainActor' can not be referenced from this synchronous context
    _ = unsafeViewModel.getValue()

    _ = viewModel.value // Error: Property 'value' isolated to global actor 'MainActor' can not be referenced from this synchronous context
    _ = viewModel.getValue() // Error: Call to main actor-isolated instance method 'getValue()' in a synchronous nonisolated context
}

Given I'm using the -warn-concurrency flag, my expectation is that when I call getValue() on unsafeViewModel this would also cause a compiler error. Not doing that makes it difficult to plan any migration to structured concurrency, as the workflow I would expect is:

  1. Mark some UI-related type(s) with @preconcurrency @MainActor
  2. Add -warn-concurrency and start resolving errors/warning by adding @preconcurrency @MainActor to as many types up the call stack as possible (where it makes sense of course)
  3. Remove -warn-concurrency and merge
  4. Repeat 2 and 3 until there are no more errors/warnings
  5. Remove all @preconcurrency >> the project is now fully concurrency-compliant

I would be really grateful if someone could explain if this behaviour is expected in Swift 5.6 and if it's expected to change in future Swift versions.

1 Like

I agree that this looks like a bug.

I added a computed property to UnsafeViewModel that does exactly the same as the getValue method. Accessing the computed property is also a compiler error (correct), so the missing diagnostic for the method call seems even more wrong:

@preconcurrency @MainActor
class UnsafeMainActorViewModel {
    var value = 0
    var computedValue: Int { value } // ← added
    func getValue() -> Int { value }
}

func doSomething(unsafeViewModel: UnsafeMainActorViewModel, viewModel: MainActorViewModel) {
    _ = unsafeViewModel.value // Error: Property 'value' isolated to global actor 'MainActor' can not be referenced from this synchronous context
    _ = unsafeViewModel.getValue()
    _ = unsafeViewModel.computedValue // ← same error as unsafeViewModel.value
1 Like

I am not sure where to ask this but this seems like an adequate place to ask before starting a brand new discussion.

I do want to use the SWIFT_STRICT_CONCURRENCY compile flag to "complete", but I do get issues in my code almost everywhere RxSwift is used with implicit concurrency through "observe(on: )" or even just a RxCocoa Binder use. This could be a question for the RxSwift community but there must be a way around, right ?