Reimplement let open, let module and let exception in terms of a single construct#13839
Reimplement let open, let module and let exception in terms of a single construct#13839nojb merged 14 commits intoocaml:trunkfrom
let open, let module and let exception in terms of a single construct#13839Conversation
|
A very bike-sheddy comment, apologies: I find |
No, I think I am becoming more and more desensitized to naming issues as I get older :) Renaming to |
|
I have looked at the PR, I think that this is a good idea overall, and I agree with what you wrote in the "Special attention" comments: the key changes to review are the |
It is not really a bug fix since the indexer primarily aims at getting items that might be exposed via the module's interface. However there is no strict enforcement of that rule right now and we don't need to prevent the additional items to be indexed. (In fact, for renaming, we will probably need to include non-exposed items in the index too.) |
OK, thanks for the explanation. |
|
@lthls: would you be interested in reviewing the changes to |
|
Eventually, yes. I've got a few other things piled up on my todo list at the moment, but when I have a bit more time I will look at this. |
Thanks! |
voodoos
left a comment
There was a problem hiding this comment.
I had a look at the parser and typer changes (not in lambda's translcore) and found no obvious issues. It's seducing to be able to share code from Typemod to type these expressions, but I am not expert enough to attest that the behavior is exactly the same, especially for the let module ... in case.
parsing/parsetree.mli
Outdated
| (** [let module M = ME in E] *) | ||
| | Pexp_letexception of extension_constructor * expression | ||
| (** [let exception C in E] *) | ||
| | Pexp_stritem of structure_item * expression |
There was a problem hiding this comment.
It would be nice to document the new constructor.
| | _ -> | ||
| assert false | ||
| end | ||
| | Pexp_letmodule(name, smodl, sbody) -> |
There was a problem hiding this comment.
With these changes the typing of let module ... in is delegated to Typemod.type_str_item as any other Pstr_module. These two implementations are slightly different and this might require a more careful review.
There was a problem hiding this comment.
LGTM except the part about local level
| | Pexp_letmodule(name, smodl, sbody) -> | ||
| let lv = get_current_level () in | ||
| let (id, pres, modl, _, body) = | ||
| with_local_level_generalize begin fun () -> |
There was a problem hiding this comment.
In particular, this level increase is not done in the corresponding section in Typemod.type_str_item.
There was a problem hiding this comment.
Based on the comment at the call of with_local_level_generalize_if in the Pexp_let case I think that it is problematic to remove the with_local_level_generalize here.
There was a problem hiding this comment.
The comment in question:
Lines 3823 to 3827 in 8f6365f
refers to a situation involving module unpacks in patterns (ie let (module P) = ... in ...). No such forms are allowed by the new construct let <stritem> in <expr>, so it is not clear to me that with_local_level_generalize is needed here (nothing is being generalized here as far as I can see).
Does this match your understanding @samsa1?
There was a problem hiding this comment.
There are two things that looks problematic for me here :
-
we can have a module unpack :
let module M = (val m) in ...,
which is expected to work likelet (module M) = m in ...- when typing
if c then (let module M = ... in ...) else e2than the level used for typing(let module M = ... in ...)is not the same a the one fore2because the typing ofPstr_moduleincreased the level in a destructive way.
- when typing
My undestanding is that adding a
let ty = newvar() in
with_local_level_generalize_if cond begin fun () ->
...
end ~before_generalize:(fun (new_env, e) -> unify new_env ty e.exp_type)here can only prevent a problem from happening here.
This comment was written by @ncik-roberts maybe him or @garrigue could help us here
There was a problem hiding this comment.
Thanks @samsa1 for the comment. I am happy to add that if I can obtain an actual example of things going "wrong". However, I have not been able to so far. Are you able to find one?
There was a problem hiding this comment.
@samsam1 I'm not sure about the need_scope_protection business (first of all, is it need or needs?). Is there any downside to unconditionally using a fresh inference region for all instances of this construct?
There was a problem hiding this comment.
Sorry for not reacting earlier. The conditional is indeed not necessary and here to mimic previous behavior. I'm totally fine to change it to creating an inference region unconditionally.
There was a problem hiding this comment.
@nojb can you change this in your patch? Then I would be happy to approve it. (We haven't had a codegeneration-specific review, but oh well.) If you don't feel confident tweaking it I can look at it and propose a modified patch.
There was a problem hiding this comment.
Of course, I will rebase the patch (+ this change) shortly.
| ~before_generalize: begin fun (_id, _pres, _modl, new_env, body) -> | ||
| (* Ensure that local definitions do not leak. *) | ||
| (* required for implicit unpack *) | ||
| enforce_current_level new_env body.exp_type |
There was a problem hiding this comment.
So, is it safe to remove this ? (I guess so since the related test passes)
There was a problem hiding this comment.
Those lines are not exactly removed. The
let tv = newvar () in
...
unify_var new_env tv exp.exp_typeIn the Pexp_stritem case does the same thing
|
Thanks @voodoos for the review (and apologies for the lag in responding!). Indeed, it is not obvious that the new typechecking code is equivalent to the old one, but the testsuite passes, and morally I see no reason for the typechecking for local structure items to differ from that of module-level structure items. Incidentally, @samsa1 told me today offline that in fact the refactoring here seems to fix a bug in the typechecking of @samsa1: would you be interested in doing a review of the typechecking part of the patch? |
ab87a17 to
7e889ad
Compare
I had to rebase the PR on top of the current |
There was a problem hiding this comment.
As a whole this PR looks good to me, except a small mismatch in refactoring in typing/typecore.ml that seems problematic.
It also removes a bug in depent.ml and reduces the risk of introducing bugs in other parts of the compiler. Thus, I'm totally in favor of this PR.
This PR says that we refactor 3 cases, however it seems it allows any local structure item (using PPX rather than the parser). Wouln't it be better to modify typing/ast_invariants.ml to prevent such cases from happening ?
I don't consider myself competent for the part of the code in the lambda folder
| | Some id -> String.Map.add id b bv | ||
| in | ||
| add_expr bv e | ||
| | Pexp_letexception(_, e) -> add_expr bv e |
There was a problem hiding this comment.
There was a bug here that is being corrected by this PR.
Missed add_extension_constructor on the first argument of Pexp_letexception
| for modules that are present. *) | ||
| let size = classify_module_expression env mexp in | ||
| let env = Ident.add mid size env in | ||
| classify_expression env e |
There was a problem hiding this comment.
Why is it okay to stop updating env in this case ?
There was a problem hiding this comment.
This only forbids more cases, so it can't cause soundness issues.
You have to be quite creative to find an example where this matters:
module type T = sig type t val x : t end
type m = T of (module T with type t = m)
module type Tm = T with type t = m
let f () =
let rec v : (module Tm) =
let module M = struct
type t = m
let x = T v
end in
(module M)
in
vIf I'm not mistaken, the code above will be rejected with this PR, but in practice there are likely no such occurrences in the wild.
There was a problem hiding this comment.
I agree that becoming more restrictive on let module is probably okay. If we find example of breakage in the wild during release testing, I will volunteer to extend the classification as desired.
| ~before_generalize: begin fun (_id, _pres, _modl, new_env, body) -> | ||
| (* Ensure that local definitions do not leak. *) | ||
| (* required for implicit unpack *) | ||
| enforce_current_level new_env body.exp_type |
There was a problem hiding this comment.
Those lines are not exactly removed. The
let tv = newvar () in
...
unify_var new_env tv exp.exp_typeIn the Pexp_stritem case does the same thing
typing/typecore.ml
Outdated
| let tv = newvar () in | ||
| let (si, newenv) = !type_str_item env si in | ||
| let exp = type_expect newenv e ty_expected_explained in | ||
| unify_var newenv tv exp.exp_type; |
There was a problem hiding this comment.
Why calling unify_var here and not unify_exp ?
This could lead to slightly better error messages for examples such as :
let module M = struct type t = A end in M.AThere was a problem hiding this comment.
Thanks for the suggestion. I took this code from what is done currently for let open ... in .... I tried replacing unify_var newenv tv exp.exp_type with unify_exp ~sexp newenv exp tv, but the error message did not change:
# let module M = struct type t = A end in M.A;;
Error: The constructor M.A has type M.t
but an expression was expected of type 'a
The type constructor M.t would escape its scope(This is also the same error message as in trunk.)
| | Pexp_letmodule(name, smodl, sbody) -> | ||
| let lv = get_current_level () in | ||
| let (id, pres, modl, _, body) = | ||
| with_local_level_generalize begin fun () -> |
There was a problem hiding this comment.
Based on the comment at the call of with_local_level_generalize_if in the Pexp_let case I think that it is problematic to remove the with_local_level_generalize here.
| | _ -> | ||
| assert false | ||
| end | ||
| | Pexp_letmodule(name, smodl, sbody) -> |
There was a problem hiding this comment.
LGTM except the part about local level
|
Note: on the semantics side, we mentioned in yesterday's meeting that most of the features can already be done using |
Indeed, thanks! |
|
Thanks @samsa1 for the review!
Indeed. In #13835 we expose the possibility of using arbitrary structure items locally in the surface syntax. But you are right that as far as this PR is concerned, this should not be allowed. I will do as suggested. |
This is now done edb9d8f. |
|
@lthls would you be able to look at the changes in translmod and translcore introduced by this PR? I haven't looked at them myself, but structurally there is a risk that the previous definitions would generate better code than the general structure-item definitions. |
|
@Octachron I would like to add this to the 5.4 milestone; is that OK? |
|
It would be certainly nice to review the PR as soon as possible. However, since this first PR is an internal change, I am not sure how helpful it would be to push for its integration into 5.4? Nevertheless if you wish, I wouldn't mind adding it to the 5.4 features milestone to extend the review period, but with the understanding that I will close the extended review period in the beginning of May. |
5873128 to
b3fe41e
Compare
|
@gasche @samsa1 I rebased the PR with a fix making the creationg of the type region unconditional. I am planning to merge once the CI passes. @Octachron we had discussed including this for 5.4, but there is no reason to rush things, so let us leave this on |
|
I have noticed several occurrences of
|
|
Thanks @t6s! I will open a PR with the fix for this documentation issue. |
|
( #14554 is a regression on |
| | Pexp_sequence (_, e) | Pexp_open (_, e) -> is_inferred e | ||
| | Pexp_sequence (_, e) -> is_inferred e | ||
| | Pexp_ifthenelse (_, e1, Some e2) -> is_inferred e1 && is_inferred e2 | ||
| | _ -> false |
There was a problem hiding this comment.
This change introduced a regression, as we should have replaced the old Pexp_open by a corresponding case for Pexp_struct_item. See #14638.
This PR is extracted from #13835 for easier reviewing. The synopsis below is adapted from the one in that PR.
It consists in a pure refactoring that reimplements the three constructs
let module,let openandlet exceptionin terms of a single general construct. In doing so, the typechecking and code-generation logic for these constructs is fully shared with the one used for ordinary structure items.The PR consists of, then, three separate pieces:
A new kind of expression is introduced in the abstract syntax:
let structure-item in expr(AST node:Pexp_stritem/Texp_stritem). This generalizes the existinglet open,let exceptionandlet module. Typechecking and code-generation of this new construct is shared with that used for module-level structure items. This part consists of the first four commits, up to and includingAdd Pexp_stritem, Texp_stritem. (The first commit should be read modulo whitespace.)Reimplement compiler support for
let open,let exceptionandlet modulein terms of this one construct. These commits only touch the parser to make it use the new AST node, but indirectly they are switching the typechecking and code-generation logic for these three constructs to the one shared with all structure items. It consists of the three commits namedReimplement ....Finally, we remove all the (now dead) code related to the implementation of the ad-hoc
let module,let openandlet exception. This is the last commit:Remove reimplemented ....What needs special attention
typecore.mldealing with the typechecking for the new constructlet structure-item in expr. There is some logic to make sure that the type ofexprmakes sense outside of the scope of thestructure-item. However, I do not have a good understanding of it, so an expert will need to take a look and explain it to me :)translcore.mldealing with the code-generation for the new construct. I am fairly confident about this part, but I would appreciate an expert pair of eyes here as well.Potential downsides
This PR will involve a fair amount of churn on the
ppxlibside and/orcompiler-libsusers, but it should be fairly manageable, I think (especially because these local forms are infrequently used).