Syntax for `nonsiolated(nonsending)` closure literal?

I'm trying to audit cases of nonisolated async functions in our codebase.

I'm getting a warning for closure literals:

await withTaskCancellationHandler { // warning: Feature 'NonisolatedNonsendingByDefault' will cause nonisolated async closure to run on the caller's actor; use '@concurrent' to preserve behavior
    ...
} onCancel: {
    ...
}

In cases where @concurrent is correct, I can insert { @concurrent in to make it explicit and suppress the warning.

But when nonsiolated(nonsending) should be used, similar syntax does not work:

await withTaskCancellationHandler { nonisolated(nonsending) in // error: Cannot find 'nonisolated' in scope
    ...
} onCancel: {
    ...
}

Full signature also does not work:

await withTaskCancellationHandler { nonisolated(nonsending) () async -> Void  in // error: Expected member name or initializer call after type name
    ...
} onCancel: {
    ...
}

As a workaround I can create intermediate variable:

let operation: nonisolated(nonsending) () async -> Void = { ... }
await withTaskCancellationHandler(operation: operation, onCancel: {
    ...
})

But this feels awkward. Is there a shorter syntax? Is this a bug?

1 Like

Grammar of a closure expression

closure-expression β†’ { attributes? closure-signature? statements? }

closure-signature β†’ capture-list? closure-parameter-clause async? throws-clause? function-result? in
closure-signature β†’ capture-list in

closure-parameter-clause β†’ ( ) | ( closure-parameter-list ,? ) | identifier-list
closure-parameter-list β†’ closure-parameter | closure-parameter , closure-parameter-list
closure-parameter β†’ closure-parameter-name type-annotation?
closure-parameter β†’ closure-parameter-name type-annotation ...
closure-parameter-name β†’ identifier

capture-list β†’ [ capture-list-items ,? ]
capture-list-items β†’ capture-list-item | capture-list-item , capture-list-items
capture-list-item β†’ capture-specifier? identifier
capture-list-item β†’ capture-specifier? identifier = expression
capture-list-item β†’ capture-specifier? self-expression
capture-specifier β†’ weak | unowned | unowned(safe) | unowned(unsafe)

β€”β€” docs.swift.org | Expressions | Closure Expression | Capture Lists

This is not a supported configuration at the moment. Take this error as guidance for reading the chart

await withTaskCancellationHandler { @nonisolated(nonsending) in
                                 // `~~~~~~~~~~~~~~~~~~~~~~~
                                 //  error: 'nonisolated' is a declaration modifier, not an attribute
    …
} onCancel: {
    …
}

Relevant Grammar:

closure-expression β†’ { attributes? closure-signature? statements? }

closure-signature β†’ capture-list? closure-parameter-clause async? throws-clause? function-result? in
closure-signature β†’ capture-list in

Declaration modifiers are not accounted in these grammar specs and as such are conflated by the compiler as variables and other valid resources. If you really don’t want a extra line, you can hint the compiler with as nonisolated(nonsending) () -> Void

You could also make a wrapper function that hints the compiler that the closure should be nonisolated(nonsending) by elaborating the type or hint the closures as being nonisolated(nonsending) with as on the closure as a parameter.

Potential Wrapper Implementation

This should be a good wrapper function to do hinting with minimal excess besides what is necessary for your use.

// This is `@inline(always)` as it is a pass through function
/// `nonisolatedNonsending:` must be passed `()` to hint
///  that this is the right function.
/// A information free type is used as this is a compiler hint
///  and as the values passed in are not accessible to the function.
/// Simply add `(nonisolatedNonsending: ())` into your call site
///  to hint for this overload.
@inline(always)
func withTaskCancellationHandler<T>(
    nonisolatedNonsending _: Void,
    operation: nonisolated(nonsending) () async throws -> T,
    onCancel handler: @Sendable () -> Void,
    isolation: isolated (any Actor)?
) async rethrows -> T {
    return try await Swift.withTaskCancellationHandler(
        operation: operation,
        onCancel : handler,
        isolation: isolation
    )
}

You could potentially use an attached macro but that would only be worth it if you have many places where you want to have nonisolated or multiple declaration modifiers that are commonly used in shorthanded closures

Is the reason to write this explicitly to suppress the warning? Or to get the new behavior before adopting the feature? I think it might be difficult to support the modifier here without trampling on parameters named nonisolated since nonisolated is a legal name for a parameter.

This isn't a direct answer to the question, but I find it strange this particular closure is even being flagged. Was this just an example?

This function is a little tricky, but I believe its two variants both always inherit the current actor context and are actually invalid to make @concurrent.

(FWIW, I have encountered other examples of incorrectly-diagnosed closures when using this mode: https://github.com/swiftlang/swift/issues/85817)

1 Like