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

Explanation about bidirectionality hints #71

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Lysxia
Copy link

@Lysxia Lysxia commented Nov 3, 2024

I think it's an explanation rather than a tutorial but I'll be happy to defer to you if you think otherwise.

@thomas-lamiaux
Copy link
Collaborator

I am going to ask @MevenBertrand to review as he is a specialist of bidirectional typechecking.
He is quite busy at the moment, so I am not sure when he'll have the time.
It is short so hopefully, it should be soon-ish

Copy link
Collaborator

@thomas-lamiaux thomas-lamiaux left a comment

Choose a reason for hiding this comment

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

Thanks very much for writing it. It is a great addition on sth I know nothing of. Actually, I was not even aware of it.

I am reviewing it as an non-expert reviewers, so I tried to point out stuff that got me a bit confusing while reading.

Globally, I thought part 1 to 3 was a really good intro to the subject.

Concerning section 4, it depends on what you/we want to go with this.
As you mentioned, right now, it is more an explanation than a tutorial, because it just explains how it works and that it is.
I personally think it is a bit too bad, because at the end, you understand what are "bidirectional hints" but you have no idea when and how to use them.
That would be great if it were possible to modify the part 4 to help the user understand that.However, I have no idea how to do that, so I can't say much.

What do you think ? Note if no one has an example in mind, it can perfectly be merged like that, in which can we can create an issue to improve it latter.

src/Explanation_Bidirectionality_Hints.v Show resolved Hide resolved

To start, we must understand the basics of how type checking works in Coq.

** Bidirectional typing
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
** Bidirectional typing
** 1. Bidirectional typing

typing rules: a bidirectional type system is a way to present a type checking
and a type inference algorithm.

Function application is usually associated with a rule for type inference ([↑]):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe recall the usual type checking rule and explain how it is transformed ?

- 2. check the arguments [a], [b] with their respective types [A], [B a];
- 3. unify [C a] and [T].

** Existential variables
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
** Existential variables
** 2. Bidirectional Typechecking and Existential variables

*** Contents

- 1. Bidirectional typing
- 2. Existential variables
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- 2. Existential variables
- 2. Bidirectional Typechecking and Existential variables

Bidirectionality hints are declared using the [Arguments] command,
as a [&] symbol. For example:
[[
Arguments f _ & _.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is linked to applications being n-ary in Coq.
It is not mentioned, maybe it is worth saying somewhere ?

]]]
*)

(** ** Examples *)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
(** ** Examples *)
(** ** 4. Examples *)

Comment on lines +161 to +162
- 2. check the second argument [(0 : B false) ↓ B ?x]: the type annotation gives us the inferred type `B false`,
which is unified with the checked type `B ?x`, and we unify `false` with `?x`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure what happens here but it sounds a bit weird to me, because it seems to say it uses injectivity of B ?

Copy link
Contributor

Choose a reason for hiding this comment

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

unification uses the first-order unification heuristic ie avoids unfolding B when unifying its arguments is enough to succeed

Copy link
Contributor

Choose a reason for hiding this comment

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

In other words: unification in Coq is lossy, and does not generally find most general unifiers, this is a typical example of that. Maybe it would make sense to mention it here?


(** With no bidirectionality hint:
- 1. check the first argument [?x ↓ bool] (same as before, do nothing);
- 2. check the second argument [0 ↓ B ?x]: it is a constant so it has an inferred type `nat`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- 2. check the second argument [0 ↓ B ?x]: it is a constant so it has an inferred type `nat`,
- 2. check the second argument [0 ↓ B ?x]: [0] is a constant so its type can be inferred, here to `nat`,

(** With the bidirectionality hint:
- 1. check the first argument [?x ↓ bool] (same as before, do nothing);
- 2. unify [C ?x ≡ C false], which unifies [?x ≡ false];
- 3. check the second argument [0 ↓ B ?x]: it has an inferred type `nat`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- 3. check the second argument [0 ↓ B ?x]: it has an inferred type `nat`,
- 3. check the second argument [0 ↓ B ?x]: its type is inferred to `nat`,

@Lysxia
Copy link
Author

Lysxia commented Nov 4, 2024

As you mentioned, right now, it is more an explanation than a tutorial, because it just explains how it works and that it is.
I personally think it is a bit too bad, because at the end, you understand what are "bidirectional hints" but you have no idea when and how to use them.

I agree completely. As it is, this explanation works best for people who already have a little idea of what the feature does.

That would be great if it were possible to modify the part 4 to help the user understand that. However, I have no idea how to do that, so I can't say much.

I remember one common situation that calls for bidirectional hints is with dependently typed records. I can add a paragraph about that, maybe even lead with it, but it might not be a deep example that turns it into a tutorial.

@thomas-lamiaux
Copy link
Collaborator

You can also ask on zulip for examples, maybe someone has ideas.
Also I forgot to mention it but it should be said not to mess with such hints if you are a beginner.
I have never needed them, and I don't think too much people do ?

{{https://coq.inria.fr/doc/V8.20.0/refman/language/extensions/evars.html} existential variables}.
To avoid fully spelling out all terms in Coq, you can write
holes ([_]) instead to let them be inferred. They are replaced with fresh
existential variables with names like [?u] right before type checking.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would say during pretyping, not right before anything (I'm not sure if what you call type checking is meant to be pretyping or the kernel check, for this sentence the kernel doesn't see evars so I guess it can't be the kernel check)


(** We replace the hole in the first argument with a fresh existential variable [?x].
With no bidirectionality hint, type checking [f _ (0 : B false) ↓ nat] proceeds as follows:
- 1. check the first argument [?x ↓ bool]: it is a hole so checking succeeds without doing anything;
Copy link
Contributor

Choose a reason for hiding this comment

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

as I said holes are turned into evars during pretyping not before it, so we actually do check _ bool ==> ?x : bool generating a fresh evar


[[[
f ↑ forall (x : A), B x -> C x
a ↓ A C a ≡ T b ↓ B a
Copy link
Contributor

Choose a reason for hiding this comment

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

there is a subtlety here
after a ↓ A we know that f a : forall y : B a, C a
we could in principle check that y is not bound in C a before trying to unify C a and T
however in pretyping it is rare that we directly call "unify a and b", instead we usually want "coerce c : a to b"
this cannot be done without having a term c
(in fact it is not really right to write c ↓ T, it should really be something like "check input preterm c at input type T producing output term c'", similarly infer has an input preterm and output term and type)

so what we do is, regardless of if y is bound in the codomain:

  • generate a fresh evar ?y : B a
  • coerce f a ?y : C a (where C a is the result of substituting y := ?y inC a) to T (which first tries unifying C a to T and if that fails tries to find a coercion to insert)
    note that if y was bound in C a this could instantiate ?y
  • check b ↓ B a
  • unify ?y and b (this time it really is unification not coercion)
  • produce term f a b : C a (which is more faithful to what the user wrote than f a ?y if coercing to T instantiated ?y, but the C a is still from substituting ?y not b) and replay any coercions to T

@thiagofelicissimo
Copy link

Three comments from someone knowledgeable in bidirectional typing but not on bidirectionality hints:

  • It is a bit strange to present rule app-syn with a double application, and moreover you are also omitting the fact that some reduction is usually required after inferring the type of the head to expose the type former. Maybe what you could do is give the usual bidirectional rule for application and then say that your rule is the result of applying it twice to the term f a b.
  • In rule switch, I think you should replace conversion by subtyping.
  • In the Coq literature it is more usual to write t ▹ T for inference and t ◃ T for checking.

@MevenBertrand MevenBertrand self-requested a review December 20, 2024 13:18
Copy link
Contributor

@MevenBertrand MevenBertrand left a comment

Choose a reason for hiding this comment

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

Sorry for taking this long! All in all, the explanation is short but good, and does what it should, nice job 🚀

I only wonder whether it would be possible to have a slightly more "realistic" example, ie one where adding a bidirectionality hint actually helps in setting things up properly. Looking up "bidirectionality hints" on the zulip gives a handful of examples, maybe one of them can be distilled as an example 3?

src/Explanation_Bidirectionality_Hints.v Show resolved Hide resolved
[[[
f ↑ forall (x : A), B x -> C x
a ↓ A b ↓ B a
----------------- app-syn
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Thiago: showing the rule with two arguments seems a bit weird at this stage? I would present the rule with just one argument first, and the derived rule later (maybe only in the example section?)

To avoid fully spelling out all terms in Coq, you can write
holes ([_]) instead to let them be inferred. They are replaced with fresh
existential variables with names like [?u] right before type checking.
Existential variables will then be instantiated during unification [?u ≡ T].
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Existential variables will then be instantiated during unification [?u ≡ T].
Existential variables will then be instantiated during unification:
when a unification problem such as [?u ≡ T] is encountered, Coq will
use this information to solve the variable [?u].

Comment on lines +161 to +162
- 2. check the second argument [(0 : B false) ↓ B ?x]: the type annotation gives us the inferred type `B false`,
which is unified with the checked type `B ?x`, and we unify `false` with `?x`;
Copy link
Contributor

Choose a reason for hiding this comment

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

In other words: unification in Coq is lossy, and does not generally find most general unifiers, this is a typical example of that. Maybe it would make sense to mention it here?

** Existential variables

To a first approximation, the unification judgement ([T’ ≡ T]) denotes
an equivalence relation between types.
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe mention that there's actually some subtyping involved? I guess there's a balance between being too scary/complex and not hiding the "reality" though, so maybe this is not necessary.

@thomas-lamiaux
Copy link
Collaborator

@Lysxia Considering the comments, I suggest fixing them quickly, merging and opening an issue to add an example later on

@thomas-lamiaux
Copy link
Collaborator

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.

5 participants