tiny.smt.sat.solver
Defined in sat.
A SAT solver that learns a new clause from each conflict it analyzes.
API (8)
Types and contracts
Public types and contracts.
BoolValue: A truth value that may also be unset.Literal: A variable or its negation, packed into one 32-bit number.ProofArtifact: The clauses, assumptions and steps behind one unsatisfiable answer, copied out of the solver and owned by the caller.ProofStep: One step of a proof trace: a clause that has to follow from the starting clauses, the assumptions and the earlier steps by unit propagation.RestartPolicy: The restart schedule: the search goes back to the first decision after a conflict count that starts atfirst_conflict_intervaland multiplies bygrowthafter each restart.SolveStats: Counts of the work one solve did.Solver: A SAT solver that learns a clause from each conflict above decision level zero over the clauses a caller adds, answers under assumed literals, and records an unsat core and a proof trace after each unsatisfiable answer.Status: The answer of one solve.
Source
Source: lib/smt/src/sat/root.zig:49
zig
pub const solver = @import("solver.zig");Source: lib/smt/src/sat/solver.zig
zig
//! A SAT solver that learns a new clause from each conflict it analyzes. The solver decides whether//! clauses over Boolean variables can all be made true, optionally under assumed literals, and//! records evidence for each unsatisfiable answer.//!//! A caller adds variables and clauses, asks for an answer, and then reads a satisfying assignment,//! or a set of assumptions that the clauses refute together and a proof that a separate check can//! confirm. A caller that asks many questions of one formula wants each question to leave the//! clauses as they were, and a caller that bounds its work wants limits on search time and on//! memory.//!//! Each assignment can make any clause that contains the opposite literal unit or false, and a//! formula holds far more clauses than one assignment touches. A long search meets many conflicts,//! and a clause learned from each one makes the clause list grow for as long as the search runs. An//! unsatisfiable answer is only as sound as the search that gave it, so each learned clause and the//! final answer need a check that replays the reasoning.//!//! [MiniSat](https://doi.org/10.1007/978-3-540-24605-3_37), by Niklas Eén and Niklas Sörensson, is//! a SAT solver that learns a clause from each conflict. The package keeps its way of solving, with//! unsat cores and proofs: each clause of two or more literals watches two of them, each conflict//! above decision level zero yields a learned clause, and a solve can run under assumed literals.//!//! Each clause of two or more literals watches two of them, and a unit clause watches its one//! literal, so an assignment visits only the clauses that watch the literal it makes false. The//! search decides the lowest unassigned variable and gives it the value it last held, or true for a//! variable assigned for the first time. At each conflict above decision level zero, the solver//! resolves back to one literal of the conflict's level, learns the resulting clause, and//! backtracks to the highest other level among its literals. Before keeping a learned clause, the//! solver replays the propagation that led to the conflict and checks that the clause follows, and//! a failed check ends the solve with `error.InvalidProofTrace`.//!//! The solver counts the distinct decision levels among a learned clause's literals (its block//! distance). A cap on learned clauses (`max_learned_clauses`) bounds only the learned clauses//! whose literals span more than two decision levels (*replaceable learned clauses*), and the//! others stay for as long as the solver holds its clauses. When their count passes the cap, the//! solver returns to decision level zero and removes them, highest block distance first and then//! the longest, keeping each clause that justifies a current value and the newly learned clause.//! Under a cap, the solver keeps those clauses in fixed memory sized before the search//! (`LearnedStore`). Under a conflict limit (`conflict_budget`), the solver keeps its proof steps//! in fixed memory sized from the limit (`ProofTrace`). The solver analyzes each conflict in//! scratch memory at the end of its list of assigned literals in assignment order//! (`ConflictScratch`). After a scheduled conflict count, the solver returns to decision level zero//! and restarts the search, and the count grows by a factor after each restart (`RestartPolicy`).//!//! A solve assigns its assumptions first, at decision level zero. When the assumption list is//! nonempty, the solve removes every clause added during it when it returns, so a plain `solve`//! keeps the clauses it learned. When no cap on learned clauses is set, a solve keeps every clause//! from before it. For callers that nest questions, the solver keeps an assumption list of its own://! `assume` adds to it, and each push keeps a saved length of the assumption list that a pop//! returns to (an *assumption frame*). After an unsatisfiable answer under assumptions, the solver//! records as the core the assumptions it had assigned when the conflict came, which is every//! assumption unless the conflict came while it assigned them. When that recorded core holds two or//! more assumptions, the solver drops one assumption at a time, keeps each drop that fresh solvers//! over the caller's clauses still refute, and certifies the final set with one more solve. Under a//! conflict limit, those extra solves share what is left of the limit. When it runs out during the//! drops, the answer is `.unsat` with every assumption in the core. When it runs out during the//! certifying solve, the answer is `.unknown` with an empty core.//!//! The proof trace holds one step per learned clause and, after an unsatisfiable answer, a final//! empty clause that is checked before it is recorded. Adding a variable, a clause or an//! assumption, changing assumption frames, or starting a solve clears the recorded core and trace.//! After a satisfiable answer, or an `.unknown` answer from the main search's conflict limit, the//! trace keeps the steps learned during the solve without a final empty clause. After an `.unknown`//! answer from the certifying solve of core minimization, the trace keeps the refutation under//! every assumption of the solve, ending in the empty clause. The assignment of the last search//! stays until the next solve. When a unit clause added after a solve has a literal that this//! assignment makes false, every later solve skips the search and checks the final empty clause at//! once, so it returns `error.InvalidProofTrace` for a formula that is still satisfiable.const std = @import("std");const proof = @import("proof.zig");const scratch = @import("scratch.zig");const store = @import("store.zig");const trace = @import("trace.zig");const types = @import("types.zig");const assert = std.debug.assert;pub const BoolValue = types.BoolValue;pub const Literal = types.Literal;pub const ProofArtifact = proof.ProofArtifact;pub const ProofStep = proof.ProofStep;pub const RestartPolicy = types.RestartPolicy;pub const SolveStats = types.SolveStats;pub const Status = types.Status;const ClauseStorage = enum { heap, pool,};const Clause = struct { literals: []Literal, learned: bool, lbd: u32, storage: ClauseStorage, watch_a: usize, watch_b: usize,};const ClauseRef = struct { index: usize,};const glue_lbd_max: u32 = 2;const PropagationResult = union(enum) { consistent, conflict: usize,};const ProbeOutcome = struct { status: Status, conflicts: usize,};const ClausePropagation = union(enum) { kept, moved, unit: Literal, conflict,};/// A SAT solver that learns a clause from each conflict above decision level zero over the clauses/// a caller adds, answers under assumed literals, and records an unsat core and a proof trace after/// each unsatisfiable answer. The package's bit-vector encoder (`bitvec.Encoder`) turns terms into/// variables and clauses of a solver it is given, and a caller then solves and reads the answer/// through the solver. `init` takes an allocator, and the solver allocates every list, clause copy/// and proof step with it and frees them in `deinit`. A caller sets `conflict_budget` and/// `max_learned_clauses`, and reads `clauses` and `has_empty_clause`. The other fields are search/// state, which a caller reads through the solver's functions. Each clause of two or more literals/// watches two of them, and a unit clause watches its one literal. A decision gives the variable/// the value it last held, or true for a variable assigned for the first time. The list of assigned/// literals in assignment order has room past the variable count for the scratch memory of conflict/// analysis (`ConflictScratch`).pub const Solver = struct { allocator: std.mem.Allocator, /// Every clause the solver holds: the caller's nonempty clauses and the learned clauses it /// keeps. An empty clause from the caller stays out of the list and sets `has_empty_clause`. /// Clause order changes when the solver removes a learned clause under a cap, because the last /// clause moves into the removed clause's place. A solve with assumptions removes the clauses /// added during it when it returns. `clauseCount` counts the list, and `dimacs.write` writes it /// with learned clauses included. clauses: std.ArrayList(Clause) = .empty, watches: std.ArrayList(std.ArrayList(ClauseRef)) = .empty, assignment: std.ArrayList(BoolValue) = .empty, saved_phase: std.ArrayList(BoolValue) = .empty, level: std.ArrayList(u32) = .empty, reason: std.ArrayList(?usize) = .empty, trail: std.ArrayList(Literal) = .empty, propagation_cursor: usize = 0, decision_limits: std.ArrayList(usize) = .empty, assumptions: std.ArrayList(Literal) = .empty, frames: std.ArrayList(usize) = .empty, last_core: std.ArrayList(Literal) = .empty, proof_steps: std.ArrayList(ProofStep) = .empty, proof_assumptions: std.ArrayList(Literal) = .empty, proof_base_clause_count: usize = 0, last_stats: SolveStats = .{}, restart_policy: RestartPolicy = .{}, conflicts_since_restart: usize = 0, next_restart_conflicts: usize = 0, /// The most conflicts one solve may meet before it returns `.unknown`. The default `null` /// leaves the search unlimited. A caller sets it before a solve to bound the search's work. /// While it is set, the solver keeps the solve's proof steps in fixed memory sized from it and /// the variable count (`ProofTrace`). The extra solves of core minimization share what is left /// of it after the main search. conflict_budget: ?usize = null, /// The most replaceable learned clauses the solver keeps. The default `null` leaves the learned /// clauses uncapped. A caller sets it before a solve to bound the memory learned clauses take. /// At the start of each solve, and whenever the count passes the cap during a search, the /// solver removes replaceable learned clauses, highest block distance first and then the /// longest. The solver keeps each clause that justifies a current value and the newly learned /// clause, so the count can pass the cap by those clauses. While it is set, the solver keeps /// replaceable learned clauses in fixed memory sized from the cap and the variable count /// (`LearnedStore`). After a caller sets it back to `null`, the next solve moves those clauses /// to heap memory. max_learned_clauses: ?usize = null, learned_clause_count: usize = 0, glue_clause_count: usize = 0, learned_store: ?store.Store = null, proof_trace: ?trace.Trace = null, proof_trace_slab: bool = true, rup_assignment: std.ArrayList(BoolValue) = .empty, /// True once the caller has added an empty clause. Every later solve then answers `.unsat`. /// `dimacs.write` writes an empty clause for it, and `lastProofArtifact` adds an empty starting /// clause for it. The solver's own proof checks accept every step while it is set. The default /// is false. has_empty_clause: bool = false, inconsistent: bool = false, /// Returns an empty solver that allocates with `allocator`. A caller creates one solver per /// formula and hands it to the code that adds the clauses. The call allocates nothing. pub fn init(allocator: std.mem.Allocator) Solver { return .{ .allocator = allocator }; } /// Frees every clause, list, fixed store, trace and proof step, and leaves the solver /// undefined. The owner of a solver calls it once, when it is done with the formula. Slices /// from `lastUnsatCore`, `lastProofTrace` and `activeAssumptions` become invalid. Artifacts /// from `lastProofArtifact` stay valid, because each owns its own copy. pub fn deinit(self: *Solver) void { for (self.clauses.items) |clause| { if (clause.storage == .heap) self.allocator.free(clause.literals); } if (self.learned_store) |*pool| pool.deinit(self.allocator); self.clauses.deinit(self.allocator); for (self.watches.items) |*watch| { watch.deinit(self.allocator); } self.watches.deinit(self.allocator); self.assignment.deinit(self.allocator); self.saved_phase.deinit(self.allocator); self.level.deinit(self.allocator); self.reason.deinit(self.allocator); self.trail.deinit(self.allocator); self.decision_limits.deinit(self.allocator); self.assumptions.deinit(self.allocator); self.frames.deinit(self.allocator); self.last_core.deinit(self.allocator); self.clearProofTrace(); if (self.proof_trace) |*owner| owner.deinit(self.allocator); self.proof_steps.deinit(self.allocator); self.proof_assumptions.deinit(self.allocator); self.rup_assignment.deinit(self.allocator); self.* = undefined; } /// Returns the number of variables: one more than the highest variable index the solver has /// added or seen. A caller reports it as a size of the encoded formula. A clause or an /// assumption that names a higher variable raises it. pub fn variableCount(self: *const Solver) usize { return self.assignment.items.len; } /// Returns the number of entries in `clauses`. A caller reports it as a size of the encoded /// formula, and `dimacs.write` prints it in its header. The count includes the learned clauses /// the solver keeps and leaves out an empty clause from the caller. pub fn clauseCount(self: *const Solver) usize { return self.clauses.items.len; } /// Returns the number of learned clauses the solver holds, at every block distance. A caller /// checks how many learned clauses survive a capped search. The count drops when the solver /// removes a learned clause under a cap and when a solve with assumptions returns. pub fn retainedLearnedClauses(self: *const Solver) usize { return self.learned_clause_count; } /// Returns the number of learned clauses whose literals span more than two decision levels. A /// caller compares it with `max_learned_clauses` after a capped search. These are the clauses /// the cap bounds. pub fn replaceableLearnedClauses(self: *const Solver) usize { assert(self.glue_clause_count <= self.learned_clause_count); return self.learned_clause_count - self.glue_clause_count; } /// Sets the restart schedule for later solves. A caller that wants restarts at a different /// pace, such as after every conflict, sets it before solving. The schedule takes effect at the /// start of the next solve. pub fn setRestartPolicy(self: *Solver, policy: RestartPolicy) void { self.restart_policy = policy; } /// Returns the restart schedule later solves use. A caller reads back the schedule that /// `setRestartPolicy` set, or the default. pub fn restartPolicy(self: *const Solver) RestartPolicy { return self.restart_policy; } /// Adds one unassigned variable and returns its index, counting from 0. `bitvec.Encoder` adds a /// variable for each Boolean it encodes, and `dimacs.parse` adds the variable count of its /// header. The call clears the recorded core and trace. The call returns `error.OutOfMemory` /// when a list fails to grow. An index at or above 2^31 overflows the literal's packed number, /// so building a literal for it panics in safe builds. pub fn addVariable(self: *Solver) !u32 { self.last_core.clearRetainingCapacity(); self.clearProofTrace(); const variable_index: u32 = @intCast(self.assignment.items.len); try self.ensureVariable(variable_index); return variable_index; } /// Adds a copy of `literals` as a clause and adds any variable it names that the solver lacks. /// `bitvec.Encoder` asserts each encoded fact through it, and `dimacs.parse` adds each clause /// of its input. The caller keeps `literals`. An empty slice sets `has_empty_clause`, and every /// later solve answers `.unsat`. The solver assigns the literal of a unit clause at once. When /// the literal of a unit clause is false under the assignment the last search left, every later /// solve skips the search and checks the final empty clause at once, so it returns /// `error.InvalidProofTrace` for a formula that is still satisfiable. The call clears the /// recorded core and trace. The call returns `error.OutOfMemory` when a copy or a list fails to /// grow, and then the clause is left out while variables it named may stay added. pub fn addClause(self: *Solver, literals: []const Literal) !void { self.last_core.clearRetainingCapacity(); self.clearProofTrace(); if (literals.len == 0) { self.has_empty_clause = true; self.inconsistent = true; return; } for (literals) |literal| { try self.ensureVariable(literal.variable()); } const owned = try self.allocator.dupe(Literal, literals); errdefer self.allocator.free(owned); const clause_index = self.clauses.items.len; const clause = Clause{ .literals = owned, .learned = false, .lbd = 0, .storage = .heap, .watch_a = 0, .watch_b = if (owned.len > 1) 1 else 0, }; try self.clauses.append(self.allocator, clause); errdefer _ = self.clauses.pop(); try self.addWatch(owned[0], clause_index); errdefer _ = self.removeWatch(owned[0], clause_index); if (owned.len > 1) { try self.addWatch(owned[1], clause_index); } if (owned.len == 1) { if (!try self.enqueue(owned[0], clause_index)) self.inconsistent = true; } } /// Solves the clauses alone, as `solveWithAssumptions` does with an empty list. A caller that /// asks one question of a formula calls it. The solver's own assumption list, which `assume` /// fills, takes no part. pub fn solve(self: *Solver) !Status { return self.solveWithAssumptions(&.{}); } /// Saves the current length of the assumption list, so the next `popAssumptionFrame` returns /// the list to it. A caller that nests questions pushes a frame before it assumes the literals /// of an inner question. The call clears the recorded core and trace. The call returns /// `error.OutOfMemory` when the frame list fails to grow. pub fn pushAssumptionFrame(self: *Solver) !void { self.last_core.clearRetainingCapacity(); self.clearProofTrace(); try self.frames.append(self.allocator, self.assumptions.items.len); } /// Returns the assumption list to the length the latest push saved and closes that frame. A /// caller that nests questions pops a frame to leave an inner question. With no frame open, the /// list stays as it is. The call clears the recorded core and trace in both cases. pub fn popAssumptionFrame(self: *Solver) void { self.last_core.clearRetainingCapacity(); self.clearProofTrace(); const frame_start = self.frames.pop() orelse return; self.assumptions.shrinkRetainingCapacity(frame_start); } /// Adds `literal` to the solver's assumption list and adds its variable when the solver lacks /// it. `bitvec.Encoder` assumes the literal of each assumed term through it, for a later /// `solveWithActiveAssumptions`. The literal stays in the list until a pop past its frame or /// `clearAssumptionFrames`. The call clears the recorded core and trace. The call returns /// `error.OutOfMemory` when a list fails to grow. pub fn assume(self: *Solver, literal: Literal) !void { self.last_core.clearRetainingCapacity(); self.clearProofTrace(); try self.ensureVariable(literal.variable()); try self.assumptions.append(self.allocator, literal); } /// Empties the assumption list and closes every frame. A caller that starts a new round of /// questions calls it to drop every assumption and frame at once. The call clears the recorded /// core and trace. pub fn clearAssumptionFrames(self: *Solver) void { self.last_core.clearRetainingCapacity(); self.clearProofTrace(); self.assumptions.clearRetainingCapacity(); self.frames.clearRetainingCapacity(); } /// Solves under the solver's own assumption list, as `solveWithAssumptions` does. /// `bitvec.Encoder.solve` calls it, so the literals assumed with `assume` take part. pub fn solveWithActiveAssumptions(self: *Solver) !Status { return self.solveWithAssumptions(self.assumptions.items); } /// Returns the solver's assumption list, oldest first. A caller reads the assumptions in force /// after pushes and pops. The slice stays valid until the list changes. pub fn activeAssumptions(self: *const Solver) []const Literal { return self.assumptions.items; } /// Returns the number of open frames. A caller checks how deep its nesting is. pub fn frameDepth(self: *const Solver) usize { return self.frames.items.len; } /// Returns a set of the last solve's assumptions that the clauses refute together, after an /// unsatisfiable answer. A caller maps the core back to the named assumptions it made, to /// report which of them conflict. The solver first records as the core the assumptions it had /// assigned when the conflict came. When that recorded core holds two or more assumptions, the /// core is the smaller set that the extra solves of core minimization certified. The core is /// empty when the clauses were refuted before the search began, as by two opposite unit clauses /// or an empty clause, after a satisfiable or unknown answer, and after any change that clears /// it. A recorded core of one assumption stays as it is, because core minimization runs only /// for two or more, so the core can hold that assumption even when the clauses alone are /// unsatisfiable. A conflict while the solver assigns the assumptions records only those /// assigned so far, so a solve under two or more assumptions can end with a core of one and no /// shrinking. Under a conflict limit, the core holds every assumption when the limit ran out /// during the drops, and it is empty with an `.unknown` answer when the limit ran out during /// the certifying solve. The slice stays valid until the next change to the solver. pub fn lastUnsatCore(self: *const Solver) []const Literal { return self.last_core.items; } /// Returns the work counts of the last solve. A caller reports them beside the answer. A solve /// resets them at its start. After core minimization certifies a smaller core, `proof_steps` /// holds the certifying solve's step count, and it is the one count core minimization sets. The /// other counts leave out the work of the extra solves of core minimization. pub fn lastSolveStats(self: *const Solver) SolveStats { return self.last_stats; } /// Returns the proof steps of the last solve, in order. A caller reads the steps in place, and /// `lastProofArtifact` makes a copy that outlives later changes. The trace holds one step per /// learned clause and, after an unsatisfiable answer, a final empty clause. After a satisfiable /// or unknown answer, the trace can still hold the steps learned during the solve. The slices /// stay valid until the next change to the solver. pub fn lastProofTrace(self: *const Solver) []const ProofStep { return self.proof_steps.items; } /// Returns true when the trace is nonempty, each step follows by unit propagation from the /// starting clauses, the assumptions and the earlier steps, and the last step is the empty /// clause. A caller confirms an unsatisfiable answer in place. The check accepts every step /// while `has_empty_clause` is set. pub fn lastProofTraceValid(self: *const Solver) bool { if (self.proof_steps.items.len == 0) return false; for (self.proof_steps.items, 0..) |step, index| { if (!self.rupCheck(step.literals, index)) return false; } return self.proof_steps.items[self.proof_steps.items.len - 1].literals.len == 0; } /// Returns a copy of the clauses, assumptions and steps behind the last unsatisfiable answer, /// allocated with `allocator` and owned by the caller, or null when the trace is empty. A /// caller keeps the evidence after the solver moves on, checks it with `ProofArtifact.valid`, /// or writes it out. The starting clauses are every clause the solver held when the solve /// began, learned clauses it kept included, then an empty clause when `has_empty_clause` is /// set. The artifact covers the solver's variable count. After a solve that learned clauses and /// then answered `.sat`, or `.unknown` at the main search's conflict limit, the call returns an /// artifact that `ProofArtifact.valid` rejects. After an `.unknown` answer from the certifying /// solve of core minimization, the artifact holds the refutation under every assumption of the /// solve, and `ProofArtifact.valid` accepts it. The call returns `error.OutOfMemory` when a /// copy fails, and then frees what it copied. pub fn lastProofArtifact(self: *const Solver, allocator: std.mem.Allocator) !?ProofArtifact { if (self.proof_steps.items.len == 0) return null; assert(self.last_stats.proof_steps == self.proof_steps.items.len); var artifact = ProofArtifact.init(allocator, self.assignment.items.len); errdefer artifact.deinit(); const base_count = @min(self.proof_base_clause_count, self.clauses.items.len); for (self.clauses.items[0..base_count]) |clause| { try artifact.appendClause(clause.literals); } if (self.has_empty_clause) try artifact.appendClause(&.{}); try artifact.appendAssumptions(self.proof_assumptions.items); for (self.proof_steps.items) |step| { try artifact.appendStep(step.literals); } return artifact; } /// Decides whether the clauses and every literal of `assumptions` can all be true, and returns /// `.sat`, `.unsat` or `.unknown`. A caller asks a question under temporary hypotheses and /// learns which of them the clauses refute together. The solve assigns the assumptions first, /// in order, at decision level zero, and adds any variable they name that the solver lacks. /// After `.unsat`, `lastUnsatCore` holds one of three sets: the assumptions the solve had /// assigned when the conflict came, the smaller set that core minimization certified when that /// recorded core holds two or more, or every assumption of the solve when a conflict limit runs /// out while core minimization drops assumptions. When `assumptions` is nonempty, every clause /// added during the solve is removed when the solve returns. The solve clears the recorded /// core, trace and counts only after it has made room for conflict analysis, so a failure /// before that point leaves the old evidence in place. The call returns `error.OutOfMemory` /// when an allocation fails, `error.CapacityOverflow` when a size for the solve overflows /// `usize`, `error.InvalidProofTrace` when a learned clause or the final step fails its check, /// `error.InvalidConflictGraph` when conflict analysis runs out of assigned literals to resolve /// on, `error.InvalidUnsatCore` when the certifying solve satisfies the core, and the errors of /// `ConflictScratch`. The assignment stays after the call, and `value` and `literalValue` read /// it. pub fn solveWithAssumptions(self: *Solver, assumptions: []const Literal) !Status { return try self.solveWithAssumptionsMode(assumptions, true); } fn solveWithAssumptionsMode( self: *Solver, assumptions: []const Literal, minimize_core: bool, ) anyerror!Status { const required_variables = try self.requiredVariableCount(assumptions); const scratch_capacity = try self.ensureConflictScratchStorage(required_variables); self.clearSolveEvidence(); errdefer self.clearSolveEvidence(); self.conflicts_since_restart = 0; self.next_restart_conflicts = self.restart_policy.first_conflict_interval; for (assumptions) |assumption| { try self.ensureVariable(assumption.variable()); } if (self.max_learned_clauses != null) { self.backtrack(0); self.proof_base_clause_count = 0; self.reduceLearned(0, null); } try self.ensureLearnedStore(); try self.ensureProofTrace(); var conflict_scratch: ?scratch.ConflictScratch = null; if (scratch_capacity) |capacity| { const storage = self.conflictScratchStorage(capacity); conflict_scratch = try scratch.ConflictScratch.init( storage, .{ .variables = capacity.variables }, ); conflict_scratch.?.activate(); } defer if (conflict_scratch) |*owner| { const expected_pointer = @intFromPtr(owner.storage.ptr); const expected_length = owner.storage.len; const returned = owner.deinit(); assert(expected_pointer == @intFromPtr(returned.ptr)); assert(expected_length == returned.len); }; const retained_clause_count = self.clauses.items.len; defer if (assumptions.len > 0) self.discardClausesFrom(retained_clause_count); self.proof_base_clause_count = retained_clause_count; try self.proof_assumptions.appendSlice(self.allocator, assumptions); self.clearSearch(); if (self.inconsistent) { self.recordConflict(); try self.recordFinalProofStep(); return .unsat; } for (assumptions, 0..) |assumption, assumption_index| { try self.ensureVariable(assumption.variable()); if (!try self.enqueue(assumption, null)) { self.recordConflict(); return try self.finishAssumptionUnsat( assumptions[0 .. assumption_index + 1], minimize_core, ); } switch (try self.propagate()) { .consistent => {}, .conflict => { self.recordConflict(); return try self.finishAssumptionUnsat( assumptions[0 .. assumption_index + 1], minimize_core, ); }, } } while (true) { switch (try self.propagate()) { .consistent => {}, .conflict => |clause_index| { self.recordConflict(); if (self.decisionLevel() == 0) { return try self.finishAssumptionUnsat(assumptions, minimize_core); } if (!try self.learnFromConflict(&conflict_scratch.?, clause_index)) { if (assumptions.len == 0) self.inconsistent = true; return try self.finishAssumptionUnsat(assumptions, minimize_core); } if (self.conflict_budget) |budget| { if (self.last_stats.conflicts >= budget) return .unknown; } self.maybeRestart(); continue; }, } if (self.nextUnassignedVariable()) |variable_index| { try self.newDecisionLevel(); self.last_stats.decisions += 1; const decision_literal = self.decisionLiteral(variable_index); if (self.saved_phase.items[variable_index] != .unset) { self.last_stats.phase_saved_decisions += 1; } if (!try self.enqueue(decision_literal, null)) { self.recordConflict(); try self.recordFinalProofStep(); return .unsat; } continue; } return .sat; } } /// Returns the value of `variable_index` in the current assignment, or null when the variable /// is unassigned or at or above the variable count. A caller reads a satisfying assignment /// after `.sat`. The values form a satisfying assignment only after `.sat`. pub fn value(self: *const Solver, variable_index: u32) ?bool { if (variable_index >= self.assignment.items.len) return null; return switch (self.assignment.items[variable_index]) { .unset => null, .false => false, .true => true, }; } fn finishAssumptionUnsat( self: *Solver, assumptions: []const Literal, minimize_core: bool, ) anyerror!Status { if (assumptions.len > 0) try self.recordUnsatCore(assumptions); try self.recordFinalProofStep(); if (minimize_core and assumptions.len > 1) return try self.minimizeLastUnsatCore(); return .unsat; } fn minimizeLastUnsatCore(self: *Solver) anyerror!Status { var pool: ?usize = if (self.conflict_budget) |budget| budget -| self.last_stats.conflicts else null; var core: std.ArrayList(Literal) = .empty; defer core.deinit(self.allocator); try core.appendSlice(self.allocator, self.last_core.items); var trial: std.ArrayList(Literal) = .empty; defer trial.deinit(self.allocator); var index: usize = 0; while (index < core.items.len) { if (poolDrained(pool)) break; trial.clearRetainingCapacity(); for (core.items, 0..) |literal, candidate_index| { if (candidate_index != index) try trial.append(self.allocator, literal); } const outcome = try self.probeStatusWithAssumptions(trial.items, pool); drainPool(&pool, outcome.conflicts); if (outcome.status == .unsat) { core.clearRetainingCapacity(); try core.appendSlice(self.allocator, trial.items); } else { index += 1; } } if (!poolDrained(pool)) { var probe = try self.baseProbe(pool); defer probe.deinit(); const certified = try probe.solveWithAssumptionsMode(core.items, false); drainPool(&pool, probe.lastSolveStats().conflicts); switch (certified) { .sat => return error.InvalidUnsatCore, .unknown => { self.last_core.clearRetainingCapacity(); return .unknown; }, .unsat => { const base_count = self.proof_base_clause_count; self.clearProofTrace(); self.proof_base_clause_count = base_count; try self.proof_assumptions.appendSlice(self.allocator, probe.lastUnsatCore()); for (probe.proof_steps.items) |step| { try self.appendStepStorage(step.literals); } self.last_stats.proof_steps = self.proof_steps.items.len; try self.recordUnsatCore(probe.lastUnsatCore()); return .unsat; }, } } try self.recordUnsatCore(self.proof_assumptions.items); return .unsat; } fn poolDrained(pool: ?usize) bool { return if (pool) |remaining| remaining == 0 else false; } fn drainPool(pool: *?usize, spent: usize) void { if (pool.*) |remaining| pool.* = remaining -| spent; } fn probeStatusWithAssumptions( self: *const Solver, assumptions: []const Literal, pool: ?usize, ) anyerror!ProbeOutcome { var probe = try self.baseProbe(pool); defer probe.deinit(); const status = try probe.solveWithAssumptionsMode(assumptions, false); return .{ .status = status, .conflicts = probe.lastSolveStats().conflicts }; } fn baseProbe(self: *const Solver, pool: ?usize) anyerror!Solver { var probe = Solver.init(self.allocator); errdefer probe.deinit(); probe.conflict_budget = pool; probe.proof_trace_slab = false; probe.max_learned_clauses = self.max_learned_clauses; probe.setRestartPolicy(self.restart_policy); for (self.clauses.items) |clause| { if (!clause.learned) try probe.addClause(clause.literals); } if (self.has_empty_clause) try probe.addClause(&.{}); return probe; } fn recordUnsatCore(self: *Solver, assumptions: []const Literal) !void { self.last_core.clearRetainingCapacity(); try self.last_core.appendSlice(self.allocator, assumptions); } fn recordConflict(self: *Solver) void { self.last_stats.conflicts += 1; self.conflicts_since_restart += 1; if (self.decisionLevel() == 0) self.last_stats.root_conflicts += 1; } fn clearProofTrace(self: *Solver) void { if (self.proof_trace) |*owner| { for (self.proof_steps.items) |step| { if (step.literals.len == 0) continue; assert(owner.owns(step.literals)); } owner.clear(); } else { for (self.proof_steps.items) |step| { self.allocator.free(step.literals); } } self.proof_steps.clearRetainingCapacity(); self.proof_assumptions.clearRetainingCapacity(); self.proof_base_clause_count = 0; } fn clearSolveEvidence(self: *Solver) void { self.clearProofTrace(); self.last_core.clearRetainingCapacity(); self.last_stats = .{}; } fn acquireStepStorage(self: *Solver, literals: []const Literal) ![]Literal { if (self.proof_trace) |*owner| { if (literals.len == 0) return owner.acquireEmpty(); return owner.acquire(literals); } return try self.allocator.dupe(Literal, literals); } fn appendStepStorage(self: *Solver, literals: []const Literal) !void { const owned = try self.acquireStepStorage(literals); if (self.proof_trace != null) { assert(self.proof_steps.items.len < self.proof_steps.capacity); self.proof_steps.appendAssumeCapacity(.{ .literals = owned }); return; } errdefer self.allocator.free(owned); try self.proof_steps.append(self.allocator, .{ .literals = owned }); } fn appendProofStep(self: *Solver, literals: []const Literal) !void { try self.appendStepStorage(literals); self.last_stats.proof_steps += 1; } fn removeLastProofStep(self: *Solver) void { const step = self.proof_steps.pop() orelse return; if (self.proof_trace) |*owner| { owner.releaseLast(step.literals); } else { self.allocator.free(step.literals); } self.last_stats.proof_steps -= 1; } fn recordFinalProofStep(self: *Solver) !void { if (!self.rupCheck(&.{}, self.proof_steps.items.len)) return error.InvalidProofTrace; try self.appendProofStep(&.{}); } fn traceBudget(self: *const Solver) ?usize { if (!self.proof_trace_slab) return null; return self.conflict_budget; } fn rupCheck(self: *const Solver, literals: []const Literal, proof_step_limit: usize) bool { if (self.has_empty_clause) return true; assert(self.rup_assignment.items.len == self.assignment.items.len); const base_count = @min(self.proof_base_clause_count, self.clauses.items.len); return proof.rupCheckKnownVariablesWithClauses( self.rup_assignment.items, Clause, self.clauses.items[0..base_count], self.proof_assumptions.items, self.proof_steps.items, literals, proof_step_limit, ); } fn rupCheckConflict(self: *const Solver, literals: []const Literal, conflict: usize) bool { return proof.rupCheckKnownVariablesWithReasons( self.rup_assignment.items, Clause, self.clauses.items, self.proof_assumptions.items, literals, .{ .trail = self.trail.items, .reasons = self.reason.items, .conflict = conflict }, ); } fn ensureProofTrace(self: *Solver) !void { assert(self.proof_steps.items.len == 0); const budget = self.traceBudget() orelse { if (self.proof_trace) |*owner| { owner.deinit(self.allocator); self.proof_trace = null; } return; }; const limits = trace.Trace.Limits.inspect(budget, self.variableCount()); const required = try trace.Trace.Capacity.derive(limits); if (self.proof_trace) |*existing| { if (existing.admits(required)) { try self.proof_steps.ensureTotalCapacity(self.allocator, existing.capacity.steps); return; } existing.deinit(self.allocator); self.proof_trace = null; } var fresh = try trace.Trace.init(self.allocator, limits); fresh.activate(); self.proof_trace = fresh; try self.proof_steps.ensureTotalCapacity(self.allocator, required.steps); } fn requiredVariableCount( self: *const Solver, assumptions: []const Literal, ) error{CapacityOverflow}!usize { var required = self.variableCount(); for (assumptions) |assumption| { const assumption_variables = std.math.add( usize, @as(usize, assumption.variable()), 1, ) catch return error.CapacityOverflow; required = @max(required, assumption_variables); } return required; } fn ensureConflictScratchStorage( self: *Solver, required_variables: usize, ) !?scratch.ConflictScratch.Capacity { if (required_variables == 0) return null; const limits = scratch.ConflictScratch.Limits.inspect(required_variables); const required = try scratch.ConflictScratch.Capacity.derive(limits); try self.trail.ensureTotalCapacityPrecise( self.allocator, required.trail_capacity, ); return required; } fn conflictScratchStorage( self: *Solver, capacity: scratch.ConflictScratch.Capacity, ) scratch.ConflictScratch.Storage { assert(self.trail.items.len <= capacity.variables); assert(self.trail.capacity >= capacity.trail_capacity); const backing = self.trail.allocatedSlice()[capacity.variables..capacity.trail_capacity]; const bytes = std.mem.sliceAsBytes(backing); assert(bytes.len >= capacity.storage_bytes); return @alignCast(bytes[0..capacity.storage_bytes]); } fn ensureLearnedStore(self: *Solver) !void { const limit = self.max_learned_clauses orelse { try self.evacuateLearnedStore(); return; }; const limits = store.Store.Limits.inspect(limit, self.variableCount()); const required = try store.Store.Capacity.derive(limits); if (self.learned_store) |*existing| { if (existing.admits(required)) return; } var fresh = try store.Store.init(self.allocator, limits); fresh.activate(); if (self.learned_store) |*old| { for (self.clauses.items) |*clause| { if (clause.storage == .pool) { assert(old.owns(clause.literals)); clause.literals = fresh.acquire(clause.literals); } } old.deinit(self.allocator); } self.learned_store = fresh; } fn evacuateLearnedStore(self: *Solver) !void { if (self.learned_store == null) return; for (self.clauses.items) |*clause| { if (clause.storage == .pool) { const heap_copy = try self.allocator.dupe(Literal, clause.literals); self.learnedPool().release(clause.literals); clause.literals = heap_copy; clause.storage = .heap; } } self.learnedPool().deinit(self.allocator); self.learned_store = null; } fn maybeRestart(self: *Solver) void { if (self.next_restart_conflicts != 0 and self.decisionLevel() > 0 and self.conflicts_since_restart >= self.next_restart_conflicts) { self.backtrack(0); self.last_stats.restarts += 1; self.conflicts_since_restart = 0; if (self.restart_policy.growth > 1) { self.next_restart_conflicts = std.math.mul( usize, self.next_restart_conflicts, self.restart_policy.growth, ) catch std.math.maxInt(usize); } } const limit = self.max_learned_clauses orelse return; if (self.replaceableLearnedClauses() <= limit) return; assert(self.clauses.items.len > 0); if (self.decisionLevel() > 0) { self.backtrack(0); self.last_stats.restarts += 1; self.conflicts_since_restart = 0; } const just_learned = self.clauses.items.len - 1; const protected = if (just_learned >= self.proof_base_clause_count) just_learned else null; self.reduceLearned(self.proof_base_clause_count, protected); } fn reduceLearned(self: *Solver, first_candidate: usize, protected: ?usize) void { const limit = self.max_learned_clauses orelse return; assert(self.decisionLevel() == 0); assert(first_candidate >= self.proof_base_clause_count); assert(self.learned_clause_count == self.countLearnedClauses()); assert(self.glue_clause_count == self.countGlueClauses()); var bound = self.clauses.items.len; while (self.replaceableLearnedClauses() > limit and bound > 0) : (bound -= 1) { const victim = self.selectVictim(first_candidate, protected) orelse break; self.removeLearnedClause(victim); self.last_stats.evicted_clauses += 1; } } fn selectVictim(self: *const Solver, first_candidate: usize, protected: ?usize) ?usize { assert(first_candidate <= self.clauses.items.len); var worst: ?usize = null; for (self.clauses.items[first_candidate..], first_candidate..) |clause, clause_index| { if (!clause.learned) continue; if (clause.lbd <= glue_lbd_max) continue; if (protected == clause_index) continue; if (self.clauseLocked(clause_index)) continue; if (worst) |current| { if (self.evictsBefore(clause_index, current)) worst = clause_index; } else { worst = clause_index; } } return worst; } fn evictsBefore(self: *const Solver, left_index: usize, right_index: usize) bool { const left = self.clauses.items[left_index]; const right = self.clauses.items[right_index]; if (left.lbd != right.lbd) return left.lbd > right.lbd; return left.literals.len > right.literals.len; } fn countLearnedClauses(self: *const Solver) usize { var count: usize = 0; for (self.clauses.items) |clause| { if (clause.learned) count += 1; } return count; } fn countGlueClauses(self: *const Solver) usize { var count: usize = 0; for (self.clauses.items) |clause| { if (clause.learned and clause.lbd <= glue_lbd_max) count += 1; } return count; } /// Returns the value of `literal` in the current assignment: `.true`, `.false`, or `.unset` /// when its variable is unassigned or at or above the variable count. `bitvec.Encoder` reads /// each Boolean of a model through it. The values form a satisfying assignment only after /// `.sat`. pub fn literalValue(self: *const Solver, literal: Literal) BoolValue { if (literal.variable() >= self.assignment.items.len) return .unset; const assigned = self.assignment.items[literal.variable()]; return if (literal.isPositive()) assigned else assigned.invert(); } fn ensureVariable(self: *Solver, variable_index: u32) !void { const needed = @as(usize, variable_index) + 1; while (self.assignment.items.len < needed) { try self.assignment.append(self.allocator, .unset); errdefer _ = self.assignment.pop(); try self.saved_phase.append(self.allocator, .unset); errdefer _ = self.saved_phase.pop(); try self.level.append(self.allocator, 0); errdefer _ = self.level.pop(); try self.reason.append(self.allocator, null); errdefer _ = self.reason.pop(); try self.rup_assignment.append(self.allocator, .unset); errdefer _ = self.rup_assignment.pop(); try self.watches.append(self.allocator, .empty); errdefer { var removed = self.watches.pop().?; removed.deinit(self.allocator); } try self.watches.append(self.allocator, .empty); } } fn addWatch(self: *Solver, literal: Literal, clause_index: usize) !void { try self.watches.items[literal.index()].append(self.allocator, .{ .index = clause_index }); } fn removeWatch(self: *Solver, literal: Literal, clause_index: usize) bool { const watch = &self.watches.items[literal.index()]; for (watch.items, 0..) |clause_ref, index| { if (clause_ref.index == clause_index) { _ = watch.swapRemove(index); return true; } } return false; } fn patchWatch(self: *Solver, literal: Literal, from_index: usize, to_index: usize) void { const watch = &self.watches.items[literal.index()]; for (watch.items) |*clause_ref| { if (clause_ref.index == from_index) { clause_ref.index = to_index; return; } } unreachable; } fn clauseLocked(self: *const Solver, clause_index: usize) bool { assert(clause_index < self.clauses.items.len); for (self.trail.items) |trail_literal| { const variable_index = trail_literal.variable(); if (self.reason.items[variable_index]) |reason_index| { if (reason_index == clause_index) return true; } } return false; } fn removeLearnedClause(self: *Solver, clause_index: usize) void { assert(self.decisionLevel() == 0); assert(clause_index >= self.proof_base_clause_count); assert(clause_index < self.clauses.items.len); const clause = self.clauses.items[clause_index]; assert(clause.learned); assert(!self.clauseLocked(clause_index)); const removed_first = self.removeWatch(clause.literals[clause.watch_a], clause_index); assert(removed_first); if (clause.watch_b != clause.watch_a) { const removed_second = self.removeWatch(clause.literals[clause.watch_b], clause_index); assert(removed_second); } const last_index = self.clauses.items.len - 1; _ = self.clauses.swapRemove(clause_index); if (clause_index != last_index) { const moved = self.clauses.items[clause_index]; self.patchWatch(moved.literals[moved.watch_a], last_index, clause_index); if (moved.watch_b != moved.watch_a) { self.patchWatch(moved.literals[moved.watch_b], last_index, clause_index); } for (self.trail.items) |trail_literal| { const variable_index = trail_literal.variable(); if (self.reason.items[variable_index]) |reason_index| { if (reason_index == last_index) { self.reason.items[variable_index] = clause_index; } } } } assert(self.learned_clause_count > 0); self.learned_clause_count -= 1; if (clause.lbd <= glue_lbd_max) { assert(self.glue_clause_count > 0); self.glue_clause_count -= 1; } self.freeClauseLiterals(clause); } fn clearSearch(self: *Solver) void { for (self.assignment.items) |*item| item.* = .unset; for (self.level.items) |*item| item.* = 0; for (self.reason.items) |*item| item.* = null; self.trail.clearRetainingCapacity(); self.decision_limits.clearRetainingCapacity(); self.propagation_cursor = 0; for (self.clauses.items, 0..) |clause, clause_index| { if (clause.literals.len == 1) { if (!(self.enqueue(clause.literals[0], clause_index) catch false)) { self.inconsistent = true; return; } } } } fn discardClausesFrom(self: *Solver, retained_clause_count: usize) void { while (self.clauses.items.len > retained_clause_count) { const clause_index = self.clauses.items.len - 1; const clause = self.clauses.pop().?; _ = self.removeWatch(clause.literals[clause.watch_a], clause_index); if (clause.watch_b != clause.watch_a) { _ = self.removeWatch(clause.literals[clause.watch_b], clause_index); } if (clause.learned) { assert(self.learned_clause_count > 0); self.learned_clause_count -= 1; if (clause.lbd <= glue_lbd_max) { assert(self.glue_clause_count > 0); self.glue_clause_count -= 1; } } self.freeClauseLiterals(clause); } } fn freeClauseLiterals(self: *Solver, clause: Clause) void { switch (clause.storage) { .heap => self.allocator.free(clause.literals), .pool => self.learnedPool().release(clause.literals), } } fn learnedPool(self: *Solver) *store.Store { assert(self.learned_store != null); return &self.learned_store.?; } fn decisionLevel(self: *const Solver) u32 { return @intCast(self.decision_limits.items.len); } fn newDecisionLevel(self: *Solver) !void { try self.decision_limits.append(self.allocator, self.trail.items.len); const level = self.decisionLevel(); if (level > self.last_stats.max_decision_level) { self.last_stats.max_decision_level = level; } } fn backtrack(self: *Solver, target_level: u32) void { while (self.decisionLevel() > target_level) { const start = self.decision_limits.pop().?; var index = self.trail.items.len; while (index > start) { index -= 1; const variable_index = self.trail.items[index].variable(); self.assignment.items[variable_index] = .unset; self.reason.items[variable_index] = null; self.level.items[variable_index] = 0; } self.trail.shrinkRetainingCapacity(start); if (self.propagation_cursor > start) self.propagation_cursor = start; } } fn enqueue(self: *Solver, literal: Literal, reason: ?usize) !bool { const variable_index = literal.variable(); assert(variable_index < self.variableCount()); const current = self.literalValue(literal); if (current == .true) return true; if (current == .false) return false; self.assignment.items[variable_index] = BoolValue.fromBool(literal.isPositive()); self.saved_phase.items[variable_index] = self.assignment.items[variable_index]; self.level.items[variable_index] = self.decisionLevel(); self.reason.items[variable_index] = reason; try self.trail.append(self.allocator, literal); return true; } fn propagate(self: *Solver) !PropagationResult { while (self.propagation_cursor < self.trail.items.len) { const literal = self.trail.items[self.propagation_cursor]; self.propagation_cursor += 1; const false_literal = literal.negated(); const watch = &self.watches.items[false_literal.index()]; var index: usize = 0; while (index < watch.items.len) { const clause_index = watch.items[index].index; const update = try self.propagateClause(clause_index, false_literal); switch (update) { .moved => { _ = watch.swapRemove(index); }, .kept => { index += 1; }, .unit => |unit_literal| { if (!try self.enqueue(unit_literal, clause_index)) { return .{ .conflict = clause_index }; } self.last_stats.propagations += 1; index += 1; }, .conflict => return .{ .conflict = clause_index }, } } } return .consistent; } fn propagateClause( self: *Solver, clause_index: usize, false_literal: Literal, ) !ClausePropagation { var clause = &self.clauses.items[clause_index]; const false_watch_is_a = clause.literals[clause.watch_a].raw == false_literal.raw; const false_watch = if (false_watch_is_a) clause.watch_a else clause.watch_b; const other_watch = if (false_watch_is_a) clause.watch_b else clause.watch_a; const other_literal = clause.literals[other_watch]; if (self.literalValue(other_literal) == .true) return .kept; for (clause.literals, 0..) |candidate, candidate_index| { if (candidate_index == other_watch or candidate_index == false_watch) continue; if (self.literalValue(candidate) != .false) { if (false_watch_is_a) { clause.watch_a = candidate_index; } else { clause.watch_b = candidate_index; } try self.addWatch(candidate, clause_index); return .moved; } } return switch (self.literalValue(other_literal)) { .unset => .{ .unit = other_literal }, .false => .conflict, .true => .kept, }; } fn learnFromConflict( self: *Solver, owner: *scratch.ConflictScratch, clause_index: usize, ) !bool { assert(owner.capacity.variables == self.variableCount()); const storage = self.conflictScratchStorage(owner.capacity); assert(storage.ptr == owner.storage.ptr); assert(storage.len == owner.storage.len); var loan: scratch.ConflictScratch.Loan = undefined; try owner.acquire(self.variableCount(), &loan); defer owner.release(&loan) catch unreachable; const conflict_level = self.decisionLevel(); var path_count: usize = 0; var clause_ref = clause_index; var cursor = self.trail.items.len; var resolved_literal: ?Literal = null; while (true) { try loan.recordResolution(); const clause = self.clauses.items[clause_ref]; for (clause.literals) |literal| { const variable_index = literal.variable(); if (!try loan.markSeen(variable_index)) continue; if (self.level.items[variable_index] == conflict_level) { path_count += 1; } else { try loan.append(literal); } } while (cursor > 0) { cursor -= 1; const trail_literal = self.trail.items[cursor]; if (try loan.isSeen(trail_literal.variable())) { resolved_literal = trail_literal; break; } } const pivot = resolved_literal orelse return error.InvalidConflictGraph; try loan.clearSeen(pivot.variable()); path_count -= 1; if (path_count == 0) { try loan.append(pivot.negated()); break; } clause_ref = self.reason.items[pivot.variable()] orelse { try loan.append(pivot.negated()); break; }; } const learned = try loan.literals(); var backtrack_level: u32 = 0; for (learned) |literal| { const literal_level = self.level.items[literal.variable()]; if (literal_level != conflict_level and literal_level > backtrack_level) { backtrack_level = literal_level; } } const lbd = self.blockDistance(learned); if (!self.rupCheckConflict(learned, clause_index)) return error.InvalidProofTrace; self.backtrack(backtrack_level); const assert_literal = learned[learned.len - 1]; try self.addLearnedClause(learned, lbd); return try self.enqueue(assert_literal, self.clauses.items.len - 1); } fn blockDistance(self: *const Solver, literals: []const Literal) u32 { assert(literals.len > 0); var distinct: u32 = 0; for (literals, 0..) |literal, index| { const literal_level = self.level.items[literal.variable()]; var counted = false; for (literals[0..index]) |prior| { if (self.level.items[prior.variable()] == literal_level) { counted = true; break; } } if (!counted) distinct += 1; } assert(distinct >= 1); assert(distinct <= literals.len); return distinct; } fn addLearnedClause(self: *Solver, literals: []const Literal, lbd: u32) !void { assert(lbd >= 1); assert(lbd <= literals.len); try self.appendProofStep(literals); errdefer self.removeLastProofStep(); const pooled = self.learned_store != null and lbd > glue_lbd_max; const owned = if (pooled) self.learnedPool().acquire(literals) else try self.allocator.dupe(Literal, literals); errdefer if (pooled) self.learnedPool().release(owned) else self.allocator.free(owned); const clause_index = self.clauses.items.len; const clause = Clause{ .literals = owned, .learned = true, .lbd = lbd, .storage = if (pooled) .pool else .heap, .watch_a = 0, .watch_b = if (owned.len > 1) 1 else 0, }; try self.clauses.append(self.allocator, clause); errdefer _ = self.clauses.pop(); try self.addWatch(owned[0], clause_index); errdefer _ = self.removeWatch(owned[0], clause_index); if (owned.len > 1) { try self.addWatch(owned[1], clause_index); } self.learned_clause_count += 1; if (lbd <= glue_lbd_max) self.glue_clause_count += 1; self.last_stats.learned_clauses += 1; } fn nextUnassignedVariable(self: *const Solver) ?u32 { for (self.assignment.items, 0..) |assigned_value, index| { if (assigned_value == .unset) return @intCast(index); } return null; } fn decisionLiteral(self: *const Solver, variable_index: u32) Literal { return switch (self.saved_phase.items[variable_index]) { .false => Literal.negative(variable_index), .true, .unset => Literal.positive(variable_index), }; }};fn countWatchRefs(solver: *const Solver, literal: Literal, clause_index: usize) usize { var count: usize = 0; for (solver.watches.items[literal.index()].items) |clause_ref| { if (clause_ref.index == clause_index) count += 1; } return count;}fn expectWatchInvariant(solver: *const Solver) !void { var total_refs: usize = 0; for (solver.watches.items) |watch| { for (watch.items) |clause_ref| { try std.testing.expect(clause_ref.index < solver.clauses.items.len); } total_refs += watch.items.len; } var expected_refs: usize = 0; for (solver.clauses.items, 0..) |clause, clause_index| { const first = clause.literals[clause.watch_a]; if (clause.watch_b != clause.watch_a) { expected_refs += 2; const second = clause.literals[clause.watch_b]; if (second.raw == first.raw) { try std.testing.expectEqual( @as(usize, 2), countWatchRefs(solver, first, clause_index), ); } else { try std.testing.expectEqual( @as(usize, 1), countWatchRefs(solver, first, clause_index), ); try std.testing.expectEqual( @as(usize, 1), countWatchRefs(solver, second, clause_index), ); } } else { expected_refs += 1; try std.testing.expectEqual( @as(usize, 1), countWatchRefs(solver, first, clause_index), ); } } try std.testing.expectEqual(expected_refs, total_refs);}fn expectReasonInvariant(solver: *const Solver) !void { for (solver.trail.items) |trail_literal| { const variable_index = trail_literal.variable(); if (solver.reason.items[variable_index]) |reason_index| { try std.testing.expect(reason_index < solver.clauses.items.len); var found = false; for (solver.clauses.items[reason_index].literals) |literal| { if (literal.raw == trail_literal.raw) found = true; } try std.testing.expect(found); } }}fn appendStoreClause(solver: *Solver, literals: []const Literal, lbd: u32) !usize { for (literals) |literal| try solver.ensureVariable(literal.variable()); const owned = try solver.allocator.dupe(Literal, literals); const clause_index = solver.clauses.items.len; try solver.clauses.append(solver.allocator, .{ .literals = owned, .learned = true, .lbd = lbd, .storage = .heap, .watch_a = 0, .watch_b = if (owned.len > 1) 1 else 0, }); try solver.addWatch(owned[0], clause_index); if (owned.len > 1) try solver.addWatch(owned[1], clause_index); solver.learned_clause_count += 1; if (lbd <= glue_lbd_max) solver.glue_clause_count += 1; return clause_index;}fn retainedShapes(solver: *const Solver, buffer: [][2]u32) usize { var count: usize = 0; for (solver.clauses.items) |clause| { if (!clause.learned) continue; buffer[count] = .{ clause.lbd, @intCast(clause.literals.len) }; count += 1; } std.mem.sort([2]u32, buffer[0..count], {}, shapeLessThan); return count;}fn shapeLessThan(context: void, left: [2]u32, right: [2]u32) bool { _ = context; if (left[0] != right[0]) return left[0] < right[0]; return left[1] < right[1];}test "eviction removes worst lbd first then longest and never glue" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./root.zig").LearnedStore, "smt_victim_order"), null, null, null, null, null, null, ); } var solver = Solver.init(std.testing.allocator); defer solver.deinit(); for (0..7) |_| _ = try solver.addVariable(); try solver.addClause(&.{ Literal.positive(0), Literal.positive(1) }); _ = try appendStoreClause(&solver, &.{ Literal.positive(0), Literal.negative(1), Literal.positive(2), }, 5); _ = try appendStoreClause(&solver, &.{ Literal.negative(0), Literal.positive(1), Literal.negative(2), Literal.positive(3), Literal.negative(3), Literal.positive(4), Literal.positive(5), }, 3); _ = try appendStoreClause(&solver, &.{ Literal.negative(4), Literal.positive(5), Literal.negative(6), Literal.positive(6), }, 3); _ = try appendStoreClause(&solver, &.{ Literal.negative(5), Literal.positive(6) }, 2); _ = try appendStoreClause(&solver, &.{Literal.negative(6)}, 1); try std.testing.expectEqual(@as(usize, 5), solver.retainedLearnedClauses()); try std.testing.expectEqual(@as(usize, 3), solver.replaceableLearnedClauses()); solver.max_learned_clauses = 1; solver.reduceLearned(1, null); try std.testing.expectEqual(@as(usize, 1), solver.replaceableLearnedClauses()); try std.testing.expectEqual(@as(usize, 2), solver.last_stats.evicted_clauses); var shapes: [8][2]u32 = undefined; var shape_count = retainedShapes(&solver, &shapes); try std.testing.expectEqual(@as(usize, 3), shape_count); try std.testing.expectEqualSlices(u32, &.{ 1, 1 }, &shapes[0]); try std.testing.expectEqualSlices(u32, &.{ 2, 2 }, &shapes[1]); try std.testing.expectEqualSlices(u32, &.{ 3, 4 }, &shapes[2]); try expectWatchInvariant(&solver); solver.max_learned_clauses = 0; solver.reduceLearned(1, null); try std.testing.expectEqual(@as(usize, 0), solver.replaceableLearnedClauses()); try std.testing.expectEqual(@as(usize, 2), solver.retainedLearnedClauses()); try std.testing.expectEqual(@as(usize, 3), solver.last_stats.evicted_clauses); shape_count = retainedShapes(&solver, &shapes); try std.testing.expectEqual(@as(usize, 2), shape_count); try std.testing.expectEqualSlices(u32, &.{ 1, 1 }, &shapes[0]); try std.testing.expectEqualSlices(u32, &.{ 2, 2 }, &shapes[1]); try expectWatchInvariant(&solver);}test "eviction skips locked and protected clauses" { var solver = Solver.init(std.testing.allocator); defer solver.deinit(); for (0..3) |_| _ = try solver.addVariable(); const locked_index = try appendStoreClause(&solver, &.{ Literal.positive(0), Literal.positive(1), }, 5); const protected_index = try appendStoreClause(&solver, &.{ Literal.positive(1), Literal.positive(2), }, 4); const victim_index = try appendStoreClause(&solver, &.{ Literal.positive(2), Literal.positive(0), }, 3); _ = victim_index; try std.testing.expect(try solver.enqueue(Literal.positive(0), locked_index)); try std.testing.expect(solver.clauseLocked(locked_index)); solver.max_learned_clauses = 0; solver.reduceLearned(0, protected_index); try std.testing.expectEqual(@as(usize, 2), solver.retainedLearnedClauses()); try std.testing.expectEqual(@as(usize, 2), solver.replaceableLearnedClauses()); try std.testing.expectEqual(@as(usize, 1), solver.last_stats.evicted_clauses); var shapes: [4][2]u32 = undefined; const shape_count = retainedShapes(&solver, &shapes); try std.testing.expectEqual(@as(usize, 2), shape_count); try std.testing.expectEqualSlices(u32, &.{ 4, 2 }, &shapes[0]); try std.testing.expectEqualSlices(u32, &.{ 5, 2 }, &shapes[1]);}test "bounded solve preserves status and proof under eviction" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./root.zig").LearnedStore, "smt_bounded_solve"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./root.zig").ProofTrace, "smt_trace_bounded_solve"), null, null, null, null, null, null, ); } var solver = Solver.init(std.testing.allocator); defer solver.deinit(); const learned_limit: usize = 6; solver.max_learned_clauses = learned_limit; solver.conflict_budget = 200_000; solver.setRestartPolicy(.{ .first_conflict_interval = 1, .growth = 2 }); const pigeons: u32 = 5; const holes: u32 = 4; for (0..pigeons * holes) |_| _ = try solver.addVariable(); for (0..pigeons) |pigeon| { var placement: [holes]Literal = undefined; for (&placement, 0..) |*literal, hole| { literal.* = Literal.positive(@intCast(pigeon * holes + hole)); } try solver.addClause(&placement); } for (0..holes) |hole| { for (0..pigeons) |first| { for (first + 1..pigeons) |second| { try solver.addClause(&.{ Literal.negative(@intCast(first * holes + hole)), Literal.negative(@intCast(second * holes + hole)), }); } } } try std.testing.expectEqual(Status.unsat, try solver.solve()); const first_stats = solver.lastSolveStats(); try std.testing.expect(first_stats.evicted_clauses > 0); try std.testing.expect(solver.replaceableLearnedClauses() <= learned_limit + 1); try std.testing.expect(solver.lastProofTraceValid()); try expectPooledStorageInvariant(&solver); try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expect(solver.replaceableLearnedClauses() <= learned_limit + 1); try std.testing.expect(solver.lastProofTraceValid()); try expectPooledStorageInvariant(&solver);}fn expectPooledStorageInvariant(solver: *Solver) !void { const pool = solver.learnedPool(); var pooled_clauses: usize = 0; for (solver.clauses.items) |clause| { switch (clause.storage) { .pool => { pooled_clauses += 1; try std.testing.expect(clause.learned); try std.testing.expect(clause.lbd > glue_lbd_max); try std.testing.expect(pool.owns(clause.literals)); }, .heap => try std.testing.expect(!pool.owns(clause.literals)), } } try std.testing.expectEqual( solver.replaceableLearnedClauses(), pooled_clauses, ); try std.testing.expectEqual( pool.capacity.slots - pooled_clauses, pool.freeSlots(), );}test "learned store pool survives variable growth and cap removal" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./root.zig").LearnedStore, "smt_pool_lifecycle"), null, null, null, null, null, null, ); } var solver = Solver.init(std.testing.allocator); defer solver.deinit(); solver.max_learned_clauses = 2; solver.setRestartPolicy(.{ .first_conflict_interval = 1, .growth = 1 }); const a = try solver.addVariable(); const b = try solver.addVariable(); const c = try solver.addVariable(); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b), Literal.positive(c) }); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b), Literal.negative(c) }); try std.testing.expectEqual(Status.sat, try solver.solve()); const first_capacity = solver.learnedPool().capacity; try std.testing.expectEqual(@as(usize, 3 + 1 + 2), first_capacity.slots); for (0..4) |_| _ = try solver.addVariable(); try std.testing.expectEqual(Status.sat, try solver.solve()); const grown_capacity = solver.learnedPool().capacity; try std.testing.expectEqual(@as(usize, 7 + 1 + 2), grown_capacity.slots); try std.testing.expectEqual(@as(usize, 7), grown_capacity.slot_width); try expectPooledStorageInvariant(&solver); try expectWatchInvariant(&solver); solver.max_learned_clauses = null; try std.testing.expectEqual(Status.sat, try solver.solve()); try std.testing.expect(solver.learned_store == null); for (solver.clauses.items) |clause| { try std.testing.expectEqual(ClauseStorage.heap, clause.storage); } try expectWatchInvariant(&solver);}fn expectTraceStorageInvariant(solver: *const Solver) !void { const owner = &solver.proof_trace.?; try std.testing.expectEqual(solver.proof_steps.items.len, owner.step_count); var slab_literals: usize = 0; for (solver.proof_steps.items) |step| { if (step.literals.len == 0) continue; try std.testing.expect(owner.owns(step.literals)); slab_literals += step.literals.len; } try std.testing.expectEqual(slab_literals, owner.literal_count); try std.testing.expect(owner.step_count <= owner.capacity.steps);}test "budgeted solve keeps proof steps in the trace slab" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./root.zig").ProofTrace, "smt_trace_slab_solve"), null, null, null, null, null, null, ); } var solver = Solver.init(std.testing.allocator); defer solver.deinit(); solver.conflict_budget = 64; const a = try solver.addVariable(); const b = try solver.addVariable(); const c = try solver.addVariable(); try solver.addClause(&.{ Literal.positive(a), Literal.positive(b) }); try solver.addClause(&.{ Literal.positive(a), Literal.negative(b) }); try solver.addClause(&.{ Literal.negative(a), Literal.positive(c) }); try solver.addClause(&.{ Literal.negative(a), Literal.negative(c) }); try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expect(solver.lastProofTraceValid()); try std.testing.expect(solver.proof_steps.items.len >= 2); try expectTraceStorageInvariant(&solver); const first_slab = solver.proof_trace.?.slab.ptr; try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expect(solver.lastProofTraceValid()); try expectTraceStorageInvariant(&solver); try std.testing.expectEqual(first_slab, solver.proof_trace.?.slab.ptr);}test "proof trace survives budget growth and removal" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(@import("./root.zig").ProofTrace, "smt_trace_lifecycle"), null, null, null, null, null, null, ); } var solver = Solver.init(std.testing.allocator); defer solver.deinit(); solver.conflict_budget = 4; const a = try solver.addVariable(); const b = try solver.addVariable(); try solver.addClause(&.{ Literal.positive(a), Literal.positive(b) }); try solver.addClause(&.{ Literal.positive(a), Literal.negative(b) }); try solver.addClause(&.{ Literal.negative(a), Literal.positive(b) }); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b) }); try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expect(solver.lastProofTraceValid()); const first_capacity = solver.proof_trace.?.capacity; try std.testing.expectEqual(@as(usize, 6), first_capacity.steps); try expectTraceStorageInvariant(&solver); solver.conflict_budget = 32; try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expect(solver.lastProofTraceValid()); try std.testing.expectEqual(@as(usize, 34), solver.proof_trace.?.capacity.steps); try expectTraceStorageInvariant(&solver); solver.conflict_budget = null; try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expect(solver.lastProofTraceValid()); try std.testing.expect(solver.proof_trace == null); for (solver.proof_steps.items) |step| { std.mem.doNotOptimizeAway(step.literals.len); }}test "entry eviction enforces a newly set cap" { var solver = Solver.init(std.testing.allocator); defer solver.deinit(); const a = try solver.addVariable(); const b = try solver.addVariable(); try solver.addClause(&.{ Literal.positive(a), Literal.positive(b) }); _ = try appendStoreClause(&solver, &.{ Literal.positive(a), Literal.negative(b) }, 4); _ = try appendStoreClause(&solver, &.{ Literal.negative(a), Literal.positive(b) }, 3); try std.testing.expectEqual(@as(usize, 2), solver.replaceableLearnedClauses()); solver.max_learned_clauses = 0; try std.testing.expectEqual(Status.sat, try solver.solve()); try std.testing.expectEqual(@as(usize, 0), solver.replaceableLearnedClauses()); try std.testing.expectEqual(@as(usize, 0), solver.retainedLearnedClauses()); try std.testing.expectEqual(@as(usize, 2), solver.lastSolveStats().evicted_clauses); try expectWatchInvariant(&solver);}test "removing a learned clause preserves base prefix and behavior" { var solver = Solver.init(std.testing.allocator); defer solver.deinit(); const a = try solver.addVariable(); const b = try solver.addVariable(); const c = try solver.addVariable(); _ = c; try solver.addClause(&.{ Literal.negative(a), Literal.negative(b), Literal.positive(2) }); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b), Literal.negative(2) }); try std.testing.expectEqual(Status.sat, try solver.solve()); try std.testing.expectEqual(@as(usize, 3), solver.clauses.items.len); try std.testing.expect(solver.clauses.items[2].learned); const base_first = solver.clauses.items[0].literals; const base_second = solver.clauses.items[1].literals; solver.backtrack(0); solver.removeLearnedClause(2); try std.testing.expectEqual(@as(usize, 2), solver.clauses.items.len); try std.testing.expectEqual(base_first.ptr, solver.clauses.items[0].literals.ptr); try std.testing.expectEqual(base_second.ptr, solver.clauses.items[1].literals.ptr); try expectWatchInvariant(&solver); try expectReasonInvariant(&solver); try std.testing.expectEqual(Status.sat, try solver.solve());}test "removing a middle learned clause patches the moved clause" { var solver = Solver.init(std.testing.allocator); defer solver.deinit(); const pigeons: u32 = 4; const holes: u32 = 3; for (0..pigeons * holes) |_| _ = try solver.addVariable(); for (0..pigeons) |pigeon| { var placement: [holes]Literal = undefined; for (&placement, 0..) |*literal, hole| { literal.* = Literal.positive(@intCast(pigeon * holes + hole)); } try solver.addClause(&placement); } for (0..holes) |hole| { for (0..pigeons) |first| { for (first + 1..pigeons) |second| { try solver.addClause(&.{ Literal.negative(@intCast(first * holes + hole)), Literal.negative(@intCast(second * holes + hole)), }); } } } const base_count = solver.clauses.items.len; try std.testing.expectEqual(Status.unsat, try solver.solve()); try std.testing.expectEqual(@as(u32, 0), solver.decisionLevel()); try std.testing.expect(solver.clauses.items.len > base_count + 1); var candidate: ?usize = null; for (base_count..solver.clauses.items.len - 1) |clause_index| { if (!solver.clauseLocked(clause_index)) { candidate = clause_index; break; } } const removed_index = candidate.?; const last_index = solver.clauses.items.len - 1; const moved_literals = solver.clauses.items[last_index].literals; solver.removeLearnedClause(removed_index); try std.testing.expectEqual(last_index, solver.clauses.items.len); try std.testing.expectEqual(moved_literals.ptr, solver.clauses.items[removed_index].literals.ptr); try expectWatchInvariant(&solver); try expectReasonInvariant(&solver); try std.testing.expectEqual(Status.unsat, try solver.solve());}test "learned clause records single-level block distance" { var solver = Solver.init(std.testing.allocator); defer solver.deinit(); const a = try solver.addVariable(); const b = try solver.addVariable(); try solver.addClause(&.{ Literal.positive(a), Literal.positive(b) }); try solver.addClause(&.{ Literal.negative(a), Literal.positive(b) }); try solver.addClause(&.{ Literal.positive(a), Literal.negative(b) }); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b) }); try std.testing.expectEqual(Status.unsat, try solver.solve()); var learned_count: usize = 0; for (solver.clauses.items) |clause| { if (clause.learned) { learned_count += 1; try std.testing.expectEqual(@as(u32, 1), clause.lbd); } else { try std.testing.expectEqual(@as(u32, 0), clause.lbd); } } try std.testing.expect(learned_count > 0);}test "learned clause records multi-level block distance" { var solver = Solver.init(std.testing.allocator); defer solver.deinit(); const a = try solver.addVariable(); const b = try solver.addVariable(); const c = try solver.addVariable(); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b), Literal.positive(c) }); try solver.addClause(&.{ Literal.negative(a), Literal.negative(b), Literal.negative(c) }); try std.testing.expectEqual(Status.sat, try solver.solve()); var learned_count: usize = 0; for (solver.clauses.items) |clause| { if (clause.learned) { learned_count += 1; try std.testing.expectEqual(@as(usize, 2), clause.literals.len); try std.testing.expectEqual(@as(u32, 2), clause.lbd); } } try std.testing.expectEqual(@as(usize, 1), learned_count);}Audit
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |