Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file ppx_mixins.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277(* ppx_mixins — inline mixin includes for OCaml signatures
=========================================================
This PPX rewrites a type declaration annotated with [@@mixins ...] inside a
[sig] block into the type declaration followed by one [include M with ...]
item per mixin. For example:
module type S = sig
type my_type [@@mixins Printable; Mappable (key = string; value := int)]
end
expands to:
module type S = sig
type my_type
include Printable with type t := my_type
include Mappable with type key = string
and type value := int
and type t := my_type
end
The attribute payload is a plain OCaml expression (single_expr_payload).
Multiple mixins are separated by [+] at the top level (parsed by the OCaml
parser as [Pexp_apply "+"]). Per-mixin parameters are [;]-separated
inside parentheses.
Constraint syntax within parameter lists:
name = Type → with type name = Type (equality / sharing constraint)
name := Type → with type name := Type (destructive substitution)
The RHS [Type] is re-parsed from an expression to a [core_type]. Supported
forms are:
int plain type constructor (zero arguments)
int list single-argument type constructor
(int, string) result multi-argument type constructor (tuple arg)
unit parsed as [Pexp_construct "()" None]
My_module.t dotted path, with or without arguments
The transformation runs in three phases:
Phase 1 — Parse
Recursively flatten the [Pexp_apply "+"] tree produced by the OCaml parser
into a [mixin list]. Each mixin carries a [module_path] (the longident of
the module type) and a [constraint_ list] parsed from the optional argument.
Per-mixin constraint RHS expressions are converted to [core_type] nodes via
[expr_to_core_type], which handles plain identifiers, dotted paths, [unit],
single-parameter type applications (e.g. [int list]), and multi-parameter
type applications (e.g. [(int, string) result]).
A bare [Constructor] with no argument yields an empty constraint list.
Phase 2 — Inject default substitution
Mixin module types conventionally name their primary type [t]. If the
constraint list contains no binding (neither [Eq] nor [Subst]) for the
name ["t"], a [Subst ("t", ptyp_constr type_name [])] constraint is
prepended automatically. This connects the mixin's [t] to the annotated
type without requiring the user to write [t := my_type] every time.
If the user supplies an explicit [t = ...] or [t := ...] constraint, the
automatic injection is suppressed.
Phase 3 — Desugar
Each [mixin] is turned into a [psig_include] node:
[include <module_path> with <constraint> and ...]
The original [psig_type] node is re-emitted without the [@@mixins]
attribute, followed by all the generated [psig_include] nodes.
The expansion is wired up as a global [Ast_traverse.map] registered via
[Driver.V2.register_transformation], so it recurses into every [sig] block
in both [.ml] and [.mli] files, including nested [sig]s inside structures.
*)openPpxlib(* ── Intermediate representation ─────────────────────────────────────────── *)(* A single type constraint in a mixin's parameter list. *)typeconstraint_=|Substofstring*core_type(* name := rhs → with type name := rhs *)|Eqofstring*core_type(* name = rhs → with type name = rhs *)typemixin={module_path:longident;constraints:constraint_list}(* ── Parsing ──────────────────────────────────────────────────────────────── *)(* Extract the longident from a type-constructor-position expression.
Accepts both [Pexp_ident] (lowercase/dotted) and [Pexp_construct] (uppercase
module paths used as type constructors, e.g. [M.t]). *)letextract_longidentexpr=matchexpr.pexp_descwith|Pexp_ident{txt;_}->Sometxt|Pexp_construct({txt;_},None)->Sometxt|_->None(* Re-parse an expression into a [core_type].
In OCaml's expression grammar, type application is written in postfix order
(e.g. [int list], [(int, string) result]), but the parser treats it as a
regular function call with the parameter(s) as the function and the
constructor as the argument:
int list -> Pexp_apply(Pexp_ident "int", [Pexp_ident "list"])
(int, string) result -> Pexp_apply(Pexp_tuple [int; string], [Pexp_ident "result"])
So for Pexp_apply we use the *argument* as the constructor and the
*function* (or tuple elements) as the type parameters.
Supported:
ident / dotted-path -> ptyp_constr path []
unit (constructor "()") -> ptyp_constr (Lident "unit") []
param Constructor -> ptyp_constr Constructor [param]
(a, b, ...) Constructor -> ptyp_constr Constructor [a; b; ...] *)letrecexpr_to_core_typeexpr=letopenAst_builder.Defaultinletloc=expr.pexp_locinmatchexpr.pexp_descwith(* unit *)|Pexp_construct({txt=Lident"()";_},None)->ptyp_constr~loc{txt=Lident"unit";loc}[](* plain identifier or dotted path: int, string, M.t, ... *)|Pexp_ident{txt;_}->ptyp_constr~loc{txt;loc}[](* type application:
single-param: Pexp_apply(param_expr, [(Nolabel, ctor_expr)])
multi-param: Pexp_apply(Pexp_tuple params, [(Nolabel, ctor_expr)]) *)|Pexp_apply(f,[(Nolabel,ctor_expr)])->(matchextract_longidentctor_exprwith|None->Location.raise_errorf~loc:ctor_expr.pexp_loc"ppx_mixins: expected a type constructor name as the last token \
(e.g. 'int list' or '(int, string) result')"|Somepath->letparams=matchf.pexp_descwith|Pexp_tupletype_args->List.mapexpr_to_core_typetype_args|_->[expr_to_core_typef]inptyp_constr~loc{txt=path;loc}params)|_->Location.raise_errorf~loc"ppx_mixins: unsupported type expression; expected a type constructor \
or type application"(* Parse one constraint expression of the form [name := rhs] or [name = rhs]. *)letparse_constraint~locexpr=matchexpr.pexp_descwith|Pexp_apply({pexp_desc=Pexp_ident{txt=Lident((":="|"=")asop);_};_},[(Nolabel,{pexp_desc=Pexp_ident{txt=Lidentname;_};_});(Nolabel,rhs_expr);])->letrhs=expr_to_core_typerhs_exprinifop=":="thenSubst(name,rhs)elseEq(name,rhs)|_->Location.raise_errorf~loc:expr.pexp_loc"ppx_mixins: expected a constraint of the form 'name = Type' or 'name \
:= Type'"(* Flatten a [Pexp_sequence]-or-single expression into a list of constraints. *)letrecparse_constraints~locexpr=matchexpr.pexp_descwith|Pexp_sequence(a,b)->parse_constraints~loca@parse_constraints~locb|_->[parse_constraint~locexpr](* Parse one mixin entry: [Constructor] or [Constructor (constraints...)]. *)letparse_mixin~locexpr=matchexpr.pexp_descwith|Pexp_construct({txt=module_path;_},arg_opt)->letconstraints=matcharg_optwithNone->[]|Somee->parse_constraints~locein{module_path;constraints}|_->Location.raise_errorf~loc:expr.pexp_loc"ppx_mixins: expected a module type name (e.g. 'Printable' or \
'Mappable (key = string)')"(* Flatten the top-level [+] chain into a list of mixins.
[A + B + C] is left-associative so the parser produces
[((A + B) + C)]; recursing on both sides handles all depths. *)letrecparse_mixins~locexpr=matchexpr.pexp_descwith|Pexp_apply({pexp_desc=Pexp_ident{txt=Lident"+";_};_},[(Nolabel,a);(Nolabel,b)])->parse_mixins~loca@parse_mixins~locb|_->[parse_mixin~locexpr](* ── Phase 2: inject default substitution ────────────────────────────────── *)(* If no binding for ["t"] is present, prepend [t := type_name] so that
the desugaring phase can treat all mixins uniformly. *)letinject_default_subst~loc~type_namemixin=lethas_t_binding=List.exists(functionSubst("t",_)|Eq("t",_)->true|_->false)inifhas_t_bindingmixin.constraintsthenmixinelseletdefault_rhs=Ast_builder.Default.ptyp_constr~loc{txt=Lidenttype_name;loc}[]in{mixinwithconstraints=Subst("t",default_rhs)::mixin.constraints}(* ── Phase 3: desugar into a [psig_include] ──────────────────────────────── *)(* Build the [type_declaration] node used as the RHS of both [Pwith_type] and
[Pwith_typesubst]. The manifest is the [core_type] already produced by
[expr_to_core_type]. *)letmake_type_decl~locrhs=Ast_builder.Default.type_declaration~loc~name:{txt="t";loc}(* name is overridden by the caller anyway *)~params:[]~cstrs:[]~kind:Ptype_abstract~private_:Public~manifest:(Somerhs)letconstraint_to_with~loc=function|Subst(name,rhs)->Pwith_typesubst({txt=Lidentname;loc},make_type_decl~locrhs)|Eq(name,rhs)->Pwith_type({txt=Lidentname;loc},make_type_decl~locrhs)letmixin_to_sig_item~locmixin=letopenAst_builder.Defaultinletbase=pmty_ident~loc{txt=mixin.module_path;loc}inletwiths=List.map(constraint_to_with~loc)mixin.constraintsinletmty=pmty_with~locbasewithsinpsig_include~loc(include_infos~locmty)(* ── Attribute & expansion ───────────────────────────────────────────────── *)letmixins_attr=Attribute.declare"mixins"Attribute.Context.type_declarationAst_pattern.(single_expr_payload__)Fun.idletexpand_type_decl~locrec_flagtd=matchAttribute.getmixins_attrtdwith|None->None|Somepayload->lettype_name=td.ptype_name.txtin(* Phase 1: parse *)letmixins=parse_mixins~locpayloadin(* Phase 2: inject default [t := type_name] where no t binding is present *)letmixins=List.map(inject_default_subst~loc~type_name)mixinsin(* Phase 3: desugar each mixin into an [include] signature item *)letinclude_items=List.map(mixin_to_sig_item~loc)mixinsinletclean_td={tdwithptype_attributes=List.filter(funa->a.attr_name.txt<>"mixins")td.ptype_attributes;}inlettype_item=Ast_builder.Default.psig_type~locrec_flag[clean_td]inSome(type_item::include_items)(* ── AST traversal ───────────────────────────────────────────────────────── *)(* Recursively walk the AST, expanding [@@mixins ...] at every signature level. *)letmapper=objectinheritAst_traverse.mapassupermethod!signaturesig_=letsig_=super#signaturesig_inList.concat_map(funitem->matchitem.psig_descwith|Psig_type(rec_flag,[td])->(matchexpand_type_decl~loc:item.psig_locrec_flagtdwith|Someitems->items|None->[item])|_->[item])sig_endlet()=Driver.V2.register_transformation"mixins"~impl:(fun_ctxt->mapper#structure)~intf:(fun_ctxt->mapper#signature)