Skip to main content

automation_structures/primitives/
budget.rs

1// Executable carrier for Budget.tla.
2//
3// Budget bounds a consumed resource by a structural ceiling. The TLA+ spec at
4// formal/structures/Budget/Budget.tla models the full reservation/eviction lifecycle with
5// four Nat-valued state variables — capacity, allocated, reserved,
6// pending_eviction — and six actions, and its .cfg checks two invariants:
7//
8//   TypeInvariant   == capacity, allocated, reserved, pending_eviction ∈ Nat
9//   SafetyInvariant == allocated + reserved + pending_eviction <= capacity
10//
11// This module discharges the full SafetyInvariant (all three claimants summed,
12// not the reserved=pending=0 restriction) across all six actions, faithful to
13// the TLA+ action structure:
14//
15//   TryAllocate / Reserve         — IF (used + amount <= capacity) THEN grow
16//                                   ELSE UNCHANGED. Modelled as bool-returning
17//                                   try-operations; the returned bool is exactly
18//                                   the TLA+ IF condition.
19//   CommitReservation / Release /
20//   MarkEviction / CompleteEviction — guarded conjunctions with an enabling
21//                                   condition (amount <= reserved / allocated /
22//                                   pending_eviction); modelled with that guard
23//                                   as a `requires`, so the action is callable
24//                                   exactly when the TLA+ action is enabled.
25//
26// TypeInvariant is realised at the type level: the four Nat-valued variables are
27// u64 fields, so "∈ Nat" holds by construction. SafetyInvariant is the maintained
28// proof obligation; the
29// spec arithmetic is lifted to `int` so the bound is stated without overflow
30// noise, and every executable sum is shown overflow-free from the invariant.
31
32use vstd::prelude::*;
33
34verus! {
35
36/// Reusable logical form of the Budget safety obligation.
37///
38/// Compositions use this predicate directly when a compact representation fuses the four Budget
39/// fields. The executable Budget carrier below is one realization of the same owner predicate.
40pub open spec fn budget_safety(
41    capacity: nat,
42    allocated: nat,
43    reserved: nat,
44    pending_eviction: nat,
45) -> bool {
46    allocated + reserved + pending_eviction <= capacity
47}
48
49/// A budget: a `capacity` ceiling against three claimants — `allocated`
50/// (committed), `reserved` (held but not committed), and `pending_eviction`
51/// (being reclaimed).
52#[derive(Clone, Copy)]
53pub struct Budget {
54    /// Fixed capacity ceiling.
55    pub capacity: u64,
56    /// Capacity committed to live allocation.
57    pub allocated: u64,
58    /// Capacity held for later commitment.
59    pub reserved: u64,
60    /// Allocated capacity marked for eviction completion.
61    pub pending_eviction: u64,
62}
63
64impl Budget {
65    // ── Specifications ──────────────────────────────────────────────────
66
67    /// Total claimed against the budget: allocated + reserved + pending_eviction.
68    /// Lifted to `int` so the sum is exact regardless of u64 range.
69    pub open spec fn used(&self) -> int {
70        self.allocated as int + self.reserved as int + self.pending_eviction as int
71    }
72
73    /// TLA+ `SafetyInvariant`.
74    pub open spec fn safety_invariant(&self) -> bool {
75        budget_safety(
76            self.capacity as nat,
77            self.allocated as nat,
78            self.reserved as nat,
79            self.pending_eviction as nat,
80        )
81    }
82
83    // ── Init (TLA+ Init) ────────────────────────────────────────────────
84
85    /// Construct an empty budget with the given capacity. Realises `Init`.
86    pub fn new(capacity: u64) -> (b: Budget)
87        ensures
88            b.capacity == capacity,
89            b.allocated == 0,
90            b.reserved == 0,
91            b.pending_eviction == 0,
92            b.safety_invariant(),
93    {
94        Budget { capacity, allocated: 0, reserved: 0, pending_eviction: 0 }
95    }
96
97    /// Headroom = capacity - used, computed overflow-safely (used <= capacity by
98    /// the invariant, so the subtraction never underflows). Exposed for callers
99    /// and the try-operations.
100    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves used is bounded by capacity")]
101    pub fn available(&self) -> (a: u64)
102        requires self.safety_invariant(),
103        ensures a as int == self.capacity as int - self.used(),
104    {
105        // allocated + reserved <= used <= capacity, so each partial sum fits u64.
106        assert(self.allocated + self.reserved <= self.capacity);
107        let used: u64 = self.allocated + self.reserved + self.pending_eviction;
108        self.capacity - used
109    }
110
111    // ── TryAllocate (TLA+ TryAllocate) ──────────────────────────────────
112
113    /// Try to commit `amount`: succeeds iff it fits under the ceiling. The
114    /// returned bool is exactly the TLA+ IF condition
115    /// `allocated + reserved + pending_eviction + amount <= capacity`.
116    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves the guarded addition is within capacity")]
117    pub fn try_allocate(&mut self, amount: u64) -> (ok: bool)
118        requires old(self).safety_invariant(),
119        ensures
120            final(self).capacity == old(self).capacity,
121            final(self).reserved == old(self).reserved,
122            final(self).pending_eviction == old(self).pending_eviction,
123            final(self).safety_invariant(),
124            ok == (old(self).used() + amount as int <= old(self).capacity as int),
125            ok ==> final(self).allocated == old(self).allocated + amount,
126            !ok ==> final(self).allocated == old(self).allocated,
127    {
128        let headroom = self.available();
129        if amount <= headroom {
130            self.allocated = self.allocated + amount;
131            true
132        } else {
133            false
134        }
135    }
136
137    // ── Reserve (TLA+ Reserve) ──────────────────────────────────────────
138
139    /// Try to reserve `amount` (held, not yet committed). Same ceiling test as
140    /// TryAllocate; on success grows `reserved`.
141    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves the guarded addition is within capacity")]
142    pub fn reserve(&mut self, amount: u64) -> (ok: bool)
143        requires old(self).safety_invariant(),
144        ensures
145            final(self).capacity == old(self).capacity,
146            final(self).allocated == old(self).allocated,
147            final(self).pending_eviction == old(self).pending_eviction,
148            final(self).safety_invariant(),
149            ok == (old(self).used() + amount as int <= old(self).capacity as int),
150            ok ==> final(self).reserved == old(self).reserved + amount,
151            !ok ==> final(self).reserved == old(self).reserved,
152    {
153        let headroom = self.available();
154        if amount <= headroom {
155            self.reserved = self.reserved + amount;
156            true
157        } else {
158            false
159        }
160    }
161
162    // ── CommitReservation (TLA+ CommitReservation) ──────────────────────
163
164    /// Commit `amount` of the reservation: moves it from `reserved` to
165    /// `allocated`. Enabling condition: amount <= reserved. The total `used` is
166    /// unchanged, so SafetyInvariant is trivially preserved.
167    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves the transfer operands are bounded")]
168    pub fn commit_reservation(&mut self, amount: u64)
169        requires
170            old(self).safety_invariant(),
171            amount <= old(self).reserved,
172        ensures
173            final(self).capacity == old(self).capacity,
174            final(self).pending_eviction == old(self).pending_eviction,
175            final(self).allocated == old(self).allocated + amount,
176            final(self).reserved == old(self).reserved - amount,
177            final(self).safety_invariant(),
178    {
179        // allocated + amount <= allocated + reserved <= used <= capacity.
180        assert(self.allocated + amount <= self.capacity);
181        self.allocated = self.allocated + amount;
182        self.reserved = self.reserved - amount;
183    }
184
185    // ── Release (TLA+ Release) ──────────────────────────────────────────
186
187    /// Release `amount` of committed allocation. Enabling condition:
188    /// amount <= allocated. Decreases `used`, so SafetyInvariant is preserved.
189    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves amount does not exceed allocated")]
190    pub fn release(&mut self, amount: u64)
191        requires
192            old(self).safety_invariant(),
193            amount <= old(self).allocated,
194        ensures
195            final(self).capacity == old(self).capacity,
196            final(self).reserved == old(self).reserved,
197            final(self).pending_eviction == old(self).pending_eviction,
198            final(self).allocated == old(self).allocated - amount,
199            final(self).safety_invariant(),
200    {
201        self.allocated = self.allocated - amount;
202    }
203
204    // ── MarkEviction (TLA+ MarkEviction) ────────────────────────────────
205
206    /// Mark `amount` of committed allocation for eviction: moves it from
207    /// `allocated` to `pending_eviction`. Enabling condition: amount <=
208    /// allocated. `used` is unchanged, so SafetyInvariant is preserved.
209    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves the allocation transfer is bounded")]
210    pub fn mark_eviction(&mut self, amount: u64)
211        requires
212            old(self).safety_invariant(),
213            amount <= old(self).allocated,
214        ensures
215            final(self).capacity == old(self).capacity,
216            final(self).reserved == old(self).reserved,
217            final(self).allocated == old(self).allocated - amount,
218            final(self).pending_eviction == old(self).pending_eviction + amount,
219            final(self).safety_invariant(),
220    {
221        // pending_eviction + amount <= pending_eviction + allocated <= used <= capacity.
222        assert(self.pending_eviction + amount <= self.capacity);
223        self.allocated = self.allocated - amount;
224        self.pending_eviction = self.pending_eviction + amount;
225    }
226
227    // ── CompleteEviction (TLA+ CompleteEviction) ────────────────────────
228
229    /// Complete eviction of `amount`: removes it from `pending_eviction`.
230    /// Enabling condition: amount <= pending_eviction. Decreases `used`, so
231    /// SafetyInvariant is preserved.
232    #[expect(clippy::arithmetic_side_effects, reason = "Verus proves amount does not exceed pending eviction")]
233    pub fn complete_eviction(&mut self, amount: u64)
234        requires
235            old(self).safety_invariant(),
236            amount <= old(self).pending_eviction,
237        ensures
238            final(self).capacity == old(self).capacity,
239            final(self).allocated == old(self).allocated,
240            final(self).reserved == old(self).reserved,
241            final(self).pending_eviction == old(self).pending_eviction - amount,
242            final(self).safety_invariant(),
243    {
244        self.pending_eviction = self.pending_eviction - amount;
245    }
246}
247
248}