Skip to content

Add configuration to control whether let-punning is used #2746

Merged
EmileTrotignon merged 7 commits intoocaml-ppx:mainfrom
WardBrian:feat/2743-let-punning-configuration
Oct 30, 2025
Merged

Add configuration to control whether let-punning is used #2746
EmileTrotignon merged 7 commits intoocaml-ppx:mainfrom
WardBrian:feat/2743-let-punning-configuration

Conversation

@WardBrian
Copy link
Copy Markdown
Contributor

Adds option --let-binding-punning={preserve|always|never} to control whether ocamlformat outputs
let punning (open to suggestions on naming).

Default is preserve in all profiles to match the current release's behavior.

Closes #2743

Copy link
Copy Markdown
Collaborator

@EmileTrotignon EmileTrotignon left a comment

Choose a reason for hiding this comment

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

I think this is good, except maybe the documentation which could have an exemple and more explicit wording.

Comment thread doc/manpage_ocamlformat.mld Outdated
Name punning in extended let bindings preserve uses let-punning
only when it exists in the source. always uses let-punning whenever
possible. never never uses let-punning. The default value is
preserve.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I know this is not the style of the current documentation, but I think an example of what let punning is should included.

Also regarding the name, I thought a let-binding is any use of let, but this only concerns bindings with let operators ? This does not have to be reflected in the name, but the documentation should probably say something.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The problem is that it's not possible to add line breaks in the documentation, forcing the explanation to be concise. This is the output of cmdliner, which removes line breaks and reflow the text.

I'd be totally in favor of a new way of generating the list of option in the documentation and taking advantage of Odoc markup (paragraph, lists, text blocks). The cmdliner output must stay thought.

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.

I've changed the name to your suggestion from the issue, letop-punning. I also made an attempt at including an example in this doc, let me know what you think!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The doc looks good to me ! It's a bit messed up because of the way we pass it through cmdliner but that can be improved independently.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The problem is that it's not possible to add line breaks in the documentation, forcing the explanation to be concise. This is the output of cmdliner, which removes line breaks and reflow the text.
I wasn't aware, thanks. I agree it would be better with improvement

Copy link
Copy Markdown
Collaborator

@Julow Julow left a comment

Choose a reason for hiding this comment

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

Other than the discussion on documentation, I'm totally in favor of this change :)

Comment thread doc/manpage_ocamlformat.mld Outdated
Name punning in extended let bindings preserve uses let-punning
only when it exists in the source. always uses let-punning whenever
possible. never never uses let-punning. The default value is
preserve.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The problem is that it's not possible to add line breaks in the documentation, forcing the explanation to be concise. This is the output of cmdliner, which removes line breaks and reflow the text.

I'd be totally in favor of a new way of generating the list of option in the documentation and taking advantage of Odoc markup (paragraph, lists, text blocks). The cmdliner output must stay thought.

Comment thread lib/Conf.ml Outdated
; Decl.Value.make ~name:"always" `Always
"$(b,always) uses let-punning whenever possible."
; Decl.Value.make ~name:"never" `Never
"$(b,never) never uses let-punning." ]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

To make the difference between never and preserve clearer, I think you should say that existing code using let-punning will be rewritten without.

@WardBrian
Copy link
Copy Markdown
Contributor Author

Question:
I have the code prepared to also have this setting work for extension lets (e.g. let%bar x = x in ... becomes let%bar x in ....

Would this be better in this PR or in another PR? I don't currently have 'preserve' working for these, because that requires parser changes, but I think I could get that done

@Julow
Copy link
Copy Markdown
Collaborator

Julow commented Oct 30, 2025

I have the code prepared to also have this setting work for extension lets (e.g. let%bar x = x in ... becomes let%bar x in ....

That's fantastic !
I usually prefer several smaller PRs instead of a large one. Now that this one is reviewed, let's merge it :)

@EmileTrotignon
Copy link
Copy Markdown
Collaborator

Would this be better in this PR or in another PR? I don't currently have 'preserve' working for these, because that requires parser changes, but I think I could get that done

This is the type of parser changes that we try to do, so please do that ! Don't forget to have the loc of every node you add (maybe in that case all the locs are here already)

Thats because comments moving around are caused by not knowing the loc of certain tokens, and therefore having no way of knowing if the comment was before or after said token.

@EmileTrotignon
Copy link
Copy Markdown
Collaborator

I am merging once the CI is green (except for that benchmark which always fails)

@WardBrian
Copy link
Copy Markdown
Contributor Author

Thanks @EmileTrotignon, I was wondering about that one. The rest all look like they passed

@EmileTrotignon
Copy link
Copy Markdown
Collaborator

Yeah, I think the benchmark has a dependency on libpcre3dev which is a recently defunct debian package.

Lets merge !

@EmileTrotignon EmileTrotignon merged commit 1609f04 into ocaml-ppx:main Oct 30, 2025
11 of 12 checks passed
Julow added a commit to Julow/opam-repository that referenced this pull request Mar 17, 2026
CHANGES:

### Highlight

- \* Support OCaml 5.5 syntax
  (ocaml-ppx/ocamlformat#2772, ocaml-ppx/ocamlformat#2774, ocaml-ppx/ocamlformat#2775, ocaml-ppx/ocamlformat#2777, ocaml-ppx/ocamlformat#2780, ocaml-ppx/ocamlformat#2781, ocaml-ppx/ocamlformat#2782, ocaml-ppx/ocamlformat#2783, @Julow)
  The update brings several tiny changes, they are listed below.

- \* Update Odoc's parser to 3.0 (ocaml-ppx/ocamlformat#2757, @Julow)
  The indentation of code-blocks containing OCaml code is reduced by 2 to avoid
  changing the generated documentation. The indentation within code-blocks is
  now significative in Odoc and shows up in generated documentation.

### Added

- Added option `letop-punning` (ocaml-ppx/ocamlformat#2746, @WardBrian) to control whether
  punning is used in extended binding operators.
  For example, the code `let+ x = x in ...` can be formatted as
  `let+ x in ...` when `letop-punning=always`. With `letop-punning=never`, it
  becomes `let+ x = x in ...`. The default is `preserve`, which will
  only use punning when it exists in the source.
  This also applies to `let%ext` bindings (ocaml-ppx/ocamlformat#2747, @WardBrian).

- Support the unnamed functor parameters syntax in module types
  (ocaml-ppx/ocamlformat#2755, ocaml-ppx/ocamlformat#2759, @Julow)
  ```ocaml
  module type F = ARG -> S
  ```
  The following lines are now formatted as they are in the source file:
  ```ocaml
  module M : (_ : S) -> (_ : S) -> S = N
  module M : S -> S -> S = N
  (* The preceding two lines are no longer turned into this: *)
  module M : (_ : S) (_ : S) -> S = N
  ```

### Fixed

- Fix dropped comment in `(function _ -> x (* cmt *))` (ocaml-ppx/ocamlformat#2739, @Julow)

- \* `cases-matching-exp-indent=compact` does not impact `begin end` nodes that
  don't have a match inside. (ocaml-ppx/ocamlformat#2742, @EmileTrotignon)
  ```ocaml
  (* before *)
  begin match () with
  | () -> begin
    f x
  end
  end
  (* after *)
  begin match () with
  | () -> begin
      f x
    end
  end
  ```

- `Ast_mapper` now iterates on *all* locations inside of Longident.t,
  instead of only some.
  (ocaml-ppx/ocamlformat#2737, @v-gb)

- Remove line break in `M with module N = N (* cmt *)` (ocaml-ppx/ocamlformat#2779, @Julow)

### Internal

- Added information on writing tests to `CONTRIBUTING.md` (ocaml-ppx/ocamlformat#2838, @WardBrian)

### Changed

- indentation of the `end` keyword in a match-case is now always at least 2. (ocaml-ppx/ocamlformat#2742, @EmileTrotignon)
  ```ocaml
  (* before *)
  begin match () with
  | () -> begin
    match () with
    | () -> ()
  end
  end
  (* after *)
  begin match () with
  | () -> begin
    match () with
    | () -> ()

- \* use shortcut `begin end` in `match` cases and `if then else` body. (ocaml-ppx/ocamlformat#2744, @EmileTrotignon)
  ```ocaml
  (* before *)
  match () with
  | () -> begin
      match () with
      | () ->
    end
  end
  (* after *)
  match () with
  | () ->
    begin match () with
      | () ->
    end
  end
  ```

- \* Set the `ocaml-version` to `5.4` by default (ocaml-ppx/ocamlformat#2750, @EmileTrotignon)
  The main difference is that the `effect` keyword is recognized without having
  to add `ocaml-version=5.3` to the configuration.
  In exchange, code that use `effect` as an identifier must use
  `ocaml-version=5.2`.

- The work to support OCaml 5.5 come with several improvements:
  + Improve the indentation of `let structure-item` with the
    `[@ocamlformat "disable"]` attribute.
    `let structure-item` means `let module`, `let open`, `let include` and
    `let exception`.
  + `(let open M in e)[@A]` is turned into `let[@A] open M in e`.
  + Long `let open ... in` no longer exceed the margin.
  + Improve indentation of `let structure-item` within parentheses:
    ```ocaml
    (* before *)
    (let module M = M in
    M.foo)
    (* after *)
    (let module M = M in
     M.foo)
    ```
Julow added a commit to Julow/opam-repository that referenced this pull request Mar 17, 2026
CHANGES:

### Highlight

- \* Support OCaml 5.5 syntax
  (ocaml-ppx/ocamlformat#2772, ocaml-ppx/ocamlformat#2774, ocaml-ppx/ocamlformat#2775, ocaml-ppx/ocamlformat#2777, ocaml-ppx/ocamlformat#2780, ocaml-ppx/ocamlformat#2781, ocaml-ppx/ocamlformat#2782, ocaml-ppx/ocamlformat#2783, @Julow)
  The update brings several tiny changes, they are listed below.

- \* Update Odoc's parser to 3.0 (ocaml-ppx/ocamlformat#2757, @Julow)
  The indentation of code-blocks containing OCaml code is reduced by 2 to avoid
  changing the generated documentation. The indentation within code-blocks is
  now significative in Odoc and shows up in generated documentation.

### Added

- Added option `letop-punning` (ocaml-ppx/ocamlformat#2746, @WardBrian) to control whether
  punning is used in extended binding operators.
  For example, the code `let+ x = x in ...` can be formatted as
  `let+ x in ...` when `letop-punning=always`. With `letop-punning=never`, it
  becomes `let+ x = x in ...`. The default is `preserve`, which will
  only use punning when it exists in the source.
  This also applies to `let%ext` bindings (ocaml-ppx/ocamlformat#2747, @WardBrian).

- Support the unnamed functor parameters syntax in module types
  (ocaml-ppx/ocamlformat#2755, ocaml-ppx/ocamlformat#2759, @Julow)
  ```ocaml
  module type F = ARG -> S
  ```
  The following lines are now formatted as they are in the source file:
  ```ocaml
  module M : (_ : S) -> (_ : S) -> S = N
  module M : S -> S -> S = N
  (* The preceding two lines are no longer turned into this: *)
  module M : (_ : S) (_ : S) -> S = N
  ```

### Fixed

- Fix dropped comment in `(function _ -> x (* cmt *))` (ocaml-ppx/ocamlformat#2739, @Julow)

- \* `cases-matching-exp-indent=compact` does not impact `begin end` nodes that
  don't have a match inside. (ocaml-ppx/ocamlformat#2742, @EmileTrotignon)
  ```ocaml
  (* before *)
  begin match () with
  | () -> begin
    f x
  end
  end
  (* after *)
  begin match () with
  | () -> begin
      f x
    end
  end
  ```

- `Ast_mapper` now iterates on *all* locations inside of Longident.t,
  instead of only some.
  (ocaml-ppx/ocamlformat#2737, @v-gb)

- Remove line break in `M with module N = N (* cmt *)` (ocaml-ppx/ocamlformat#2779, @Julow)

### Internal

- Added information on writing tests to `CONTRIBUTING.md` (ocaml-ppx/ocamlformat#2838, @WardBrian)

### Changed

- indentation of the `end` keyword in a match-case is now always at least 2. (ocaml-ppx/ocamlformat#2742, @EmileTrotignon)
  ```ocaml
  (* before *)
  begin match () with
  | () -> begin
    match () with
    | () -> ()
  end
  end
  (* after *)
  begin match () with
  | () -> begin
    match () with
    | () -> ()

- \* use shortcut `begin end` in `match` cases and `if then else` body. (ocaml-ppx/ocamlformat#2744, @EmileTrotignon)
  ```ocaml
  (* before *)
  match () with
  | () -> begin
      match () with
      | () ->
    end
  end
  (* after *)
  match () with
  | () ->
    begin match () with
      | () ->
    end
  end
  ```

- \* Set the `ocaml-version` to `5.4` by default (ocaml-ppx/ocamlformat#2750, @EmileTrotignon)
  The main difference is that the `effect` keyword is recognized without having
  to add `ocaml-version=5.3` to the configuration.
  In exchange, code that use `effect` as an identifier must use
  `ocaml-version=5.2`.

- The work to support OCaml 5.5 come with several improvements:
  + Improve the indentation of `let structure-item` with the
    `[@ocamlformat "disable"]` attribute.
    `let structure-item` means `let module`, `let open`, `let include` and
    `let exception`.
  + `(let open M in e)[@A]` is turned into `let[@A] open M in e`.
  + Long `let open ... in` no longer exceed the margin.
  + Improve indentation of `let structure-item` within parentheses:
    ```ocaml
    (* before *)
    (let module M = M in
    M.foo)
    (* after *)
    (let module M = M in
     M.foo)
    ```
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.

Bug: let punning changed in 0.27/0.28

3 participants