Skip to content

Support modules of non-legacy modes #3759

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

Open
wants to merge 2 commits into
base: minor-fix-modes
Choose a base branch
from
Open

Conversation

riaqn
Copy link
Contributor

@riaqn riaqn commented Mar 27, 2025

Based on #3757 and #3758 and #3824

This PR adds the support for modules of non-legacy modes. Functor parameter and body are still fixed to be legacy.

TODO:

  • Improve inclusion error messages
  • Add tests reflecting subtle code changes in type checking
  • Add tests showing functor application mode error message

The room for improvement shouldn't block preliminary review and discussion. In particular, changes to the existing tests seem correct, and can serve as a good starting point.

@riaqn riaqn requested a review from lukemaurer March 27, 2025 17:24
@riaqn riaqn added typing modes Work on modes. There's some overlap with the `multicore` label, but not strictly so. labels Mar 27, 2025
*)

(** Gives the modes suitable for the inclusion check of a child item. *)
val child_modes: string ->
?modalities:(Mode.Modality.Value.t * Mode.Modality.Value.t) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on what modalities means?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could still be clearer what modalities is saying: why are there two of them and what do they mean? (Too bad we don't have named tuples here)

@riaqn riaqn force-pushed the modal-module branch 4 times, most recently from eefebc5 to 2f89dc6 Compare April 4, 2025 16:59
@riaqn riaqn changed the base branch from main to minor-fix-modes April 8, 2025 13:43
module M' = (M @ nonportable)
(* CR zqian: The following should fail but doesn't, due to A limitation
described in "inferred modalities" in [mode.ml] *)
let _ = portable_use M'.x (* [M] is nonportable and [x] doesn't have modality *)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems pretty bad. Is this a soundness bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a soundness bug in the sense that we are just forgetting the explicit weakening.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR should also include documentation for the new syntax. (Just making this comment here so that GitHub sees it as a Discussion that won't get lost.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not in this PR as it's not still stable, with various issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we will add documentation in another PR.

3 | (module M : S)
^
Error: Signature mismatch:
Got "regional" but expected "global".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt users will understand what regional means, and local seems fine here even though it's a bit imprecise?

is not included in
val foo : 'a -> 'a @@ portable
The second is portable and the first is nonportable.
Got "nonportable" but expected "portable".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit sad. We don't need to drill down all the way to foo anymore, but the user should still find out that the module M' is expected to be portable but is nonportable.

@@ -123,7 +123,6 @@ module type S = sig
with [t] to tie the knot on recursive operations. *)

module Result : sig
@@ portable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems strange for this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, since there is @@ portable at the begining of this file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I don't care enough to put my foot down here, but I think we should avoid this kind of unrelated cleanup on otherwise unaffected files.

*)

(** Gives the modes suitable for the inclusion check of a child item. *)
val child_modes: string ->
?modalities:(Mode.Modality.Value.t * Mode.Modality.Value.t) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could still be clearer what modalities is saying: why are there two of them and what do they mean? (Too bad we don't have named tuples here)

| Sig_module (id, pres, md, rec_, vis) ->
let md_modalities =
md.md_modalities
|> Mode.Modality.Value.to_const_exn
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we expect to_const_exn to succeed here? I suppose a modality variable in a signature wouldn't make much sense? (There should be a comment here in any case)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include S @@ modalities, here S must be a constant modality (as opposed to inferred modalities). See maybe_infer_modalities in this file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename the function apply_modalities_included_signature then to express this constraint, in addition to adding the comment here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe apply_constant_modalities_sg

match Mode.Modality.Value.Const.is_id modalities with
| true -> mty
| false -> apply_modalities_module_type env modalities mty
let transl_modalities ~sig_modalities modalities =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This surprises me as a thing we want to do? This suggests we're treating sig_modalities as defaults rather than something onto which we want to compose other modalities. Oh, I guess they're the default modalities from the start of the signature? I guess then the overriding makes sense, but I think we should find a better name than sig_modalities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right that this is very bad. Ideally we want per-axis overriding: I.e., if default modality is portable, then later if we write nonportable, that will override. If later we write global, that will compose into global portable.

This should be fixed ASAP.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh heh good point. Still, calling the argument ~default_modalities seems like an improvement

Comment on lines +1922 to +1923
(* the module is added only for its types, so we give it the
strongest mode to avoid false mode errors. *)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, this could be a bit clearer. I would emphasize that we're in a signature here, so this newenv will only be used to typecheck signature items

Comment on lines +64 to +74
(* CR zqian: currently modules are checked bottom-up, which gives worse error
messages. The following example should point to [foo] instead. *)
module M @ many = struct
let (foo @ once) () = ()
end
[%%expect{|
Lines 1-3, characters 18-3:
1 | ..................struct
2 | let (foo @ once) () = ()
3 | end
Error: This is "once", but expected to be "many".
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to highlight this issue.

Copy link
Collaborator

@goldfirere goldfirere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a quick look at the tests, just to understand the design better. Looks good to me, just a few comments as I was scrolling through.

1 | module M' = (M @ portable)
^^^^^^^^
Error: Mode annotations on modules are not supported yet.
module M' = M
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect a portable modality to be printed here, I think

Copy link
Contributor Author

@riaqn riaqn May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though the module is portable, its "exposed mode" could be nonportable, and since printtyp.ml zaps to legacy, it would be nonportable.

(Yes, we should let printtyp.ml zap to floor, in align with module type of and mli-less files).

1 | module M @ portable = struct end
^^^^^^^^
Error: Mode annotations on modules are not supported yet.
module M : sig end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would expect to see a portable modality printed here. Maybe mode-crossing eliminates it?

^^^^^^^^
Error: Mode annotations on modules are not supported yet.
File "_none_", line 1:
Error: Recursive modules require an explicit module type.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test would be more informative if we put on the type signatures to fix this error, so that the rest of the syntax gets checked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait is the syntax check interrupted by type error? Oh wow.

In the long run this file should just be about syntax... the type errors are already checked by other files...

(* packing is allocation, and we can't construct a global module using local values *)
let bar (local_ m) =
let module M = (val m : S) in
(module M : S)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this restriction (that an expression for packing cannot contain any local values) should get a brief mention in the reference page for stack allocation. (Why document this small point? Because I think the time cost of writing the doc -- 5-10 minutes -- is less than my estimate of the expected value of the amount of time lost when someone tries to do this and fails, even taking into account my estimate of the percentage of people who actually read the docs.)

@@ -160,7 +148,7 @@ module type S = sig
end

module M : S = struct
let foo = fun x -> x
let foo @ nonportable = fun x -> x
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Urgh. I've figured it out. This is necessary to make M nonportable so that a test 70 lines later still tests the right thing. Ok, I suppose. But it would be much better if we included the portability of modules in printing, and then this would be more obvious. Actually I'm a little surprised that printing doesn't zap the modes. Maybe at least leave a comment on this line saying what the motivation for the annotation is? As written, it looks like perhaps the nonportable is necessary in order for M to type-check against S.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the printing still zaps to id. Now with #3922 , I think the default should be zapping to floor.

@@ -123,7 +123,6 @@ module type S = sig
with [t] to tie the knot on recursive operations. *)

module Result : sig
@@ portable
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, since there is @@ portable at the begining of this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we will add documentation in another PR.

Comment on lines +1063 to +1070
| Sig_module (id, pres, md, rec_, vis) ->
let md_modalities =
md.md_modalities
|> Mode.Modality.Value.to_const_exn
|> (fun then_ -> Mode.Modality.Value.Const.concat ~then_ modalities)
|> Mode.Modality.Value.of_const
in
let md = {md with md_modalities} in
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in #3943 , now include S @@ modalities is properly implemented, we can mention it in syntax.md.

| Sig_module (id, pres, md, rec_, vis) ->
let md_modalities =
md.md_modalities
|> Mode.Modality.Value.to_const_exn
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include S @@ modalities, here S must be a constant modality (as opposed to inferred modalities). See maybe_infer_modalities in this file.

@@ -160,7 +148,7 @@ module type S = sig
end

module M : S = struct
let foo = fun x -> x
let foo @ nonportable = fun x -> x
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the printing still zaps to id. Now with #3922 , I think the default should be zapping to floor.

Comment on lines +393 to +394
module M' = (M @ nonportable)
let () = portable_use M'.length
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, M has an explicit signature that doesn't have @@ portable.

Copy link
Contributor

@lukemaurer lukemaurer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not entirely reviewed but just have env.ml and env.mli left.

@@ -2977,6 +3038,8 @@ and type_open_decl_aux ?used_slot ?toplevel funct_body names env od =
in
let md = { mod_desc = Tmod_ident (path, lid);
mod_type = Mty_alias path;
mod_mode = Value.(disallow_right max), None;
(* CR zqian: maybe put in the correct mode and locks. *)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up here?

Copy link
Contributor Author

@riaqn riaqn May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm being lazy and not putting in the correct mode. This is fine because the type checking only cares about newenv, and this open_descr is only used in typedtree.

However, it's probably also not much work to do it properly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather the typedtree have the correct info if it's not much work—at some point, odoc's going to want to know these things from the typedtree as well. (But that won't be for a bit so if this turns out to be hard I won't lose any sleep over it)

@@ -799,7 +857,6 @@ and signature_components :
let id, item, shape_map, present_at_runtime =
match sigi1, sigi2 with
| Sig_value(id1, valdecl1, _) ,Sig_value(_id2, valdecl2, _) ->
let mmodes = append_ldot (Ident.name id1) mmodes in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear to me why we don't need this anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now handled in child_modes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a call to child_modes though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled in Includecore.value_descriptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
modes Work on modes. There's some overlap with the `multicore` label, but not strictly so. typing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants