This page explains how to use ocamlmig. For an overview of what ocamlmig does, see this instead.
To rewrite your code with ocamlmig, you need to:
- build the ocamlmig command line executable
- use dune to build your own code
- use ocamlformat. You can try things out without ocamlformat, but all code will be
printed back ocamlformatted. ocamlmig needs not be linked with the exact ocamlformat
version you use. Or if ocamlformat is really not an option, you could try an
experimental printer that reduces (but does not minimize) gratuitous reformatting,
with
OCAMLMIG_PRESERVE_FORMAT= ocamlmig ...
.
You can set up a test repository this way:
cd /tmp &&
git init trymig &&
cd trymig &&
(echo _build; echo _opam) > .gitignore &&
opam switch create . --packages ocaml.5.2.1,dune,ocamlformat,ocamlmig &&
eval "$(opam env)" &&
dune init project trymig . &&
{ cat > bin/main.ml <<'EOF'
let () = Printf.printf "should be 1: %f\n" ((cos 0.3)**2. +. (sin 0.3)**2.)
let f1 x = x + 1 [@@migrate { repl = fun x -> Rel.f2 ~x }]
let f2 ~x = x + 1
let _ = f1 1
let _ = f2
EOF
} &&
touch .ocamlformat &&
{ dune fmt 2> /dev/null || true; } &&
dune build @check &&
git add . &&
git commit -m init &&
cat <<EOF
*** All good! ***
Now look at bin/main.ml, and try these:
$ ocamlmig mig
$ ocamlmig mig -extra-migration ocamlmig.stdlib_to_stdlib
EOF
In an existing project, you can either opam install ocamlmig
in that project's
switch, or use the ocamlmig binary installed in a different switch (in which case,
ocamlmig and the project must be built with the same compiler version).
If you merely want to add attributes to a project, you can do so without dependencies or build system tweaks. You may still want to install ocamlmig to check that your attributes are working as intended though.
If someone else has written @migrate
attributes for you, all you need to do is:
- build the part of the repository you want to upgrade.
dune build @check
is the recommended way to do that, as a meredune build
doesn't quite build everything that's necessary. If the build fails due to warnings, you should build withdune build --profile release
so the build succeeds. - run
ocamlmig migrate
to see the proposed diff. By default, the command runs on all .ml files in the repository, but you can specify fewer files with e.g.ocamlmig migrate lib/*.ml
. - run
ocamlmig migrate -w
to actually update the source files, if the diff looks good
If something is not working right, the usual way to debug is by adding a
[@migrate.log]
around the expression where you expected a different result, then
rerunning ocamlmig migrate
. [@migrate.log]
should generally wrap as small an
expression as possible, as the amount of debug output can be overwhelming otherwise.
ocamlmig transform
provides a few refactorings described below. As you'll notice, it
wouldn't be possible to implement similar things with sed invocations and the
like. You can chain such refactorings sequentially, so long as the code successfully
builds between steps, to build larger and more interesting changes.
After compiling the following program with dune:
open Format
let () = print_string "a"
let () = prerr_string "a"
ocamlmig would refactor it this way:
$ ocamlmig transform rescope -unopen Format
-open Format
-let () = print_string "a"
+let () = Format.print_string "a"
let () = prerr_string "a"
$ ocamlmig transform rescope -unopen Format -w # update the file if the diff is good
Notice that print_string
was turned to Format.print_string
, but prerr_string
is unchanged (since prerr_string
is not defined by Format
).
Inversely, ocamlmig transform rescope -open Format
applied to
a file:
let () = print_string "a"
let () = Format.print_string "a"
would generate this diff:
-let () = print_string "a"
+open Format
+
+let () = Stdlib.print_string "a"
let () = Format.print_string "a"
print_string
was requalified, otherwise the extra open Format
would make it refer
to Format.print_string
. As you can see Format.print_string
wasn't shortened to
print_string
, although it could have been. The next transformation does exactly that.
ocamlmig transform rescope -unqualify Format
on:
open Format
let () = Stdlib.print_string "a"
let () = Format.print_string "a"
would update the code like so:
open Format
let () = Stdlib.print_string "a"
-let () = Format.print_string "a"
+let () = print_string "a"
Here, we will assume you have read through the high level picture first.
For library authors, here is how to describe a migration:
(* This is library Foo *)
val starts_with2 : string -> prefix:string -> bool
val starts_with : string -> string -> bool
[@@migrate { repl = (fun str prefix -> Rel.starts_with2 str ~prefix) }]
What this means is: during ocamlmig migrate
, any reference Foo.starts_with
to the
value above will be replaced by (fun str prefix -> Foo.starts_with2 str ~prefix)
.
Rel
is special syntax to refer to the module path to the old value (mostly meant so
that if the module of the value has been aliased in the calling code, the updated code
keeps using the alias). ocamlmig then cleans up the code if possible:
(* original code *)
let _ = Foo.starts_with "a" "b"
(* after "inlining" the migrate annotation and replacing Rel *)
let _ = (fun str prefix -> Foo.starts_with2 str ~prefix) "a" "b"
(* after clean up *)
let _ = Foo.starts_with2 "a" ~prefix:b"
As you may have noticed, the repl
field contains an expression that, aside from the
use of Rel
, is probably exactly how starts_with
is implemented in the library.
In general, the migrate
attribute is supported:
-
on
val
signature items,val x : unit -> unit [@@migrate ...]
-
on let bindings defining a single variable,
let x () = () [@@migrate ...]
-
on an arbitrary identifier, to define a migration without modifying the definition of the identifier:
let _ = List.map [@migrate { repl = fun f l -> ListLabels.map ~f l }]`
Note the single
@
sign.Such attributes differ from inline ones in a couple of ways:
- They must be defined in a single-module library. Let's say the library is called
use_labels
, then the code above would live inuse_labels.ml
. ocamlmig migrate
will not apply such transform automatically, this must be requested explicitly with a-extra-migration use_labels
flag.
Currently, the following syntax is also supported:
let _ = [ List.map; (fun f l -> ListLabels.map ~f l) ] [@migrate]`
The advantage of this version is that the replacement expression is typechecked, and is checked to have a type that's unifiable with the original identifier. The downside is that the annotated code maybe need more dependencies to compile.
- They must be defined in a single-module library. Let's say the library is called
-
(experimental) on module declaration,
module M : ... [@@migrate { repl = Mnew }]
. The replacement must be a path, not a general expression, and these migrations attributes are only searched for withocamlmig mig -module-migration
.
The general form of the migrate
attribute is:
[@@migrate
{ repl (* a required expression, e.g. *) = (fun f l -> ListLabels.map l ~f)
; libraries (* an optional list of libraries names, e.g. *) = [ "core.unix" ]
}]
There are a few things to know about the repl
expression:
-
Since the expression is specified in an attribute, it will be parsed but not typechecked, so you may want to manually check (by applying the migration) that your migration generates working code.
-
Since the expression is meant to be inserted into the caller's source code, although any arbitrary ocaml expression can be inserted, not every ocaml bit of the language is a good idea to use. Here are known things to be wary about:
- The scope at the attribute definition and at the call site is different. In
particular sum type constructors and record fields may need to be qualified. The
intention is that replacement expression should assume that Stdlib is in scope
and use fully qualified paths (or
Rel
) to refer to other names. - Type variables
(... : 'a list)
should be avoided, because they can collide with type variables of the same name in the caller, and cause typing errors even if they are fresh. - Extension nodes (say
[%compare: int]
) should be avoided. If the replacement code contains an extension node, then the caller would either get the macro-expansion or the extension node (depending on how the ppx works), and neither is ideal.
- The scope at the attribute definition and at the call site is different. In
particular sum type constructors and record fields may need to be qualified. The
intention is that replacement expression should assume that Stdlib is in scope
and use fully qualified paths (or
Finally, the code supports applying slightly different rewrites depending on the context of the original identifier. Concretely, it looks like this:
let _ = compare
[@migrate { repl = function [%context: int -> _] -> Int.compare
| [%context: string -> _] -> String.compare }
| _ -> Compare.Poly.compare
]
let z1 x = compare x 1 (* compare would be replaced by Int.compare *)
let z2 (x : string) y = compare x y (* compare would be replaced by String.compare *)
let z3 x y = compare x y (* compare would be replaced by Compare.Poly.compare *)
The function
syntax maps conditions about the context to a replacement expression in
that context. [%context: type]
accepts only call sites compatible with the specified
type, while _
accepts all call sites. A final _
is not required: if no contexts
match, a call site won't be rewritten.
The libraries
contains a list of libraries names as specified in dune files. This
should be used when the replacement expression refers to libraries that the caller code
may not have in scope. Whenever that migrate attribute is used, ocamlmig will add the
dependency to the relevant dune files.