Skip to content

Check role declarations against inferred roles#3873

Merged
hdgarrood merged 9 commits intopurescript:masterfrom
hdgarrood:check-role-annotations
Jun 25, 2020
Merged

Check role declarations against inferred roles#3873
hdgarrood merged 9 commits intopurescript:masterfrom
hdgarrood:check-role-annotations

Conversation

@hdgarrood
Copy link
Copy Markdown
Contributor

Fixes #3867. The main change here is that we are now storing checked
roles in the types field of the Environment for data and
foreign import data declarations, via the additions to the TypeKind
type. Because we are storing roles in the environment, we no longer need
to infer roles for any given type more than once. As such, checkRoles
now works in a monad with MonadState CheckState, and when we come
across another type while inferring roles, we can simply look its roles
up from the Environment. The BindingGroups desugaring step ensures
that types are in topological order, so this is safe: any mentioned
types' roles will already have been checked and stored in the
Environment.

One case that we do not yet handle is types which cyclically depend on
each other, e.g.

data F a = F (G a)
data G a = G (F a)

Rather than crashing the compiler, these will now be incorrectly marked
Phantom. I would like to address this in a future PR.

Fixes purescript#3867. The main change here is that we are now storing checked
roles in the `types` field of the Environment for `data` and
`foreign import data` declarations, via the additions to the `TypeKind`
type. Because we are storing roles in the environment, we no longer need
to infer roles for any given type more than once. As such, `checkRoles`
now works in a monad with `MonadState CheckState`, and when we come
across another type while inferring roles, we can simply look its roles
up from the Environment. The BindingGroups desugaring step ensures
that types are in topological order, so this is safe: any mentioned
types' roles will already have been checked and stored in the
Environment.

One case that we do not yet handle is types which cyclically depend on
each other, e.g.

   data F a = F (G a)
   data G a = G (F a)

Rather than crashing the compiler, these will now be incorrectly marked
Phantom. I would like to address this in a future PR.
@hdgarrood hdgarrood requested a review from natefaubion May 13, 2020 21:46
@hdgarrood
Copy link
Copy Markdown
Contributor Author

@kl0tl would you like to review this? I think the approach I've taken here should allow us to take advantage of existing parts of the compiler -- specifically the BindingGroups desugaring pass and the type checking Environment -- to make this a bit more performant and robust. Starting from what I have here, my hope is that handling role inference for recursive types should be relatively straightforward, since we can take advantage of the BindingGroups pass, but it will end up looking a bit different from how you did it in your other PR.

@hdgarrood
Copy link
Copy Markdown
Contributor Author

I'm also not bothering to deal with type synonyms or constrained types in data declarations yet, and that's also on purpose - I'd prefer to address these things incrementally.

Copy link
Copy Markdown
Member

@kl0tl kl0tl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great!

Is there a good reason for giving phantom roles to prim types and kinds? I would’ve picked nominal. I guess it doesn’t matter much since Coercible isn’t polykinded but it allows one to coerce Row Type and RowList Type to Row Symbol and RowList Symbol for instance, which doesn’t make sense to me.

@hdgarrood
Copy link
Copy Markdown
Contributor Author

Is there a good reason for giving phantom roles to prim types and kinds? I would’ve picked nominal.

My general approach was that I was trying to give the same roles as would have been inferred. We've got:

  • Row as phantom -- I think this is fine because the type Row a has no inhabitants. This won't allow coercing e.g. Record (a :: Int) to Record (b :: String), because we only care about the types of fields when inferring roles, not their kinds.
  • Function as representational representational -- I'm pretty confident about this one, because it's what the paper says, and we do want to be able to coerce e.g. Int -> Int to Age -> Age, but not to Int -> String.
  • Array as representational -- this is an easy one, I'm pretty confident about this one
  • Record as representational -- we want to ensure that all of the fields are representationally equal. It occurs to me that the role inference might be wrong for rows, actually, because I don't think it looks at the labels right now, but the labels clearly do affect the representation.
  • RowList as phantom -- like Row, I think this is fine because RowList a has no inhabitants.
  • Cons has all of its parameters as phantom because its parameters definitely can't affect its representation, because it has none, since it's not even of kind Type once it has had all its parameters supplied.
  • All the Prim.TypeError types as phantom, again because they have no inhabitants. I think you're right that the classes should be nominal though.

@kl0tl
Copy link
Copy Markdown
Member

kl0tl commented May 14, 2020

I don’t think the roles picked cause any issues but type role Row phantom and type role RowList phantom mean that the constraints Coercible (Row Type) (Row Symbol) and Coercible (RowList Type) (RowList Symbol) hold (between kinds 🙃) and those axioms feel unnecessary to me unless we gain something by assuming them.

@hdgarrood
Copy link
Copy Markdown
Contributor Author

Admittedly I'm unable to come up with a situation in which those dictionaries are useful to have, but it seems reasonable to me that you could e.g. coerce an Either (Row Type) Int to an Either (Row Symbol) Int. If you look at Row and RowList as versions of Proxy but without any constructors (and with a little bit of compiler support), I think it makes sense?

@kl0tl
Copy link
Copy Markdown
Member

kl0tl commented May 16, 2020

I guess you‘re right. This still feels weird to me but that‘s definitely not a strong enough argument against phantom roled Row and RowList 😄

Comment on lines +640 to +643
TypeApp _ (TypeApp _ fn k1) _k2 | eqType fn tyFunction ->
go (Nominal : acc) k1
_k ->
Nominal : acc
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should recurse on the second argument of -> and return the accumulator untouched in the base case. Otherwise we return [Nominal] for Type, [Nominal, Nominal] for Type -> Type and Type -> Type -> … -> Type, [Nominal, Nominal, Nominal] for (Type -> Type) -> Type

module Example where

import Safe.Coerce (coerce)

foreign import data Foreign3 :: Type -> Type -> Type -> Type

-- We infer `nominal nominal` instead of `nominal nominal nominal`,
-- this is equivalent to giving a phantom role to `Foreign3` last parameter:

example ::  a b c d. Foreign3 a b c -> Foreign3 a b d
example = coerce

Also now that we have kind polymorphism we should recurse under ForAll:

module Example where

import Safe.Coerce (coerce)

foreign import data Foreign2 ::  k. k -> k -> Type

-- We infer `nominal` instead of `nominal nominal`,
-- this is equivalent to giving a phantom role to `Foreign2` last parameter:

example ::  k (a :: k) (b :: k) (c :: k). Foreign2 a b -> Foreign2 a c
example = coerce

I understand this function was just renamed and extracted from Language.PureScript.TypeChecker.Roles so I can open an issue and deal with this in another pull request if you so wish.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dealing with this in another pull request sounds preferable to me. Thanks!

@hdgarrood
Copy link
Copy Markdown
Contributor Author

Thanks @natefaubion, @kl0tl!

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.

Explicit roles may be more permissive than inferred

3 participants