Crash on the boundary between Swift 5 and Swift 6 code

I'm getting a crash in the runtime using Xcode 16.4 (Apple Swift version 6.1.2). Also reproducible with a nightly toolchain (2025-06-22).

// MyLib: Swift 6

@preconcurrency public typealias Action = @MainActor () -> Void

@preconcurrency @MainActor
public func useIt(_ actions: [String: Action]) {
    actions["xyz"]?()
}
// MyApp: Swift 5, minimal checking
import MyLib

func getActions() -> [String: Action] {
    return [
        "abc": { print("abc") },
        "xyz": { print("xyz") }
    ]
}

// CRASH: Could not cast value of type '() -> ()' (0x104818518) to '@Swift.MainActor @Sendable () -> ()' (0x10487ebf8).
//
// #0 __pthread_kill ()
// #1 pthread_kill ()
// #2 abort ()
// #3 swift::fatalErrorv ()
// #4 swift::fatalError ()
// #5 swift::swift_dynamicCastFailure ()
// #6 swift::swift_dynamicCastFailure ()
// #7 swift_dynamicCast ()
// #8 Swift._dictionaryUpCast<τ_0_0, τ_0_1, τ_0_2, τ_0_3 where τ_0_0: Swift.Hashable, τ_0_2: Swift.Hashable>(Swift.Dictionary<// τ_0_0, τ_0_1>) -> Swift.Dictionary<τ_0_2, τ_0_3> ()
// 
useIt(getActions())

Issue does not reproduce when app is compiled with complete isolation checking.
There are no compile time diagnostics emitted.

Is this a compiler bug or am I using @preconcurrency incorrectly?

1 Like

Does it still crash if you try updating getActions() like so:

func getActions() -> [String: Action] {
    return [
        "abc": { @MainActor in print("abc") },
        "xyz": { @MainActor in print("xyz") }
    ]
}

Still crashes:

func getActions() -> [String: Action] {
    return [
        "abc": { @MainActor in print("abc") }, // ⚠️ Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'; this is an error in the Swift 6 language mode
        "xyz": { @MainActor in print("xyz") }  // ⚠️ Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'; this is an error in the Swift 6 language mode
    ]
}

Reported as App crashes on the boundary between Swift 5 and Swift 6 code · Issue #82618 · swiftlang/swift · GitHub

It seems that adding -warn-concurrency to the Swift 5 target fixes the issue, aligning dictionary types as Dictionary<String, @MainActor @Sendable () -> ()>.

@Douglas_Gregor, @beccadax, is it expected that @preconcurrency works only in tandem with -warn-concurrency?

EDIT: But with -warn-concurrency calls to Action in MyApp also trigger the error, while calls to useIt() do not. Is this another bug?

Since MyLib is compiled with -swift-version 6, why bother putting @preconcurrency on those 2 nominal decls? There shouldn't be any crash after removing those 2 occurrences of @preconcurrency in MyLib.

Because MyApp is compiled in Swift 5 and is still missing concurrency annotations. Without @preconcurrency usages of those APIs in MyApp trigger an error even with -strict-concurrency=minimal.

I understand your motivations.

The consequence of using @preconcurrency on MyLib.Action is, at all use sites with minimal concurrency checking, Action will be treated as () -> Void instead of the preferred @MainActor @Sendable () -> Void) (reference).

I agree a relevant runtime conversion crash is surprising (although not common), but is understandable from my perspective.

Playing with this further, with generic types that don't have special treat (i.e. other than Array, Set, Dictionary), compiler produces a compilation error:

// MyLib: Swift 6

public struct Box<T> {
    public var value: T
    public init(value: T) {
        self.value = value
    }
}

@preconcurrency @MainActor
public func useBox(_ box: Box<Action>) {
    box.value()
}
// MyApp: Swift 5, minimal checking

func getBox() -> Box<Action> {
    return Box { print("box") }
}

useBox(getBox()) // error: Cannot convert value of type 'Box<() -> Void>' to expected argument type 'Box<Action>' (aka 'Box<@MainActor @Sendable () -> ()>')

Should compiler produce the same error for the Array, Set and Dictionary? @Douglas_Gregor, @beccadax, WDYT?