(* * Copyright (c) 2018-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) open! IStd module F = Format module Partition = struct type 'node t = | Empty | Node of {node: 'node; next: 'node t} | Component of {head: 'node; rest: 'node t; next: 'node t} let empty = Empty let add_node node next = Node {node; next} let prepend_node next node = Node {node; next} let add_component head rest next = Component {head; rest; next} let rec fold_nodes partition ~init ~f = match partition with | Empty -> init | Node {node; next} -> let init = f init node in (fold_nodes [@tailcall]) next ~init ~f | Component {head; rest; next} -> let init = f init head in let init = fold_nodes rest ~init ~f in (fold_nodes [@tailcall]) next ~init ~f let rec fold_heads partition ~init ~f = match partition with | Empty -> init | Node {next} -> (fold_heads [@tailcall]) next ~init ~f | Component {head; rest; next} -> let init = f init head in let init = fold_heads rest ~init ~f in (fold_heads [@tailcall]) next ~init ~f let rec expand ~fold_right partition = match partition with | Empty -> Empty | Node {node; next} -> let init = expand ~fold_right next in fold_right node ~init ~f:prepend_node | Component {head; rest; next} -> ( let next = expand ~fold_right next in let init = expand ~fold_right rest in match fold_right head ~init ~f:prepend_node with | Empty | Component _ -> (* [fold_right] is expected to always provide a non-empty sequence. Hence the result of [fold_right ~f:prepend_node] will always start with a Node. *) Logging.(die InternalError) "WeakTopologicalOrder.Partition.expand: the expansion function fold_right should \ not return ~init directly" | Node {node= head; next= rest} -> Component {head; rest; next} ) let rec pp ~prefix ~pp_node fmt = function | Empty -> () | Node {node; next} -> F.fprintf fmt "%s%a" prefix pp_node node ; pp ~prefix:" " ~pp_node fmt next | Component {head; rest; next} -> F.fprintf fmt "%s(%a%a)" prefix pp_node head (pp ~prefix:" " ~pp_node) rest ; pp ~prefix:" " ~pp_node fmt next let pp ~pp_node = pp ~prefix:"" ~pp_node end module type PreProcCfg = sig module Node : sig type t type id val id : t -> id module IdMap : PrettyPrintable.PPMap with type key = id end type t val fold_succs : t -> (Node.t, Node.t, 'accum) Container.fold val start_node : t -> Node.t end module type S = sig module CFG : PreProcCfg val make : CFG.t -> CFG.Node.t Partition.t end module type Make = functor (CFG : PreProcCfg) -> S with module CFG = CFG module Bourdoncle_SCC (CFG : PreProcCfg) = struct module CFG = CFG (** [dfn] contains a DFS pre-order indexing. A node is not in the map if it has never been visited. A node's dfn is +oo if it has been fully visited (head of cross-edges) or we want to hide it for building a subcomponent partition (head of highest back-edges). *) module Dfn = CFG.Node.IdMap (* Unlike Bourdoncle's paper version or OCamlGraph implementation, this implementation handles high DFS-depth graphs, which would stack-overflow otherwise. It still doesn't handle high component nesting, but it is pretty unlikely to happen in real code (means a lot of loop nesting). *) type stack = { node: CFG.Node.t ; node_id: CFG.Node.id ; node_dfn: int ; succs: CFG.Node.t list ; mutable succs_to_visit: CFG.Node.t list ; mutable head: int (** Minimum [dfn] of the nodes accessibles from [node]. *) ; mutable component: CFG.Node.id ARList.t (** Nodes in the current strict-connected component. *) ; mutable building_component: bool ; next: stack option } let make cfg = let num = ref 0 in let dfn = ref Dfn.empty in let stack = ref None in let push_on_stack node = let node_id = CFG.Node.id node in incr num ; let node_dfn = !num in dfn := Dfn.add node_id node_dfn !dfn ; let succs = IContainer.to_rev_list ~fold:(CFG.fold_succs cfg) node in stack := Some { node ; node_id ; node_dfn ; succs ; succs_to_visit= succs ; head= Int.max_value ; component= ARList.empty ; building_component= false ; next= !stack } in let record_head ?add_to_component cur_head = let stack_top = Option.value_exn !stack in stack_top.head <- min stack_top.head cur_head ; Option.iter add_to_component ~f:(fun add -> stack_top.component <- ARList.append add stack_top.component ) in let visit node = let node_id = CFG.Node.id node in match Dfn.find node_id !dfn with | node_dfn -> (* [node_dfn] is going to be either +oo (see [Dfn] for why), in which case [record_head] will have no effect; or be the [dfn] of the head of a back-edge or cross-edge in the current strictly connected component. *) record_head node_dfn | exception Caml.Not_found -> push_on_stack node | exception Not_found_s _ -> push_on_stack node in let rec process_stack partition = match !stack with | None -> () | Some ({succs_to_visit= succ :: succs_to_visit} as stack_top) -> stack_top.succs_to_visit <- succs_to_visit ; visit succ ; (process_stack [@tailcall]) partition | Some {succs_to_visit= []; building_component= true} -> () | Some {succs_to_visit= []; building_component= false; node_id; node_dfn; head; component; next} when head < node_dfn -> (* [node] is in a strictly connected component but is (locally) not its head. *) stack := next ; record_head head ~add_to_component:(ARList.cons node_id component) ; (process_stack [@tailcall]) partition | Some ( { succs_to_visit= [] ; building_component= false ; node ; node_id ; node_dfn ; succs ; head ; component ; next } as stack_top ) -> dfn := Dfn.add node_id Int.max_value !dfn ; if head > node_dfn then (* [node] is not (locally) in a strictly connected component *) partition := Partition.add_node node !partition else ( (* head = node_dfn. [node] is (locally) the head of a strictly connected component. [node] is marked as already visited (line dfn := ... above). All nodes in the current [component] are marked as not visited. And we recursively construct a WTO for the component. *) Container.iter component ~fold:ARList.fold_unordered ~f:(fun nid -> dfn := Dfn.remove nid !dfn ) ; let component_partition = let partition = ref Partition.empty in stack_top.building_component <- true ; stack_top.succs_to_visit <- succs ; process_stack partition ; !partition in partition := Partition.add_component node component_partition !partition ) ; stack := next ; (process_stack [@tailcall]) partition in let partition = ref Partition.empty in push_on_stack (CFG.start_node cfg) ; process_stack partition ; !partition end