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