123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335(**************************************************************************)(* This file is part of the Codex semantics library. *)(* *)(* Copyright (C) 2013-2025 *)(* 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 2.1 *)(* for more details (enclosed in the file LICENSE). *)(* *)(**************************************************************************)(** Each component of the graph is either an individual node of the graph
(without) self loop, or a strongly connected component where a node is
designed as the head of the component and the remaining nodes are given
by a list of components topologically ordered. *)type'ncomponent=|Componentof'n*'npartition(** A strongly connected component, described by its head node and the
remaining sub-components topologically ordered *)|Nodeof'n(** A single node without self loop *)(** A list of strongly connected components, sorted topologically *)and'npartition='ncomponentlistletfold_headsfaccl=letrecpartitionaccl=List.fold_leftcomponentacclandcomponentacc=function|Node_->acc|Component(h,l)->partition(facch)linpartitionacclletflattenwto=letrecfacc=function|[]->acc|Nodev::l->f(v::acc)l|Component(v,w)::l->f(f(v::acc)w)linList.rev(f[]wto)(* Bourdoncle's WTO algorithm builds on Tarjan's SCC algorithm. In Tarjan:
- We visit every node once, starting from root, by following the
successors; this creates a spanning tree of the graph. SCCs are
subtrees of this spanning tree, whose root is the head of the SCC
(although in non-natural SCCs, it is possible to enter into a SCC
without going through the head).
- This spanning tree is obtained using DFS. What DFS guarantees is
that there is no path from a child c of a node n to other
children of n, provided that there is no path from c to an
ancestor of n. Thus when we visit other children of n, we know
that they are no path to them from the descendants of c.
- We assign consecutive numbers to each node in the order in which
they have been visited. As the iteration is depth-first search,
this gives a depth-first numbering (DFN).
- Each time we visit a node n, we push it on a stack. After the
visit, n is popped, unless a path exists from n to an element
earlier on the stack. So the stack contains elements currently
visited or that belongs to a non-trivial scc. Moreover, they
are in topological order.
About the proof of Tarjan:
http://ls2-www.cs.uni-dortmund.de/~wegener/papers/connected.pdf
*)moduleMake(N:sigtypet(* = int *)valequal:t->t->boolvalhash:t->intvalpretty:Format.formatter->t->unit(* val succ: t -> t list *)end)=structletrecequal_component(x:N.tcomponent)(y:N.tcomponent)=matchx,ywith|Nodex,Nodey->N.equalxy|Component(x,cx),Component(y,cy)->N.equalxy&&equal_partitioncxcy|_->falseandequal_partitionxy=(tryList.for_all2equal_componentxywithInvalid_argument_->false)letrecpretty_partitionfmtpart=List.iter(funx->Format.fprintffmt"@ %a"pretty_componentx)partandpretty_componentfmt:N.tcomponent->unit=function|Noden->N.prettyfmtn|Component(head,part)->Format.fprintffmt"@[<hov 1>(%a%a)@]"N.prettyheadpretty_partitionpartmoduleDFN=Hashtbl.Make(N);;moduleBourdoncle_Original=structtypestate={dfn:intDFN.t;(* Mapping from nodes to its dfn, depth-first
numbering. Note that we replaced the DFN=0
test by presence in the Hashtable. *)mutablenum:int;(* Number of visited nodes. *)succs:N.t->(N.tlist);(* Successors transition. *)stack:N.tStack.t}(* Visit [vertex], and all the vertices reachable from [vertex]
which have not been explored yet (this is a depth-first search).
Also gives [partition], which is the partition built so far
Returns a pair (lowlink,partition) where
- [lowlink] is the deepest node in the stack, that is the
successor of an explored node (i.e. a node which is below than
[vertex] in the spanning tree built by the DFS);
- [partition] is returned completed.
Except for the call to component, and changing the DFN of vertex
to max_int, this function exactly performs Tarjan's SCC
algorithm. *)letrecvisitstatevertexpartition=Stack.pushvertexstate.stack;letn=state.num+1instate.num<-n;DFN.replacestate.dfnvertexn;letsuccs=state.succsvertexinlet(head,loop,partition)=List.fold_left(fun(head,loop,partition)succ->let(min,partition)=(* If already visited. *)ifDFN.memstate.dfnsucc(* Is another branch, which can have been visited before or after [vertex]. *)then(succ,partition)elsevisitstatesuccpartitionin(* OPTIM: On doit pouvoir sauvegarder le dfn de head et min pour gagner des lookups. *)assert(DFN.memstate.dfnmin);assert(DFN.memstate.dfnhead);(* If an element below in the spanning tree links to deepest
in the stack than [vertex]. *)if(DFN.findstate.dfnmin)<=(DFN.findstate.dfnhead)then(min,true,partition)else(head,loop,partition))(vertex,false,partition)succsin(* If no element below in the spanning tree link to deepest than [vertex]. *)ifN.equalheadvertexthenbegin(* Makes edges to [vertex] invisible in the second visit. *)DFN.replacestate.dfnvertexmax_int;letelement=Stack.popstate.stackin(* If an element below in the spanning tree links to [vertex],
which is the head of the SCC. *)ifloopthenbeginletrecloopelement=ifnot@@N.equalelementvertexthenbegin(* Remove from DFN for a second visit. *)DFN.removestate.dfnelement;loop(Stack.popstate.stack)endelse()inloopelement;(head,componentstatevertex::partition)endelse(head,(Nodevertex)::partition)end(* [vertex] is part of a strongly connected component, and is not
the root. Do not update partition; the vertex will be added in
the next call to [component]. *)else(head,partition)(* We found a SCC; revisit it with the [edge] to its head made
invisible. *)andcomponentstatevertex=letsuccs=state.succsvertexinletpartition=List.fold_left(funpartitionsucc->(* Select only the branches of the spanning tree which are in a SCC with [vertex];
and which have been removed from the DFN by [visit]. *)ifDFN.memstate.dfnsuccthenpartitionelselet(_,part)=visitstatesuccpartitioninpart)[]succsinComponent(vertex,partition);;let_partition~init~succs=letstate={dfn=DFN.create17;num=0;succs;stack=Stack.create()}insnd@@visitstateinit[];;endmoduleWith_Preference=struct(* This implements Francois Bobot's improvements (preference on
the choice of a head, maybe others, but possibly the WTO
construction is slower. *)typelevel=int(** Status of a visited vertex during the algorithm. *)typestatus=|Invisible(** The vertex have already been added into the partition and
is hidden until the end of the search. *)|Parentoflevel(** The vertex have been visited and given a [level]. For
the algorithm, this implies that there is a path
between this vertex and the current vertex. *)(** Result of one [visit] *)typeloop=|NoLoop(** The vertex is not in a loop *)|LoopofN.t*level(** The vertex is inside at least one loop, and
level is the smallest level of all these loops *)letmin_loopxy=matchx,ywith|NoLoop,z|z,NoLoop->z|Loop(_,xi),Loop(_,yi)->ifxi<=yithenxelseytypestate={dfn:statusDFN.t;(* Mapping from nodes to its dfn, depth-first
numbering. Note that we replaced the DFN=0
test by presence in the Hashtable. *)mutablenum:level;(* Number of visited nodes. *)succs:N.t->(N.tlist);(* Successors transition. *)stack:N.tStack.t}(** Visit [vertex], and all the vertices reachable from [vertex]
which have not been explored yet (this is a depth-first search).
Also gives [partition], which is the partition built so far
Returns a pair (loop,partition) where
- [loop] tells whether we are in a loop or not and gives the vertex of
this loop with the lowest level. This vertex is also the deepest in the
stack and the neareast vertex from the root that is below [vertex] in
the spanning tree built by the DFS);
- [partition] is returned completed. *)letrecvisit~prefstatevertexpartition=matchDFN.findstate.dfnvertexwith(* The vertex is already in the partition *)|Invisible->NoLoop,partition(* skip it *)(* The vertex have been visited but is not yet in the partition *)|Parenti->Loop(vertex,i),partition(* we are in a loop *)(* The vertex have not been visited yet *)|exceptionNot_found->(* Put the current vertex into the stack *)Stack.pushvertexstate.stack;(* Number it and mark it as visited *)letn=state.num+1instate.num<-n;DFN.replacestate.dfnvertex(Parentn);(* Visit all its successors *)letsuccs=state.succsvertexinlet(loop,partition)=List.fold_left(fun(loop,partition)succ->let(loop',partition)=visit~prefstatesuccpartitioninletloop=min_looplooploop'in(loop,partition))(NoLoop,partition)succsinmatchloopwith(* We are not in a loop. Add the vertex to the partition *)|NoLoop->let_=Stack.popstate.stackinDFN.replacestate.dfnvertexInvisible;(NoLoop,Node(vertex)::partition)(* We are in a loop and the current vertex is the head of this loop *)|Loop(head,_)whenN.equalheadvertex->(* Unmark all vertices in the loop, and, if pref is given, try to
return a better head *)letrecreset_SCCbest_head=(* pop until vertex *)letelement=Stack.popstate.stackinDFN.removestate.dfnelement;ifnot(N.equalelementvertex)thenbeginletbest_head=matchprefwith(* the strict is important because we are conservative *)|Somecmpwhencmpbest_headelement<0->element|_->best_headinreset_SCCbest_headendelsebest_headinletbest_head=reset_SCCvertexinletvertex,succs=ifN.equalbest_headvertexthenvertex,succselsebest_head,state.succsbest_headin(* Makes [vertex] invisible in the subsequents visits. *)DFN.replacestate.dfnvertexInvisible;(* Restart the component analysis *)letcomponent=List.fold_left(funcomponentsucc->let(loop,component)=visit~prefstatesucccomponentin(* Since we reset the component we should have no loop *)assert(loop=NoLoop);component)[]succsin(NoLoop,Component(vertex,component)::partition)|_->(* [vertex] is part of a strongly connected component but is not the
head. Do not update partition; the vertex will
be added during the second visit of the SCC. *)(loop,partition)typepref=N.t->N.t->intletpartition~pref~init~succs=letstate={dfn=DFN.create17;num=0;succs;stack=Stack.create()}inletloop,component=visit~pref:(Somepref)stateinit[]inassert(loop=NoLoop);componentendincludeWith_Preferenceend