Skip to content

Oxcaml: Support for modes - #1454

Open
art-w wants to merge 2 commits into
ocaml:masterfrom
art-w:oxcaml-modes
Open

Oxcaml: Support for modes#1454
art-w wants to merge 2 commits into
ocaml:masterfrom
art-w:oxcaml-modes

Conversation

@art-w

@art-w art-w commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This PR adds support for rendering OxCaml modes. Most of the complexity comes from the heuristics copied from the OxCaml compiler to omit modes which are either legacy, implied by other axes, or inferred from the context (for arrow types, e.g. a @ local -> b -> c -> d is internally represented as a @ local -> (b -> (c -> d @ local) @ local) @ local because a partial application of the first argument a @ local must return a local closure (otherwise the local value could escape its scope)... but since the @ local expansion is unreadable, we display the short form as it implies the long one)

Fixes #1417

@Leonidas-from-XIV Leonidas-from-XIV left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks nice, the logic determining what to leave out is actually more readable than the description makes it sound IMHO :)

| [ (arg, _) ] -> TypeExpr.Arrow (None, arg, res)
TypeExpr.Arrow
(None, (Tuple (List.map (fun (x, _mods) -> None, x) args), []), (res, []))
| [ (arg, _) ] -> TypeExpr.Arrow (None, (arg, []), (res, []))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder whether it wouldn't potentially make sense to make this into a record type (with some usefully named helper constructor functions) because the empty lists everywhere make it a bit verbose.

Comment thread src/loader/cmti.ml
let read_label = Cmi.read_label

let rec read_core_type env container ctyp =
read_core_type_modal env Cmi.legacy_modes container ctyp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why does this always read legacy_modes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This argument is the base value for the accumulator for the "currently implicit modes" (such that we don't need to display them). It gets updated when traversing an arrow type such that the argument' modes carry over implicit modes for the output (otherwise we would display the redundant @ local described in the PR). In OCaml legacy_modes is just ():unit since we don't make use of it :)

Comment thread src/model/lang.ml
| Any
| Alias of t * string
| Arrow of label option * t * t
| Arrow of label option * (t * Modes.t) * (t * Modes.t)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As mentioned above maybe it would be better to have a with_mode record type of sorts here.

val mode_both : int @ local -> int @ local
(** Modes on both argument and return. *)

val mode_multi : string @ local once -> string @ local unique

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What if the modes are flipped?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Flipped as in string @ once local -> string @ unique local? I'll add a test to show that similarly to the OxCaml compiler printer for the toplevel or inferred interfaces, the modes order is normalized by Cmi.tree_of_modes predefined order :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yep, that's exactly what I meant :)

Comment thread src/loader/cmt.ml
let type_ =
match Cmi.read_type_expr env expr.exp_type with
| Arrow (_, _, t) -> t
| Arrow (_, _, (t, _)) -> t

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there anything subtle here? Can the first argument have any mode that we're throwing away?

@panglesd panglesd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I will need a bit more time to review the loading, since it is touching the internals of oxcaml (eg I'm not sure what zap_to_legacy is) but the rest is solid, and very well tested! Thanks!

Comment thread src/loader/cmi.ml
| _, _ -> Some modes.portability
in
let diff = Mode.Alloc.Const.diff modes Mode.Alloc.Const.legacy in
let diff = { diff with forkable; yielding; contention; portability } in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe it would be nice to fully destruct diff here: It would make a compiler error if a new axis is introduced as a field in the record, warning us that there is a line to add in the list below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I realize only now this is a copy of an oxcaml function, but I still think we could modify it to be warned when a new field is added.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure to see where this module comes from. Maybe a leftover from a previous organization where things were grouped into modules?

Comment thread src/loader/cmi.ml
arg.ca_type, read_modalities Immutable arg.ca_modalities

let tree_of_modes (modes : Mode.Alloc.Const.t) : string list =
(* Same as the OxCaml's [Printtyp.tree_of_modes]: axes whose value is legacy

@panglesd panglesd Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In today's OxCaml's main, tree_of_modes has been moved to Out_type. Maybe we could avoid naming the module containing the function?

Suggested change
(* Same as the OxCaml's [Printtyp.tree_of_modes]: axes whose value is legacy
(* Same as the OxCaml's [tree_of_modes]: axes whose value is legacy

Comment thread src/loader/cmi.ml
let curried_acc modes arg_mode =
Ctype.curry_mode modes (Mode.Alloc.zap_to_legacy arg_mode)

let mode_is_implied modes res_mode =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

modes could be named implied_mode, if I understand correctly. And I think the same in other functions would make them easier to read!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support OxCaml modes

4 participants