open Sidekick_core open Sigs open Base_types_ let invalid_argf fmt = Format.kasprintf (fun msg -> invalid_arg ("sidekick.sat: " ^ msg)) fmt type clause = Clause0.t type store = Store.t type plugin = Sigs.plugin module Atom = Store.Atom module Var = Store.Var module Clause = Store.Clause module H = Heap.Make [@specialise] (struct type store = Store.t type t = var let[@inline] cmp store i j = Var.weight store j < Var.weight store i (* comparison by weight *) let heap_idx = Var.heap_idx let set_heap_idx = Var.set_heap_idx let of_int_unsafe = Var.of_int_unsafe end) (* cause of "unsat", possibly conditional to local assumptions *) type unsat_cause = | US_local of { first: atom; (* assumption which was found to be proved false *) core: atom list; (* the set of assumptions *) } | US_false of clause (* true unsat *) exception E_sat exception E_unsat of unsat_cause exception UndecidedLit exception Restart exception Conflict of clause let var_decay : float = 1. /. 0.95 (* inverse of the activity factor for variables *) let clause_decay : float = 1. /. 0.999 (* inverse of the activity factor for clauses *) let restart_inc : float = 1.5 (* multiplicative factor for restart limit *) let learntsize_inc : float = 1.1 (* multiplicative factor for [learntsize_factor] at each restart *) (** Passed to clause pools when it's time to garbage collect clauses *) module type GC_ARG = sig val store : Store.t val must_keep_clause : clause -> bool val flag_clause_for_gc : clause -> unit end (** A clause pool *) module type CLAUSE_POOL = sig val add : clause -> unit val descr : unit -> string val gc : (module GC_ARG) -> unit val iter : f:(clause -> unit) -> unit val needs_gc : unit -> bool val size : unit -> int end (* a clause pool *) type clause_pool = (module CLAUSE_POOL) (* a pool with garbage collection determined by [P] *) module Make_gc_cp (P : sig val descr : unit -> string val max_size : int ref end) () : CLAUSE_POOL = struct let clauses_ : clause Vec.t = Vec.create () (* Use a [Vec] so we can sort it. TODO: when we can sort any vec, come back to that. *) let descr = P.descr let add c = Vec.push clauses_ c let iter ~f = Vec.iter ~f clauses_ let size () = Vec.size clauses_ let needs_gc () = size () > !P.max_size let gc (module G : GC_ARG) : unit = (* find clauses to GC *) let to_be_pushed_back = CVec.create () in (* sort by decreasing activity *) Vec.sort clauses_ (fun c1 c2 -> compare (Clause.activity G.store c2) (Clause.activity G.store c1)); while Vec.size clauses_ > !P.max_size do let c = Vec.pop_exn clauses_ in if G.must_keep_clause c then CVec.push to_be_pushed_back c (* must keep it, it's on the trail *) else G.flag_clause_for_gc c done; (* transfer back clauses we had to keep *) CVec.iter ~f:(Vec.push clauses_) to_be_pushed_back; () end let make_gc_clause_pool_ ~descr ~max_size () : clause_pool = (module Make_gc_cp (struct let descr = descr let max_size = max_size end) ()) let[@inline] cp_size_ (module P : CLAUSE_POOL) : int = P.size () let[@inline] cp_needs_gc_ (module P : CLAUSE_POOL) : bool = P.needs_gc () let[@inline] cp_add_ (module P : CLAUSE_POOL) c : unit = P.add c let[@inline] cp_to_iter_ (module P : CLAUSE_POOL) yield : unit = P.iter ~f:yield (* initial limit for the number of learnt clauses, 1/3 of initial number of clauses by default *) let learntsize_factor = 1. /. 3. (** Actions from theories and user, but to be done in specific points of the solving loops. *) module Delayed_actions : sig type t val create : unit -> t val is_empty : t -> bool val clear_on_backtrack : t -> unit val add_clause_learnt : t -> clause -> unit val propagate_atom : t -> atom -> lvl:int -> clause lazy_t -> unit val add_decision : t -> atom -> unit val iter : decision:(atom -> unit) -> propagate:(atom -> lvl:int -> clause lazy_t -> unit) -> add_clause_learnt:(clause -> unit) -> add_clause_pool:(clause -> clause_pool -> unit) -> t -> unit end = struct type t = { clauses_to_add_learnt: CVec.t; (* Clauses either assumed or pushed by the theory, waiting to be added. *) clauses_to_add_in_pool: (clause * clause_pool) Vec.t; (* clauses to add into a pool *) mutable prop_level: int; propagate: (atom * int * clause lazy_t) Vec.t; decisions: atom Vec.t; } let create () : t = { clauses_to_add_learnt = CVec.create (); clauses_to_add_in_pool = Vec.create (); prop_level = -1; propagate = Vec.create (); decisions = Vec.create (); } let clear self = let { clauses_to_add_learnt; clauses_to_add_in_pool; prop_level = _; propagate; decisions; } = self in Vec.clear clauses_to_add_in_pool; CVec.clear clauses_to_add_learnt; Vec.clear propagate; Vec.clear decisions let clear_on_backtrack self = let { clauses_to_add_learnt = _; clauses_to_add_in_pool = _; propagate; prop_level = _; decisions; } = self in Vec.clear propagate; Vec.clear decisions let is_empty self = let { clauses_to_add_learnt; clauses_to_add_in_pool; prop_level = _; propagate; decisions; } = self in Vec.is_empty clauses_to_add_in_pool && CVec.is_empty clauses_to_add_learnt && Vec.is_empty decisions && Vec.is_empty propagate let add_clause_learnt (self : t) c = CVec.push self.clauses_to_add_learnt c let propagate_atom self p ~lvl c = if (not (Vec.is_empty self.propagate)) && lvl < self.prop_level then Vec.clear self.propagate (* will be immediately backtracked *); if lvl <= self.prop_level then ( self.prop_level <- lvl; Vec.push self.propagate (p, lvl, c) ) let add_decision self p = Vec.push self.decisions p let iter ~decision ~propagate ~add_clause_learnt ~add_clause_pool self : unit = let { clauses_to_add_learnt; clauses_to_add_in_pool; prop_level = _; propagate = prop; decisions; } = self in Vec.iter clauses_to_add_in_pool ~f:(fun (c, p) -> add_clause_pool c p); CVec.iter ~f:add_clause_learnt clauses_to_add_learnt; Vec.iter ~f:decision decisions; Vec.iter prop ~f:(fun (p, lvl, c) -> propagate p ~lvl c); clear self; () end (* Singleton type containing the current state *) type t = { store: store; (* atom/var/clause store *) plugin: plugin; (* user defined theory *) proof: Proof_trace.t; (* the proof object *) (* Clauses are simplified for efficiency purposes. In the following vectors, the comments actually refer to the original non-simplified clause. *) clauses_hyps: CVec.t; (* clauses added by the user, never removed *) max_clauses_learnt: int ref; (* used to direct GC in {!clauses_learnt} *) clauses_learnt: clause_pool; (* learnt clauses (tautologies true at any time, whatever the user level). GC'd regularly. *) clause_pools: clause_pool Vec.t; (* custom clause pools *) delayed_actions: Delayed_actions.t; mutable unsat_at_0: clause option; (* conflict at level 0, if any *) mutable next_decisions: atom list; (* When the last conflict was a semantic one (mcsat), this stores the next decision to make; if some theory wants atoms to be decided on (for theory combination), store them here. *) trail: AVec.t; (* decision stack + propagated elements (atoms or assignments). *) var_levels: Veci.t; (* decision levels in [trail] *) assumptions: AVec.t; (* current assumptions *) mutable th_head: int; (* Start offset in the queue {!trail} of unit facts not yet seen by the theory. *) mutable elt_head: int; (* Start offset in the queue {!trail} of unit facts to propagate, within the trail *) (* invariant: - during propagation, th_head <= elt_head - then, once elt_head reaches length trail, Th.assume is called so that th_head can catch up with elt_head - this is repeated until a fixpoint is reached; - before a decision (and after the fixpoint), th_head = elt_head = length trail *) order: H.t; (* Heap ordered by variable activity *) to_clear: var Vec.t; (* variables to unmark *) (* temporaries *) temp_atom_vec: AVec.t; temp_clause_vec: CVec.t; temp_step_vec: Step_vec.t; mutable var_incr: float; (* increment for variables' activity *) mutable clause_incr: float; (* increment for clauses' activity *) (* FIXME: use event *) on_conflict: (Clause.t, unit) Event.Emitter.t; on_decision: (Lit.t, unit) Event.Emitter.t; on_learnt: (Clause.t, unit) Event.Emitter.t; on_gc: (Lit.t array, unit) Event.Emitter.t; stat: Stat.t; n_conflicts: int Stat.counter; n_propagations: int Stat.counter; n_decisions: int Stat.counter; n_restarts: int Stat.counter; n_minimized_away: int Stat.counter; } type solver = t (* intial restart limit *) let restart_first = 100 let _nop_on_conflict (_ : atom array) = () (* Starting environment. *) let create_ ~store ~proof ~stat ~max_clauses_learnt (plugin : plugin) : t = { store; plugin; unsat_at_0 = None; next_decisions = []; max_clauses_learnt; clauses_hyps = CVec.create (); clauses_learnt = make_gc_clause_pool_ ~descr:(fun () -> "cp.learnt-clauses") ~max_size:max_clauses_learnt (); delayed_actions = Delayed_actions.create (); clause_pools = Vec.create (); to_clear = Vec.create (); temp_clause_vec = CVec.create (); temp_atom_vec = AVec.create (); temp_step_vec = Step_vec.create (); th_head = 0; elt_head = 0; trail = AVec.create (); var_levels = Veci.create (); assumptions = AVec.create (); order = H.create store; var_incr = 1.; clause_incr = 1.; proof; stat; n_conflicts = Stat.mk_int stat "sat.n-conflicts"; n_decisions = Stat.mk_int stat "sat.n-decisions"; n_propagations = Stat.mk_int stat "sat.n-propagations"; n_restarts = Stat.mk_int stat "sat.n-restarts"; n_minimized_away = Stat.mk_int stat "sat.n-confl-lits-minimized-away"; on_conflict = Event.Emitter.create (); on_decision = Event.Emitter.create (); on_learnt = Event.Emitter.create (); on_gc = Event.Emitter.create (); } let on_gc self = Event.of_emitter self.on_gc let on_conflict self = Event.of_emitter self.on_conflict let on_decision self = Event.of_emitter self.on_decision let on_learnt self = Event.of_emitter self.on_learnt (* iterate on all learnt clauses, pools included *) let iter_clauses_learnt_ (self : t) ~f : unit = let[@inline] iter_pool (module P : CLAUSE_POOL) = P.iter ~f in iter_pool self.clauses_learnt; Vec.iter ~f:iter_pool self.clause_pools; () let[@inline] decision_level st = Veci.size st.var_levels let[@inline] nb_clauses st = CVec.size st.clauses_hyps let stat self = self.stat (* Do we have a level-0 empty clause? *) let[@inline] check_unsat_ st = match st.unsat_at_0 with | Some c -> raise (E_unsat (US_false c)) | None -> () (* Variable and literal activity. Activity is used to decide on which variable to decide when propagation is done. Uses a heap (implemented in Iheap), to keep track of variable activity. To be more general, the heap only stores the variable/literal id (i.e an int). *) let[@inline] insert_var_order st (v : var) : unit = H.insert st.order v (* find atom for the lit, if any *) let[@inline] find_atom_ (self : t) (p : Lit.t) : atom option = Store.find_atom self.store p (* create a new atom, pushing it into the decision queue if needed *) let make_atom_ (self : t) ?default_pol (p : Lit.t) : atom = let a = Store.alloc_atom self.store ?default_pol p in if Atom.level self.store a < 0 then insert_var_order self (Atom.var a) else assert (Atom.is_true self.store a || Atom.is_false self.store a); a (* Rather than iterate over all the heap when we want to decrease all the variables/literals activity, we instead increase the value by which we increase the activity of 'interesting' var/lits. *) let[@inline] var_decay_activity st = st.var_incr <- st.var_incr *. var_decay let[@inline] clause_decay_activity st = st.clause_incr <- st.clause_incr *. clause_decay (* increase activity of [v] *) let var_bump_activity self v = let store = self.store in Var.set_weight store v (Var.weight store v +. self.var_incr); if Var.weight store v > 1e100 then ( Store.iter_vars store (fun v -> Var.set_weight store v (Var.weight store v *. 1e-100)); self.var_incr <- self.var_incr *. 1e-100 ); if H.in_heap self.order v then H.decrease self.order v (* increase activity of clause [c] *) let clause_bump_activity self (c : clause) : unit = let store = self.store in Clause.set_activity store c (Clause.activity store c +. self.clause_incr); if Clause.activity store c > 1e20 then ( let update_clause c = Clause.set_activity store c (Clause.activity store c *. 1e-20) in iter_clauses_learnt_ self ~f:update_clause; self.clause_incr <- self.clause_incr *. 1e-20 ) (* Simplification of clauses. When adding new clauses, it is desirable to 'simplify' them, i.e minimize the amount of literals in it, because it greatly reduces the search space for new watched literals during propagation. Additionally, we have to partition the lits, to ensure the watched literals (which are the first two lits of the clause) are appropriate. Indeed, it is better to watch true literals, and then unassigned literals. Watching false literals should be a last resort, and come with constraints (see {!add_clause}). *) exception Trivial (* get/build the proof for [a], which must be an atom true at level 0. This uses a global cache to avoid repeated computations, as many clauses might resolve against a given 0-level atom. *) let rec proof_of_atom_lvl0_ (self : t) (a : atom) : Proof_step.id = assert (Atom.is_true self.store a && Atom.level self.store a = 0); match Atom.proof_lvl0 self.store a with | Some p -> p | None -> let p = match Atom.reason self.store a with | None -> assert false | Some Decision -> assert false (* no decisions at level0 *) | Some (Bcp c2 | Bcp_lazy (lazy c2)) -> Log.debugf 50 (fun k -> k "(@[sat.proof-of-atom-lvl0.clause@ %a@])" (Clause.debug self.store) c2); let steps = ref [] in (* recurse, so we get the whole level-0 resolution graph *) Clause.iter self.store c2 ~f:(fun a2 -> if not (Var.equal (Atom.var a) (Atom.var a2)) then ( Log.debugf 50 (fun k -> k "(@[sat.proof-of-atom-lvl0@ :of %a@ @[:resolve-with@ \ %a@]@])" (Atom.debug self.store) a (Atom.debug self.store) a2); let p2 = proof_of_atom_lvl0_ self (Atom.neg a2) in steps := p2 :: !steps )); let proof_c2 = Clause.proof_step self.store c2 in if !steps = [] then proof_c2 else Proof_trace.add_step self.proof @@ fun () -> Proof_sat.sat_redundant_clause [ Atom.lit self.store a ] ~hyps:Iter.(cons proof_c2 (of_list !steps)) in Atom.set_proof_lvl0 self.store a p; (* put in cache *) p (* Preprocess clause, by doing the following: - Partition literals for new clauses, into: - true literals (maybe makes the clause trivial if the lit is proved true at level 0) - unassigned literals, yet to be decided - false literals (not suitable to watch, those at level 0 can be removed from the clause) and order them as such in the result. - Also, removes literals that are false at level0, and returns a proof for their removal. - Also, removes duplicates. *) let preprocess_clause_ (self : t) (c : Clause.t) : Clause.t = let store = self.store in let res0_proofs = ref [] in (* steps of resolution at level 0 *) let add_proof_lvl0_ p = res0_proofs := p :: !res0_proofs in let trues = Vec.create () in let unassigned = Vec.create () in let falses = Vec.create () in (* cleanup marks used to detect duplicates *) let cleanup () = Clause.iter store c ~f:(fun a -> Store.clear_var store (Atom.var a)) in let consider_atom (a : atom) : unit = if not (Atom.marked store a) then ( Atom.mark store a; if Atom.marked_both store a then raise Trivial; if Atom.is_true store a then ( let lvl = Atom.level store a in if lvl = 0 then (* Atom true at level 0 gives a trivially true clause *) raise Trivial; Vec.push trues a ) else if Atom.is_false store a then ( let lvl = Atom.level store a in if lvl = 0 then ( (* Atom var false at level 0 can be eliminated from the clause, but we need to kepp in mind that we used another clause to simplify it. *) Log.debugf 50 (fun k -> k "(@[sat.preprocess-clause.resolve-away-lvl0@ %a@])" (Atom.debug store) a); let p = proof_of_atom_lvl0_ self (Atom.neg a) in add_proof_lvl0_ p ) else Vec.push falses a ) else Vec.push unassigned a ) in (try Clause.iter store c ~f:consider_atom; cleanup () with e -> cleanup (); raise e); (* merge all atoms together *) let atoms = let v = trues in Vec.append ~into:v unassigned; Vec.append ~into:v falses; Vec.to_array v in if !res0_proofs = [] then (* no change except in the order of literals *) Clause.make_a store atoms ~removable:(Clause.removable store c) (Clause.proof_step store c) else ( assert (Array.length atoms < Clause.n_atoms store c); (* some atoms were removed by resolution with level-0 clauses *) Log.debugf 30 (fun k -> k "(@[sat.add-clause.resolved-lvl-0@ :into [@[%a@]]@])" (Atom.debug_a store) atoms); let proof = Proof_trace.add_step self.proof @@ fun () -> let lits = Util.array_to_list_map (Atom.lit store) atoms in let hyps = Iter.(cons (Clause.proof_step self.store c) (of_list !res0_proofs)) in Proof_sat.sat_redundant_clause lits ~hyps in Clause.make_a store atoms proof ~removable:(Clause.removable store c) ) let new_decision_level (self : t) = assert (self.th_head = AVec.size self.trail); assert (self.elt_head = AVec.size self.trail); Veci.push self.var_levels (AVec.size self.trail); let (module P) = self.plugin in P.push_level (); () (* Attach/Detach a clause. Atom clause is attached (to its watching lits) when it is first added, either because it is assumed or learnt. *) let attach_clause (self : t) c = let store = self.store in assert (not @@ Clause.attached store c); Log.debugf 20 (fun k -> k "(@[sat.attach-clause@ %a@])" (Clause.debug store) c); (* TODO: change when watchlist are updated *) CVec.push (Atom.watched store (Atom.neg (Clause.atoms_a store c).(0))) c; CVec.push (Atom.watched store (Atom.neg (Clause.atoms_a store c).(1))) c; Clause.set_attached store c true; () (* Backtracking. Used to backtrack, i.e cancel down to [lvl] excluded, i.e we want to go back to the state the solver was in after decision level [lvl] was created and fully propagated. *) let cancel_until (self : t) lvl = let store = self.store in assert (lvl >= 0); (* Nothing to do if we try to backtrack to a non-existent level. *) if decision_level self <= lvl then Log.debugf 20 (fun k -> k "(@[sat.cancel-until.nop@ :already-at-level <= %d@])" lvl) else ( Log.debugf 5 (fun k -> k "(@[sat.cancel-until %d@])" lvl); (* We set the head of the solver and theory queue to what it was. *) let head = ref (Veci.get self.var_levels lvl) in self.elt_head <- !head; self.th_head <- !head; (* Now we need to cleanup the vars that are not valid anymore (i.e to the right of elt_head in the queue. *) for c = self.elt_head to AVec.size self.trail - 1 do let a = AVec.get self.trail c in (* Atom literal is unassigned, we nedd to add it back to the heap of potentially assignable literals, unless it has a level lower than [lvl], in which case we just move it back. *) (* Atom variable is not true/false anymore, one of two things can happen: *) if Atom.level store a <= lvl then ( (* It is a late propagation, which has a level lower than where we backtrack, so we just move it to the head of the queue, to be propagated again. *) AVec.set self.trail !head a; head := !head + 1 ) else ( (* it is a result of bolean propagation, or a semantic propagation with a level higher than the level to which we backtrack, in that case, we simply unset its value and reinsert it into the heap. *) Atom.set_is_true store a false; Atom.set_is_true store (Atom.neg a) false; Var.set_level store (Atom.var a) (-1); Var.set_reason store (Atom.var a) None; insert_var_order self (Atom.var a) ) done; (* Recover the right theory state. *) let n = decision_level self - lvl in assert (n > 0); (* Resize the vectors according to their new size. *) AVec.shrink self.trail !head; Veci.shrink self.var_levels lvl; let (module P) = self.plugin in P.pop_levels n; Delayed_actions.clear_on_backtrack self.delayed_actions; (* TODO: for scoped clause pools, backtrack them *) self.next_decisions <- [] ); () let pp_unsat_cause self out = function | US_local { first = _; core } -> Format.fprintf out "(@[unsat-cause@ :false-assumptions %a@])" (Format.pp_print_list (Atom.pp self.store)) core | US_false c -> Format.fprintf out "(@[unsat-cause@ :false %a@])" (Clause.debug self.store) c (* Unsatisfiability is signaled through an exception, since it can happen in multiple places (adding new clauses, or solving for instance). *) let report_unsat self (us : unsat_cause) : _ = Log.debugf 3 (fun k -> k "(@[sat.unsat-conflict@ %a@])" (pp_unsat_cause self) us); let us = match us with | US_false c -> self.unsat_at_0 <- Some c; Event.emit self.on_learnt c; let p = Clause.proof_step self.store c in Proof_trace.add_unsat self.proof p; US_false c | US_local _ -> us in raise (E_unsat us) (* Boolean propagation. Wrapper function for adding a new propagated lit. *) let enqueue_bool (self : t) a ~level:lvl reason : unit = let store = self.store in if Atom.is_false store a then ( Log.debugf 0 (fun k -> k "(@[sat.error.trying to enqueue a false literal %a@])" (Atom.debug store) a); assert false ); assert ( (not (Atom.is_true store a)) && Atom.level store a < 0 && Atom.reason store a == None && lvl >= 0); (* backtrack if required *) if lvl < decision_level self then cancel_until self lvl; Atom.set_is_true store a true; Var.set_level store (Atom.var a) lvl; Var.set_reason store (Atom.var a) (Some reason); AVec.push self.trail a; Log.debugf 20 (fun k -> k "(@[sat.enqueue[%d]@ %a@])" (AVec.size self.trail) (Atom.debug store) a); () (* swap elements of array *) let[@inline] swap_arr a i j = if i <> j then ( let tmp = a.(i) in a.(i) <- a.(j); a.(j) <- tmp ) (* move atoms assigned at high levels first *) let put_high_level_atoms_first (store : store) (arr : atom array) : unit = Array.iteri (fun i a -> if i > 0 && Atom.level store a > Atom.level store arr.(0) then if (* move first to second, [i]-th to first, second to [i] *) i = 1 then swap_arr arr 0 1 else ( let tmp = arr.(1) in arr.(1) <- arr.(0); arr.(0) <- arr.(i); arr.(i) <- tmp ) else if i > 1 && Atom.level store a > Atom.level store arr.(1) then swap_arr arr 1 i) arr (* find which level to backtrack to, given a conflict clause and a boolean stating whether it is a UIP ("Unique Implication Point") precond: the atom list is sorted by decreasing decision level *) let backtrack_lvl (self : t) (arr : atom array) : int * bool = let store = self.store in if Array.length arr <= 1 then 0, true else ( let a = arr.(0) in let b = arr.(1) in assert (Atom.level store a > 0); if Atom.level store a > Atom.level store b then ( (* backtrack below [a], so we can propagate [not a] *) Atom.level store b, true ) else ( assert (Atom.level store a = Atom.level store b); assert (Atom.level store a >= 0); max (Atom.level store a - 1) 0, false ) ) (* abtraction of the assignment level of [v] in an integer *) let[@inline] abstract_level_ (self : t) (v : var) : int = 1 lsl (Var.level self.store v land 31) exception Non_redundant (* can we remove [a] by self-subsuming resolutions with other lits of the learnt clause? *) let lit_redundant (self : t) (abstract_levels : int) (steps : Step_vec.t) (v : var) : bool = let store = self.store in let to_unmark = self.to_clear in let steps_size_init = Step_vec.size steps in (* save current state of [to_unmark] *) let top = Vec.size to_unmark in let rec aux v = match Var.reason store v with | None -> assert false | Some Decision -> raise_notrace Non_redundant | Some (Bcp c | Bcp_lazy (lazy c)) -> let c_atoms = Clause.atoms_a store c in assert (Var.equal v (Atom.var c_atoms.(0))); if Proof_trace.enabled self.proof then Step_vec.push steps (Clause.proof_step self.store c); (* check that all the other lits of [c] are marked or redundant *) for i = 1 to Array.length c_atoms - 1 do let v2 = Atom.var c_atoms.(i) in let lvl_v2 = Var.level store v2 in if not (Var.marked store v2) then ( match Var.reason store v2 with | None -> assert false | _ when lvl_v2 = 0 -> (* can always remove literals at level 0, but got to update proof properly *) if Proof_trace.enabled self.proof then ( let p = proof_of_atom_lvl0_ self (Atom.neg c_atoms.(i)) in Step_vec.push steps p ) | Some (Bcp _ | Bcp_lazy _) when abstract_level_ self v2 land abstract_levels <> 0 -> (* possibly removable, its level may comprise an atom in learnt clause *) Vec.push to_unmark v2; Var.mark store v2; aux v2 | Some _ -> raise_notrace Non_redundant ) done in try aux v; true with Non_redundant -> (* clear new marks, they are not actually redundant *) for i = top to Vec.size to_unmark - 1 do Var.unmark store (Vec.get to_unmark i) done; Vec.shrink to_unmark top; Step_vec.shrink steps steps_size_init; (* restore proof *) false (* minimize conflict by removing atoms whose propagation history is ultimately self-subsuming with [lits] *) let minimize_conflict (self : t) (_c_level : int) (learnt : AVec.t) (steps : Step_vec.t) : unit = let store = self.store in (* abstraction of the levels involved in the conflict at all, as logical "or" of each literal's approximate level *) let abstract_levels = AVec.fold_left (fun lvl a -> lvl lor abstract_level_ self (Atom.var a)) 0 learnt in let j = ref 1 in for i = 1 to AVec.size learnt - 1 do let a = AVec.get learnt i in let keep = match Atom.reason store a with | Some Decision -> true (* always keep decisions *) | Some (Bcp _ | Bcp_lazy _) -> not (lit_redundant self abstract_levels steps (Atom.var a)) | None -> assert false in if keep then ( AVec.set learnt !j a; incr j ) else Stat.incr self.n_minimized_away done; AVec.shrink learnt !j; () (* result of conflict analysis, containing the learnt clause and some additional info. *) type conflict_res = { cr_backtrack_lvl: int; (* level to backtrack to *) cr_learnt: atom array; (* lemma learnt from conflict *) cr_is_uip: bool; (* conflict is UIP? *) cr_steps: Step_vec.t; } (* conflict analysis, starting with top of trail and conflict clause *) let analyze (self : t) (c_clause : clause) : conflict_res = let store = self.store in let to_unmark = self.to_clear in (* for cleanup *) Vec.clear to_unmark; let learnt = self.temp_atom_vec in AVec.clear learnt; let steps = self.temp_step_vec in (* for proof *) assert (Step_vec.is_empty steps); (* loop variables *) let pathC = ref 0 in let continue = ref true in let blevel = ref 0 in let c = ref (Some c_clause) in (* current clause to analyze/resolve *) let tr_ind = ref (AVec.size self.trail - 1) in (* pointer in trail *) (* conflict level *) assert (decision_level self > 0); let conflict_level = let (module P) = self.plugin in if P.has_theory then Clause.fold store 0 c_clause ~f:(fun acc p -> max acc (Atom.level store p)) else decision_level self in Log.debugf 30 (fun k -> k "(@[sat.analyze-conflict@ :c-level %d@ :clause %a@])" conflict_level (Clause.debug store) c_clause); while !continue do (match !c with | None -> Log.debug 30 "(@[sat.analyze-conflict: skipping resolution for semantic \ propagation@])" | Some clause -> Log.debugf 30 (fun k -> k "(@[sat.analyze-conflict.resolve@ %a@])" (Clause.debug store) clause); if Clause.removable store clause then clause_bump_activity self clause; if Proof_trace.enabled self.proof then Step_vec.push steps (Clause.proof_step self.store clause); (* visit the current predecessors *) let atoms = Clause.atoms_a store clause in for j = 0 to Array.length atoms - 1 do let q = atoms.(j) in assert (Atom.has_value store q); assert (Atom.level store q >= 0); if Atom.level store q = 0 then ( (* skip [q] entirely, resolved away at level 0 *) assert (Atom.is_false store q); if Proof_trace.enabled self.proof then ( let step = proof_of_atom_lvl0_ self (Atom.neg q) in Step_vec.push steps step ) ) else if not (Var.marked store (Atom.var q)) then ( Var.mark store (Atom.var q); Vec.push to_unmark (Atom.var q); if Atom.level store q > 0 then ( var_bump_activity self (Atom.var q); if Atom.level store q >= conflict_level then incr pathC else ( AVec.push learnt q; blevel := max !blevel (Atom.level store q) ) ) ) done); (* look for the next node to expand *) while let a = AVec.get self.trail !tr_ind in Log.debugf 30 (fun k -> k "(@[sat.analyze-conflict.at-trail-elt@ %a@])" (Atom.debug store) a); (not (Var.marked store (Atom.var a))) || Atom.level store a < conflict_level do decr tr_ind done; let p = AVec.get self.trail !tr_ind in decr pathC; decr tr_ind; match !pathC, Atom.reason store p with | 0, _ -> continue := false; AVec.push learnt (Atom.neg p) | n, Some (Bcp cl | Bcp_lazy (lazy cl)) -> assert (n > 0); assert (Atom.level store p >= conflict_level); c := Some cl | _, (None | Some Decision) -> assert false done; Log.debugf 10 (fun k -> k "(@[sat.conflict.res@ %a@])" (AVec.pp @@ Atom.debug store) learnt); (* minimize conflict, to get a more general lemma *) minimize_conflict self conflict_level learnt steps; let cr_steps = Step_vec.copy steps in Step_vec.clear self.temp_step_vec; (* cleanup marks *) Vec.iter ~f:(Store.clear_var store) to_unmark; Vec.clear to_unmark; (* put high-level literals first, so that: - they make adequate watch lits - the first literal is the UIP, if any *) let cr_learnt = AVec.to_array learnt in AVec.clear learnt; Array.sort (fun p q -> compare (Atom.level store q) (Atom.level store p)) cr_learnt; (* put_high_level_atoms_first a; *) let level, is_uip = backtrack_lvl self cr_learnt in Log.debugf 10 (fun k -> k "(@[sat.conflict.res.final@ :lvl %d@ {@[%a@]}@])" level (Util.pp_array @@ Atom.debug store) cr_learnt); { cr_backtrack_lvl = level; cr_learnt; cr_is_uip = is_uip; cr_steps } (* Get the correct vector to insert a clause in. *) let[@inline] add_clause_to_vec_ ~pool self c = if Clause.removable self.store c && Clause.n_atoms self.store c > 2 then (* add clause to some pool/set of clauses *) cp_add_ pool c else CVec.push self.clauses_hyps c (* add the learnt clause to the clause database, propagate, etc. *) let record_learnt_clause (self : t) ~pool (cr : conflict_res) : unit = let store = self.store in (match cr.cr_learnt with | [||] -> assert false | [| fuip |] -> assert (cr.cr_backtrack_lvl = 0 && decision_level self = 0); let p = Proof_trace.add_step self.proof @@ fun () -> let lits = Util.array_to_list_map (Atom.lit self.store) cr.cr_learnt in Proof_sat.sat_redundant_clause lits ~hyps:(Step_vec.to_iter cr.cr_steps) in let uclause = Clause.make_a store ~removable:true cr.cr_learnt p in Event.emit self.on_learnt uclause; if Atom.is_false store fuip then (* incompatible at level 0 *) report_unsat self (US_false uclause) else (* no need to attach [uclause], it is true at level 0 *) enqueue_bool self fuip ~level:0 (Bcp uclause) | _ -> let fuip = cr.cr_learnt.(0) in let p = Proof_trace.add_step self.proof @@ fun () -> let lits = Util.array_to_list_map (Atom.lit self.store) cr.cr_learnt in Proof_sat.sat_redundant_clause lits ~hyps:(Step_vec.to_iter cr.cr_steps) in let lclause = Clause.make_a store ~removable:true cr.cr_learnt p in add_clause_to_vec_ ~pool self lclause; attach_clause self lclause; clause_bump_activity self lclause; Event.emit self.on_learnt lclause; assert cr.cr_is_uip; enqueue_bool self fuip ~level:cr.cr_backtrack_lvl (Bcp lclause)); var_decay_activity self; clause_decay_activity self (* process a conflict: - learn clause - backtrack - report unsat if conflict at level 0 *) let add_boolean_conflict (self : t) (confl : clause) : unit = let store = self.store in Log.debugf 5 (fun k -> k "(@[sat.add-bool-conflict@ %a@])" (Clause.debug store) confl); self.next_decisions <- []; assert (decision_level self >= 0); if decision_level self = 0 || Clause.for_all store confl ~f:(fun a -> Atom.level store a <= 0) then (* Top-level conflict *) report_unsat self (US_false confl); let cr = analyze self confl in cancel_until self (max cr.cr_backtrack_lvl 0); record_learnt_clause ~pool:self.clauses_learnt self cr (* Add a new clause, simplifying, propagating, and backtracking if the clause is false in the current trail *) let add_clause_ (self : t) ~pool (init : clause) : unit = let store = self.store in Log.debugf 30 (fun k -> k "(@[sat.add-clause@ @[%a@]@])" (Clause.debug store) init); (* Insertion of new lits is done before simplification. Indeed, else a lit in a trivial clause could end up being not decided on, which is a bug. *) Clause.iter store init ~f:(fun x -> insert_var_order self (Atom.var x)); try (* preprocess to remove dups, sort literals, etc. *) let clause = preprocess_clause_ self init in assert (Clause.removable store clause = Clause.removable store init); Log.debugf 5 (fun k -> k "(@[sat.new-clause@ @[%a@]@])" (Clause.debug store) clause); let atoms = Clause.atoms_a self.store clause in match atoms with | [||] -> report_unsat self @@ US_false clause | [| a |] -> cancel_until self 0; if Atom.is_false store a then (* cannot recover from this *) report_unsat self @@ US_false clause else if Atom.is_true store a then () (* atom is already true, (at level 0) nothing to do *) else ( Log.debugf 40 (fun k -> k "(@[sat.add-clause.unit-clause@ :propagating %a@])" (Atom.debug store) a); add_clause_to_vec_ ~pool self clause; enqueue_bool self a ~level:0 (Bcp clause) ) | _ -> let a = atoms.(0) in let b = atoms.(1) in add_clause_to_vec_ ~pool self clause; if Atom.is_false store a then ( (* Atom need to be sorted in decreasing order of decision level, or we might watch the wrong literals. *) put_high_level_atoms_first store (Clause.atoms_a store clause); attach_clause self clause; add_boolean_conflict self clause ) else ( attach_clause self clause; if Atom.is_false store b && not (Atom.has_value store a) then ( (* unit, propagate [a] *) let lvl = Array.fold_left (fun m a -> max m (Atom.level store a)) 0 atoms in cancel_until self lvl; Log.debugf 50 (fun k -> k "(@[sat.add-clause.propagate@ %a@ :lvl %d@])" (Atom.debug store) a lvl); enqueue_bool self a ~level:lvl (Bcp clause) ) ) with Trivial -> Log.debugf 5 (fun k -> k "(@[sat.add-clause@ :ignore-trivial @[%a@]@])" (Clause.debug store) init) type watch_res = Watch_kept | Watch_removed (* boolean propagation. [a] is the false atom, one of [c]'s two watch literals [i] is the index of [c] in [a.watched] @return whether [c] was removed from [a.watched] *) let propagate_in_clause (self : t) (a : atom) (c : clause) (i : int) : watch_res = let store = self.store in let atoms = Clause.atoms_a store c in let first = atoms.(0) in if first = Atom.neg a then ( (* false lit must be at index 1 *) atoms.(0) <- atoms.(1); atoms.(1) <- first ) else assert (Atom.neg a = atoms.(1)); let first = atoms.(0) in if Atom.is_true store first then Watch_kept (* true clause, keep it in watched *) else ( try (* look for another watch lit *) for k = 2 to Array.length atoms - 1 do let ak = atoms.(k) in if not (Atom.is_false store ak) then ( (* watch lit found: update and exit *) atoms.(1) <- ak; atoms.(k) <- Atom.neg a; (* remove [c] from [a.watched], add it to [ak.neg.watched] *) CVec.push (Atom.watched store (Atom.neg ak)) c; assert (Clause.equal (CVec.get (Atom.watched store a) i) c); CVec.fast_remove (Atom.watched store a) i; raise_notrace Exit ) done; (* no watch lit found *) if Atom.is_false store first then ( (* clause is false *) self.elt_head <- AVec.size self.trail; raise_notrace (Conflict c) ) else ( Stat.incr self.n_propagations; enqueue_bool self first ~level:(decision_level self) (Bcp c) ); Watch_kept with Exit -> Watch_removed ) (* propagate atom [a], which was just decided. This checks every clause watching [a] to see if the clause is false, unit, or has other possible watches @param res the optional conflict clause that the propagation might trigger *) let propagate_atom (self : t) a : unit = let store = self.store in let watched = Atom.watched store a in let rec aux i = if i >= CVec.size watched then () else ( let c = CVec.get watched i in assert (Clause.attached store c); let j = if Clause.dead store c then i (* remove on the fly *) else ( match propagate_in_clause self a c i with | Watch_kept -> i + 1 | Watch_removed -> i (* clause at this index changed *) ) in aux j ) in aux 0 exception Th_conflict of Clause.t let acts_add_clause self ?(keep = false) (l : Lit.t list) (p : Proof_step.id) : unit = let atoms = List.rev_map (make_atom_ self) l in let removable = not keep in let c = Clause.make_l self.store ~removable atoms p in Log.debugf 5 (fun k -> k "(@[sat.th.add-clause@ %a@])" (Clause.debug self.store) c); (* will be added later, even if we backtrack *) Delayed_actions.add_clause_learnt self.delayed_actions c let acts_add_decision_lit (self : t) (f : Lit.t) (sign : bool) : unit = let store = self.store in let a = make_atom_ self f in let a = if sign then a else Atom.neg a in if not (Atom.has_value store a) then ( Log.debugf 10 (fun k -> k "(@[sat.th.add-decision-lit@ %a@])" (Atom.debug store) a); Delayed_actions.add_decision self.delayed_actions a ) let acts_raise self (l : Lit.t list) (p : Proof_step.id) : 'a = let atoms = List.rev_map (make_atom_ self) l in (* conflicts can be removed *) let c = Clause.make_l self.store ~removable:true atoms p in Log.debugf 5 (fun k -> k "(@[@{sat.th.raise-conflict@}@ %a@])" (Clause.debug self.store) c); (* we can shortcut the rest of the theory propagations *) raise_notrace (Th_conflict c) let check_consequence_lits_false_ self l p : unit = let store = self.store in Log.debugf 50 (fun k -> k "(@[sat.check-consequence-lits:@ :consequence (@[%a@])@ :for %a@])" (Util.pp_list (Atom.debug store)) l (Atom.debug store) p); match List.find (fun a -> Atom.is_true store a) l with | a -> invalid_argf "slice.acts_propagate:@ Consequence should contain only false literals,@ \ but @[%a@] is true@ when propagating %a" (Atom.debug store) p (Atom.debug store) a | exception Not_found -> () let acts_propagate (self : t) f (expl : reason) = let store = self.store in match expl with | Consequence mk_expl -> let p = make_atom_ self f in Log.debugf 30 (fun k -> k "(@[sat.propagate-from-theory@ %a@])" (Atom.debug store) p); if Atom.is_true store p then () else if Atom.is_false store p then ( let lits, proof = mk_expl () in let guard = List.rev_map (fun f -> Atom.neg @@ make_atom_ self f) lits in check_consequence_lits_false_ self guard p; let c = Clause.make_l store ~removable:true (p :: guard) proof in raise_notrace (Th_conflict c) ) else ( insert_var_order self (Atom.var p); let c, level = (* Check literals + proof eagerly, as it's safer. We could check invariants in a [lazy] block, as conflict analysis would run in an environment where the literals should be true anyway, since it's an extension of the current trail. (otherwise the propagated lit would have been backtracked and discarded already.) However it helps catching propagation bugs to verify truthiness of the guard (and level) eagerly. *) let lits, proof = mk_expl () in let guard = List.rev_map (fun f -> Atom.neg @@ make_atom_ self f) lits in check_consequence_lits_false_ self guard p; let level = List.fold_left (fun l a -> max l (Atom.level store a)) 0 guard in assert (level <= decision_level self); (* delay creating the actual clause. *) lazy (Clause.make_l store ~removable:true (p :: guard) proof), level in Delayed_actions.propagate_atom self.delayed_actions p ~lvl:level c ) let[@inline never] perform_delayed_actions_ (self : t) : unit = let add_clause_learnt c = add_clause_ ~pool:self.clauses_learnt self c and add_clause_pool c pool = add_clause_ ~pool self c and decision a = self.next_decisions <- a :: self.next_decisions and propagate p ~lvl c = if Atom.is_true self.store p then () else if Atom.is_false self.store p then raise_notrace (Th_conflict (Lazy.force c)) else ( Stat.incr self.n_propagations; enqueue_bool self p ~level:lvl (Bcp_lazy c) ) in Delayed_actions.iter self.delayed_actions ~add_clause_learnt ~add_clause_pool ~propagate ~decision; () let[@inline] has_no_delayed_actions (self : t) : bool = Delayed_actions.is_empty self.delayed_actions let[@inline] perform_delayed_actions self = if not (has_no_delayed_actions self) then perform_delayed_actions_ self let[@specialise] acts_iter self ~full head f : unit = for i = if full then 0 else head to AVec.size self.trail - 1 do let a = AVec.get self.trail i in f (Atom.lit self.store a) done let eval_atom_ self a = if Atom.is_true self.store a then L_true else if Atom.is_false self.store a then L_false else L_undefined let[@inline] acts_eval_lit self (f : Lit.t) : lbool = let a = make_atom_ self f in eval_atom_ self a let[@inline] acts_add_lit self ?default_pol f : unit = ignore (make_atom_ ?default_pol self f : atom) let[@inline] current_slice st : acts = let module M = struct let proof = st.proof let iter_assumptions = acts_iter st ~full:false st.th_head let eval_lit = acts_eval_lit st let add_lit = acts_add_lit st let add_clause = acts_add_clause st let propagate = acts_propagate st let raise_conflict c pr = acts_raise st c pr let add_decision_lit = acts_add_decision_lit st end in (module M) (* full slice, for [if_sat] final check *) let[@inline] full_slice st : acts = let module M = struct let proof = st.proof let iter_assumptions = acts_iter st ~full:true st.th_head let eval_lit = acts_eval_lit st let add_lit = acts_add_lit st let add_clause = acts_add_clause st let propagate = acts_propagate st let raise_conflict c pr = acts_raise st c pr let add_decision_lit = acts_add_decision_lit st end in (module M) (* Assert that the conflict is indeeed a conflict *) let check_is_conflict_ self (c : Clause.t) : unit = if not @@ Clause.for_all self.store c ~f:(Atom.is_false self.store) then ( Log.debugf 0 (fun k -> k "conflict should be false: %a" (Clause.debug self.store) c); assert false ) (* some boolean literals were decided/propagated within Msat. Now we need to inform the theory of those assumptions, so it can do its job. @return the conflict clause, if the theory detects unsatisfiability *) let rec theory_propagate self : clause option = assert (self.elt_head = AVec.size self.trail); assert (self.th_head <= self.elt_head); if self.th_head = self.elt_head then None (* fixpoint/no propagation *) else ( let slice = current_slice self in self.th_head <- self.elt_head; (* catch up *) let (module P) = self.plugin in match P.partial_check slice with | () -> perform_delayed_actions self; propagate self | exception Th_conflict c -> check_is_conflict_ self c; Clause.iter self.store c ~f:(fun a -> insert_var_order self (Atom.var a)); Some c ) (* fixpoint between boolean propagation and theory propagation @return a conflict clause, if any *) and propagate (st : t) : clause option = (* First, treat the stack of lemmas/actions added by the theory, if any *) perform_delayed_actions st; (* Now, check that the situation is sane *) assert (st.elt_head <= AVec.size st.trail); if st.elt_head = AVec.size st.trail then theory_propagate st else ( match while st.elt_head < AVec.size st.trail do let a = AVec.get st.trail st.elt_head in propagate_atom st a; st.elt_head <- st.elt_head + 1 done with | () -> theory_propagate st | exception Conflict c -> Some c ) (* compute unsat core from assumption [a] *) let analyze_final (self : t) (a : atom) : atom list = let store = self.store in Log.debugf 5 (fun k -> k "(@[sat.analyze-final@ :lit %a@])" (Atom.debug store) a); assert (Atom.is_false store a); let core = ref [ a ] in let idx = ref (AVec.size self.trail - 1) in Var.mark store (Atom.var a); let seen = ref [ Atom.var a ] in while !idx >= 0 do let a' = AVec.get self.trail !idx in if Var.marked store (Atom.var a') then ( match Atom.reason store a' with | Some Decision -> core := a' :: !core | Some (Bcp c | Bcp_lazy (lazy c)) -> Clause.iter store c ~f:(fun a -> let v = Atom.var a in if not (Var.marked store v) then ( seen := v :: !seen; Var.mark store v )) | None -> () ); decr idx done; List.iter (Var.unmark store) !seen; Log.debugf 5 (fun k -> k "(@[sat.analyze-final.done@ :core %a@])" (Format.pp_print_list (Atom.debug store)) !core); !core (* GC: remove some learnt clauses. This works even during the proof with a non empty trail. *) let reduce_clause_db (self : t) : unit = let store = self.store in Log.debugf 3 (fun k -> k "(@[sat.gc-clauses.start :max-learnt %d@])" !(self.max_clauses_learnt)); let to_be_gc = self.temp_clause_vec in (* clauses to collect *) assert (CVec.is_empty to_be_gc); (* atoms whose watches will need to be rebuilt to filter out dead clauses *) let dirty_atoms = self.temp_atom_vec in assert (AVec.is_empty dirty_atoms); (* [a] is watching at least one removed clause, we'll need to trim its watchlist *) let[@inline] mark_dirty_atom a = if not (Atom.marked store a) then ( Atom.mark store a; AVec.push dirty_atoms a ) in (* iter on the clauses that are used to explain atoms on the trail, which we must therefore keep for now as they might participate in conflict resolution *) let iter_clauses_on_trail ~f : unit = AVec.iter self.trail ~f:(fun a -> match Atom.reason store a with | Some (Bcp c) -> f c | Some (Bcp_lazy lc) when Lazy.is_val lc -> f (Lazy.force lc) | _ -> ()) in iter_clauses_on_trail ~f:(fun c -> Clause.set_marked store c true); (* first, mark clauses used on the trail, we cannot GC them. TODO: once we use DRUP, we can avoid marking level-0 explanations as they will never participate in resolution. *) AVec.iter ~f:(fun a -> match Atom.reason store a with | Some (Bcp c) -> Clause.set_marked store c true | Some (Bcp_lazy lc) when Lazy.is_val lc -> Clause.set_marked store (Lazy.force lc) true | _ -> ()) self.trail; (* GC the clause [c] *) let flag_clause_for_gc c : unit = assert (Clause.removable store c); Log.debugf 10 (fun k -> k "(@[sat.gc.will-collect@ %a@])" (Clause.debug store) c); CVec.push to_be_gc c; Clause.set_dead store c true; let atoms = Clause.atoms_a store c in mark_dirty_atom (Atom.neg atoms.(0)); (* need to remove from watchlists *) mark_dirty_atom (Atom.neg atoms.(1)); Event.emit self.on_gc (Clause.lits_a store c); Proof_trace.delete self.proof (Clause.proof_step store c) in let gc_arg = (module struct let store = self.store let flag_clause_for_gc = flag_clause_for_gc let must_keep_clause c = Clause.marked store c end : GC_ARG) in (* GC a pool, if it needs it *) let gc_pool (module P : CLAUSE_POOL) : unit = if P.needs_gc () then ( Log.debugf 5 (fun k -> k "(@[sat.gc.pool@ :descr %s@])" (P.descr ())); P.gc gc_arg ) in gc_pool self.clauses_learnt; Vec.iter ~f:gc_pool self.clause_pools; let n_collected = CVec.size to_be_gc in (* update watchlist of dirty atoms *) AVec.iter dirty_atoms ~f:(fun a -> assert (Atom.marked store a); Atom.unmark store a; let w = Atom.watched store a in CVec.filter_in_place (fun c -> not (Clause.dead store c)) w); AVec.clear dirty_atoms; (* actually remove the clauses now that they are detached *) CVec.iter ~f:(Clause.dealloc store) to_be_gc; CVec.clear to_be_gc; (* remove marks on clauses on the trail *) iter_clauses_on_trail ~f:(fun c -> Clause.set_marked store c false); Log.debugf 3 (fun k -> k "(@[sat.gc.done :collected %d@])" n_collected); () (* Decide on a new literal, and enqueue it into the trail. Return [true] if a decision was made. @param full if true, do decisions; if false, only pick from [self.next_decisions] and [self.assumptions] *) let pick_branch_lit ~full self : bool = let rec pick_lit () = match self.next_decisions with | atom :: tl -> self.next_decisions <- tl; pick_with_given_atom atom | [] when decision_level self < AVec.size self.assumptions -> (* use an assumption *) let a = AVec.get self.assumptions (decision_level self) in if Atom.is_true self.store a then ( new_decision_level self; (* pseudo decision level, [a] is already true *) pick_lit () ) else if Atom.is_false self.store a then ( (* root conflict, find unsat core *) let core = analyze_final self a in raise (E_unsat (US_local { first = a; core })) ) else pick_with_given_atom a | [] when not full -> false | [] -> (match H.remove_min self.order with | v -> pick_with_given_atom (if Var.default_pol self.store v then Atom.pa v else Atom.na v) | exception Not_found -> false) (* pick a decision, trying [atom] first if it's not assigned yet. *) and pick_with_given_atom (atom : atom) : bool = let v = Atom.var atom in if Var.level self.store v >= 0 then ( assert ( Atom.is_true self.store (Atom.pa v) || Atom.is_true self.store (Atom.na v)); pick_lit () ) else ( new_decision_level self; let current_level = decision_level self in enqueue_bool self atom ~level:current_level Decision; Stat.incr self.n_decisions; Event.emit self.on_decision (Atom.lit self.store atom); true ) in pick_lit () (* do some amount of search, until the number of conflicts or clause learnt reaches the given parameters *) let search (self : t) ~on_progress ~(max_conflicts : int) : unit = let@ () = Profile.with_ "sat.search" in Log.debugf 3 (fun k -> k "(@[sat.search@ :max-conflicts %d@ :max-learnt %d@])" max_conflicts !(self.max_clauses_learnt)); let n_conflicts = ref 0 in while true do match propagate self with | Some confl -> (* Conflict *) incr n_conflicts; (* When the theory has raised Unsat, add_boolean_conflict might 'forget' the initial conflict clause, and only add the analyzed backtrack clause. So in those case, we use add_clause to make sure the initial conflict clause is also added. *) if Clause.attached self.store confl then add_boolean_conflict self confl else add_clause_ ~pool:self.clauses_learnt self confl; Stat.incr self.n_conflicts; Event.emit self.on_conflict confl | None -> (* No Conflict *) assert (self.elt_head = AVec.size self.trail); assert (self.elt_head = self.th_head); if max_conflicts > 0 && !n_conflicts >= max_conflicts then ( Profile.instant "sat.restart"; Log.debug 1 "(sat.restarting)"; cancel_until self 0; Stat.incr self.n_restarts; raise_notrace Restart ); (* if decision_level() = 0 then simplify (); *) let do_gc = !(self.max_clauses_learnt) > 0 && cp_size_ self.clauses_learnt - AVec.size self.trail > !(self.max_clauses_learnt) || Vec.exists cp_needs_gc_ self.clause_pools in if do_gc then ( reduce_clause_db self; on_progress () ); let decided = pick_branch_lit ~full:true self in if not decided then raise_notrace E_sat done let eval_level (self : t) (a : atom) = let lvl = Atom.level self.store a in if Atom.is_true self.store a then ( assert (lvl >= 0); true, lvl ) else if Atom.is_false self.store a then false, lvl else raise UndecidedLit let[@inline] eval st lit = fst @@ eval_level st lit (* fixpoint of propagation and decisions until a model is found, or a conflict is reached *) let solve_ ~on_progress (self : t) : unit = let@ () = Profile.with_ "sat.solve" in Log.debugf 5 (fun k -> k "(@[sat.solve :assms %d@])" (AVec.size self.assumptions)); check_unsat_ self; try perform_delayed_actions self; (* add initial clauses *) let max_conflicts = ref (float_of_int restart_first) in let max_learnt = ref (float_of_int (nb_clauses self) *. learntsize_factor) in while true do on_progress (); try self.max_clauses_learnt := int_of_float !max_learnt; search self ~on_progress ~max_conflicts:(int_of_float !max_conflicts) with | Restart -> max_conflicts := !max_conflicts *. restart_inc; max_learnt := !max_learnt *. learntsize_inc | E_sat -> assert ( self.elt_head = AVec.size self.trail && has_no_delayed_actions self && self.next_decisions = []); on_progress (); let (module P) = self.plugin in (match P.final_check (full_slice self) with | () -> if self.elt_head = AVec.size self.trail && has_no_delayed_actions self && self.next_decisions = [] then (* nothing more to do, that means the plugin is satisfied with the trail *) raise_notrace E_sat; (* otherwise, keep on *) perform_delayed_actions self | exception Th_conflict c -> check_is_conflict_ self c; Clause.iter self.store c ~f:(fun a -> insert_var_order self (Atom.var a)); Profile.instant "sat.th-conflict"; Log.debugf 5 (fun k -> k "(@[sat.theory-conflict-clause@ %a@])" (Clause.debug self.store) c); Stat.incr self.n_conflicts; Event.emit self.on_conflict c; Delayed_actions.add_clause_learnt self.delayed_actions c; perform_delayed_actions self; on_progress ()) done with E_sat -> () let assume self cnf : unit = List.iter (fun l -> let atoms = Util.array_of_list_map (make_atom_ self) l in let proof = Proof_trace.add_step self.proof @@ fun () -> Proof_sat.sat_input_clause l in let c = Clause.make_a self.store ~removable:false atoms proof in Log.debugf 10 (fun k -> k "(@[sat.assume-clause@ @[%a@]@])" (Clause.debug self.store) c); Delayed_actions.add_clause_learnt self.delayed_actions c) cnf let[@inline] store st = st.store let[@inline] proof st = st.proof let[@inline] add_lit self ?default_pol lit = ignore (make_atom_ self lit ?default_pol : atom) let[@inline] set_default_pol (self : t) (lit : Lit.t) (pol : bool) : unit = let a = make_atom_ self lit ~default_pol:pol in Var.set_default_pol self.store (Atom.var a) pol (* Result type *) type res = Sat of sat_state | Unsat of clause unsat_state let pp_all self lvl status = Log.debugf lvl (fun k -> k "(@[sat.full-state :res %s - Full summary:@,\ @[Trail:@\n\ %a@]@,\ @[Hyps:@\n\ %a@]@,\ @[Lemmas:@\n\ %a@]@,\ @]@." status (AVec.pp @@ Atom.debug self.store) self.trail (CVec.pp @@ Clause.debug self.store) self.clauses_hyps (Util.pp_iter @@ Clause.debug self.store) (cp_to_iter_ self.clauses_learnt)) let mk_sat (self : t) : sat_state = pp_all self 99 "SAT"; let t = self.trail in let module M = struct let iter_trail f = AVec.iter ~f:(fun a -> f (Atom.lit self.store a)) t let[@inline] eval f = eval self (make_atom_ self f) let[@inline] eval_level f = eval_level self (make_atom_ self f) end in (module M) (* make a clause that contains no level-0 false literals, by resolving against them. This clause can be used in a refutation proof. Note that the clause might still contain some {b assumptions}. *) let resolve_with_lvl0 (self : t) (c : clause) : clause = let lvl0 = ref [] in let res = ref [] in let to_unmark = self.temp_atom_vec in assert (AVec.is_empty to_unmark); (* resolve against the root cause of [a] being false *) let resolve_with_a (a : atom) : unit = assert (Atom.is_false self.store a && Atom.level self.store a = 0); if not (Var.marked self.store (Atom.var a)) then ( Log.debugf 50 (fun k -> k "(@[sat.resolve-lvl0@ :atom %a@])" (Atom.debug self.store) a); AVec.push to_unmark a; Var.mark self.store (Atom.var a); let p = proof_of_atom_lvl0_ self (Atom.neg a) in lvl0 := p :: !lvl0 ) in Clause.iter self.store c ~f:(fun a -> if Atom.level self.store a = 0 then resolve_with_a a); AVec.iter to_unmark ~f:(fun a -> Var.unmark self.store (Atom.var a)); AVec.clear to_unmark; if !lvl0 = [] then c (* no resolution happened *) else ( let proof = Proof_trace.add_step self.proof @@ fun () -> let lits = List.rev_map (Atom.lit self.store) !res in let hyps = Iter.of_list (Clause.proof_step self.store c :: !lvl0) in Proof_sat.sat_redundant_clause lits ~hyps in Clause.make_l self.store ~removable:false !res proof ) let mk_unsat (self : t) (us : unsat_cause) : _ unsat_state = pp_all self 99 "UNSAT"; let store = store self in let unsat_assumptions () = match us with | US_local { first = _; core } -> let lits = Iter.of_list core |> Iter.map (Atom.lit store) in lits | _ -> Iter.empty in let unsat_conflict = match us with | US_false c0 -> Log.debugf 10 (fun k -> k "(@[sat.unsat-conflict-clause@ %a@])" (Clause.debug store) c0); let c = resolve_with_lvl0 self c0 in Log.debugf 10 (fun k -> k "(@[sat.unsat-conflict-clause.proper@ %a@])" (Clause.debug store) c); fun () -> c | US_local { core = []; _ } -> assert false | US_local { first; core } -> (* TODO: do we need to filter out literals? *) let c = lazy (let core = List.rev core in (* increasing trail order *) assert (Atom.equal first @@ List.hd core); let proof = Proof_trace.add_step self.proof @@ fun () -> let lits = List.rev_map (Atom.lit self.store) core in Proof_sat.sat_unsat_core lits in Clause.make_l self.store ~removable:false [] proof) in fun () -> Lazy.force c in let module M = struct type clause = Clause.t let unsat_conflict = unsat_conflict let unsat_assumptions = unsat_assumptions let unsat_proof () = let c = unsat_conflict () in Clause.proof_step self.store c end in (module M) type propagation_result = | PR_sat | PR_conflict of { backtracked: int } | PR_unsat of clause unsat_state (* decide on assumptions, and do propagations, but no other kind of decision *) let propagate_under_assumptions (self : t) : propagation_result = let result = ref PR_sat in try while true do match propagate self with | Some confl -> (* When the theory has raised Unsat, add_boolean_conflict might 'forget' the initial conflict clause, and only add the analyzed backtrack clause. So in those case, we use add_clause to make sure the initial conflict clause is also added. *) if Clause.attached self.store confl then add_boolean_conflict self confl else add_clause_ ~pool:self.clauses_learnt self confl; Stat.incr self.n_conflicts; (* see by how much we backtracked the decision trail *) let new_lvl = decision_level self in assert (new_lvl < AVec.size self.assumptions); let backtracked = AVec.size self.assumptions - new_lvl in result := PR_conflict { backtracked }; AVec.shrink self.assumptions new_lvl; raise_notrace Exit | None -> (* No Conflict *) let decided = pick_branch_lit self ~full:false in if not decided then ( result := PR_sat; raise Exit ) done; assert false with Exit -> !result let add_clause_atoms_ self ~pool ~removable (c : atom array) (pr : Proof_step.id) : unit = try let c = Clause.make_a self.store ~removable c pr in add_clause_ ~pool self c with E_unsat (US_false c) -> self.unsat_at_0 <- Some c let add_clause_a self c pr : unit = let c = Array.map (make_atom_ self) c in add_clause_atoms_ ~pool:self.clauses_learnt ~removable:false self c pr let add_clause self (c : Lit.t list) (pr : Proof_step.id) : unit = let c = Util.array_of_list_map (make_atom_ self) c in add_clause_atoms_ ~pool:self.clauses_learnt ~removable:false self c pr let add_input_clause self (c : Lit.t list) = let pr = Proof_trace.add_step self.proof @@ fun () -> Proof_sat.sat_input_clause c in add_clause self c pr let add_input_clause_a self c = let pr = Proof_trace.add_step self.proof @@ fun () -> Proof_sat.sat_input_clause (Array.to_list c) in add_clause_a self c pr (* run [f()] with additional assumptions *) let with_local_assumptions_ (self : t) (assumptions : Lit.t list) f = let old_assm_lvl = AVec.size self.assumptions in List.iter (fun lit -> let a = make_atom_ self lit in AVec.push self.assumptions a) assumptions; try let x = f () in AVec.shrink self.assumptions old_assm_lvl; x with e -> AVec.shrink self.assumptions old_assm_lvl; raise e let solve ?(on_progress = fun _ -> ()) ?(assumptions = []) (self : t) : res = cancel_until self 0; (* make sure we are at level 0 *) with_local_assumptions_ self assumptions @@ fun () -> try solve_ ~on_progress self; Sat (mk_sat self) with E_unsat us -> Unsat (mk_unsat self us) let push_assumption (self : t) (lit : Lit.t) : unit = let a = make_atom_ self lit in AVec.push self.assumptions a let pop_assumptions self n : unit = let n_ass = AVec.size self.assumptions in assert (n <= n_ass); AVec.shrink self.assumptions (n_ass - n) let check_sat_propagations_only ?(assumptions = []) (self : t) : propagation_result = cancel_until self 0; with_local_assumptions_ self assumptions @@ fun () -> try check_unsat_ self; perform_delayed_actions self; (* add initial clauses *) propagate_under_assumptions self with E_unsat us -> let us = mk_unsat self us in PR_unsat us let true_at_level0 (self : t) (lit : Lit.t) : bool = match find_atom_ self lit with | None -> false | Some a -> (try let b, lev = eval_level self a in b && lev = 0 with UndecidedLit -> false) let[@inline] eval_lit self (lit : Lit.t) : lbool = match find_atom_ self lit with | Some a -> eval_atom_ self a | None -> L_undefined let create ?(stat = Stat.global) ?(size = `Big) ~proof (p : plugin) : t = let store = Store.create ~size ~stat () in let max_clauses_learnt = ref 0 in let self = create_ ~max_clauses_learnt ~store ~proof ~stat p in self let plugin_cdcl_t (module P : THEORY_CDCL_T) : (module PLUGIN) = (module struct include P let has_theory = true end) let mk_plugin_cdcl_t ~push_level ~pop_levels ?(partial_check = ignore) ~final_check () : (module PLUGIN) = (module struct let push_level = push_level let pop_levels = pop_levels let partial_check = partial_check let final_check = final_check let has_theory = true end) let plugin_pure_sat : plugin = (module struct let push_level () = () let pop_levels _ = () let partial_check _ = () let final_check _ = () let has_theory = false end) let create_pure_sat ?stat ?size ~proof () : t = create ?stat ?size ~proof plugin_pure_sat