Check roles of mutually recursive types#3860
Conversation
| TypeConstructor _ t1Name -> | ||
| let t1Roles = inferRoles env t1Name | ||
| let roles = M.lookup t1Name roleEnv' | ||
| inferred = fromMaybe (repeat Phantom) roles |
There was a problem hiding this comment.
In a couple of places you use Phantom as a default for a lookup. Can you clarify why this is desirable?
There was a problem hiding this comment.
The phantom role is used as a default because it is the least restrictive, so later inference passes can only refine it to a more precise role. We always prefer the more restrictive role inferred for the same parameter in the Semigroup instance for RoleMap and updateRoles.
For instance, given these definitions:
data D1 a = D1 a
data D2 a = D2 (D1 a)When inferring D2 roles we discover they depend on those of D1, but we don’t know them yet so we pick phantom for a and store that in our role environment. Then we infer the type parameter of D1 is representational, store that in our role environment and go back to D2. This time we know D1 roles so we can infer a is representational and update the role environment by taking the more restrictive of phantom and representational for a, which is representational.
3c9a777 to
3d55d09
Compare
|
Thanks for picking this up! While reviewing, I realised that we have another bug in Coercible (#3867), and I think it might be preferable to address that one before this one, as that one might necessitate a more fundamental change in the mechanics of how we handle roles in the compiler. |
Removing the approval while we decide how to proceed.
|
I managed to rebase this on top of #3873! I was able to follow GHC approach more closely now that we don’t have to discover dependencies between mutually recursive as we go anymore. I feared my previous approach wouldn’t propagate roles in some intricate cases but didn’t found any example of that. I find reassuring to do somewhat like GHC though. |
|
@kl0tl I'd like to try to get this PR merged next. Could you merge or rebase master so that it applies cleanly when you have a moment, please? |
|
I rebased the PR. |
hdgarrood
left a comment
There was a problem hiding this comment.
Looks great so far, thanks!
| Nothing -> | ||
| pure $ fromMaybe (Phantom <$ tyArgs) inferredRoles | ||
|
|
||
| inferDataBindingGroupRoles |
There was a problem hiding this comment.
Rather than keeping this function "pure", I think it might be preferable (or at least better aligned with the way the rest of the type checker is written) for it to operate in MonadState CheckState m, and read and write directly from the actual type-checking environment, rather than doing its own thing at first and then later updating the environment after inferring/checking roles. I don't feel particularly strongly about this, though. @natefaubion do you have any thoughts on this?
There was a problem hiding this comment.
I don't really have much of an opinion. Is there some currying advantage to how the functions are arranged, and how it returns an additional lambda, that's exploited elsewhere?
There was a problem hiding this comment.
The explicit floating of initialRoleEnv and inferredRoleEnv outside of the do block in checkDataBindingGroupRoles is supposed to allow the roles of the data declarations in the binding group to be inferred only once and then looked up repeatedly in the DataBindingGroupDeclaration case of Language.PureScript.TypeChecker.typeCheckAll. This isn’t something I’ve verified because I’m not familiar with Core inspection but I’m not sure GHC would be able to share the work otherwise.
I can try to store the inferred roles environment in the typechecker monad state and separate roles inference from roles checking in binding groups. This should be moree performant than computing a new roles map from scratch each time we need to check roles.
There was a problem hiding this comment.
Since neither @natefaubion nor I feel strongly I’m tempted to say let’s leave this as is for now. I’m happy to merge once there’s a comment about what the Any in inferDataDeclarationRoles means.
There was a problem hiding this comment.
Ah, I guess it’s that we want to keep walking over all of the data declarations in the binding group until we reach a point where walking doesn’t change anything in the roleEnv, and the Any part of the result indicates whether something changed?
There was a problem hiding this comment.
I lifted the role inference to the typechecker monad!
| then inferDataBindingGroupRoles moduleName group roleEnv' | ||
| else roleEnv' | ||
|
|
||
| inferDataDeclarationRoles |
There was a problem hiding this comment.
Could you add some documentation about the meaning of the Any in the return type please?
There was a problem hiding this comment.
I added a comment and aliased the Any to DidRolesChange to make things explicit.
522d6a1 to
bf2c450
Compare
|
@kl0tl Thanks for making these changes! On reflection, though, I'm less sure about this design. With this branch as it is now, we are storing roles in three separate places in the CheckState: in the CheckState's Speaking of minimising the amount of mutable state we have, it occurred to me that now we are dealing with data binding groups sensibly, we can probably make the I'm not actually very keen on |
|
I’ve rollback the lifting of the role inference state to the typechecker monad and the I don’t think we can get away with storing roles only in the types field of the environment because we need to infer the roles of a data type before inserting it in the environment and we need to store the role declarations separately in order to check they’re not less restrictive than the inferred roles but we can discuss this in another pull request. |
| _ -> | ||
| [] | ||
| lookupRoles env tyName = | ||
| fromMaybe [] $ M.lookup tyName (getRoleEnv env) |
There was a problem hiding this comment.
This change seems a little excessive. Whenever you want to lookup a role, instead of looking it up directly, you are mapping over the entire type environment to build another environment that is a subset of the original. I understand that there may be some clever laziness involved (it isn't clear to me that you are getting that with Map and mapMaybe vs something like List), but this seems like an easy way to also accidentally introduce poor asymptotics. What was the motivation for this change? If it's only to share code, I think you could extract out the predicate for reuse rather than try to reuse getRoleEnv directly.
There was a problem hiding this comment.
I wanted to share logic and thought the whole map wouldn’t actually be built thanks to laziness but I should have verified this assumption. I’ve reused a typeKindRoles function in both getRoleEnv and lookupRoles.
|
🎉 |
Here’s a tentative to fix #3857.
This turns role inference into a stateful process repeatedly updating roles in the environment until we infer the roles of all the data types belonging to a mutually recursive group. The idea is taken from GHC own implementation (see
GHC.Tc.TyCl.Utils.inferRoles).Unlike GHC we discover mutually recursive data types as we go so an inference step returns
S.Set (Qualified (ProperName 'TypeName)instead ofBooland we infer our dependencies roles before taking another step, stopping when we don’t find any dependencies.