tiny.smt.ConflictScratch
Defined in sat.scratch.
Scratch memory for one conflict analysis at a time, in storage its caller hands over: a buffer of up to one literal per variable and one mark bit per variable.
API (35)
Actions
Public operations.
Capacity.derive: Returns the sizes forlimits.Limits.inspect: Returns the limits forvariables.Loan.append: Addsliteralafter the loan's earlier literals.Loan.clearSeen: Removes the mark fromvariable_index.Loan.isSeen: Returns true whenvariable_indexis marked.Loan.literals: Returns the literals added sinceacquire, in order.Loan.markSeen: Marksvariable_indexas met and returns true, or returns false when it was already marked.Loan.recordResolution: Counts one resolution step.acquire: Clears the marks and fillsloanas the one loan out, coveringvariablesvariables.activate: Moves the scratch from.initializationto.steady, after whichacquirecan hand out loans.admits: Returns true when the scratch covers at leastrequired.variablesvariables.deinit: Moves the scratch to.teardown, returns the storage passed toinit, and leaves the scratch undefined.init: Returns a scratch in.initializationoverstorage, sized forlimits.release: Ends the current loan, soacquirecan hand out the next one.
Types and contracts
Public types and contracts.
Capacity: The sizes of one scratch and of the trail allocation that holds it, in bytes and in literals.Capacity.DeriveError: The error ofderive:error.CapacityOverflow.Exhaustion: The errors thatacquire,releaseand the loan functions return.InitError: The errors ofinit:error.CapacityOverflowfromCapacity.derive, anderror.StorageLengthMismatchwhen the storage length differs fromCapacity.storage_bytes.Limits: The input that sizes a scratch: its variable count.Loan: A handle to the scratch memory for one conflict analysis, which reaches the scratch's literal buffer and marks and holds the scratch's address and the loan's number.Storage: The byte slice a scratch takes from its caller, aligned for literals.claim: A compile-time record of the scratch's memory: what the storage covers, what stays outside it, the equation that sizes it fromLimits, the refusal behavior, the work bound, and the tests that witness each obligation.storage_alignment: The alignment the caller's storage needs, equal to the alignment of a literal.work_limits: Work bounds that the shape check reads: any number of setup steps, and zero cleanup steps per call.
Fields and members
Public fields and members.
capacityliteralsloan_epochloan_identityloan_literal_countloan_resolution_stepsloan_variablesloanedphaseseenstorage
Source
Source: lib/smt/src/sat/scratch.zig:39
zig
/// Scratch memory for one conflict analysis at a time, in storage its caller hands over: a buffer/// of up to one literal per variable and one mark bit per variable. The solver builds one over the/// tail of its trail allocation at the start of each solve and takes a loan from it for each/// conflict it analyzes. `init` takes the storage, `activate` moves the scratch from/// `.initialization` to `.steady`, and `deinit` moves it to `.teardown` and returns the storage./// The scratch calls no allocator. One loan is out at a time, and every loan function refuses a/// released or copied loan.pub const ConflictScratch = struct { /// The errors that `acquire`, `release` and the loan functions return. The solver's conflict /// analysis passes them up with `try`, so they belong to the errors a solve can return. Each /// error is returned before any change to the scratch. pub const Exhaustion = error{ /// `acquire` was called while a loan is out. ConflictScratchInUse, /// `acquire` asked for more variables than the scratch covers, or `Loan.append` or /// `Loan.recordResolution` went past the loan's variable count. ConflictScratchCapacityExceeded, /// `acquire` found the loan counter at the largest `u64`. ConflictScratchLoanEpochExhausted, /// A loan function or `release` got a loan that is released, a copy at another address, /// from a scratch outside `.steady`, or from another scratch. ConflictScratchStaleLoan, }; /// A compile-time record of the scratch's memory: what the storage covers, what stays outside /// it, the equation that sizes it from `Limits`, the refusal behavior, the work bound, and the /// tests that witness each obligation. The shape check at the end of the file validates the /// record and the scratch's shape at compile time. The equation gives the variable count times /// the size of a literal, plus the variable count divided by 8 and rounded up, in bytes. The /// record states that short storage, oversize use, an exhausted loan counter, copied loans and /// overlapping loans are refused before the caller's storage or the scratch changes. The record /// states the work bound: taking a loan clears at most one byte per 8 variables, and an /// analysis takes at most one resolution step per variable. The learned clauses the solver /// keeps, the trail's own entries, the clauses, the watch lists, the search state and the proof /// steps stay outside the storage. pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "smt.conflict_scratch", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "conflict_analysis_learned_literal_scratch", .lifetime = .transferred, .detail = "one learned literal per admitted solver variable in the exact trail-tail storage transferred by the caller", }, .{ .id = "conflict_analysis_seen_variable_scratch", .lifetime = .transferred, .detail = "one dense seen bit per admitted solver variable in the exact trail-tail storage transferred by the caller", }, }, .excluded = &.{ "retained learned-clause literals copied by addLearnedClause", "the primary trail prefix, rounded tail padding, original clauses, clause headers, watch lists, and search state", "proof trace steps and reverse-unit-propagation assignment state", "old plus new trail backing overlap while variable growth establishes a larger admitted solve workspace", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "variables", "variables"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(Literal, "literal"), }, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .constant = 8 }, .{ .ceiling_division = .{ .left = 0, .right = 2 } }, .{ .add = .{ .left = 1, .right = 3 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 4, }}, }, .overload = .{ .kind = .reject_before_mutation, .detail = "short storage, oversize use, epoch exhaustion, copied tokens, and overlapping address-bound affine loans fail before caller storage or ownership state changes", }, .risks = .{ .transitive = .{ .status = .open, .detail = "learnFromConflict uses one address-bound epoch loan within stable token-storage and owner lifetimes and bounded SAT properties check status, proof, core, and learned-clause semantics; retained learned-clause copies remain a separate owner", }, .foreign = .{ .status = .excluded, .detail = "the exact transferred trail tail is process-local memory and conflict analysis crosses no operating-system or foreign-runtime edge", }, }, .work = .{ .equation = "initialization <= 1, acquire clearing <= ceil(variables / 8), and resolution steps <= variables", }, .obligations = &.{ .{ .key = "smt_conflict_scratch_capacity", .role = .capacity_model }, .{ .key = "smt_conflict_scratch_acquisition", .role = .custom }, .{ .key = "smt_conflict_scratch_storage_identity", .role = .foreign_risk }, .{ .key = "smt_conflict_scratch_overload", .role = .overload }, .{ .key = "smt_conflict_scratch_reuse", .role = .overload }, .{ .key = "smt_conflict_scratch_work_bound", .role = .work_bound }, .{ .key = "smt_conflict_scratch_solver_admission", .role = .overload }, .{ .key = "smt_conflict_scratch_solver_semantics", .role = .transitive_risk }, .{ .key = "smt_conflict_scratch_foreign_risk", .role = .foreign_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, }, }, }, }; /// The alignment the caller's storage needs, equal to the alignment of a literal. A caller /// aligns a buffer of its own to this value before it hands the buffer to `init`. pub const storage_alignment: usize = @alignOf(Literal); /// The byte slice a scratch takes from its caller, aligned for literals. A caller passes one to /// `init` and gets the same one back from `deinit`. pub const Storage = []align(storage_alignment) u8; /// The errors of `init`: `error.CapacityOverflow` from `Capacity.derive`, and /// `error.StorageLengthMismatch` when the storage length differs from `Capacity.storage_bytes`. /// The solver passes them up from the start of a solve. pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch}; /// Work bounds that the shape check reads: any number of setup steps, and zero cleanup steps /// per call. The shape check at the end of the file requires it beside the record in `claim`. pub const work_limits: alloc_phase.capacity.WorkLimits = .{ .transition_steps_max = std.math.maxInt(usize), .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; phase: alloc_phase.capacity.Phase, /// The sizes `Capacity.derive` gave for the limits passed to `init`. `admits` and `acquire` /// compare with its variable count, and the solver reads it to find the storage again at each /// analysis. capacity: Capacity, /// The storage passed to `init`, which `deinit` returns. The literal buffer starts at its first /// byte, and the mark bits start at `Capacity.seen_offset`. The solver checks at each analysis /// that it still matches the tail of the trail allocation. storage: Storage, literals: []Literal, seen: []u8, loaned: bool = false, loan_epoch: u64 = 0, loan_identity: usize = 0, loan_variables: usize = 0, loan_literal_count: usize = 0, loan_resolution_steps: usize = 0, /// The input that sizes a scratch: its variable count. The solver builds one from the variable /// count a solve needs, counting every variable its assumptions name. pub const Limits = struct { /// The number of variables the scratch covers. variables: usize, /// Returns the limits for `variables`. The solver builds its scratch limits through it. pub fn inspect(variables: usize) Limits { return .{ .variables = variables }; } }; /// The sizes of one scratch and of the trail allocation that holds it, in bytes and in /// literals. The solver derives it at the start of each solve to size its trail allocation and /// to find the scratch storage inside it. pub const Capacity = struct { /// The variable count from the limits, which is the most literals and marks a loan uses. variables: usize, /// Bytes of the literal buffer: the variable count times the size of a literal. literal_bytes: usize, /// The byte offset of the mark bits in the storage, equal to `literal_bytes`. seen_offset: usize, /// Bytes of mark bits: the variable count divided by 8, rounded up. seen_bytes: usize, /// Bytes of storage: `literal_bytes` plus `seen_bytes`. `init` refuses storage of any other /// length. storage_bytes: usize, /// The storage size in literals, rounded up, which is the number of trail entries the /// solver sets aside for the scratch. backing_literals: usize, /// The trail allocation the solver needs, in literals: the variable count plus /// `backing_literals`. trail_capacity: usize, /// The error of `derive`: `error.CapacityOverflow`. `init` and the solver pass it up before /// anything is allocated. pub const DeriveError = error{CapacityOverflow}; /// Returns the sizes for `limits`. The solver sizes its trail allocation from it before a /// solve, and `init` checks the storage length against it. The call returns /// `error.CapacityOverflow` when the literal bytes, the storage bytes or the trail /// allocation overflows `usize`. pub fn derive(limits: Limits) DeriveError!Capacity { const literal_bytes = std.math.mul( usize, limits.variables, @sizeOf(Literal), ) catch return error.CapacityOverflow; const seen_bytes = limits.variables / 8 + @intFromBool(limits.variables % 8 != 0); const storage_bytes = std.math.add( usize, literal_bytes, seen_bytes, ) catch return error.CapacityOverflow; const backing_literals = storage_bytes / @sizeOf(Literal) + @intFromBool(storage_bytes % @sizeOf(Literal) != 0); const trail_capacity = std.math.add( usize, limits.variables, backing_literals, ) catch return error.CapacityOverflow; assert(literal_bytes % @alignOf(Literal) == 0); return .{ .variables = limits.variables, .literal_bytes = literal_bytes, .seen_offset = literal_bytes, .seen_bytes = seen_bytes, .storage_bytes = storage_bytes, .backing_literals = backing_literals, .trail_capacity = trail_capacity, }; } }; /// A handle to the scratch memory for one conflict analysis, which reaches the scratch's /// literal buffer and marks and holds the scratch's address and the loan's number. The solver's /// conflict analysis takes one per conflict with `acquire`, marks the variables it meets, /// collects the learned clause's literals, and hands the loan back with `release`. Every loan /// function checks the loan first and returns `error.ConflictScratchStaleLoan` when the loan /// was released, when a later `acquire` replaced it, when it is a copy at another address, or /// when the scratch is outside `.steady`. The loan stays valid only at the address `acquire` /// filled, so the caller keeps it in place until `release`. pub const Loan = struct { const BitPosition = struct { byte: usize, mask: u8, }; owner: *ConflictScratch, epoch: u64, /// Marks `variable_index` as met and returns true, or returns false when it was already /// marked. The analysis calls it on each literal of a clause it resolves on, and a false /// result skips a variable it has already counted. The call asserts that `variable_index` /// is below the loan's variable count. The call returns `error.ConflictScratchStaleLoan` /// for a stale loan. pub fn markSeen(self: *const Loan, variable_index: u32) Exhaustion!bool { const owner = try self.ownerFor(); assert(variable_index < owner.loan_variables); const bit = bitPosition(variable_index); if (owner.seen[bit.byte] & bit.mask != 0) return false; owner.seen[bit.byte] |= bit.mask; return true; } /// Removes the mark from `variable_index`. The analysis unmarks the variable it resolves on /// at each step. The call asserts that `variable_index` is below the loan's variable count /// and is marked. The call returns `error.ConflictScratchStaleLoan` for a stale loan. pub fn clearSeen(self: *const Loan, variable_index: u32) Exhaustion!void { const owner = try self.ownerFor(); assert(variable_index < owner.loan_variables); const bit = bitPosition(variable_index); assert(owner.seen[bit.byte] & bit.mask != 0); owner.seen[bit.byte] &= ~bit.mask; } /// Returns true when `variable_index` is marked. The analysis walks the trail backward and /// stops at the first marked variable. The call asserts that `variable_index` is below the /// loan's variable count. The call returns `error.ConflictScratchStaleLoan` for a stale /// loan. pub fn isSeen(self: *const Loan, variable_index: u32) Exhaustion!bool { const owner = try self.ownerFor(); assert(variable_index < owner.loan_variables); const bit = bitPosition(variable_index); return owner.seen[bit.byte] & bit.mask != 0; } /// Adds `literal` after the loan's earlier literals. The analysis adds each literal of the /// learned clause through it. The call returns `error.ConflictScratchCapacityExceeded` when /// the loan already holds its variable count of literals, and /// `error.ConflictScratchStaleLoan` for a stale loan, before any change. pub fn append(self: *const Loan, literal: Literal) Exhaustion!void { const owner = try self.ownerFor(); if (owner.loan_literal_count == owner.loan_variables) { return error.ConflictScratchCapacityExceeded; } owner.literals[owner.loan_literal_count] = literal; owner.loan_literal_count += 1; } /// Counts one resolution step. The analysis calls it at the start of each resolution step. /// The call returns `error.ConflictScratchCapacityExceeded` when the loan has already /// counted its variable count of steps, and `error.ConflictScratchStaleLoan` for a stale /// loan, before counting. pub fn recordResolution(self: *const Loan) Exhaustion!void { const owner = try self.ownerFor(); if (owner.loan_resolution_steps == owner.loan_variables) { return error.ConflictScratchCapacityExceeded; } owner.loan_resolution_steps += 1; } /// Returns the literals added since `acquire`, in order. The analysis reads the learned /// clause through it when the resolution ends. The slice points into the scratch storage, /// so literals added under a later loan overwrite it. The call returns /// `error.ConflictScratchStaleLoan` for a stale loan. pub fn literals(self: *const Loan) Exhaustion![]const Literal { const owner = try self.ownerFor(); assert(owner.loan_literal_count <= owner.loan_variables); return owner.literals[0..owner.loan_literal_count]; } fn ownerFor(self: *const Loan) Exhaustion!*ConflictScratch { const owner = self.owner; if (owner.phase != .steady or !owner.loaned or owner.loan_epoch != self.epoch or owner.loan_identity != @intFromPtr(self)) { return error.ConflictScratchStaleLoan; } return owner; } fn bitPosition(variable_index: u32) BitPosition { return .{ .byte = variable_index / 8, .mask = @as(u8, 1) << @intCast(variable_index % 8), }; } }; /// Returns a scratch in `.initialization` over `storage`, sized for `limits`. The solver builds /// one over the tail of its trail allocation at the start of each solve that has at least one /// variable. The call returns `error.CapacityOverflow` from `Capacity.derive`, and /// `error.StorageLengthMismatch` when `storage` is shorter or longer than /// `Capacity.storage_bytes`. On either error, `storage` stays unchanged and stays with the /// caller. The scratch holds `storage` until `deinit` returns it. pub fn init(storage: Storage, limits: Limits) InitError!ConflictScratch { const capacity = try Capacity.derive(limits); if (storage.len != capacity.storage_bytes) { return error.StorageLengthMismatch; } const owned = storage; const literals = typedSlice(Literal, owned, 0, capacity.variables); const seen = owned[capacity.seen_offset..][0..capacity.seen_bytes]; assert(literals.len * @sizeOf(Literal) == capacity.literal_bytes); assert(seen.len == capacity.seen_bytes); return .{ .phase = .initialization, .capacity = capacity, .storage = owned, .literals = literals, .seen = seen, }; } /// Moves the scratch from `.initialization` to `.steady`, after which `acquire` can hand out /// loans. The solver calls it once, right after `init`. The call asserts that the scratch is as /// `init` left it. pub fn activate(self: *ConflictScratch) void { assert(self.phase == .initialization); assert(self.storage.len == self.capacity.storage_bytes); assert(!self.loaned); assert(self.loan_identity == 0); assert(self.loan_variables == 0); assert(self.loan_literal_count == 0); assert(self.loan_resolution_steps == 0); self.phase = .steady; } /// Returns true when the scratch covers at least `required.variables` variables. A caller that /// holds a scratch checks with it whether the scratch covers a variable count before reusing /// the scratch for that count. The solver builds a new scratch at each solve, so the package /// itself makes no call to it. The call asserts that the scratch is `.steady`. pub fn admits(self: *const ConflictScratch, required: Capacity) bool { assert(self.phase == .steady); return self.capacity.variables >= required.variables; } /// Clears the marks and fills `loan` as the one loan out, covering `variables` variables. The /// analysis takes a loan with it at the start of each conflict it analyzes. The call returns /// `error.ConflictScratchInUse` while another loan is out, /// `error.ConflictScratchCapacityExceeded` when `variables` passes the scratch's variable /// count, and `error.ConflictScratchLoanEpochExhausted` when the loan counter is at the largest /// `u64`. Each error leaves the scratch and its marks as they were. The call clears every mark /// byte, at most one byte per 8 variables of the scratch. The call records the address of /// `loan`, so the caller keeps `loan` in place until `release`. The call asserts that the /// scratch is `.steady`. pub fn acquire( self: *ConflictScratch, variables: usize, loan: *Loan, ) Exhaustion!void { assert(self.phase == .steady); if (self.loaned) return error.ConflictScratchInUse; if (variables > self.capacity.variables) { return error.ConflictScratchCapacityExceeded; } const epoch = std.math.add(u64, self.loan_epoch, 1) catch { return error.ConflictScratchLoanEpochExhausted; }; @memset(self.seen, 0); loan.* = .{ .owner = self, .epoch = epoch, }; self.loaned = true; self.loan_epoch = epoch; self.loan_identity = @intFromPtr(loan); self.loan_variables = variables; self.loan_literal_count = 0; self.loan_resolution_steps = 0; } /// Ends the current loan, so `acquire` can hand out the next one. The analysis releases its /// loan when it returns, whether it learned a clause or failed. The call returns /// `error.ConflictScratchStaleLoan` when `loan` is stale or comes from another scratch, and /// then the current loan stays out. Every loan function refuses `loan` after the call. The call /// asserts that the scratch is `.steady`. pub fn release(self: *ConflictScratch, loan: *const Loan) Exhaustion!void { assert(self.phase == .steady); const owner = try loan.ownerFor(); if (owner != self) return error.ConflictScratchStaleLoan; assert(self.loan_literal_count <= self.loan_variables); assert(self.loan_resolution_steps <= self.loan_variables); self.loaned = false; self.loan_identity = 0; self.loan_variables = 0; self.loan_literal_count = 0; self.loan_resolution_steps = 0; } /// Moves the scratch to `.teardown`, returns the storage passed to `init`, and leaves the /// scratch undefined. The solver calls it when a solve ends and checks that the storage came /// back with the same address and length. The call asserts that no loan is out. The call works /// from `.initialization` or `.steady`. pub fn deinit(self: *ConflictScratch) Storage { assert(self.phase != .teardown); assert(!self.loaned); assert(self.loan_identity == 0); assert(self.loan_variables == 0); assert(self.loan_literal_count == 0); assert(self.loan_resolution_steps == 0); assert(self.storage.len == self.capacity.storage_bytes); self.phase = .teardown; const storage = self.storage; self.* = undefined; return storage; }};Source: lib/smt/src/root.zig:104
zig
pub const ConflictScratch = sat.ConflictScratch;Also reachable as
Audit
| Definitions | 25 |
|---|---|
| Public names | 75 |
| Members | 26 |
| Version | 26.7.0 |
| Revision | daab053ee433 |