[ConstraintSystem] A non natural behavior of compiler solving sync function even if I use `await` keyword

Environment

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0

I run this code.

func f(_ a: () async -> Void) async {
    print("async")
}
func f(_ a: () -> Void) {
    print("sync")
}

func g() async {
    await f { } 
}
await g()

output

swift  Source.swift
Source.swift:8:5: warning: no 'async' operations occur within 'await' expression
    await f { }
    ^
sync

I know why func f(_ a: () -> Void) is called According to an output of -debug-constraints. It is not a kind of bug.
However, I think this behavior is not natural.

I think it would be great always solve async of func when using await keyword.
since I think no one would be happy synchronous is called when we use await keyword.

(In this case, I think it would be great to increase SK_AsyncInSyncMismatch or create a new score in constraint system.)

What do you think?

Why `func f(_ a: () -> Void)` is called?

An output of swiftc -Xfrontend -debug-constraints Source.swift.

---Solver statistics---
Total number of scopes explored: 5
Maximum depth reached while exploring solutions: 3
Time: 8.310000e-01ms
Comparing 2 viable solutions

--- Solution #0 ---
Fixed score: [component: sync-in-asynchronous(s), value: 1]
Type variables:
  $T0 as (() async -> Void) async -> () @ locator@0x153188200 [OverloadedDeclRef@Source.swift:8:11]
  $T1 as () -> () @ locator@0x153188398 [Closure@Source.swift:8:13]
  $T2 as () @ locator@0x1531883e0 [Closure@Source.swift:8:13 → closure result]
  $T3 as () @ locator@0x1531884e8 [Call@Source.swift:8:11 → function result]

Overload choices:
  locator@0x153188200 [OverloadedDeclRef@Source.swift:8:11] with Source.(file).f@Source.swift:1:6 as f: (() async -> Void) async -> ()
Trailing closure matching:
  locator@0x153188620 [Call@Source.swift:8:11 → apply argument]: forward

--- Solution #1 ---
Fixed score: [component: sync-in-asynchronous(s), value: 1]
Type variables:
  $T0 as (() -> Void) -> () @ locator@0x153188200 [OverloadedDeclRef@Source.swift:8:11]
  $T1 as () -> () @ locator@0x153188398 [Closure@Source.swift:8:13]
  $T2 as () @ locator@0x1531883e0 [Closure@Source.swift:8:13 → closure result]
  $T3 as () @ locator@0x1531884e8 [Call@Source.swift:8:11 → function result]

Overload choices:
  locator@0x153188200 [OverloadedDeclRef@Source.swift:8:11] with Source.(file).f@Source.swift:4:6 as f: (() -> Void) -> ()
Trailing closure matching:
  locator@0x153188620 [Call@Source.swift:8:11 → apply argument]: forward
comparing solutions 1 and 0
Comparing declarations
func f(_ a: () -> Void) {

}
and
func f(_ a: () async -> Void) async {

}
(isDynamicOverloadComparison: 0)
(increasing 'sync-in-asynchronous' score by 1 @ locator@0x15318cc00 [])
  (found solution: [component: sync-in-asynchronous(s), value: 1])
comparison result: better
Comparing declarations
func f(_ a: () async -> Void) async {

}
and
func f(_ a: () -> Void) {

}
(isDynamicOverloadComparison: 0)
(failed constraint () async -> Void subtype () -> Void @ locator@0x15318dc00 [])
comparison result: not better
comparing solutions 1 and 0

the compiler solve overloads based on the rule

  1. The compiler check overloads based on the score rule. func f(_ a: () async -> Void) async and func f(_ a: () -> Void).
  2. The both functions will be scored the same score [component: sync-in-asynchronous(s), value: 1]
  3. When the scores are the same value, the compiler will check which is a subtype in arguments A: _ a: () async -> Void and B: _ a: () -> Void.
  4. The compiler will recognize B is better since B is a subtype of A.
  5. The compiler will solve func f(_ a: () -> Void) according to the result that B is better.
4 Likes