Currently, a closure declared as nonisolated(nonsending) while also being @Sendable completely omits the effect of nonisolated(nonsending). I’m uncertain whether this is intended behavior. If not, I think it would be wise to emit a compiler diagnostic indicating that nonisolated(nonsending) is ineffective in such cases.
P.S. The order in which, and when, one can apply nonisolated(nonsending) seems rather random.
To clarify – what do you mean by it "omits the effect"? In the SIL for your example it looked like the function & closure both had the implicit actor parameter to me. The ordering limitation is a bit odd, though personally I prefer reading @Sendable nonisolated... to nonsiolated @Sendable....
Perhaps I’m misunderstanding how the keyword works and the SIL, but from my current understanding, nonisolated(nonsending) on a @Sendable closure doesn’t do anything, we still hop to the GCE, whereas this is not the case for functions: Godbolt.
Maybe that behavior is a bug with the 6.2 version of the compiler currently on compiler explorer. If you update to main or the 6.3 branch, it seems you don't get executor hops.
It also works correctly on Swift 6.2.4. I could have sworn I tested it on the devsnapshot. Good to see that it’s fixed and that it was just a bug. It seems that the executor passed to the closure was simply ignored, or rather, it was missing as an argument to the closure. On a side note, we also switched from Optional<any Actor> to Builtin.ImplicitActor.
That leaves the fragile placement of nonisolated(nonsending). Do you think this is a bug or a current limitation? I’ve noticed that this is a general trend with recent keywords and attributes.
nonisolated(nonsending) @Sendable func f() async {} // error
@Sendable nonisolated(nonsending) func f() async {} // okay
let c: @Sendable nonisolated(nonsending) () async -> Void = {} // error
let c: nonisolated(nonsending) @Sendable () async -> Void = {} // okay
let c = { nonisolated(nonsending) @Sendable () async -> Void in } // error
let c = { @Sendable nonisolated(nonsending) () async -> Void in } // error
Personally I'm not certain, though I lean toward its being an intentional behavior. I looked into it a bit, and it seems like when parsing declarations nonisolated is considered a "modifier", and gets consumed after attributes are processed (which I believe @Sendable is in this context), but when parsing type annotations, nonisolated is considered a "specifier", and specifiers are parsed before attributes. That would seem to explain the function vs closure-with-explicit-type-signature difference, though I don't know if there's a principled reason for it or if it's just historically contingent and it can no longer be changed.
The closure signature one seems a little different, in that there's some explicit logic that disables parameterizing nonisolated in that position. The stated reason is:
// 'nonisolated' cannot be parameterized in a closure due to ambiguity with
// closure parameters.
though it's not immediately obvious to me how that ambiguity manifests or if there's some way the restriction could be lifted. @rintaro may know more details about this.
After experimenting a bit more, I found this curiosity. It seems that when you capture the isolated parameter (including the implicit self of an actor method) inside a nonisolated(nonsending)@Sendable or sending closure, that closure loses its dynamic isolation and falls back to being nonisolated (@concurrent), explicitly switching to the GCE. Ideally, the whole program should run as a single job.
@rayx Let me know if you’ve already discovered this.
This applies to nonisolated in general, it's a limitation that we currently have, nonisolated cannot be used an as attribute on the closure itself.
This is very interesting, it's @Sendable that has this effect on captures and makes closure nonisolated. I think I have in idea why, I believe it's due to isIsolationInferenceBoundaryClosure check. Will take a closure look tomorrow. Meanwhile could you please file a GitHub issue for this if you haven't already?