Skip to content

Interaction solver for Coercible constraints#3955

Merged
hdgarrood merged 3 commits intopurescript:masterfrom
kl0tl:coercible/interaction-solver
Dec 19, 2020
Merged

Interaction solver for Coercible constraints#3955
hdgarrood merged 3 commits intopurescript:masterfrom
kl0tl:coercible/interaction-solver

Conversation

@kl0tl
Copy link
Copy Markdown
Member

@kl0tl kl0tl commented Nov 15, 2020

This pull request implements an interaction solver for Coercible constraints based on the section 5.2 An overview of OUTSIDEIN(X) of the Safe Zero-cost Coercions for Haskell paper, the section 7.4 Rewriting constraints of the Modular type inference with local assumptions paper and GHC implementation. This allows Coercible to obey transitivity in a much better way than I initially tried with #3930.

Instead of solving Coercible constraints by computing subgoals and recursively solving them like ordinary constraints we keep rewriting each constraints then interacting them with givens until they become irreducible or we encounter an error. Solving fails when there’s some irreducible wanted constraints left.

We also solve givens beforehand in order to deduce more equalities and solve more wanteds. For instance the declarations:

data D a = D a

example :: forall a b. Coercible (D a) (D b) => a -> b
example = coerce

yield a given Coercible (D a) (D b) from which can deduce a constraint Coercible a b that we can use to discharge the wanted.

Other improvements

This leads to more precise error messages because we can always mention the original constraint in ErrorSolvingConstraint hints (see the changes to the golden tests) and enables some other improvements:

We can throw TypesDoNotUnify errors on nominal parameters mismatches in the decomposition rule.

Given the following example:

data Representational a
type role Representational representational

data Nominal a
type role Nominal nominal

example :: forall a b. Representational (Nominal a) -> Representational (Nominal b)
example = coerce

we can now report:

Error found:
in module Example
at Example.purs:12:11 - 12:17 (line 12, column 11 - line 12, column 17)

  Could not match type
      
    a1
      
  with type
      
    b3
      

while solving type class constraint
                                                                 
  Prim.Coerce.Coercible (Representational @Type (Nominal @t0 a1))
                        (Representational @Type (Nominal @t2 b3))
                                                                 
while checking that type forall (a :: Type) (b :: Type). Coercible @Type a b => a -> b
  is at least as general as type Representational @Type (Nominal @t0 a1) -> Representational @Type (Nominal @t2 b3)
while checking that expression coerce
  has type Representational @Type (Nominal @t0 a1) -> Representational @Type (Nominal @t2 b3)
in value declaration example

where a1 is a rigid type variable
        bound at (line 12, column 11 - line 12, column 17)
      b3 is a rigid type variable
        bound at (line 12, column 11 - line 12, column 17)
      t0 is an unknown type
      t2 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.

instead of:

 No type class instance was found for
                                          
    Prim.Coerce.Coercible (Nominal @t0 a1)
                          (Nominal @t2 b3)
                                          
  The instance head contains unknown type variables. Consider adding a type annotation.

while checking that type forall (a :: Type) (b :: Type). Coercible @Type a b => a -> b
  is at least as general as type Representational @Type (Nominal @t0 a1) -> Representational @Type (Nominal @t2 b3)
while checking that expression coerce
  has type Representational @Type (Nominal @t0 a1) -> Representational @Type (Nominal @t2 b3)
in value declaration example

where a1 is a rigid type variable
        bound at (line 12, column 11 - line 12, column 17)
      b3 is a rigid type variable
        bound at (line 12, column 11 - line 12, column 17)
      t0 is an unknown type
      t2 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md for more information,
or to contribute content related to this error.

Throwing TypesDoNotUnify errors before would have report:

Error found:
in module Example
at Example.purs:12:11 - 12:17 (line 12, column 11 - line 12, column 17)

  Could not match type
      
    a1
      
  with type
      
    b3
      

while solving type class constraint
                                        
  Prim.Coerce.Coercible (Nominal @t0 a1)
                        (Nominal @t2 b3)
                                        
while checking that type forall (a :: Type) (b :: Type). Coercible @Type a b => a -> b
  is at least as general as type Representational @Type (Nominal @t0 a1) -> Representational @Type (Nominal @t2 b3)
while checking that expression coerce
  has type Representational @Type (Nominal @t0 a1) -> Representational @Type (Nominal @t2 b3)
in value declaration example

where a1 is a rigid type variable
        bound at (line 12, column 11 - line 12, column 17)
      b3 is a rigid type variable
        bound at (line 12, column 11 - line 12, column 17)
      t0 is an unknown type
      t2 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.
We can relax the nominal equality constraint between rows tails to a representational one.

Given the following declaration:

newtype Age = Age Int

we used to only accept closed rows:

example :: { age :: Int } -> { age :: Age }
example = coerce

or open rows whose tails unify:

example :: forall r. { age :: Int | r } -> { age :: Age | r }
example = coerce

but now we can also accept open rows whose tails are coercible to each others:

example :: forall r s. Coercible r s => { age :: Int | r } -> { age :: Age | s }
example = coerce

The new solver also follows the paper more accurately on newtypes:

We unwrap newtypes first.

The paper doesn’t explain why but there’s a note about this in GHC (see “Unwrap newtypes first” in GHC.Tc.Solver.Canonical). Given the following declaration:

newtype N f a = N (f a)

decomposing Coercible (N Maybe a) (N Maybe b) would otherwise fail because the second parameter of N is nominal. On the other hand, unwraping on both sides yields Coercible (Maybe a) (Maybe b) which we can then decompose to Coercible a b and eventually solve if a is a newtype over b for instance.

We only unwrap finite chains of newtypes.

Given the following declarations:

newtype N a = N (N a)
type role N representational

trying to solve Coercible (N a) (N b) would otherwise yield a PossiblyInfiniteCoercibleInstance error although we can decompose to Coercible a b and discharge that constraint with an appropriate given.

Bugfixes

I fixed some bugs I introduced in #3893:

Polymorphic kind variables should be instantiated when saturating higher kinded types, either to the kinds explicitely provided via kind applications or to unknowns.

Given the following declarations:

data D :: forall k. k -> Type
data D a

type role D representational

naively saturating D while solving Coercible (D @k1) (D @k2) yields Coercible (D (t0 :: k)) (D (t0 :: k)) instead of Coercible (D (t0 :: k1)) (D (t0 :: k2)) and k is not bound anywhere.

Kind applications can mention unknowns, so we should apply the current substitution to Coercible constraints parameters once we unified their kinds. This is what I mentioned in #3893 (comment) but did not understand how to observe before.

Coercible (D D) (D D) actually means Coercible (D @(k1 -> Type) (D @k1)) (D @(k2 -> Type) (D @k2)), which we decompose to Coercible (D @k1) (D @k2), where k1 and k2 are unknowns. This constraint is not reflexive because D @k1 and D @k2 are differents but both arguments kinds unify with k -> Type, where k is a fresh unknown, so applying the substitution to D @k1 and D @k2 yields a Coercible (D @k) (D @k) constraint which could be trivially solved by reflexivity instead of having to saturate the type constructors.

Future work

Polytypes.

The paper mention polytypes in section 4.2 Coercions but there are omitted from section 5.2 An overview of OUTSIDEIN(X) “due to the complexities they add to the algorithm”.

They seem to be canonicalized with can_eq_nc_forall in GHC.Tc.Solver.Canonical but the comment there would led me to believe that GHC would be able to compile the following:

{-# LANGUAGE RankNTypes #-}

module Example where

data D a = D a

example :: (forall a. D a) -> (forall b. D b)
example = coerce

or if (forall a. D a) -> (forall b. D b) is interpreted as forall b. (forall a. D a) -> D b:

{-# LANGUAGE RankNTypes #-}

module Example where

data D a = D a
newtype N = N (forall a. D a)

example :: (forall a. D a) -> N
example = coerce

but both examples fail with

Example.hs:10:11: error:
    • Couldn't match representation of type ‘a0’ with that of ‘b’
        arising from a use of ‘coerce’
      ‘b’ is a rigid type variable bound by
        the type signature for:
          example :: (forall a. D a) -> forall b. D b
        at Example.hs:10:1-16
    • In the expression: coerce
      In an equation for ‘example’: example = coerce
   |
10 | example = coerce
   |           ^^^^^^

and

Example.hs:11:11: error:
    • Couldn't match representation of type ‘forall a. D a’
                               with that of ‘D a0’
        arising from a use of ‘coerce’
    • In the expression: coerce
      In an equation for ‘example’: example = coerce
   |
11 | example = coerce
   |           ^^^^^^

I’m definitively missing something here but our current solver does not support polytypes either so I think we can safely omit them for now.

Nominal equalities.

The interaction solver only works with representational equalities for now but we could extend it to support nominal equalities in the future. We could then compute given nominal equalities from functional dependencies of given constraints when solving representational and nominal equalities, and emit nominal equalities on unification errors!

@kl0tl kl0tl force-pushed the coercible/interaction-solver branch from 55c1c76 to 5405332 Compare November 15, 2020 00:29
@hdgarrood
Copy link
Copy Markdown
Contributor

At first glance this looks great - the code looks good, especially the comments, and the improvements to the error messages are fantastic. Really nice work! I'm not sure if I am going to be able to find the time to review this properly soon, and this does seem like something we'd want to include in 0.14, so I might say that if, say, a week has passed and there are no outstanding issues we're aware of, then I'll approve and we can merge this and cut a new RC?

@kl0tl
Copy link
Copy Markdown
Member Author

kl0tl commented Nov 17, 2020

This sounds good to me!

Would you prefer that I integrate #3927 into this pull request or to review it separately once this is merged ? I’ve rebased on top of this locally and the diff much nicer! It brings yet another small improvement to error messages that I’d like to ship with the v0.14.0.

Here’s the changes to the CoercibleRepresentational6 and CoercibleRepresentational7 golden tests for instance:

 Error found:
 in module [33mMain[0m
 at tests/purs/failing/CoercibleRepresentational6.purs:8:10 - 8:16 (line 8, column 10 - line 8, column 16)
 
-  No type class instance was found for
-                                
-    Prim.Coerce.Coercible (N a0)
-                          a0    
-                      
+  The newtype constructor [33mN[0m is not in scope, it should be imported to allow coercions to and from its representation.
+
+while solving type class constraint
+[33m                              [0m
+[33m  Prim.Coerce.Coercible (N a0)[0m
+[33m                        a0    [0m
+[33m                              [0m
 while checking that type [33mforall (a :: Type) (b :: Type). Coercible @Type a b => a -> b[0m
   is at least as general as type [33mN a0 -> a0[0m
 while checking that expression [33mcoerce[0m
   has type [33mN a0 -> a0[0m
 in value declaration [33munwrap[0m
 
 where [33ma0[0m is a rigid type variable
         bound at (line 8, column 10 - line 8, column 16)

 See https://github.com/purescript/documentation/blob/master/errors/CannotUnwrapHiddenNewtypeConstructor.md for more information,
 or to contribute content related to this error.
 Error found:
 in module [33mMain[0m
 at tests/purs/failing/CoercibleRepresentational7.purs:8:10 - 8:16 (line 8, column 10 - line 8, column 16)
 
-  No type class instance was found for
-                                
-    Prim.Coerce.Coercible (N a0)
-                          a0    
-   
+  The newtype constructor [33mN[0m is not in scope, it should be imported to allow coercions to and from its representation.
+
+while solving type class constraint
+[33m                              [0m
+[33m  Prim.Coerce.Coercible (N a0)[0m
+[33m                        a0    [0m
+[33m                              [0m
 while checking that type [33mforall (a :: Type) (b :: Type). Coercible @Type a b => a -> b[0m
   is at least as general as type [33mN a0 -> a0[0m
 while checking that expression [33mcoerce[0m
   has type [33mN a0 -> a0[0m
 in value declaration [33munwrap[0m
 
 where [33ma0[0m is a rigid type variable
         bound at (line 8, column 10 - line 8, column 16)
 
 See https://github.com/purescript/documentation/blob/master/errors/CannotUnwrapHiddenNewtypeConstructor.md for more information,
 or to contribute content related to this error.

Also I discovered a bug while testing this on the WIP v0.14.0 package set, with purescript/purescript-newtype#22 and some compiler support (this is a good way to test a wide variety of coercions ^^): lookupNewtypeConstructorInScope fails to lookup re-exported newtypes because their names are resolved to their original module of definition, which obviously doesn’t match the re-exporting module.

I have thought of three ways to fix this:

  1. We can forward the Language.PureScript.Sugar.Names.Env.Env that we build during desugaring to lookupNewtypeConstructorInScope and look for the newtype constructor in the exports of the current module imports. This requires adding it to CheckState.
  2. We can modify checkCurrentModuleImports in typeCheckModule and resolve re-exports there, so that given import Module where Module re-exports a newtype N and its constructor from Module.N we would add (_, "Module.N", Explicit [TypeRef _ "N" (Just ["N"])], Nothing) to the current module imports.
  3. We can resolve re-exports earlier in renameInModule, during desugaring. This requires adding a Maybe ModuleName field to import declarations in order to store the source module name.

Do you have any advice, or can you think of some better alternative?

It seems to me that whatever choice we make here we should keep in mind the second point you listed in #3724 (comment). Perhaps if this is straightforward enough we should rather do this? I don’t understand the implications to be honest but I can try with some guidance.

@kl0tl
Copy link
Copy Markdown
Member Author

kl0tl commented Nov 23, 2020

There’s maybe a better way to do this but in the meantime I went with an alternative to 1 and 2: instead of storing the whole names environment in the typechecker monad state or resolving re-exports we can just annotate the current module imports with the types exported from those modules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants