pattern aliases do not ignore type constraints#1655
Conversation
|
To give more context regarding why I stumbled upon this: I am currently experimenting with a patch allowing a limited use of GADTs under or-patterns (for which I'll give more details in another PR soon), and the issue appeared with the following code: type _ t =
| IntLit : int t
| BoolLit : bool t
let f (type a) (t : a t) a =
match t, a with
| IntLit, ((3 : a) as x)
| BoolLit, ((true : a) as x) -> ignore x
| _, _ -> ()Without the patch proposed in this GPR, the compiler rejects the example above with the following error message: The reason being that for constant patterns (in this case |
|
I understand the problem you point at, but I do not agree with this semantics. Now, going back to a possibly acceptable semantics that does not ignore type constraints in patterns, the natural approach would be to translate the constraint twice, once for the pattern and once for the generated type. This matters particularly if there are some wild cards in the type, but it may also matter for principality reasons. |
Indeed.
If I understand what you mean correctly, I believe that you are correct, and that there are already are some principality issues in the current implementation (related to the handling of mutable record fields): |
|
I rebased and did (in 857b38c) the change you proposed. At least I believe so. This doesn't handle the issue pointed at in my previous message (about mutable record fields), I think I'll handle that in another PR. |
|
No, this is not what I meant. I was talking about the semantics, not the implementation. # function (`A, _ : _ * int) as x -> x;;
- : [< `A ] * int -> [> `A ] * int = <fun>But there are various difficulties:
# function (`A, _ : 'a * int) as x -> x;;
- : [`A ] * int -> [`A ] * int = <fun>because we end up forcing unification between Can you open a bug report, so that we can think a bit more about this problem? |
|
We haven't had time to properly discuss it yet. I still believe something should be done, though I'm not saying the approach proposed here is necessarily the right one. I'm hoping we'll get some time to discuss this in the beginning of next year, in the meantime, I'd like to keep this PR open. |
|
In Tezos codebase we stumbled upon a similar issue with GADTs under or-patterns when using bisect_ppx (2.4.1 as of today) which introduces an type _ t = A : unit t | B : bool t
let f : type a. a t -> int =
fun x -> match x with A | B -> 0is instrumented as let f : type a. a t -> int =
fun x ->
___bisect_visit___ 2;
(match x with
| A|B as ___bisect_matched_value___ ->
((((match ___bisect_matched_value___ with
| A -> (___bisect_visit___ 0; ())
| B -> (___bisect_visit___ 1; ())
| _ -> ()))
[@ocaml.warning "-4-8-9-11-26-27-28"]);
0))rejected by the typechecker: I tried adding type annotations, e.g. |
|
Looking again at this PR, wouldn't it make sense to use a different syntax: Namely, what we want to choose here is the type of |
|
Note that that wouldn't help @mbouaziz, at least not straightaway, as the as-pattern is being generated by I don't mind enriching what is accepted on as' rhs, I believe @gasche would like to go in that direction as well (actually, I think he'd like to go further and turn |
|
What is the typing rule for When we write This is fairly conservative, and OCaml uses a more flexible typing rule (and our programs rely on it), in particular for polymorphic variants: Ideally, the type inferred for Of course, this "most general type of (For equations introduced by GADTs, I think this suggests keeping the local type constructor as it is in the expected type (if there is one; otherwise we check the subpatterns without the equation), as it is more general than any ambiguous type that would use the equation.) For the example of @garrigue |
|
I had a meeting this morning with @trefis and @garrigue. We arrived at a specification for the feature we liked, which suggests an implementation approach, which @trefis volunteered to implement. The general idea to type The specification we propose uses a feature that does not exist in OCaml patterns today, namely For example, The natural desugaring of type annotations is as follows: where we see that two different fresh instances of will give Note: as @trefis explained to me, in the implementation we cannot just take two instances of the pattern type as found in the typedtree at this point, as the type has already been "polluted" by unifications occurring during type-checking. We should either keep the syntactic type around (maybe it's already there?) or, during type-checking, proactively take two independent instances of the syntactic type, which @garrigue describes as "one for the input, one for the output" (one for the scrutinee's type, one in the environment exported by the pattern), or "for the inside, for the outside". The latter approach is what @trefis would like to implement. Finally, @trefis and @garrigue discussed what happens with mutable fields: it is interesting as there the desugaring of then the pattern even though this desugaring is (not valid OCaml syntax and) incorrect from a runtime/operational/dynamic point of view, as it loses sharing in |
I don't think the second approach you describe would work, if you have I do find it slightly distasteful to keep some parsetree inside the typedtree (even if it's an extra node), and I had considered instead introducing
But this is more involved than simply storing the parsetree, and I'm not sure building a skeleton and then fixing it is a good idea even though no one should ever be able to observe this partially built core type (unless there is a bug in Any opinion? |
|
I don't know much about this part, but instead of having two flexible instances generated during typechecking, could we store a "rigid" version that we can later take instances of (and then one instance maybe would be taken immediately, if this is what typechecking needs)? This sounds related to your "refresh" proposal but with a slightly different perspective. (I was hoping to get compliments on my rewording of Jacques "view patterns" formulation as a simpler/nicer recursive expansion of |
3d7b090 to
ec0b02f
Compare
Indeed it does sound related. However, I went ahead with the "refresh" strategy, rather than the "rigid translation" one simply because it didn't require any thinking (on my part): one just need to do the same thing that is already being done. There is one question though that exists with all the approaches described here, which is: should the "globalisation" of variables be delayed when refreshing the constraint? I guess to answer that question one needs to know why it is delayed in the first place when typing Of course there is nothing exercising this behavior in the testsuite, and no explanation in the commit which introduced it. So we will need @garrigue to explain that one. |
I discussed this with @garrigue this morning, and this is actually done to prevent using external type information while typing patterns. let f (x : 'a) y =
if true then
ignore (x : M.t)
else
match y with (A : 'a) -> ()even though it's clearly not principal (I believe #1931 would help in this case, but #1931 has its own issues). Independently of principality concerns, this can also impact the type produced in presence of polymorphic variants. @garrigue said he would provide an example of this (in the form of a testsuite addition). Going back to the "refresh type" vs "rigid constraint" discussion, @garrigue solution is closer to the "rigidify"-version, except there is no need to make the variables rigid, they just need to be quantified, i.e. they just need to be introduced at the generic level. As for the "which variables?" question, we both agree that only the ones coming from underscores in the source should be generalized. I'll try to implement that. PS: looking at if try unify env v ty; true with _ -> Btype.backtrack snap; false
then try
r := (loc, v, TyVarMap.find name !type_variables) :: !r
with Not_found ->
...And @garrigue wasn't sure in which situations the unification could fail, and why it was then ok to ignore that faire and not globalize the variable. It sure would be nice if an explanation emerged. |
|
I updated the content of the PR, and it is now ready for review. |
|
Rebased and changelog entry added. @gasche: do you want to have a look before I merge? |
gasche
left a comment
There was a problem hiding this comment.
I made a review for code readability, but haven't looked yet at whether I understand the actual change in the Tpat_constraint case.
| 3 | !x.lbl | ||
| ^^^ | ||
| Warning 18: this type-based field disambiguation is not principal. | ||
| val u : M.r ref -> int = <fun> |
There was a problem hiding this comment.
I'm slightly confused here, why is it that !x.lbl does not warn about principality anymore but x := { lbl = 4 } does? Basically I don't understand why the test above still fails in principal mode, given that I would expect (_ : t) as x and (x : t) to be equivalent patterns when t contains no flexible inference variable.
There was a problem hiding this comment.
I would expect
(_ : t) as xand(x : t)to be equivalent patterns when t contains no flexible inference variable.
And indeed they are, if you look at examples r and s a few lines above, you'll notice the warning is emitted in the same situations:
let r arg =
match arg with
| (x : M.r ref) ->
!x.lbl
;;
[%%expect{|
val r : M.r ref -> int = <fun>
|}]
let s arg =
match arg with
| (x : M.r ref) ->
x := { lbl = 4 }
;;
[%%expect{|
val s : M.r ref -> unit = <fun>
|}, Principal{|
Line 4, characters 9-20:
4 | x := { lbl = 4 }
^^^^^^^^^^^
Warning 18: this type-based record disambiguation is not principal.
val s : M.r ref -> unit = <fun>
|}]The warning here has nothing to do with the type of the pattern, it's imputable to the way applications are typed. Indeed the following don't warn:
let s' arg =
match arg with
| (x : M.r ref) ->
x.contents <- { lbl = 4 }
;;
[%%expect{|
val s' : M.r ref -> unit = <fun>
|}]
let s'' arg =
match arg with
| (x : M.r ref) ->
((:=) : _ -> M.r -> unit) x { lbl = 4 }
;;
[%%expect{|
val s'' : M.r ref -> unit = <fun>
|}]but this does:
let s''' (arg : M.r ref) =
arg := { lbl = 4 }
;;
[%%expect{|
val s''' : M.r ref -> unit = <fun>
|}, Principal{|
Line 2, characters 9-20:
2 | arg := { lbl = 4 }
^^^^^^^^^^^
Warning 18: this type-based record disambiguation is not principal.
val s''' : M.r ref -> unit = <fun>
|}]d0393c7 to
487ebd1
Compare
gasche
left a comment
There was a problem hiding this comment.
I think that the code would be nicer if transl_simpl_type also returned a tuple with cty.cty_type as well, but I have the impression that you didn't want to do this refactoring as it would touch more code not affected by this PR. Okay. In any case, the new code reads nicer than the first second iteration, thanks! If @garrigue reviewed the core logic for correctness, I'm happy to also approve.
build_as_typecurrently ignores type constraints. While this doesn't pose any soundness issue, it can lead to surprising results, e.g.The fix consists simply in reusing the pattern type if a constraint is present.
I added a few test cases to the testsuite and I believe the new behavior is an improvement.