1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051(**************************************************************************)(* This file is part of the Codex semantics library *)(* (Union-find lattice subcomponent). *)(* *)(* Copyright (C) 2026 *)(* CEA (Commissariat à l'énergie atomique et aux énergies *)(* alternatives) *)(* *)(* you can redistribute it and/or modify it under the terms of the GNU *)(* Lesser General Public License as published by the Free Software *)(* Foundation, version 2.1. *)(* *)(* It is distributed in the hope that it will be useful, *)(* but WITHOUT ANY WARRANTY; without even the implied warranty of *)(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *)(* GNU Lesser General Public License for more details. *)(* *)(* See the GNU Lesser General Public License version 3.0 *)(* for more details (enclosed in the file LICENSE). *)(* *)(**************************************************************************)modulePT=PatriciaTreemoduleArrayWithCopy(Config:Parameters.ARRAY_CONFIG)(Node:Parameters.POLYMORPHIC_NODE)(Relation:Parameters.POLYMORPHIC_GROUP)(Value:Parameters.POLYMORPHIC_VALUEwithtype('a,'b)relation=('a,'b)Relation.tandtype'anode='aNode.t)=structtype'anode='aNode.ttype'avalue='aValue.ttype('a,'b)relation=('a,'b)Relation.tlet(**)=Relation.composelet(~~)=Relation.inverse(** Parent pointers. Since we have no reverse mapping [int -> node],
we must store [self]-nodes alongside the parents (needed in the join). *)type'aptr=|Uninitialized:'aptr(** Unitialized element. This will always be a element that never appeared in a union,
and has no value. *)|Root:{self:'aNode.t;value:'aValue.toption;(** value attached to the class *)}->'aptr|Child:{self:'aNode.t;relation:('a,'b)Relation.t;parent:'bNode.t;}->'aptr(** [\exists 'a, 'a ptr] type. *)typewrapped=Wrap:'aptr->wrapped[@@unboxed]typet={parents:wrappedarray;ranks:intarray;}letdefault_rank=0letmaken={parents=Array.maken(WrapUninitialized);ranks=Array.makendefault_rank;}type'afind_result=FindResult:{representative:'bnode;relation:('a,'b)relation;}->'afind_result(** Find in CPS style to avoid stack overflows *)letrecfind:typea.t->anode->(afind_result->'res)->'res=funufxk->leti=Node.to_intxinmatchuf.parents.(i)with|WrapUninitialized->uf.parents.(i)<-Wrap(Root{self=x;value=None});k(FindResult{representative=x;relation=Relation.identity})|Wrap(Root_)->k(FindResult{representative=x;relation=Relation.identity})|Wrap(Childc)->findufc.parent(fun(FindResulty)->letrelation=y.relation**c.relationinmatchNode.polyeqc.selfxwith|Diff->assertfalse|Eq->ifConfig.path_compression=`Lazythenuf.parents.(i)<-Wrap(Child{self=x;relation;parent=y.representative});k(FindResult{ywithrelation}))letfindufx=findufxFun.id(** Variant of {!find} that also returns the value *)type'afind_all=FindAll:{representative:'bnode;relation:('a,'b)relation;value:'bvalueoption;}->'afind_allletrecfind_all:typea.t->anode->(afind_all->'b)->'b=funufxk->leti=Node.to_intxinmatchuf.parents.(i)with|WrapUninitialized->uf.parents.(i)<-Wrap(Root{self=x;value=None});k(FindAll{representative=x;relation=Relation.identity;value=None})|Wrap(Rootv)->beginmatchNode.polyeqv.selfxwith(* match only needed for type-checking *)|Eq->k(FindAll{representative=x;relation=Relation.identity;value=v.value})|Diff->assertfalseend|Wrap(Childc)->find_allufc.parent(fun(FindAlly)->letrelation=y.relation**c.relationinmatchNode.polyeqc.selfxwith(* match only needed for type-checking *)|Diff->assertfalse|Eq->ifConfig.path_compression=`Lazythenuf.parents.(i)<-Wrap(Child{self=x;relation;parent=y.representative});k(FindAll{ywithrelation}))letfind_allufx=find_allufxFun.idletcheck_relatedufxy=letFindResultx=findufxinletFindResulty=findufyinmatchNode.polyeqx.representativey.representativewith|Eq->Some(~~(y.relation)**x.relation)|Diff->Noneletget_valueufx=letFindAllx=find_allufxinmatchx.valuewith|None->None|Somev->Some(Value.applyx.representativev~~(x.relation))letset_value~intersectufzv=letFindAllx=find_allufzinletv=Value.applyzvx.relationinletv=matchx.valuewith|None->v|Somev'->ifintersectthenValue.meetx.representativevv'elsevinuf.parents.(Node.to_intx.representative)<-Wrap(Root{self=x.representative;value=Somev});uf(** Helper for {!add_relation}, performs a directed union, making [child] point to [parent].
Assumes [child] and [parent] are representatives *)letmk_childufchildparentrelationchild_valuerank=letchild_ptr=Child{self=child;parent=parent;relation=relation}inuf.parents.(Node.to_intchild)<-Wrapchild_ptr;letparent_id=Node.to_intparentinbeginmatchuf.parents.(parent_id)with|WrapUninitialized->uf.parents.(parent_id)<-Wrap(Root{self=parent;value=None})|_->()end;uf.ranks.(parent_id)<-rank;matchchild_valuewith|None->uf|Somev->set_value~intersect:trueufchildvletcombine_rankssmalllarge=ifsmall=largethenlarge+1elselargeletadd_relation:typeab.t->aNode.t->bNode.t->(a,b)Relation.t->(t,(a,b)Relation.t)result=funtabrel->letFindAlla=find_alltainletFindAllb=find_alltbinmatchNode.polyeqa.representativeb.representativewith|Eq->(* Both elements point to the same representative *)letold_rel=~~(b.relation)**a.relationinifRelation.equalrelold_relthenOktelseErrorold_rel|Diff->letrank_a=t.ranks.(Node.to_inta.representative)inletrank_b=t.ranks.(Node.to_intb.representative)inifrank_a<rank_bthenmk_childta.representativeb.representative(b.relation**rel**~~(a.relation))a.value(combine_ranksrank_arank_b)|>Result.okelsemk_childtb.representativea.representative(a.relation**~~rel**~~(b.relation))b.value(combine_ranksrank_brank_a)|>Result.ok(** {1 Lattice operations} *)(****************************************************************************)letcopy{parents;ranks}={parents=Array.copyparents;ranks=Array.copyranks}(** {2 Meet} *)(********************************************************************)typewrapped_relation=Rel:'anode*('a,'b)relation*'bnode->wrapped_relationletmeetuv=letreciterresnerrs=ifn<0thenres,errselsematchArray.getv.parentsnwith|Wrap(Root{value=Somev;self})->iter(set_value~intersect:trueresselfv)(n-1)errs|Wrap(Uninitialized)|Wrap(Root_)->iterres(n-1)errs|Wrap(Child{self;parent;relation})->matchadd_relationresselfparentrelationwith|Okres->iterres(n-1)errs|Error_->letres=matchget_valuevselfwith|None->res|Somev->set_value~intersect:trueresselfviniterres(n-1)(Rel(self,relation,parent)::errs)initer(copyu)(Array.lengthu.parents-1)[](** {2 Incl} *)(********************************************************************)letincluv=letrecitern=ifn<0thentrueelsematchArray.getv.parentsnwith|Wrap(Root{value=Somev;self})->beginmatchget_valueuselfwith|None->false|Somev'->Value.inclselfv'v&&iter(n-1)end|Wrap(Uninitialized)|Wrap(Root_)->iter(n-1)|Wrap(Child{self;parent;relation})->matchcheck_relateduselfparentwith|None->false|Somerel->Relation.equalrelationrel&&iter(n-1)initer(Array.lengthu.parents-1)moduleTriple=structtype_t=Triple:'anode*'bnode*('a,'b)relation->('a*'b)tletpolyeq:typeab.at->bt->(a,b)PatriciaTree.cmp=fun(Triple(l1,r1,rel))(Triple(l2,r2,rel'))->matchNode.polyeql1l2with|Diff->Diff|Eq->matchNode.polyeqr1r2withEq->ifRelation.equalrelrel'thenEqelseDiff|Diff->Difflethash:typea.at->int=fun(Triple(l,r,rel))->Utils.Functions.hash_pair(Node.to_intl)(Node.to_intr)|>Utils.Functions.hash_pair(Relation.hashrel)endtype_memoized_item=Item:{representative:'cnode;left_relation:('a,'c)relation;}->('a*'b)memoized_itemmoduleH=Utils.HetHashtbl.Make(Triple)(structtype('a,_)t='amemoized_itemend)letvalue_join_optx(FindAlll)(FindAllr)=matchl.value,r.valuewith|None,_|_,None->None|Somev1,Somev2->Value.joinx(Value.applyl.representativev1(~~(l.relation)))(Value.applyr.representativev2(~~(r.relation)))typewnode=WNode:'aNode.t->wnodeletget_nodeuf_auf_bi=matchuf_a.parents.(i)with|Wrap(Child{self;_;})->Some(WNodeself)|Wrap(Root{self;_})->Some(WNodeself)|WrapUninitialized->matchuf_b.parents.(i)with|Wrap(Child{self;_;})->Some(WNodeself)|Wrap(Root{self;_})->Some(WNodeself)|WrapUninitialized->Noneletjoinuf_auf_b=letn=Array.lengthuf_a.parentsinletres=ref(maken)inletnew_classes=H.create10infori=0ton-1domatchget_nodeuf_auf_biwith|None->()(* uninitialized in both -> uninitialized in the join. *)|Some(WNodenode)->letFindAllaasfa=find_alluf_anodeinletFindAllbasfb=find_alluf_bnodeinlettriple=Triple.Triple(a.representative,b.representative,b.relation**~~(a.relation))inmatchH.find_optnew_classestriplewith|Some(Itemi)->res:=add_relation!resnodei.representative(i.left_relation**a.relation)|>Result.get_ok|None->H.addnew_classestriple(Item{representative=node;left_relation=~~(a.relation)});matchvalue_join_optnodefafbwith|None->()|Somev->res:=set_value~intersect:false!resnodev;done;!resletcheck_invariants_=Noneletprettyfmtt=Format.pp_print_list(funfmtparent->matchparentwith|WrapUninitialized->Format.pp_print_stringfmt"U"|Wrap(Rootr)->Format.fprintffmt"R(%a)"(Utils.Functions.pp_option(Value.prettyr.self))r.value|Wrap(Childc)->Format.fprintffmt"C(%a,%a)"Node.prettyc.parentRelation.prettyc.relation)fmt(Array.to_listt.parents)endmodulePatriciaTree(Config:Parameters.PATRICIA_TREE_CONFIG)(Node:Parameters.POLYMORPHIC_NODE)(Relation:Parameters.POLYMORPHIC_GROUP)(Value:Parameters.POLYMORPHIC_VALUEwithtype('a,'b)relation=('a,'b)Relation.tandtype'anode='aNode.t)=structtype'anode='aNode.ttype'avalue='aValue.ttype('a,'b)relation=('a,'b)Relation.tletcopyx=x(** {2 Existential wrappers for the return type of find operations} *)type'afind_result=FindResult:{representative:'bnode;relation:('a,'b)relation;}->'afind_resulttype'aparent=|Child:{rank:int;parent:'bnode;relation:('a,'b)relation;}->'aparent|Root:{rank:int;value:'aValue.toption}->'aparentmoduleReprMap=PatriciaTree.MakeHeterogeneousMap(Node)(structtype('a,'b)t='aparentend)(** Map [_ ReprMap.t] mapping ['a Node.t] to ['a parent] *)(** Union-find structure
Values absent from the map implicitly point to themselves.
However, non-trivial representatives MUST be present in the map for the
{!join}.*)typet={mutableparents:unitReprMap.t;(** map: ['a Node.t --> ('b Node.t * ('a, 'b) relation)],
mapping elements to representatives.
representatives appear in this map's domain. *)}let(**)=Relation.composelet(~~)=Relation.inverseletempty={parents=ReprMap.empty;}letmake_=empty(** {2 Find operation} *)(** Find in CPS style to avoid stack overflows *)letrecfind:typea.t->anode->(afind_result->'b)->'b=funufxk->matchReprMap.findxuf.parentswith|Childc->findufc.parent(fun(FindResultp)->letrelation=p.relation**c.relationin(* path compression *)ifConfig.path_compression=`LazythenbeginmatchNode.polyeqp.representativec.parentwith|Eq->()|Diff->uf.parents<-ReprMap.addx(Child{cwithparent=p.representative;relation})uf.parentsend;k(FindResult{representative=p.representative;relation}))|Root_|exceptionNot_found->k(FindResult{representative=x;relation=Relation.identity})letfindufx=findufxFun.id(** Variant of {!find} that also returns the value and the rank of the representative *)type('a,'b)find_all_record={representative:'bnode;relation:('a,'b)relation;value:'bvalueoption;rank:int}type'afind_all=FindAll:('a,'b)find_all_record->'afind_all[@@unboxed]letdefault_rank=0letrecfind_all:typea.t->anode->(afind_all->'b)->'b=funufxk->matchReprMap.findxuf.parentswith|Childc->find_allufc.parent(fun(FindAllp)->letrelation=p.relation**c.relationin(* path compression *)ifConfig.path_compression=`LazythenbeginmatchNode.polyeqp.representativec.parentwith|Eq->()|Diff->uf.parents<-ReprMap.addx(Child{cwithparent=p.representative;relation})uf.parentsend;k(FindAll{pwithrelation}))|Rootr->k(FindAll{representative=x;relation=Relation.identity;rank=r.rank;value=r.value})|exceptionNot_found->k(FindAll{representative=x;relation=Relation.identity;rank=default_rank;value=None})letfind_allufx=find_allufxFun.idletget_valueufx=letFindAllx=find_allufxinmatchx.valuewith|None->None|Somey->Some(Value.applyx.representativey~~(x.relation))(** {2 Printers} *)letprettyfmtuf=ifReprMap.is_emptyuf.parentsthenFormat.fprintffmt"Empty"elseFormat.fprintffmt"@[%a@]"(ReprMap.pretty~pp_sep:(funfmt()->Format.fprintffmt",@ "){f=funfmtnp->matchpwith|Rootr->Format.fprintffmt"%a Root (%a, %d)"Node.prettyn(Utils.Functions.pp_option(Value.prettyn))r.valuer.rank|Childc->Format.fprintffmt"%a -(%a)-> %a"Node.prettynRelation.prettyc.relationNode.prettyc.parent})uf.parents(** {2 Misc functions} *)letcheck_relatedufab=letFindResulta=findufainletFindResultb=findufbinmatchNode.polyeqa.representativeb.representativewith|Eq->Some(~~(b.relation)**a.relation)|Diff->Noneletset_value~intersectufxv=letFindAlla=find_allufxinletv=Value.applyxva.relationinmatcha.valuewith|None->{parents=ReprMap.adda.representative(Root{rank=a.rank;value=Somev})uf.parents}|Somev'->letv=ifintersectthenValue.meeta.representativev'velsevinifValue.equala.representativev'vthenufelse(* keep physical equality when possible *){parents=ReprMap.adda.representative(Root{rank=a.rank;value=Somev})uf.parents}(** {2 Union operation} *)(** Returns the new rank, and a boolean signifying wether the rank has changed *)letcombine_rankssmalllarge=ifsmall=largethenlarge+1,trueelselarge,false(** Helper for {!add_relation}, performs a directed union, making [child] point to [parent].
Assumes [child] and [parent] are representatives *)letmk_childufchildparentrelation(rank,rank_changed)=letparents=ReprMap.addchild.representative(Child{parent=parent.representative;relation=relation;rank=child.rank})uf.parentsin(* We only need to write the parent if the value or the rank has changed *)letvalue,changed=matchchild.value,parent.valuewith|None,parent_value->parent_value,false|Somev,None->Value.applychild.representativevrelation|>Option.some,true|Somev,Somev'->letv=Value.applychild.representativevrelation|>Value.meetparent.representativev'inSomev,not(Value.equalparent.representativevv')inifchanged||rank_changedthenOk{parents=ReprMap.addparent.representative(Root{value;rank})parents}elseOk{parents}letadd_relation:typeab.t->aNode.t->bNode.t->(a,b)Relation.t->(t,(a,b)Relation.t)result=funtabrel->letFindAlla=find_alltainletFindAllb=find_alltbinmatchNode.polyeqa.representativeb.representativewith|Eq->(* Both elements point to the same representative *)letold_rel=~~(b.relation)**a.relationinifRelation.equalrelold_relthenOktelseErrorold_rel|Diff->if(a.rank<=b.rank)thenmk_childtab(b.relation**rel**~~(a.relation))(combine_ranksa.rankb.rank)elsemk_childtba(a.relation**~~rel**~~(b.relation))(combine_ranksb.ranka.rank)(** {1 Lattice operations} *)(****************************************************************************)typewrapped_relation=Rel:'anode*('a,'b)relation*'bnode->wrapped_relationletmeetab=ReprMap.fold_on_nonequal_union{f=funx_vb((res,errs)asacc)->matchvbwith|Some(Child{parent;relation;_})->beginmatchadd_relationresxparentrelationwith|Okres->(res,errs)|Error_->letvalue=get_valuebxinletres=matchvaluewith|None->res|Somev->set_value~intersect:trueresxvinres,Rel(x,relation,parent)::errsend|Some(Root{value=Somev;_})->set_value~intersect:trueresxv,errs|_->acc}a.parentsb.parents(a,[])letinclab=ReprMap.reflexive_subset_domain_for_all2{f=funxvb_->matchvbwith|Child{parent;relation;_}->beginmatchcheck_relatedaxparentwith|None->false|Somer->Relation.equalrrelationend|Root{value=Somev;_}->beginmatchget_valueaxwith|None->false|Somev'->Value.inclxv'vend|_->true}b.parentsa.parents(** {2 Join operation} *)(**************************************************************)(** {3 Join that calls union (from Section 3)} *)(********************************************************)moduleTriple=structtype_t=Triple:'anode*'bnode*('a,'b)relation->('a*'b)tletpolyeq:typeab.at->bt->(a,b)PatriciaTree.cmp=fun(Triple(l1,r1,rel))(Triple(l2,r2,rel'))->matchNode.polyeql1l2with|Diff->Diff|Eq->matchNode.polyeqr1r2withEq->ifRelation.equalrelrel'thenEqelseDiff|Diff->Difflethash:typea.at->int=fun(Triple(l,r,rel))->Utils.Functions.hash_pair(Node.to_intl)(Node.to_intr)|>Utils.Functions.hash_pair(Relation.hashrel)endletjoin_valuesrpapb=matchpa.value,pb.valuewith|None,None|Some_,None|None,Some_->None|Someva,Somevb->Value.joinr(Value.applypa.representativeva~~(pa.relation))(Value.applypb.representativevb~~(pb.relation))letrank=function|Root{rank;_}->rank|Child{rank;_}->ranktype_memoized_item=Item:{representative:'cnode;left_rel:('a,'c)relation;(* we need the left_rel to map new items to this candidate *)rank:int;incr_rank:bool;}->('a*'b)memoized_itemmoduleH=Utils.HetHashtbl.Make(Triple)(structtype('a,_)t='amemoized_itemend)letmemoized_get:typea.unitH.t->aTriple.t->int->amemoized_itemoption=funnew_classes(Triple.Triple(l,r,rel)astriple)rank->matchNode.polyeqlrwith|Diff->H.find_optnew_classestriple|Eq->ifRelation.equalrelRelation.identitythenSome(Item{representative=l;left_rel=Relation.identity;rank;incr_rank=false})elseH.find_optnew_classestripleletjoinab=(* map : repr_a -> repr_b -> list of repr_of_intersection for memoization *)letnew_classes=H.create10in(* First loop: find the representative of the new class *)letnew_classes=ReprMap.fold_on_nonequal_inter{f=fun(typea)(x:anode)vavbnew_classes->letFindAllpa=find_allaxinletFindAllpb=find_allbxinletrepr_rank=minpa.rankpb.rankinletrank=min(rankva)(rankvb)inlettriple=Triple.Triple(pa.representative,pb.representative,pb.relation**~~(pa.relation))inbeginmatchmemoized_getnew_classestriplerepr_rankwith|Some(Itemcandidate)->ifcandidate.rank<rankthenH.replacenew_classestriple(Item{representative=x;left_rel=~~(pa.relation);rank;incr_rank=false;})elseifcandidate.rank=rank&¬candidate.incr_rankthenH.replacenew_classestriple(Item{candidatewithincr_rank=true})|None->H.addnew_classestriple(Item{representative=x;left_rel=~~(pa.relation);rank;incr_rank=false;})end;new_classes}a.parentsb.parentsnew_classesin(* Second loop: compute the intersection *)letparents=ReprMap.idempotent_inter_filter{f=fun(typea)(x:anode)__->letFindAllpa=find_allaxinletFindAllpb=find_allbxinletrepr_rank=minpa.rankpb.rankinlettriple=Triple.Triple(pa.representative,pb.representative,pb.relation**~~(pa.relation))inletItemi=memoized_getnew_classestriplerepr_rank|>Option.getinmatchNode.polyeqi.representativexwith|Eq->Some(Root{rank=i.rank+Bool.to_inti.incr_rank;value=join_valuesxpapb})|Diff->Some(Child{parent=i.representative;relation=i.left_rel**pa.relation;rank=minpa.rankpb.rank})}a.parentsb.parentsin{parents}(** {1 Debug operations} *)(****************************************************************************)letreccount_rank:typea._->_->anode->_=funtni->matchReprMap.finditwith|Root{rank;_}->(Format.asprintf"%a"Node.prettyi,rank,n)|Childc->count_rankt(n+1)(c.parent)letcheck_invariantst=leterrors=ReprMap.fold{f=funkverrors->matchvwith|Root_->errors|Child_->letroot,rank,r=count_rankt.parents0kinifrank<rthen(Format.asprintf"- path from %a to %s has length %d, but stored rank is %d"Node.prettykrootrrank::errors)elseerrors}t.parents[]iniferrors=[]thenNoneelseletstr=errors|>List.rev|>String.concat"\n"inSome("Invalid ranks:\n"^str)endmodulePersistentArrayBase(PersistentArray:PersistentArray.S)(Config:Parameters.PERSISTENT_ARRAY_CONFIG)(Node:Parameters.POLYMORPHIC_NODE)(Relation:Parameters.POLYMORPHIC_GROUP)(Value:Parameters.POLYMORPHIC_VALUEwithtype'anode='aNode.tandtype('a,'b)relation=('a,'b)Relation.t)=structtype'anode='aNode.ttype'avalue='aValue.ttype('a,'b)relation=('a,'b)Relation.tletcopyx=xlet(**)=Relation.composelet(~~)=Relation.inverse(** The type of values store in our array. In classical union find these are
just parent pointer, with representatives pointing to themselves. This version
is a bit more complex:
- We store values associated to each class at the {!Root}.
- We store relation to the parent in {!Child} items.
One other challenge is that array are indexed by integer, but we use {!Node.t}.
One can get an integer from an [Node.t] by {!Node.to_int}, but there is no
reverse mapping [int -> Node.t]. To that end:
- we store self elements in {!Root} and {!Child}.
- we have an {!Uninitialized} constructor for elements we have never seen. *)type'aptr=|Uninitialized:'aptr(** uninitialized element. This will always be a element that never appeared in a union,
and has no value. *)|Root:{self:'aNode.t;value:'aValue.toption;(** value attached to the class *)}->'aptr|Child:{self:'aNode.t;relation:('a,'b)Relation.t;parent:'bNode.t;}->'aptr(** [\exists 'a, 'a ptr] type. *)typewrapped=Wrap:'aptr->wrapped[@@unboxed](** Unfortunately, there is no easy type safe way of remembering the ['a] type
of each cell. *)typet={mutableparents:wrappedPersistentArray.t;ranks:intPersistentArray.t;}(** {1 Array access} *)(****************************************************************************)(** For extendable arrays, all access must check for size, with set
increasing the size if needed *)letget_fixedarri~default:_=PersistentArray.getarriletget_extendablearri~default=ifi>=PersistentArray.sizearrthendefaultelsePersistentArray.getarriletset_fixedarri~default:_=PersistentArray.setarriletset_extendablearri~defaultv=letn=PersistentArray.sizearrinifi>=nthenPersistentArray.extendarr(maxn(i+1-n))default;PersistentArray.setarrivletget=ifConfig.extendablethenget_extendableelseget_fixedletset=ifConfig.extendablethenset_extendableelseset_fixed(** {1 Union-find operations} *)(****************************************************************************)letdefault_rank=0letuninitialized=WrapUninitializedletmaken={parents=PersistentArray.makenuninitialized;ranks=PersistentArray.makendefault_rank;}type'afind_result=FindResult:{representative:'bnode;relation:('a,'b)relation;}->'afind_result(** CPS version of find, to avoid stack-overflows when performing deep-unions *)letrecfind:typea.t->aNode.t->(afind_result->'b)->'b=funufnodek->leti=Node.to_intnodeinletWrap(parent)=get~default:uninitializeduf.parentsiinmatchparentwith|Uninitialized->k(FindResult{representative=node;relation=Relation.identity})|Rootr->(* This match is only needed for type-checking, since node and r.self
should always be equal. *)beginmatchNode.polyeqnoder.selfwith|Eq->k(FindResult{representative=r.self;relation=Relation.identity})|Diff->failwith"Multiple nodes mapped to same to_int value"end|Childc->(* This match is only needed for type-checking, since node and r.self
should always be equal. *)matchNode.polyeqnodec.selfwith|Diff->failwith"Multiple nodes mapped to same to_int value"|Eq->findufc.parent(fun(FindResultres)->letrel=res.relation**c.relationin(* path compression *)ifConfig.path_compression=`Lazythenuf.parents<-set~default:uninitializeduf.parentsi(Wrap(Child{self=node;relation=rel;parent=res.representative}));k(FindResult{reswithrelation=rel}))letfindtn=findtnFun.id(** Variant of {!find} that also returns the value *)type'afind_all=FindAll:{representative:'bnode;relation:('a,'b)relation;value:'bvalueoption;}->'afind_all(** CPS version of find, to avoid stack-overflows when performing deep-unions *)letrecfind_all:typea.t->aNode.t->(afind_all->'b)->'b=funufnodek->leti=Node.to_intnodeinletWrap(parent)=get~default:uninitializeduf.parentsiinmatchparentwith|Uninitialized->FindAll{representative=node;relation=Relation.identity;value=None;}|>k|Rootr->(* This match is only needed for type-checking, since node and r.self
should always be equal. *)beginmatchNode.polyeqnoder.selfwith|Eq->FindAll{representative=r.self;relation=Relation.identity;value=r.value;}|>k|Diff->failwith"Multiple nodes mapped to same to_int value"end|Childc->(* This match is only needed for type-checking, since node and r.self
should always be equal. *)matchNode.polyeqnodec.selfwith|Diff->failwith"Multiple nodes mapped to same to_int value"|Eq->find_allufc.parent(fun(FindAllres)->letrel=res.relation**c.relationin(* path compression *)ifConfig.path_compression=`Lazythenuf.parents<-set~default:uninitializeduf.parentsi(Wrap(Child{self=node;relation=rel;parent=res.representative}));FindAll{reswithrelation=rel}|>k)letfind_alltn=find_alltnFun.idletcheck_relatedufab=letFindResulta=findufainletFindResultb=findufbinmatchNode.polyeqa.representativeb.representativewith|Eq->Some(~~(b.relation)**a.relation)|Diff->Noneletget_valueufa=letFindAlla=find_allufainmatcha.valuewith|None->None|Somev->Some(Value.applya.representativev~~(a.relation))letset_value~intersectufxv=letFindAlla=find_allufxinletv=Value.applyxva.relationinmatcha.valuewith|None->{ufwithparents=set~default:uninitializeduf.parents(Node.to_inta.representative)(Wrap(Root{self=a.representative;value=Somev}))}|Somev'->letv=ifintersectthenValue.meeta.representativevv'elsevinifValue.equala.representativevv'thenufelse{ufwithparents=set~default:uninitializeduf.parents(Node.to_inta.representative)(Wrap(Root{self=a.representative;value=Somev}))}(** {2 union operation} *)(*********************************************************************)(** We return an option indicating wether or not the value has changed *)letcombine_rankssmalllarge=ifsmall=largethenSome(large+1)elseNone(** Helper for {!add_relation}, performs a directed union, making [child] point to [parent].
Assumes [child] and [parent] are representatives *)letmk_childufchildparentrelationchild_valueparent_valuerank=letchild_ptr=Child{self=child;parent=parent;relation=relation}inletparents=set~default:uninitializeduf.parents(Node.to_intchild)(Wrapchild_ptr)in(* We could do the union and then call [add_value uf child child_value],
but inlining the call here removes a [PersistentArray.set]. *)letvalue=matchchild_value,parent_valuewith|None,parent_value->parent_value|Somev,None->Value.applychildvrelation|>Option.some|Somev,Somev'->letv=Value.applychildvrelation|>Value.meetparentv'inifValue.equalparentvv'thenparent_valueelseSomevinletparent_id=Node.to_intparentinletparents=ifvalue==parent_value&&get~default:uninitializeduf.parentsparent_id<>uninitializedthenparentselseset~default:uninitializedparentsparent_id(Wrap(Root{self=parent;value;}))inOk{parents;ranks=matchrankwith|None->uf.ranks|Somerank->set~default:default_rankuf.ranksparent_idrank;}letadd_relation:typeab.t->aNode.t->bNode.t->(a,b)Relation.t->(t,(a,b)Relation.t)result=funtabrel->letFindAlla=find_alltainletFindAllb=find_alltbinmatchNode.polyeqa.representativeb.representativewith|Eq->(* Both elements point to the same representative *)letold_rel=~~(b.relation)**a.relationinifRelation.equalrelold_relthenOktelseErrorold_rel|Diff->letrank_a=get~default:default_rankt.ranks(Node.to_inta.representative)inletrank_b=get~default:default_rankt.ranks(Node.to_intb.representative)inif(rank_a<rank_b)thenmk_childta.representativeb.representative(b.relation**rel**~~(a.relation))a.valueb.value(combine_ranksrank_arank_b)elsemk_childtb.representativea.representative(a.relation**~~rel**~~(b.relation))b.valuea.value(combine_ranksrank_brank_a)(** {1 Lattice operations} *)(****************************************************************************)(** {2 Meet} *)(*********************************************************************)typeinterim=|Check_Value:'anode*'avalue->interim|Check_Rel:'anode*'bnode*('a,'b)relation*'avalueoption->interimtypewrapped_relation=Rel:'anode*('a,'b)relation*'bnode->wrapped_relationletmeetuv=PersistentArray.diff_keyv.parentsu.parents|>fst|>Utils.Functions.list_of_hashtbl_keys|>List.filter_map(funx->matchget~default:uninitializedv.parentsxwith|Wrap(Root{self;value=Somevalue})->Some(Check_Value(self,value))|Wrap(Child{self;parent;relation})->Some(Check_Rel(self,parent,relation,get_valuevself))|Wrap(Root_)|WrapUninitialized->None)|>List.fold_left(fun(res,errs)elt->matcheltwith|Check_Value(x,v)->set_value~intersect:trueresxv,errs|Check_Rel(x,px,rel,v)->matchadd_relationresxpxrelwith|Okr->r,errs|Error_->(* We can't recover this relation, but we can keep its value *)letres=matchvwith|None->res|Somev->set_value~intersect:trueresxvinres,Rel(x,rel,px)::errs)(u,[])(** {2 Incl} *)(*********************************************************************)letincluv=PersistentArray.diff_keyv.parentsu.parents|>fst|>Utils.Functions.list_of_hashtbl_keys|>List.filter_map(funx->matchget~default:uninitializedv.parentsxwith|Wrap(Root{self;value=Somevalue})->Some(Check_Value(self,value))|Wrap(Child{self;parent;relation})->Some(Check_Rel(self,parent,relation,None))|Wrap(Root_)|Wrap(Uninitialized)->None)|>List.for_all(function|Check_Rel(x,px,rel,_)->beginmatchcheck_relateduxpxwith|None->false|Somerel'->Relation.equalrelrel'end|Check_Value(x,vv)->matchget_valueuxwith|None->false|Somevu->Value.inclxvuvv)(** {2 Join} *)(*********************************************************************)moduleTriple=structtype_t=Triple:'anode*'bnode*('a,'b)relation->('a*'b)tletpolyeq:typeab.at->bt->(a,b)PT.cmp=fun(Triple(l1,r1,rel))(Triple(l2,r2,rel'))->matchNode.polyeql1l2with|Diff->Diff|Eq->matchNode.polyeqr1r2withEq->ifRelation.equalrelrel'thenEqelseDiff|Diff->Difflethash:typea.at->int=fun(Triple(l,r,rel))->Utils.Functions.hash_pair(Node.to_intl)(Node.to_intr)|>Utils.Functions.hash_pair(Relation.hashrel)endletjoin_valuesr(FindAllpa)(FindAllpb)=matchpa.value,pb.valuewith|None,None|Some_,None|None,Some_->None|Someva,Somevb->Value.joinr(Value.applypa.representativeva~~(pa.relation))(Value.applypb.representativevb~~(pb.relation))(** {3 Join by parent edits (Appendix A)} *)(********************************************************)(** For this join, we need the ranks of both the current element and its representatives *)(** Unlike the paper, we can't simply run find on both sides, since do not know
the node, only its integer id. Fortunately, the parents at that position should
contain the node, if they are not uninitialized.
They will not both be uninitialized since they appear in the diff, so they must be set,
and we never set anything to uninitialized.
Thus we have two special types: [find_left] which is returned by the find on
the first argument, and [find_right] which is returned by the find on the
second argument *)typefind_left=|FL_NotFoundofint|FL_Found:{id:int;node:'anode;left:'afind_all;rank:int;parent_rank:int}->find_lefttypefind_right=FindRight:{id:int;node:'anode;left:'afind_all;right:'afind_all;rank:int;parent_rank:int;}->find_rightletfind_leftaid=(* id comes from the diff, so we can use get directly, no need to check in bounds *)matchPersistentArray.geta.parentsidwith|WrapUninitialized->FL_NotFoundid|Wrap(Rootx)->(* the rank array can sometimes be smaller than parents, so it requires checks *)letrank=get~default:default_ranka.ranksidinletleft=FindAll{representative=x.self;value=x.value;relation=Relation.identity}inFL_Found{id;node=x.self;left;rank;parent_rank=rank;}|Wrap(Childx)->let(FindAll{representative;_}asleft)=find_allax.selfinletrank=get~default:default_ranka.ranksidinFL_Found{id;node=x.self;left;rank;parent_rank=get~default:default_ranka.ranks(Node.to_intrepresentative);}letfind_rightb=function|FL_Found{id;node;rank;left;parent_rank;}->let(FindAll{representative;_}asright)=find_allbnodeinFindRight{id;node;left;right;rank=minrank(get~default:default_rankb.ranksid);parent_rank=minparent_rank(get~default:default_rankb.ranks(Node.to_intrepresentative));}|FL_NotFoundid->matchPersistentArray.getb.parentsidwith|WrapUninitialized->failwith"Unreachable"|Wrap(Rootx)->letrepr=FindAll{representative=x.self;relation=Relation.identity;value=None}inFindRight{id;node=x.self;rank=default_rank;left=repr;right=repr;parent_rank=default_rank;}|Wrap(Childx)->letright=find_allbx.selfinFindRight{id;node=x.self;rank=default_rank;left=FindAll{representative=x.self;relation=Relation.identity;value=None};right;parent_rank=default_rank;}type_memoized_item=Item:{representative:'cnode;rank:int;incr_rank:bool;left_rel:('a,'c)relation;}->('a*'b)memoized_itemmoduleH=Utils.HetHashtbl.Make(Triple)(structtype('a,_)t='amemoized_itemend)letmemoized_get:typea.unitH.t->aTriple.t->int->amemoized_itemoption=funnew_classes(Triple.Triple(l,r,rel)astriple)rank->matchNode.polyeqlrwith|Diff->H.find_optnew_classestriple|Eq->ifRelation.equalrelRelation.identitythenSome(Item{representative=l;left_rel=Relation.identity;rank;incr_rank=false})elseH.find_optnew_classestriple(* First loop: find the representative of the new class *)letfind_representativesnew_classes(FindRight{node;left=FindAllleft;right=FindAllright;rank;parent_rank;_})=letcross_rel=right.relation**~~(left.relation)inlettriple=Triple.Triple(left.representative,right.representative,cross_rel)in(* lookup previous candidate for this pair, if no candidates and same repr, initialize with that repr *)matchmemoized_getnew_classestripleparent_rankwith|Some(Itemcandidate)->ifcandidate.rank<rankthenH.replacenew_classestriple(Item{representative=node;left_rel=~~(left.relation);rank;incr_rank=false;})elseifcandidate.rank=rank&¬candidate.incr_rankthenH.replacenew_classestriple(Item{candidatewithincr_rank=true})|None->H.addnew_classestriple(Item{representative=node;left_rel=~~(left.relation);rank;incr_rank=false;})(* second loop body, update the arrays with the selected representatives *)letset_representativesnew_classes(ranks,parents)(FindRight{id;node;left=(FindAllleftasl);right=(FindAllrightasr);rank;parent_rank;})=letcross_rel=right.relation**~~(left.relation)inlettriple=Triple.Triple(left.representative,right.representative,cross_rel)inletItemcandidate=memoized_getnew_classestripleparent_rank|>Option.getinmatchNode.polyeqcandidate.representativenodewith|Diff->(set~default:default_rankranksidrank,PersistentArray.setparentsid(Wrap(Child{self=node;parent=candidate.representative;relation=candidate.left_rel**left.relation})))|Eq->set~default:default_rankranksid(candidate.rank+Bool.to_intcandidate.incr_rank),PersistentArray.setparentsid(Wrap(Root{self=node;value=join_valuescandidate.representativelr}))letjoinab=letdiff,ancestor=PersistentArray.diff_keya.parentsb.parentsin(* reroots PersistentArray at a *)letparents=matchancestorwithSomea->a|None->b.parentsinletdiff_list=diff|>Utils.Functions.list_of_hashtbl_keys|>List.map(find_lefta)(* using rev-map for tail recursion, since the order does not matter*)|>List.map(find_rightb)in(* reroots PersistentArray at b *)letnew_classes=H.create100inList.iter(find_representativesnew_classes)diff_list;letranks,parents=List.fold_left(set_representativesnew_classes)(b.ranks,parents)diff_listin{ranks;parents}(** {1 Debug operations} *)(****************************************************************************)letprettyfmtt=PersistentArray.pretty(funfmtparent->matchparentwith|WrapUninitialized->Format.pp_print_stringfmt"U"|Wrap(Rootr)->Format.fprintffmt"R(%a,%a)"Node.prettyr.self(Utils.Functions.pp_option(Value.prettyr.self))r.value|Wrap(Childc)->Format.fprintffmt"C(%a->%a,%a)"Node.prettyc.selfNode.prettyc.parentRelation.prettyc.relation)fmtt.parentsletreccount_ranktni=matchPersistentArray.gettiwith|Wrap(Root_)|WrapUninitialized->(n,i)|Wrap(Childc)->count_rankt(n+1)(Node.to_intc.parent)(** check that the class sizes are correct. *)letcheck_invariantst=leterrors=Utils.Functions.range_fold(funierrors->let(rank,parent)=count_rankt.parents0iinletstored_rank=PersistentArray.gett.ranksparentinif(rank<=stored_rank)thenerrorselse(Format.asprintf"- path from %d to %d has length %d, but stored rank is %d"iparentrankstored_rank)::errors)(PersistentArray.sizet.parents-1)[]iniferrors=[]thenNoneelseletstr=errors|>List.rev|>String.concat"\n"inSome("Invalid ranks:\n"^str)endmodulePersistentArrayNCA=PersistentArrayBase(PersistentArray.Versioned)modulePersistentArray=PersistentArrayBase(PersistentArray)