123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657(*---------------------------------------------------------------------------
Copyright (c) 2026 Invariant Systems. All rights reserved.
SPDX-License-Identifier: ISC
---------------------------------------------------------------------------*)(* A guard replaces its site in place and its disarmed arm is the original
expression, so the rewriter is a map that threads one context: whether an
expression must be a boolean, and whether it is a condition. Every arm
keeps the emission law of
instrument.mli, because all the mutants share one binary and one ill-typed
arm breaks the build of the whole project. *)openPpxlibopenAst_builder.Default(* Attributes *)typedirective=No_directive|Offofstring|On|Exclude_file(* The grammar is the [coverage] attribute's (ppx/coverage/instrument.ml). *)letdirective{attr_name;attr_payload;attr_loc}=letpayload=matchattr_payloadwith|PStr[{pstr_desc=Pstr_eval(payload,_);_}]->Somepayload.pexp_desc|_->Noneinifnot(String.equalattr_name.txt"mutate")thenNo_directiveelsematchpayloadwith|Some(Pexp_ident{txt=Lident"off";_})->Off""|Some(Pexp_ident{txt=Lident"on";_})->On|Some(Pexp_ident{txt=Lident"exclude_file";_})->Exclude_file|Some(Pexp_apply({pexp_desc=Pexp_ident{txt=Lident"off";_};_},[(Nolabel,{pexp_desc=Pexp_constant(Pconst_string(reason,_,_));_});]))->Offreason|_->Location.raise_errorf~loc:attr_loc"Bad payload in mutate attribute."leterr_misplacedattributename=Location.raise_errorf~loc:attribute.attr_loc"mutate %s is not allowed here."name(* Every attribute is read, so a malformed one is an error beside an [off]
too. *)letoff_reasonattributes=List.fold_left(funfoundattribute->matchdirectiveattributewith|No_directive->found|Offreason->Somereason|On->err_misplacedattribute"on"|Exclude_file->err_misplacedattribute"exclude_file")Noneattributesletis_offattributes=Option.is_some(off_reasonattributes)letexcludes_filestructure=List.exists(function|{pstr_desc=Pstr_attributeattribute;_}->(matchdirectiveattributewith|Exclude_file->true|No_directive|Off_|On->false)|_->false)structure(* Inline tests *)(* ppx_windtrap marks each item it generates for a test with
[[@@windtrap.test]]. Without ppx_windtrap a test stays an extension node,
which is never traversed. *)letis_test_itemsi=letmarked=List.exists(funa->String.equala.attr_name.txt"windtrap.test")inmatchsi.pstr_descwith|Pstr_value(_,bindings)->List.exists(funb->markedb.pvb_attributes)bindings|Pstr_modulemb->markedmb.pmb_attributes|_->false(* Operators *)typefamily=Ari|Cmp|Con(* Each rewritten operator with its family, the name of its rewrite and the
operator that replaces it. *)letoperators=[("+",(Ari,"sub","-"));("-",(Ari,"add","+"));("+.",(Ari,"fsub","-."));("-.",(Ari,"fadd","+."));("<",(Cmp,"le","<="));("<=",(Cmp,"lt","<"));(">",(Cmp,"ge",">="));(">=",(Cmp,"gt",">"));("=",(Cmp,"neq","<>"));("<>",(Cmp,"eq","="));("&&",(Con,"or","||"));("||",(Con,"and","&&"));](* A family keeps its sites only where its operators are Stdlib's: [ari]
names its replacement, [con] turns [&&] into an [if], and [cmp] swaps
operands of one type. A binding that this pass sees loses the family of
its name; what an [open] brings in is not seen. *)letkept_familiesstructure=letlosenamelost=matchList.assoc_optnameoperatorswith|Some(family,_,_)->family::lost|None->lostinletscan=objectinherit[familylist]Ast_traverse.foldassupermethod!patternplost=letlost=matchp.ppat_descwith|Ppat_var{txt;_}->losetxtlost|_->lostinsuper#patternplostmethod!value_descriptionvdlost=super#value_descriptionvd(losevd.pval_name.txtlost)endinletlost=scan#structurestructure[]infunfamily->not(List.memfamilylost)(* Mutants *)(* [Condition] is an [if] or [while] condition or an arm's guard, and
[Boolean] a direct operand of [&&] or [||]. [cmp] fires in these two
alone, where the expression must be a boolean, so that a user's comparison
cannot give its two arms two types. *)typecontext=Condition|Boolean|Ordinary(* A bare operator applied to two unlabelled arguments. A qualified operator
may be anything, and is no site. *)typeapplication={name:string;operator:expression;left:expression;right:expression;}letapplicatione=matche.pexp_descwith|Pexp_apply(({pexp_desc=Pexp_ident{txt=Lidentname;_};_}asoperator),[(Nolabel,left);(Nolabel,right)])->Some{name;operator;left;right}|_->Noneletappliesnamee=matchapplicationewith|Someapp->String.equalapp.namename|None->falseletis_connective~keepse=keepsCon&&(applies"&&"e||applies"||"e)(* [apply ~loc name args] applies the bare operator [name] to [args]. *)letapply~locnameargs=pexp_apply~loc(pexp_ident~loc{txt=Lidentname;loc})(List.map(funarg->(Nolabel,arg))args)(* A text is printed from the parsetree, since a preprocessor cannot rely on
the path of its source, and each run of blanks becomes one space. *)letrendere=Pprintast.string_of_expression{ewithpexp_attributes=[]}|>String.map(function'\t'|'\n'|'\r'->' '|c->c)|>String.split_on_char' '|>List.filter(funword->word<>"")|>String.concat" "(* How a guard arms its site. The emission law forbids naming a comparison's
partner, so [cmp] negates the operator the source wrote: on a total order
[a <= b] is [not (b < a)], so an ordering swaps its operands, and [a <> b]
is [not (a = b)], so an equality does not. *)typeguard=Neg|Operatorofapplication*operator_guardandoperator_guard=|Negate(* [cmp] on an equality *)|Swap(* [cmp] on an ordering *)|Branch(* [con] *)|Replaceofstring(* [ari], by this operator *)typemutant={guard:guard;rewrite:string;after:string}(* A connective with a connective operand carries no mutant, since its guard
would hold another site. *)letmutant~keepscontexte=letloc=e.pexp_locinletneg()=ifcontext<>ConditionthenNoneelseletnegated=apply~loc"not"[{ewithpexp_attributes=[]}]inSome{guard=Neg;rewrite="not";after=rendernegated}inmatchapplicationewith|None->neg()|Someapp->(matchList.assoc_optapp.nameoperatorswith|Some(family,rewrite,replacement)whenkeepsfamily->(letafter=render(apply~locreplacement[app.left;app.right])inletmutantguard=Some{guard=Operator(app,guard);rewrite;after}inmatchfamilywith|Con->ifis_connective~keepsapp.left||is_connective~keepsapp.rightthenNoneelsemutantBranch|Cmpwhencontext=Ordinary->None|Cmp->mutant(ifapp.name="="||app.name="<>"thenNegateelseSwap)|Ari->mutant(Replacereplacement))|Some_|None->neg())(* Guards *)(* A binder is site-indexed in a reserved namespace, so that a guard shadows
neither a user's binding nor another guard's. *)letbinderindexrole=Printf.sprintf"__windtrap_mut_%d_%s"indexrole(* The module is named after the file, so that each compilation unit calls its
own. The coverage rewriter mangles its module in the same way. *)letgenerated_module_namefile=letbuffer=Buffer.create(String.lengthfile+16)inBuffer.add_stringbuffer"Windtrap_mut___";String.iter(function|('A'..'Z'|'a'..'z'|'0'..'9'|'_')asc->Buffer.add_charbufferc|_->Buffer.add_stringbuffer"___")file;Buffer.contentsbufferletarmed~loc~module_nameindex=pexp_apply~loc(pexp_ident~loc{txt=Ldot(Lidentmodule_name,"___windtrap_armed___");loc})[(Nolabel,eint~locindex)](* [neg], and [cmp] on an equality. The value is bound once, so it is
evaluated as often as before, and binding the whole comparison leaves its
operands the order the compiler gives them. *)letnegation~loc~module_name~indexvalue=letp=binderindex"p"in[%exprlet[%ppvar~locp]=[%evalue]inif[%earmed~loc~module_nameindex]thenStdlib.not[%eevar~locp]else[%eevar~locp]](* [cmp] on an ordering, and [ari]: [let l, r = left, right in if armed then
... else l op r], where the disarmed arm is the application of [e] rebuilt
over the binders, with its location and its attributes.
The type-checker reads the tuple left to right, as it reads the arguments
of an application, so a qualified field of the left operand still resolves
an unqualified field of the right one. The match compiler destructures the
literal tuple without building it, into the [let] chain the application
compiles to, right operand first: each operand is evaluated once, in the
original order, with no allocation. A [let] chain written here would impose
one order on both, and a right-to-left check breaks that disambiguation.
An application also checks its right argument against the type of the left
one, which resolves a constructor or a record literal whose name a later
type declaration reuses. [~pin] restores that for an ordering by the
annotation [_ operands], with [type 'a operands = 'a * 'a]. The swapped arm
already needs one type for both operands, so the pin rejects nothing. A
variable [: 'a] cannot say it: it is scoped to the whole toplevel phrase,
so it would tie the phrase's sites together and keep a local
[let lt x y = x < y] from generalizing. [ari] does not pin, since an
[open]-provided [+] may take two types, and number types disambiguate
nothing.
An applied [(fun l r -> ...) left right] would keep its call and its
closure in bytecode under [-g]. Binding the operator, as in [let op = if
armed then ( <= ) else ( < )], names a second operator, and reaches the
polymorphic comparison even when nothing is armed. *)letbinary~loc~module_name~index~pineapp~armed_armleftright=letl=binderindex"l"andr=binderindex"r"inletoperands=lettuple=pexp_tuple~loc[left;right]inifnotpinthentupleelsepexp_constraint~loctuple(ptyp_constr~loc{txt=Ldot(Lidentmodule_name,"operands");loc}[ptyp_any~loc])inletdisarmed={(pexp_apply~loc:e.pexp_locapp.operator[(Nolabel,evar~locl);(Nolabel,evar~locr)])withpexp_attributes=e.pexp_attributes;}in[%exprlet[%ppvar~locl],[%ppvar~locr]=[%eoperands]inif[%earmed~loc~module_nameindex]then[%earmed_arm(evar~locl)(evar~locr)]else[%edisarmed]](* [con] branches once on the armed flag, which keeps the short circuit, the
tail position of [b] and one copy of each operand:
a && b -> let p = a in
if Stdlib.( <> ) (p : Stdlib.Bool.t) (armed i) then b else p
a || b -> let p = a in
if Stdlib.( = ) (p : Stdlib.Bool.t) (armed i) then b else p
Armed, the first reads [if not p then b else p], which is [a || b]. The
constraint makes the comparison an integer one, and it names
[Stdlib.Bool.t] because a file may declare its own [bool]. No node of the
guard is the original expression, so the whole guard takes its
attributes. *)letbranch~loc~module_name~indexeappleftright=letp=binderindex"p"inletvalue=[%expr([%eevar~locp]:Stdlib.Bool.t)]inletarmed=armed~loc~module_nameindexinlettest=ifString.equalapp.name"&&"then[%exprStdlib.(<>)[%evalue][%earmed]]else[%exprStdlib.(=)[%evalue][%earmed]]in{([%exprlet[%ppvar~locp]=[%eleft]inif[%etest]then[%eright]else[%eevar~locp]])withpexp_attributes=e.pexp_attributes;}(* Traversal *)(* [lazy] of a trivial syntactic value compiles as already forced, and a
guard under it would change that. Coverage's predicate, verbatim. No guard
weakens the generalization of a binding, since every site is an
application or a condition, which is never a syntactic value. *)letrecis_trivial_syntactic_valuee=matche.pexp_descwith|Pexp_function_|Pexp_poly_|Pexp_ident_|Pexp_constant_|Pexp_construct(_,None)->true|Pexp_constraint(inner,_)|Pexp_coerce(inner,_,_)->is_trivial_syntactic_valueinner|_->false(* Nothing under an opaque expression is rewritten. The type-checker gives
[assert false] every type only as written. *)letis_opaquee=matche.pexp_descwith|Pexp_assert_->true|Pexp_lazybody->is_trivial_syntactic_valuebody|_->falsetypesite={line:int;col:int;rewrite:string;before:string;after:string;dismissed:stringoption;}classinstrumenter~keeps~module_name=object(self)inheritAst_traverse.mapassuper(* The identifier of every recorded site, so its length is the next
index. *)valseen=Hashtbl.create64valmutablesites=[](* newest first *)valmutablepins=false(* a guard names [operands] *)valmutablesuppressed=false(* inside a [[@@@mutate off]] region *)methodsites=List.revsitesmethodpins=pins(* [site e mutant ~dismissed] records the site of [e] and is its index.
It is [None], with nothing recorded, for generated code and for a site
whose identifier another one has, as a deriver's copy does. *)methodprivatesitee(mutant:mutant)~dismissed=letstart=e.pexp_loc.loc_startinletline=start.pos_lnumandcol=start.pos_cnum-start.pos_bolinletkey=(line,col,mutant.rewrite)inife.pexp_loc.loc_ghost||Hashtbl.memseenkeythenNoneelsebeginletindex=Hashtbl.lengthseeninHashtbl.addseenkey();letsite={line;col;rewrite=mutant.rewrite;before=rendere;after=mutant.after;dismissed;}insites<-site::sites;Someindexend(* [chained] marks the left operand of a site that applies the same
operator: a chain of one operator carries one mutant, and reading it
from the tree keeps a bracket from changing the count. A dismissal
leaves [e] as written. *)methodprivatemutate?(chained=false)contexte=letoff=off_reasone.pexp_attributesinifis_opaqueetheneelsematch(off,mutant~keepscontexte)with|Some_,None->e|Somereason,Somemutant->ifnotchainedthenignore(self#siteemutant~dismissed:(Somereason));e|None,None->self#descende|None,Somemutant->letindex=ifchainedthenNoneelseself#siteemutant~dismissed:Noneinself#guardemutantindexmethodprivateguardemutantindex=letloc={e.pexp_locwithloc_ghost=true}inmatchmutant.guardwith|Neg->(letvalue=self#descendeinmatchindexwith|None->value|Someindex->negation~loc~module_name~indexvalue)|Operator(app,guard)->(letcontext=matchguardwith|Branch->Boolean|Negate|Swap|Replace_->Ordinaryinletchained=appliesapp.nameapp.leftinletleft=self#mutate~chainedcontextapp.leftinletright=self#mutatecontextapp.rightinletrebuilt={ewithpexp_desc=Pexp_apply(app.operator,[(Nolabel,left);(Nolabel,right)]);}inmatchindexwith|None->rebuilt|Someindex->(matchguardwith|Negate->negation~loc~module_name~indexrebuilt|Swap->pins<-true;binary~loc~module_name~index~pin:trueeappleftright~armed_arm:(funlr->letswapped=pexp_apply~locapp.operator[(Nolabel,r);(Nolabel,l)]in[%exprStdlib.not[%eswapped]])|Branch->branch~loc~module_name~indexeappleftright|Replacereplacement->binary~loc~module_name~index~pin:falseeappleftright~armed_arm:(funlr->apply~locreplacement[l;r])))(* [descend e] rewrites the children of [e], each in the context that [e]
gives it. A connective that carries no mutant still has boolean
operands. *)methodprivatedescende=matche.pexp_descwith|Pexp_ifthenelse(condition,then_,else_)->{ewithpexp_desc=Pexp_ifthenelse(self#mutateConditioncondition,self#expressionthen_,Option.mapself#expressionelse_);}|Pexp_while(condition,body)->{ewithpexp_desc=Pexp_while(self#mutateConditioncondition,self#expressionbody);}|Pexp_apply(operator,[(Nolabel,left);(Nolabel,right)])whenis_connective~keepse->{ewithpexp_desc=Pexp_apply(operator,[(Nolabel,self#mutateBooleanleft);(Nolabel,self#mutateBooleanright);]);}|_->super#expressione(* Inside a region, every structure item but a value binding reaches its
expressions through here. *)method!expressione=ifsuppressedtheneelseself#mutateOrdinaryemethod!casec={pc_lhs=self#patternc.pc_lhs;pc_guard=Option.map(self#mutateCondition)c.pc_guard;pc_rhs=self#expressionc.pc_rhs;}method!module_bindingmb=ifis_offmb.pmb_attributesthenmbelsesuper#module_bindingmbmethod!structure_itemsi=matchsi.pstr_descwith|Pstr_attributeattribute->(matchdirectiveattributewith|No_directive->()|Off_->ifsuppressedthenLocation.raise_errorf~loc:attribute.attr_loc"Mutation is already off.";suppressed<-true|On->ifnotsuppressedthenLocation.raise_errorf~loc:attribute.attr_loc"Mutation is already on.";suppressed<-false|Exclude_file->err_misplacedattribute"exclude_file");si|Pstr_value(rec_flag,bindings)whennotsuppressed->letbindingb=ifis_offb.pvb_attributesthenbelse{bwithpvb_expr=self#expressionb.pvb_expr}in{siwithpstr_desc=Pstr_value(rec_flag,List.mapbindingbindings);}|_->super#structure_itemsimethod!structureitems=letouter=suppressedinletitems=List.map(funsi->ifis_test_itemsithensielseself#structure_itemsi)itemsinsuppressed<-outer;itemsmethod!extensionextension=extensionmethod!attributeattribute=attributeend(* The generated module *)(* The module re-exports the runtime's [site] type, so that the table names
its fields without type-directed disambiguation: the runtime declares these
labels on three records, and warning 42 is fatal under [-w +a -warn-error
+a]. The type equation turns a runtime record that drifted into a compile
error. Because of its labels the module is never opened, and a file whose
every site is dismissed has no unused [open].
[register] allocates the arrays of the file, so indices are file-local, and
the stop comments hide the module from odoc. The module stands above the
user's code, so it names [int], [option] and [Some] unqualified, which a
guard cannot. *)letruntime_initialization~file~module_name~sites~pins=letloc={(Location.in_filefile)withloc_ghost=true}inletsite{line;col;rewrite;before;after;dismissed}=letdismissed=matchdismissedwith|None->[%exprNone]|Somereason->[%exprSome[%eestring~locreason]]in[%expr{line=[%eeint~locline];col=[%eeint~loccol];rewrite=[%eestring~locrewrite];before=[%eestring~locbefore];after=[%eestring~locafter];dismissed=[%edismissed];}]inletsite_type=[%stritypesite=Windtrap_runtime.Mutate.site={line:int;col:int;rewrite:string;before:string;after:string;dismissed:stringoption;}]inletoperands=[%stritype'aoperands='a*'a]inletarmed=[%strilet___windtrap_armed___=Windtrap_runtime.Mutate.register~file:[%eestring~locfile]~sites:[%epexp_array~loc(List.mapsitesites)]]inletitems=ifpinsthen[site_type;operands;armed]else[site_type;armed]inletgenerated=pstr_module~loc(module_binding~loc~name:{txt=Somemodule_name;loc}~expr:(pmod_structure~locitems))inletstop_comment=[%stri[@@@ocaml.text"/*"]]in[stop_comment;generated;stop_comment](* Rewriting *)(* The ignore lists are the coverage rewriter's. *)letalways_ignore_paths=["//toplevel//";"(stdin)"]letalways_ignore_basenames=[".ocamlinit";"topfind"]lettransform_impl_filectxtast=letfile=Expansion_context.Base.input_namectxtinifList.memfilealways_ignore_paths||List.mem(Filename.basenamefile)always_ignore_basenames||excludes_fileastthenastelseletmodule_name=generated_module_namefileinletinstrumenter=newinstrumenter~keeps:(kept_familiesast)~module_nameinletinstrumented=instrumenter#structureastinmatchinstrumenter#siteswith|[]->ast|sites->runtime_initialization~file~module_name~sites~pins:instrumenter#pins@instrumented