source/domains/abstractTreeDomain.ml (984 lines of code) (raw):
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
[@@@ocamlformat "wrap-comments=false"]
module Option = Core_kernel.Option
module MapPoly = Core_kernel.Map.Poly
module Fn = Core_kernel.Fn
(* optional transform: 'a option -> ('a -> 'b option) -> 'b option *)
let ( >>= ) = Option.( >>= )
module type CONFIG = sig
val max_tree_depth_after_widening : unit -> int
val check_invariants : bool
end
module type ELEMENT = sig
include AbstractDomainCore.S
val transform_on_widening_collapse : t -> t
end
module type CHECK = sig
type witness
val create_witness : bool -> false_witness:string -> witness
val option_construct : message:(unit -> string) -> witness -> witness
val false_witness : message:(unit -> string) -> witness
(* Captures true, as a witness, i.e. without extra info. *)
val true_witness : witness
val is_true : witness -> bool
val get_witness : witness -> string option
val and_witness : witness -> witness -> witness
(* Calls the argument function if checking is on, otherwise ignores it. *)
val check : (unit -> unit) -> unit
end
module WithChecks : CHECK = struct
type witness = string option
let create_witness condition ~false_witness =
if not condition then
Some false_witness
else
None
let option_construct ~message = function
| None -> None
| Some value -> Some (message () ^ "->" ^ value)
let false_witness ~message = Some (message ())
let true_witness = None
let is_true = Option.is_none
let get_witness witness = witness
let and_witness left_witness right_witness =
match left_witness, right_witness with
| None, None -> None
| Some _, None -> left_witness
| None, Some _ -> right_witness
| Some left_witness, Some right_witness -> Some (left_witness ^ "\n&& " ^ right_witness)
let check f = f ()
end
module WithoutChecks : CHECK = struct
type witness = bool
let create_witness condition ~false_witness:_ = condition
let option_construct ~message:_ witness = witness
let false_witness ~message:_ = false
let true_witness = true
let is_true witness = witness
let get_witness = function
| true -> None
| false -> Some "<no witness>"
let and_witness = ( && )
let check _ = ()
end
module Label = struct
type t =
| Index of string
| Field of string
| AnyIndex
let compare : t -> t -> int = compare
let pp formatter = function
| Index name -> Format.fprintf formatter "[%s]" name
| Field name -> Format.fprintf formatter ".%s" name
| AnyIndex -> Format.fprintf formatter "[*]"
let show = Format.asprintf "%a" pp
type path = t list
let pp_path formatter = ListLabels.iter ~f:(pp formatter)
let show_path = Format.asprintf "%a" pp_path
let create_name_index name = Index name
let create_int_index i = Index (string_of_int i)
let common_prefix left right =
let rec common_prefix_reversed left right so_far =
match left, right with
| left_element :: left_rest, right_element :: right_rest when left_element = right_element ->
common_prefix_reversed left_rest right_rest (left_element :: so_far)
| _ -> so_far
in
common_prefix_reversed left right [] |> List.rev
let rec is_prefix ~prefix path =
match prefix, path with
| prefix_head :: prefix_rest, path_head :: path_rest when prefix_head = path_head ->
is_prefix ~prefix:prefix_rest path_rest
| [], _ -> true
| _ -> false
let rec compare_path left right =
match left, right with
| left :: left_rest, right :: right_rest -> (
match compare left right with
| 0 -> compare_path left_rest right_rest
| n -> n)
| [], [] -> 0
| [], _ -> -1
| _, [] -> 1
let equal_path = ( = )
end
module Make (Config : CONFIG) (Element : ELEMENT) () = struct
module Checks = (val if Config.check_invariants then
(module WithChecks)
else
(module WithoutChecks) : CHECK)
module LabelMap = struct
module Map = Map.Make (Label)
include Map
let filter_mapi ~f map =
Map.mapi (fun key data -> f ~key ~data) map
|> Map.filter (fun _key -> function
| None -> false
| Some _ -> true)
|> Map.map (fun selected -> Option.value_exn selected)
let filter_map ~f = filter_mapi ~f:(fun ~key:_ ~data -> f data)
let fold ~f map ~init = Map.fold (fun key data acc -> f ~key ~data acc) map init
let find_false_witness ~f map =
let witness = ref Checks.true_witness in
let find_and_set_false_witness key data =
let current_witness = f ~key ~data in
if Checks.is_true current_witness then
false
else begin
witness := current_witness;
true
end
in
Map.exists find_and_set_false_witness map |> ignore;
!witness
let add ~key ~data map = Map.add key data map
let fold2 ~f ~init left right =
let combine _key left right =
match left, right with
| Some left, None -> Some (`Left left)
| None, Some right -> Some (`Right right)
| Some left, Some right -> Some (`Both (left, right))
| None, None -> None
in
merge combine left right |> fold ~f ~init
end
type t = {
(* Abstract contribution at this node. (Not the join from the root!) *)
element: Element.t;
(* Edges to child nodes.
NOTE: Indices are special. If the AnyIndex [*] is present then it
covers all indices [i], that are not explicitly present.
*)
children: t LabelMap.t;
}
(** Access Path tree nodes have an abstract domain element and a set of children indexed by
AccessPath.PathElement.t *)
let create_leaf element = { element; children = LabelMap.empty }
let empty_tree = create_leaf Element.bottom
let is_empty_info children element = LabelMap.is_empty children && Element.is_bottom element
let is_empty_tree { children; element } = is_empty_info children element
let bottom = empty_tree
let is_bottom = is_empty_tree
let create_tree_option path tree =
let rec create_tree_internal path tree =
match path with
| [] -> tree
| label_element :: rest ->
{
element = Element.bottom;
children = LabelMap.singleton label_element (create_tree_internal rest tree);
}
in
if is_empty_tree tree then
None
else
Some (create_tree_internal path tree)
type widen_depth = int option
(** Captures whether we need to widen and at what tree level. None -> no widening Some i -> widen
start i levels down. *)
let must_widen_depth = function
| None -> false
| Some i -> i = 0
let must_widen_element = Option.is_some
let decrement_widen = function
| None -> None
| Some i when i > 0 -> Some (i - 1)
| Some _ -> failwith "Decrementing widen depth below 0"
let element_join ~widen_depth w1 w2 =
if must_widen_element widen_depth then
Element.widen ~iteration:2 ~prev:w1 ~next:w2
else
Element.join w1 w2
(** Fold over tree, where each non-bottom element node is visited. The function ~f is passed the
path to the node, the non-bottom element at the node and the accumulator. *)
let fold_tree_paths ~init ~f tree =
let rec walk_children path { element; children } first_accumulator =
let second_accumulator =
if Element.is_bottom element then
first_accumulator
else
f ~path ~element first_accumulator
in
if LabelMap.is_empty children then
second_accumulator
else
let walk ~key:label_element ~data:subtree =
walk_children (path @ [label_element]) subtree
in
LabelMap.fold children ~init:second_accumulator ~f:walk
in
walk_children [] tree init
let pp formatter ({ element; children } as tree) =
if LabelMap.is_empty children then
Element.pp formatter element
else
let pp_node ~path ~element _ =
match path with
| [] -> Format.fprintf formatter "@,%a" Element.pp element
| _ -> Format.fprintf formatter "@,%a -> %a" Label.pp_path path Element.pp element
in
let pp _ = fold_tree_paths ~init:() ~f:pp_node in
Format.fprintf formatter "{@[<v 2>%a@]@,}" pp tree
let show = Format.asprintf "%a" pp
let rec max_depth { children; _ } =
LabelMap.fold
~f:(fun ~key:_ ~data:tree accumulator -> max (1 + max_depth tree) accumulator)
children
~init:0
let rec min_depth { children; element } =
if not (Element.is_bottom element) then
0
else
LabelMap.fold
~f:(fun ~key:_ ~data:tree accumulator -> min (1 + min_depth tree) accumulator)
children
~init:0
let rec is_minimal ancestors ({ element; children } as tree) =
if is_empty_tree tree then
Checks.false_witness ~message:(fun () -> "empty leaf.")
else if
(not (Element.is_bottom element)) && Element.less_or_equal ~left:element ~right:ancestors
then
Checks.false_witness ~message:(fun () -> "tree.element redundant.")
else
let ancestors = Element.join ancestors element in
let all_minimal ~key ~data:subtree =
is_minimal ancestors subtree |> Checks.option_construct ~message:(fun () -> Label.show key)
in
LabelMap.find_false_witness ~f:all_minimal children
let check_minimal_non_empty ~message tree =
is_minimal Element.bottom tree
|> Checks.get_witness
|> function
| None -> ()
| Some witness ->
let message =
Format.sprintf "%s not minimal: %s: result %s" (message ()) witness (show tree)
in
failwith message
let check_minimal ~message tree =
if is_empty_tree tree then
()
else
check_minimal_non_empty ~message tree
let lookup_tree_with_default { children; _ } element =
match LabelMap.find_opt element children with
| None -> empty_tree
| Some subtree -> subtree
(** Compute join of all element components in tree t. *)
let collapse ?(transform = Fn.id) ~widen_depth { element; children } =
let transform e = if Element.is_bottom e then e else transform e in
let rec collapse_tree { element; children } element_accumulator =
let element_accumulator = element_join ~widen_depth element_accumulator (transform element) in
let collapse_child ~key:_ ~data:subtree = collapse_tree subtree in
LabelMap.fold ~f:collapse_child children ~init:element_accumulator
in
(* Don't apply `transform` on the root element, only on collapsed elements. *)
collapse_tree { element = Element.bottom; children } element
let create_leaf_option ~ancestors ~element =
let difference = Element.subtract ancestors ~from:element in
if Element.less_or_equal ~left:difference ~right:ancestors then
None
else
Some (create_leaf difference)
let create_node_option element children =
if is_empty_info children element then
None
else
Some { element; children }
let option_node_tree ~message = function
| None -> empty_tree
| Some tree ->
Checks.check (fun () -> check_minimal_non_empty ~message tree);
tree
type filtered_element_t = {
new_element: Element.t;
ancestors: Element.t;
}
let filter_by_ancestors ~ancestors ~element =
let difference = Element.subtract ancestors ~from:element in
if Element.less_or_equal ~left:difference ~right:ancestors then
{ new_element = Element.bottom; ancestors }
else
{ new_element = difference; ancestors = Element.join ancestors element }
let rec prune_tree ancestors { element; children } =
let { new_element; ancestors } = filter_by_ancestors ~ancestors ~element in
let children = LabelMap.filter_map ~f:(prune_tree ancestors) children in
create_node_option new_element children
let set_or_remove key value map =
match value with
| None -> LabelMap.remove key map
| Some data -> LabelMap.add ~key ~data map
(** Widen differs from join in that right side does not extend trees, and Element uses widen.
`widen_depth` is less or equal to the max depth allowed in this subtree or None if we don't
widen.
`transform_on_collapse` is applied when collapsing trees during widening. *)
let rec join_trees
ancestors
~(widen_depth : widen_depth)
~transform_on_collapse
({ element = left_element; children = left_children } as left_tree)
({ element = right_element; children = right_children } as right_tree)
=
if must_widen_depth widen_depth then
(* Collapse left_tree and right_tree to achieve depth limit. Note that left_tree is a leaf,
only if the widen depth was exactly the depth of left_tree. *)
let collapsed_left_element =
collapse ~transform:transform_on_collapse ~widen_depth left_tree
in
let collapsed_right_element =
collapse ~transform:transform_on_collapse ~widen_depth right_tree
in
create_leaf_option
~ancestors
~element:(element_join ~widen_depth collapsed_left_element collapsed_right_element)
else
let joined_element = element_join ~widen_depth left_element right_element in
let { new_element; ancestors } = filter_by_ancestors ~ancestors ~element:joined_element in
let children =
join_children
ancestors
~transform_on_collapse
~widen_depth:(decrement_widen widen_depth)
left_children
right_children
in
create_node_option new_element children
and join_option_trees ancestors ~transform_on_collapse ~widen_depth left right =
match left, right with
| None, None -> None
| Some left, None -> prune_tree ancestors left
| None, Some right when widen_depth = None -> prune_tree ancestors right
| None, Some right -> join_trees ancestors ~transform_on_collapse ~widen_depth empty_tree right
| Some left, Some right -> join_trees ancestors ~transform_on_collapse ~widen_depth left right
and join_children ancestors ~transform_on_collapse ~widen_depth left_tree right_tree =
(* Merging is tricky because of the special meaning of [*] and [f]. We
have to identify the three sets of indices:
L : indices [f] only in left_tree
R : indices [f] only in right_tree
C : indices [f] common in left_tree and right_tree.
Let left_star be the tree associated with left_tree[*] and right_star = right_tree[*].
The merge result joined is then:
joined.element = pointwise merge of left_tree.element and right_tree.element
(if element is not an index)
joined.[*] = left_star merge right_star
joined.[c] = left_tree[c] merge right_tree[c] if c in C
joined.[l] = left_tree[l] merge right_star if l in L
joined.[r] = right_tree[r] merge left_star if r in R
joined.[<keys>] = left_tree[<keys>] merge right_tree[<keys>]
*)
let left_star = LabelMap.find_opt Label.AnyIndex left_tree in
let right_star = LabelMap.find_opt Label.AnyIndex right_tree in
(* merge_left takes care of C and L, as well as the dictionary keys *)
let merge_left ~key:element ~data:left_subtree accumulator =
match element with
| Label.AnyIndex ->
set_or_remove
element
(join_option_trees
ancestors
~transform_on_collapse
~widen_depth
(Some left_subtree)
right_star)
accumulator
| Label.Index _ -> (
match LabelMap.find_opt element right_tree with
| Some right_subtree ->
(* f in C *)
set_or_remove
element
(join_trees
ancestors
~transform_on_collapse
~widen_depth
left_subtree
right_subtree)
accumulator
| None ->
(* f in L *)
set_or_remove
element
(join_option_trees
ancestors
~transform_on_collapse
~widen_depth
(Some left_subtree)
right_star)
accumulator)
| Label.Field _ -> (
match LabelMap.find_opt element right_tree with
| Some right_subtree ->
set_or_remove
element
(join_trees
ancestors
~transform_on_collapse
~widen_depth
left_subtree
right_subtree)
accumulator
| None ->
let join_tree =
join_option_trees
ancestors
~transform_on_collapse
~widen_depth
(Some left_subtree)
None
in
set_or_remove element join_tree accumulator)
in
(* merge_right takes care of R *)
let merge_right ~key:element ~data:right_subtree accumulator =
match LabelMap.find_opt element left_tree with
| Some _ ->
(* pointwise, already done in merge_left. *)
accumulator
| None -> (
match element with
| Label.Index _ ->
let join_tree =
join_option_trees
ancestors
~transform_on_collapse
~widen_depth
left_star
(Some right_subtree)
in
set_or_remove element join_tree accumulator
| Label.AnyIndex
| Label.Field _ ->
let join_tree =
join_option_trees
ancestors
~transform_on_collapse
~widen_depth
None
(Some right_subtree)
in
set_or_remove element join_tree accumulator)
in
let left_done = LabelMap.fold ~init:LabelMap.empty left_tree ~f:merge_left in
LabelMap.fold ~init:left_done right_tree ~f:merge_right
(** Assign or join subtree into existing tree at path. *)
let rec assign_or_join_path
~do_join
~ancestors
~tree:({ element; children } as tree)
path
~subtree
=
if is_empty_tree tree then (* Shortcut *)
prune_tree ancestors subtree >>= create_tree_option path
else
match path with
| [] ->
if do_join then
join_trees ancestors ~transform_on_collapse:Fn.id ~widen_depth:None tree subtree
(* Join point. *)
else (* Note: we are overwriting t.element, so no need to add it to the path. *)
prune_tree ancestors subtree (* Assignment/join point. *)
| label_element :: rest -> (
let ancestors = Element.join ancestors element in
let existing = lookup_tree_with_default tree label_element in
match label_element with
| Label.AnyIndex ->
(* Special case. Must merge with AnyIndex and also every specific index. *)
let augmented = LabelMap.add ~key:Label.AnyIndex ~data:existing children in
let children =
LabelMap.filter_mapi ~f:(join_each_index ~ancestors rest ~subtree) augmented
in
create_node_option element children
| Label.Index _
| Label.Field _ ->
let children =
set_or_remove
label_element
(assign_or_join_path ~do_join ~ancestors ~tree:existing rest ~subtree)
children
in
create_node_option element children)
and join_each_index ~ancestors rest ~subtree ~key:element ~data:tree =
match element with
| Label.AnyIndex -> assign_or_join_path ~do_join:true ~ancestors ~tree rest ~subtree
| Label.Index _
| Label.Field _ ->
Some tree
(** Assign subtree subtree into existing tree at path. *)
let assign_path = assign_or_join_path ~do_join:false
(** Like assign_path, but at assignment point, joins the tree with existing tree, effectively a
weak assign. *)
let join_path = assign_or_join_path ~do_join:true
(** Read the subtree at path within tree and return the ancestors separately. ~use_precise_labels
overrides the default handling of [*] matching all fields. This is used solely in determining
port connections when emitting json.
ancestors is accumulated down the recursion and returned when we reach the end of that path.
That way the recursion is tail-recursive. *)
let rec read_raw ~transform_non_leaves ~use_precise_labels ~ancestors path { children; element } =
match path with
| [] -> ancestors, create_node_option element children
| label_element :: rest -> (
let ancestors = transform_non_leaves path element |> Element.join ancestors in
match label_element with
| Label.AnyIndex when not use_precise_labels ->
(* lookup all indexes and join result *)
let find_index_and_join ~key ~data:subtree (ancestors_accumulator, tree_accumulator) =
(* Fields are special - they should be excluded from [*]
accesses unconditionally. *)
match key with
| Label.Field _ -> Element.bottom, None
| _ ->
let ancestors_result, subtree =
read_raw ~transform_non_leaves ~use_precise_labels ~ancestors rest subtree
in
let subtree =
join_option_trees
Element.bottom
~transform_on_collapse:Fn.id
~widen_depth:None
tree_accumulator
subtree
in
Element.join ancestors_result ancestors_accumulator, subtree
in
LabelMap.fold ~init:(ancestors, None) ~f:find_index_and_join children
| Label.Index _ when not use_precise_labels -> (
(* read [f] or [*] *)
match LabelMap.find_opt label_element children with
| None -> (
match LabelMap.find_opt Label.AnyIndex children with
| Some subtree ->
read_raw ~transform_non_leaves ~use_precise_labels ~ancestors rest subtree
| None -> ancestors, None)
| Some subtree ->
read_raw ~transform_non_leaves ~use_precise_labels ~ancestors rest subtree)
| _ -> (
match LabelMap.find_opt label_element children with
| None -> ancestors, None
| Some subtree ->
read_raw ~transform_non_leaves ~use_precise_labels ~ancestors rest subtree))
(** Read the subtree at path p within t. Returns the pair ancestors, tree_at_tip. *)
let read_tree_raw
?(transform_non_leaves = fun _p element -> element)
?(use_precise_labels = false)
path
tree
=
let message () =
Format.sprintf "read tree_raw: %s :from: %s" (Label.show_path path) (show tree)
in
let ancestors, tree_option =
read_raw ~transform_non_leaves ~use_precise_labels ~ancestors:Element.bottom path tree
in
ancestors, option_node_tree ~message tree_option
let assign_tree_path ~tree path ~subtree =
let message () =
Format.sprintf
"assign tree: %s :to: %s :in: %s"
(show subtree)
(Label.show_path path)
(show tree)
in
assign_path ~ancestors:Element.bottom ~tree path ~subtree |> option_node_tree ~message
let join_tree_path ~tree path ~subtree =
let message () =
Format.sprintf
"join tree: %s :to: %s :in: %s"
(show subtree)
(Label.show_path path)
(show tree)
in
if is_bottom subtree then
tree
else
join_path ~ancestors:Element.bottom ~tree path ~subtree |> option_node_tree ~message
let assign ?(weak = false) ~tree path ~subtree =
if weak then
join_tree_path ~tree path ~subtree
else
assign_tree_path ~tree path ~subtree
(** right_ancestors is the path element of right_tree, i.e. the join of element's along the spine
of the right tree to this point. *)
let rec less_or_equal_tree
{ element = left_element; children = left_children }
right_ancestors
{ element = right_element; children = right_children }
=
let right_ancestors = Element.join right_ancestors right_element in
if not (Element.less_or_equal ~left:left_element ~right:right_ancestors) then
let message () =
Format.sprintf
"Element not less_or_equal: %s\nvs\n%s\n"
(Element.show left_element)
(Element.show right_ancestors)
in
Checks.false_witness ~message
else
less_or_equal_children left_children right_ancestors right_children
and less_or_equal_option_tree left_option_tree right_ancestors right_option_tree =
match left_option_tree, right_option_tree with
| None, _ -> Checks.true_witness
| Some left_tree, None ->
(* Check that all on left <= right_ancestors *)
less_or_equal_tree left_tree right_ancestors empty_tree
| Some left_tree, Some right_tree -> less_or_equal_tree left_tree right_ancestors right_tree
and less_or_equal_all left_label_map right_ancestors =
let check_less_or_equal ~key:_ ~data:left_subtree =
less_or_equal_tree left_subtree right_ancestors empty_tree
in
LabelMap.find_false_witness left_label_map ~f:check_less_or_equal
and less_or_equal_children left_label_map right_ancestors right_label_map =
if LabelMap.is_empty left_label_map then
Checks.true_witness
else if LabelMap.is_empty right_label_map then
(* Check that all on the left <= right_ancestors *)
less_or_equal_all left_label_map right_ancestors
else
(* Pointwise on non-index elements, common index elements, and on dictionary keys.
Let L, R be the index elements present only in left_label_map and right_label_map
respectively, and let left_star, right_star be the [*] subtrees of left_label_map and
right_label_map respectively. Then,
left_star <= right_star /\
left_star <= right_label_map[r] for all r in R /\
left_label_map[l] <= right_star for all l in L.
And with the understanding of left_label_map[<keys>] bottom and
right_label_map[<keys>] = top if the index is missing (we choose this behavior to ensure
that key taint doesn't interfere with value taint),
left_label_map[<keys>] <= right_label_map[<keys>] *)
let left_star = LabelMap.find_opt Label.AnyIndex left_label_map in
let right_star = LabelMap.find_opt Label.AnyIndex right_label_map in
let check_less_or_equal ~key:label_element ~data:left_subtree =
match label_element with
| Label.AnyIndex ->
less_or_equal_option_tree left_star right_ancestors right_star
|> Checks.option_construct ~message:(fun () -> "[left *]")
| Label.Index _ -> (
match LabelMap.find_opt label_element right_label_map with
| None ->
(* in L *)
less_or_equal_option_tree (Some left_subtree) right_ancestors right_star
|> Checks.option_construct ~message:(fun () -> "[right *]")
| Some right_subtree ->
(* in common *)
less_or_equal_tree left_subtree right_ancestors right_subtree
|> Checks.option_construct ~message:(fun () -> Label.show label_element))
| Label.Field _ -> (
match LabelMap.find_opt label_element right_label_map with
| Some right_subtree -> less_or_equal_tree left_subtree right_ancestors right_subtree
| None ->
less_or_equal_option_tree (Some left_subtree) right_ancestors None
|> Checks.option_construct ~message:(fun () -> "[right <keys>]"))
in
(* Check that all non-star indexes on right are larger than star1,
unless they were matched directly. *)
let check_star_left ~key:label_element ~data:right_subtree =
match label_element with
| Label.Index _ when not (LabelMap.mem label_element left_label_map) ->
less_or_equal_option_tree left_star right_ancestors (Some right_subtree)
|> Checks.option_construct ~message:(fun () -> "[left *]")
| _ -> Checks.true_witness
in
let result = LabelMap.find_false_witness ~f:check_less_or_equal left_label_map in
if Checks.is_true result then
LabelMap.find_false_witness ~f:check_star_left right_label_map
else
result
let read ?(transform_non_leaves = fun _p element -> element) path tree =
let ancestors, tree = read_tree_raw ~transform_non_leaves ~use_precise_labels:false path tree in
let message () = Format.sprintf "read [%s] from %s" (Label.show_path path) (show tree) in
(* Important to properly join the trees and not just join ancestors and
tree.element, as otherwise this could result in non-minimal trees. *)
join_trees
Element.bottom
~transform_on_collapse:Fn.id
~widen_depth:None
(create_leaf ancestors)
tree
|> option_node_tree ~message
(** Collapses all subtrees at depth. Used to limit amount of detail propagated across function
boundaries, in particular for scaling. *)
let collapse_to ?(transform = Fn.id) ~depth tree =
let message () = Format.sprintf "collapse to %d\n%s\n" depth (show tree) in
join_trees Element.bottom ~transform_on_collapse:transform ~widen_depth:(Some depth) tree tree
|> option_node_tree ~message
(* Makes sure tree has at most ~width leaves by collapsing levels (lowest
first). Only entire levels are collapsed. *)
let limit_to ?(transform = Fn.id) ~width tree =
let rec compute_max_depth ~depth leaves_left roots =
if roots = [] then
raise Exit (* tree does not exceed leaf limit *)
else if leaves_left < List.length roots then
depth - 1
else
let leaves, roots =
List.fold_left
(fun (leaves, roots) tree ->
if LabelMap.is_empty tree.children then
leaves + 1, roots
else
( leaves,
LabelMap.fold ~f:(fun ~key:_ ~data -> List.cons data) tree.children ~init:roots ))
(0, [])
roots
in
compute_max_depth ~depth:(depth + 1) (leaves_left - leaves) roots
in
try
let depth = compute_max_depth ~depth:0 width [tree] in
collapse_to ~transform ~depth tree
with
| Exit -> tree
let verify_less_or_equal left_tree right_tree message =
match less_or_equal_tree left_tree Element.bottom right_tree |> Checks.get_witness with
| None -> ()
| Some witness ->
Format.sprintf
"bad join %s - %s:\n%s\nvs\n%s"
message
witness
(show left_tree)
(show right_tree)
|> failwith
let check_join_property left_tree right_tree result =
if Config.check_invariants then begin
verify_less_or_equal left_tree result "left_tree<=result";
verify_less_or_equal right_tree result "right_tree<=result"
end;
result
let join left right =
if left == right || is_bottom right then
left
else
let message () =
Format.sprintf "join trees: left_tree\n%s\nright_tree:\n%s\n" (show left) (show right)
in
join_trees Element.bottom ~transform_on_collapse:Fn.id ~widen_depth:None left right
|> option_node_tree ~message
|> check_join_property left right
(* Shape tree ~mold transforms the left tree so it only contains branches present in mold. *)
let rec shape_tree
~transform
~ancestors
{ element = left_element; children = left_children }
~mold:{ element = _; children = mold_children }
=
let widen_depth = None in
let joined_element, left_children =
let lift_dead_branches ~key ~data (lifted, result) =
match data with
| `Both (left, _mold) -> lifted, LabelMap.add ~key ~data:left result
| `Left left -> Element.join lifted (collapse ~transform ~widen_depth left), result
| `Right _ -> lifted, result
in
LabelMap.fold2
left_children
mold_children
~init:(left_element, LabelMap.empty)
~f:lift_dead_branches
in
let { new_element; ancestors } = filter_by_ancestors ~ancestors ~element:joined_element in
let children = shape_children ~transform ancestors left_children ~mold:mold_children in
create_node_option new_element children
(* left_tree already contains only branches that are also in mold. *)
and shape_children ~transform ancestors left_children ~mold =
let mold_branch ~key ~data result =
match data with
| `Both (left_tree, mold) -> (
match shape_tree ~transform ~ancestors left_tree ~mold with
| Some merged -> LabelMap.add result ~key ~data:merged
| None -> result)
| `Right _mold -> failwith "Invariant broken. Mold should not have more branches"
| `Left _ -> failwith "Invariant broken. Left branch should have been lifted"
in
LabelMap.fold2 left_children mold ~init:LabelMap.empty ~f:mold_branch
let shape ?(transform = Fn.id) tree ~mold =
let message () = Format.sprintf "shape tree\n%s\nmold:\n%s\n" (show tree) (show mold) in
shape_tree ~transform ~ancestors:Element.bottom tree ~mold |> option_node_tree ~message
let get_root { element; _ } = element
(** Filter map over tree, where each non-bottom element node is visited. The function ~f is passed
the path to the node and the non-bottom element at the node and returns a new Element to
substitute (possibly bottom). *)
let filter_map_tree_paths ~f tree =
let build ~path ~element access_path_tree =
let new_path, element = f ~path ~element in
if Element.is_bottom element then
access_path_tree
else
assign_tree_path ~tree:access_path_tree new_path ~subtree:(create_leaf element)
in
let result = fold_tree_paths ~init:empty_tree ~f:build tree in
let message () = "filter_map_tree_paths" in
Checks.check (fun () -> check_minimal ~message result);
result
(** Removes all subtrees at depth. Used to limit amount of propagation across function boundaries,
in particular for scaling. *)
let cut_tree_after ~depth tree =
let filter ~path ~element =
if List.length path > depth then
path, Element.bottom
else
path, element
in
filter_map_tree_paths ~f:filter tree
let create_tree path element =
let message () = Format.sprintf "create_tree %s" (Label.show_path path) in
create_tree_option path element |> option_node_tree ~message
open AbstractDomainCore
type _ part += Path : (Label.path * Element.t) part
module rec Base : (BASE with type t := t) = MakeBase (struct
type nonrec t = t
include Domain
end)
and Domain : (S with type t := t) = struct
type nonrec t = t
let show = show
let pp = pp
type _ part += Self : t part
let join = join
let bottom = bottom
let is_bottom = is_bottom
let less_or_equal ~left ~right = less_or_equal_tree left Element.bottom right |> Checks.is_true
let subtract _to_remove ~from =
(* Correct, but one can probably do better when needed. *)
from
let widen ~iteration:_ ~prev ~next =
let message () =
Format.sprintf "wident trees: previous\n%s\nnext:\n%s\n" (show prev) (show next)
in
join_trees
Element.bottom
~transform_on_collapse:Element.transform_on_widening_collapse
~widen_depth:(Some (Config.max_tree_depth_after_widening ()))
prev
next
|> option_node_tree ~message
|> check_join_property prev next
let transform : type a f. a part -> ([ `Transform ], a, f, _) operation -> f:f -> t -> t =
fun part op ~f tree ->
match part, op with
| Path, Map ->
let transform_node ~path ~element = f (path, element) in
filter_map_tree_paths ~f:transform_node tree
| Path, Add ->
let path, element = f in
join tree (create_tree path (create_leaf element))
| Path, Filter ->
filter_map_tree_paths
~f:(fun ~path ~element ->
if f (path, element) then
path, element
else
path, Element.bottom)
tree
| _, Context (Path, op) ->
let transform_node ~path ~element =
path, Element.transform part op ~f:(f (path, element)) element
in
filter_map_tree_paths ~f:transform_node tree
| (Path | Self), _ -> Base.transform part op ~f tree
| _ ->
let transform_node ~path ~element = path, Element.transform part op ~f element in
filter_map_tree_paths ~f:transform_node tree
let reduce
: type a f b. a part -> using:([ `Reduce ], a, f, b) operation -> f:f -> init:b -> t -> b
=
fun part ~using:op ~f ~init tree ->
match part, op with
| Path, Acc ->
let fold_tree_node ~path ~element accumulator = f (path, element) accumulator in
fold_tree_paths ~init ~f:fold_tree_node tree
| Path, Exists ->
let fold_tree_node ~path ~element accumulator = accumulator || f (path, element) in
init || fold_tree_paths ~init ~f:fold_tree_node tree
| _, Context (Path, op) ->
let fold_tree_node ~path ~element accumulator =
Element.reduce part ~using:op ~f:(f (path, element)) ~init:accumulator element
in
fold_tree_paths ~init ~f:fold_tree_node tree
| (Path | Self), _ -> Base.reduce part ~using:op ~f ~init tree
| _ ->
let fold_tree_node ~path:_ ~element accumulator =
Element.reduce part ~using:op ~init:accumulator ~f element
in
fold_tree_paths ~init ~f:fold_tree_node tree
let partition
: type a f b. a part -> ([ `Partition ], a, f, b) operation -> f:f -> t -> (b, t) MapPoly.t
=
fun part op ~f tree ->
let update path element existing =
let leaf = create_leaf element in
match existing with
| None -> create_tree path leaf
| Some tree -> assign_tree_path ~tree path ~subtree:leaf
in
match part, op with
| Path, By ->
let partition ~path ~element result =
let partition_key = f (path, element) in
MapPoly.update result partition_key ~f:(update path element)
in
fold_tree_paths ~init:MapPoly.empty ~f:partition tree
| Path, ByFilter ->
let partition ~path ~element result =
match f (path, element) with
| None -> result
| Some partition_key -> MapPoly.update result partition_key ~f:(update path element)
in
fold_tree_paths ~init:MapPoly.empty ~f:partition tree
| _, Context (Path, op) ->
let partition ~path ~element result =
let element_partition = Element.partition part op ~f:(f (path, element)) element in
let distribute ~key ~data result = MapPoly.update result key ~f:(update path data) in
MapPoly.fold ~init:result ~f:distribute element_partition
in
fold_tree_paths ~init:MapPoly.empty ~f:partition tree
| (Path | Self), _ -> Base.partition part op ~f tree
| _ ->
let partition ~path ~element result =
let element_partition = Element.partition part op ~f element in
let distribute ~key ~data result = MapPoly.update result key ~f:(update path data) in
MapPoly.fold ~init:result ~f:distribute element_partition
in
fold_tree_paths ~init:MapPoly.empty ~f:partition tree
let introspect (type a) (op : a introspect) : a =
match op with
| GetParts f ->
f#report Self;
f#report Path;
Element.introspect op
| Structure ->
let range = Element.introspect op in
"Tree ->" :: ListLabels.map ~f:(fun s -> " " ^ s) range
| Name part -> (
match part with
| Path -> Format.sprintf "Tree.Path"
| Self -> Format.sprintf "Tree.Self"
| _ -> Element.introspect op)
let create parts =
let create_path result part =
match part with
| Part (Path, (path, tip)) -> create_leaf tip |> create_tree path |> join result
| Part (Self, info) -> join result (info : t)
| _ ->
(* Assume [] path *)
Element.create [part] |> create_leaf |> join result
in
ListLabels.fold_left parts ~init:bottom ~f:create_path
let fold = Base.fold
let meet = Base.meet
let apply = Base.apply
end
let _ = Base.fold (* unused module warning work-around *)
include Domain
let collapse = collapse ~widen_depth:None
let prepend = create_tree
let read_raw = read_tree_raw
let labels { children; _ } =
LabelMap.fold ~init:[] ~f:(fun ~key ~data:_ acc -> key :: acc) children
end