Skip to content

Improve the type clash error message#12182

Merged
Octachron merged 5 commits into
ocaml:trunkfrom
Julow:print-expr-type-clash
Jul 18, 2024
Merged

Improve the type clash error message#12182
Octachron merged 5 commits into
ocaml:trunkfrom
Julow:print-expr-type-clash

Conversation

@Julow

@Julow Julow commented Apr 13, 2023

Copy link
Copy Markdown
Contributor

Related: #12152

The second commit re-use the mechanism implemented in #11679 for type clash errors:

Error: The expression '43' has type int
       but an expression was expected of type int32

The third commit change the denomination for some expressions instead of the generic "expression".
For example:

The constant '42' has type ...

This field has type ...

This is similar to the previous PR (#11679):

The function 'foo' has type ...

This could be extended to most expressions (eg. object, unnamed function, tuple, match expression, etc..).

@Julow

Julow commented Apr 13, 2023

Copy link
Copy Markdown
Contributor Author

This is request for comment. I don't mind dropping the third commit or giving up the entire idea.

@gasche

gasche commented Apr 13, 2023

Copy link
Copy Markdown
Member

Interesting, thanks!

I think that I disagree with some of the category names. For example "The method x#m" reads wrong to me, x#m is a method call, m is the method. I suspect that I don't like "record field" for the same reason, but I wasn't able to find an example in the testsuite to make an opinion. Could you ensure that all special cases are covered by at least one test?

@Julow

Julow commented Apr 14, 2023

Copy link
Copy Markdown
Contributor Author

A more correct denomination might be "method call", "record field access". I don't think length is a problem at this point.

Perhaps the message could print "The method 'm' has type" and remove the x# part, that could not be nominal in some cases ?

Line 3, characters 18-22:
3 | let _ = print_int Cons
^^^^
Error: This expression has type v but an expression was expected of type int

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's a bug here.

Comment thread Changes Outdated
Comment on lines +20 to +25
- #12182: Improve the type clash error message.
For example, this message:
This expression has type ...
is changed into:
The constant '42' has type ...
(Jules Aguillon, review by Gabriel Scherer)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The change entry is perhaps too long ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fine with me. I like descriptive change entries.

@Octachron

Copy link
Copy Markdown
Member

I believe that using the method 'm' or the field 'f' doesn't work because both don't have a type on their own. Consider for instance:

type ('a,'b,'c) t = { x:'a; y:'b; z:'c }
let f r = let a = r.z (1 + r.x) ("y" ^ r.y) in [None; r.z]

It would be confusing to state that

Error: The field 'z' has type int -> string -> 'a
       but an expression was expected of type 'b option

when there is no such constraints on the field z in general.

@gasche

gasche commented Apr 8, 2024

Copy link
Copy Markdown
Member

Do we want to revisit this or close it?

@Octachron

Copy link
Copy Markdown
Member

Overall, I like the change; and if @Julow has the time I would be happy to revisit it.

@Julow

Julow commented Apr 8, 2024

Copy link
Copy Markdown
Contributor Author

Hi, I would be happy to revisit it. I also don't mind if this is closed even after I put some time into it.

@Julow

Julow commented Apr 8, 2024

Copy link
Copy Markdown
Contributor Author

I believe that using the method 'm' or the field 'f' doesn't work because both don't have a type on their own. Consider for instance:

To anchor the error message in the context, we could say "the method 'x#m'" or "the field 'r.f'" ?

@Octachron

Copy link
Copy Markdown
Member

Possibly, but then we have the issue that the field r.f reads a bit strange when the field is f.
Another option would be to restructure the error message from

"%a has type" "The field f"

to

"In the record projection* `r.f`, the field `f` has type" (* projection => access? *)

(which also has the advantage of being localisable)

Comment thread typing/typecore.ml Outdated
@Julow Julow force-pushed the print-expr-type-clash branch from b127ccc to c78123e Compare April 11, 2024 15:14
@Julow

Julow commented Apr 11, 2024

Copy link
Copy Markdown
Contributor Author

With the last commits, a denomination is used for multi-keyword expressions like for, while, if, match.
It's a useful confirmation that the error is about the containing expression rather than a nested one, for which the printed code is hard to read when spanning several lines. There's no example for some of these but this would become useful if other messages use the new mechanism, notably Apply_non_function and Not_a_function.

What do you think of applying this change to the other messages starting in "This expression" ?

I worry that adding an intro to the message makes reading harder as it's of irregular length. What do you think of these:
Simpler and still localisable: (currently implemented in this branch)

The field access "r.f" has type ...

More correct:

The field "f" has type ...
in the context of the field access "r.f"

Perhaps more uses of the context could be found ?

Line 1, characters 14-36:
1 | let _ : int = while false do () done
^^^^^^^^^^^^^^^^^^^^^^
Error: This while expression has type "unit"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We want inline code quote here is since while is not part of the grammatical structure of the sentence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented!

@Julow Julow force-pushed the print-expr-type-clash branch from 34e668d to f9dd42d Compare April 11, 2024 16:30
@Octachron

Copy link
Copy Markdown
Member

Using field access and method call is fine with me. I think it is better to keep this PR simple, we can think later if we want to add more context.

P.S: Note that those messages are not localizable, the issue is that composing error messages by fragments like %a has type makes the hypothesis that the subject and the rest of the sentence can be translated independently. This may be not be the case if for instance the verb conjugation depends on the subject gender.

Comment thread typing/typecore.ml Outdated
(** [sexp_for_hint] is used by error messages to report literals in their
original formatting *)
let unify_pat ?sdesc_for_hint env pat expected_ty =
let unify_pat ?sexp_for_hint env pat expected_ty =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR is still only using the description for hints in the pattern case. I propose to keep the code unchanged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread typing/typecore.ml Outdated
(** [sexp_for_hint] is used by error messages to report literals in their
original formatting *)
let unify_exp ?sdesc_for_hint env exp expected_ty =
let unify_exp ?sexp_for_hint env exp expected_ty =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The sexp_for_hint is used for more than hints now. I think it would be better to rename it to sexp and make it non-optional. That will fix the missing constructors case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, this fixed many cases indeed.

Comment thread typing/typecore.ml Outdated
Comment thread typing/typecore.ml Outdated
| Pexp_constant { pconst_desc = Pconst_string (s, _, None); _ } ->
(* Avoid adding quotes around a quoted string constant but make sure to
use consistent inline code style. *)
Format.pp_print_string ppf (String.escaped s)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is only an issue with dumb terminal and in the testsuite, isn't it? Moreover using String.escaped does not mesh well with non-ascii contents. I would propose to skip this step.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right, making the strings nominal is problematic. I removed it.

Comment thread typing/typecore.ml

let report_expr_type_clash_hints exp diff =
match exp with
| Some (Pexp_constant const) -> report_literal_type_constraint const diff

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

match exp.pexp_desc with ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Improved.

Comment thread typing/typecore.ml Outdated
let d = match denom with Some d -> d | None -> "expression" in
fprintf ppf "%s" d
in
report_general_this_exp denom ppf

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems to me that the two report_this_exp function could be merged without loss of readability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you mean change report_this_pexp to take an option pexp ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, I was thinking of making the choice of denomination in one place:

let report_this_pexp_opt denom ppf exp =
  let denom ppf =
    match denom, exp with
    | Some d, _ -> fprintf ppf "%s" d
    | None, Some exp -> pp_pexp_denom ppf exp
    | None, None -> fprintf ppf "expression"
  in
  let nexp = Option.bind exp Pprintast.Doc.(nominal_exp_doc longident) in
  match nexp with
  | Some nexp -> fprintf ppf "The %t %a" denom pp_doc nexp
  | _ -> fprintf ppf "This %t" denom

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking at the call sites, we could also make the sentence more readable by adding the has type suffix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You removed the wrapping of nexp with Style.as_inline_code in your snippet. Is that intended ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, that was a mistake.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I've changed the code.

@Octachron

Copy link
Copy Markdown
Member

There is non-trivial conflict with #13169 (because I didn't convert most of the AST printers which are generally used to print immediately). Assuming that #13169 is merged first, @Julow don't hesitate to ping for fixing the conflict.

@Julow Julow left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rebased and ready for an other review. Thanks for waiting!

^^^^
Error: This expression has type "bool" but an expression was expected of type
"int"
Error: The constructor "\#true" has type "bool"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This could be improved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks like an instance of #13263.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can have a look at this issue later.

Comment thread typing/typecore.ml

let report_expr_type_clash_hints exp diff =
match exp with
| Some (Pexp_constant const) -> report_literal_type_constraint const diff

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Improved.

Comment thread typing/typecore.ml Outdated
let d = match denom with Some d -> d | None -> "expression" in
fprintf ppf "%s" d
in
report_general_this_exp denom ppf

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you mean change report_this_pexp to take an option pexp ?

Comment thread typing/typecore.ml Outdated
(** [sexp_for_hint] is used by error messages to report literals in their
original formatting *)
let unify_pat ?sdesc_for_hint env pat expected_ty =
let unify_pat ?sexp_for_hint env pat expected_ty =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread typing/typecore.ml Outdated
(** [sexp_for_hint] is used by error messages to report literals in their
original formatting *)
let unify_exp ?sdesc_for_hint env exp expected_ty =
let unify_exp ?sexp_for_hint env exp expected_ty =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, this fixed many cases indeed.

Comment thread typing/typecore.ml Outdated
| Pexp_constant { pconst_desc = Pconst_string (s, _, None); _ } ->
(* Avoid adding quotes around a quoted string constant but make sure to
use consistent inline code style. *)
Format.pp_print_string ppf (String.escaped s)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right, making the strings nominal is problematic. I removed it.

Comment thread parsing/pprintast.ml Outdated
- Do not contain spaces when printed.
- Is a constant that is short enough.
*)
let nominal_exp_doc lid t =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have the impression that we don't need to delay the dependency on longident anymore?
Also since the function is in the Doc submodule, the _doc suffix seems redundant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

@Octachron Octachron left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The PR looks good to me: directly quoting the conflicting part of the source code should help readers to locate the source of the type error.

This should be a noticeable improvement, which only requires a small and localized change in Typecore.

@Octachron

Copy link
Copy Markdown
Member

@Julow , do you wish to clean the history in order to preserve the individual message commits? Otherwise, I would squash the PR and merge the message commits.

@Julow

Julow commented Jul 17, 2024

Copy link
Copy Markdown
Contributor Author

Thanks :) Yes, I can clean up the history.

Julow added 5 commits July 17, 2024 17:57
This function can be used to improve the type clash error message, which
uses the parsetree instead of the typedtree to avoid loosing some
information from the source code (eg. how constants are printed).

This function is used for the "this function ..." message, which can be
easily changed to use untyped expression.
Re-use the mechanism implemented for the "This function" message in
type clash errors to print the original expression if possible.

Type error messages now look like this:

    Error: The expression '43' has type int
           but an expression was expected of type int32

The argument 'unify_exp ~sexp' is made non-optional as this isn't too
intrusive and ensures the new error is implemented in more cases.
Clarify type clash error messages by printing the kind of the reported
expression.

Currently implemented for expression that are likely 'nominal' but not
necessarily.

Example:

    The constant "42" has type ...

    This field has type ...

This is similar to the `Apply_non_function` error:

    The function "foo" has type ...

    This function has type ...
Add denominations for expressions like `for`, `while`, `if`, `match`,
etc.. that are often spanning several lines.

It acts as a confirmation that the error is about the containing
expression rather than a nested one as the printed code is sometimes
harder to read.

There's no test for some of them (`try`, `if`) but they'll become
easily reachable if the denomination is used in other messages.
@Julow Julow force-pushed the print-expr-type-clash branch from 9162094 to bc88b1b Compare July 17, 2024 16:27
@Octachron Octachron merged commit 9a982c0 into ocaml:trunk Jul 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants