Two parts:
(1) I think that a “migration” PPX rewriter is really valuable – it makes your code much, much more succinct, and it gives you a flexibility in being able to modify your AST types and then, with relatively small changes, get a lot of stuff generated for you easily. Am example of this is: when a new OCaml AST version comes out, it typically takes me 30min to generate the migration for it. That’s a lot faster than having to do it “by hand”.
(2) Hereinafter I’m going to describe pa_ppx_migrate
, a PPX rewriter that automates much of the process of creating “migrations” between two very-similar-but-not-identical families of AST types., e.g. OCaml v 4.02 and v4.03.
Caveat: what I describe below is based on Camlp5 and pa_ppx, so it’s possible (OK: near-certain) that you won’t want to use it, b/c it will be … not so compatible with the standard PPX rewriters. Notwithstanding, the ideas in this PPX rewriter should be easily re-implementable in a PPX rewriter based on the standard tooling.
Link: GitHub - camlp5/pa_ppx_migrate: PPX Rewriter to help write AST migrations for Ocaml (using Camlp5 and pa_ppx)
You can look at examples in the tests
directory, or full examples of migrations between the various versions of the OCaml AST in pa_ocaml_migrate_parsetree
.
The idea is this: you define two types[1], the SRC and the DST, and then, as a type-deriver on SRC, you provide (hopefully) minimal hints to the machinery that generates a function from SRC to DST. The migration is specified as a bunch of “type migrations” one for each type, or more precisely, for type-patterns. That is, you write a pattern in the type-language of the types of SRC, and then you specify a type in the type-language of DST. If the types match up, then auto-generated match code will do the job. For the cases where it does not, you can supply the few case-branches that change, or specify which fields are omitted or added-in, or even specify the entirety of the code. For polymorphic type-constructors, you can either specify instances (with type-variables instantiated to concrete types), and a migration for each instance. Or, you can specify a migration for the polymorphic type, in which case the machinery generates a migration function that takes migrations for the parameter-types. There are lots of examples of these in the tests.
I’ve worked thru migrations for all the OCaml AST versions 4.02…4.12, mostly 4.N <-> 4.{N+1) but also from 4.02 to a bunch of other versions (just to see how it would work out). I’ve also use this PPX rewriter in other PPX rewriters, like camlp5/pa_ppx_ag
(an attribute-grammar evaluator-generator).
Again, I would strongly note that if you want to keep using the standard set of PPX rewriters, you can probably use pa_ppx_migrate
for the one task of generating migrations, but again, it’s not designed to be composable with the standard set, b/c it’s based on Camlp5 and pa_ppx.
As another set of examples, I use this machinery to generate migrations amongst
- an AST type
- that same AST type with hashcons nodes inserted (
camlp5/pa_ppx_hashcons
)
- that same type with unique-ids at every node (
camlp5/pa_ppx_unique
) (hence, “anti-hash-consing”)
- that same AST with slots added in nodes for attributes (
camlp5/pa_ppx_ag
).
Obviously, it would be tedious to maintain all of these manually, as one changes the base AST type.
If you want to use this as an example to implement your own migration PPX rewriter using the standard tooling, I’d be happy to explain how it works – ti’s basically does matching and rewriting on type-expressions, to drive the generation of the match-code. Not actually very complicated at all.
[1] actually, two sets of mutually-recursive types, of course.