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.
Trace.Capacity.derive: Returns the sizes a trace needs forlimits.Trace.Limits.inspect: Returns the limits forconflictsandvariables.Trace.acquire: Copiesliteralsafter the last step, counts one step, and returns the copy.Trace.acquireEmpty: Counts one empty step and returns an empty slice at the end of the packed literals.Trace.activate: Moves the trace from.initializationto.steady, after which the trace calls no allocator untildeinit.Trace.admits: Returns true when the trace's step count, step width and literal count are each at least those ofrequired.Trace.clear: Drops every step, so the next step starts at the beginning of the slab.Trace.deinit: Frees the slab withallocatorand leaves the trace undefined.Trace.freeSteps: Returns the number of steps still free since the lastclear.Trace.init: Returns a trace in.initializationwhose slab comes fromallocator, with every step free.Trace.owns: Returns true whenliteralsstarts inside the slab.Trace.releaseLast: Takes back the most recent step, which has to beliteralsand has to hold at least one literal.
Types and contracts
Public types and contracts.
Trace: 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.Trace.Capacity: The sizes of one trace: its step count, its step width in literals and its total literal count.Trace.Limits: The inputs that size a trace: the solve's conflict limit and the solver's variable count.
Values and defaults
Public values and defaults.
Trace.claim: A compile-time record of the trace's memory: what the slab covers, what stays outside it, the equation that sizes it fromLimits, the overload behavior, and the tests that witness each obligation.
Source
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
| Definitions | 17 |
|---|---|
| Public names | 33 |
| Members | 10 |
| Version | 26.7.0 |
| Revision | daab053ee433 |