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

[SR-13454] Wrong overloaded function called #55896

Open
swift-ci opened this issue Aug 27, 2020 · 7 comments
Open

[SR-13454] Wrong overloaded function called #55896

swift-ci opened this issue Aug 27, 2020 · 7 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself type checker Area → compiler: Semantic analysis

Comments

@swift-ci
Copy link
Collaborator

Previous ID SR-13454
Radar rdar://problem/67915513
Original Reporter arobinson (JIRA User)
Type Bug
Environment

macOS 10.15.6 (19G2021)

Xcode 12 beta 5

Xcode 12 beta 6

Additional Detail from JIRA
Votes 2
Component/s Compiler
Labels Bug, TypeChecker
Assignee None
Priority Medium

md5: 8417e922afc81d139a2d6f25a08b54a1

Issue Description:

Using this code as an example:

extension Array {
    func run<T>(_ effect: @escaping @autoclosure () -> T) -> Self {
        print("Run; mapping: \(self)")
        return self.map { value in
            _ = effect()
            return value
        }
    }

    func run<T>(_ effect: @escaping (Element) -> Array<T>) -> Self {
        print("Run; flat mapping: \(self)")
        return self.flatMap { value in
            effect(value).map { _ in value }
        }
    }
}

let doSomething: (String, String) -> Array<Void> = { _, _ in return [] }

func f1() {
    [1].flatMap({ _ in [("A", "a")]})
        .run(doSomething)
}

func f2() {
    let things = [1].flatMap({ _ in [("B", "b")]})
    things.run(doSomething)
}

let doSomethingElse: (String) -> Array<Void> = { _ in return [] }

func f3() {
    [1].flatMap({ _ in ["C"]})
        .run(doSomethingElse)
}

func f4() {
    let things = [1].flatMap({ _ in ["D"]})
    things.run(doSomethingElse)
}

f1()
f2()
f3()
f4()

Expected output:

Run; flat mapping: [("A", "a")]
Run; flat mapping: [("B", "b")]
Run; flat mapping: ["C"]
Run; flat mapping: ["D"] 

Actual output:

Run; mapping: [[("A", "a")]]
Run; flat mapping: [("B", "b")]
Run; flat mapping: ["C"]
Run; flat mapping: ["D"]

I expect the implementations of f1() and f2() to follow the exact same code path, calling the run function that uses flatMap. This is the case in Xcode 11, and I believe earlier Xcode 12 betas.

In Xcode 12 betas 5 and 6, f1() uses the map version.

It may be related to tuples, as f3() and f4() – which don't use tuples – both run through the flat mapping version as expected.

@theblixguy
Copy link
Collaborator

Simplified reproducer:

extension Array {
  func foo<T>(_: @escaping @autoclosure () -> T) -> Self {
    print("A")
    return self
  }

  func foo<T>(_: @escaping (Element) -> Array<T>) -> Self {
    print("B")
    return self
  }
}

let closure: (Int, Int) -> [Int] = { _, _ in return [] }
_ = [].flatMap { _ in [(0, 1)] }.foo(closure)

Prints A on Swift 5.3 but B on Swift 5.1 (haven't tried 5.2).

@LucianoPAlmeida
Copy link
Collaborator

@xedin @DougGregor A side effect of forward-scan for trailing closures?

@theblixguy
Copy link
Collaborator

I was looking at debug constraints output and I noticed it was choosing one overload over the other due to a failed constraint... not sure if it’s related to trailing closures though.

@typesanitizer
Copy link

@swift-ci create

@xedin
Copy link
Member

xedin commented Aug 28, 2020

The behavior in Xcode 12 is correct, because picking `run` overload with signature `(Element) -> Array<T>` requires a function conversion to be called with `doSomething`. Convertion would transform two arguments of `doSomething` into a single tuple type to be used for a `Element` when calling `run`. So when `run` is used in combination with `flatMap` it would mean that we’d always prefer an overload which requires the least number of conversions - in this case `@autoclosure () -> T` and there is no other contextual information available to clarify the choice. The reason why it picks another overload of `run` when `run` is used separately from `flatMap` is due to an incorrect hack in constraint solver which tries to compare two generic overloads to determine which one is better and ignoring all of the other context in the process, this is something which we need to fix and once that's done, calls to `f1` and `f2` are going to consistently pick `run<T>(_ effect: @escaping @autoclosure () -> T) -> Self` overload.

This is also behavior which is consistent with SE-0110.

@swift-ci
Copy link
Collaborator Author

Comment by Alex Robinson (JIRA)

Thanks for the explanation @xedin. Even with that insight, it's still surprising behaviour to me.

Annotating with a return type (below) makes it run the function that I expect, but to anyone reading or writing this code there's no ambiguity at the call site, and nothing warns users about possible ambiguity either.

[1].flatMap { _ -> [(String, String)] in
         [("X", "x")]
     }
     .run(doSomething)
// prints: Run; flat mapping: [("X", "x")]
 

Or is this just another version of the bug in f2() and this should also not end up calling the flatMap version?

@xedin
Copy link
Member

xedin commented Sep 8, 2020

The difference here is that result of closure argument passed to flatMap is specified explicitly which rules out overload of flatMap which accepts a closure that returns `ElementOfResult?` so "expected" overload `run` gets picked as a result.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself type checker Area → compiler: Semantic analysis
Projects
None yet
Development

No branches or pull requests

5 participants