Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler does not know how to choose between an async and a non-async version of a function #60469

Open
stefanspringer1 opened this issue Aug 9, 2022 · 6 comments
Labels
async & await Feature → concurrency: asynchronous function aka the async/await pattern bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler itself concurrency Feature: umbrella label for concurrency language features overload resolution Area → compiler → type checker: Overload resolution (ranking) swift 6.0 type checker Area → compiler: Semantic analysis unexpected error Bug: Unexpected error

Comments

@stefanspringer1
Copy link

stefanspringer1 commented Aug 9, 2022

Acoording to the specification, the compiler should know if an async or non-async version of a function is to be called. This is not the case in the following example, at the line [1,2,3].forEach { n in show(n) } the compiler gives the error "Ambiguous use of 'forEach'". Only when the await keyword is used in the line after it the compiler knows how to choose between the two versions.

Note that if instead of the definition of forEach in the example a version of it allowing throwing is used, the compiler does know which one to choose, even if the actual code does not throw at all.

Environment: macOS 12.5, Xcode 13.4.1, Swift 5.6.1. Also see this topic in the forums.

public extension Sequence {
    
    func forEach (
        _ operation: (Element) async -> Void
    ) async {
        for element in self {
            await operation(element)
        }
    }
    
}

@main
struct App {
    static func main() async {
        
        func show(_ n: Int) {
            print(n)
        }
        
        func showAsync(_ n: Int) async {
            print(n)
        }
        
        func showAsyncThrowing(_ n: Int) async throws {
            print(n)
        }
    
        [1,2,3].forEach { n in show(n) }
        await [1,2,3].forEach { n in await showAsync(n) }
        
    }
}
@stefanspringer1 stefanspringer1 added the bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. label Aug 9, 2022
@LucianoPAlmeida LucianoPAlmeida added the concurrency Feature: umbrella label for concurrency language features label Aug 10, 2022
@LucianoPAlmeida
Copy link
Contributor

I guess this is somewhat similar to #60318

@ole
Copy link
Contributor

ole commented Aug 10, 2022

This may be related to the fact that the synchronous variant of forEach is defined in the standard library. If you define both APIs next to each other (calling them e.g. myForEach and myForEach async to avoid the name conflict with the stdlib), the compiler can correctly disambiguate.

The standard library and some other system libraries have a lower overload resolution priority than "normal" modules. If I remember correctly, this was introduced to avoid ambiguity errors when the standard library introduces new symbols. E.g. when the standard library added the Result type, existing programs that already imported a different Result type from an open-source package should continue compiling.

Maybe this lower overload priority ends up giving the sync forEach from the stdlib the same priority as the async forEach variant defined by you. That would explain why the compiler can't decide between the two.

@saagarjha
Copy link
Contributor

Does it? I'm not able to get this code to compile:

func foo(_ expression: @autoclosure () -> Int) {
	_ = expression()
}

func foo(_ expression: @autoclosure () async -> Int) async {
	_ = await expression()
}


foo(1)
await foo(await 1)

@AnthonyLatsis AnthonyLatsis added compiler The Swift compiler itself type checker Area → compiler: Semantic analysis overload resolution Area → compiler → type checker: Overload resolution (ranking) swift 6.0 async & await Feature → concurrency: asynchronous function aka the async/await pattern unexpected error Bug: Unexpected error labels Oct 29, 2023
@AnthonyLatsis
Copy link
Collaborator

@saagarjha This is similar but feels like a different problem internally. Could you please open a new issue?

@saagarjha
Copy link
Contributor

Filed it as #69489.

@hborla
Copy link
Member

hborla commented Nov 14, 2023

Here's the overloading rule from SE-0296:

Instead, we propose an overload-resolution rule to select the appropriate function based on the context of the call. Given a call, overload resolution prefers non-async functions within a synchronous context (because such contexts cannot contain a call to an async function). Furthermore, overload resolution prefers async functions within an asynchronous context (because such contexts should avoid stepping out of the asynchronous model into blocking APIs).

The reason why [1,2,3].forEach { n in show(n) } is ambiguous is because overload resolution ranks synchronous calls in an async context in the same way that it ranks a sync-to-async function conversion, considering both to be a "synchronous call in an async context". The synchronous overload of forEach is a direct synchronous call in an async context, and the async overload of forEach contains a sync-to-async function conversion for the closure argument. So, both solutions end up with the same score.

Based on the specified rule in the proposal, this is arguably correct behavior. To resolve this, we would need to break "synchronous call in async context" and the "sync-to-async function conversion" (currently both represented by SK_SyncInAsync) into separate score bits. If "synchronous call in async context" is ordered before "sync-to-async function conversion" (meaning the former is considered a worse solution than the latter), the code would still not compile because the effects checker would diagnose an async call missing await. If the order is flipped, the code would successfully compile by selecting the synchronous overload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async & await Feature → concurrency: asynchronous function aka the async/await pattern bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler itself concurrency Feature: umbrella label for concurrency language features overload resolution Area → compiler → type checker: Overload resolution (ranking) swift 6.0 type checker Area → compiler: Semantic analysis unexpected error Bug: Unexpected error
Projects
None yet
Development

No branches or pull requests

6 participants