Ambiguous use of operator in Swift 5.4

This code compiles in Swift 5.3. 5.4 fails with ambiguous use of operator on (3.0/8.0). Two questions:

  1. What changed between 5.3 and 5.4?

  2. How can I tell Swift 5.4 that though Expr is ExpressibleByFloatLiteral, and defines
    func / (_ x: Expr, _ y: Expr)
    func / (_ x: Double, _ y: Expr)
    func / (_ x: Expr, _ y: Double)
    that I want 3.0/8.0 to use / (Double, Double) -> Double without explicitly saying so every time?

    final class Expr : ExpressibleByFloatLiteral {
         var n: Double = 0
         init(_ n: Double)  { self.n = n }
         convenience init(floatLiteral x: Double)  { self.init(x) }
     }
    
     func * (_ x: Expr,   _ y: Expr)   -> Expr { x }
     func * (_ x: Double, _ y: Expr)   -> Expr { y }
     func / (_ x: Expr,   _ y: Expr)   -> Expr { x }
     func / (_ x: Double, _ y: Expr)   -> Expr { y }
     func / (_ x: Expr,   _ y: Double) -> Expr { x }
    
     let k = (3.0/8.0) * Expr(0)
    

I removed a hack in Swift 5.4 which used to force the type checker to use the same type for literals of the same kind in operator expressions (in certain cases including this one). This would mean that the type checker could only find a solution using overloads of / that use the same parameter type on either side, because the literals 3.0 and 8.0 had to have the same type.

So, in Swift 5.4, the type checker can also find solutions using the other, non-symmetric overloads of /. The type checker should be able to find a solution using (Double, Double) -> Double for / and (Double, Expr) -> Expr for *, but it doesn't because of a different performance hack that "favors" (Expr, Expr) -> Expr for * and stops after it finds a solution using this overload.

The "favoring" hack is based on the argument types, so if you use a contextual type (e.g. a type annotation) instead, it does successfully type check:

 let k: Expr = (3.0/8.0) * 0.0

It also successfully type checks if you add a (Double, Double) -> Expr overload for / because the type checker will prefer a solution that uses more default literal types, and the default float literal type is Double.

Please let me know if you have any further questions! I have been trying to remove the "favoring" hack as well, but at the moment the type checker is too dependent on this hack for performance in many cases.

15 Likes

Does this do anything to move the needle forward in addressing SR-13755? Would now an appropriately added overload fix the problem given the current arrangement of hacks?

I haven’t tried, and I don’t remember off the top of my head how the favoring hack behaves with generic overloads, but the more fundamental problem causing SR-13755 is the way the solver handles default literal types. Essentially, non-default literal types are modeled like conversions, so the solutions that infer the type of a literal to be T are pruned if the solver already has a solution that uses the default literal type.

I would love for non-default literal types to not be modeled like conversions and instead only be considered when solutions are ranked at the end, but the solver is very dependent on pruning for performance of expressions involving literals. I’ve been wanting to experiment with making the solver prefer symmetric / less generic operator overloads in a more principled way, but haven’t yet had the time. Pavel and I have also been considering doing a bigger re-think of overload resolution and ranking rules (and documenting them!) at some point, which would be an opportunity for us to fix some of the issues like this which need a more fundamental re-modeling of these rules.

7 Likes

Is the "favoring" hack the reason that ==(Expr, Expr) is preferred over ==(Expr,String) and ==(Expr, Double) here?

    struct Expr: ExpressibleByStringLiteral, ExpressibleByFloatLiteral {
        init(floatLiteral value: Double) { }
        init(stringLiteral value: String) { }
        init() { }
        
        static func == (lhs: Expr, rhs: Expr) -> Bool { true }
        static func == (lhs: Expr, rhs: Double) -> Bool { false }
        static func == (lhs: Expr, rhs: String) -> Bool { false }
    }

    let expr = Expr()
    print(expr == "42")   // true, uses ==(Expr, Expr)
    print(expr == 42.0)   // true, uses ==(Expr, Expr)

Yes. If you're extra curious and want to look at some verbose and hard-to-read debug logging for type inference, you can run swiftc with -Xfrontend -debug-constraints and see which declarations end up getting favored by the solver:

  disjunction [[locator@0x7fe3bf0ff868 [OverloadedDeclRef@test.swift:14:11]]]:
>  [favored]  $T1 bound to decl test.(file).Expr.==@test.swift:7:15 : (Expr.Type) -> (Expr, Expr) -> Bool [[locator@0x7fe3bf0ff868 [OverloadedDeclRef@test.swift:14:11]]];
>             $T1 bound to decl test.(file).Expr.==@test.swift:8:15 : (Expr.Type) -> (Expr, Double) -> Bool [[locator@0x7fe3bf0ff868 [OverloadedDeclRef@test.swift:14:11]]];
>             $T1 bound to decl test.(file).Expr.==@test.swift:9:15 : (Expr.Type) -> (Expr, String) -> Bool [[locator@0x7fe3bf0ff868 [OverloadedDeclRef@test.swift:14:11]]];
...all the other overloads of ==...

Later on in the output, you can see the solver attempting the favored overload first, and then stopping once it has found a solution, so the other overloads aren't even attempted:

  (solving component #1
    ($T3 delayed literal=3 bindings={})
    (attempting disjunction choice $T1 bound to decl test.(file).Expr.==@test.swift:7:15 : (Expr.Type) -> (Expr, Expr) -> Bool [[locator@0x7fe3bf0ff868 [OverloadedDeclRef@test.swift:14:11]]];
      (overload set choice binding $T1 := (Expr, Expr) -> Bool)
      ($T3 bindings={(subtypes of) Expr})
      Initial bindings: $T3 := Expr
      (attempting type variable $T3 := Expr
        (increasing score due to non-default literal)
        (found solution 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0)
      )
    )
  finished component #1)
    (composed solution 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0)
5 Likes