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.

4 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.

2 Likes