(* * Copyright (c) 2009-2013, Monoidics ltd. * Copyright (c) 2013-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 (** Execution Paths *) module L = Logging module F = Format (* =============== START of the Path module ===============*) module Path : sig (** type for paths *) type t type session = int val add_call : bool -> t -> Typ.Procname.t -> t -> t (** add a call with its sub-path, the boolean indicates whether the subtrace for the procedure should be included *) val add_skipped_call : t -> Typ.Procname.t -> string -> Location.t option -> t (** add a call to a procname that's had to be skipped, along with the reason and the location of the procname when known *) val contains_position : t -> PredSymb.path_pos -> bool (** check wether the path contains the given position *) val create_loc_trace : t -> PredSymb.path_pos option -> Errlog.loc_trace (** Create the location trace of the path, up to the path position if specified *) val curr_node : t -> Procdesc.Node.t option (** return the current node of the path *) val d : t -> unit (** dump a path *) val d_stats : t -> unit (** dump statistics of the path *) val extend : Procdesc.Node.t -> Typ.Name.t option -> session -> t -> t (** extend a path with a new node reached from the given session, with an optional string for exceptions *) val add_description : t -> string -> t (** extend a path with a new node reached from the given session, with an optional string for exceptions *) val fold_all_nodes_nocalls : (t, Procdesc.Node.t, 'accum) Container.fold (** fold over each node in the path, excluding calls, once *) val iter_shortest_sequence : (int -> t -> int -> Typ.Name.t option -> unit) -> PredSymb.path_pos option -> t -> unit val join : t -> t -> t (** join two paths *) val pp : Format.formatter -> t -> unit (** pretty print a path *) val pp_stats : Format.formatter -> t -> unit (** pretty print statistics of the path *) val start : Procdesc.Node.t -> t (** create a new path with given start node *) (* (** equality for paths *) val equal : t -> t -> bool val get_description : t -> string option *) end = struct type session = int [@@deriving compare] type stats = { mutable max_length: int ; (* length of the longest linear sequence *) mutable linear_num: float (* number of linear sequences described by the path *) } (* type aliases for components of t values that compare should ignore *) type stats_ = stats let compare_stats_ _ _ = 0 type procname_ = Typ.Procname.t let compare_procname_ _ _ = 0 type string_option_ = string option let compare_string_option_ _ _ = 0 type path_exec_ = | ExecSkipped of string * Location.t option (** call was skipped with a reason *) | ExecCompleted of t (** call was completed *) and t = (* INVARIANT: stats are always set to dummy_stats unless we are in the middle of a traversal *) (* in particular: a new traversal cannot be initiated during an existing traversal *) | Pstart of Procdesc.Node.t * stats_ (** start node *) | Pnode of Procdesc.Node.t * Typ.Name.t option * session * t * stats_ * string_option_ (** we got to [node] from last [session] perhaps propagating exception [exn_opt], and continue with [path]. *) | Pjoin of t * t * stats_ (** join of two paths *) | Pcall of t * procname_ * path_exec_ * stats_ (** add a sub-path originating from a call *) [@@deriving compare] let get_dummy_stats () = {max_length= -1; linear_num= -1.0} let get_description path = match path with Pnode (_, _, _, _, _, descr_opt) -> descr_opt | _ -> None let add_description path description = let add_descr descr_option description = match descr_option with Some descr -> descr ^ " " ^ description | None -> description in match path with | Pnode (node, exn_opt, session, path, stats, descr_opt) -> let description = add_descr descr_opt description in Pnode (node, exn_opt, session, path, stats, Some description) | _ -> path let set_dummy_stats stats = stats.max_length <- -1 ; stats.linear_num <- -1.0 let rec curr_node = function | Pstart (node, _) -> Some node | Pnode (node, _, _, _, _, _) -> Some node | Pcall (path, _, _, _) -> curr_node path | Pjoin _ -> None let start node = Pstart (node, get_dummy_stats ()) let extend (node: Procdesc.Node.t) exn_opt session path = Pnode (node, exn_opt, session, path, get_dummy_stats (), None) let join p1 p2 = Pjoin (p1, p2, get_dummy_stats ()) let add_call include_subtrace p pname p_sub = if include_subtrace then Pcall (p, pname, ExecCompleted p_sub, get_dummy_stats ()) else p let add_skipped_call p pname reason loc_opt = Pcall (p, pname, ExecSkipped (reason, loc_opt), get_dummy_stats ()) (** functions in this module either do not assume, or do not re-establish, the invariant on dummy stats *) module Invariant = struct (** check whether a stats is the dummy stats *) let stats_is_dummy stats = Int.equal stats.max_length (-1) (** return the stats of the path, assumes that the stats are computed *) let get_stats = function | Pstart (_, stats) -> stats | Pnode (_, _, _, _, stats, _) -> stats | Pjoin (_, _, stats) -> stats | Pcall (_, _, _, stats) -> stats (** restore the invariant that all the stats are dummy, so the path is ready for another traversal assumes that the stats are computed beforehand, and ensures that the invariant holds afterwards *) let rec reset_stats = function | Pstart (_, stats) -> if not (stats_is_dummy stats) then set_dummy_stats stats | Pnode (_, _, _, path, stats, _) | Pcall (path, _, ExecSkipped _, stats) -> if not (stats_is_dummy stats) then ( reset_stats path ; set_dummy_stats stats ) | Pjoin (path1, path2, stats) -> if not (stats_is_dummy stats) then ( reset_stats path1 ; reset_stats path2 ; set_dummy_stats stats ) | Pcall (path1, _, ExecCompleted path2, stats) -> if not (stats_is_dummy stats) then ( reset_stats path1 ; reset_stats path2 ; set_dummy_stats stats ) (** Iterate [f] over the path and compute the stats, assuming the invariant: all the stats are dummy. Function [f] (typically with side-effects) is applied once to every node, and max_length in the stats is the length of a longest sequence of nodes in the path where [f] returned [true] on at least one node. max_length is 0 if the path was visited but no node satisfying [f] was found. Assumes that the invariant holds beforehand, and ensures that all the stats are computed afterwards. Since this breaks the invariant, it must be followed by reset_stats. *) let rec compute_stats do_calls (f: Procdesc.Node.t -> bool) = let nodes_found stats = stats.max_length > 0 in function | Pstart (node, stats) -> if stats_is_dummy stats then ( let found = f node in stats.max_length <- (if found then 1 else 0) ; stats.linear_num <- 1.0 ) | Pnode (node, _, _, path, stats, _) -> if stats_is_dummy stats then ( compute_stats do_calls f path ; let stats1 = get_stats path in let found = f node || nodes_found stats1 (* the order is important as f has side-effects *) in stats.max_length <- (if found then 1 + stats1.max_length else 0) ; stats.linear_num <- stats1.linear_num ) | Pjoin (path1, path2, stats) -> if stats_is_dummy stats then ( compute_stats do_calls f path1 ; compute_stats do_calls f path2 ; let stats1, stats2 = (get_stats path1, get_stats path2) in stats.max_length <- max stats1.max_length stats2.max_length ; stats.linear_num <- stats1.linear_num +. stats2.linear_num ) | Pcall (path1, _, ExecCompleted path2, stats) -> if stats_is_dummy stats then ( let stats2 = match do_calls with | true -> compute_stats do_calls f path2 ; get_stats path2 | false -> {max_length= 0; linear_num= 0.0} in let stats1 = let f' = if nodes_found stats2 then fun _ -> true (* already found in call, no need to search before the call *) else f in compute_stats do_calls f' path1 ; get_stats path1 in stats.max_length <- stats1.max_length + stats2.max_length ; stats.linear_num <- stats1.linear_num ) | Pcall (path, _, ExecSkipped _, stats) -> if stats_is_dummy stats then ( let stats1 = compute_stats do_calls f path ; get_stats path in stats.max_length <- stats1.max_length ; stats.linear_num <- stats1.linear_num ) end (* End of module Invariant *) (** fold over each node in the path, excluding calls, once *) let fold_all_nodes_nocalls path ~init ~f = let acc = ref init in Invariant.compute_stats false (fun node -> acc := f !acc node ; true ) path ; Invariant.reset_stats path ; !acc let get_path_pos node = let pn = Procdesc.Node.get_proc_name node in let n_id = Procdesc.Node.get_id node in (pn, (n_id :> int)) let contains_position path pos = let found = ref false in let f node = if PredSymb.equal_path_pos (get_path_pos node) pos then found := true ; true in Invariant.compute_stats true f path ; Invariant.reset_stats path ; !found (** iterate over the longest sequence belonging to the path, restricting to those where [filter] holds of some element. If a node is reached via an exception, pass the exception information to [f] on the previous node *) let iter_shortest_sequence_filter (f: int -> t -> int -> Typ.Name.t option -> unit) (filter: Procdesc.Node.t -> bool) (path: t) : unit = let rec doit level session path prev_exn_opt = match path with | Pstart _ -> f level path session prev_exn_opt | Pnode (_, exn_opt, session', p, _, _) -> (* no two consecutive exceptions *) let next_exn_opt = if prev_exn_opt <> None then None else exn_opt in doit level (session' :> int) p next_exn_opt ; f level path session prev_exn_opt | Pjoin (p1, p2, _) -> if (Invariant.get_stats p1).max_length <= (Invariant.get_stats p2).max_length then doit level session p1 prev_exn_opt else doit level session p2 prev_exn_opt | Pcall (p1, _, ExecCompleted p2, _) -> let next_exn_opt = None in (* exn must already be inside the call *) doit level session p1 next_exn_opt ; doit (level + 1) session p2 next_exn_opt | Pcall (p, _, ExecSkipped _, _) -> let next_exn_opt = None in doit level session p next_exn_opt ; f level path session prev_exn_opt in Invariant.compute_stats true filter path ; doit 0 0 path None ; Invariant.reset_stats path (** iterate over the shortest sequence belonging to the path, restricting to those containing the given position if given. Do not iterate past the last occurrence of the given position. [f level path session exn_opt] is passed the current nesting [level] and [path] and previous [session] and possible exception [exn_opt] *) let iter_shortest_sequence (f: int -> t -> int -> Typ.Name.t option -> unit) (pos_opt: PredSymb.path_pos option) (path: t) : unit = let filter node = match pos_opt with | None -> true | Some pos -> PredSymb.equal_path_pos (get_path_pos node) pos in let path_pos_at_path p = try match curr_node p with Some node -> pos_opt <> None && filter node | None -> false with exn when SymOp.exn_not_failure exn -> false in let position_seen = ref false in let inverse_sequence = let log = ref [] in let g level p session exn_opt = if path_pos_at_path p then position_seen := true ; log := (level, p, session, exn_opt) :: !log in iter_shortest_sequence_filter g filter path ; !log in let sequence_up_to_last_seen = if !position_seen then let rec remove_until_seen = function | ((_, p, _, _) as x) :: l -> if path_pos_at_path p then List.rev (x :: l) else remove_until_seen l | [] -> [] in remove_until_seen inverse_sequence else List.rev inverse_sequence in List.iter ~f:(fun (level, p, session, exn_opt) -> f level p session exn_opt) sequence_up_to_last_seen (** return the node visited most, and number of visits, in the shortest linear sequence *) let repetitions path = let map = ref Procdesc.NodeMap.empty in let add_node = function | Some node -> ( try let n = Procdesc.NodeMap.find node !map in map := Procdesc.NodeMap.add node (n + 1) !map with Caml.Not_found -> map := Procdesc.NodeMap.add node 1 !map ) | None -> () in iter_shortest_sequence (fun _ p _ _ -> add_node (curr_node p)) None path ; let max_rep_node = ref (Procdesc.Node.dummy None) in let max_rep_num = ref 0 in Procdesc.NodeMap.iter (fun node num -> if num > !max_rep_num then ( max_rep_node := node ; max_rep_num := num ) ) !map ; (!max_rep_node, !max_rep_num) let stats_string path = Invariant.compute_stats true (fun _ -> true) path ; let node, repetitions = repetitions path in let str = "linear paths: " ^ string_of_float (Invariant.get_stats path).linear_num ^ " max length: " ^ string_of_int (Invariant.get_stats path).max_length ^ " has repetitions: " ^ string_of_int repetitions ^ " of node " ^ string_of_int (Procdesc.Node.get_id node :> int) in Invariant.reset_stats path ; str let pp_stats fmt path = F.pp_print_string fmt (stats_string path) let d_stats path = L.d_str (stats_string path) module PathMap = Caml.Map.Make (struct type nonrec t = t let compare = compare end) let pp fmt path = let delayed_num = ref 0 in let delayed = ref PathMap.empty in let add_path p = try ignore (PathMap.find p !delayed) with Caml.Not_found -> incr delayed_num ; delayed := PathMap.add p !delayed_num !delayed in let path_seen p = (* path seen before *) PathMap.mem p !delayed in let rec add_delayed path = if not (path_seen path) (* avoid exponential blowup *) then match path with (* build a map from delayed paths to a unique number *) | Pstart _ -> () | Pnode (_, _, _, p, _, _) | Pcall (p, _, ExecSkipped _, _) -> add_delayed p | Pjoin (p1, p2, _) | Pcall (p1, _, ExecCompleted p2, _) -> (* delay paths occurring in a join *) add_delayed p1 ; add_delayed p2 ; add_path p1 ; add_path p2 in let rec doit n fmt path = try if n > 0 then raise Caml.Not_found ; let num = PathMap.find path !delayed in F.fprintf fmt "P%d" num with Caml.Not_found -> match path with | Pstart (node, _) -> F.fprintf fmt "n%a" Procdesc.Node.pp node | Pnode (node, _, session, path, _, _) -> F.fprintf fmt "%a(s%d).n%a" (doit (n - 1)) path (session :> int) Procdesc.Node.pp node | Pjoin (path1, path2, _) -> F.fprintf fmt "(%a + %a)" (doit (n - 1)) path1 (doit (n - 1)) path2 | Pcall (path1, _, ExecCompleted path2, _) -> F.fprintf fmt "(%a{%a})" (doit (n - 1)) path1 (doit (n - 1)) path2 | Pcall (path, _, ExecSkipped (reason, _), _) -> F.fprintf fmt "(%a: %s)" (doit (n - 1)) path reason in let print_delayed () = if not (PathMap.is_empty !delayed) then ( let f path num = F.fprintf fmt "P%d = %a@\n" num (doit 1) path in F.fprintf fmt "where@\n" ; PathMap.iter f !delayed ) in add_delayed path ; doit 0 fmt path ; print_delayed () let d p = L.add_print pp p let create_loc_trace path pos_opt : Errlog.loc_trace = let trace = ref [] in let g level path _ exn_opt = match (path, curr_node path) with | Pcall (_, pname, ExecSkipped (reason, loc_opt), _), Some curr_node -> let curr_loc = Procdesc.Node.get_loc curr_node in let descr = Format.sprintf "Skipping %s: %s" (Typ.Procname.to_simplified_string pname) reason in let node_tags = [] in trace := Errlog.make_trace_element level curr_loc descr node_tags :: !trace ; Option.iter ~f:(fun loc -> if Typ.Procname.is_java pname && not (SourceFile.is_invalid loc.Location.file) then let definition_descr = Format.sprintf "Definition of %s" (Typ.Procname.to_simplified_string pname) in trace := Errlog.make_trace_element (level + 1) loc definition_descr [] :: !trace ) loc_opt | _, Some curr_node -> ( let curr_loc = Procdesc.Node.get_loc curr_node in match Procdesc.Node.get_kind curr_node with | Procdesc.Node.Join_node -> () (* omit join nodes from error traces *) | Procdesc.Node.Start_node pname -> let descr = "start of procedure " ^ Typ.Procname.to_simplified_string pname in let node_tags = [Errlog.Procedure_start pname] in trace := Errlog.make_trace_element level curr_loc descr node_tags :: !trace | Procdesc.Node.Prune_node (is_true_branch, if_kind, _) -> let descr = match (is_true_branch, if_kind) with | true, Sil.Ik_if -> "Taking true branch" | false, Sil.Ik_if -> "Taking false branch" | true, (Sil.Ik_for | Sil.Ik_while | Sil.Ik_dowhile) -> "Loop condition is true. Entering loop body" | false, (Sil.Ik_for | Sil.Ik_while | Sil.Ik_dowhile) -> "Loop condition is false. Leaving loop" | true, Sil.Ik_switch -> "Switch condition is true. Entering switch case" | false, Sil.Ik_switch -> "Switch condition is false. Skipping switch case" | true, (Sil.Ik_bexp | Sil.Ik_land_lor) -> "Condition is true" | false, (Sil.Ik_bexp | Sil.Ik_land_lor) -> "Condition is false" in let node_tags = [Errlog.Condition is_true_branch] in trace := Errlog.make_trace_element level curr_loc descr node_tags :: !trace | Procdesc.Node.Exit_node pname -> let descr = "return from a call to " ^ Typ.Procname.to_string pname in let node_tags = [Errlog.Procedure_end pname] in trace := Errlog.make_trace_element level curr_loc descr node_tags :: !trace | _ -> let descr, node_tags = match exn_opt with | None -> ("", []) | Some exn_name -> let exn_str = Typ.Name.name exn_name in let desc = if String.is_empty exn_str then "exception" else "exception " ^ exn_str in (desc, [Errlog.Exception exn_name]) in let descr = match get_description path with | Some path_descr -> if String.length descr > 0 then descr ^ " " ^ path_descr else path_descr | None -> descr in trace := Errlog.make_trace_element level curr_loc descr node_tags :: !trace ) | _, None -> () in iter_shortest_sequence g pos_opt path ; let equal lt1 lt2 = [%compare.equal : int * Location.t] (lt1.Errlog.lt_level, lt1.Errlog.lt_loc) (lt2.Errlog.lt_level, lt2.Errlog.lt_loc) in let relevant lt = lt.Errlog.lt_node_tags <> [] in IList.remove_irrelevant_duplicates ~equal ~f:relevant (List.rev !trace) end (* =============== END of the Path module ===============*) module PropMap = Caml.Map.Make (struct type t = Prop.normal Prop.t let compare = Prop.compare_prop end) (* =============== START of the PathSet module ===============*) module PathSet : sig type t val add_renamed_prop : Prop.normal Prop.t -> Path.t -> t -> t (** It's the caller's resposibility to ensure that Prop.prop_rename_primed_footprint_vars was called on the prop *) val d : t -> unit (** dump the pathset *) val diff : t -> t -> t (** difference between two pathsets *) val empty : t (** empty pathset *) val elements : t -> (Prop.normal Prop.t * Path.t) list (** list of elements in a pathset *) val equal : t -> t -> bool (** equality for pathsets *) val fold : (Prop.normal Prop.t -> Path.t -> 'a -> 'a) -> t -> 'a -> 'a (** fold over a pathset *) val from_renamed_list : (Prop.normal Prop.t * Path.t) list -> t (** It's the caller's resposibility to ensure that Prop.prop_rename_primed_footprint_vars was called on the list *) val is_empty : t -> bool (** check whether the pathset is empty *) val iter : (Prop.normal Prop.t -> Path.t -> unit) -> t -> unit (** iterate over a pathset *) val map : (Prop.normal Prop.t -> Prop.normal Prop.t) -> t -> t (** map over the prop component of a pathset *) val map_option : (Prop.normal Prop.t -> Prop.normal Prop.t option) -> t -> t (** map over the prop component of a pathset using a partial function; elements mapped to None are discarded *) val partition : (Prop.normal Prop.t -> bool) -> t -> t * t (** partition a pathset on the prop component *) val size : t -> int (** number of elements in the pathset *) val to_proplist : t -> Prop.normal Prop.t list (** convert to a list of props *) val to_propset : Tenv.t -> t -> Propset.t (** convert to a set of props *) val union : t -> t -> t (** union of two pathsets *) end = struct type t = Path.t PropMap.t let equal = PropMap.equal (fun _ _ -> true) (* only discriminate props, and ignore paths *) let empty : t = PropMap.empty let elements ps = let plist = ref [] in let f prop path = plist := (prop, path) :: !plist in PropMap.iter f ps ; !plist let to_proplist ps = List.map ~f:fst (elements ps) let to_propset tenv ps = Propset.from_proplist tenv (to_proplist ps) let partition f ps = let elements = ref [] in PropMap.iter (fun p _ -> elements := p :: !elements) ps ; let el1, el2 = (ref ps, ref ps) in List.iter ~f:(fun p -> if f p then el2 := PropMap.remove p !el2 else el1 := PropMap.remove p !el1) !elements ; (!el1, !el2) (** It's the caller's responsibility to ensure that [Prop.prop_rename_primed_footprint_vars] was called on the prop *) let add_renamed_prop (p: Prop.normal Prop.t) (path: Path.t) (ps: t) : t = let path_new = try let path_old = PropMap.find p ps in Path.join path_old path with Caml.Not_found -> path in PropMap.add p path_new ps let union (ps1: t) (ps2: t) : t = PropMap.fold add_renamed_prop ps1 ps2 (** check if the nodes in path p1 are a subset of those in p2 (not trace subset) *) let path_nodes_subset p1 p2 = let get_nodes p = Path.fold_all_nodes_nocalls p ~init:Procdesc.NodeSet.empty ~f:(fun s n -> Procdesc.NodeSet.add n s ) in Procdesc.NodeSet.subset (get_nodes p1) (get_nodes p2) (** difference between pathsets for the differential fixpoint *) let diff (ps1: t) (ps2: t) : t = let res = ref ps1 in let rem p path = try let path_old = PropMap.find p !res in if path_nodes_subset path path_old (* do not propagate new path if it has no new nodes *) then res := PropMap.remove p !res with Caml.Not_found -> res := PropMap.remove p !res in PropMap.iter rem ps2 ; !res let is_empty = PropMap.is_empty let iter = PropMap.iter let fold = PropMap.fold let map_option f ps = let res = ref empty in let do_elem prop path = match f prop with None -> () | Some prop' -> res := add_renamed_prop prop' path !res in iter do_elem ps ; !res let map f ps = map_option (fun p -> Some (f p)) ps let size ps = let res = ref 0 in let add _ _ = incr res in let () = PropMap.iter add ps in !res let pp pe fmt ps = let count = ref 0 in let pp_path fmt path = F.fprintf fmt "[path: %a@\n%a]" Path.pp_stats path Path.pp path in let f prop path = incr count ; F.fprintf fmt "PROP %d:%a@\n%a@\n" !count pp_path path (Prop.pp_prop pe) prop in iter f ps let d (ps: t) = let pp pe fmt ps = F.fprintf fmt "%a@\n" (pp pe) ps in L.add_print_with_pe pp ps (** It's the caller's resposibility to ensure that Prop.prop_rename_primed_footprint_vars was called on the list *) let from_renamed_list (pl: ('a Prop.t * Path.t) list) : t = List.fold ~f:(fun ps (p, pa) -> add_renamed_prop p pa ps) ~init:empty pl end (* =============== END of the PathSet module ===============*)