123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638(*---------------------------------------------------------------------------
Copyright (c) 2026 Invariant Systems. All rights reserved.
Portions adapted from Bisect_ppx (MIT license).
SPDX-License-Identifier: ISC
---------------------------------------------------------------------------*)openPpxlibopenAst_builder.DefaultmoduleExp=Ast_helper.Exp(* Attributes *)(* The grammar is ppx/mutate/instrument.ml's too, under its own name;
test/cram/ppx/coverage.t pins that the two agree. The reason of an [off] is
for the reader of the source. *)letcoverage_attribute{attr_name;attr_payload;attr_loc=loc}=ifnot(String.equalattr_name.txt"coverage")then`Noneelseletpayload=matchattr_payloadwith|PStr[{pstr_desc=Pstr_eval(payload,_);_}]->Somepayload.pexp_desc|_->Noneinmatchpayloadwith|Some(Pexp_ident{txt=Lident"off";_})|Some(Pexp_apply({pexp_desc=Pexp_ident{txt=Lident"off";_};_},[(Nolabel,{pexp_desc=Pexp_constant(Pconst_string_);_})]))->`Off|Some(Pexp_ident{txt=Lident"on";_})->`On|Some(Pexp_ident{txt=Lident"exclude_file";_})->`Exclude_file|_->Location.raise_errorf~loc"Bad payload in coverage attribute."leterr_misplaced~locspelling=Location.raise_errorf~loc"coverage %s is not allowed here."spelling(* Every attribute is checked, not only those before an [off]. *)letis_offattributes=List.fold_left(funoffattribute->matchcoverage_attributeattributewith|`None->off|`Off->true|`On->err_misplaced~loc:attribute.attr_loc"on"|`Exclude_file->err_misplaced~loc:attribute.attr_loc"exclude_file")falseattributesletis_tail_mod_consattributes=List.exists(fun{attr_name={txt;_};_}->txt="tail_mod_cons"||txt="ocaml.tail_mod_cons")attributesletexcludes_file=function|{pstr_desc=Pstr_attributeattribute;_}->coverage_attributeattribute=`Exclude_file|_->false(* 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(* Points *)(* [key], the identity of a point, is one byte offset of the expression the
point is attributed to; the extent is what a report paints. *)typepoint={key:int;start_ofs:int;end_ofs:int}typestate={mutablerev_points:pointlist;(* newest first *)mutablecount:int;(* the length of [rev_points] *)mutableuses_post:bool;(* an out-edge was marked *)}(* The index of the point at [key], allocated with [extent] unless there is
one already, whose extent then stands. *)letpointst~key(extent:location)=letrecfindindex=function|p::_whenp.key=key->index|_::older->find(index-1)older|[]->letstart_ofs=extent.loc_start.pos_cnuminletend_ofs=extent.loc_end.pos_cnuminst.rev_points<-{key;start_ofs;end_ofs}::st.rev_points;st.count<-st.count+1;st.count-1infind(st.count-1)st.rev_points(* Marks *)(* A mark is attributed to [anchor]: its point is keyed at the anchor's
start, or at its last byte under [at_end], and a generated (ghost) or
switched-off anchor takes no mark. The wrapper takes the location of [e],
so a later mark of the same node keys and paints the source. *)letmarkst~anchor~at_end~extent~poste=letat=anchor.pexp_locinifat.loc_ghost||is_offanchor.pexp_attributestheneelseletkey=ifat_endthenat.loc_end.pos_cnum-1elseat.loc_start.pos_cnuminletloc=e.pexp_locinletindex=eint~loc(pointst~keyextent)inifpostthenbeginst.uses_post<-true;[%expr___windtrap_post_visit___[%eindex][%ee]]endelse[%expr___windtrap_visit___[%eindex];[%ee]]letentryst?extente=letextent=Option.valueextent~default:e.pexp_locinmarkst~anchor:e~at_end:false~extent~post:falsee(* The body of an arm paints the whole arm, from its pattern, so an arm never
entered shows its [| pattern ->] line. The guard runs whenever the pattern
matches, so it is a block of its own. An arm that cannot be entered
([assert false], a refutation) or is switched off keeps its guard unmarked
too. *)letarmstcase=matchcase.pc_rhswith|[%exprassertfalse]|{pexp_desc=Pexp_unreachable;_}->case|{pexp_attributes;_}whenis_offpexp_attributes->case|rhs->letpat=case.pc_lhs.ppat_locinletextent=ifpat.loc_ghost||pat.loc_start.pos_cnum>rhs.pexp_loc.loc_start.pos_cnumthenrhs.pexp_locelse{rhs.pexp_locwithloc_start=pat.loc_start}inletpc_guard=Option.map(entryst)case.pc_guardinletpc_rhs=entryst~extentrhsin{casewithpc_guard;pc_rhs}(* Semantics guards *)(* [lazy] compiles a trivial syntactic value as already forced, and a visit
would make it a thunk. ppx/mutate/instrument.ml has the same test. *)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(* The primitives whose applications take no out-edge, as Bisect_ppx lists
them. They cannot fail interestingly or never return, and an out-edge on
every operator would double the table for no signal. *)lettrivial_primitives=String.split_on_char' '"&& & not = <> < <= > >= == != ref ! := @ ^ + - * / +. -. *. /. mod land \
lor lxor lsl lsr asr raise raise_notrace failwith ignore ##"letis_trivial_functione=matche.pexp_descwith|Pexp_ident{txt=Lidentname;_}->List.memnametrivial_primitives|Pexp_ident{txt=Ldot(Lident"Sys","opaque_identity")|Ldot(Lident"Obj","magic");_;}->true|_->false(* Whether a tail call can sit in [e] when [e] is in tail position. The
right operand of [||] that can hold one keeps its position and gives up its
point, since an [if] condition would take the call out of tail position. *)letholds_tail_calle=matche.pexp_descwith|Pexp_apply(callee,_)->not(is_trivial_functioncallee)|Pexp_send_|Pexp_new_|Pexp_let_|Pexp_letmodule_|Pexp_letexception_|Pexp_open_|Pexp_match_|Pexp_try_|Pexp_ifthenelse_|Pexp_sequence_|Pexp_letop_|Pexp_constraint_|Pexp_coerce_->true|_->false(* Traversal *)(* The position of an expression decides its out-edge. In [Tail] position a
call is never wrapped, since the wrap would take it out of tail position.
Elsewhere the out-edge is keyed at the start of the expression control
reaches next, when it is known ([Before]); is left to an enclosing form
that observes the return itself ([Observed]); or is keyed at the
expression's own callee ([Nontail]). *)typeposition=Tail|Nontail|Beforeofexpression|Observedletis_tail=functionTail->true|Nontail|Before_|Observed->false(* A sub-expression that ends its parent's evaluation is in tail position
when its parent is, and knows no successor. *)letinheritedposition=ifis_tailpositionthenTailelseNontail(* The head function of a curried application. *)letrechead_calleee=matche.pexp_descwith|Pexp_apply(f,_)whennot(is_offe.pexp_attributes)->head_calleef|_->eletout_edgestposition~calleee=matchpositionwith|Tail|Observed->e|Nontail->markst~anchor:callee~at_end:true~extent:e.pexp_loc~post:truee|Beforenext->markst~anchor:next~at_end:false~extent:e.pexp_loc~post:truee(* A node's sub-expressions are traversed before its own blocks are marked,
so a wrapper is never traversed and inner points are numbered first. *)classinstrumenterst=object(self)inheritAst_traverse.mapassuper(* Set in a [[@@@coverage off]] region; [structure] restores it on exit. *)valmutablesuppressed=false(* Set in the body of a [[@tail_mod_cons]] binding. TMC rewrites a call
in a constructor argument of a tail expression, and an out-edge wrap
leaves it no call to rewrite. The result is warning 71, an error in
dune's dev profile, or else a function that silently consumes stack. *)valmutablein_tmc_body=falsemethodprivatebindingtraverse(binding:value_binding)=letouter=in_tmc_bodyinifis_tail_mod_consbinding.pvb_attributesthenin_tmc_body<-true;letpvb_expr=traversebinding.pvb_exprinin_tmc_body<-outer;{bindingwithpvb_expr}method!expressione=ifsuppressedtheneelseletrectraversepositione=ifis_offe.pexp_attributestheneelseletloc=e.pexp_locandattrs=e.pexp_attributesinmatche.pexp_descwith|Pexp_apply((([%expr(|>)]|[%expr(|.)])aspipe),[(l,lhs);(l',rhs)])->letlhs'=traverse(Beforerhs)lhsinletrhs'=traverseObservedrhsinletapply=Exp.apply~loc~attrspipe[(l,lhs');(l',rhs')]inout_edgestposition~callee:(head_calleerhs)apply|Pexp_apply(([%expr(||)]|[%expr(or)]),[(_,left);(_,right)])->(* [(v; true)], where the visit [v] counts the times
[operand] was true. *)letwas_trueoperand=markst~anchor:operand~at_end:true~extent:operand.pexp_loc~post:false[%exprtrue]inletleft_true=was_trueleftinletright'=matchright.pexp_descwith|Pexp_apply(([%expr(||)]|[%expr(or)]),_)->traverse(inheritedposition)right|_whenis_tailposition&&holds_tail_callright->traverseTailright|_->letright'=traverseNontailrightinletright_true=was_truerightin[%exprif[%eright']then[%eright_true]elsefalse]inletleft'=traverseNontailleftin[%exprif[%eleft']then[%eleft_true]else[%eright']]|Pexp_apply(fn,args)->letargs=match(fn,args)with|([%expr(&&)]|[%expr(&)]),[(l,left);(l',right)]->(* [right] runs only when [left] was true, and an entry
never takes it out of tail position. *)letleft=traverseNontailleftinletright=traverse(inheritedposition)rightin[(l,left);(l',entrystright)]|([%expr(@@)],[(l,({pexp_desc=Pexp_apply_;_}asf));(l',x)])->letf=traverseObservedfin[(l,f);(l',traverseNontailx)]|_->List.map(fun(l,x)->(l,traverseNontailx))argsin(* A [new] or a method call as the callee returns into the
application, whose out-edge observes it. *)letfn'=matchfn.pexp_descwith|Pexp_new_->fn|Pexp_send_->traverseObservedfn|_->traverseNontailfninletapply=Exp.apply~loc~attrsfn'argsin(* An application whose every argument is labelled may be
partial, and its closure says nothing of the call; the
parsetree shows only the labels. *)letall_labelled=List.for_all(function|Nolabel,_->false|(Labelled_|Optional_),_->true)argsinifin_tmc_body||all_labelled||is_trivial_functionfnthenapplyelseletcallee=match(fn,args)with|[%expr(@@)],[(_,f);_]->f|_->fninout_edgestposition~calleeapply|Pexp_send(obj,meth)->letsend=Exp.send~loc~attrs(traverseNontailobj)methinifin_tmc_bodythensendelseout_edgestposition~callee:sendsend|Pexp_new_->out_edgestposition~callee:ee|Pexp_assert[%exprfalse]->e|Pexp_assertinner->letassertion=Exp.assert_~loc~attrs(traverseNontailinner)inmarkst~anchor:inner~at_end:false~extent:loc~post:trueassertion|Pexp_function(params,constraint_,body)->letparams=List.mapparamparamsin(* Only the leaf body of a curried chain is a block; a
constraint or a coercion on it stays around the visit. *)letrecleafbody=matchbody.pexp_descwith|Pexp_function_->body|Pexp_constraint(inner,t)->{bodywithpexp_desc=Pexp_constraint(leafinner,t)}|Pexp_coerce(inner,t,t')->{bodywithpexp_desc=Pexp_coerce(leafinner,t,t')}|_->entrystbodyinletbody=matchbodywith|Pfunction_bodybody->Pfunction_body(leaf(traverseTailbody))|Pfunction_cases(cases,cases_loc,cases_attrs)->Pfunction_cases(armsTailcases,cases_loc,cases_attrs)in{ewithpexp_desc=Pexp_function(params,constraint_,body)}|Pexp_match(scrutinee,cases)->letcases=arms(inheritedposition)casesinExp.match_~loc~attrs(traverseObservedscrutinee)cases|Pexp_try(body,cases)->letcases=arms(inheritedposition)casesinExp.try_~loc~attrs(traverseNontailbody)cases|Pexp_ifthenelse(cond,then_,else_)->letcond=traverseObservedcondinletthen_=traverse(inheritedposition)then_inletelse_=Option.map(funelse_->entryst(traverse(inheritedposition)else_))else_inExp.ifthenelse~loc~attrscond(entrystthen_)else_|Pexp_while(cond,body)->letcond=traverseNontailcondinExp.while_~loc~attrscond(entryst(traverseNontailbody))|Pexp_for(pat,init,bound,direction,body)->letinit=traverseNontailinitinletbound=traverseNontailboundinExp.for_~loc~attrspatinitbounddirection(entryst(traverseNontailbody))|Pexp_lazybody->letbody=traverseTailbodyinExp.lazy_~loc~attrs(ifis_trivial_syntactic_valuebodythenbodyelseentrystbody)|Pexp_poly(body,t)->letbody=traverseTailbodyinletbody=matchbody.pexp_descwith|Pexp_function_->body|_->entrystbodyinExp.poly~loc~attrsbodyt|Pexp_letop{let_;ands;body}->letoperandop={opwithpbop_exp=traverseNontailop.pbop_exp}inletlet_=operandlet_inletands=List.mapoperandandsinExp.letop~loc~attrslet_ands(entryst(traverseTailbody))|Pexp_let(rec_flag,bindings,body)->letbound=matchbindingswith[_]->Beforebody|_->Nontailinletbindings=List.map(self#binding(traversebound))bindingsinExp.let_~loc~attrsrec_flagbindings(traverse(inheritedposition)body)|Pexp_sequence(first,second)->letsecond=traverse(inheritedposition)secondin(* After [if c then e], reaching the rest is informative:
the point of [e] does not count when [c] is false. *)letsecond=matchfirst.pexp_descwith|Pexp_ifthenelse(_,_,None)->entrystsecond|_->secondinExp.sequence~loc~attrs(traverse(Beforesecond)first)second|Pexp_ident_|Pexp_constant_|Pexp_extension_|Pexp_unreachable->e|Pexp_tuplees->Exp.tuple~loc~attrs(List.map(traverseNontail)es)|Pexp_construct(c,arg)->Exp.construct~loc~attrsc(Option.map(traverseNontail)arg)|Pexp_variant(c,arg)->Exp.variant~loc~attrsc(Option.map(traverseNontail)arg)|Pexp_record(fields,base)->letfields=List.map(fun(f,x)->(f,traverseNontailx))fieldsinExp.record~loc~attrsfields(Option.map(traverseNontail)base)|Pexp_field(record,f)->Exp.field~loc~attrs(traverseNontailrecord)f|Pexp_setfield(record,f,value)->letrecord=traverseNontailrecordinExp.setfield~loc~attrsrecordf(traverseNontailvalue)|Pexp_arrayes->Exp.array~loc~attrs(List.map(traverseNontail)es)|Pexp_constraint(inner,t)->Exp.constraint_~loc~attrs(traverse(inheritedposition)inner)t|Pexp_coerce(inner,t,t')->Exp.coerce~loc~attrs(traverse(inheritedposition)inner)tt'|Pexp_setinstvar(f,value)->Exp.setinstvar~loc~attrsf(traverseNontailvalue)|Pexp_overridefields->Exp.override~loc~attrs(List.map(fun(f,x)->(f,traverseNontailx))fields)|Pexp_letmodule(m,module_expr,body)->letmodule_expr=self#module_exprmodule_exprinExp.letmodule~loc~attrsmmodule_expr(traverse(inheritedposition)body)|Pexp_letexception(c,body)->Exp.letexception~loc~attrsc(traverse(inheritedposition)body)|Pexp_open(decl,body)->letdecl=self#open_declarationdeclinExp.open_~loc~attrsdecl(traverse(inheritedposition)body)|Pexp_newtype(t,body)->Exp.newtype~loc~attrst(traverse(inheritedposition)body)|Pexp_objectc->Exp.object_~loc~attrs(self#class_structurec)|Pexp_packm->Exp.pack~loc~attrs(self#module_exprm)(* The default of an optional argument runs only when the caller
omits the argument, so it is a block. *)andparamp=matchp.pparam_descwith|Pparam_val(label,Somedefault,pat)->letdefault=entryst(traverseNontaildefault)in{pwithpparam_desc=Pparam_val(label,Somedefault,pat)}|Pparam_val(_,None,_)|Pparam_newtype_->pandarmspositioncases=letcasec=letpc_guard=Option.map(traverseNontail)c.pc_guardin{cwithpc_guard;pc_rhs=traversepositionc.pc_rhs}inList.map(armst)(List.mapcasecases)intraverseNontaile(* The optional-argument defaults of a class, its concrete method bodies
and its initializers are blocks. *)method!class_exprce=ifsuppressedthenceelseletce=super#class_exprceinmatchce.pcl_descwith|Pcl_fun(label,default,pat,body)->letdefault=Option.map(entryst)defaultin{cewithpcl_desc=Pcl_fun(label,default,pat,body)}|_->cemethod!class_fieldcf=ifsuppressedthencfelseletcf=super#class_fieldcfinmatchcf.pcf_descwith|Pcf_method(name,private_,Cfk_concrete(override,body))->letbody=Cfk_concrete(override,entrystbody)in{cfwithpcf_desc=Pcf_method(name,private_,body)}|Pcf_initializerbody->{cfwithpcf_desc=Pcf_initializer(entrystbody)}|_->cfmethod!module_bindingmb=ifis_offmb.pmb_attributesthenmbelsesuper#module_bindingmbmethod!structure_itemsi=matchsi.pstr_descwith|Pstr_attributeattribute->letloc=attribute.attr_locin(matchcoverage_attributeattributewith|`None->()|`Off->ifsuppressedthenLocation.raise_errorf~loc"Coverage is already off.";suppressed<-true|`On->ifnotsuppressedthenLocation.raise_errorf~loc"Coverage is already on.";suppressed<-false|`Exclude_file->err_misplaced~loc"exclude_file");si|Pstr_value(rec_flag,bindings)whennotsuppressed->letbindingb=ifis_offb.pvb_attributesthenbelseself#bindingself#expressionbin{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!extensionx=xmethod!attributex=xend(* Generated module *)(* Each compilation unit calls the visit functions of its own module, named
after the file, since a bare binding could be shadowed by a later [open]
and a file that includes another would collide with it.
ppx/mutate/instrument.ml mangles the same way under its own prefix. *)letmodule_namefile=letb=Buffer.create(String.lengthfile+16)inBuffer.add_stringb"Windtrap_cov___";String.iter(function|('A'..'Z'|'a'..'z'|'0'..'9'|'_')asc->Buffer.add_charbc|_->Buffer.add_stringb"___")file;Buffer.contentsb(* The stop comments hide the module from odoc. *)letgenerated_modulest~file=letloc={(Location.in_filefile)withloc_ghost=true}inletname=module_namefilein(* Every field is qualified, since a bare one would resolve by
type-directed disambiguation (warning 42). *)letpointp=[%expr{Windtrap_runtime.Coverage.start_ofs=[%eeint~locp.start_ofs];Windtrap_runtime.Coverage.end_ofs=[%eeint~locp.end_ofs];}]inletvisit=[%strilet___windtrap_visit___=letcounts=Array.make[%eeint~locst.count]0inWindtrap_runtime.Coverage.register~file:[%eestring~locfile]~points:[%epexp_array~loc(List.rev_mappointst.rev_points)]~counts;funindex->Windtrap_runtime.Coverage.visitcountsindex]inletpost_visit=[%strilet___windtrap_post_visit___point_indexresult=___windtrap_visit___point_index;result]inletitems=ifst.uses_postthen[visit;post_visit]else[visit]inletstop_comment=[%stri[@@@ocaml.text"/*"]]in[stop_comment;pstr_module~loc(module_binding~loc~name:{txt=Somename;loc}~expr:(pmod_structure~locitems));pstr_open~loc(open_infos~loc~override:Fresh~expr:(pmod_ident~loc{txt=Lidentname;loc}));stop_comment;](* Rewriting *)(* Toplevel phrases and findlib's scripts; ppx/mutate/instrument.ml skips
the same inputs. *)letis_ignoredfile=List.memfile["//toplevel//";"(stdin)"]||List.mem(Filename.basenamefile)[".ocamlinit";"topfind"]lettransform_impl_filectxtast=letfile=Expansion_context.Base.input_namectxtinifis_ignoredfile||List.existsexcludes_fileastthenastelseletst={rev_points=[];count=0;uses_post=false}inletinstrumented=(newinstrumenterst)#structureastinifst.count=0thenastelsegenerated_modulest~file@instrumented