Skip to content
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

Merged
merged 1 commit into from
Mar 28, 2024
Merged

Conversation

yannham
Copy link
Member

@yannham yannham commented Mar 27, 2024

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 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 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.

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.
@yannham yannham requested review from jneem and vkleen March 27, 2024 18:06
@@ -2857,7 +2900,8 @@ fn instantiate_foralls(
..
} = ty
{
let kind = (&var_kind).into();
let kind: VarKindDiscriminant = (&var_kind).into();
Copy link
Member Author

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.

Comment on lines +2881 to +2892
//
// 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();
}
Copy link
Member Author

@yannham yannham Mar 27, 2024

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.

@github-actions github-actions bot temporarily deployed to pull request March 27, 2024 18:09 Inactive
@yannham yannham added this pull request to the merge queue Mar 28, 2024
Merged via the queue into master with commit d0459a6 Mar 28, 2024
5 checks passed
@yannham yannham deleted the fix/polymorphic-field-annot branch March 28, 2024 08:51
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.

Typechecking issues with polymorphic types annotations on record fields
2 participants