-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fix] Polymorphic field typechecking #1872
Conversation
Using a polymorphic type annotation on a record field, as in `{id : forall a. a -> a = std.function.id}` would unexpectedly file with a failure to unify `forall a. a -> a` with `a -> a`, which indicates that the type annotation wasn't properly instantiated before being used to check the value of the field. It turns out the typechecking of the field per se is fine, but the issue comes from the recursive environment: in a recursive record, we need to build an environment with types for all record fields _before_ we can start to actually typecheck each field. When the field isn't annotated, we use a fresh unification variable. When typechecking the corresponding field, we unify what's in the type environment (the unification variable, but which might have been unified with another type at this point) with the type inferred for the field. However, when the field has a type annotation, we use this annotation for the recursive environment instead (which is important to keep polymorphic types...polymorphic). In this case, we would unify a polymorphic type with the inferred type, which is the place that was missing the instantiation. One possibility is to just add the missing instantiation, but in fact this unification is useless when there is a type annotation: to avoid this useless work, we rather remember which fields are annotated, and we entirely skip the unification for them.
@@ -2857,7 +2900,8 @@ fn instantiate_foralls( | |||
.. | |||
} = ty | |||
{ | |||
let kind = (&var_kind).into(); | |||
let kind: VarKindDiscriminant = (&var_kind).into(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cosmetic change: I find it better to put an explicit type annotation when there's an into()
that isn't used right away.
// | ||
// As this function can be called on monomorphic types, we only increment the level when we | ||
// really introduce a new block of rigid type variables. | ||
if matches!( | ||
ty, | ||
UnifType::Concrete { | ||
typ: TypeF::Forall { .. }, | ||
.. | ||
} | ||
) { | ||
ctxt.var_level.incr(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated small change that was on my radar for some time. It doesn't change anything with respect to correctness (the typechecker was already correct before), but would just introduce more levels that necessary.
Closes #1690
Using a polymorphic type annotation on a record field, as in
{id : forall a. a -> a = std.function.id}
would unexpectedly fail with a failure to unifyforall a. a -> a
witha -> a
, which indicates that the type annotation wasn't properly instantiated before being used to check the value of the field.It turns out the typechecking of the field per se is fine, but the issue comes from the recursive environment: in a recursive record, we need to build an environment with the types of all record fields before we can start to actually typecheck each field. When the field isn't annotated, we use a fresh unification variable. When typechecking the corresponding field, we unify what's in the type environment (the unification variable, but which might have been unified with another type at this point) with the type inferred for the field. Doing so, we reject things like
{a = true, b = a + 1} : _
.However, when the field has a type annotation, we use this annotation for the recursive environment instead (which is important e.g. to keep polymorphic types...polymorphic). In this case, the additional unification step described above would unify a polymorphic type with the inferred type, which is the place that was missing the instantiation.
One possibility is to just add the missing instantiation, but in fact this unification is useless when there is a type annotation, as the typechecking of the field with a polymorphic annotation will perform the same work anyway (infer a type and check that it's a subtype of the instantiated annotation). We thus rather remember which fields are annotated and we entirely skip the unification in this case.