fix an environment problem in merge_constraint#9623
Conversation
|
I hope more knowledgeable people than me will offer an explanation, but after having looked briefly at the code it seems that we're indeed always "translating" the rhs of the constraint in the initial environment (i.e. the one just before the definition of the module type), and then checking that it is well-formed in the current environment. That sort of makes sense to me, it's forcing the rhs of the constraint to refer only to things outside of the module type. What I believe your patch does (if it was generalized to all the calls of Now, what's really surprising me is that in your example, |
|
Being a bit imaginative, here is a proposed explanation (but I am not sure it matches the code): |
|
I agree with @trefis. |
|
Here is a reproduction case for a similar problem with variance instead of separability: module type Sig = sig
type +'a abstract
type +'a user = Foo of 'a abstract
end
module type Problem = sig
include Sig
module M : Sig
with type 'a abstract = 'a abstract
and type 'a user = 'a user
type +'a foo = 'a M.user
end |
|
I was amused to trace the introduction of the |
24 years later, I'm afraid not. But @trefis 's explanation above (#9623 (comment)) feels very plausible to me: the right-hand sides must be defined in the initial environment but must still make sense when brought into the new signature. How this explains the loss of variance of separability info, I don't know. |
056920c to
add7f28
Compare
|
I now propose a fix that feels more correct and more complete to me. Reviews would be very welcome -- this bug is causing a regression in at least the Jane Street codebase for 4.11. There was something fundamentally flawed with the idea that we would type-check a definition in an environment, but then we could only check its well-formedness in a richer environment. I think that the problem was that the |
add7f28 to
a60a9c6
Compare
I'm not sure I understand this reasoning. Also, your scenario doesn't seem to make sense: even if the signature contains a type I agree that your variance code demonstrates a bug, and I'm going to look into this. |
|
@garrigue The problem that @gasche is getting at is that we pass not just the right-hand side of the constraint but also the definition from the signature to |
|
I should probably also point out that the "internal" environment calculated by module type Sig = sig
type +'a user = Foo of 'a abstract
and +'a abstract
end
module type Problem = sig
include Sig
module M : Sig
with type 'a abstract = 'a abstract
and type 'a user = 'a user
type +'a foo = 'a M.user
endis broken even with your patch. |
garrigue
left a comment
There was a problem hiding this comment.
It took me a long while to understand the problem, but now I see it.
I'm going to try to build an example showing the problem with constrained parameters
typing/typedecl.ml
Outdated
| let arity_ok = List.length params = sig_decl.type_arity in | ||
| if arity_ok then | ||
| List.iter2 (Ctype.unify_var env) params orig_decl.type_params; | ||
| List.iter2 (Ctype.unify_var outer_env) params sig_decl.type_params; |
There was a problem hiding this comment.
Actually, I think this should be sig_env rather than outer_env.
However, this means that this unification should only happen after all the transl_simpl_type (which should not refer to types of the signature), and protect against unification errors.
I see the problem. In this case this could be solved by using an environment with the full signature, rather than expanding it incrementally. In general, like for functors, the variance (and other mutually recursive properties) should be computed on the final signature, after all substitutions are done. But maybe this last part can be delayed until we do it for functors. |
|
@lpw25 I understand what you mean and I might see how to go in a direction of fixing this problem (rewrite |
|
Example of problem with module type Sig2 =
sig type 'a u = 'a list type +'a t constraint 'a = 'b u end
type +'a t = 'b constraint 'a = 'b list
module type Sig3 = Sig2 with type 'a t = 'a t |
|
@gasche The problem @lpw25 describes is that, if you're calling |
|
@garrigue I pushed a new commit that changes the place where the I needed to tweak your example slightly to get it to compile in my fixed version. As it is written, with my commit it fails with the following error: Two changes make this compile: I can drop the variance annotations or I can define |
garrigue
left a comment
There was a problem hiding this comment.
Additionally, in merge_constraint, I would add
let full_env = Env.add_signature sg initial_env in
let (tcstr, sg) = merge full_env sg names None in
typing/typedecl.ml
Outdated
| there. *) | ||
| let env = sig_env in | ||
| if arity_ok then | ||
| List.iter2 (Ctype.unify_var env) params sig_decl.type_params; |
There was a problem hiding this comment.
Here you may get a unification error.
There was a problem hiding this comment.
I don't understand what you mean. Was it not possible to get a unification error before? Should we do something specific about unification errors, and if so, why?
There was a problem hiding this comment.
This is a question of unification order: since the ptype_params have to be distinct type variables, you cannot get a unification error by unifying them with constrained parameters. However, this could cause an error when unifying the ptype_cstrs, which would be reported as an Inconsistent_constraint error. So you need to wrap this call to unify in the same way.
|
Hm, it also works with |
|
@gasche My proposed solution would be what Jacques said: use
Does that actually check the variance? Once upon a time I submitted a bug report that it didn't, and I do not recall the outcome. |
Since the call in |
|
Note that removing the |
|
Actually, the correct code for constrained parameters is a bit different. I will send you a patch in 1 minute. |
6196482 to
abd00cf
Compare
If I understand correctly, you are proposing to do the more-likely-to-fail unification first. Could we document the reason for this? (It makes the code more complex.) Is the idea that we get better error messages because they are expressed in terms of the type parameters, rather than the type expressions coming from constraint processing? Secondary question: we have a few check under a |
Ok, maybe my explanation was not clear. This is the opposite. The goal of unifying the parameters before the constraints is to have unification fail on constraints rather than on parameters. The role of the unification on parameters is to inherit constraints from the original declaration, avoiding having to redeclare constraints by hand. Then, constraint unification will fail if the is a contradiction between the constraints in the original declaration and those added in the
After calling At this point, you may have correctly deduced that writing parameter constraints in with constraints is actually pointless, since anyway you can only inherit the original constraints, and cannot even refine them. I think this is only allowed for backward compatibility reasons (originally there was no constraint inheritance, and there was really no need for |
989b2ec to
72b97c5
Compare
|
Thanks for the explanation. I did as you recommend, delaying the constraint unification to after the parameter unification, with an explanatory comment. That said, I would rather be in favor of doing the constraint unification first, to get simpler code, especially since you explain that using constraints there is in fact useless. |
|
The diff I propose looks like this: List.map (fun (ty, ty', loc) ->
let cty = transl_simple_type env false ty in
let cty' = transl_simple_type env false ty' in
- (* Note: We delay the unification of those constraints
- after the unification of parameters, so that clashing
- constraints report an error on the constraint location
- rather than the parameter location. *)
+ begin
+ try Ctype.unify_var env cty.ctyp_type tparam
+ with Ctype.Unify tr ->
+ raise(Error(cty.ctyp_loc, Inconsistent_constraint (env, tr)))
+ end;
(cty, cty', loc)
) sdecl.ptype_cstrs
in
@@ -1424,14 +1425,6 @@ let transl_with_constraint id row_path ~sig_env ~sig_decl ~outer_env sdecl =
with Ctype.Unify tr ->
raise(Error(cty.ctyp_loc, Inconsistent_constraint (env, tr)))
) tparams sig_decl.type_params;
- List.iter (fun (cty, cty', loc) ->
- (* Note: contraints must also be enforced in [sig_env] because
- they may contain parameter variables from [tparams]
- that have now be unified in [sig_env]. *)
- try Ctype.unify env cty.ctyp_type cty'.ctyp_type
- with Ctype.Unify tr ->
- raise(Error(loc, Inconsistent_constraint (env, tr)))
- ) constraints; |
garrigue
left a comment
There was a problem hiding this comment.
Looks good to me, with clear comments.
|
Thanks! (I gather that you are not in favor of doing the constraint unification earlier to simplify the code?) |
|
The CI failure is due to a test that was previously failing, and now passes, in (* Valid substitutions in a recursive module may fail due to the ordering of
the modules. *)
module type S0 = sig
module rec M : sig type t = M2.t end
and M2 : sig type t = int end
end with type M.t = int
[%%expect {|
-Lines 1-4, characters 17-23:
-1 | .................sig
-2 | module rec M : sig type t = M2.t end
-3 | and M2 : sig type t = int end
-4 | end with type M.t = int
-Error: In this `with' constraint, the new definition of M.t
- does not match its original definition in the constrained signature:
- Type declarations do not match:
- type t = int
- is not included in
- type t = M2.t
+module type S0 =
+ sig module rec M : sig type t = int end and M2 : sig type t = int end end
|}]Please correct me if I'm wrong, but my understanding is that this is a welcome change, an improvement due to @lpw25's suggestion to use the whole signature to check the with-constraint. |
947200d to
429f849
Compare
|
The CI failures seem spurious: I compiled this PR rebased on top of trunk, and |
|
Well I rebased the PR to fix the CI failures, so if you observed the current state of the branch it is natural that it would work. (But I'll still wait for the CI results to merge, and cherry-pick to 4.11.) |
|
I believe I pulled and rebased before you pushed the new version (i.e. I had to rebase myself), but I may have missed something. |
429f849 to
ef24898
Compare
This PR fixes an old bug in the interaction between [merge_constraint] and [Typedecl.transl_with_constraint], where variance (and now separability) are recomputed in an invalid type environment. See ocaml#9624 and the new tests.
(suggested by Leo White and Jacques Garrigue)
ef24898 to
8b2f64c
Compare
fix an environment problem in merge_constraint (cherry picked from commit 84c87bb)
|
Merged, and cherry-picked in 4.11 ( 003dd39 ). |
This is a preliminary fix for an uncaught
Not_foundexception in the type-checker, from my analysis of the reproduction case provided by @hhugo in #9609 (comment) .The patch includes a reproduction case:
Without the patch, checking
type t = t(which results in a recomputation of the validity of the[@@unboxed]attribute in the resulting signature) raises aNot_foundexception during the lookup of aTconstrpath in the typing environment. (The code assumes that all type-constructor paths are bound in the typing environment.)The patch fixes the issue by bluntly replacing a use of
initial_envinmerge_constraintbyenv. I suspect however that the patch is incorrect or incomplete. The authors ofmerge_constrainttook care to separate the initial and partially-checked environments, I suppose there is a reason for it -- I would welcome an explanation. If usingenvwas the correct change, then many other occurrences ofinitial_envshould be modified.(cc @garrigue and, somewhat randomly, @lpw25 @trefis @Octachron)