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.

1046 lines
39 KiB

(*
* 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.
*)
(** The analysis' semantics of control flow. *)
open Domain_intf
open Control_intf
(** An element of work to be scheduled. The scheduling strategies have very
few dependencies on elements, mainly some just need to test if two
elements have the same destination. An example element instance is a
pair of a control-flow edge with a symbolic state that has reached the
source of the edge and has yet to be propagated to the destination. *)
module type Elt = sig
type t [@@deriving compare, equal, sexp_of]
val pp : t pp
val equal_destination : t -> t -> bool
val dnf : t -> t list
end
(** Interface of analysis control scheduler "queues". *)
module type QueueS = sig
type elt
(** a "queue" of elements, which need not be FIFO *)
type t
val pp : t pp
val create : unit -> t
(** create an empty queue *)
val add : elt -> t -> t
(** add an element *)
val top : t -> (elt * elt list * t) option
(** [top q] is [None] if [q] is empty and otherwise is [Some (e, es, q')]
where [e] is the selected element in [q] and any elements [es] have
the same destination as [e]. [q'] is equivalent to [q] but possibly
more compactly represented. *)
val remove : elt -> elt list -> t -> t
end
(** Type of a queue implementation, which is parameterized over elements. *)
module type Queue = functor (Elt : Elt) -> QueueS with type elt = Elt.t
(** A strategy that implements an iterative breadth-first exploration that
joins scheduled elements whenever possible, thereby DAGifying the
execution tree. *)
module PriorityQueue (Elt : Elt) : QueueS with type elt = Elt.t = struct
type elt = Elt.t
module Elts = Set.Make (Elt)
type t = {queue: Elt.t FHeap.t; removed: Elts.t}
let pp ppf {queue; removed} =
let rev_elts =
FHeap.fold queue ~init:[] ~f:(fun rev_elts elt ->
if Elts.mem elt removed then rev_elts else elt :: rev_elts )
in
Format.fprintf ppf "@[%a@]" (List.pp " ::@ " Elt.pp) (List.rev rev_elts)
let create () = {queue= FHeap.create ~cmp:Elt.compare; removed= Elts.empty}
let add elt {queue; removed} =
let removed' = Elts.remove elt removed in
if removed' == removed then {queue= FHeap.add queue elt; removed}
else {queue; removed= removed'}
let rec top {queue; removed} =
let* next = FHeap.top queue in
let removed' = Elts.remove next removed in
if removed' != removed then
let queue' = FHeap.remove_top_exn queue in
top {queue= queue'; removed= removed'}
else
let elts =
FHeap.fold queue ~init:[] ~f:(fun elts elt ->
if Elt.equal_destination next elt && not (Elts.mem elt removed)
then elt :: elts
else elts )
in
Some (next, elts, {queue; removed})
let remove top elts {queue; removed} =
assert (Elt.equal top (FHeap.top_exn queue)) ;
let queue = FHeap.remove_top_exn queue in
let removed = Elts.add_list elts removed in
{queue; removed}
end
module RandomQueue (Elt : Elt) : QueueS with type elt = Elt.t = struct
type elt = Elt.t
module M = Int.Map
(** The analyzer, after calling [create], performs a sequence of [add] and
[pop] operations. Implicitly, each [add] is for an element that is a
successor of the element that was returned by the last [pop]. This
module assumes this implicit protocol in order to infer the structure
of the execution tree, and uses it to assign weights aiming to
implement fair random sampling of paths.
Each edge of an execution tree conceptually has a "weight" [1/w]
(represented by just the denominator [w]) indicating that there is a
[1] in [w] chance of making the sequence of branching choices leading
to the edge. An execution tree starts with a single edge to the
initial control point with weight [1]. For an edge with weight [1/w]
that has [n] successors, the edge to each of the successors has weight
[1 / (w * n)].
The scheduling "frontier" is a set of edges with weights (represented
as a map from weights to lists of edges) that have been reached but
not followed.
Edges are selected from the frontier randomly using the weights to
simulate fair sampling of paths as follows. Let [{eᵢ}] be a sequence
of the edges of the frontier in decreasing weight order. The weight of
edge [eᵢ] is written [wᵢ]. The sum of the weights of the frontier
is [s = Σᵢ wᵢ]. Now choose a random number [0 ≤ n ≤ s]. This
determines an edge [eᵣ] where [r] is least such that
[Σᵢ₌₀ʳ wᵢ ≥ n].
The inferred structure of the execution tree is also used to schedule
the analysis to proceed depth-first where a random successor is chosen
at each point until no further progress is possible, at which point a
new path is sampled. Successors are added by the analyzer prior to
knowing whether they are feasible. For example executing a conditional
branch results in two [add] operations where the next instruction on
each is to assume the condition and the negation of the condition. To
avoid depth-first execution from being thwarted by choosing infeasible
branches in such cases, a [recent] list is maintained that contains
the successors of the last popped element. When an element is popped
from the recent list, it is not known whether or not it is immediately
infeasible. If it is, the next operation will be another [pop], and
this is also taken from the [recent] list. If the element was not
immediately infeasible, the next operation is an [add] (of a
successor), at which point the recent list is flushed to the
"frontier". In this way, each [pop] that requests the next branch to
explore is chosen from the successors of the last control point,
effecting depth-first exploration. Only when the recent list is empty,
is an element chosen from the "frontier" of untaken branches. *)
type t =
{ recent: Elt.t RAL.t (** elements added since last pop; add *)
; recent_weight: int (** combined weight of recent *)
; frontier: Elt.t RAL.t M.t (** weight-keyed elements to be explored *)
; frontier_weight: float (** combined weight of frontier *)
; last: last_operation (** single step of execution history *) }
and last_operation =
| Add_or_pop_frontier
(** last operation was either [add] or [pop] where [recent] was
empty *)
| Pop_recent of int
(** last operation was [pop] where [recent] was not empty, and the
returned element had given weight *)
let pp ppf {recent; frontier} =
Format.fprintf ppf "@[%a @@@ %a@]" (List.pp " ::@ " Elt.pp)
(RAL.to_list recent)
(M.pp Int.pp (RAL.pp " ::@ " Elt.pp))
frontier
let create () =
{ recent= RAL.empty
; recent_weight= 1
; frontier= M.empty
; frontier_weight= 0.
; last= Add_or_pop_frontier }
let add elt q =
let add_elt l = List.fold ~f:RAL.cons (Elt.dnf elt) l in
match q.last with
| Add_or_pop_frontier ->
(* elt is a sibling of the elements of recent, so extend recent *)
{q with recent= add_elt q.recent}
| Pop_recent elt_weight ->
(* elt is a successor of the last popped element (which is itself a
sibling of the elements of recent), so flush recent to frontier
and reset recent to the singleton elt with a combined weight
equal to that of the previously popped element *)
{ recent= add_elt RAL.empty
; recent_weight= elt_weight
; frontier=
( if RAL.is_empty q.recent then q.frontier
else
M.update elt_weight q.frontier ~f:(function
| Some data -> Some (RAL.append q.recent data)
| None -> Some q.recent ) )
; frontier_weight=
q.frontier_weight
+. Float.of_int (RAL.length q.recent)
/. Float.of_int elt_weight
; last= Add_or_pop_frontier }
let _pop q =
let num_recent = RAL.length q.recent in
if num_recent > 0 then
let elt, recent =
RAL.get_and_remove_exn q.recent (Random.int num_recent)
in
match q.last with
| Pop_recent _ ->
(* elt is sibling to last popped element, with same elt_weight *)
Some (elt, [], {q with recent})
| Add_or_pop_frontier ->
(* recent is now complete, and weight of each element can be
computed from combined weight and length *)
let elt_weight = q.recent_weight * num_recent in
Some (elt, [], {q with recent; last= Pop_recent elt_weight})
else
let random_weight = Random.float q.frontier_weight in
M.fold_until q.frontier 0.
~f:(fun ~key ~data prefix_weight ->
let len = RAL.length data in
let w = Float.of_int len /. Float.of_int key in
let prefix_weight = prefix_weight +. w in
if Float.(prefix_weight < random_weight) then
`Continue prefix_weight
else
let elt, data = RAL.get_and_remove_exn data (Random.int len) in
`Stop
(Some
( elt
, []
, { recent= RAL.empty
; recent_weight= key
; frontier=
( if RAL.is_empty data then M.remove key q.frontier
else M.add ~key ~data q.frontier )
; frontier_weight= q.frontier_weight -. w
; last= Add_or_pop_frontier } )) )
~finish:(fun _ ->
assert (M.is_empty q.frontier) ;
None )
let top _ = todo "concurrent sampling analysis" ()
let remove _ = todo "concurrent sampling analysis" ()
end
module Make (Config : Config) (D : Domain) (Queue : Queue) = struct
module Stack : sig
type t
val empty : t
val push_call : Llair.func Llair.call -> D.from_call -> t -> t
val pop_return : t -> (D.from_call * Llair.jump * t) option
val pop_throw :
t
-> 'a
-> unwind:
( Llair.Reg.t iarray
-> Llair.Reg.Set.t
-> D.from_call
-> 'a
-> 'a)
-> (D.from_call * Llair.jump * t * 'a) option
type as_inlined_location = t [@@deriving compare, equal, sexp_of]
end = struct
type t =
| Return of
{ dst: Llair.Jump.t
; formals: Llair.Reg.t iarray
; locals: Llair.Reg.Set.t
; from_call: D.from_call
; stk: t }
| Throw of Llair.Jump.t * t
| Empty
[@@deriving sexp_of]
let rec pp ppf stk =
let pp ppf = function
| Empty -> ()
| stk -> Format.fprintf ppf "; %a" pp stk
in
match stk with
| Return {dst; stk= s} ->
Format.fprintf ppf "R#%i%a" dst.dst.sort_index pp s
| Throw (dst, s) ->
Format.fprintf ppf "T#%i%a" dst.dst.sort_index pp s
| Empty -> ()
let invariant s =
let@ () = Invariant.invariant [%here] s [%sexp_of: t] in
match s with
| Return _ | Throw (_, Return _) | Empty -> ()
| Throw _ -> fail "malformed stack: %a" pp s ()
let empty = Empty |> check invariant
let push_return call from_call stk =
let Llair.{callee= {formals; locals}; return; _} = call in
Return {dst= return; formals; locals; from_call; stk}
|> check invariant
let push_throw call stk =
( match call.Llair.throw with
| None -> stk
| Some jmp -> Throw (jmp, stk) )
|> check invariant
let push_call call from_call stk =
push_throw call (push_return call from_call stk)
let rec pop_return = function
| Throw (_, stk) -> pop_return stk
| Return {from_call; dst; stk} -> Some (from_call, dst, stk)
| Empty -> None
let pop_throw stk state ~unwind =
let rec pop_throw_ state = function
| Return {formals; locals; from_call; stk} ->
pop_throw_ (unwind formals locals from_call state) stk
| Throw (dst, Return {from_call; stk}) ->
Some (from_call, dst, stk, state)
| Empty -> None
| Throw _ as stk -> violates invariant stk
in
pop_throw_ state stk
type as_inlined_location = t [@@deriving sexp_of]
(* Treat a stack as a code location in a hypothetical expansion of the
program where functions have been inlined. In particular, only the
dst and stk of Return frames is considered. *)
let rec compare_as_inlined_location x y =
if x == y then 0
else
match (x, y) with
| Return {dst= j; stk= x}, Return {dst= k; stk= y} -> (
match Llair.Jump.compare j k with
| 0 -> compare_as_inlined_location x y
| n -> n )
| Return _, _ -> -1
| _, Return _ -> 1
| Throw (j, x), Throw (k, y) -> (
match Llair.Jump.compare j k with
| 0 -> compare_as_inlined_location x y
| n -> n )
| Throw _, _ -> -1
| _, Throw _ -> 1
| Empty, Empty -> 0
let equal_as_inlined_location = [%compare.equal: as_inlined_location]
end
(** Instruction Pointer, of a single thread so includes thread id.
Functions are treated as-if-inlined by including a call stack in each
instruction pointer, effectively copying the control-flow graph for
each calling context. *)
type ip = {ip: Llair.IP.t; stk: Stack.t; tid: ThreadID.t}
(** Instruction Pointer of a single thread *)
module IP : sig
type t = ip [@@deriving compare, equal, sexp_of]
val pp : t pp
end = struct
type t = ip =
{ip: Llair.IP.t; stk: Stack.as_inlined_location; tid: ThreadID.t}
[@@deriving compare, equal, sexp_of]
let pp ppf {ip} = Llair.IP.pp ppf ip
end
(** Representation of a single thread, including identity and scheduling
state *)
module Thread : sig
type t = Runnable of IP.t | Terminated of ThreadID.t
[@@deriving equal, sexp_of]
val compare : t Ord.t
val compare_without_tid : t Ord.t
val pp : t pp
val id : t -> ThreadID.t
end = struct
(** Because [ip] needs to include [tid], this is represented as a sum of
products, but it may be more natural to think in terms of the
isomorphic representation using a product of a sum such as
[(Runnable of ... | Terminated ...) * ThreadID.t]. *)
type t = Runnable of IP.t | Terminated of ThreadID.t
[@@deriving sexp_of]
let pp ppf = function
| Runnable ip -> IP.pp ppf ip
| Terminated tid -> Format.fprintf ppf "T%i" tid
let id = function Runnable {tid} -> tid | Terminated tid -> tid
(* Note: Threads.inactive relies on comparing tid last *)
let compare_aux compare_tid x y =
let open Ord.Infix in
if x == y then 0
else
match (x, y) with
| Runnable x, Runnable y ->
Llair.IP.compare x.ip y.ip
<?> (Stack.compare_as_inlined_location, x.stk, y.stk)
<?> (compare_tid, x.tid, y.tid)
| Runnable _, _ -> -1
| _, Runnable _ -> 1
| Terminated x_tid, Terminated y_tid -> compare_tid x_tid y_tid
let compare = compare_aux ThreadID.compare
let equal = [%compare.equal: t]
let compare_without_tid = compare_aux (fun _ _ -> 0)
end
(** Set of threads *)
module Threads : sig
type t [@@deriving compare, equal, sexp_of]
type inactive [@@deriving sexp_of]
val compare_inactive : inactive Ord.t
val compare_inactive_tids : inactive Ord.t
val init : t
val create : Llair.block -> t -> ThreadID.t * t
val after_step : Thread.t -> t -> t * inactive
val join : ThreadID.t -> t -> t option
val fold : t -> 's -> f:(Thread.t -> 's -> 's) -> 's
end = struct
module M = Map.Make (ThreadID)
type t = Thread.t M.t [@@deriving compare, equal, sexp_of]
type inactive = Thread.t array [@@deriving sexp_of]
let compare_inactive = Ord.array Thread.compare_without_tid
let compare_inactive_tids = Ord.(array (ThreadID.compare >|= Thread.id))
let inactive active_id threads =
let a = Iter.to_array (M.values (M.remove active_id threads)) in
Array.sort ~cmp:Thread.compare a ;
a
let init = M.empty
let fold threads s ~f =
M.fold threads s ~f:(fun ~key:_ ~data s -> f data s)
let create entry threads =
let ip = Llair.IP.mk entry in
let max_tid =
match M.max_binding threads with
| Some (tid, _) -> tid
| None -> ThreadID.init
in
let tid = max_tid + 1 in
let thread = Thread.Runnable {ip; stk= Stack.empty; tid} in
(tid, M.add ~key:tid ~data:thread threads)
let after_step active threads =
let tid = Thread.id active in
let inactive = inactive tid threads in
let threads = M.add ~key:tid ~data:active threads in
(threads, inactive)
let join tid threads =
match M.find tid threads with
| Some (Thread.Terminated _) -> Some (M.remove tid threads)
| _ -> None
end
(** A control-flow transition of a single thread. In the common case an
edge from block [src] to [dst = Runnable {ip; stk; tid}] represents a
transition by thread [tid] with call stack [stk] from (usually the
terminator of) block [src] to the instruction pointer [ip]. If a
scheduling point is encountered within a block, the represented
transition need not originate from the terminator of [src]. Edges can
also represent transitions that produce threads in non-[Runnable]
scheduling states, determined by the form of [dst]. *)
type edge = {dst: Thread.t; src: Llair.Block.t} [@@deriving sexp_of]
module Edge = struct
type t = edge [@@deriving sexp_of]
let pp fs {dst; src= {sort_index; lbl}} =
Format.fprintf fs "%a <-t%i- #%i %%%s" Thread.pp dst (Thread.id dst)
sort_index lbl
(** Each retreating edge has a depth for each calling context, except
for recursive calls. Recursive call edges are instead compared
without considering their stacks. Bounding the depth of edges
therefore has the effect of bounding the number of recursive calls
in any calling context. *)
let compare_aux compare_tid x y =
let open Ord.Infix in
if x == y then 0
else
match (x, y) with
| {dst= Runnable x_t}, {dst= Runnable y_t} ->
let is_rec_call = function
| {Llair.term= Call {recursive= true}} -> true
| _ -> false
in
let compare_stk stk1 stk2 =
if is_rec_call x.src then 0
else Stack.compare_as_inlined_location stk1 stk2
in
Llair.IP.compare x_t.ip y_t.ip
<?> (Llair.Block.compare, x.src, y.src)
<?> (compare_stk, x_t.stk, y_t.stk)
<?> (compare_tid, x_t.tid, y_t.tid)
| {dst= Runnable _}, _ -> -1
| _, {dst= Runnable _} -> 1
| {dst= Terminated x_tid}, {dst= Terminated y_tid} ->
Llair.Block.compare x.src y.src <?> (compare_tid, x_tid, y_tid)
let compare = compare_aux ThreadID.compare
let equal = [%compare.equal: t]
let compare_without_tid = compare_aux (fun _ _ -> 0)
end
module Depths = struct
module M = Map.Make (Edge)
type t = int M.t [@@deriving compare, equal, sexp_of]
let empty = M.empty
let find = M.find
let add = M.add
let join x y =
M.merge x y ~f:(fun _ -> function
| `Left d | `Right d -> Some d
| `Both (d1, d2) -> Some (Int.max d1 d2) )
end
type switches = int [@@deriving compare, equal, sexp_of]
(** Abstract memory, control, and history state, with a slot used for the
current "control position", such as an instruction pointer. Consists
of a symbolic [state] and a scheduling state of the [threads], plus a
coarse abstraction of the preceding execution history in the form of
the number of context [switches] and [depths] representing the number
of times retreating edges have been crossed. *)
type 'a memory_control_history =
{ ctrl: 'a (** current control position *)
; state: D.t (** symbolic memory and register state *)
; threads: Threads.t (** scheduling state of the threads *)
; switches: switches (** count of preceding context switches *)
; depths: Depths.t (** count of retreating edge crossings *) }
[@@deriving sexp_of]
(** An abstract machine state consists of the instruction pointer of the
active thread plus the memory, control, and history state. *)
type ams = IP.t memory_control_history [@@deriving sexp_of]
(** A unit of analysis work is an abstract machine state from which
execution should continue, with additional control-flow [edge] info
used by the analysis scheduler. *)
type work = edge memory_control_history
(** An element of the frontier of execution is a control-flow [edge] that
has been executed by the thread [Thread.id edge.dst], yielding a
memory, control, and history state. The [threads] indicates the
scheduling state of the point after the transition indicated by the
edge. *)
type elt = elt_ctrl memory_control_history [@@deriving sexp_of]
and elt_ctrl =
{ edge: Edge.t
; depth: int
(** pre-computed depth of [edge], for use by e.g. [Elt.compare] *)
; inactive: Threads.inactive
(** pre-computed summary of inactive thread scheduling states, for
use by e.g. [Elt.compare] *) }
module Work : sig
type t
val init : D.t -> Llair.block -> t
val add : retreating:bool -> work -> t -> t
val run : f:(ams -> t -> t) -> t -> unit
end = struct
(** Element of the frontier of execution, ordered for scheduler's
priority queue *)
module Elt = struct
type t = elt [@@deriving sexp_of]
let pp ppf {ctrl= {edge; depth}; switches} =
Format.fprintf ppf "%i,%i: %a" switches depth Edge.pp edge
let compare x y =
let open Ord.Infix in
if x == y then 0
else
( (Int.compare >|= fun x -> x.switches)
@? (Int.compare >|= fun x -> x.ctrl.depth)
@? (Edge.compare_without_tid >|= fun x -> x.ctrl.edge)
@? (Threads.compare_inactive >|= fun x -> x.ctrl.inactive)
@? (ThreadID.compare >|= fun x -> Thread.id x.ctrl.edge.dst)
@? (Threads.compare_inactive_tids >|= fun x -> x.ctrl.inactive)
@? (Depths.compare >|= fun x -> x.depths)
@? (D.compare >|= fun x -> x.state) )
x y
let equal = [%compare.equal: t]
let equal_destination x y = Threads.equal x.threads y.threads
let dnf x = List.map ~f:(fun state -> {x with state}) (D.dnf x.state)
end
module Queue = Queue (Elt)
(** Concurrent state and history projection of abstract machine states.
Abstract machine states with the same [switches], [ip], and
[threads] fields have, as far as the scheduler is concerned, the
same concurrent state and history and can be joined. *)
module ConcSH = struct
module T = struct
type t = switches * IP.t * Threads.t
[@@deriving compare, equal, sexp_of]
end
include T
module Map = Map.Make (T)
end
(** Sequential state and history projection of abstract machine states.
Complementary to [ConcSH], [SeqSH] represents the subset of [ams]
fields that can be joined across several executions that share the
same abstract concurrent state and history. *)
module SeqSH = struct
module T = struct
type t = D.t * Depths.t [@@deriving compare, equal, sexp_of]
end
include T
module Set = Set.Make (T)
let join s =
let states, depths =
Set.fold s ([], Depths.empty) ~f:(fun (q, d) (qs, ds) ->
let qqs =
match qs with
| q0 :: _ when D.equal q q0 -> qs
| _ -> q :: qs
in
(qqs, Depths.join d ds) )
in
(D.joinN states, depths)
end
(** Sequential states indexed by concurrent states. When sequential
states and histories are joined across executions that reach the
same abstract concurrent state and history, there are multiple
successor executions corresponding to which thread is selected to
execute. Executing some such successors can lead to additional
executions that reach the original abstract concurrent state and
history. These new executions also need to be joined with the old
ones. To handle this, the successors of a join are enumerated
lazily, returning them one-by-one from the scheduler and adding them
to the analysis worklist. The "cursor" that maintains the current
progress of this enumeration is a set of sequential states that is
indexed by concurrent states. *)
module Cursor = struct
type t = SeqSH.Set.t ConcSH.Map.t
let empty = ConcSH.Map.empty
let add = ConcSH.Map.add
let find = ConcSH.Map.find
end
(** Analysis exploration state *)
type t = Queue.t * Cursor.t
let prune depth {ctrl= edge} wl =
[%Trace.info " %i: %a" depth Edge.pp edge] ;
Report.hit_bound Config.bound ;
wl
let enqueue depth ({ctrl= {dst} as edge; threads; depths} as elt)
(queue, cursor) =
[%Trace.info
" %i,%i: %a@ | %a" elt.switches depth Edge.pp edge Queue.pp queue] ;
let depths = Depths.add ~key:edge ~data:depth depths in
let threads, inactive = Threads.after_step dst threads in
let queue =
Queue.add
{elt with ctrl= {edge; depth; inactive}; threads; depths}
queue
in
(queue, cursor)
let init state curr =
let depth = 0 in
let ip = Llair.IP.mk curr in
let stk = Stack.empty in
let prev = curr in
let tid = ThreadID.init in
let edge = {dst= Runnable {ip; stk; tid}; src= prev} in
let threads = Threads.init in
let switches = 0 in
let depths = Depths.empty in
let queue = Queue.create () in
let cursor = Cursor.empty in
enqueue depth
{ctrl= edge; state; threads; switches; depths}
(queue, cursor)
let add ~retreating ({ctrl= edge; depths} as elt) wl =
let depth = Option.value (Depths.find edge depths) ~default:0 in
let depth = if retreating then depth + 1 else depth in
if depth > Config.bound && Config.bound >= 0 then prune depth elt wl
else enqueue depth elt wl
module Succs = struct
module M = ConcSH.Map
let empty = M.empty
let add = M.add_multi
let find_first m ~f =
let exception Stop in
let found = ref None in
let hit_end = ref true in
( try
M.iteri m ~f:(fun ~key ~data ->
match !found with
| None -> (
match f ~key ~data with
| None -> ()
| Some r -> found := Some r )
| Some _ ->
hit_end := false ;
raise_notrace Stop )
with Stop -> () ) ;
(!found, !hit_end)
end
let rec dequeue (queue, cursor) =
let* ({threads} as top), elts, queue = Queue.top queue in
let succs =
List.fold (top :: elts) Succs.empty ~f:(fun incoming succs ->
let {ctrl= {edge= {dst}}; state; switches; depths} = incoming in
let incoming_tid = Thread.id dst in
Threads.fold threads succs ~f:(fun active succs ->
match active with
| Terminated _ -> succs
| Runnable ({tid} as ip) ->
let switches =
if tid = incoming_tid then switches else switches + 1
in
Succs.add ~key:(switches, ip, threads)
~data:(state, depths) succs ) )
in
let found, hit_end =
Succs.find_first succs
~f:(fun ~key:(switches, ip, threads) ~data:incoming ->
let next = (switches, ip, threads) in
let curr = SeqSH.Set.of_list incoming in
let+ next_states =
match Cursor.find next cursor with
| Some already_done ->
let next_states = SeqSH.Set.diff curr already_done in
if SeqSH.Set.is_empty next_states then None
else Some next_states
| None -> Some curr
in
( next
, next_states
, Cursor.add ~key:next ~data:next_states cursor ) )
in
let queue = if hit_end then Queue.remove top elts queue else queue in
match found with
| Some ((switches, ip, threads), next_states, cursor) ->
[%Trace.info
" %i,%i: %a@ | %a" switches top.ctrl.depth Edge.pp top.ctrl.edge
Queue.pp queue] ;
let state, depths = SeqSH.join next_states in
Some
({ctrl= ip; state; threads; switches; depths}, (queue, cursor))
| None -> dequeue (queue, cursor)
let rec run ~f wl =
match dequeue wl with
| Some (ams, wl) -> run ~f (f ams wl)
| None -> ()
end
let summary_table = Llair.Function.Tbl.create ()
let pp_st () =
[%Trace.printf
"@[<v>%t@]" (fun fs ->
Llair.Function.Tbl.iteri summary_table ~f:(fun ~key ~data ->
Format.fprintf fs "@[<v>%a:@ @[%a@]@]@ " Llair.Function.pp key
(List.pp "@," D.pp_summary)
data ) )]
let exec_jump jump ({ctrl= {ip; stk; tid}} as ams) wl =
let src = Llair.IP.block ip in
let {Llair.dst; retreating} = jump in
let ip = Llair.IP.mk dst in
let edge = {dst= Runnable {ip; stk; tid}; src} in
Work.add ~retreating {ams with ctrl= edge} wl
let exec_skip_func areturn return ({ctrl= {ip; tid}; state} as ams) wl =
Report.unknown_call (Llair.IP.block ip).term ;
let state = Option.fold ~f:(D.exec_kill tid) areturn state in
exec_jump return {ams with state} wl
let exec_call globals call ({ctrl= {stk; tid}; state} as ams) wl =
let Llair.{callee; actuals; areturn; return; recursive} = call in
let Llair.{name; formals; freturn; locals; entry} = callee in
[%Trace.call fun {pf} ->
pf " t%i@[<2>@ %a from %a with state@]@;<1 2>%a" tid
Llair.Func.pp_call call Llair.Function.pp return.dst.parent.name
D.pp state]
;
let dnf_states =
if Config.function_summaries then D.dnf state else [state]
in
let domain_call =
D.call tid ~globals ~actuals ~areturn ~formals ~freturn ~locals
in
List.fold dnf_states wl ~f:(fun state wl ->
match
if not Config.function_summaries then None
else
let state = fst (domain_call ~summaries:false state) in
let* summary = Llair.Function.Tbl.find summary_table name in
List.find_map ~f:(D.apply_summary state) summary
with
| None ->
let state, from_call =
domain_call ~summaries:Config.function_summaries state
in
let ip = Llair.IP.mk entry in
let stk = Stack.push_call call from_call stk in
let src = Llair.IP.block ams.ctrl.ip in
let edge = {dst= Runnable {ip; stk; tid}; src} in
Work.add ~retreating:recursive {ams with ctrl= edge; state} wl
| Some post -> exec_jump return {ams with state= post} wl )
|>
[%Trace.retn fun {pf} _ -> pf ""]
let exec_call call ams wl =
let Llair.{callee= {name} as callee; areturn; return; _} = call in
if Llair.Func.is_undefined callee then
exec_skip_func areturn return ams wl
else
let globals = Domain_used_globals.by_function Config.globals name in
exec_call globals call ams wl
let exec_return exp ({ctrl= {ip; stk; tid}; state} as ams) wl =
let block = Llair.IP.block ip in
let func = block.parent in
let Llair.{name; formals; freturn; locals} = func in
[%Trace.call fun {pf} -> pf " t%i@ from: %a" tid Llair.Function.pp name]
;
let summarize post_state =
if not Config.function_summaries then post_state
else
let function_summary, post_state =
D.create_summary tid ~locals ~formals post_state
in
Llair.Function.Tbl.add_multi ~key:name ~data:function_summary
summary_table ;
pp_st () ;
post_state
in
let pre_state = state in
let exit_state =
match (freturn, exp) with
| Some freturn, Some return_val ->
D.exec_move tid (IArray.of_ (freturn, return_val)) pre_state
| None, None -> pre_state
| _ -> violates Llair.Func.invariant func
in
( match Stack.pop_return stk with
| Some (from_call, retn_site, stk) ->
let post_state =
summarize (D.post tid locals from_call exit_state)
in
let retn_state = D.retn tid formals freturn from_call post_state in
exec_jump retn_site
{ams with ctrl= {ams.ctrl with stk}; state= retn_state}
wl
| None ->
if Config.function_summaries then
summarize exit_state |> (ignore : D.t -> unit) ;
Work.add ~retreating:false
{ams with ctrl= {dst= Terminated tid; src= block}}
wl )
|>
[%Trace.retn fun {pf} _ -> pf ""]
let exec_throw exc ({ctrl= {ip; stk; tid}; state} as ams) wl =
let func = (Llair.IP.block ip).parent in
let Llair.{name; formals; freturn; fthrow; locals} = func in
[%Trace.call fun {pf} -> pf "@ from %a" Llair.Function.pp name]
;
let unwind formals scope from_call state =
D.retn tid formals (Some fthrow) from_call
(D.post tid scope from_call state)
in
let pre_state = state in
( match Stack.pop_throw stk ~unwind pre_state with
| Some (from_call, retn_site, stk, unwind_state) ->
let exit_state =
D.exec_move tid (IArray.of_ (fthrow, exc)) unwind_state
in
let post_state = D.post tid locals from_call exit_state in
let retn_state = D.retn tid formals freturn from_call post_state in
exec_jump retn_site
{ams with ctrl= {ams.ctrl with stk}; state= retn_state}
wl
| None -> wl )
|>
[%Trace.retn fun {pf} _ -> pf ""]
let exec_assume cond jump ({ctrl= {tid}; state} as ams) wl =
match D.exec_assume tid state cond with
| Some state -> exec_jump jump {ams with state} wl
| None ->
[%Trace.info " infeasible %a@\n@[%a@]" Llair.Exp.pp cond D.pp state] ;
wl
let exec_thread_create reg {Llair.entry; locals} return
({ctrl= {tid}; state; threads} as ams) wl =
let child_tid, threads = Threads.create entry threads in
let child =
Llair.Exp.integer (Llair.Reg.typ reg) (Z.of_int child_tid)
in
let state = D.exec_move tid (IArray.of_ (reg, child)) state in
let state = D.enter_scope child_tid locals state in
exec_jump return {ams with state; threads} wl
let exec_thread_join thread return ({ctrl= {tid}; state; threads} as ams)
wl =
List.fold (D.resolve_int tid state thread) wl ~f:(fun join_tid wl ->
match Threads.join join_tid threads with
| Some threads -> exec_jump return {ams with threads} wl
| None -> wl )
let resolve_callee (pgm : Llair.program) tid callee state =
let lookup name = Llair.Func.find name pgm.functions in
D.resolve_callee lookup tid callee state
let exec_term pgm ({ctrl= {ip; tid}; state} as ams) wl =
let block = Llair.IP.block ip in
let term = block.term in
[%Trace.info
" t%i@\n@[%a@]@\n%a" tid D.pp state Llair.Term.pp block.term] ;
Report.step_term block ;
match (term : Llair.term) with
| Switch {key; tbl; els} ->
let wl =
exec_assume
(IArray.fold tbl Llair.Exp.true_ ~f:(fun (case, _) b ->
Llair.Exp.and_ (Llair.Exp.dq key case) b ))
els ams wl
in
IArray.fold tbl wl ~f:(fun (case, jump) wl ->
exec_assume (Llair.Exp.eq key case) jump ams wl )
| Iswitch {ptr; tbl} ->
IArray.fold tbl wl ~f:(fun jump wl ->
exec_assume
(Llair.Exp.eq ptr
(Llair.Exp.label
~parent:(Llair.Function.name jump.dst.parent.name)
~name:jump.dst.lbl))
jump ams wl )
| Call ({callee; actuals; areturn; return} as call) -> (
match
(Llair.Function.name callee.name, IArray.to_array actuals, areturn)
with
| "sledge_thread_create", [|callee|], Some reg -> (
match resolve_callee pgm tid callee state with
| [] -> exec_skip_func areturn return ams wl
| callees ->
List.fold callees wl ~f:(fun callee wl ->
exec_thread_create reg callee return ams wl ) )
| "sledge_thread_join", [|thread|], None ->
exec_thread_join thread return ams wl
| _ -> exec_call call ams wl )
| ICall ({callee; areturn; return} as call) -> (
match resolve_callee pgm tid callee state with
| [] -> exec_skip_func areturn return ams wl
| callees ->
List.fold callees wl ~f:(fun callee wl ->
exec_call {call with callee} ams wl ) )
| Return {exp} -> exec_return exp ams wl
| Throw {exc} -> exec_throw exc ams wl
| Unreachable -> wl
let rec exec_ip pgm ({ctrl= {ip; stk; tid}; state} as ams) wl =
match Llair.IP.inst ip with
| Some inst -> (
[%Trace.info " t%i@\n@[%a@]@\n%a" tid D.pp state Llair.Inst.pp inst] ;
Report.step_inst ip ;
match D.exec_inst tid inst state with
| Ok state ->
let ip = Llair.IP.succ ip in
if Llair.IP.is_schedule_point ip then
let src = Llair.IP.block ip in
let edge = {dst= Runnable {ip; stk; tid}; src} in
Work.add ~retreating:false {ams with ctrl= edge; state} wl
else exec_ip pgm {ams with ctrl= {ams.ctrl with ip}; state} wl
| Error alarm ->
Report.alarm alarm ;
wl )
| None -> exec_term pgm ams wl
let call_entry_point pgm =
let+ {name; formals; freturn; locals; entry} =
List.find_map Config.entry_points ~f:(fun entry_point ->
let* func = Llair.Func.find entry_point pgm.Llair.functions in
if IArray.is_empty func.formals then Some func else None )
in
let summaries = Config.function_summaries in
let globals = Domain_used_globals.by_function Config.globals name in
let actuals = IArray.empty in
let areturn = None in
let state, _ =
D.call ThreadID.init ~summaries ~globals ~actuals ~areturn ~formals
~freturn ~locals (D.init pgm.globals)
in
Work.init state entry
let exec_pgm pgm =
match call_entry_point pgm with
| Some wl -> Work.run ~f:(exec_ip pgm) wl
| None -> fail "no entry point found" ()
let compute_summaries pgm =
assert Config.function_summaries ;
exec_pgm pgm ;
Llair.Function.Tbl.fold summary_table Llair.Function.Map.empty
~f:(fun ~key ~data map ->
match data with
| [] -> map
| _ -> Llair.Function.Map.add ~key ~data map )
end
[@@inlined]