Constraint solution producing invalid coercion

I've been reworking the constraint generation for UnresolvedMemberExprs, and am hitting an issue with the following simple snippet of code:

struct S {
    static var string = ""
}

let _: S = .string

Obviously, this shouldn't compile. There are type variables for the (implicit) base of the member reference ($T0), the result of the member access ($T1), and the contextual type of the expression ($T2). Constraints are specified such that $T0 and $T1 must be convertible to $T2. As expected, the first pass at solving this system fails:

(solving component #0
  ($T2 involves_type_vars bindings={(subtypes of) S})
  Initial bindings: $T2 := S
  (attempting type variable $T2 := S
    ($T0 bindings={(subtypes of) S})
    ($T1 fully_bound involves_type_vars bindings={(subtypes of) S})
    Initial bindings: $T0 := S
    (attempting type variable $T0 := S
      (overload set choice binding $T1 := @lvalue String)
      (failed constraint $T1 conv $T2 [[locator@0x11d043c20 [UnresolvedMember@/Users/freddy/Development/scratch/test.swift:7:13 -> unresolved member]]];)
    )
  )
failed component #0)

However, the solver then enters the salvage stage, where this happens:

  (solving component #0
    ($T0 potentially_incomplete involves_type_vars #defaultable_bindings=1 bindings={<<unresolvedtype>>})
    ($T2 involves_type_vars bindings={(subtypes of) S})
    Initial bindings: $T2 := S
    (attempting type variable $T2 := S
      ($T0 bindings={(subtypes of) S})
      ($T1 fully_bound involves_type_vars bindings={(subtypes of) S})
      Initial bindings: $T0 := S
      (attempting type variable $T0 := S
        (overload set choice binding $T1 := @lvalue String)
        (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
      )
    )
  finished component #0)
    (composed solution 0 0 0 0 0 0 0 0 0 0 0 0 0)

The full solution is,

$T2 as S
$T1 as @lvalue String
$T0 as S

which causes a crash down the line when the compiler attempts to coerce an @lvalue String to an S.

What is different about the salvage() pass that allows the above solution to be considered a success? If anyone has an idea of what's going on here, I'd appreciate any advice they can provide!

The full output of the solution process (cleaned up a bit for noise) can be found here:

Full output
Score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Contextual Type: S at [test.swift:7:8 - line:7:8]
Type Variables:
  $T0 [noescape allowed] potentially_incomplete involves_type_vars #defaultable_bindings=1 bindings={<<unresolvedtype>>} @ locator@1 [UnresolvedMember@test.swift:7:13 -> member reference base]
  $T1 [lvalue allowed] [noescape allowed] fully_bound subtype_of_existential involves_type_vars bindings={} @ locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]
  $T2 [lvalue allowed] [noescape allowed] involves_type_vars bindings={(subtypes of) S} @ locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]
  $T3 [noescape allowed] bindings={(subtypes of) S} @ locator@3 [UnresolvedMember@test.swift:7:13 -> contextual type -> pattern match]

Active Constraints:

Inactive Constraints:
  $T0.Type[(implicit) .string: value] == $T1 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
  $T1 conv $T2 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
  $T0 conv $T2 [[locator@4 [UnresolvedMember@test.swift:7:13 -> rvalue adjustment]]];
  $T3 conv S [[locator@5 [UnresolvedMember@test.swift:7:13 -> contextual type -> pattern match]]];
  $T2 conv S [[locator@6 [UnresolvedMember@test.swift:7:13 -> contextual type]]];
  ---Constraint graph---
  $T0:
    Constraints:
      $T0.Type[(implicit) .string: value] == $T1 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
      $T0 conv $T2 [[locator@4 [UnresolvedMember@test.swift:7:13 -> rvalue adjustment]]];

  $T1:
    Constraints:
      $T0.Type[(implicit) .string: value] == $T1 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
      $T1 conv $T2 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];

  $T2:
    Constraints:
      $T1 conv $T2 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
      $T0 conv $T2 [[locator@4 [UnresolvedMember@test.swift:7:13 -> rvalue adjustment]]];
      $T2 conv S [[locator@6 [UnresolvedMember@test.swift:7:13 -> contextual type]]];

  $T3:
    Constraints:
      $T3 conv S [[locator@5 [UnresolvedMember@test.swift:7:13 -> contextual type -> pattern match]]];

---Connected components---
  0: $T0 $T1 $T2
  1: $T3
  (solving component #1
    ($T3 bindings={(subtypes of) S})
    Initial bindings: $T3 := S
    (attempting type variable $T3 := S
      (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
    )
  finished component #1)
  (solving component #0
    ($T2 involves_type_vars bindings={(subtypes of) S})
    Initial bindings: $T2 := S
    (attempting type variable $T2 := S
      ($T0 bindings={(subtypes of) S})
      ($T1 fully_bound involves_type_vars bindings={(subtypes of) S})
      Initial bindings: $T0 := S
      (attempting type variable $T0 := S
        (overload set choice binding $T1 := @lvalue String)
        (failed constraint $T1 conv $T2 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];)
      )
    )
  failed component #0)
---Solver statistics---
Total number of scopes explored: 6
Maximum depth reached while exploring solutions: 4
Time: 2.651000e+00ms
---Attempting to salvage and emit diagnostics---
  ---Constraint graph---
  $T0:
    Constraints:
      $T0.Type[(implicit) .string: value] == $T1 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
      $T0 conv $T2 [[locator@4 [UnresolvedMember@test.swift:7:13 -> rvalue adjustment]]];

  $T1:
    Constraints:
      $T0.Type[(implicit) .string: value] == $T1 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
      $T1 conv $T2 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];

  $T2:
    Constraints:
      $T1 conv $T2 [[locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]]];
      $T0 conv $T2 [[locator@4 [UnresolvedMember@test.swift:7:13 -> rvalue adjustment]]];
      $T2 conv S [[locator@6 [UnresolvedMember@test.swift:7:13 -> contextual type]]];

  $T3:
    Constraints:
      $T3 conv S [[locator@5 [UnresolvedMember@test.swift:7:13 -> contextual type -> pattern match]]];

---Connected components---
  0: $T0 $T1 $T2
  1: $T3
  (solving component #1
    ($T3 bindings={(subtypes of) S})
    Initial bindings: $T3 := S
    (attempting type variable $T3 := S
      (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
    )
  finished component #1)
  (solving component #0
    ($T0 potentially_incomplete involves_type_vars #defaultable_bindings=1 bindings={<<unresolvedtype>>})
    ($T2 involves_type_vars bindings={(subtypes of) S})
    Initial bindings: $T2 := S
    (attempting type variable $T2 := S
      ($T0 bindings={(subtypes of) S})
      ($T1 fully_bound involves_type_vars bindings={(subtypes of) S})
      Initial bindings: $T0 := S
      (attempting type variable $T0 := S
        (overload set choice binding $T1 := @lvalue String)
        (found solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
      )
    )
  finished component #0)
    (composed solution 0 0 0 0 0 0 0 0 0 0 0 0 0)
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
  $T2 as S @ locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]
  $T1 as @lvalue String @ locator@2 [UnresolvedMember@test.swift:7:13 -> unresolved member]
  $T0 as S @ locator@1 [UnresolvedMember@test.swift:7:13 -> member reference base]
  $T3 as S @ locator@3 [UnresolvedMember@test.swift:7:13 -> contextual type -> pattern match]

The "salvage" stage repeats constraint solving while allowing "fixes" to be applied for any failed constraints along the way. These fixes are recorded in the constraint system and reported back to the user once a solution is found. You can see when a fix is applied in the -debug-constraints output, it will look something like:

(attempting fix [fix: ignore specified contextual type] @ locator@0x12980e128 [UnresolvedMember@test.swift:34:13 -> rvalue adjustment])

After a fix is applied for a constraint (which would have failed in the first solving pass), the solver considers the constraint successfully simplified and it continues on - this is the main difference between the first solving pass and the second. In the source code, you'll see it check shouldAttemptFixes() to know whether to fail or apply a fix and continue on.

I'm not seeing the fix being applied in the output you pasted above - perhaps a change you made locally is preventing the solver from recording the fix?

Also, more details about the constraint fix strategy is described here: Swift.org - New Diagnostic Architecture Overview

2 Likes

Thanks @hborla. Yeah, the lack of any recorded fixes is what confused me as well. I don't believe that any of the changes I've made would impact how fixes get recorded—I've really only modified the constraints that get generated for a few different expressions (and changed a bit of parsing logic). Solving/salvaging logic is untouched!

Hmm. It sounds like changing the constraints that are generated has broken some assumption(s) in the salvaging code. I know of some places in CSSimplify that allow certain constraints to be solved without applying fixes because it assumes that a fix was already applied previously. For example, this bit of code in matchTypesBindTypeVar:

      // Just like in cases where both sides are dependent member types
      // with resolved base that can't be simplified to a concrete type
      // let's ignore this mismatch and mark affected type variable as a hole
      // because something else has to be fixed already for this to happen.
      if (type->is<DependentMemberType>() && !type->hasTypeVariable()) {
        recordPotentialHole(typeVar);
        return getTypeMatchSuccess();
      }

Alright, that's a helpful breadcrumb. I'll see if I can track down where a fix like that might be getting applied. Thanks for your help!

1 Like

FWIW, this was the offending snippet in CSSimplify:

   // Unresolved member type mismatches are handled when
   // r-value adjustment constraint fails.		
   case ConstraintLocator::UnresolvedMember:		
     return true;

I had removed the RValueAdjustment element from constraint generation!

1 Like