Skip to content

Modular explicits #2456

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 25 commits into
base: main
Choose a base branch
from
Open

Modular explicits #2456

wants to merge 25 commits into from

Conversation

samsa1
Copy link

@samsa1 samsa1 commented Apr 16, 2024

First draft of modular explicit.

Currently labels are not supported and modes are also not handled.

More testing and code refactoring would need to be done in order to be production ready, thus this PR should not be merged !

However feel free to hack a bit with modular explicit in order to give feedback about syntax, GADTs filtering, typing bugs

@samsa1
Copy link
Author

samsa1 commented Apr 18, 2024

The current version should be able to pass tests however the type display is quite horrible. The implementation of shadowing display on the upstream version is much nicer to look which leads to better results with modular explicits.

@samsa1 samsa1 force-pushed the modular-explicit branch from 273835a to 0d029fc Compare April 19, 2024 12:35
@samsa1 samsa1 requested a review from Ekdohibs as a code owner April 19, 2024 12:35
@samsa1 samsa1 force-pushed the modular-explicit branch 2 times, most recently from 5f1c4d2 to 607e124 Compare April 26, 2024 09:50
@samsa1
Copy link
Author

samsa1 commented Apr 26, 2024

There is a bug related to layout that I've detected but haven't managed to fix yet.

On another notice an extension of modular explicit with labels is available at :
https://github.com/samsa1/flambda-backend/tree/modular-explicit-labels

Copy link
Collaborator

@lpw25 lpw25 left a comment

Choose a reason for hiding this comment

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

I've reviewed as far as unification. The main issue is that unification needs to enforce sharing between its inputs to ensure principality. This requires a change in how you are representing the identifiers that are bound by Tfunctor. It's really quite hard to find examples that this breaks -- I've included one in my comments -- but it is a fundamental part of how the type-checker works and I think your PR needs to respect it.

List.iter (fun (_n, ty) -> reify env ty) (fl1 @ fl2);
(* if !generate_equations then List.iter2 (mcomp !env) tl1 tl2 *)
end;
let old_env = !env in
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not do the updates of env inside the call to enter_functor_for rather than remembering the old value and passing that in?

let mty1 = !modtype_of_package old_env Location.none p1 fl1 in
let mty2 = !modtype_of_package old_env Location.none p2 fl2 in
env := Env.add_module id1 Mp_present mty1 !env;
env := Env.add_module id2 Mp_present mty2 !env;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think it is a good idea to leave these identifiers in the environment after the unification is finished. They can't be looked up by name, so it's probably sound, but it's surprising and likely to lead to bugs in weird places. For example:

module type S = sig type t val plus : t -> t -> t end

type ('a, 'b, 'c) eq = Refl : ('a, 'a, {Norm : S} -> Norm.t) eq

let foo (type a) (e : (a, int, {M : S} -> M.t) eq) =
  match e with
  | Refl -> Norb.foo

currently gives:

File "foo.ml", line 7, characters 12-20:
7 |   | Refl -> Norb.foo
                ^^^^^^^^
Error: Unbound module Norb
Hint: Did you mean Norm?

with a hint that refers to a module that shouldn't be in scope. In general it is a good idea to be hygienic around issues of scoping.

Copy link
Author

Choose a reason for hiding this comment

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

As the handling of the environment during unification is different on the upstream compiler this problem doesn't occur. However the method proposed below might be a good way to obtain a good result

@@ -3653,6 +3941,8 @@ and unify3 env t1 t1' t2 t2' =
raise_for Unify (Obj (Abstract_row First))
| (_, _) -> raise_unexplained_for Unify
end;
if in_functor then
Transient_expr.set_desc tt1' d1;
Copy link
Collaborator

@lpw25 lpw25 May 1, 2024

Choose a reason for hiding this comment

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

Here and in some other places you break the property that unification makes types physically equal -- so that future mutations of one input affect the other. This breaks OCaml's model of the world and, in particular, it's approach to ensuring principality.

For example, the following code is not principal, and triggers an ambiguity error upstream if you use -principal, but this branch gives no error:

type float2 = float

let forty_two : float2 = 42.0

type 'a ty =
  | Float : float ty
  | String : string ty

let bar (type a) p (ty : a ty) (a : a) =
  match ty with
  | Float ->
      let r = ref None in
      r.contents <- Some forty_two;
      let bar () =
        match r.contents with
        | None -> 3.5
        | Some x -> x
      in
      r.contents <- Some a;
      bar ()
  | String -> assert false

I think you need to change your approach slightly so that the identifiers bounds by Tfunctor nodes are mutable such that one can be mutated to be permanently considered equal to another. This would allow you to have unification continue to make types physically equal.

I even wonder if it might be better for these identifiers to be a separate type from Ident.t with their own constructor in Path.t and their own table in Env.t.

Copy link
Author

Choose a reason for hiding this comment

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

Having their own table in Env.t would allow a simple fix to not keeping things in scope. Thus making it a separate type from Ident.t could be an efficient method.

Copy link
Author

Choose a reason for hiding this comment

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

The problem of principality should now be solved due to the implementation of an unscoped type for identifiers that are unifiable.

@mshinwell mshinwell changed the title Modular explicit Modular explicits May 2, 2024
@samsa1 samsa1 force-pushed the modular-explicit branch from 607e124 to f130681 Compare May 14, 2024 12:38
@lpw25
Copy link
Collaborator

lpw25 commented May 16, 2024

From a quick scan these latest commits look really good. I'm away next week, but I'll try to do a more thorough review of this the week after.

@samsa1 samsa1 force-pushed the modular-explicit branch 3 times, most recently from 7a41100 to a090088 Compare July 9, 2024 11:59
Copy link
Collaborator

@lpw25 lpw25 left a comment

Choose a reason for hiding this comment

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

I had hoped to get through this whole PR by now (ideally quite a lot before now), but I've still got ctype.ml and parts of typecore.ml to get through. I'm posting what I have now since I'm about to take a few days off, there are already plenty of comments and you've waited long enough.

@@ -4336,6 +4342,19 @@ strict_function_or_labeled_tuple_type:
{ let ty, ltys = $3 in
ptyp_ltuple $sloc ((Some label, ty) :: ltys)
}
(* TODO handle modes *)
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 it should be pretty easy to support modes on these, and you'll need to so that you can give a most general type to functions with first-class module parameters. Here you just need to copy the mode stuff from the code just above.

in
let type_pack pack =
let pack = Typetexp.transl_simple_type env ~new_var_jkind:Any
~closed:false Alloc.Const.legacy pack in
Copy link
Collaborator

Choose a reason for hiding this comment

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

(Weird indentation)

@@ -299,6 +303,17 @@ val filter_arrow: Env.t -> type_expr -> arg_label -> force_tpoly:bool ->
[force_poly] is false then the usual invariant that the
argument type be a [Tpoly] node is not enforced. Raises
[Filter_arrow_failed] instead of [Unify]. *)

type filtered_functor =
{ ret : (arrow_desc * Ident.unscoped * (Path.t * (Longident.t * type_expr) list) * type_expr) option;
Copy link
Collaborator

Choose a reason for hiding this comment

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

ret is a very confusing name for something that includes both the argument and the return type of the functor.

when is_unpack (arg_label, pat) && could_be_functor env ty_expected ->
let typed_arg_label, pat =
Typetexp.transl_label_from_pat arg_label pat
in
Copy link
Collaborator

Choose a reason for hiding this comment

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

The logic in this next section is a little convoluted. I think it could be simplified by doing things in two steps:

  1. Inspecting the expected type and producing an expected type for the pattern and an expected type the rest of the function (which includes an Ident.unscoped that should be instantiated away for the parameter's identifier). Both of these expected types can just be unification variables if there is no useful information in the expected type.
  2. Typing the pattern and unifying it with the expected pattern type -- giving an error if the signature cannot be inferred. This part can be split off into its own type_functor_argument function.

Copy link

github-actions bot commented Oct 7, 2024

Parser Change Checklist

This PR modifies the parser. Please check that the following tests are updated:

  • parsetree/source_jane_street.ml

This test should have examples of every new bit of syntax you are adding. Feel free to just check the box if your PR does not actually change the syntax (because it is refactoring the parser, say).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants