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
Fixing the limitations on destructive substitutions #792
Conversation
Let me look into that after ICFP. |
(For anyone wondering, ICFP is between September 18th and September 24th this year, and Jacques is and has been doing an awful lot of organization work for it.) |
I tried using a path map in subst.ml, see trunk...sliquister:generalize-destr-subst3. I can clean up the history a bit and replace the current pull request, if you think it's better. |
I haven't looked at the patch yet but, depending on how much effort it is, I might suggest splitting the third case into a separate pull request, as it's soundness is much less clear than the other cases. All the existing uses of For example,
will need to be an error. Although:
should probably be allowed. |
I recommend that people read commit by commit, rather than the whole diff (but not for the branch with path maps, as I haven't cleaned up the history). About soundness, if you look at the third commit, you'll see that the typer already allows broken signatures (I haven't checked if this leads to segfaults). I left the behavior unchanged because I don't know what to do about it, but for the new kinds of substitutions, an error is returned instead. |
4329d61
to
8e6be36
Compare
I was asked a couple of days ago about why we can't write a type expression on the rhs of a destructive substitution. @garrigue, have you had a chance to take a look at this feature? I rebased the feature, and switched the implementation to the one with path substitutions. Although while writing some of the changes, I saw that nondep_type/nondep_supertype implement a deep copy of types more simply that subst.ml, so perhaps that would be another way to go. |
Sorry, I knew that I had something on my plate, but couldn't find it anymore. I will look at this soon. |
Indeed, reusing the code for |
I wasn't thinking of anything specific, I simply noticed that nondep_type does some kind of rewriting on types and doesn't use subst.ml for that, and so it could make sense to do the same here. |
I just ran into this issue again this morning. |
Well, this pull request needs more than a rebase to be merged: it needs a review. |
Sorry to be dragging my feet on this one. It is going to be merged once I review it properly. |
Again, sorry for the long delay. After a final review, I think that the code in its current form is fine, and the design choices reasonable. So, if for the time being we keep these implementations, the next question is which one to use for destructive substitutions. And there the natural answer is In conclusion, @sliquister , could you rebase the code, so that we merge it in trunk ASAP. 👍 |
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.
See my comment in the discussion.
I was also in the process of reviewing this change. I'm pretty sure there are a couple of bugs as well as some tidying up that would be useful. So @garrigue could you hold off on the merge until I've finished reviewing it -- should be done today or tomorrow. |
@garrigue Thanks! In addition to issues Leo may have uncovered, the bug in the initial description should still be present, so I need to take a look at that before the code gets merged. |
This is taking me a bit longer than I expected, but I've finished looking at the first part -- the commit that allows general expressions for type substitutions -- and thought I would report what I've found so far. There are two main issues. The first is the # module type S = sig
type t = [ `Foo ]
type s = private [< t ]
end;;
# module type T = S with type t := [ `Foo ];;
Fatal error: exception File "typing/subst.ml", line 103, characters 23-29:
Assertion failed or: # module type S = sig
type t
type t += T
end;;
# module type T = S with type t := int;;
Fatal error: exception File "typing/subst.ml", line 103, characters 23-29:
Assertion failed That first example (and a couple of other cases like it) can be fixed by simply dropping the path in question because it is only used for printing anyway. The second example would be addressed by #1253 as long as the substitution used The second problem is the use of # type ('a, 'b) foo = Foo;;
# type 'a s = 'b list constraint 'a = (int, 'b) foo;;
# module type S = sig
type 'a bar = (int, 'a) foo
type 'a t
constraint 'a = (int, 'b) foo
val x : string bar t
end;;
# module type T = S with type 'a t := 'a s * bool;;
Fatal error: exception Ctype.Cannot_apply I'm less sure what to do about this case. Tracking the environment during substitution would be a major pain and damage performance. I think that probably we would need to ban type expression substitution for type definitions which have constraints. Then the parameters would be guaranteed to be simple type variables and could be substituted using |
Ok, I think I follow your first point. #1253 would make the following code ill-typed sig
type t
type t += T
end with type t := int * int so we can assume that if an extensible type constructor is substituted, it's only by a type constructor with the same parameters? For the second problem, it doesn't seem necessary to have an environment, because the constraints on the type constructor and the type expression are checked to be identical. Litteraly replacing |
I'm not sure what is going wrong here. It should be OK for unification to assume that all undefined types are abstract, and in that case the above case should not fail, so I'm a bit confused. |
For my above example the parameter of the substituting expression is something like: (int, 'b) foo and the body of the substituting expression is something like: (int, 'b) foo list * bool The type 'a foo = Foo
class c = object end
type 'a s = [ `Foo of 'b ] constraint 'a = 'b foo
type 'b r = [ `Foo of 'b | `Baz of 'b ]
module type S = sig
type ('a, 'b) bar = 'a foo
type 'a t
constraint 'a = #c foo
val x : (< foo: string >, < foo: bool >) bar t
end
module type T = S with type 'a t := [ 'a s | #c r ] Here the parameter for the type expression is: (< .. > as 'b) foo and the body is [ `Foo of <..> as 'b | `Baz of <..> as 'b ] so just trying to mutate the parameter will not produce the correct body. The correct resulting type for sig
type ('a, 'b) bar = 'a foo
val x : [ `Foo of < foo : string > | `Baz of < foo : string > ]
end however if we changed type ('a, 'b) bar = 'b foo then the correct resulting type would be: sig
type ('a, 'b) bar = 'b foo
val x : [ `Foo of < foo : bool > | `Baz of < foo : bool > ]
end which demonstrates the need for an accurate environment. |
I see 3 solutions to this problem of substituting constrained types:
For now I believe that the first one is the best, as it doesn't remove any option for the future. |
I think that the best solution is to prevent using type expressions in destructive substitution when the type has constrained parameters. This is similar to your first option but has a clearer, less implementation-dependent criteria for rejecting these substitutions. With this restriction the parameters are guaranteed to be type variables and so can be mutated directly with |
Another possibility would be to have a separate set of functions for doing destructive substitutions that preserve the environment. This would mean having another set of functions for traversing types, but it would separate the potentially failing substitutions associated with these more expressive destructive substitutions from the normal substitutions used the rest of the time. |
In particular, using a separate set of functions for destructive substitution would allow the additional checks for |
So what you are suggesting is my option 1, but willingly breaking backward compatibility? By the way, I'm not sure what you mean by "destructive substitutions that preserve the environment". |
No, it would only restrict destructive substitutions if they are using a general type expression rather than a type path. So all existing ones would work but the new more relaxed substitutions wouldn't be available for constrained types.
Sure, I just have a slight preference for using a specific function for this restricted case to make it clearer what invariant is being relied upon rather than just passing
Sorry, bad writing on my part. I meant that the separate set of functions would preserve the environment. I now think this is the best option. There are enough awkward corners around these more expressive destructive substitutions that I think they justify having their own functions rather than using |
For backwards compatibility (since we now reject contrained types), and to support extensible sum types.
30362be
to
f1539ae
Compare
There is no particular advantage to changing the signature of |
It'd be useful to be able to write |
Indeed. In fact, I'm surprised that |
@garrigue is this ok for merging now? |
It's ok with me, if @lpw25 has nothing to add. |
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've added a couple of small comments inline. There are also two larger issues:
-
merge_constraints
only callscheck_usage_of_path_of_substituted_item
on the parts of the signature after the definition. However, with e.g. recursive modules there can be references earlier in the signature as well. -
Similarly, the environment created by merge constraints only contains items from earlier in the signature, however items from later in the signature may be referenced as well. Since this is the environment used for calls to
check_usage_of_path_of_substituted_item
it can produce false positives.
You can probably ignore the second point in this PR, as it's actually a preexisting bug in merge_constraints
. For example, in 4.04.1:
# module type S = sig
module rec M : sig
type t = N.t
end
and N : sig
type t = int
end
end;;
module type S =
sig module rec M : sig type t = N.t end and N : sig type t = int end end
# module X = struct type t = int end;;
module X : sig type t = int end
# module type T = S with module M := X;;
Characters 16-36:
module type T = S with module M := X;;
^^^^^^^^^^^^^^^^^^^^
Error: In this `with' constraint, the new definition of M
does not match its original definition in the constrained signature:
Modules do not match:
sig type t = int end
is not included in
sig type t = N.t end
Type declarations do not match:
type t = int
is not included in
type t = N.t
This PR provides a new symptom of this bug, but it doesn't really cause it.
However, the first point is solely the responsibility of this PR and should really be fixed.
let args = List.map (typexp s) args in | ||
begin match PathMap.find p s.types with | ||
| exception Not_found -> Tconstr(type_path s p, args, ref Mnil) | ||
| Path _ -> Tconstr(type_path s p, args, ref Mnil) |
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 call to type_path
seems pointless since we already have the path as the argument to Path _
.
typing/typemod.ml
Outdated
let mty_param = | ||
match Env.scrape_alias env mty_functor with | ||
| Mty_functor (_, Some mty_param, _) -> mty_param | ||
| _ -> assert false |
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.
Depressingly this assert false
can be triggered due to MPR#7611. I wouldn't bother changing it, but thought I should probably mention it.
typing/typemod.ml
Outdated
|
||
let path_is_prefix = | ||
let rec list_is_prefix ~strict l ~prefix = | ||
match l, prefix with |
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.
It seems that strict
is always true at every call site. If so it should probably be removed.
25b9825
to
8ec77c9
Compare
I should have addressed all of Leo's remarks (except the one in subst.ml which we talked about directly). |
61c5274
to
e87113c
Compare
Looks good to me. I'll merge this now and look at fixing the existing bug in |
stdlib: simplify is_main_domain
25188da flambda-backend: Missed comment from PR802 (ocaml#887) 9469765 flambda-backend: Improve the semantics of asynchronous exceptions (new simpler version) (ocaml#802) d9e4dd0 flambda-backend: Fix `make runtest` on NixOS (ocaml#874) 4bbde7a flambda-backend: Simpler symbols (ocaml#753) ef37262 flambda-backend: Add opaqueness to Obj.magic under Flambda 2 (ocaml#862) a9616e9 flambda-backend: Add build system hooks for ocaml-jst (ocaml#869) 045ef67 flambda-backend: Allow the compiler to build with stock Dune (ocaml#868) 3cac5be flambda-backend: Simplify Makefile logic for natdynlinkops (ocaml#866) c5b12bf flambda-backend: Remove unnecessary install lines (ocaml#860) ff12bbe flambda-backend: Fix unused variable warning in st_stubs.c (ocaml#861) c84976c flambda-backend: Static check for noalloc: attributes (ocaml#825) ca56052 flambda-backend: Build system refactoring for ocaml-jst (ocaml#857) 39eb7f9 flambda-backend: Remove integer comparison involving nonconstant polymorphic variants (ocaml#854) c102688 flambda-backend: Fix soundness bug by using currying information from typing (ocaml#850) 6a96b61 flambda-backend: Add a primitive to enable/disable the tick thread (ocaml#852) f64370b flambda-backend: Make Obj.dup use a new primitive, %obj_dup (ocaml#843) 9b78eb2 flambda-backend: Add test for ocaml#820 (include functor soundness bug) (ocaml#841) 8f24346 flambda-backend: Add `-dtimings-precision` flag (ocaml#833) 65c2f22 flambda-backend: Add test for ocaml#829 (ocaml#831) 7b27a49 flambda-backend: Follow-up PR#829 (comballoc fixes for locals) (ocaml#830) ad7ec10 flambda-backend: Use a custom condition variable implementation (ocaml#787) 3ee650c flambda-backend: Fix soundness bug in include functor (ocaml#820) 2f57378 flambda-backend: Static check noalloc (ocaml#778) aaad625 flambda-backend: Emit begin/end region only when stack allocation is enabled (ocaml#812) 17c7173 flambda-backend: Fix .cmt for included signatures (ocaml#803) e119669 flambda-backend: Increase delays in tests/lib-threads/beat.ml (ocaml#800) ccc356d flambda-backend: Prevent dynamic loading of the same .cmxs twice in private mode, etc. (ocaml#784) 14eb572 flambda-backend: Make local extension point equivalent to local_ expression (ocaml#790) 487d11b flambda-backend: Fix tast_iterator and tast_mapper for include functor. (ocaml#795) a50a818 flambda-backend: Reduce closure allocation in List (ocaml#792) 96c9c60 flambda-backend: Merge ocaml-jst a775b88 flambda-backend: Fix ocaml/otherlibs/unix 32-bit build (ocaml#767) f7c2679 flambda-backend: Create object files internally to avoid invoking GAS (ocaml#757) c7a46bb flambda-backend: Bugfix for Cmmgen.expr_size with locals (ocaml#756) b337cb6 flambda-backend: Fix build_upstream for PR749 (ocaml#750) 8e7e81c flambda-backend: Differentiate is_int primitive between generic and variant-only versions (ocaml#749) git-subtree-dir: ocaml git-subtree-split: 25188da
…vement (ocaml#792) Co-authored-by: Sabine Schmaltz <sabine@tarides.com>
With trunk, one cannot write any of the following:
because the type being removed must be at the root of the signature and be replaced by a type constructor applied to the exact same parameters.
This pull request lifts these limitations, by allowing the same substitutions as non-destructive
substitutions, except I disallow substituting types defined inside modules that are aliased later in the signature:
as I didn't know whether that should be an error, or should substitute through the alias, or expand the alias and substitute inside of it. I doubt it matters in practice, so I went with the error.
It should be pretty clear why fixing these limitations is good: it's useful, it makes the language more regular because it's expected to work by analogy with non-destructive substitutions, working around it is annoying and makes type errors worse by introducing irrelevant names.
What's not done (for now):
Now, about the implementation:
I chose to make Subst extensible through some ad-hoc callbacks. It's simple, at least.
I thought of not using subst, but it looks like nothing else in the typer knows how to copy types.
I also thought of making subst support replacing type constructor applications by type declarations, but the dependencies in the typer look awkward if we do that. Also, it forces to write code to compose substitutions containing these new cases, even though it's dead code. Finally, it forces to extend subst to supports rewriting paths instead of idents, which may not be that simple, and bumps against the problem of how to do the substitution of M.t -> int in module B = M? At least with the current solution, this resolution is in Typemod, next to the problem.
There's one bug left:
is rewritten wrongly (look at the last val x), because of duplicate Ident.t in the type when using 'with .. and .. and':
I'm relying on an invariant that the Idents bound in a signature are all different. Clearly it doesn't hold here, but I am not sure if this is simply not an invariant, or if it should be but it's broken temporarily?