Skip to content

Start work on improving subtype error messages #7404

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

Merged
merged 18 commits into from
May 26, 2025

Conversation

zth
Copy link
Collaborator

@zth zth commented Apr 24, 2025

This improves the subtype error messages to make them understandable/actionable where relevant.

In this first iteration I've focused on adding optional context to each subtype error. We use that to, where possible, provide better subtype errors, that are actionable.

This does not cover all cases, and there's plenty of follow ups (some listed below). But, it's a start, and validates the concept that we can do this this way.

Follow ups

Will put these in a separate issue to be tracked

  • Variant constructor inline record payloads
  • Variant constructor tuple payloads
  • Record representation
  • Polyvariant :> variant

Then there's the larger follow up of tracking where in a nested subtype check an error is happening. Example: Records in records, where a record further down the type structure is not a subtype.

@zth zth force-pushed the better-subtype-error-messages branch 2 times, most recently from 55476b9 to 1093261 Compare May 23, 2025 11:02
issues: Record_coercion.record_field_subtype_violation list;
}

exception Subtype of type_pairs * type_pairs * subtype_context option
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Adding subtype_context is the real change here. It tries to track the context a subtype issue was found in, so better error messages can be presented.

| Tarrow (l1, t1, u1, _, _), Tarrow (l2, t2, u2, _, _)
when Asttypes.Noloc.same_arg_label l1 l2 ->
let cstrs = subtype_rec env ((t2, t1) :: trace) t2 t1 cstrs in
subtype_rec env ((u1, u2) :: trace) u1 u2 cstrs
| Ttuple tl1, Ttuple tl2 -> subtype_list env trace tl1 tl2 cstrs
| Ttuple tl1, Ttuple tl2 ->
(* TODO(subtype-errors) Tuple as context *)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can leave these TODO:s in here since I'll do another pass on this to extend it more soonish.

Copy link

pkg-pr-new bot commented May 23, 2025

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@7404

@rescript/darwin-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@7404

@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@7404

@rescript/linux-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@7404

@rescript/linux-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@7404

@rescript/win32-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@7404

commit: 2d3efd5

@zth zth changed the title [WIP] Improve subtype error messages Start work on improving subtype error messages May 23, 2025
@zth zth force-pushed the better-subtype-error-messages branch from fcffc7e to e3a2b95 Compare May 23, 2025 12:02
@zth zth marked this pull request as ready for review May 23, 2025 12:02
@zth zth requested review from cristianoc, cknitt and tsnobip and removed request for cristianoc May 23, 2025 12:03
@zth
Copy link
Collaborator Author

zth commented May 23, 2025

@cristianoc would you mind having a look at the general approach here, to make sure it's good enough?

@tsnobip @cknitt would you mind looking at the error messages, like you have in the previous PRs? 🙏

Copy link
Member

@tsnobip tsnobip left a comment

Choose a reason for hiding this comment

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

@zth doing God's work! Keep them coming!

Comment on lines +12 to +13
The record x cannot be coerced to the record y because:
- The field x is optional in record x, but is not optional in record y
Copy link
Member

@tsnobip tsnobip May 23, 2025

Choose a reason for hiding this comment

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

the error message is very detailed and insightful, nice! That reminds me that the other way around is not yet possible unfortunately, you can't coerce to a type that has more optional fields than the original type.

module Foo = {
  type t = {foo: int}
}

module Bar = {
  type t = {foo: int, bar?: int}
}

let foo = {Foo.foo: 1}

let bar = (foo :> Bar.t) // error

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would that be reasonable to support?

Copy link
Member

@tsnobip tsnobip May 23, 2025

Choose a reason for hiding this comment

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

definitely low-priority but I once had the need for such a thing and I can't think of any argument against it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@cristianoc you see any issues with supporting it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd forgotten about it, but the reason is now coming back: transitivity.

Since {foo:int, bar:string} :> {foo:int}, it means that Foo.t says nothing about bar. It does not say that bar should not be there.
So the conversion Foo.t :> Bar.t would be dangerous as it could accidentally capture a value that is already in bar and that value does not have to be an int either, it could be a string or anything.

The only safe way to do that coercion would be to add some runtime code, which removes the field bar. Type coercion is currently type-only. So we'd have to change the nature of type coercion, in a way that goes beyond the type checking phase.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, got it! So for regular coercion, it's a no go. At some point we talked very loosely about "runtime conversion" (maybe with its own operator like ::>) as a concept. The discussions were in the context of JSON. This would be another place where that type of thing could be useful.

Type x is not a subtype of y

The record x cannot be coerced to the record y because:
- Field x runtime representation is configured to be "z" (via the @as attribute) in record y, but in record x it is configured to be "w" (via the @as attribute). Runtime representations must match.
Copy link
Member

Choose a reason for hiding this comment

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

This is really nice!

Copy link
Collaborator

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

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

The approach looks great!
Left some comments.

@{<info>@unboxed@} attribute to the variant @{<info>%s@}."
(Path.name variant_name) (Path.name primitive)
(Path.name variant_name)
| Primitive_coercion_target_variant_no_catch_all
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 about the terminology catch-all as well as primitive in error messages.
I think the former means: a case with a payload of that type. While "catch-all" is more reminiscent of _ in pattern matching or 'a in types.
And for primitive: I think it just refers to "a type".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah good point. I was struggling to come up with good, clear names. I'll do another pass at it.

Comment on lines +12 to +13
The record x cannot be coerced to the record y because:
- The field x is optional in record x, but is not optional in record y
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd forgotten about it, but the reason is now coming back: transitivity.

Since {foo:int, bar:string} :> {foo:int}, it means that Foo.t says nothing about bar. It does not say that bar should not be there.
So the conversion Foo.t :> Bar.t would be dangerous as it could accidentally capture a value that is already in bar and that value does not have to be an int either, it could be a string or anything.

The only safe way to do that coercion would be to add some runtime code, which removes the field bar. Type coercion is currently type-only. So we'd have to change the nature of type coercion, in a way that goes beyond the type checking phase.


Type x is not a subtype of string

The constructor One of variant x has an inline record as payload. Inline records cannot be coerced.
Copy link
Collaborator

Choose a reason for hiding this comment

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

perhaps add "currently" as there's no in-principle reason why inline records could not be coerced in future

@@ -71,6 +71,17 @@ type block_type =
| ObjectType
| UnknownType

let block_type_to_string = function
Copy link
Collaborator

Choose a reason for hiding this comment

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

I first thought this was for debug, but it's for user-visible error messages.
Should probably clarify in the function name or comments what role this print plays. If it's about the runtime representation, then it should say "number". If it's about the rescript type then int.

fprintf ppf
"The constructor @{<info>%s@} of variant @{<info>%s@} has an \
@{<info>@as@} payload that has a runtime representation of \
@{<info>%s@}, which is not compatible with the expected of \
Copy link
Collaborator

Choose a reason for hiding this comment

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

"with the expected of" has some typo in it

@zth zth force-pushed the better-subtype-error-messages branch from c2a1805 to 2d3efd5 Compare May 26, 2025 09:44
@zth zth merged commit 6f4275c into master May 26, 2025
16 checks passed
@zth zth deleted the better-subtype-error-messages branch May 26, 2025 10:06
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.

4 participants