Hi,
I released a new version of ocamlmig in opam, whose main change is to avoid reformatting everything in codebases that don’t use ocamlformat. Instead, only subexpressions touched by a rewrite are reformatted.
It also requalifies identifier in more places to preserve their meaning (e.g. when replacing string_of_int
by Int.to_string
, there might be an Int
module in scope that’s not Stdlib.Int
. In such case, ocamlmig would more often replace string_of_int
by Stdlib.Int.to_string
).
Separately, I’ve thought about the recent addition of let+ operators in Cmdliner, and how one might migrate from the use of $
to them. Concretetely, given:
let bistro () (`Dry_run dry_run) (`Package_names pkg_names) ... = the code
open Cmdliner
let term = Term.(const bistro $ Cli.setup $ Cli.dry_run $ ...)
you’d want to have instead:
open Cmdliner
let term =
Term.(Syntax.(
let+ () = Cli.setup
and+ (`Dry_run dry_run) = Cli.dry_run
and+ (`Package_names pkg_names) = ...
...
in
the code))
ocamlmig can now transform code this way, at the tip of the ocamlmig repo (not the last release). You can see it in the second commit in this branch (and further mechanical cleanups in the commits with “…” bubbles), but to explain a bit:
let bistro () (`Dry_run dry_run) (`Package_names pkg_names) ... = the code
open Cmdliner
let term = Term.(const bistro $ Cli.setup $ Cli.dry_run $ ...)
is first turned into:
open Cmdliner
let term = Term.(const (fun () (`Dry_run dry_run) (`Package_names pkg_names) ... -> the code)
$ Cli.setup $ Cli.dry_run $ ...)
which is then turned into the final code:
open Cmdliner
let term =
Term.(Syntax.(
let+ () = Cli.setup
and+ (`Dry_run dry_run) = Cli.dry_run
and+ (`Package_names pkg_names) = ...
...
in
the code))
The first step is done using ocamlmig replace -w -e 'const [%move_def __f] /// const __f'
. In short, what this does is anytime it sees const some-identifier
, it tries to inline the definition of the identifier. In details, the left side of the ///
specifies the code to search for, and the right side what to replace it with. const ...
searches for literally const
applied to one argument. [%move_def __f]
is trickier: it matches identifiers that are let-bound somewhere in the current file, removes said let binding, and recursively matches the right hand side of the binding against __f
. Variables that start with two underscores name a term for use in the replacement expression.
The second step is done with:
ocamlmig replace -w \
-e 'const (fun __p1 __p2 __p3 -> __body) $ __e1 $ __e2 $ __e3
/// let open Syntax in let+ __p1 = __e1 and+ __p2 = __e2 and+ __p3 = __e3 in __body'
This is longer, but given the previous explanation, it’s hopefully fairly clear what this does. The only twist is that ocamlmig generalizes this search/replace for three elements into an n-ary version (implicitly, although perhaps it should be explicit).
And that’s it. So this is the full command that I used:
ocamlmig replace -w \
-e 'const [%move_def __f] /// const __f' \
-e 'const (fun __p1 __p2 __p3 -> __body) $ __e1 $ __e2 $ __e3
/// let open Syntax in let+ __p1 = __e1 and+ __p2 = __e2 and+ __p3 = __e3 in __body'
which seems pretty reasonable considering the rewrite is somewhat sophisticated.
In general, mechanizing a change can reduce the chance of accidentally modifying something, but in this specific case, ocamlmig also detects shadowing when moving code with [%move_def]
. Shadowing would likely cause type errors or tests errors, but if it didn’t, it’d be quite hard to catch during code review.
Finally, if you want to try this out on your code, I’ll note that ocamlmig replace
is in flux, and that while the commands above work, obvious variations of them may not.