Skip to content

Compiler allows converting @concurrent closure to nonisolated(nonsending) closure #87487

@rayx

Description

@rayx

Description

According to SE-0461 a @concurrent function shouldn't be converted to nonisolated(nonsending) function. However, I find compiler fails to prevent it in some scenarios. In the code below, both test1 and test2 should fail to compile, but test2 compiles.

@concurrent
func fn(_ ns: NS) async {}

func foo(_ fn: nonisolated(nonsending) (NS) async -> Void) {}

func test1() {
    foo(fn) // error: cannot convert '@Sendable (NS) async -> ()' to 'nonisolated(nonsending) (NS) async -> Void' because crossing of an isolation boundary requires parameter and result types to conform to 'Sendable' protocol
}

func test2() {
    let fn: @concurrent (NS) async -> Void = { _ in }
    foo(fn) // OK
}

Below is a more complete test program to verify that fn runs on global executor indeed. It also demonstrates how that could lead to a data race.

class NS {
    var value = 0
}

func currentIsolation(isolation actor: (any Actor)? = #isolation) -> String {
    return "\(actor, default: "non-isolated")"
}

func logStart(_ fnName: String, _ isolation: String) {
    print("BEGIN \(fnName) [\(isolation)]")
}

func logEnd(_ fnName: String) {
    print("END \(fnName)")
}

actor A {
    var ns = NS()

    func foo(_ fn: nonisolated(nonsending) (NS) async -> Void) async {
        logStart("foo", currentIsolation())
        await fn(self.ns)
        logEnd("foo")
    }

    func test() {
        let fn: @concurrent (NS) async -> Void = { ns in 
            logStart("fn", currentIsolation())
            _ = ns
            logEnd("fn")
        }

        Task { 
            await foo(fn)
        }

        // Task {
        //     await foo(fn)
        // }
    }
}

let a = A()
await a.test()
try? await Task.sleep(for: .seconds(1))

// test output:
// BEGIN foo [output.A]
// BEGIN fn [non-isolated]
// END fn
// END foo

Note the output. It shows that fn actually runs on global executor. If we starts multiple tasks (see the code commented out), there are multiple instances of fn running on global executor and mutating A.ns simultaneously.

Reproduction

See above

Expected behavior

test2 in example code should fail to compile.

Environment

nightly build. The issue can be reproduced in dev snapshot and Swift 6.2 too.

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage neededThis issue needs more specific labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions