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}