You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
7.7 KiB

(*
* 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