For reasons unrelated to your problem, what I’m going to suggest might not be useful to you, but it might be interesting to look at anyway. pa_ppx_migrate
(part of the Camlp5 suite of PPX rewriters) does this pretty much onthenose.
[Why not useful? B/c it’s based on Camlp5, which is not compatible with the standard PPX rewriter infrastructure (it implements its own, workalike for many rewriters, but not internally compatible)]
The problem it solves:

suppose you have two types A,B (actually two families of mutuallyrecursive types) that are pretty similar, but with some differences (for example, two different versions of the OCaml AST types).

you want to implement functions that go from the type A_i to the types B_i, and maybe back also.

You don’t want to write all the boilerplate code for the cases where various casebranches and typeexpressions are the same between A, B.

And maybe you want to step in with code for a casebranch that is different, or a field that needs to be supplied, or a field that no longer exists. Or, custom code for a case branch that you want to deviate from straight copying.
pa_ppx_migrate
automates the generation of the boilerplate, while providing the spots for those custommizations. Here’s a few examples based on your type:
First, your type (in foo.ml):
type exp =
 Int of int
 Add of exp * exp
 Sub of exp * exp
then just copying a value of your type
type exp = [%import: Foo.exp
[@with int := t2]
]
and t2 = int
[@@deriving migrate
{ dispatch_type = dispatch_table_t
; dispatch_table_constructor = make_dt
; default_open_recursion = true
; default_dispatchers = [
{
srcmod = Foo
; dstmod = Foo
; types = [
exp
]
}
]
; dispatchers = {
migrate_t2 = {
srctype = [%typ: t2]
; dsttype = [%typ: int]
; code = (fun __dt__ x > x)
}
}
}
]
now add one to the Int
values
type exp = [%import: Foo.exp
[@with int := t2]
]
and t2 = int
[@@deriving migrate
{ dispatch_type = dispatch_table_t
; dispatch_table_constructor = make_dt
; default_open_recursion = true
; default_dispatchers = [
{
srcmod = Foo
; dstmod = Foo
; types = [
exp
]
}
]
; dispatchers = {
migrate_t2 = {
srctype = [%typ: t2]
; dsttype = [%typ: int]
; code = (fun __dt__ x > x)
}
}
}
]
and now swap the subtrees of Add
constructors
type exp = [%import: Foo.exp
[@with int := t2]
]
and t2 = int
[@@deriving migrate
{ dispatch_type = dispatch_table_t
; dispatch_table_constructor = make_dt
; default_open_recursion = true
; dispatchers = {
migrate_exp = {
srctype = [%typ: exp]
; dsttype = [%typ: exp]
; custom_branches_code = function
Add(a,b) > Add (__dt__.migrate_exp __dt__ b, __dt__.migrate_exp __dt__ a)
}
; migrate_t2 = {
srctype = [%typ: t2]
; dsttype = [%typ: int]
; code = (fun __dt__ x > x)
}
}
}
]
And here it is in use in a toplevel:
#use "topfind.camlp5";;
#load "foo.cmo";;
#load "foo_migrate.cmo";;
open Foo ;;
open Foo_migrate ;;
let x = Add(Int 1, Int 2) ;;
let dt0 = Copy.make_dt () ;;
dt0.migrate_exp dt0 x ;;
let dt1 = AddOne.make_dt () ;;
dt1.migrate_exp dt1 x ;;
let dt2 = SwapAdd.make_dt () ;;
dt2.migrate_exp dt2 x ;;
val x : Foo.exp = Add (Int 1, Int 2)
val dt0 : unit Foo_migrate.Copy.dispatch_table_t =
{Foo_migrate.Copy.aux = (); migrate_exp = <fun>; migrate_t2 = <fun>}
 : Foo.exp = Add (Int 1, Int 2)
val dt1 : unit Foo_migrate.AddOne.dispatch_table_t =
{Foo_migrate.AddOne.aux = (); migrate_exp = <fun>; migrate_t2 = <fun>}
 : Foo.exp = Add (Int 2, Int 3)
val dt2 : unit Foo_migrate.SwapAdd.dispatch_table_t =
{Foo_migrate.SwapAdd.aux = (); migrate_exp = <fun>; migrate_t2 = <fun>}
 : Foo.exp = Add (Int 2, Int 1)
The project GitHub  camlp5/pa_ppx_migrate: PPX Rewriter to help write AST migrations for Ocaml (using Camlp5 and pa_ppx) has a ton of examples (but sadly, not so much documentation) and I use it to generate migrations between the various versions of the OCaml AST, so it’s pretty fullfeatured.
ETA: and you can override the functions in the “dispatch table” after it’s been created, so starting with one that copies blindly, you can override a casebranch to swap the subtrees of an Add
:
let dt2' =
let dt = Copy.make_dt() in
let old_migrate_exp = dt.migrate_exp in
{ dt with migrate_exp = fun __dt__ e >
match e with
Add(a,b) > Add(__dt__.migrate_exp __dt__ b, __dt__.migrate_exp __dt__ a)
 _ > old_migrate_exp __dt__ e }
;;
dt2'.migrate_exp dt2' x ;;
val dt2' : unit Foo_migrate.Copy.dispatch_table_t =
{Foo_migrate.Copy.aux = (); migrate_exp = <fun>; migrate_t2 = <fun>}
# dt2'.migrate_exp dt2' x ;;
 : Foo.exp = Add (Int 2, Int 1)
#