(* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) open! IStd open PulseBasicInterface module L = Logging type value = AbstractValue.t [@@deriving compare] type event = | ArrayWrite of {aw_array: value; aw_index: value} | Call of {return: value option; arguments: value list; procname: Procname.t} [@@deriving compare] let pp_comma_seq f xs = Pp.comma_seq ~print_env:Pp.text_break f xs let pp_event f = function | ArrayWrite {aw_array; aw_index} -> Format.fprintf f "@[ArrayWrite %a[%a]@]" AbstractValue.pp aw_array AbstractValue.pp aw_index | Call {return; arguments; procname} -> let procname = Procname.hashable_name procname (* as in [static_match] *) in Format.fprintf f "@[call@ %a=%s(%a)@]" (Pp.option AbstractValue.pp) return procname (pp_comma_seq AbstractValue.pp) arguments type vertex = ToplAutomaton.vindex [@@deriving compare] type register = ToplAst.register_name [@@deriving compare] type configuration = {vertex: vertex; memory: (register * value) list} [@@deriving compare] type substitution = (AbstractValue.t * ValueHistory.t) AbstractValue.Map.t type 'a substitutor = substitution * 'a -> substitution * 'a let sub_value (sub, value) = match AbstractValue.Map.find_opt value sub with | Some (v, _history) -> (sub, v) | None -> let v = AbstractValue.mk_fresh () in let sub = AbstractValue.Map.add value (v, []) sub in (sub, v) let sub_list : 'a substitutor -> 'a list substitutor = fun sub_elem (sub, xs) -> let f (sub, xs) x = let sub, x = sub_elem (sub, x) in (sub, x :: xs) in let sub, xs = List.fold ~init:(sub, []) ~f xs in (sub, List.rev xs) module Constraint : sig type predicate type t [@@deriving compare] type operand = PathCondition.operand val make : Binop.t -> operand -> operand -> predicate val true_ : t val and_predicate : predicate -> t -> t val and_constr : t -> t -> t val and_n : t list -> t val normalize : t -> t val negate : t list -> t list (** computes ¬(c1∨...∨cm) as d1∨...∨dn, where n=|c1|x...x|cm| *) val eliminate_exists : keep:AbstractValue.Set.t -> t -> t (** quantifier elimination *) val size : t -> int val substitute : t substitutor val prune_path : t -> PathCondition.t -> PathCondition.t val pp : Format.formatter -> t -> unit end = struct type predicate = Binop.t * PathCondition.operand * PathCondition.operand [@@deriving compare] type t = predicate list [@@deriving compare] type operand = PathCondition.operand let make binop lhs rhs = (binop, lhs, rhs) let true_ = [] let is_trivially_true (predicate : predicate) = match predicate with | Eq, AbstractValueOperand l, AbstractValueOperand r when AbstractValue.equal l r -> true | _ -> false let and_predicate predicate constr = if is_trivially_true predicate then constr else predicate :: constr let and_constr constr_a constr_b = List.rev_append constr_a constr_b let and_n constraints = List.concat_no_order constraints let normalize constr = List.dedup_and_sort ~compare:compare_predicate constr let negate_predicate (predicate : predicate) : predicate = match predicate with | Eq, l, r -> (Ne, l, r) | Ne, l, r -> (Eq, l, r) | Ge, l, r -> (Lt, r, l) | Gt, l, r -> (Le, r, l) | Le, l, r -> (Gt, r, l) | Lt, l, r -> (Ge, r, l) | _ -> L.die InternalError "PulseTopl.negate_predicate should handle all outputs of ToplUtils.binop_to" let negate disjunction = IList.product (List.map ~f:(List.map ~f:negate_predicate) disjunction) let size constr = List.length constr let substitute_predicate (sub, predicate) = let avo x : PathCondition.operand = AbstractValueOperand x in match (predicate : predicate) with | op, AbstractValueOperand l, AbstractValueOperand r -> let sub, l = sub_value (sub, l) in let sub, r = sub_value (sub, r) in (sub, (op, avo l, avo r)) | op, AbstractValueOperand l, r -> let sub, l = sub_value (sub, l) in (sub, (op, avo l, r)) | op, l, AbstractValueOperand r -> let sub, r = sub_value (sub, r) in (sub, (op, l, avo r)) | _ -> (sub, predicate) let substitute = sub_list substitute_predicate let prune_path constr path_condition = let f path_condition (op, l, r) = let path_condition, _new_eqs = PathCondition.prune_binop ~negated:false op l r path_condition in path_condition in List.fold ~init:path_condition ~f constr let pp_predicate f (op, l, r) = Format.fprintf f "@[%a%a%a@]" PathCondition.pp_operand l Binop.pp op PathCondition.pp_operand r let pp = Pp.seq ~sep:"∧" pp_predicate let eliminate_exists ~keep constr = (* TODO(rgrigore): replace the current weak approximation *) let is_live_operand = PathCondition.( function LiteralOperand _ -> true | AbstractValueOperand v -> AbstractValue.Set.mem v keep) in let is_live_predicate (_op, l, r) = is_live_operand l && is_live_operand r in List.filter ~f:is_live_predicate constr end type predicate = Binop.t * PathCondition.operand * PathCondition.operand [@@deriving compare] type step = { step_location: Location.t ; step_predecessor: simple_state (** state before this step *) ; step_data: step_data } and step_data = SmallStep of event | LargeStep of (Procname.t * (* post *) simple_state) and simple_state = { pre: configuration (** at the start of the procedure *) ; post: configuration (** at the current program point *) ; pruned: Constraint.t (** path-condition for the automaton *) ; last_step: step option [@compare.ignore] (** for trace error reporting *) } [@@deriving compare] (* TODO: include a hash of the automaton in a summary to avoid caching problems. *) (* TODO: limit the number of simple_states to some configurable number (default ~5) *) type state = simple_state list let pp_mapping f (x, value) = Format.fprintf f "@[%s↦%a@]@," x AbstractValue.pp value let pp_memory f memory = Format.fprintf f "@[<2>[%a]@]" (pp_comma_seq pp_mapping) memory let pp_configuration f {vertex; memory} = Format.fprintf f "@[{ topl-config@;vertex=%d@;memory=%a }@]" vertex pp_memory memory let pp_simple_state f {pre; post; pruned} = Format.fprintf f "@[<2>{ topl-simple-state@;pre=%a@;post=%a@;pruned=(%a) }@]" pp_configuration pre pp_configuration post Constraint.pp pruned let pp_state f = Format.fprintf f "@[<2>[ %a ]@]" (pp_comma_seq pp_simple_state) let start () = let mk_simple_states () = let a = Topl.automaton () in let memory = List.map ~f:(fun r -> (r, AbstractValue.mk_fresh ())) (ToplAutomaton.registers a) in let configurations = let n = ToplAutomaton.vcount a in let f acc vertex = {vertex; memory} :: acc in IContainer.forto n ~init:[] ~f in List.map ~f:(fun c -> {pre= c; post= c; pruned= Constraint.true_; last_step= None}) configurations in if Topl.is_deep_active () then mk_simple_states () else (* Avoids work later *) [] let get env x = match List.Assoc.find env ~equal:String.equal x with | Some v -> v | None -> L.die InternalError "TOPL: Cannot find %s. Should be caught by static checks" x let set = List.Assoc.add ~equal:String.equal let eval_guard memory tcontext guard : Constraint.t = let operand_of_value (value : ToplAst.value) : PathCondition.operand = match value with | Constant (LiteralInt x) -> LiteralOperand (IntLit.of_int x) | Register reg -> AbstractValueOperand (get memory reg) | Binding v -> AbstractValueOperand (get tcontext v) in let conjoin_predicate pruned (predicate : ToplAst.predicate) = match predicate with | Binop (binop, l, r) -> let l = operand_of_value l in let r = operand_of_value r in let binop = ToplUtils.binop_to binop in Constraint.and_predicate (Constraint.make binop l r) pruned | Value v -> let v = operand_of_value v in let one = PathCondition.LiteralOperand IntLit.one in Constraint.and_predicate (Constraint.make Binop.Ne v one) pruned in List.fold ~init:Constraint.true_ ~f:conjoin_predicate guard let apply_action tcontext assignments memory = let apply_one memory (register, variable) = set memory register (get tcontext variable) in List.fold ~init:memory ~f:apply_one assignments type tcontext = (ToplAst.variable_name * AbstractValue.t) list let pp_tcontext f tcontext = Format.fprintf f "@[[%a]@]" (pp_comma_seq (Pp.pair ~fst:String.pp ~snd:AbstractValue.pp)) tcontext let static_match_array_write arr index label : tcontext option = match label.ToplAst.pattern with | ArrayWritePattern -> let v1, v2 = match label.ToplAst.arguments with | Some [v1; v2] -> (v1, v2) | _ -> L.die InternalError "Topl: #ArrayWrite should have exactly two arguments" in Some [(v1, arr); (v2, index)] | _ -> None let static_match_call return arguments procname label : tcontext option = let rev_arguments = List.rev arguments in let procname = Procname.hashable_name procname in let match_name () : bool = match label.ToplAst.pattern with | ProcedureNamePattern pname -> Str.string_match (Str.regexp pname) procname 0 | _ -> false in let match_args () : tcontext option = let match_formals formals : tcontext option = let bind ~init rev_formals = let f tcontext variable value = (variable, value) :: tcontext in match List.fold2 ~init ~f rev_formals rev_arguments with | Ok c -> Some c | Unequal_lengths -> None in match (List.rev formals, return) with | [], Some _ -> None | rev_formals, None -> bind ~init:[] rev_formals | r :: rev_formals, Some v -> bind ~init:[(r, v)] rev_formals in Option.value_map ~default:(Some []) ~f:match_formals label.ToplAst.arguments in if match_name () then match_args () else None (** Returns a list of transitions whose pattern matches (e.g., event type matches). Each match produces a tcontext (transition context), which matches transition-local variables to abstract values. *) let static_match event : (ToplAutomaton.transition * tcontext) list = let match_one transition = let f label = match event with | ArrayWrite {aw_array; aw_index} -> static_match_array_write aw_array aw_index label | Call {return; arguments; procname} -> static_match_call return arguments procname label in let tcontext_opt = Option.value_map ~default:(Some []) ~f transition.ToplAutomaton.label in L.d_printfln "@[<2>PulseTopl.static_match:@;transition %a@;event %a@;result %a@]" ToplAutomaton.pp_transition transition pp_event event (Pp.option pp_tcontext) tcontext_opt ; Option.map ~f:(fun tcontext -> (transition, tcontext)) tcontext_opt in ToplAutomaton.tfilter_map (Topl.automaton ()) ~f:match_one let is_unsat_cheap path_condition pruned = PathCondition.is_unsat_cheap (Constraint.prune_path pruned path_condition) let is_unsat_expensive path_condition pruned = let _path_condition, unsat, _new_eqs = PathCondition.is_unsat_expensive (Constraint.prune_path pruned path_condition) in unsat let drop_infeasible ?(expensive = false) path_condition state = let is_unsat = if expensive then is_unsat_expensive else is_unsat_cheap in let f {pruned} = not (is_unsat path_condition pruned) in List.filter ~f state let normalize_memory memory = List.sort ~compare:[%compare: register * value] memory let normalize_configuration {vertex; memory} = {vertex; memory= normalize_memory memory} let normalize_simple_state {pre; post; pruned; last_step} = { pre= normalize_configuration pre ; post= normalize_configuration post ; pruned= Constraint.normalize pruned ; last_step } let normalize_state state = List.map ~f:normalize_simple_state state let apply_conjuncts_limit state = let f simple_state = Constraint.size simple_state.pruned <= Config.topl_max_conjuncts in IList.filter_changed ~f state let apply_disjuncts_limit state = if List.length state <= Config.topl_max_disjuncts then state else let new_len = (Config.topl_max_disjuncts / 2) + 1 in let add_score simple_state = (Constraint.size simple_state.pruned, simple_state) in let compare_score (score1, _simple_state1) (score2, _simple_state2) = Int.compare score1 score2 in let strip_score (_score, simple_state) = simple_state in state |> List.map ~f:add_score |> List.sort ~compare:compare_score |> Fn.flip List.take new_len |> List.map ~f:strip_score let apply_limits state = state |> apply_conjuncts_limit |> apply_disjuncts_limit let small_step loc path_condition event simple_states = let simple_states = apply_limits simple_states in let tmatches = static_match event in let evolve_transition (old : simple_state) (transition, tcontext) : state = let mk ?(memory = old.post.memory) ?(pruned = Constraint.true_) significant = let last_step = if significant then Some {step_location= loc; step_predecessor= old; step_data= SmallStep event} else old.last_step in (* NOTE: old pruned is discarded, because evolve_simple_state needs to see only new prunes to determine skip transitions. It will then add back old prunes. *) let post = {vertex= transition.ToplAutomaton.target; memory} in {old with post; pruned; last_step} in match transition.ToplAutomaton.label with | None -> (* "any" transition *) let is_loop = Int.equal transition.ToplAutomaton.source transition.ToplAutomaton.target in [mk (not is_loop)] | Some label -> let memory = old.post.memory in let pruned = eval_guard memory tcontext label.ToplAst.condition in let memory = apply_action tcontext label.ToplAst.action memory in [mk ~memory ~pruned true] in let evolve_simple_state old = let path_condition = Constraint.prune_path old.pruned path_condition in let tmatches = List.filter ~f:(fun (t, _) -> Int.equal old.post.vertex t.ToplAutomaton.source) tmatches in let nonskip = drop_infeasible path_condition (List.concat_map ~f:(evolve_transition old) tmatches) in let skip = let nonskip_disjunction = List.map ~f:(fun {pruned} -> pruned) nonskip in let skip_disjunction = Constraint.negate nonskip_disjunction in let f pruned = {old with pruned} (* keeps last_step from old *) in drop_infeasible path_condition (List.map ~f skip_disjunction) in let add_old_pruned s = {s with pruned= Constraint.and_constr s.pruned old.pruned} in List.map ~f:add_old_pruned (List.rev_append nonskip skip) in let result = List.concat_map ~f:evolve_simple_state simple_states in L.d_printfln "@[<2>PulseTopl.small_step:@;%a@ -> %a@]" pp_state simple_states pp_state result ; result let of_unequal (or_unequal : 'a List.Or_unequal_lengths.t) = match or_unequal with | Ok x -> x | Unequal_lengths -> L.die InternalError "PulseTopl expected lists to be of equal lengths" let sub_configuration (sub, {vertex; memory}) = let keys, values = List.unzip memory in let sub, values = sub_list sub_value (sub, values) in let memory = of_unequal (List.zip keys values) in (sub, {vertex; memory}) (* Does not substitute in [last_step]: not usually needed, and takes much time. *) let sub_simple_state (sub, {pre; post; pruned; last_step}) = let sub, pre = sub_configuration (sub, pre) in let sub, post = sub_configuration (sub, post) in let sub, pruned = Constraint.substitute (sub, pruned) in (sub, {pre; post; pruned; last_step}) let large_step ~call_location ~callee_proc_name ~substitution ~condition ~callee_prepost state = let state = apply_limits state in let seq ((p : simple_state), (q : simple_state)) = if not (Int.equal p.post.vertex q.pre.vertex) then None else let substitution, new_eqs = (* Update the substitution, matching formals with actuals. We work a bit to avoid introducing equalities, because a growing [pruned] leads to quadratic behaviour. *) let mk_eq val1 val2 = let op x = PathCondition.AbstractValueOperand x in Constraint.make Binop.Eq (op val1) (op val2) in let f (sub, eqs) (reg1, val1) (reg2, val2) = if not (String.equal reg1 reg2) then L.die InternalError "PulseTopl: normalized memories are expected to have the same domain" else match AbstractValue.Map.find_opt val2 sub with | Some (old_val1, _history) -> if AbstractValue.equal old_val1 val1 then (sub, eqs) else (sub, Constraint.and_predicate (mk_eq old_val1 val1) eqs) | None -> (AbstractValue.Map.add val2 (val1, []) sub, eqs) in of_unequal (List.fold2 p.post.memory q.pre.memory ~init:(substitution, Constraint.true_) ~f) in let _substitution, q = sub_simple_state (substitution, q) in let pruned = Constraint.and_n [new_eqs; q.pruned; p.pruned] in let last_step = Some { step_location= call_location ; step_predecessor= p ; step_data= LargeStep (callee_proc_name, q) } in Some {pre= p.pre; post= q.post; pruned; last_step} in (* TODO(rgrigore): may be worth optimizing the cartesian_product *) let state = normalize_state state in let callee_prepost = normalize_state callee_prepost in let new_state = List.filter_map ~f:seq (List.cartesian_product state callee_prepost) in let result = drop_infeasible condition new_state in L.d_printfln "@[<2>PulseTopl.large_step:@;callee_prepost=%a@;%a@ -> %a@]" pp_state callee_prepost pp_state state pp_state result ; result let filter_for_summary path_condition state = drop_infeasible ~expensive:true path_condition state let simplify ~keep state = let simplify_simple_state {pre; post; pruned; last_step} = (* NOTE(rgrigore): registers could be considered live for the program path_condition as well. That should improve precision, but I'm wary of altering what the Pulse program state is just because Topl is enabled. *) let collect memory keep = List.fold ~init:keep ~f:(fun keep (_reg, value) -> AbstractValue.Set.add value keep) memory in let keep = keep |> collect pre.memory |> collect post.memory in let pruned = Constraint.eliminate_exists ~keep pruned in {pre; post; pruned; last_step} in let state = List.map ~f:simplify_simple_state state in List.dedup_and_sort ~compare:compare_simple_state state let description_of_step_data step_data = ( match step_data with | SmallStep (Call {procname}) | LargeStep (procname, _) -> Format.fprintf Format.str_formatter "@[call to %a@]" Procname.pp procname | SmallStep (ArrayWrite _) -> Format.fprintf Format.str_formatter "@[write to array@]" ) ; Format.flush_str_formatter () let report_errors proc_desc err_log state = let a = Topl.automaton () in let rec make_trace nesting trace q = match q.last_step with | None -> trace | Some {step_location; step_predecessor; step_data} -> let description = description_of_step_data step_data in let trace = let trace_element = Errlog.make_trace_element nesting step_location description [] in match step_data with | SmallStep _ -> trace_element :: trace | LargeStep (_, {last_step= None}) -> trace (* skip trivial large steps (i.e., those with no steps) *) | LargeStep (_, qq) -> trace_element :: make_trace (nesting + 1) trace qq in make_trace nesting trace step_predecessor in let rec first_error_ss q = match q.last_step with | Some {step_predecessor} -> if not (ToplAutomaton.is_error a step_predecessor.post.vertex) then q else first_error_ss step_predecessor | None -> L.die InternalError "PulseTopl.report_errors inv broken" in let is_nested_large_step q = match q.last_step with | Some {step_data= LargeStep (_, prepost)} when ToplAutomaton.is_start a prepost.pre.vertex && ToplAutomaton.is_error a prepost.post.vertex -> true | _ -> false in let report_simple_state q = if ToplAutomaton.is_start a q.pre.vertex && ToplAutomaton.is_error a q.post.vertex then let q = first_error_ss q in (* Only report at the innermost level where error appears. *) if not (is_nested_large_step q) then let loc = Procdesc.get_loc proc_desc in let ltr = make_trace 0 [] q in let message = Format.asprintf "%a" ToplAutomaton.pp_message_of_state (a, q.post.vertex) in Reporting.log_issue proc_desc err_log ~loc ~ltr ToplOnPulse IssueType.topl_pulse_error message in List.iter ~f:report_simple_state state