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
Add explanation for errors caused by a type constraint propagated from a keyword #1510
Conversation
cf803a1
to
3b3ddc7
Compare
bool because it is the condition of a if-statement | ||
|}];; | ||
|
||
fun b -> if true then (if b then ()) else (print_int b);; |
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.
You should explicit point out that this is a case where the refined message doesn't show up because of propagation order.
typing/typecore.ml
Outdated
let loc = sexp.pexp_loc in | ||
(* Record the expression type before unifying it with the expected type *) | ||
let unify_with_explanation = unify_with_explanation explanation in |
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.
This should be with_explanation
typing/typecore.ml
Outdated
|
||
and type_expect_ | ||
?in_function ?(recarg=Rejected) ?explanation | ||
env sexp ty_expected = |
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.
Instead of a separate explanation
argument, have you considered carrying the information within ty_expected
, turning it into a pair of a type and an optional explanation?
Some more comments on the design choices: The Arthur's implementation is careful to delay the unification of the expected type until the last possible moment. This implementation does not do it, which means that in some case the explanation will not be used while it should be. If you write The reasoning for having this feature even though the propagation order makes it more fragile than with Arthur's approach is:
Finally: Armaël commented above that the explanation is not reused for recursive calls to the type-checker to a different expected type. Of course! The context of those sub-calls is different so it would be incorrect to reuse the explanation. The question is rather of why it is correct to reuse the explanation for the sub-calls with the same expected types. For example, if you have a program of the form In the case of moving from This is obvious for if- and while-conditions of for-bounds for examples, which are always very small expressions anyway. It is less clear for the context while cond do
match foo with
| K1 -> ()
| K2 -> 2
| K3 -> ()
done then |
e0ea009
to
597bc79
Compare
@@ -49,7 +66,7 @@ val check_partial: | |||
Location.t -> Typedtree.case list -> Typedtree.partial | |||
val type_expect: | |||
?in_function:(Location.t * type_expr) -> | |||
Env.t -> Parsetree.expression -> type_expr -> Typedtree.expression | |||
Env.t -> Parsetree.expression -> type_expected -> Typedtree.expression |
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.
Wouldn't it be lighter to pass the explanation as an (optional) extra argument, instead of wrapping it in a record with the expected type?
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.
Well, that's what I was doing at first, but then @gasche suggested that I attach the explanation to the expected type...
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.
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.
Ah ok, sorry, I did not see @gasche 's comment. I've a preference for the old version, but I don't mind if the new one is preferred. If we go for the record solution: (i) would it make sense to include in_function in it? (ii) perhaps use a function to build the record instead of using literal; it will simplify adding more optional fields later if needed.
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.
My intuition is that we will more towards more and more structured representations of the bidirectional information propagated during type-checking, and that having it packed in one value is the right way for nice code evolution.
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.
I just pushed a commit that provides a function to build the record as @alainfrisch suggested.
I wonder if, in general, these messages talking about syntax will end up being undesirable in generated code. I suppose they can be disabled by replacing [if e1 then e2] by [if e1 then e2 else ()], or [while b do e done] by [while (b : bool) do e; (); done] etc, so it's not too bad. But with the rec one, the hints are going to be weird (not tested with an actual ppx, but the result should be the same), since it will say "you are missing the rec keyword" on a line that says type sexp
type t (* forgot [@@deriving sexp_of] *)
module M = struct
type nonrec t = t [@@deriving sexp_of]
let sexp_of_t : sexp -> t = sexp_of_t
end
Error: Unbound value sexp_of_t
Hint: You are probably missing the `rec' keyword on line 7. |
One way of addressing that issue would be to add a |
It's clearly not worth adding syntax for my concern. I suppose the |
I had forgotten to add an explanation for the left-hand side of a sequence. The last commit implements this. |
I tweaked the formatting to insert a break hint before the "because it is ..." explanation. This is probably better that no break hint; however now the messages in the tests look a bit weird, because of the staggering produced by the structural hovbox that contains the messages. |
typing/typecore.ml
Outdated
@@ -3163,7 +3164,8 @@ and type_expect_ | |||
exp_env = env } | |||
end | |||
| Pexp_sequence(sexp1, sexp2) -> | |||
let exp1 = type_statement env sexp1 in | |||
let exp1 = type_statement ~explanation:Sequence_left_hand_side | |||
env sexp1 in |
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.
I would have expected a test for -strict-sequence
to show up in the patch. Where does this happen?
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.
It doesn't appear in this patch because it is already present in type_statement
which was modified previously (loop bodies are handled in exactly the same way as the lhs of a sequence).
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.
Thanks!
Isn't it one of those cases where we should check if the associated expression is ghost before using it as an explanation ? |
@Drup: we agree with that proposal, thanks! @Armael plans to implement it. @alainfrisch would you be willing to "drive" this issue, and in particular approve it if you do approve of the proposed change? (I have handled previous change proposals by Armaël but I'm basically a co-author of this one so I don't think it would be best.) |
I only looked superficially to the implementation, and don't have any opinion on the proposal itself. I think @gasche would do a better job here (as often, and unfortunately for him). |
I added a commit that (I think) implements @Drup suggestion. |
(I believe this commit is correct as well.) The search for a voluntary review goes on: @sliquister, @Drup, would one of you be willing to review this patch and formally "approve" it if you believe it is correct? |
( @let-def, maybe you would be interested in reviewing this as well? ) |
I am wondering if let f g = g ?x:1 ();;
|
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.
The implementation looks good to me.
I don't have a strong opinion about the effect on end-users, though I believe it is a improvement.
typing/typecore.mli
Outdated
type type_expected = private { | ||
ty: type_expr; | ||
explanation: type_forcing_context option; | ||
} |
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.
For future reference, would you mind adding a small comment to explain the point of type_forcing_context
and type_expected
?
At least that type_forcing_context
is not affecting type checking itself but is used for generating error messages.
Line _, characters 3-4: | ||
Error: This expression has type int but an expression was expected of type | ||
bool | ||
because it is in the condition of a if-statement |
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.
an if statement
?
@let-def : thanks for your comments, I just added commits that address them (I think).
@Octachron : maybe -- though after a quick glance at the existing code, supporting that doesn't seem to be an simple extension of the current patch. So I think it better belongs in a separate PR. |
There is a minor typo your last commit message: cnotext -> context. (I wonder if your comments shouldn't go in the .mli rather the .ml. I think that both are fine, but maybe you could manage to start a trend of actually, you know, documenting interfaces, and that would be Big!) |
c96053d
to
b2e7c75
Compare
Good point. I amended the last commit and put the comments in the .mli rather than in the .ml. |
Thanks for having a look, I was just asking in case it was an one-liner extension. |
b2e7c75
to
73d4e8b
Compare
I rebased on top of trunk, to benefit from the super awesome error highlighting in expect tests. |
Thanks for the approval @let-def ! Should I rebase on top of trunk and clean-up the history now? |
Yes. (I didn't ping explicitly as I didn't know how much OCaml you wanted during your holidays.) |
73d4e8b
to
55624d7
Compare
Rebased. |
Another change extracted from PR #102 . This adds specific explanation for type errors that involve type information propagated by a keyword such as if, for, while...
For example,
highlights "42" and prints:
The idea for this patch comes from #102 but the implementation proposed here is different. The original patch changed the order of propagation in order to get more precise information -- but this doesn't interact well with non-principality AFAIU. The approach followed in this patch is as follows: in the
type_expect
function, we add an "explanation" optional argument. If unification fails between something and the expected typety_expected
, then this explanation (if present) explains why this type was expected (e.g. "because it was the body of a for-loop"). Whentype_expect
calls itself recursively with a different expected type, the explanation is dropped.