sledge/src/sh.ml (964 lines of code) (raw):
(*
* 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.
*)
(** Symbolic Heap Formulas *)
open Fol
(** enable stronger unsat checking during normalization *)
let strong_unsat = false
[@@@warning "+9"]
type seg = {loc: Term.t; bas: Term.t; len: Term.t; siz: Term.t; cnt: Term.t}
[@@deriving compare, equal, sexp]
module Seg = struct
type t = seg [@@deriving compare, equal, sexp]
end
module Segs : sig
type t [@@deriving compare, equal, sexp]
val empty : t
val of_ : seg -> t
val remove : seg -> t -> t
val union : t -> t -> t option
val is_empty : t -> bool
val map : t -> f:(seg -> seg) -> t option
val filter : t -> f:(seg -> bool) -> t
val fold : t -> 's -> f:(seg -> 's -> 's) -> 's
val to_iter : t -> seg iter
val to_list : t -> seg list
end = struct
include Set.Make (Seg)
include Provide_of_sexp (Seg)
let add x s =
let s' = add x s in
if s' == s then None else Some s'
let union s t = if disjoint s t then Some (union s t) else None
let map s ~f =
let s, ys =
fold s (s, []) ~f:(fun x (s, ys) ->
let y = f x in
if y == x then (s, ys) else (remove x s, y :: ys) )
in
List.fold ys (Some s) ~f:(fun y -> function
| Some s -> add y s | None -> None )
end
module T0 = struct
type compare [@@deriving compare, equal, sexp]
type starjunction =
{ heap: Segs.t
; djns: disjunction list
; pure: Formula.t
; xs: Var.Set.t
; us: Var.Set.t
; ctx: Context.t [@ignore] }
and disjunction = (starjunction, compare) Set.t
[@@deriving compare, equal, sexp]
type t = starjunction [@@deriving compare, equal, sexp]
end
module T = struct
include Comparer.Counterfeit (T0)
include T0
end
module Set = struct
include Set.Make_from_Comparer (T)
include Provide_of_sexp (T)
end
include T
(** Replay debugging *)
type call = Freshen_xs of t * Var.Set.t | Simplify of t [@@deriving sexp]
(** Basic values *)
let emp =
{ us= Var.Set.empty
; xs= Var.Set.empty
; ctx= Context.empty
; pure= Formula.tt
; heap= Segs.empty
; djns= [] }
let false_ us =
{emp with us; ctx= Context.unsat; pure= Formula.ff; djns= [Set.empty]}
(** Traversals *)
let map_seg ~f h =
let loc = f h.loc in
let bas = f h.bas in
let len = f h.len in
let siz = f h.siz in
let cnt = f h.cnt in
if
loc == h.loc
&& bas == h.bas
&& len == h.len
&& siz == h.siz
&& cnt == h.cnt
then h
else {loc; bas; len; siz; cnt}
let fold_terms_seg {loc; bas; len; siz; cnt} s ~f =
f loc (f bas (f len (f siz (f cnt s))))
let fold_vars_seg seg s ~f =
fold_terms_seg ~f:(Iter.fold ~f << Term.vars) seg s
let fold_vars_stem ?ignore_ctx ?ignore_pure
{us= _; xs= _; ctx; pure; heap; djns= _} s ~f =
let unless flag f s = if Option.is_some flag then s else f s in
Segs.fold ~f:(fold_vars_seg ~f) heap s
|> unless ignore_pure (Iter.fold ~f (Formula.vars pure))
|> unless ignore_ctx (Iter.fold ~f (Context.vars ctx))
let fold_vars ?ignore_ctx ?ignore_pure fold_vars q s ~f =
fold_vars_stem ?ignore_ctx ?ignore_pure ~f q s
|> List.fold ~f:(Set.fold ~f:fold_vars) q.djns
(** Pretty-printing *)
let rec var_strength_ xs m q =
let add v m =
Var.Map.update v m ~f:(function
| None -> Some `Anonymous
| Some `Anonymous -> Some `Existential
| o -> o )
in
let xs = Var.Set.union xs q.xs in
let m_stem =
fold_vars_stem ~ignore_ctx:() q m ~f:(fun var m ->
if not (Var.Set.mem var xs) then
Var.Map.add ~key:var ~data:`Universal m
else add var m )
in
let m =
List.fold q.djns m_stem ~f:(fun djn m ->
let ms =
Set.fold
~f:(fun dj ms -> snd (var_strength_ xs m dj) :: ms)
djn []
in
List.reduce ms ~f:(fun m1 m2 ->
Var.Map.union m1 m2 ~f:(fun _ s1 s2 ->
match (s1, s2) with
| `Anonymous, `Anonymous -> Some `Anonymous
| `Universal, _ | _, `Universal -> Some `Universal
| `Existential, _ | _, `Existential -> Some `Existential ) )
|> Option.value ~default:m )
in
(m_stem, m)
let var_strength ?(xs = Var.Set.empty) q =
let m =
Var.Set.fold xs Var.Map.empty ~f:(fun x ->
Var.Map.add ~key:x ~data:`Existential )
in
var_strength_ xs m q
let pp_chunk x fs (siz, cnt) =
Format.fprintf fs "@[<2>@<1>⟨%a,%a@<1>⟩@]" (Term.ppx x) siz (Term.ppx x)
cnt
let pp_seg x fs {loc; bas; len; siz; cnt} =
let term_pp = Term.ppx x in
Format.fprintf fs "@[<2>%a@ @[@[-[%a)->@]@ %a@]@]" term_pp loc
(fun fs (bas, len) ->
if (not (Term.equal loc bas)) || not (Term.equal len siz) then
Format.fprintf fs " %a, %a " term_pp bas term_pp len )
(bas, len) (pp_chunk x) (siz, cnt)
let pp_seg_norm ctx fs seg =
let x _ = None in
pp_seg x fs (map_seg seg ~f:(Context.normalize ctx))
let pp_block x fs segs =
let is_full_alloc segs =
match segs with
| {loc; bas; len; _} :: _ -> (
Term.equal loc bas
&&
match Term.get_z len with
| Some z -> (
match
List.fold segs (Some Z.zero) ~f:(fun seg len ->
match (len, Term.get_z seg.siz) with
| Some len, Some siz -> Some (Z.add len siz)
| _ -> None )
with
| Some blk_len -> Z.equal z blk_len
| _ -> false )
| _ -> false )
| [] -> false
in
let term_pp = Term.ppx x in
let pp_chunks =
List.pp "@,^" (fun fs seg -> pp_chunk x fs (seg.siz, seg.cnt))
in
match segs with
| {loc; bas; len; _} :: _ ->
Format.fprintf fs "@[<2>%a@ @[@[-[%t)->@]@ @[%a@]@]@]" term_pp loc
(fun fs ->
if not (is_full_alloc segs) then
Format.fprintf fs " %a, %a " term_pp bas term_pp len )
pp_chunks segs
| [] -> ()
let pp_heap x ?pre ctx fs heap =
let bas_off = Term.split_const in
let cmp s1 s2 =
[%compare: Term.t * (Term.t * Q.t)]
(Context.normalize ctx s1.bas, bas_off (Context.normalize ctx s1.loc))
(Context.normalize ctx s2.bas, bas_off (Context.normalize ctx s2.loc))
in
let eq s1 s2 =
Term.equal s1.bas s2.bas
&& Term.equal s1.len s2.len
&& Context.implies ctx (Formula.eq (Term.add s1.loc s1.siz) s2.loc)
in
let heap = List.map ~f:(map_seg ~f:(Context.normalize ctx)) heap in
let blocks = List.group_succ ~eq (List.sort ~cmp heap) in
List.pp ?pre "@ * " (pp_block x) fs blocks
let pp_us ?vs fs us =
match vs with
| None ->
if not (Var.Set.is_empty us) then
[%Trace.fprintf fs "@<2>∀ @[%a@] .@ " Var.Set.pp us]
| Some vs ->
if not (Var.Set.equal vs us) then
[%Trace.fprintf fs "@<2>∀ @[%a@] .@ " Var.Set.pp_diff (vs, us)]
let rec pp_ ?var_strength ?vs ancestor_xs parent_ctx fs
{us; xs; ctx; pure; heap; djns} =
Format.pp_open_hvbox fs 0 ;
let x v = Option.bind ~f:(fun (_, m) -> Var.Map.find v m) var_strength in
pp_us ?vs fs us ;
( match djns with
| [djn] when Set.is_empty djn && Option.is_some var_strength ->
Format.fprintf fs "false"
| _ ->
let vs = Option.value vs ~default:Var.Set.empty in
let xs_d_vs, xs_i_vs =
Var.Set.diff_inter
(Var.Set.filter xs ~f:(fun v -> Poly.(x v <> Some `Anonymous)))
vs
in
if not (Var.Set.is_empty xs_i_vs) then (
Format.fprintf fs "@<3>∃↑ @[%a@] ." (Var.Set.ppx x) xs_i_vs ;
if not (Var.Set.is_empty xs_d_vs) then Format.fprintf fs "@ " ) ;
if not (Var.Set.is_empty xs_d_vs) then
Format.fprintf fs "@<2>∃ @[%a@] .@ " (Var.Set.ppx x) xs_d_vs ;
let first =
if Option.is_some var_strength then
Context.ppx_diff x fs parent_ctx pure ctx
else (
Format.fprintf fs "@[ %a@ @<2>∧ %a@]" Context.pp ctx Formula.pp
pure ;
false )
in
if Segs.is_empty heap then
Format.fprintf fs
( if first then if List.is_empty djns then " emp" else ""
else "@ @<5>∧ emp" )
else
pp_heap x
~pre:(if first then " " else "@ @<2>∧ ")
(if Option.is_some var_strength then ctx else emp.ctx)
fs (Segs.to_list heap) ;
let first = first && Segs.is_empty heap in
List.pp
~pre:(if first then " " else "@ * ")
"@ * "
(pp_djn ?var_strength
(Var.Set.union vs (Var.Set.union us xs))
(Var.Set.union ancestor_xs xs)
(if Option.is_some var_strength then ctx else emp.ctx) )
fs djns ) ;
Format.pp_close_box fs ()
and pp_djn ?var_strength vs xs ctx fs djn =
if Set.is_empty djn then Format.fprintf fs "false"
else
Format.fprintf fs "@[<hv>( %a@ )@]"
(List.pp "@ @<2>∨ " (fun fs sjn ->
let var_strength =
let+ var_strength_stem, _ = var_strength in
var_strength_ xs var_strength_stem sjn
in
Format.fprintf fs "@[<hv 1>(%a)@]"
(pp_ ?var_strength ~vs (Var.Set.union xs sjn.xs) ctx)
sjn ) )
(Set.to_list djn)
let pp_us fs us = pp_us fs us
let pp_diff_eq ?us ?(xs = Var.Set.empty) ctx fs q =
pp_ ~var_strength:(var_strength ~xs q) ?vs:us xs ctx fs q
let pp fs q = pp_diff_eq Context.empty fs q
let pp_djn fs d =
pp_djn ?var_strength:None Var.Set.empty Var.Set.empty Context.empty fs d
let pp_raw fs q =
pp_ ?var_strength:None ?vs:None Var.Set.empty Context.empty fs q
let fv_seg seg = fold_vars_seg ~f:Var.Set.add seg Var.Set.empty
let fv ?ignore_ctx ?ignore_pure q =
let rec fv_union q s =
Var.Set.diff
(fold_vars ?ignore_ctx ?ignore_pure fv_union ~f:Var.Set.add q s)
q.xs
in
fv_union q Var.Set.empty
let rec invariant q =
let@ () = Invariant.invariant [%here] q [%sexp_of: t] in
let {us; xs; ctx; pure; heap; djns} = q in
try
assert (
Var.Set.disjoint us xs
|| fail "inter: @[%a@]@\nq: @[%a@]" Var.Set.pp (Var.Set.inter us xs)
pp q () ) ;
assert (
Var.Set.subset (fv q) ~of_:us
|| fail "unbound but free: %a" Var.Set.pp (Var.Set.diff (fv q) us) () ) ;
Context.invariant ctx ;
match djns with
| [djn] when Set.is_empty djn ->
assert (Context.is_unsat ctx) ;
assert (Formula.equal Formula.ff pure) ;
assert (Segs.is_empty heap)
| _ ->
assert (not (Context.is_unsat ctx)) ;
assert (not (Formula.equal Formula.ff pure)) ;
assert (not (List.exists djns ~f:Set.is_empty)) ;
List.iter djns ~f:(fun djn ->
assert (Set.cardinal djn > 1) ;
Set.iter djn ~f:(fun sjn ->
assert (Var.Set.subset sjn.us ~of_:(Var.Set.union us xs)) ;
invariant sjn ) )
with exc ->
let bt = Printexc.get_raw_backtrace () in
[%Trace.info " %a" pp_raw q] ;
Printexc.raise_with_backtrace exc bt
(** Query *)
(** syntactically empty: empty heap and no pure constraints *)
let is_emp q =
Context.is_empty q.ctx
&& Formula.equal Formula.tt q.pure
&& Segs.is_empty q.heap
&& List.is_empty q.djns
(** (incomplete syntactic) test that all satisfying heaps are empty *)
let rec is_empty q =
Segs.is_empty q.heap && List.for_all ~f:(Set.for_all ~f:is_empty) q.djns
(** syntactically inconsistent *)
let is_false q = match q.djns with [djn] -> Set.is_empty djn | _ -> false
(** Quantification and Vocabulary *)
let exists_fresh xs q =
[%Trace.call fun {pf} ->
pf "@ {@[%a@]}@ %a" Var.Set.pp xs pp q ;
assert (
Var.Set.disjoint xs q.us
|| fail "Sh.exists_fresh xs ∩ q.us: %a" Var.Set.pp
(Var.Set.inter xs q.us) () )]
;
( if Var.Set.is_empty xs then q
else {q with xs= Var.Set.union q.xs xs} |> check invariant )
|>
[%Trace.retn fun {pf} -> pf "%a" pp]
let exists xs q =
[%Trace.call fun {pf} -> pf "@ {@[%a@]}@ %a" Var.Set.pp xs pp q]
;
assert (
Var.Set.subset xs ~of_:q.us
|| fail "Sh.exists xs - q.us: %a" Var.Set.pp (Var.Set.diff xs q.us) () ) ;
( if Var.Set.is_empty xs then q
else
{q with us= Var.Set.diff q.us xs; xs= Var.Set.union q.xs xs}
|> check invariant )
|>
[%Trace.retn fun {pf} -> pf "%a" pp]
(** remove quantification on variables disjoint from vocabulary *)
let elim_exists xs q =
assert (Var.Set.disjoint xs q.us) ;
{q with us= Var.Set.union q.us xs; xs= Var.Set.diff q.xs xs}
let rec map ~f_sjn ~f_ctx ~f_trm ~f_fml
({us; xs= _; ctx; pure; heap; djns} as q) =
let pure = f_fml pure in
if Formula.equal Formula.ff pure then false_ us
else
let xs, ctx = f_ctx ctx in
if Context.is_unsat ctx then false_ us
else
let djns, hoisted =
List.partition_map_endo djns ~f:(fun djn ->
let djn' =
Set.filter_map djn ~f:(fun sjn ->
let sjn' = f_sjn sjn in
if is_false sjn' then None else Some sjn' )
in
match Set.classify djn' with
| One dj -> Right dj
| _ -> Left djn' )
in
if List.exists ~f:Set.is_empty djns then false_ us
else
match Segs.map heap ~f:(map_seg ~f:f_trm) with
| None -> false_ us
| Some heap ->
if
ctx == q.ctx
&& pure == q.pure
&& heap == q.heap
&& djns == q.djns
&& Var.Set.is_empty xs
then q
else
exists_fresh xs
(List.fold ~f:star hoisted {q with ctx; pure; heap; djns})
(** primitive application of a substitution, ignores us and xs, may violate
invariant *)
and apply_subst sub q =
[%trace]
~call:(fun {pf} -> pf "@ @[%a@]@ %a" Var.Subst.pp sub pp q)
~retn:(fun {pf} -> pf "%a" pp)
@@ fun () ->
map q ~f_sjn:(rename sub)
~f_ctx:(fun r -> (Var.Set.empty, Context.rename r sub))
~f_trm:(Term.rename sub) ~f_fml:(Formula.rename sub)
|> check (fun q' ->
assert (Var.Set.disjoint (fv q') (Var.Subst.domain sub)) )
and rename_ Var.Subst.{sub; dom; rng} q =
[%Trace.call fun {pf} ->
pf "@ @[%a@]@ %a" Var.Subst.pp sub pp q ;
assert (Var.Set.subset dom ~of_:q.us)]
;
let q = extend_us rng q in
( if Var.Subst.is_empty sub then q
else {(apply_subst sub q) with us= Var.Set.diff q.us dom} )
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp q' ;
invariant q' ;
assert (Var.Set.disjoint q'.us (Var.Subst.domain sub))]
and rename sub q =
[%Trace.call fun {pf} -> pf "@ @[%a@]@ %a" Var.Subst.pp sub pp q]
;
rename_ (Var.Subst.restrict_dom sub q.us) q
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp q' ;
invariant q' ;
assert (Var.Set.disjoint q'.us (Var.Subst.domain sub))]
(** freshen existentials, preserving vocabulary *)
and freshen_xs q ~wrt =
try
[%Trace.call fun {pf} ->
pf "@ {@[%a@]}@ %a" Var.Set.pp wrt pp q ;
assert (Var.Set.subset q.us ~of_:wrt)]
;
let Var.Subst.{sub; dom; rng}, _ = Var.Subst.freshen q.xs ~wrt in
( if Var.Subst.is_empty sub then q
else
let q' = apply_subst sub q in
let xs = Var.Set.union (Var.Set.diff q'.xs dom) rng in
if q' == q && xs == q.xs then q
else
let us = Var.Set.diff q'.us xs in
{q' with us; xs} )
|>
[%Trace.retn fun {pf} q' ->
pf "%a@ %a" Var.Subst.pp sub pp q' ;
assert (Var.Set.equal q'.us q.us) ;
assert (Var.Set.disjoint q'.xs (Var.Subst.domain sub)) ;
assert (Var.Set.disjoint q'.xs (Var.Set.inter q.xs wrt)) ;
invariant q']
with exc ->
let bt = Printexc.get_raw_backtrace () in
Format.eprintf "@\n%a@." Sexp.pp_hum
(sexp_of_call (Freshen_xs (q, wrt))) ;
Printexc.raise_with_backtrace exc bt
and extend_us us q =
let us = Var.Set.union us q.us in
(if us == q.us then q else {(freshen_xs q ~wrt:us) with us})
|> check invariant
and star q1 q2 =
[%trace]
~call:(fun {pf} -> pf "@ (%a)@ (%a)" pp q1 pp q2)
~retn:(fun {pf} q ->
pf "%a" pp q ;
invariant q ;
assert (Var.Set.equal q.us (Var.Set.union q1.us q2.us)) )
@@ fun () ->
if is_false q1 || is_false q2 then false_ (Var.Set.union q1.us q2.us)
else if is_emp q1 then extend_us q1.us q2
else if is_emp q2 then extend_us q2.us q1
else
let us = Var.Set.union q1.us q2.us in
let q1 = freshen_xs q1 ~wrt:(Var.Set.union us q2.xs) in
let q2 = freshen_xs q2 ~wrt:(Var.Set.union us q1.xs) in
let {us= us1; xs= xs1; ctx= c1; pure= p1; heap= h1; djns= d1} = q1 in
let {us= us2; xs= xs2; ctx= c2; pure= p2; heap= h2; djns= d2} = q2 in
assert (Var.Set.equal us (Var.Set.union us1 us2)) ;
match Segs.union h1 h2 with
| None -> false_ us
| Some heap ->
let xs, ctx =
Context.union (Var.Set.union us (Var.Set.union xs1 xs2)) c1 c2
in
if Context.is_unsat ctx then false_ us
else
let pure = Formula.and_ p1 p2 in
if Formula.equal Formula.ff pure then false_ us
else
exists_fresh xs
{ us
; xs= Var.Set.union xs1 xs2
; ctx
; pure
; heap
; djns= List.append d1 d2 }
let starN = function
| [] -> emp
| [q] -> q
| q :: qs -> List.fold ~f:star qs q
let or_ q1 q2 =
[%Trace.call fun {pf} -> pf "@ (%a)@ (%a)" pp_raw q1 pp_raw q2]
;
( match (q1, q2) with
| _ when is_false q1 -> extend_us q1.us q2
| _ when is_false q2 -> extend_us q2.us q1
| ({djns= []; _} as q), ({us= _; xs; ctx= _; pure; heap; djns= [djn]} as d)
when Var.Set.is_empty xs
&& Formula.(equal tt pure)
&& Segs.is_empty heap ->
{d with us= Var.Set.union q.us d.us; djns= [Set.add q djn]}
| ({us= _; xs; ctx= _; pure; heap; djns= [djn]} as d), ({djns= []; _} as q)
when Var.Set.is_empty xs
&& Formula.(equal tt pure)
&& Segs.is_empty heap ->
{d with us= Var.Set.union q.us d.us; djns= [Set.add q djn]}
| _ when equal q1 q2 -> q1
| _ ->
{ us= Var.Set.union q1.us q2.us
; xs= Var.Set.empty
; ctx= Context.empty
; pure= Formula.tt
; heap= Segs.empty
; djns= [Set.add q1 (Set.of_ q2)] } )
|>
[%Trace.retn fun {pf} q ->
pf "%a" pp_raw q ;
invariant q ;
assert (Var.Set.equal q.us (Var.Set.union q1.us q2.us))]
let orN djn =
match Set.pop djn with
| None -> false_ Var.Set.empty
| Some (q, qs) -> Set.fold ~f:or_ qs q
let freshen q ~wrt =
[%Trace.call fun {pf} -> pf "@ {@[%a@]}@ %a" Var.Set.pp wrt pp q]
;
let xsub, _ = Var.Subst.freshen q.us ~wrt:(Var.Set.union wrt q.xs) in
let q' = extend_us wrt (rename_ xsub q) in
(q', xsub.sub)
|>
[%Trace.retn fun {pf} (q', _) ->
pf "%a" pp q' ;
invariant q' ;
assert (Var.Set.subset wrt ~of_:q'.us) ;
assert (Var.Set.disjoint wrt (fv q'))]
let bind_exists q ~wrt =
[%Trace.call fun {pf} -> pf "@ {@[%a@]}@ %a" Var.Set.pp wrt pp q]
;
let q' =
if Var.Set.is_empty wrt then q
else freshen_xs q ~wrt:(Var.Set.union q.us wrt)
in
(q'.xs, {q' with us= Var.Set.union q'.us q'.xs; xs= Var.Set.empty})
|>
[%Trace.retn fun {pf} (_, q') -> pf "%a" pp q']
(** Construct *)
(** conjoin an FOL context assuming vocabulary is compatible *)
let and_ctx_ ctx q =
assert (Var.Set.subset (Context.fv ctx) ~of_:q.us) ;
let xs, ctx = Context.union (Var.Set.union q.us q.xs) q.ctx ctx in
if Context.is_unsat ctx then false_ q.us else exists_fresh xs {q with ctx}
let and_ctx ctx q =
[%Trace.call fun {pf} -> pf "@ %a@ %a" Context.pp ctx pp q]
;
(if is_false q then q else and_ctx_ ctx (extend_us (Context.fv ctx) q))
|>
[%Trace.retn fun {pf} q ->
pf "%a" pp q ;
invariant q]
let pure (p : Formula.t) =
[%Trace.call fun {pf} -> pf "@ %a" Formula.pp p]
;
Iter.fold (Context.dnf p) (false_ Var.Set.empty)
~f:(fun (xs, pure, ctx) q ->
let us = Formula.fv pure in
if Context.is_unsat ctx || Formula.equal Formula.ff pure then
extend_us us q
else or_ q (exists_fresh xs {emp with us; ctx; pure}) )
|>
[%Trace.retn fun {pf} q ->
pf "%a" pp q ;
invariant q]
let and_ b q =
[%trace]
~call:(fun {pf} -> pf "@ (%a)@ (%a)" Formula.pp b pp q)
~retn:(fun {pf} -> pf "%a" pp)
@@ fun () ->
let p = pure (Formula.map_terms ~f:(Context.normalize q.ctx) b) in
if is_emp p then q
else
let xs, q = bind_exists q ~wrt:(Formula.fv b) in
exists xs (star p q)
let andN bs q = and_ (Formula.andN bs) q
let and_subst subst q =
[%Trace.call fun {pf} -> pf "@ %a@ %a" Context.Subst.pp subst pp q]
;
Context.Subst.fold_eqs ~f:and_ subst q
|>
[%Trace.retn fun {pf} q ->
pf "%a" pp q ;
invariant q]
let subst sub q =
[%Trace.call fun {pf} -> pf "@ @[%a@]@ %a" Var.Subst.pp sub pp q]
;
let dom, eqs =
Var.Subst.fold sub (Var.Set.empty, Formula.tt)
~f:(fun var trm (dom, eqs) ->
( Var.Set.add var dom
, Formula.and_ (Formula.eq (Term.var var) (Term.var trm)) eqs ) )
in
exists dom (and_ eqs q)
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp q' ;
invariant q' ;
assert (Var.Set.disjoint q'.us (Var.Subst.domain sub))]
let seg pt =
let us = fv_seg pt in
if Term.equal Term.zero pt.loc then false_ us
else {emp with us; heap= Segs.of_ pt} |> check invariant
(** Update *)
let rem_seg seg q = {q with heap= Segs.remove seg q.heap} |> check invariant
let filter_heap ~f q =
{q with heap= Segs.filter q.heap ~f} |> check invariant
(** Disjunctive-Normal Form *)
let fold_dnf ~conj ~disj sjn (xs, conjuncts) disjuncts =
let rec add_disjunct pending_splits sjn (xs, conjuncts) disjuncts =
let ys, sjn = bind_exists sjn ~wrt:xs in
let xs = Var.Set.union ys xs in
let djns = sjn.djns in
let sjn = {sjn with djns= []} in
split_case
(Iter.append (Iter.of_list djns) pending_splits)
(xs, conj sjn conjuncts)
disjuncts
and split_case pending_splits (xs, conjuncts) disjuncts =
match Iter.pop pending_splits with
| Some (split, pending_splits) ->
Set.fold split disjuncts ~f:(fun sjn disjuncts ->
add_disjunct pending_splits sjn (xs, conjuncts) disjuncts )
| None -> disj (xs, conjuncts) disjuncts
in
add_disjunct Iter.empty sjn (xs, conjuncts) disjuncts
let dnf q =
[%Trace.call fun {pf} -> pf "@ %a" pp q]
;
let conj sjn conjuncts = sjn :: conjuncts in
let disj (xs, conjuncts) disjuncts =
Set.add (exists xs (starN conjuncts)) disjuncts
in
fold_dnf ~conj ~disj q (Var.Set.empty, []) Set.empty
|>
[%Trace.retn fun {pf} -> pf "%a" pp_djn]
(** Logical query *)
(** first-order approximation of heap constraints *)
let rec pure_approx q =
Formula.andN
( [ q.pure
; Formula.distinct
(Array.of_list
(Segs.fold q.heap [Term.zero] ~f:(fun seg locs ->
seg.loc :: locs ) ) ) ]
|> List.fold q.djns ~f:(fun djn p ->
Formula.orN
(Set.fold ~f:(fun dj ps -> pure_approx dj :: ps) djn [])
:: p ) )
let pure_approx q =
[%Trace.call fun {pf} -> pf "@ %a" pp q]
;
pure_approx q
|>
[%Trace.retn fun {pf} -> pf "%a" Formula.pp]
(** enumerate a DNF-expansion of a symbolic heap's first-order constraints
conjoined with a first-order approximation of the heap constraints until
a branch that is not unsatisfiable is found *)
let is_unsat_dnf q =
let exception NotUnsat in
let conj sjn (wrt, ctx, fml) =
let wrt = Var.Set.union wrt sjn.xs in
let zs, ctx = Context.union wrt ctx sjn.ctx in
let wrt = Var.Set.union wrt zs in
let fml = Formula.and_ fml sjn.pure in
let fml =
Segs.fold sjn.heap fml ~f:(fun seg ->
Formula.and_ (Formula.dq0 seg.loc) )
in
(wrt, ctx, fml)
in
let disj (_, (_, ctx, fml)) () =
if not (Context.is_unsat ctx || Context.refutes ctx fml) then
raise_notrace NotUnsat
in
try
fold_dnf ~conj ~disj q (Var.Set.empty, (q.us, emp.ctx, emp.pure)) () ;
true
with NotUnsat -> false
let is_unsat q =
if strong_unsat then is_unsat_dnf q
else Context.refutes q.ctx (pure_approx q)
(** Simplify *)
let rec normalize_ q =
[%Trace.call fun {pf} -> pf "@ %a" pp_raw q]
;
let q' =
map q ~f_sjn:normalize_
~f_ctx:(fun x -> (Var.Set.empty, x))
~f_trm:(Context.normalize q.ctx)
~f_fml:(Formula.map_terms ~f:(Context.normalize q.ctx))
in
let pure = Context.fold_eqs ~f:Formula.and_ q.ctx q'.pure in
{q' with pure}
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp_raw q' ;
invariant q']
let do_normalize = ref false
let normalize q =
[%Trace.call fun {pf} -> pf "@ %a" pp_raw q]
;
(if !do_normalize then normalize_ q else q)
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp_raw q' ;
invariant q']
let rec norm_ s q =
[%Trace.call fun {pf} -> pf "@ @[%a@]@ %a" Context.Subst.pp s pp_raw q]
;
map q ~f_sjn:(norm_ s)
~f_ctx:(Context.apply_subst (Var.Set.union q.us q.xs) s)
~f_trm:(Context.Subst.subst s)
~f_fml:(Formula.map_terms ~f:(Context.Subst.subst s))
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp_raw q' ;
invariant q']
let norm s q =
[%Trace.call fun {pf} ->
pf "@ @[%a@]@ %a" Context.Subst.pp s pp_raw q ;
assert (
let unbound = Var.Set.diff (Context.Subst.fv s) q.us in
Var.Set.is_empty unbound
|| fail "unbound subst vars: %a" Var.Set.pp unbound () )]
;
(if Context.Subst.is_empty s then q else norm_ s q)
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp_raw q' ;
invariant q']
(** rename existentially quantified variables to avoid shadowing, and reduce
quantifier scopes by sinking them as low as possible into disjunctions *)
let rec freshen_nested_xs us q =
[%Trace.call fun {pf} -> pf "@ %a" pp q]
;
(* trim xs to those that appear in stem or >1 disjunction and sink rest *)
let xs_sink, _ =
let fv_stem = fv {q with xs= Var.Set.empty; djns= []} in
let xs_sink, xs_djns = (Var.Set.diff q.xs fv_stem, Var.Set.empty) in
List.fold q.djns (xs_sink, xs_djns) ~f:(fun djn (xs_sink, xs_djns) ->
Set.fold djn (xs_sink, xs_djns) ~f:(fun djt (xs_sink, xs_djns) ->
let fv_djt = fv djt in
let dont_sink = Var.Set.inter xs_djns fv_djt in
let xs_sink = Var.Set.diff xs_sink dont_sink in
let xs_djns = Var.Set.union xs_djns fv_djt in
(xs_sink, xs_djns) ) )
in
let xs = Var.Set.diff q.xs xs_sink in
let us = Var.Set.union us q.us in
let djns, hoisted, xs_below =
List.fold_partition_map q.djns Var.Set.empty ~f:(fun djn xs_below ->
let dj, xs_below =
Set.fold_map djn xs_below ~f:(fun dj xs_below ->
(* freshen xs that shadow ancestor us *)
let us = Var.Set.union us dj.us in
let dj = freshen_xs dj ~wrt:us in
(* quantify xs not in stem and freshen disjunct *)
let dj =
freshen_nested_xs us
(exists (Var.Set.inter xs_sink dj.us) dj)
in
let xs_below = Var.Set.union xs_below dj.xs in
(dj, xs_below) )
in
match Set.classify dj with
| One q -> (Right q, xs_below)
| _ -> (Left dj, xs_below) )
in
(* rename xs to miss all xs in subformulas *)
freshen_xs
(List.fold ~f:star hoisted {q with xs; djns})
~wrt:(Var.Set.union q.us xs_below)
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp q' ;
invariant q']
let rec propagate_context_ ancestor_vs ancestor_ctx q =
[%Trace.call fun {pf} -> pf "@ (%a)@ %a" Context.pp ancestor_ctx pp q]
;
(* extend vocabulary with variables in scope above *)
let q = {q with us= Var.Set.union ancestor_vs q.us} in
(* strengthen context with that from above *)
let q = and_ctx_ ancestor_ctx q in
( if is_false q then false_ q.us
else
(* decompose formula *)
let xs, q = bind_exists q ~wrt:Var.Set.empty in
let stem, djns = ({q with djns= emp.djns}, q.djns) in
(* propagate over disjunctions *)
let q' =
List.fold djns stem ~f:(fun djn q' ->
let djn, dj_ctxs =
Set.fold_map djn [] ~f:(fun dj dj_ctxs ->
let dj = propagate_context_ q.us q.ctx dj in
(dj, (dj.xs, dj.ctx) :: dj_ctxs) )
in
let new_xs, djn_ctx = Context.interN q.us dj_ctxs in
(* hoist xs appearing in disjunction's context *)
let djn_xs = Var.Set.diff (Context.fv djn_ctx) q'.us in
let djn = Set.map ~f:(elim_exists djn_xs) djn in
let ctx_djn = and_ctx_ djn_ctx (orN djn) in
assert (is_false ctx_djn || Var.Set.subset new_xs ~of_:djn_xs) ;
star (exists djn_xs ctx_djn) q' )
in
(* requantify existentials *)
let q' = exists xs q' in
(* strengthening contexts can reveal inconsistency *)
if is_false q' then false_ q'.us else q' )
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp q' ;
invariant q']
let propagate_context ancestor_vs ancestor_ctx q =
[%Trace.call fun {pf} -> pf "@ (%a)@ %a" Context.pp ancestor_ctx pp q]
;
propagate_context_ ancestor_vs ancestor_ctx q
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp q' ;
invariant q']
let pp_vss fs vss =
Format.fprintf fs "[@[%a@]]"
(List.pp ";@ " (fun fs vs -> Format.fprintf fs "{@[%a@]}" Var.Set.pp vs))
vss
let remove_absent_xs ks q =
[%trace]
~call:(fun {pf} -> pf "@ %a%a" Var.Set.pp_xs ks pp q)
~retn:(fun {pf} -> pf "%a" pp)
@@ fun () ->
let ks = Var.Set.inter ks q.xs in
if Var.Set.is_empty ks then q
else
let xs = Var.Set.diff q.xs ks in
let djns, hoisted =
let rec trim_ks ks djns =
List.partition_map_endo djns ~f:(fun djn ->
let djn =
Set.map djn ~f:(fun sjn ->
let us = Var.Set.diff sjn.us ks in
let djns, hoisted = trim_ks ks sjn.djns in
if us == sjn.us && djns == sjn.djns then sjn
else List.fold ~f:star hoisted {sjn with us; djns} )
in
match Set.classify djn with One dj -> Right dj | _ -> Left djn )
in
trim_ks ks q.djns
in
List.fold ~f:star hoisted {q with xs; djns}
let rec simplify_ us ancestor_xs rev_xss survived ancestor_subst q =
[%Trace.call fun {pf} ->
pf "@ %a@ %a@ %a" pp_vss (List.rev rev_xss) Context.Subst.pp
ancestor_subst pp_raw q]
;
assert (not (is_false q)) ;
let q0 = q in
(* bind existentials *)
let xs, q = bind_exists ~wrt:emp.us q in
let rev_xss = xs :: rev_xss in
(* try to solve equations in ctx for variables in xss *)
let stem_subst = Context.solve_for_vars (us :: List.rev rev_xss) q.ctx in
let subst = Context.Subst.compose ancestor_subst stem_subst in
( if Context.Subst.is_empty subst then normalize q0
else
(* normalize context wrt solutions *)
let union_xss = Var.Set.union xs ancestor_xs in
let wrt = Var.Set.union us union_xss in
let fresh, ctx, removed =
Context.apply_and_elim ~wrt union_xss subst q.ctx
in
let q = {(extend_us (Var.Set.union fresh ancestor_xs) q) with ctx} in
(* normalize stem wrt its context *)
let stem = normalize {q with djns= emp.djns} in
(* normalize stem wrt both ancestor and current solutions *)
let stem =
(* opt: ctx already normalized so just preserve it *)
{(norm subst {stem with ctx= emp.ctx}) with ctx= q.ctx}
in
if strong_unsat && is_unsat stem then false_ stem.us
else
(* recursively simplify subformulas *)
let survived =
Var.Set.union survived (fv (elim_exists stem.xs stem))
in
let djns, hoisted =
List.rev_partition_map q.djns ~f:(fun djn ->
let djn =
Set.map ~f:(simplify_ us union_xss rev_xss survived subst) djn
in
match Set.classify djn with
| One sjn -> Right sjn
| _ -> Left (orN djn) )
in
let q = List.fold ~f:star hoisted (List.fold ~f:star djns stem) in
if is_false q then false_ q.us
else
let removed = Var.Set.diff removed survived in
let removed =
if Var.Set.is_empty removed then Var.Set.empty
else Var.Set.diff removed (fv (elim_exists q.xs q))
in
(* removed may not contain all variables stem_subst has solutions
for, so the equations in [∃ removed. stem_subst] that are not
universally valid need to be re-conjoined since they have alredy
been normalized out *)
let keep, removed, _ =
Context.Subst.partition_valid removed stem_subst
in
let q = and_subst keep q in
(* (re)quantify existentials *)
let q = exists (Var.Set.union fresh xs) q in
(* remove the eliminated variables from xs and subformulas' us *)
remove_absent_xs removed q )
|>
[%Trace.retn fun {pf} q' ->
pf "%a@ %a" Context.Subst.pp stem_subst pp_raw q' ;
invariant q']
let count_simplify = ref (-1)
let simplify q =
[%Trace.call fun {pf} -> pf " %i@ %a" !count_simplify pp_raw q]
;
( if is_false q then false_ q.us
else
let q = freshen_nested_xs Var.Set.empty q in
let q = propagate_context Var.Set.empty Context.empty q in
if is_false q then false_ q.us
else simplify_ q.us Var.Set.empty [] Var.Set.empty Context.Subst.empty q
)
|>
[%Trace.retn fun {pf} q' ->
pf "%a" pp_raw q' ;
invariant q']
(*
* Replay debugging
*)
let replay c =
match call_of_sexp (Sexp.of_string c) with
| Freshen_xs (q, wrt) -> freshen_xs q ~wrt |> ignore
| Simplify q -> simplify q |> ignore
let dump_simplify = ref (-1)
let simplify q =
Int.incr count_simplify ;
if !count_simplify = !dump_simplify then
fail "%a" Sexp.pp_hum (sexp_of_call (Simplify q)) ()
else
try simplify q
with exc ->
let bt = Printexc.get_raw_backtrace () in
Format.eprintf "@\n%a@." Sexp.pp_hum (sexp_of_call (Simplify q)) ;
Printexc.raise_with_backtrace exc bt