tiny.smt.sat.store
Defined in sat.
Fixed memory for the learned clauses a solver may remove under a cap, sized before a search and reused slot by slot.
API (14)
Actions
Public operations.
Store.Capacity.derive: Returns the sizes a store needs forlimits.Store.Limits.inspect: Returns the limits forreplaceable_clausesandvariables.Store.acquire: Copiesliteralsinto a free slot and returns the copy.Store.activate: Moves the store from.initializationto.steady, after which the store calls no allocator untildeinit.Store.admits: Returns true when the store has at leastrequired.slotsslots, each at leastrequired.slot_widthliterals wide.Store.deinit: Frees the slab and the stack withallocatorand leaves the store undefined.Store.freeSlots: Returns the number of free slots.Store.init: Returns a store in.initializationwhose slab and stack come fromallocator, with every slot free.Store.owns: Returns true whenliteralsstarts inside the slab.Store.release: Returns the slot that holdsliteralsto the free stack.
Types and contracts
Public types and contracts.
Store: Fixed memory for the replaceable learned clauses of one solver: equal slots in one slab of literals, one clause per slot, with a stack of free slot numbers.Store.Capacity: The sizes of one store: its slot count, its slot width in literals and its total literal count.Store.Limits: The inputs that size a store: the cap on replaceable learned clauses and the solver's variable count.
Values and defaults
Public values and defaults.
Store.claim: A compile-time record of the store's memory: what the slab and the stack cover, what stays outside them, the equation that sizes them fromLimits, the overload behavior, and the tests that witness each obligation.
Source
Source: lib/smt/src/sat/root.zig:50
zig
pub const store = @import("store.zig");Source: lib/smt/src/sat/store.zig
zig
//! Fixed memory for the learned clauses a solver may remove under a cap, sized before a search and//! reused slot by slot. A solver with a cap on learned clauses adds and removes clauses throughout//! a search, and a caller that bounds memory wants that churn to stay inside memory whose size is//! known before the search starts.//!//! Learned clauses differ in length, and each holds at most one literal per variable. The solver//! keeps a clause that justifies a current value even when the cap asks for a removal, and it keeps//! the clause learned most recently, so the count it holds can pass the cap.//!//! The store (`Store`) holds only the learned clauses whose literals span more than two decision//! levels (*replaceable learned clauses*), and the other learned clauses stay in the solver's heap//! memory, as every learned clause does while the cap is null. The store gives each clause one slot//! as wide as the variable count, all in one block of literals (a slab), and keeps the numbers of//! the free slots on a stack, so taking or returning a slot is constant work with no allocator//! call. The slot count is the larger of the cap and one more than the variable count, plus 2,//! which covers the clauses kept because they justify current values, the clause learned most//! recently and one more. Taking a slot from a full store trips an assertion, because the slot//! count is meant to rule that case out. When the cap or the variable count grows, the solver//! builds a larger store and moves the stored clauses into it, and when the cap is removed it moves//! them back to its heap. A compile-time record of this memory (`Store.claim`) states the size//! equation and the overload behavior, and the shape check at the end of the file validates it.const std = @import("std");const alloc_phase = @import("alloc_phase");const types = @import("types.zig");const assert = std.debug.assert;const Allocator = std.mem.Allocator;const Literal = types.Literal;/// Fixed memory for the replaceable learned clauses of one solver: equal slots in one slab of/// literals, one clause per slot, with a stack of free slot numbers. The solver keeps one while/// `Solver.max_learned_clauses` is set, and it moves each replaceable learned clause into it as the/// clause is learned. `init` allocates the slab and the stack, `activate` moves the store from/// `.initialization` to `.steady`, and `deinit` moves it to `.teardown` and frees both. In/// `.steady`, taking and returning a slot is constant work and calls no allocator.pub const Store = struct { /// A compile-time record of the store's memory: what the slab and the stack cover, what stays /// outside them, the equation that sizes them from `Limits`, the overload behavior, and the /// tests that witness each obligation. The shape check at the end of the file validates the /// record and the store's shape at compile time. The equation gives the slot count times the /// variable count in literals, plus one 32-bit stack entry per slot, with the slot count that /// `Capacity.derive` computes. The record states that an overflow is refused before any storage /// exists, and that taking a slot from a full store is a sizing bug that trips an assertion. /// Learned clauses whose literals span at most two decision levels, and the learned clauses of /// a solver whose cap is null, stay outside the store in the solver's heap memory. pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "smt.learned_store", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "replaceable_learned_clause_literal_slab_of_slots_x_fc9c9a108360", .lifetime = .steady, .detail = "replaceable learned-clause literal slab of slots x variableCount literals", }, .{ .id = "slot_free_list_stack", .lifetime = .steady, .detail = "slot free-list stack", }, }, .excluded = &.{ "glue learned clauses (lbd <= 2): the monotone termination set stays heap-owned by design", "original clauses, clause headers, watch lists, trail, and search state in solver-owned dynamic containers", "proof assumptions and unbudgeted proof steps; budgeted step literal storage is owned by smt.proof_trace", "learned clauses on solvers without max_learned_clauses, which keep stage-three heap semantics", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "replaceable_clauses", "replaceable_clauses"), alloc_phase.capacity.bindInput(Limits, "variables", "variables"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(Literal, "literal"), alloc_phase.capacity.bindType(u32, "u32"), }, .nodes = &.{ .{ .input = 0 }, .{ .input = 1 }, .{ .constant = 1 }, .{ .add = .{ .left = 1, .right = 2 } }, .{ .maximum = .{ .left = 0, .right = 3 } }, .{ .constant = 2 }, .{ .add = .{ .left = 4, .right = 5 } }, .{ .product = .{ .left = 6, .right = 1 } }, .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .product = .{ .left = 7, .right = 8 } }, .{ .scale = .{ .node = 6, .coefficient = .{ .size_of_concrete_type = 1 } } }, .{ .add = .{ .left = 9, .right = 10 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 11, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "Limits and Capacity.derive reject arithmetic overflow before any storage is acquired; steady acquire asserts a free slot because the slot equation covers locked clauses plus the protected just-learned clause plus one admission, so steady exhaustion is a capacity-model bug and crashes", }, .risks = .{ .transitive = .{ .status = .open, .detail = "clause headers, watch lists, and proof steps referencing pool literals live in solver-owned dynamic containers, pool versus heap freeing is routed by a per-clause storage tag, BoundedStoreExerciseProperty forces pooled eviction and checks retained-clause entailment against a satisfiable truth-table model, and BoundedStoreProperty checks bounded status, cores, and artifacts through the smt-pbt lane", }, .foreign = .{ .status = .excluded, .detail = "the slab and free list are process-local memory from the caller allocator with no operating-system or foreign-runtime edge", }, }, .obligations = &.{ .{ .key = "smt_capacity_capacity_model", .role = .capacity_model }, .{ .key = "smt_capacity_overload", .role = .overload }, .{ .key = "smt_oom_retry", .role = .foreign_risk }, .{ .key = "smt_sealed_fill_overload", .role = .overload }, .{ .key = "smt_sealed_fill_foreign_risk", .role = .foreign_risk }, .{ .key = "smt_victim_order", .role = .transitive_risk }, .{ .key = "smt_bounded_solve", .role = .transitive_risk }, .{ .key = "smt_pool_lifecycle", .role = .transitive_risk }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; phase: alloc_phase.capacity.Phase, /// The sizes that `Capacity.derive` gave when the store was built. `admits` compares them with /// the sizes a solve needs. capacity: Capacity, slab: []Literal, free_slots: []u32, free_count: usize, /// The inputs that size a store: the cap on replaceable learned clauses and the solver's /// variable count. The solver builds one from its cap and its variable count at the start of /// each capped solve. pub const Limits = struct { /// The cap on replaceable learned clauses, taken from `Solver.max_learned_clauses`. replaceable_clauses: usize, /// The solver's variable count, which is the most literals a learned clause holds. variables: usize, /// Returns the limits for `replaceable_clauses` and `variables`. The solver builds its /// store limits through it. pub fn inspect(replaceable_clauses: usize, variables: usize) Limits { return .{ .replaceable_clauses = replaceable_clauses, .variables = variables, }; } }; /// The sizes of one store: its slot count, its slot width in literals and its total literal /// count. The solver derives the sizes a solve needs and asks its current store whether it /// `admits` them before it builds a new one. pub const Capacity = struct { /// The number of clauses the store holds at once: the larger of the cap and one more than /// the variable count, plus 2. The count covers the clauses kept because they justify /// current values, the clause learned most recently and one more. slots: usize, /// Literals per slot, equal to the variable count. slot_width: usize, /// Literals in the slab: the slot count times the slot width. literal_count: usize, /// Returns the sizes a store needs for `limits`. The solver and `init` size a store from it /// before anything is allocated. The call returns `error.CapacityOverflow` when the slot /// count, the literal count or the slab's byte size overflows `usize`, or when the slot /// count passes the largest `u32`. pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity { const locked_floor = std.math.add(usize, limits.variables, 1) catch return error.CapacityOverflow; const retained_peak = @max(limits.replaceable_clauses, locked_floor); const slots = std.math.add(usize, retained_peak, 2) catch return error.CapacityOverflow; if (slots > std.math.maxInt(u32)) return error.CapacityOverflow; const literal_count = std.math.mul(usize, slots, limits.variables) catch return error.CapacityOverflow; _ = std.math.mul(usize, literal_count, @sizeOf(Literal)) catch return error.CapacityOverflow; assert(slots > limits.replaceable_clauses); assert(slots > limits.variables); return .{ .slots = slots, .slot_width = limits.variables, .literal_count = literal_count, }; } }; /// Returns a store in `.initialization` whose slab and stack come from `allocator`, with every /// slot free. The solver builds its store with it at the start of a capped solve that needs /// more room than its current store has. The call returns `error.CapacityOverflow` from /// `Capacity.derive` and `error.OutOfMemory` from the allocator, and it frees the slab when the /// stack allocation fails. The caller passes the same allocator to `deinit`. pub fn init(allocator: Allocator, limits: Limits) !Store { const capacity = try Capacity.derive(limits); const slab = try allocator.alloc(Literal, capacity.literal_count); errdefer allocator.free(slab); const free_slots = try allocator.alloc(u32, capacity.slots); for (free_slots, 0..) |*slot, index| { slot.* = @intCast(capacity.slots - 1 - index); } return .{ .phase = .initialization, .capacity = capacity, .slab = slab, .free_slots = free_slots, .free_count = capacity.slots, }; } /// Moves the store from `.initialization` to `.steady`, after which the store calls no /// allocator until `deinit`. The solver calls it once, right after `init`. The call asserts /// that the store is as `init` left it. pub fn activate(self: *Store) void { assert(self.phase == .initialization); assert(self.slab.len == self.capacity.literal_count); assert(self.free_slots.len == self.capacity.slots); assert(self.free_count == self.capacity.slots); self.phase = .steady; } /// Returns true when the store has at least `required.slots` slots, each at least /// `required.slot_width` literals wide. The solver keeps its store across solves while the /// store admits what the next solve needs. The call asserts that the store is `.steady`. pub fn admits(self: *const Store, required: Capacity) bool { assert(self.phase == .steady); if (self.capacity.slots < required.slots) return false; return self.capacity.slot_width >= required.slot_width; } /// Returns the number of free slots. A caller checks how many slots remain free, for example to /// confirm that every slot is either free or holds a stored clause. pub fn freeSlots(self: *const Store) usize { assert(self.phase == .steady); assert(self.free_count <= self.capacity.slots); return self.free_count; } /// Returns true when `literals` starts inside the slab. The solver checks which memory holds a /// clause before it moves or frees the clause. The call works in any phase before `.teardown`. pub fn owns(self: *const Store, literals: []const Literal) bool { assert(self.phase != .teardown); const base = @intFromPtr(self.slab.ptr); const address = @intFromPtr(literals.ptr); if (address < base) return false; return address < base + self.slab.len * @sizeOf(Literal); } /// Copies `literals` into a free slot and returns the copy. The solver stores each replaceable /// learned clause through it and keeps the returned slice in its clause list. The slot taken is /// the one freed most recently. The call asserts that the store is `.steady`, that `literals` /// holds from 1 to `slot_width` literals and lies outside the slab, and that a slot is free, /// because a full store is a sizing bug. pub fn acquire(self: *Store, literals: []const Literal) []Literal { assert(self.phase == .steady); assert(literals.len >= 1); assert(literals.len <= self.capacity.slot_width); assert(self.free_count > 0); assert(!self.owns(literals)); self.free_count -= 1; const slot = self.free_slots[self.free_count]; assert(slot < self.capacity.slots); const base = @as(usize, slot) * self.capacity.slot_width; const target = self.slab[base .. base + literals.len]; @memcpy(target, literals); return target; } /// Returns the slot that holds `literals` to the free stack. The solver calls it when it /// removes a learned clause or moves the clause back to its heap. The next `acquire` reuses /// that slot. The call asserts that the store is `.steady` and that `literals` starts at a slot /// boundary inside the slab. pub fn release(self: *Store, literals: []Literal) void { assert(self.phase == .steady); assert(self.owns(literals)); assert(self.capacity.slot_width > 0); const offset = @intFromPtr(literals.ptr) - @intFromPtr(self.slab.ptr); const slot_bytes = self.capacity.slot_width * @sizeOf(Literal); assert(offset % slot_bytes == 0); const slot = offset / slot_bytes; assert(slot < self.capacity.slots); assert(literals.len <= self.capacity.slot_width); assert(self.free_count < self.capacity.slots); self.free_slots[self.free_count] = @intCast(slot); self.free_count += 1; } /// Frees the slab and the stack with `allocator` and leaves the store undefined. The solver /// calls it when it replaces its store, when it drops the store after the cap is removed, and /// when it is freed itself. `allocator` has to be the one given to `init`. Every slice the /// store handed out becomes invalid. pub fn deinit(self: *Store, allocator: Allocator) void { assert(self.phase != .teardown); self.phase = .teardown; allocator.free(self.free_slots); allocator.free(self.slab); self.* = undefined; }};comptime { alloc_phase.capacity.requireAllocatorExactOwnerShape(Store);}test "learned store capacity matches an independent typed-byte model" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Store, "smt_capacity_capacity_model"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Store, "smt_capacity_overload"), null, null, null, null, null, null, ); } const limits = Store.Limits.inspect(256, 96); const capacity = try Store.Capacity.derive(limits); const expected_slots = 256 + 2; try std.testing.expectEqual(@as(usize, expected_slots), capacity.slots); try std.testing.expectEqual(@as(usize, 96), capacity.slot_width); try std.testing.expectEqual(@as(usize, expected_slots * 96), capacity.literal_count); const locked_dominated = try Store.Capacity.derive(Store.Limits.inspect(4, 20)); try std.testing.expectEqual(@as(usize, 20 + 1 + 2), locked_dominated.slots); try std.testing.expectEqual(@as(usize, 20), locked_dominated.slot_width); try std.testing.expectError( error.CapacityOverflow, Store.Capacity.derive(Store.Limits.inspect(std.math.maxInt(usize) - 1, 8)), ); try std.testing.expectError( error.CapacityOverflow, Store.Capacity.derive(Store.Limits.inspect(8, std.math.maxInt(usize) - 1)), ); try std.testing.expectError( error.CapacityOverflow, Store.Capacity.derive(Store.Limits.inspect(std.math.maxInt(u32), 4)), );}fn checkStoreInitFailures(allocator: Allocator, limits: Store.Limits) !void { var store = try Store.init(allocator, limits); defer store.deinit(allocator); try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, store.phase);}test "learned store init cleans every allocation failure and retries" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Store, "smt_oom_retry"), null, null, null, null, null, null, ); } const limits = Store.Limits.inspect(4, 6); try std.testing.checkAllAllocationFailures( std.testing.allocator, checkStoreInitFailures, .{limits}, ); var store = try Store.init(std.testing.allocator, limits); defer store.deinit(std.testing.allocator); store.activate(); try std.testing.expectEqual(@as(usize, 9), store.freeSlots());}test "learned store fills to capacity with stable slots and zero steady operations" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Store, "smt_sealed_fill_overload"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(Store, "smt_sealed_fill_foreign_risk"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var store = try Store.init(failing.allocator(), Store.Limits.inspect(2, 3)); defer store.deinit(failing.allocator()); store.activate(); const slot_count = store.capacity.slots; try std.testing.expectEqual(@as(usize, 6), slot_count); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; const pattern = [_]Literal{ Literal.positive(0), Literal.negative(1), Literal.positive(2) }; var held: [8][]Literal = undefined; for (0..slot_count) |index| { held[index] = store.acquire(pattern[0 .. 1 + index % 3]); } try std.testing.expectEqual(@as(usize, 0), store.freeSlots()); for (0..slot_count) |index| { try std.testing.expect(store.owns(held[index])); try std.testing.expectEqual(@as(u32, Literal.positive(0).raw), held[index][0].raw); } const first_pointer = held[0].ptr; store.release(held[0]); try std.testing.expectEqual(@as(usize, 1), store.freeSlots()); const reacquired = store.acquire(&.{Literal.negative(2)}); try std.testing.expectEqual(first_pointer, reacquired.ptr); try std.testing.expectEqual(@as(usize, 0), store.freeSlots()); for (held[1..slot_count]) |slice| store.release(slice); store.release(reacquired); try std.testing.expectEqual(slot_count, store.freeSlots()); try std.testing.expect(!failing.has_induced_failure);}Audit
| Definitions | 15 |
|---|---|
| Public names | 29 |
| Members | 10 |
| Version | 26.7.0 |
| Revision | daab053ee433 |