Skip to documentation
SLOP

tiny.smt.sat.store

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

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallsNo direct callsprivate sourcelib.smt.src.sat.solver.SolverensureLearnedStoresat.store.Storeinittest sourcelib.smt.src.sat.storetest: learned store capacity matches ...sat.LearnedStore.Capacityderive
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.smt.src.sat.solver.SolverensureLearnedStoretest sourcelib.smt.src.sat.storetest: learned store capacity matches ...test sourcelib.smt.src.sat.storetest: learned store fills to capacity...test sourcelib.smt.src.sat.storetest: learned store init cleans every...sat.LearnedStore.Limitsinspect
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.smt.src.sat.solver.SolverensureLearnedStoretest sourcelib.smt.src.sat.storetest: learned store fills to capacity...sat.store.Storeownssat.LearnedStoreacquire
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest sourcelib.smt.src.sat.storetest: learned store fills to capacity...test sourcelib.smt.src.sat.storetest: learned store init cleans every...sat.LearnedStoreactivate
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.smt.src.sat.storecheckStoreInitFailurestest sourcelib.smt.src.sat.storetest: learned store fills to capacity...test sourcelib.smt.src.sat.storetest: learned store init cleans every...sat.LearnedStoredeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstest sourcelib.smt.src.sat.storetest: learned store fills to capacity...test sourcelib.smt.src.sat.storetest: learned store init cleans every...sat.LearnedStorefreeSlots
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.smt.src.sat.storecheckStoreInitFailurestest sourcelib.smt.src.sat.storetest: learned store fills to capacity...test sourcelib.smt.src.sat.storetest: learned store init cleans every...sat.store.Store.Capacityderivesat.LearnedStoreinit
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callssat.store.Storeacquiresat.store.Storereleasetest sourcelib.smt.src.sat.storetest: learned store fills to capacity...sat.LearnedStoreowns
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.smt.src.sat.storetest: learned store fills to capacity...sat.store.Storeownssat.LearnedStorerelease
Static calls · unresolved targets: 0 · external targets: 0.

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

Definitions15
Public names29
Members10
Version26.7.0
Revisiondaab053ee433