Skip to documentation
SLOP

tiny.smt.sat.trace

Reference tiny.smt sat trace

Defined in sat.

Fixed memory for the proof steps of one solve, used when the solve has a conflict limit.

API (16)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallsNo direct callsprivate sourcelib.smt.src.sat.solver.SolverensureProofTracesat.ProofTraceinittest sourcelib.smt.src.sat.tracetest: proof trace capacity matches an...sat.ProofTrace.Capacityderive
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.smt.src.sat.solver.SolverensureProofTracetest sourcelib.smt.src.sat.tracetest: proof trace capacity matches an...test sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...test sourcelib.smt.src.sat.tracetest: proof trace init cleans every a...sat.ProofTrace.Limitsinspect
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...sat.ProofTraceownssat.ProofTraceacquire
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...sat.ProofTraceacquireEmpty
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.smt.src.sat.solver.SolverensureProofTracetest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...test sourcelib.smt.src.sat.tracetest: proof trace init cleans every a...sat.ProofTraceactivate
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...sat.ProofTraceclear
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.smt.src.sat.tracecheckTraceInitFailurestest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...test sourcelib.smt.src.sat.tracetest: proof trace init cleans every a...sat.ProofTracedeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...test sourcelib.smt.src.sat.tracetest: proof trace init cleans every a...sat.ProofTracefreeSteps
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.smt.src.sat.solver.SolverensureProofTraceprivate sourcelib.smt.src.sat.tracecheckTraceInitFailurestest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...test sourcelib.smt.src.sat.tracetest: proof trace init cleans every a...sat.ProofTrace.Capacityderivesat.ProofTraceinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callssat.ProofTraceacquiresat.ProofTracereleaseLasttest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...sat.ProofTraceowns
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.smt.src.sat.tracetest: proof trace fills to capacity w...sat.ProofTraceownssat.ProofTracereleaseLast
Static calls · unresolved targets: 0 · external targets: 0.

Source: lib/smt/src/sat/root.zig:51

zig
pub const trace = @import("trace.zig");

Source: lib/smt/src/sat/trace.zig

zig
//! Fixed memory for the proof steps of one solve, used when the solve has a conflict limit. A//! solver records a proof step for each clause it learns, and a caller that bounds a search by a//! conflict limit wants the memory for those steps known before the search starts.//!//! A solve learns at most one clause per conflict, and each clause holds at most one literal per//! variable, so a conflict limit bounds the steps and their literals, with room left for the final//! empty clause. The steps of a solve are dropped all at once when the solver clears them, and the//! solver takes back only the step it added last, when that step's clause fails to be stored.//!//! The trace storage (`Trace`) packs the step literals end to end in one block of literals (a//! slab), with room for the conflict limit plus 2 steps, each at most as wide as the variable//! count. Adding a step copies its literals after the last one, clearing resets two counters, and//! taking back a step drops the last one, so no operation after setup calls an allocator. Adding a//! step to a full trace trips an assertion, because the step count is meant to rule that case out.//! When the conflict limit is null, the solver keeps its steps in its heap memory, and so do the//! extra solves of core minimization. A compile-time record of this memory (`Trace.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 literals of one solve's proof steps, packed end to end in one slab, with/// counts of the steps and literals handed out. The solver keeps one while `Solver.conflict_budget`/// is set and records each proof step of a solve in it. `init` allocates the slab, `activate` moves/// the trace from `.initialization` to `.steady`, and `deinit` moves it to `.teardown` and frees/// the slab. In `.steady`, adding, taking back and clearing steps call no allocator, and adding a/// step costs the copy of its literals. The solver clears it whenever it clears its proof trace,/// and keeps it across solves while it admits what the next solve needs.pub const Trace = struct {    /// A compile-time record of the trace's memory: what the slab covers, what stays outside it,    /// the equation that sizes it 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 trace's    /// shape at compile time. The equation gives the conflict limit plus 2, times the variable    /// count, in literals. The record states that an overflow is refused before any storage exists,    /// and that adding a step to a full trace is a sizing bug that trips an assertion. The solver's    /// list of steps, the proof assumptions, the steps of solves whose conflict limit is null and    /// of core minimization, and exported proof artifacts stay outside the slab.    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "smt.proof_trace",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{                    .{                        .id = "proof_step_literal_slab_of_steps_x_variablecount_li_d1a74d0532c5",                        .lifetime = .steady,                        .detail = "proof step literal slab of steps x variableCount literals on budgeted solves",                    },                },                .excluded = &.{                    "proof step headers in the solver-owned proof_steps list, whose capacity is reserved during trace initialization",                    "proof assumptions, which track caller assumption counts",                    "steps on solvers without conflict_budget and on probe scratch solvers (proof_trace_slab = false), which keep heap dupe semantics",                    "ProofArtifact exports, which copy into caller-owned storage",                },            },            .capacity = .{                .inputs = &.{                    alloc_phase.capacity.bindInput(Limits, "conflicts", "conflicts"),                    alloc_phase.capacity.bindInput(Limits, "variables", "variables"),                },                .type_selectors = &.{                    alloc_phase.capacity.bindType(Literal, "literal"),                },                .nodes = &.{                    .{ .input = 0 },                    .{ .constant = 2 },                    .{ .add = .{ .left = 0, .right = 1 } },                    .{ .input = 1 },                    .{ .product = .{ .left = 2, .right = 3 } },                    .{ .constant = 1 },                    .{ .scale = .{ .node = 5, .coefficient = .{ .size_of_concrete_type = 0 } } },                    .{ .product = .{ .left = 4, .right = 6 } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .exact,                    .expression = 7,                }},            },            .overload = .{                .kind = .reject_before_seal,                .detail = "Capacity.derive rejects arithmetic overflow before any storage is acquired, so oversized budgets fail the solve loudly; steady acquire asserts a free step and slab room because the step equation covers every learn plus the final step, so steady exhaustion is a capacity-model bug and crashes",            },            .risks = .{                .transitive = .{                    .status = .open,                    .detail = "step headers referencing slab literals live in the solver-owned proof_steps list with capacity reserved at trace initialization, slab versus heap freeing is routed by trace presence with all-or-nothing storage per clear cycle, and the bounded differential and store-exercise properties independently check artifact acceptance, exact-scope unsatisfiability, and retained-clause entailment through the smt-pbt lane",                },                .foreign = .{                    .status = .excluded,                    .detail = "the slab is process-local memory from the caller allocator with no operating-system or foreign-runtime edge",                },            },            .obligations = &.{                .{ .key = "smt_trace_capacity_capacity_model", .role = .capacity_model },                .{ .key = "smt_trace_capacity_overload", .role = .overload },                .{ .key = "smt_trace_oom_retry", .role = .foreign_risk },                .{ .key = "smt_trace_sealed_fill_overload", .role = .overload },                .{ .key = "smt_trace_sealed_fill_foreign_risk", .role = .foreign_risk },                .{ .key = "smt_trace_slab_solve", .role = .transitive_risk },                .{ .key = "smt_trace_lifecycle", .role = .transitive_risk },                .{ .key = "smt_trace_bounded_solve", .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 trace was built. `admits`, `freeSteps` and    /// the solver's reservation of its step list read them.    capacity: Capacity,    slab: []Literal,    step_count: usize,    literal_count: usize,    /// The inputs that size a trace: the solve's conflict limit and the solver's variable count.    /// The solver builds one from `conflict_budget` and its variable count at the start of each    /// solve with a conflict limit.    pub const Limits = struct {        /// The conflict limit of the solve, taken from `Solver.conflict_budget`.        conflicts: usize,        /// The solver's variable count, which is the most literals one step holds.        variables: usize,        /// Returns the limits for `conflicts` and `variables`. The solver builds its trace limits        /// through it.        pub fn inspect(conflicts: usize, variables: usize) Limits {            return .{                .conflicts = conflicts,                .variables = variables,            };        }    };    /// The sizes of one trace: its step count, its step width in literals and its total literal    /// count. The solver derives the sizes a solve needs and asks its current trace whether it    /// `admits` them before it builds a new one.    pub const Capacity = struct {        /// Steps the trace holds: the conflict limit plus 2. The count covers a step for every        /// clause learned within the limit and the final empty step.        steps: usize,        /// The most literals in one step, equal to the variable count.        step_width: usize,        /// Literals in the slab: the step count times the step width.        literal_count: usize,        /// Returns the sizes a trace needs for `limits`. The solver and `init` size a trace from it        /// before anything is allocated. The call returns `error.CapacityOverflow` when the step        /// count, the literal count or the slab's byte size overflows `usize`.        pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {            const steps = std.math.add(usize, limits.conflicts, 2) catch                return error.CapacityOverflow;            const literal_count = std.math.mul(usize, steps, limits.variables) catch                return error.CapacityOverflow;            _ = std.math.mul(usize, literal_count, @sizeOf(Literal)) catch                return error.CapacityOverflow;            assert(steps > limits.conflicts);            assert(literal_count == steps * limits.variables);            return .{                .steps = steps,                .step_width = limits.variables,                .literal_count = literal_count,            };        }    };    /// Returns a trace in `.initialization` whose slab comes from `allocator`, with every step    /// free. The solver builds its trace with it at the start of a solve whose conflict limit or    /// variable count needs more room than its current trace has. The call returns    /// `error.CapacityOverflow` from `Capacity.derive` and `error.OutOfMemory` from the allocator.    /// The caller passes the same allocator to `deinit`.    pub fn init(allocator: Allocator, limits: Limits) !Trace {        const capacity = try Capacity.derive(limits);        const slab = try allocator.alloc(Literal, capacity.literal_count);        return .{            .phase = .initialization,            .capacity = capacity,            .slab = slab,            .step_count = 0,            .literal_count = 0,        };    }    /// Moves the trace from `.initialization` to `.steady`, after which the trace calls no    /// allocator until `deinit`. The solver calls it once, right after `init`. The call asserts    /// that the trace is as `init` left it.    pub fn activate(self: *Trace) void {        assert(self.phase == .initialization);        assert(self.slab.len == self.capacity.literal_count);        assert(self.step_count == 0);        assert(self.literal_count == 0);        self.phase = .steady;    }    /// Returns true when the trace's step count, step width and literal count are each at least    /// those of `required`. The solver keeps its trace across solves while the trace admits what    /// the next solve needs. The call asserts that the trace is `.steady`.    pub fn admits(self: *const Trace, required: Capacity) bool {        assert(self.phase == .steady);        if (self.capacity.steps < required.steps) return false;        if (self.capacity.step_width < required.step_width) return false;        return self.capacity.literal_count >= required.literal_count;    }    /// Returns the number of steps still free since the last `clear`. A caller checks how many    /// steps remain before the trace is full.    pub fn freeSteps(self: *const Trace) usize {        assert(self.phase == .steady);        assert(self.step_count <= self.capacity.steps);        return self.capacity.steps - self.step_count;    }    /// Returns true when `literals` starts inside the slab. The solver checks, before it clears its    /// proof trace, that every nonempty step it holds lives in the slab. The call works in any    /// phase before `.teardown`.    pub fn owns(self: *const Trace, 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` after the last step, counts one step, and returns the copy. The solver    /// records each nonempty proof step through it. The call asserts that the trace is `.steady`,    /// that `literals` holds from 1 to `step_width` literals and lies outside the slab, and that a    /// step and enough slab room are free, because a full trace is a sizing bug.    pub fn acquire(self: *Trace, literals: []const Literal) []Literal {        assert(self.phase == .steady);        assert(literals.len >= 1);        assert(literals.len <= self.capacity.step_width);        assert(self.step_count < self.capacity.steps);        assert(self.literal_count + literals.len <= self.capacity.literal_count);        assert(!self.owns(literals));        const target = self.slab[self.literal_count .. self.literal_count + literals.len];        @memcpy(target, literals);        self.step_count += 1;        self.literal_count += literals.len;        return target;    }    /// Counts one empty step and returns an empty slice at the end of the packed literals. The    /// solver records the final empty clause of a refutation through it. The call asserts that the    /// trace is `.steady` and that a step is free.    pub fn acquireEmpty(self: *Trace) []Literal {        assert(self.phase == .steady);        assert(self.step_count < self.capacity.steps);        self.step_count += 1;        return self.slab[self.literal_count..self.literal_count];    }    /// Takes back the most recent step, which has to be `literals` and has to hold at least one    /// literal. The solver takes back the step of a learned clause that it then fails to store. The    /// call asserts that the trace is `.steady` and that `literals` ends where the packed literals    /// end.    pub fn releaseLast(self: *Trace, literals: []Literal) void {        assert(self.phase == .steady);        assert(literals.len >= 1);        assert(self.step_count > 0);        assert(self.literal_count >= literals.len);        assert(self.owns(literals));        const cursor = @intFromPtr(self.slab.ptr) + self.literal_count * @sizeOf(Literal);        assert(@intFromPtr(literals.ptr) + literals.len * @sizeOf(Literal) == cursor);        self.step_count -= 1;        self.literal_count -= literals.len;    }    /// Drops every step, so the next step starts at the beginning of the slab. The solver clears it    /// whenever it clears its proof trace: at the start of each solve and at each change that makes    /// the trace stale. Slices handed out before the call are overwritten by later steps. The call    /// asserts that the trace is `.steady`.    pub fn clear(self: *Trace) void {        assert(self.phase == .steady);        assert(self.literal_count <= self.capacity.literal_count);        self.step_count = 0;        self.literal_count = 0;    }    /// Frees the slab with `allocator` and leaves the trace undefined. The solver calls it when it    /// replaces its trace, when it drops the trace after the conflict limit is removed, and when it    /// is freed itself. `allocator` has to be the one given to `init`. Every slice the trace handed    /// out becomes invalid.    pub fn deinit(self: *Trace, allocator: Allocator) void {        assert(self.phase != .teardown);        self.phase = .teardown;        allocator.free(self.slab);        self.* = undefined;    }};comptime {    alloc_phase.capacity.requireAllocatorExactOwnerShape(Trace);}test "proof trace capacity matches an independent typed-byte model" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(Trace, "smt_trace_capacity_capacity_model"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(Trace, "smt_trace_capacity_overload"),            null,            null,            null,            null,            null,            null,        );    }    const limits = Trace.Limits.inspect(2_000, 96);    const capacity = try Trace.Capacity.derive(limits);    try std.testing.expectEqual(@as(usize, 2_002), capacity.steps);    try std.testing.expectEqual(@as(usize, 96), capacity.step_width);    try std.testing.expectEqual(@as(usize, 2_002 * 96), capacity.literal_count);    const zero_budget = try Trace.Capacity.derive(Trace.Limits.inspect(0, 8));    try std.testing.expectEqual(@as(usize, 2), zero_budget.steps);    try std.testing.expectEqual(@as(usize, 16), zero_budget.literal_count);    try std.testing.expectError(        error.CapacityOverflow,        Trace.Capacity.derive(Trace.Limits.inspect(std.math.maxInt(usize) - 1, 8)),    );    try std.testing.expectError(        error.CapacityOverflow,        Trace.Capacity.derive(Trace.Limits.inspect(8, std.math.maxInt(usize) - 1)),    );    try std.testing.expectError(        error.CapacityOverflow,        Trace.Capacity.derive(Trace.Limits.inspect(std.math.maxInt(u64) / 2, 4)),    );}fn checkTraceInitFailures(allocator: Allocator, limits: Trace.Limits) !void {    var owner = try Trace.init(allocator, limits);    defer owner.deinit(allocator);    try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, owner.phase);}test "proof trace init cleans every allocation failure and retries" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(Trace, "smt_trace_oom_retry"),            null,            null,            null,            null,            null,            null,        );    }    const limits = Trace.Limits.inspect(4, 6);    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkTraceInitFailures,        .{limits},    );    var owner = try Trace.init(std.testing.allocator, limits);    defer owner.deinit(std.testing.allocator);    owner.activate();    try std.testing.expectEqual(@as(usize, 6), owner.freeSteps());}test "proof trace fills to capacity with stable pointers and zero steady operations" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(Trace, "smt_trace_sealed_fill_overload"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(Trace, "smt_trace_sealed_fill_foreign_risk"),            null,            null,            null,            null,            null,            null,        );    }    var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});    var owner = try Trace.init(failing.allocator(), Trace.Limits.inspect(2, 3));    defer owner.deinit(failing.allocator());    owner.activate();    try std.testing.expectEqual(@as(usize, 4), owner.freeSteps());    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) };    const first = owner.acquire(pattern[0..3]);    const second = owner.acquire(pattern[0..2]);    const third = owner.acquire(pattern[0..1]);    const final = owner.acquireEmpty();    try std.testing.expectEqual(@as(usize, 0), owner.freeSteps());    try std.testing.expectEqual(@as(usize, 6), owner.literal_count);    try std.testing.expect(owner.owns(first));    try std.testing.expect(owner.owns(second));    try std.testing.expect(owner.owns(third));    try std.testing.expectEqual(@as(usize, 0), final.len);    try std.testing.expectEqual(first.ptr + 3, second.ptr);    try std.testing.expectEqual(@as(u32, Literal.positive(0).raw), second[0].raw);    owner.clear();    try std.testing.expectEqual(@as(usize, 4), owner.freeSteps());    const refilled = owner.acquire(pattern[0..2]);    try std.testing.expectEqual(first.ptr, refilled.ptr);    try std.testing.expectEqual(@as(usize, 2), owner.literal_count);    owner.releaseLast(refilled);    try std.testing.expectEqual(@as(usize, 4), owner.freeSteps());    try std.testing.expectEqual(@as(usize, 0), owner.literal_count);    try std.testing.expect(!failing.has_induced_failure);}

Audit

Definitions17
Public names33
Members10
Version26.7.0
Revisiondaab053ee433