Skip to documentation
SLOP

tiny.smt.sat.solver

Reference 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.

No direct callersNo direct callssatsolver
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions1
Public names1
Members0
Version26.7.0
Revisiondaab053ee433