When exactly 1 function matches a call site, should it be called or cause a compiler error?

I brought this up on Swift Dev and was told to check on Swift Evolution to
see if a proposal is needed.

Currently, the following code produces a compiler error:

func foo(_ x: Int, _ y: Int) -> Int {
  return x + y
}

extension Int {
  func foo() -> Int {
    return foo(self, self) // Error here
  }
}

Notice that the two functions named “foo” have entirely different
signatures. The global function takes 2 arguments, while the member
function takes 0 (or 1, if referenced as “Int.foo”).

There is exactly one function “foo” which takes 2 arguments, so a call to
“foo” with 2 arguments, like the one shown, should be unambiguous. However,
instead of calling the function with matching signature, there is instead a
compiler error.

This is already documented as SR–2450
<https://bugs.swift.org/browse/SR-2450>, with an example from the standard
library (global 2-argument “min” vs. 0-argument “Collection.min”).

It appears that in any situation where a global function and a member
function share the same base name, but only the global function’s signature
matches the call site, the result is a compiler error.

I suggest that, when there is exactly one function available with the
proper name and signature, instead of a compiler error the
matching function should be called.

Do we need a Swift Evolution proposal for this change?

Nevin

Yeah, this is an unfortunate wart. Right now unqualified lookup starts at the innermost scope and stops when it finds a candidate which matches the name being looked up. Overload sets are only formed if there are multiple candidates inside the same scope, not in different scopes.

It would be nice to fix this but note that it might have a compile-time performance impact, because now we will be looking up names in more scopes. In particular, this means almost every name lookup will have to look at all imported modules.

If this can be implemented in a clever way without impacting compile time, I’ll be all for this change.

However, note that the most common way in which people hit this is with type(of:) vs a local name named type — I think this can be solved without fundamentally changing unqualified lookup, by having unqualified lookup look at the DeclName rather than an Identifier. So if you have

var type = …

type(of: foo)

We would not consider the ‘var type’ at all, since it doesn’t match the DeclName type(of:). This might also address min vs Collection.min if we consider the number of arguments when performing the lookup too.

Either way I think an evolution proposal is a good idea, this has source compatibility impact since it can introduce ambiguity at call sites that were formerly unambiguous. But we should be careful not to impact compile time.

Slava

···

On Nov 12, 2017, at 5:12 PM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org> wrote:

I brought this up on Swift Dev and was told to check on Swift Evolution to see if a proposal is needed.

Currently, the following code produces a compiler error:

func foo(_ x: Int, _ y: Int) -> Int {
  return x + y
}

extension Int {
  func foo() -> Int {
    return foo(self, self) // Error here
  }
}

Notice that the two functions named “foo” have entirely different signatures. The global function takes 2 arguments, while the member function takes 0 (or 1, if referenced as “Int.foo”).

There is exactly one function “foo” which takes 2 arguments, so a call to “foo” with 2 arguments, like the one shown, should be unambiguous. However, instead of calling the function with matching signature, there is instead a compiler error.

This is already documented as SR–2450 <https://bugs.swift.org/browse/SR-2450>, with an example from the standard library (global 2-argument “min” vs. 0-argument “Collection.min”).

It appears that in any situation where a global function and a member function share the same base name, but only the global function’s signature matches the call site, the result is a compiler error.

I suggest that, when there is exactly one function available with the proper name and signature, instead of a compiler error the matching function should be called.

Do we need a Swift Evolution proposal for this change?

Nevin
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Yeah, this is an unfortunate wart. Right now unqualified lookup starts at
the innermost scope and stops when it finds a candidate which matches the
name being looked up. Overload sets are only formed if there are multiple
candidates inside the same scope, not in different scopes.

It would be nice to fix this but note that it might have a compile-time
performance impact, because now we will be looking up names in more scopes.
In particular, this means almost every name lookup will have to look at all
imported modules.

Well, the fact that it currently gives a different error message if you
delete the global function, tells me that *something* in the compiler is
already looking at both the member function and the global function.

If this can be implemented in a clever way without impacting compile time,
I’ll be all for this change.

However, note that the most common way in which people hit this is with
type(of:) vs a local name named type — I think this can be solved without
fundamentally changing unqualified lookup, by having unqualified lookup
look at the DeclName rather than an Identifier. So if you have

var type = …

type(of: foo)

We would not consider the ‘var type’ at all, since it doesn’t match the
DeclName type(of:). This might also address min vs Collection.min if we
consider the number of arguments when performing the lookup too.

Either way I think an evolution proposal is a good idea, this has source
compatibility impact since it can introduce ambiguity at call sites that
were formerly unambiguous. But we should be careful not to impact compile
time.

Pardon my lack of imagination, but could you provide an example of a call
site that would become ambiguous?

The change I am proposing has the effect of taking something that is
currently a compiler error (calling a global function when a member
function has the same base name but a different signature) and making it
not-an-error (since the global function is the only one whose signature
matches the call site).

Nevin

···

On Sun, Nov 12, 2017 at 8:44 PM, Slava Pestov <spestov@apple.com> wrote:

Yeah, this is an unfortunate wart. Right now unqualified lookup starts at the innermost scope and stops when it finds a candidate which matches the name being looked up. Overload sets are only formed if there are multiple candidates inside the same scope, not in different scopes.

It would be nice to fix this but note that it might have a compile-time performance impact, because now we will be looking up names in more scopes. In particular, this means almost every name lookup will have to look at all imported modules.

Well, the fact that it currently gives a different error message if you delete the global function, tells me that *something* in the compiler is already looking at both the member function and the global function.

Yeah, if we fail to solve the constraint system with the innermost candidate as the choice of overload, the diagnostics pass performs an additional name lookup to try to figure out what the user meant. However this doesn’t mean we’re performing the global lookup in the normal path.

Pardon my lack of imagination, but could you provide an example of a call site that would become ambiguous?

protocol P {}
protocol Q {}
struct S : P, Q {}

struct Outer {
  static func foo(_: P) {}

  struct Inner {
    static func foo(_: Q) {}

    static func bar() {
       foo(S())
    }
  }
}

Slava

···

On Nov 12, 2017, at 7:14 PM, Nevin Brackett-Rozinsky <nevin.brackettrozinsky@gmail.com> wrote:
On Sun, Nov 12, 2017 at 8:44 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

The change I am proposing has the effect of taking something that is currently a compiler error (calling a global function when a member function has the same base name but a different signature) and making it not-an-error (since the global function is the only one whose signature matches the call site).

Nevin

Resolves to Inner.foo just like it does today.

We would still start from the innermost scope and work our way outward
until we find a match. The only difference is we no longer stop partway up
the chain *without* finding a match.

If we do find a match then yes, of course we stop there and use it.

Nevin

···

On Sun, Nov 12, 2017 at 10:16 PM, Slava Pestov <spestov@apple.com> wrote:

Pardon my lack of imagination, but could you provide an example of a call
site that would become ambiguous?

protocol P {}
protocol Q {}
struct S : P, Q {}

struct Outer {
  static func foo(_: P) {}

  struct Inner {
    static func foo(_: Q) {}

    static func bar() {
       foo(S())
    }
  }
}

Pardon my lack of imagination, but could you provide an example of a call site that would become ambiguous?

protocol P {}
protocol Q {}
struct S : P, Q {}

struct Outer {
  static func foo(_: P) {}

  struct Inner {
    static func foo(_: Q) {}

    static func bar() {
       foo(S())
    }
  }
}

Resolves to Inner.foo just like it does today.

We would still start from the innermost scope and work our way outward until we find a match. The only difference is we no longer stop partway up the chain *without* finding a match.

That’s not implementable as described. Right now we find all possible overloads for a call site up front, then evaluate every combination of overloads at all call sites in an expression. The valid solutions are then ranked and the “best” one chosen, or an ambiguity is diagnosed. This means you can’t “stop” evaluating overloads, you need some way to disambiguate them and you have to consider all of them.

Slava

···

On Nov 12, 2017, at 7:27 PM, Nevin Brackett-Rozinsky <nevin.brackettrozinsky@gmail.com> wrote:
On Sun, Nov 12, 2017 at 10:16 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

If we do find a match then yes, of course we stop there and use it.

Nevin