Skip to content

include functor support for OxCaml - #1452

Open
Leonidas-from-XIV wants to merge 23 commits into
ocaml:masterfrom
Leonidas-from-XIV:include-functor
Open

include functor support for OxCaml#1452
Leonidas-from-XIV wants to merge 23 commits into
ocaml:masterfrom
Leonidas-from-XIV:include-functor

Conversation

@Leonidas-from-XIV

Copy link
Copy Markdown
Member

This PR is a follow up to #1368 and adds support for include functor where it now displays that the items were included via the functor.

@Leonidas-from-XIV
Leonidas-from-XIV force-pushed the include-functor branch 3 times, most recently from 18499bf to ca9079a Compare July 1, 2026 08:28
@Leonidas-from-XIV
Leonidas-from-XIV marked this pull request as ready for review July 1, 2026 12:22

@art-w art-w 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.

Thanks a lot, this looks great! I only have minor feedback but nothing blocking :)

Comment thread src/xref2/tools.ml Outdated
Comment thread src/document/generator.ml Outdated
(** This is a Module where the type is named and then included. *)
module type Make = (_ : sig type t end) -> sig type included end
type t
include functor Make

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.

Out of curiosity, can we test the behavior with (** @inline *)?

@jonludlam

Copy link
Copy Markdown
Member

I think there's a chunk of logic missing from this PR. At the moment, all it does AFAICT is get the expansion of the functor and splice it into the expansion of the include. While this works for simple examples, it's not sufficient for more involved cases.

As a simple example, consider this:

module F ( I : sig type t end ) = struct
  type myt = I.t
end

module M = struct
  type t = float
  include functor F
end

Running this through ocamlc -c -i gives what we ought to end up with (roughly):

module F : functor (I : sig type t end) -> sig type myt = I.t end
module M : sig type t = float type myt = float end

If we just simply get the signature of the functor body and splice it in, we end up something more like:

...
module M : sig
  type t = float
  type myt = I.t
end

and obviously that I.t need something done to it. If you run it through odoc as of this branch, it chokes on exactly this:

Exception Failure("Not_found: I/3") handling type_expr: resolved(I/3.t)
backtrace:
Raised at Stdlib.failwith in file "stdlib.ml" (inlined), line 39, characters 17-33
...

I suspect the most straightforward implementation of this would be to do exactly what the docs suggest it does internally. Treat it more like:

module M = struct
  module __DUMMY__ = struct
    type t = float
  end

  include __DUMMY__
  include F(__DUMMY__)
end

We can ensure that the dummy module is hidden, meaning that the include will just inline the contents,
so it should end up rendering in a sensible way. I'm not sure whether it would be best to do this as
a preprocessing step or whether we might be able to rewrite the loaders with an accumulator so we can
do this sort of mangling on loading of the cmt/cmtis.

@Leonidas-from-XIV
Leonidas-from-XIV force-pushed the include-functor branch 4 times, most recently from 5997c73 to ac92802 Compare August 13, 2026 13:51
@Leonidas-from-XIV

Leonidas-from-XIV commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

I've now implemented @jonludlam's proposal of compiling down the include functor to basically a BODY module and a functor application. This has some extra steps to deal with odd cases like anonymous functors but seems to work fine. I've also incorporated the example and it compiles now properly.

The way it works is by parsing the include functor as Include.Functor expression (with a path or module type) and then post-processing the structure items if there were include functors and compiling them to Include.Functor with paths (to be able to use `Apply thus avoiding having to infer types) in the shape of <GENERATED_FUNCTOR_MODULE>(GENERATED_WRAPPER_MODULE>). I'm also retaining the original expression, so the include functor <X> will be rendered as include functor <X> instead of include functor APPLY_23(BODY_12).

However there's one outstanding issue: while the items coming from the functor expansions contain the right path (e.g. val functor_output = APPLY_23(BODY_12).functor_output) this is is quite ugly and messy. If I hide the generated modules the cross-reference disappears and just the value remains. This might be fine but also somewhat less informative.

What I could imagine would be best if I could hide the generated modules but also tell the cross-linker that I want the item to be cross-referenced to the ("include") functor, not the generated module. Is there a way to tell odoc to "redirect" paths?

@Leonidas-from-XIV

Copy link
Copy Markdown
Member Author

Odoc has a mechanism to specify a substitution via @canonical which is represented by the `Canonical constructor, however that can only be constructed with resolved modules, whereas the Typedtree transformation is happening before. Maybe it is possible to resolve these modules early.

@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'll continue the review, but here is already some comments. I think the Make functor should be made a little bit more complex, and in particular use its argument.

module Make (T : sig type t end) : sig type included = T.t end

Now suppose it is used in the following way:

type t
include functor Make

From my understanding of the oxcaml feature, the rendered signature should be:

type t
> include functor Make
  type included = t

(where > is for the opened include accordion 🪗 )
In my testing, that is not the case!

I wonder if it is possible to fix that using the current "sugar". I believe the "desugared" form of the example above (extended with a value and a module) could use aliases, for types, modules and module types, instead of relying on the DUMMY__ include :

type t
val f : t
module X : S
module DUMMY__ : sig
    type nonrec t = t
    val f : t
    module X = X
  end

  module STRUCT__Make : Make
  include module type of STRUCT__Make(DUMMY__)
end

What do you think?

Also, definitely we want those generated modules to be hidden. As @jonludlam mentioned, that would turn the "inlining" invisible if we keep it, and odoc would use links to a visible alias.

Also note that while I'd like to investigate the alternate sugar, the current version is better than no support, and as such (I still need more review of course) I would not block the merge on the problem noted above!

@panglesd

Copy link
Copy Markdown
Contributor

I had a go at the "alias desugaring" idea: https://github.com/panglesd/odoc/tree/include-functor-alias-desugaring

It looks a reasonable first solution, I think. Maybe implementing the "include functor expansion" mechanism in odoc would be better, but slightly more invasive...

jonludlam added a commit to jonludlam/odoc that referenced this pull request Aug 26, 2026
The synthetic module standing in for the functor argument held copies of the
preceding items.  Since it is hidden, everything the functor's expansion
inherited from its argument came out as `BODY__n.t` -- a hidden path, which
`Link.type_decl` resolves by looking through it, landing on the copy's own
abstract declaration.  So a functor that used its argument, as
`Comparable.Make` does, lost exactly the part worth documenting.

Alias the items instead: a type's manifest points at the real type, a module
becomes an alias of the real module, a module type a path to it.  `BODY__n.t`
then reduces to the enclosing signature's own `t`, and is rendered and linked
as such.

Class types are not aliased.  A functor can refer to one from its argument, but
odoc does not chase class type aliases the way it chases type manifests, so
such a reference is still left printing the wrapper's hidden name.  That is a
pre-existing limitation, reachable without `include functor`:

    module Hidden__thing : sig class type ct = object method m : int end end
    type from_class = Hidden__thing.ct

Suggested by @panglesd in ocaml#1452.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jonludlam added a commit to jonludlam/odoc that referenced this pull request Aug 26, 2026
Rather than put the functor arguments in a module and `include` it,
leave them where they were and construct a synthetic module that's
just full of aliases to the items.

Suggested by @panglesd in ocaml#1452.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jonludlam and others added 6 commits August 31, 2026 15:50
Four new cases, plus two existing ones strengthened:

- `Include_functor_argument_shapes` exercises everything the expansion of the
  functor can inherit from its argument - plain, parameterised and
  anonymously parameterised types, a submodule and a module type.
- `Multiple_include_functors` has two `include functor`s with an item between
  them, and `Include_functor_not_last` a single one with an item after it.
- `Include_functor_named_type`'s functor and `Oxcaml_impl.Include_functor`'s
  now use their argument.  The structure side had no case that did, so the
  argument being ignored would have gone unnoticed there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Simplifies the desugaring of:

    include functor Make

which in a signature became

    module INCLUDE__1 : <module type of Make>                            (hidden)
    module APPLIED__2 : module type of struct include INCLUDE__1(BODY__3) end
    include APPLIED__2                                                   (hidden)

where all that `APPLIED__2` achieves is giving the application a name to be
referred to by.  But `INCLUDE__1` is already a path, so the application can be
written as one directly, as the structure reader has always done:

    module INCLUDE__1 : <module type of Make>                            (hidden)
    include INCLUDE__1(BODY__3)

That drops one generated module per `include functor`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An `include functor` in a structure need not name its functor:

    type t
    include functor (functor (T : sig type t end) -> struct type included = T.t end)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrapper module was allocated once per signature and shared, the enclosing
signature was rebuilt as an `include` of it, and the include functors were
partitioned out of the fold and re-emitted at the end. This had 2 issues:

- the fold rebound the accumulator instead of appending to it, so only the
  include functors yielded by the *final* item of a signature survived.  In
  practice that means an `include functor` was silently dropped unless nothing
  followed it: writing a single one and then a type below it lost the whole
  include, and of two in a row only the second was kept.
- the shared wrapper was built from the non-include-functor items only, so a
  second `include functor` could not see the output of the first, and the
  reference to it leaked the hidden wrapper's name into the rendered output.

Instead notice the `include functor` during the fold over a signature items
and pass the accumulator directly. This ends up reverting the threading
through of the `include_functor_wrapper` parameter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rather than put the functor arguments in a module and `include` it,
leave them where they were and construct a synthetic module that's
just full of aliases to the items.

Suggested by @panglesd in ocaml#1452.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Leonidas-from-XIV

Copy link
Copy Markdown
Member Author

I've ended up cherry-picking the commits from @jonludlam's branch, which among an alternative desugaring also solves another real problem that I didn't think of: the BODY module needs to be re-created for every include functor application as one include functor application might depend on items that were generated by a preceding include functor application (e.g. imagine a Identifier include functor that generates (amongst others) a type key and a then a Map include functor which consumes as a type key, the latter needs to see the output of the former).

Also added an additional example of when the body of the module contains an include (a potential issue noticed by @panglesd) and rebased on master (thus running on Oxcaml minus39).

These were obsoleted by Jon's code and the PR pulled out of this was
rejected so no need to keep them around.

@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 have some small comments on the included items in the wrapper module, which needs some testing and (I would bet but not a lot) some implementation.

Otherwise, very good work! Thanks!

Comment thread src/loader/cmi.ml Outdated
Comment on lines +1709 to +1713
| Value _ | ModuleSubstitution _ | ModuleTypeSubstitution _ | Open _
| TypeSubstitution _ | TypExt _ | Exception _ | Comment _ ->
(* Nothing in the expansion of [F(BODY__n)] can refer to these. *)
acc
| Class _ | ClassType _ ->

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.

Nothing can refer to them, but without including them, we apply F(BODY_n) without BODY_n having module type what F is expecting.

Maybe that is fine, I don't think odoc will check that as it assumes the compiler has already enforced this, however I think it is worth adding tests (as for instance if later odoc requires BODY_n to have the right signature.
But maybe that is not fine! What happens to the values, if the input module is stored in the expansion?

I think it would be good to add a test with values, ..., classes in the input to the include functor. Moreover, the input functor should alias its input, to test the expansion of the situation described above.

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.

Oops, yes, you're right. Nothing stopping the functor from just making an alias of its argument, and then we'd lose all of those other items. My mistake, sorry!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added a test for class, class types and aliases which exposes this issue:

  • The classes and class types refer to the generated BODY__n module. It's somewhat surprising given BODY__n should be hidden, I think there's some is_hidden check missing in some place.
  • The Aliased module is constructed properly but indeed missing the class types.

Comment thread src/loader/cmi.ml
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.

4 participants