Skip to content

Zero alloc annotation for OxCaml - #1422

Merged
jonludlam merged 24 commits into
ocaml:masterfrom
Leonidas-from-XIV:zero-alloc-annotation
Jun 4, 2026
Merged

Zero alloc annotation for OxCaml#1422
jonludlam merged 24 commits into
ocaml:masterfrom
Leonidas-from-XIV:zero-alloc-annotation

Conversation

@Leonidas-from-XIV

Copy link
Copy Markdown
Member

This PR aims to take the zero alloc annotation from the source and retain it in the parsed output.

Currently this PR supports the [@@zero_alloc] annotation on signatures. Next step would be to support the [@zero_alloc] annotation on bindings; not sure if it would make sense to add it in this PR or another one.

This is my first PR to Odoc so excuse me for being a bit lost, I'll happily fix up/change stuff.

cc @art-w

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

It looks good! I have a minor nitpick, mostly to be safe because I don't know if the additional O.cut could break existing docs. Do you prefer addressing the remaining TODOs for the cmti in a separate PR?

Comment thread src/document/generator.ml Outdated
Comment thread src/loader/doc_attr.ml Outdated

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

Thanks!

I've left a nitpick, a comment about payload support and one about not loading attributes in OxCaml.

Some small comments that probably require no change in this PR:

  • In the oxcaml doc, they use val[@zero_alloc] f : t rather than val f : t [@@zero_alloc] as in this PR (they are different syntax for the same thing). So maybe oxcaml users are more familiar with the former? But keeping as is is fine to me!
  • There is already some logic to extract special attributes (eg @alert and @deprecated). However, they are rendered differently (using odoc @-tags). It's a pity to have two ways to handle attributes, but for zero_alloc (and other oxcaml-specific attributes to come), I think I prefer your approach.

Comment thread src/model/lang.ml
Comment on lines +324 to +327
module Zero_alloc : sig
type t = Assume | Opt | Strict
end
type attr = Zero_alloc of Zero_alloc.t

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.

According to the docs, the only allowed payloads are opt, strict and arity <n>.

If I understand correctly "assume" does not correspond to the default. (Assume in a module type would be: do not check that the implementation do not allocate, but use that information in uses outside.)

Moreover, it seems possible to use multiple (valid) payloads.

So I think we have two options in terms of type representation: the "more correct" one

Suggested change
module Zero_alloc : sig
type t = Assume | Opt | Strict
end
type attr = Zero_alloc of Zero_alloc.t
module Zero_alloc : sig
type t = {opt : unit option ; strict: unit option; arity: int option }
end
type attr = Zero_alloc of Zero_alloc.t

or the one that uses the fact that it has already been validated by the oxcaml compiler:

Suggested change
module Zero_alloc : sig
type t = Assume | Opt | Strict
end
type attr = Zero_alloc of Zero_alloc.t
module Zero_alloc : sig
type t = Arity of n | Opt | Strict
end
type attr = Zero_alloc of Zero_alloc.t

Both are fine to me! And the second one is probably less work.

Here are some examples of zero_alloc uses in module types to "validate" the oxcaml doc:

# module type T = sig val[@zero_alloc] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc] end

(** The error says the accepted attributes {e for implementation}, not interface, unfortunately *)

# module type T = sig val[@zero_alloc gloubli-boulga] f : int -> int -> int end ;;
Warning 47 [attribute-payload]: illegal payload for attribute 'zero_alloc'.
It must be either 'assume', 'assume_unless_opt', 'strict', 'opt', 'opt strict', 'assume strict', 'assume never_returns_normally', 'assume never_returns_normally strict', 'assume error', 'ignore', 'arity <int_constant>', 'custom_error_message <string_constant>' or empty

(** assume is not allowed *)

# module type T = sig val[@zero_alloc assume] f : int -> int -> int end ;;
Error: zero_alloc assume attributes are not supported in signatures

(** strict, opt, and arity <n> (and combinations) are allowed *)

# module type T = sig val[@zero_alloc strict] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc strict] end
# module type T = sig val[@zero_alloc opt] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc opt] end
# module type T = sig val[@zero_alloc arity 1] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc arity 1] end
# module type T = sig val[@zero_alloc strict opt] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc strict opt] end
# module type T = sig val[@zero_alloc strict arity 2] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc strict] end
# module type T = sig val[@zero_alloc arity 1 opt strict] f : int -> int -> int end ;;
module type T =
  sig val f : int -> int -> int [@@zero_alloc strict opt arity 1] end
# module type T = sig val[@zero_alloc] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc] end
# module type T = sig val[@zero_alloc] f : int -> int -> int end ;;
module type T = sig val f : int -> int -> int [@@zero_alloc] end

Comment thread src/loader/doc_attr.ml Outdated
Comment thread src/loader/doc_attr.ml Outdated
@Leonidas-from-XIV

Copy link
Copy Markdown
Member Author

In the oxcaml doc, they use val[@zero_alloc] f : t rather than val f : t [@@zero_alloc] as in this PR (they are different syntax for the same thing). So maybe oxcaml users are more familiar with the former? But keeping as is is fine to me!

Very good point. I mostly did it this way as this is how the oxcaml toplevel prints it. So I assumed this is the way to go, but I will at least add a test-case to make sure that the val[@zero_alloc] variant is supported correctly.

@art-w

art-w commented May 6, 2026

Copy link
Copy Markdown
Contributor

In the oxcaml doc, they use val[@zero_alloc] f : t rather than val f : t [@@zero_alloc] as in this PR (they are different syntax for the same thing).

We received feedback that the later form looks nicer for documentation :)

@Leonidas-from-XIV

Copy link
Copy Markdown
Member Author

I've rewritten the code to parse the arguments in [@@zero_alloc], with better names and better self-contained. My testing has revealed that ignore, opt, strict, arity and custom_error_message are valid in signatures.

However there are cases where the parser silently ignores errors (e.g. "arity" used but no argument given), in that case I think it would be nice to do something. Probably exiting is too extreme so I didn't want to throw but it would be nice to emit a warning that something went wrong?

Or maybe its ok to silently continue because the OxCaml compiler will complain anyway? WDYT?

@panglesd

Copy link
Copy Markdown
Contributor

Or maybe its ok to silently continue because the OxCaml compiler will complain anyway? WDYT?

I would say it is ok to silently continue, as I would say those warnings are more the responsibility of the compiler.

But that made me think... Isn't this information already available in the compiler artefact? I have quickly checked in types.mli, and I have found something that looks like it, although I'm not sure (it is in a Wrapped functor that's instantiated and inlined 🫤 ).

If we can avoid the parsing ourself, that would be much better!

@panglesd

panglesd commented May 11, 2026

Copy link
Copy Markdown
Contributor

After setting up an OxCaml environment 😅 I was able to find that yes, the information is already there!

In cmi files, the "zero alloc" info is stored at vd.val_zero_alloc, and in cmti files, it is stored in vd.val_val.val_zero_alloc. For instance:

    let za = vd.val_val.val_zero_alloc |> Zero_alloc.get in
    let za = match za with
      | Default_zero_alloc -> _
      | Ignore_assert_all -> _
      | Check _ -> _
      | Assume _ -> _
    in
    Value.Zero_alloc za

using this info to create a lang, rather than reparse it from the attribute, seems better! And would remove all the parsing code.

@Leonidas-from-XIV

Copy link
Copy Markdown
Member Author

@panglesd Thanks for the pointers, they were very useful. I've implemented your changes, adapting the value type to be more aligned with the oxcaml compiler-libs version of it. However there's two concerns:

  1. The compiler-libs return the "computed" arity, not the specified one. So I have no way (bar checking the annotation manually) to distinguish whether an arity value was passed or not. So for now we always print the arity. This doesn't seem to be a problem per-se, but it is a bit messy. Maybe I could read the type and determine whether the arities match and omit it in such case?
  2. In the CMT case I don't always have a value_description. Maybe also not a problem, because the CMT loader calls Cmti.read_value_description which does the right thing.

@Leonidas-from-XIV

Copy link
Copy Markdown
Member Author

@panglesd Could you take another look and share your thoughts? I've rebased the branch on master which works on a minus31 OxCaml switch.

Comment thread src/document/generator.ml Outdated
Comment thread src/xref2/lang_of.ml Outdated
@panglesd

panglesd commented May 18, 2026

Copy link
Copy Markdown
Contributor

In the CMT case I don't always have a value_description. Maybe also not a problem, because the CMT loader calls Cmti.read_value_description which does the right thing.

Hmm, I think it is a problem, as I don't see value cmt loading calling into Cmti.read_value_description. However, after a quick slow look at oxcaml's typedtree, I think you can use Typedtree.let_bound_idents_with_modes_sorts_and_checks to get the ident -> Zero_alloc.t map.

Comment thread test/generators/html/Oxcaml.html
Comment thread src/loader/cmt.ml Outdated

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

Thanks a lot for the work!

I think the only thing that would need feedback from an actual user of oxcaml, is the added arity: Should we, as you suggested, omit it when it corresponds to the visible arity of the function?

Probably, but I would be more confident if I had experience using the zero_alloc tag myself!

Anyway, that can be in another PR: this one is already good as is. I left some small comment if you have time to do them, and then we can merge.

Comment thread src/loader/doc_attr.ml Outdated
Comment thread src/loader/cmt.ml Outdated
Signed-off-by: Marek Kubica <marek@tarides.com>
Signed-off-by: Marek Kubica <marek@tarides.com>
@jonludlam

Copy link
Copy Markdown
Member

Thanks @Leonidas-from-XIV !

@jonludlam
jonludlam merged commit 4ecf595 into ocaml:master Jun 4, 2026
11 checks passed
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.

6 participants