Feedback wanted: confusing concurrency diagnostic messages

This is actually already on my radar. We did a pass over of the main diagnostics some time ago, but I did not do "sending" since we were still figuring out the final name. I am going to be doing another pass over specifically the sending diagnostics soon. But thank you for the report! I appreciate it = ).

3 Likes

Very minor, but I've now found a case where the compiler does the opposite of what was decided on in another issue related to "nonisolated" vs "non-isolated".

I also discovered differences in how the suggestion to add a global actor annotation is made.

1 Like

I got a bit confused the other day in this code snippet:

let standardError = Pipe()
var error = ""
var errorStream = Data()

let standardErrorQueue: OperationQueue = createStandardErrorQueue()
standardErrorQueue.addOperation(BlockOperation(block: {
	while handleInput(pipe: standardError, stream: &errorStream, result: &error) {}
}))

It was in the middle of a static func, and gave me the errors Mutation of captured var 'error' in concurrently-executed code and Mutation of captured var 'errorStream' in concurrently-executed code.

I think I understand how this might create a race condition, though I don't think it does (the rest of the code is very straightforward). I haven't yet studied actors and sendable etc to know what would be an ideal way to rewrite this, and for now I just wanted the code to compile so I could fix other (more important) stuff, so after some trial and error I added nonisolated(unsafe) to the variable declarations and the error disappeared.

I thought It'd be worth mentioning because the error didn't really direct me towards either an ideal solution or an "unsafe fix". I only thought about adding nonisolated(unsafe) after a while, and even then it was only because this had worked to fix other errors that suggested that annotation as a fix-it.


Edit: here's the link to the full code, in case it's useful: Gryphon/Sources/GryphonLib/Shell.swift at 89cc51358a8858865e05d98a9d734c209e6eb035 · vinivendra/Gryphon · GitHub

1 Like

I was helping a friend who was new to Swift with a project earlier today and ran into this.

@MainActor
struct Foo {
  func baz() { }
}

func bar() {
  let foo = Foo()
  foo.baz()
}

For this we get the following error description and fixit

Call to main actor-isolated instance method 'baz()' in a synchronous nonisolated context

INFO: Calls to instance method 'baz()' from outside of its actor context are implicitly asynchronous

FIXIT: Add '@MainActor' to make global function 'bar()' part of global actor 'MainActor'

The person I was mentoring had trouble understanding what was happening and what they did wrong. We started out by accepting the fixit in order to get compiling code, and then planned to come back and talk about why this change was needed or not needed. This resulted in a cascade of errors where adding @MainActor to one function required that they also accept fixits in several other places.

Some suggestions:

  • While it might not be possible to have specific error messages for arbitrary actors, I expect that new users of Swift will likely first run into this error in relation to @MainActor, so altering the error message to be less jargon-heavy when @MainActor is involved might allow for a more gentle onboarding.
  • A potential rewording of the error message could be baz is isolated to the main actor by @MainActor, but the calling function is not and could be executed by any actor. Function calls that are not explicitly isolated to the same actor as their parent must be preformed asynchronously.
  • Two fixits could be suggested here. Either isolate bar with @MainActor, or make bar async and prepend the call to baz with await.
  • A note may be justified here to alert the user of the hazards of isolating their function to the main actor since it's potentially a mistake if the function also contains CPU intensive work.
  • The wording of make global function 'bar()' part of global actor 'MainActor' feels wrong to me. It's difficult to understand what it means for a function to be "part of" an actor. Would it also be accurate to say @MainActor causes the function to be run on MainActor or executed by MainActor? Either of those feel more approachable to me.
  • Nit, and probably out of scope for this discussion, but specifying that a function is an instance method/global function feels overly wordy to me, and in my opinion is not particularly useful. Same thing with "global actor".

In the end after talking through my mentee what it meant to annotate a function with @MainActor, we annotated only the function that needed it (it had a call to a 3rd party lib that required it), and made the calling functions do so asynchronously.

5 Likes
protocol NodeProtocol: Actor { }
struct PrintNode: NodeProtocol { } // Error: Non-class type 'PrintNode' cannot conform to class protocol 'Actor'

This error message is incorrect since the protocol is not a class protocol and making PrintNode a class will not fix the issue. When we switch PrintNode to a class, we get the correct error message.

// Non-actor type 'PrintNode' cannot conform to the 'Actor' protocol

We also get a fixit to switch PrintNode to an actor, which we should probably also get when PrintNode is a struct. Changing the type from value to reference semantics is a little iffy though, so I can understand why you might not want to do that.

3 Likes

The following Swift-6 error (Xcode 16 beta 4) is hard to understand and the steps to resolve the error unclear:

error: cannot form key path to main actor-isolated property 'xyz'

See the following thread for an example:

key path error

Here's one that doesn't make very clear what the underlying problem is:

Here is one:
'AnyActor' is deprecated: Use 'any Actor' with 'DistributedActor.asLocalActor' instead

public protocol OrdoObjectListPlugin: AnyActor {
...
}

No fix-it, the intention is to constrain the protocol to actors - not sure how I am expected to constrain the protocol with the information provided?

3 Likes

If you're just trying to constrain to actors, you should refine Actor. Are you trying to constrain conformances to either actors or distributor actors?

1 Like

Thanks Holly,

Just constraining to Actor is what I wanted in this case - must admit I think that constraint was just assumed to be analogous to constraining to AnyObject and it "worked" for 5.10, so got confused by the error message... Thanks!

1 Like

Here's one. I think there's quite a bit of redundant information the note. But, a little rewording and some links to help find the relevent definitions could help a lot.

Here's one that recently left me confused until I realized what my mistake was:

let values = AsyncStream { 0 }

for value in values { // Error: For-in loop requires 'AsyncStream<Int>' to conform to 'Sequence'
  print(value)
}

The solution of course is to use for await. Maybe the compiler could hint at this when it sees you're trying to use a normal for-loop for an async sequence.

10 Likes

I ran across this error message today:

Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for ' accessed after being transferred; later accesses could race

class NonSendable {}

func test() async {
    await withTaskGroup(of: Bool.self) { group in
        for ns in [NonSendable()] {
            // 🛑: Value of non-Sendable type '@isolated(any)
            // @async @callee_guaranteed @substituted <τ_0_0> () 
            // -> @out τ_0_0 for <Bool>' accessed after being 
            // transferred; later accesses could race.
            group.addTask {
                print(ns)
                return false
            }
        }
        await group.waitForAll()
    }
}

I suspect most developers (including myself) don't know what a @isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <Bool> is.

The actual problem here seems to be that I'm capturing a non-sendable type, but the diagnostic message didn't lead me in that direction!

2 Likes

Oh yeah, I have an even worse variant of that:

Lock.swift:225:31: warning: returning a task-isolated '@noescape @callee_guaranteed @substituted <τ_0_0, τ_0_1, τ_0_2, τ_0_3> (UnsafeMutablePointer<τ_0_0>, UnsafeMutablePointer<τ_0_1>) -> (@out τ_0_3, @error_indirect τ_0_2) for <os_unfair_lock_s, State, E, R>' value as a 'sending' result risks causing data races; this is an error in the Swift 6 language mode
return try lockBuffer.withUnsafeMutablePointers(unsafeWithLockPointers)
^
Lock.swift:225:31: note: returning a task-isolated '@noescape @callee_guaranteed @substituted <τ_0_0, τ_0_1, τ_0_2, τ_0_3> (UnsafeMutablePointer<τ_0_0>, UnsafeMutablePointer<τ_0_1>) -> (@out τ_0_3, @error_indirect τ_0_2) for <os_unfair_lock_s, State, E, R>' value risks causing races since the caller assumes the value can be safely sent to other isolation domains
return try lockBuffer.withUnsafeMutablePointers(unsafeWithLockPointers)
^
Lock.swift:225:31: note: '@noescape @callee_guaranteed @substituted <τ_0_0, τ_0_1, τ_0_2, τ_0_3> (UnsafeMutablePointer<τ_0_0>, UnsafeMutablePointer<τ_0_1>) -> (@out τ_0_3, @error_indirect τ_0_2) for <os_unfair_lock_s, State, E, R>' is a non-Sendable type
return try lockBuffer.withUnsafeMutablePointers(unsafeWithLockPointers)

Then there's this one that came up in another forum thread the other day:

5 Likes

Not a compile-time diagnostic but I think falls squarely in this realm: App crashes from compiler added hidden assert

This has likely been posted, but I wanted to bring up this case again, from the AoC thread.

This error is especially confusing because it points to the closure being passed to addTask and not to the actual cause of the issue. While the closure type may technically be the cause of the issue, it's really caused by the closure type inference failing due to the code inside the closure. Is there any way the compiler can distinguish inferred types, especially for closures, and look at what cause the inference to return a different type than required? It could even be special cased to just closure parameters, as the closure itself doesn't usually feel like part of the code the user provides but something that the IDE/compiler/API gave us to fill in. Especially in Swift where the syntax is simply {}. Even something as simply as the compiler silently compiling a version of the closure as if it had an explicit return type that matched the parameter type (in this case { () async -> Int in }), which usually get the compiler to return the true errors.

2 Likes

Thanks for posting, Jon. I did stick my head in this thread and I saw some similar messages earlier, so I wasn't sure if it was valuable to add it here or not.

The code in question. Just change line 29 to assign a mutable copy of grid instead to trigger the error.

I find the compiler error "Task or actor isolated value cannot be sent" (discussed e.g. in this thread: Task or actor isolated value cannot be sent) confusing for grammar reasons.

I'm not a native English speaker, but I think this diagnostic should be as follows (note the added hyphens), to make clear that the task itself is not the problem:

"Task- or actor-isolated value cannot be sent"

or alternatively:

"Task-isolated or actor-isolated value cannot be sent"

It would be even better if the name of the variable that is being sent could be named in the diagnostic, e.g.:

"Task- or actor-isolated value 'myVar' cannot be sent"

It's sometimes not immediately clear to me which variable this is talking about. For example, this post from the thread mentioned above shows a screenshot where the diagnostic is attached rather generically to a Task closure rather than a specific expression in the closure body (perhaps because the isolated value in question in this case is the implicit 'self'?). If the closure body were longer than just a single expression, it would not be immediately apparent to which expression the diagnostic is referring.

9 Likes

Unless there's a type of isolation not mentioned, I'm not sure the Task or actor part even helps. Ideally the diagnostic would include all the relevant info in the message itself.

  • "Main-actor isolated value myVar cannot be sent, as it is not otherwise Sendable."
  • "Default executor isolated value myVar cannot be sent, as there is a local mutable reference."
  • "Task-local value myVar cannot be sent..."

Unfortunately Swift seems to prefer messages which include little contextual information, instead relying on the code position information to inform users, which is itself often incorrect. Simple messages likely make creating and maintaining them easier, but we miss out on critical information.

5 Likes

That is not actually what is happening. What is most likely happening is that the closure is capturing some sort of main actor isolated state, causing the closure itself to also be main actor isolated. It does not have anything to do with the type system.

That being said, this has been already improved on main. Look at the following example:

class NS {}

@MainActor
class C {
  let ns = NS()

  func test() async {
    let x = ns
    await withTaskGroup { group in
      group.addTask {
        print(x)
      }
    }
  }
}

Now we emit the following error:

test.swift:12:21: error: passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure
10 |     let x = ns
11 |     await withTaskGroup { group in
12 |       group.addTask {
   |                     `- error: passing closure as a 'sending' parameter risks causing data races between main actor-isolated code and concurrent execution of the closure
13 |         print(x)
   |               `- note: closure captures 'x' which is accessible to main actor-isolated code
14 |       }
15 |     }

notice how it points out the error as the closure... but it also is able to point out that the reason for the error is that the closure captures 'x' which is part of the main actor since it was bound to the value of 'ns' earlier in the function.

3 Likes