Skip to content

Commit 1609f04

Browse files
authored
Add configuration to control whether let-punning is used (#2746)
* Add configuration to control whether let-punning is used * Add tests for let punning configuration * Update manpage in documentation * CHANGES entry * Rename to letop-punning * More documentation with example * Fix CHANGES
1 parent 1646c68 commit 1609f04

23 files changed

+292
-7
lines changed

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ profile. This started with version 0.26.0.
66

77
## unreleased
88

9+
### Added
10+
11+
- Added option `letop-punning` (#2746, @WardBrian) to control whether
12+
punning is used in extended binding operators.
13+
For example, the code `let+ x = x in ...` can be formatted as
14+
`let+ x in ...` when `letop-punning=always`. With `letop-punning=never`, it
15+
becomes `let+ x = x in ...`. The default is `preserve`, which will
16+
only use punning when it exists in the source.
17+
918
### Fixed
1019

1120
- Fix dropped comment in `(function _ -> x (* cmt *))` (#2739, @Julow)

doc/manpage_ocamlformat.mld

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,16 @@ OPTIONS (CODE FORMATTING STYLE)
271271
... = and before the in if the module declaration does not fit on
272272
a single line. The default value is compact.
273273

274+
--letop-punning={preserve|always|never}
275+
Name punning in bindings using extended let operators. preserve
276+
uses let-punning only when it exists in the source; the code "let*
277+
foo and* z = z in ..." will be left unchanged. always uses
278+
let-punning whenever possible; the code "let* foo and* z = z in
279+
..." will be rewritten to "let* foo and* z in ...". never never
280+
uses let-punning; the code "let* foo and* z = z in ..." will be
281+
rewritten to "let* foo = foo and* z = z in ...". The default value
282+
is preserve.
283+
274284
--line-endings={lf|crlf}
275285
Line endings used. lf uses Unix line endings. crlf uses Windows
276286
line endings. The default value is lf. Cannot be set in

lib/Conf.ml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ let conventional_profile from =
8686
; let_binding_deindent_fun= elt true
8787
; let_binding_spacing= elt `Compact
8888
; let_module= elt `Compact
89+
; letop_punning= elt `Preserve
8990
; line_endings= elt `Lf
9091
; margin= elt 80
9192
; match_indent= elt 0
@@ -156,6 +157,7 @@ let ocamlformat_profile from =
156157
; let_binding_deindent_fun= elt true
157158
; let_binding_spacing= elt `Compact
158159
; let_module= elt `Compact
160+
; letop_punning= elt `Preserve
159161
; line_endings= elt `Lf
160162
; margin= elt 80
161163
; match_indent= elt 0
@@ -225,6 +227,7 @@ let janestreet_profile from =
225227
; let_binding_deindent_fun= elt false
226228
; let_binding_spacing= elt `Double_semicolon
227229
; let_module= elt `Sparse
230+
; letop_punning= elt `Preserve
228231
; line_endings= elt `Lf
229232
; margin= elt 90
230233
; match_indent= elt 0
@@ -994,6 +997,27 @@ module Formatting = struct
994997
(fun conf elt -> update conf ~f:(fun f -> {f with let_module= elt}))
995998
(fun conf -> conf.fmt_opts.let_module)
996999

1000+
let letop_punning =
1001+
let doc = "Name punning in bindings using extended let operators." in
1002+
let names = ["letop-punning"] in
1003+
let all =
1004+
[ Decl.Value.make ~name:"preserve" `Preserve
1005+
"$(b,preserve) uses let-punning only when it exists in the \
1006+
source; the code \"$(i,let* foo and* z = z in ...)\" will be \
1007+
left unchanged."
1008+
; Decl.Value.make ~name:"always" `Always
1009+
"$(b,always) uses let-punning whenever possible; the code \
1010+
\"$(i,let* foo and* z = z in ...)\" will be rewritten to \
1011+
\"$(i,let* foo and* z in ...)\"."
1012+
; Decl.Value.make ~name:"never" `Never
1013+
"$(b,never) never uses let-punning; the code \"$(i,let* foo and* \
1014+
z = z in ...)\" will be rewritten to \"$(i,let* foo = foo and* z \
1015+
= z in ...)\". " ]
1016+
in
1017+
Decl.choice ~names ~all ~default ~doc ~kind
1018+
(fun conf elt -> update conf ~f:(fun f -> {f with letop_punning= elt}))
1019+
(fun conf -> conf.fmt_opts.letop_punning)
1020+
9971021
let let_open =
9981022
let names = ["let-open"] in
9991023
let msg = concrete_syntax_preserved_msg in
@@ -1353,6 +1377,7 @@ module Formatting = struct
13531377
; elt let_binding_deindent_fun
13541378
; elt let_binding_spacing
13551379
; elt let_module
1380+
; elt letop_punning
13561381
; elt line_endings
13571382
; elt margin
13581383
; elt match_indent

lib/Conf_t.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ type fmt_opts =
9494
; let_binding_deindent_fun: bool elt
9595
; let_binding_spacing: [`Compact | `Sparse | `Double_semicolon] elt
9696
; let_module: [`Compact | `Sparse] elt
97+
; letop_punning: [`Always | `Preserve | `Never] elt
9798
; line_endings: [`Lf | `Crlf] elt
9899
; margin: int elt
99100
; match_indent: int elt

lib/Conf_t.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type fmt_opts =
9292
(** De-indent the [fun] in a let-binding body. *)
9393
; let_binding_spacing: [`Compact | `Sparse | `Double_semicolon] elt
9494
; let_module: [`Compact | `Sparse] elt
95+
; letop_punning: [`Always | `Preserve | `Never] elt
9596
; line_endings: [`Lf | `Crlf] elt
9697
; margin: int elt (** Format code to fit within [margin] columns. *)
9798
; match_indent: int elt

lib/Extended_ast.ml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ let map (type a) (x : a t) (m : Ast_mapper.mapper) : a -> a =
5757
| Documentation -> Fn.id
5858

5959
module Parse = struct
60-
let normalize_mapper ~ocaml_version ~preserve_beginend =
60+
let normalize_mapper ~ocaml_version ~preserve_beginend ~prefer_let_puns =
6161
let open Asttypes in
6262
let open Ast_mapper in
6363
let enable_short_field_annot =
@@ -233,7 +233,17 @@ module Parse = struct
233233
let b' =
234234
let loc_start = b.pbop_op.loc.loc_start in
235235
let loc_end = b.pbop_exp.pexp_loc.loc_end in
236-
{b with pbop_loc= {b.pbop_loc with loc_start; loc_end}}
236+
let pbop_is_pun =
237+
match prefer_let_puns with
238+
| None -> b.pbop_is_pun
239+
| Some false -> false
240+
| Some true -> (
241+
match (b.pbop_pat.ppat_desc, b.pbop_exp.pexp_desc) with
242+
| Ppat_var {txt= v; _}, Pexp_ident {txt= Lident e; _} ->
243+
String.equal v e
244+
| _ -> false )
245+
in
246+
{b with pbop_loc= {b.pbop_loc with loc_start; loc_end}; pbop_is_pun}
237247
in
238248
Ast_mapper.default_mapper.binding_op m b'
239249
in
@@ -307,9 +317,10 @@ module Parse = struct
307317
in
308318
Ast_mapper.{default_mapper with expr; pat; binding_op}
309319

310-
let ast (type a) (fg : a t) ~ocaml_version ~preserve_beginend ~input_name
311-
str : a =
312-
map fg (normalize_mapper ~ocaml_version ~preserve_beginend)
320+
let ast (type a) (fg : a t) ~ocaml_version ~preserve_beginend
321+
~prefer_let_puns ~input_name str : a =
322+
map fg
323+
(normalize_mapper ~ocaml_version ~preserve_beginend ~prefer_let_puns)
313324
@@
314325
let lexbuf = Lexing.from_string str in
315326
let ocaml_version =

lib/Extended_ast.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module Parse : sig
3737
'a t
3838
-> ocaml_version:Ocaml_version.t
3939
-> preserve_beginend:bool
40+
-> prefer_let_puns:bool option
4041
-> input_name:string
4142
-> string
4243
-> 'a

lib/Parse_with_comments.ml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@ let parse ?(disable_w50 = false) ?(disable_deprecated = false) parse fragment
106106

107107
let parse_ast (conf : Conf.t) fg ~ocaml_version ~input_name s =
108108
let preserve_beginend = Poly.(conf.fmt_opts.exp_grouping.v = `Preserve) in
109-
Extended_ast.Parse.ast fg ~ocaml_version ~preserve_beginend ~input_name s
109+
let prefer_let_puns =
110+
match conf.fmt_opts.letop_punning.v with
111+
| `Always -> Some true
112+
| `Never -> Some false
113+
| `Preserve -> None
114+
in
115+
Extended_ast.Parse.ast fg ~ocaml_version ~preserve_beginend
116+
~prefer_let_puns ~input_name s
110117

111118
(** [is_repl_block x] returns whether [x] is a list of REPL phrases and
112119
outputs of the form:

test/cli/print_config.t

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ No redundant values:
5858
let-binding-deindent-fun=true (profile conventional (file .ocamlformat:1))
5959
let-binding-spacing=compact (profile conventional (file .ocamlformat:1))
6060
let-module=compact (profile conventional (file .ocamlformat:1))
61+
letop-punning=preserve (profile conventional (file .ocamlformat:1))
6162
line-endings=lf (profile conventional (file .ocamlformat:1))
6263
margin=80 (profile conventional (file .ocamlformat:1))
6364
match-indent=0 (profile conventional (file .ocamlformat:1))
@@ -138,6 +139,7 @@ Redundant values from the conventional profile:
138139
let-binding-deindent-fun=true (profile conventional (file .ocamlformat:1))
139140
let-binding-spacing=compact (profile conventional (file .ocamlformat:1))
140141
let-module=compact (profile conventional (file .ocamlformat:1))
142+
letop-punning=preserve (profile conventional (file .ocamlformat:1))
141143
line-endings=lf (profile conventional (file .ocamlformat:1))
142144
margin=80 (file .ocamlformat:3) -- Warning (redundant): (profile conventional (file .ocamlformat:1))
143145
match-indent=0 (profile conventional (file .ocamlformat:1))
@@ -218,6 +220,7 @@ Redundant values from the ocamlformat profile:
218220
let-binding-deindent-fun=true (profile ocamlformat (file .ocamlformat:1))
219221
let-binding-spacing=compact (profile ocamlformat (file .ocamlformat:1))
220222
let-module=compact (profile ocamlformat (file .ocamlformat:1))
223+
letop-punning=preserve (profile ocamlformat (file .ocamlformat:1))
221224
line-endings=lf (profile ocamlformat (file .ocamlformat:1))
222225
margin=80 (file .ocamlformat:3) -- Warning (redundant): (profile ocamlformat (file .ocamlformat:1))
223226
match-indent=0 (profile ocamlformat (file .ocamlformat:1))

test/passing/gen/dune.inc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3722,6 +3722,42 @@
37223722
(package ocamlformat)
37233723
(action (diff let_punning.ml.err let_punning.ml.stderr)))
37243724

3725+
(rule
3726+
(deps .ocamlformat)
3727+
(package ocamlformat)
3728+
(action
3729+
(with-stdout-to let_punning_denied.ml.stdout
3730+
(with-stderr-to let_punning_denied.ml.stderr
3731+
(run %{bin:ocamlformat} --name let_punning_denied.ml --margin-check --letop-punning=never --ocaml-version=4.14.0 %{dep:../tests/let_punning_denied.ml})))))
3732+
3733+
(rule
3734+
(alias runtest)
3735+
(package ocamlformat)
3736+
(action (diff let_punning_denied.ml.ref let_punning_denied.ml.stdout)))
3737+
3738+
(rule
3739+
(alias runtest)
3740+
(package ocamlformat)
3741+
(action (diff let_punning_denied.ml.err let_punning_denied.ml.stderr)))
3742+
3743+
(rule
3744+
(deps .ocamlformat)
3745+
(package ocamlformat)
3746+
(action
3747+
(with-stdout-to let_punning_preferred.ml.stdout
3748+
(with-stderr-to let_punning_preferred.ml.stderr
3749+
(run %{bin:ocamlformat} --name let_punning_preferred.ml --margin-check --letop-punning=always --ocaml-version=4.14.0 %{dep:../tests/let_punning_preferred.ml})))))
3750+
3751+
(rule
3752+
(alias runtest)
3753+
(package ocamlformat)
3754+
(action (diff let_punning_preferred.ml.ref let_punning_preferred.ml.stdout)))
3755+
3756+
(rule
3757+
(alias runtest)
3758+
(package ocamlformat)
3759+
(action (diff let_punning_preferred.ml.err let_punning_preferred.ml.stderr)))
3760+
37253761
(rule
37263762
(deps .ocamlformat)
37273763
(package ocamlformat)

0 commit comments

Comments
 (0)