-
Notifications
You must be signed in to change notification settings - Fork 469
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
Conversation
55476b9
to
1093261
Compare
issues: Record_coercion.record_field_subtype_violation list; | ||
} | ||
|
||
exception Subtype of type_pairs * type_pairs * subtype_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.
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 *) |
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.
Can leave these TODO:s in here since I'll do another pass on this to extend it more soonish.
rescript
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/win32-x64
commit: |
fcffc7e
to
e3a2b95
Compare
@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? 🙏 |
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.
@zth doing God's work! Keep them coming!
The record [1;33mx[0m cannot be coerced to the record [1;33my[0m because: | ||
- The field [1;33mx[0m is optional in record [1;33mx[0m, but is not optional in record [1;33my[0m |
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 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
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.
Would that be reasonable to support?
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.
definitely low-priority but I once had the need for such a thing and I can't think of any argument against it.
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.
@cristianoc you see any issues with supporting it?
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'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.
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.
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 [1;33mx[0m cannot be coerced to the record [1;33my[0m because: | ||
- Field [1;33mx[0m runtime representation is configured to be [1;33m"z"[0m (via the @as attribute) in record [1;33my[0m, but in record [1;33mx[0m it is configured to be [1;33m"w"[0m (via the @as attribute). Runtime representations must match. |
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 is really nice!
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 approach looks great!
Left some comments.
compiler/ml/printtyp.ml
Outdated
@{<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 |
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.
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".
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.
Yeah good point. I was struggling to come up with good, clear names. I'll do another pass at it.
The record [1;33mx[0m cannot be coerced to the record [1;33my[0m because: | ||
- The field [1;33mx[0m is optional in record [1;33mx[0m, but is not optional in record [1;33my[0m |
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'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 [1;33mOne[0m of variant [1;33mx[0m has an inline record as payload. Inline records cannot be coerced. |
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.
perhaps add "currently" as there's no in-principle reason why inline records could not be coerced in future
compiler/ml/ast_untagged_variants.ml
Outdated
@@ -71,6 +71,17 @@ type block_type = | |||
| ObjectType | |||
| UnknownType | |||
|
|||
let block_type_to_string = function |
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 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
.
compiler/ml/printtyp.ml
Outdated
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 \ |
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.
"with the expected of" has some typo in it
This reverts commit 55476b9.
c2a1805
to
2d3efd5
Compare
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
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.