Skip to main content

automation_structures/compositions/
allocation_snapshot.rs

1// Executable carrier for AllocationSnapshot.tla. The allocation decision has
2// three state variables: `accepted`, `total_cost`, and `budget_remaining`.
3// `accept_node` implements the guarded `AcceptNode(n, cost)` transition and
4// maintains:
5//
6//   TypeInvariant     == accepted ⊆ Nodes /\ total_cost ∈ Nat
7//                                          /\ budget_remaining ∈ Nat
8//   BudgetConsistency == total_cost + budget_remaining <= BudgetCapacity
9//
10// `new` implements `Init`. `capture` folds a request sequence through the same
11// guarded transition and returns a record with no exposed mutator.
12// Representation mapping:
13//   - Nodes is the index universe 0..num_nodes; `accepted ⊆ Nodes` becomes
14//     "every accepted id < num_nodes".
15//   - `accepted` is a TLA+ set variable; it is modelled as a Vec<u64> carrying
16//     a no-duplicates invariant, preserving the `n ∉ accepted` guard.
17//   - total_cost, budget_remaining ∈ Nat are carried by u64; the spec arithmetic
18//     is lifted to `int` to state BudgetConsistency without overflow noise.
19
20use vstd::prelude::*;
21
22verus! {
23
24/// Sum of the registered costs in the first `n` entries.
25pub open spec fn cost_sum_to(entries: Seq<(u64, u64)>, n: int) -> int
26    decreases n,
27{
28    if n <= 0 || n > entries.len() {
29        0
30    } else {
31        entries[n - 1].1 as int + cost_sum_to(entries, n - 1)
32    }
33}
34
35/// Appending one registry entry leaves every prior cost-sum prefix unchanged.
36proof fn cost_sum_push_prefix(entries: Seq<(u64, u64)>, entry: (u64, u64), n: int)
37    requires 0 <= n <= entries.len(),
38    ensures cost_sum_to(entries.push(entry), n) == cost_sum_to(entries, n),
39    decreases n,
40{
41    if n > 0 {
42        cost_sum_push_prefix(entries, entry, n - 1);
43        assert(entries.push(entry)[n - 1] == entries[n - 1]);
44    }
45}
46
47/// Appending one cost extends the registered cost sum by exactly that cost.
48pub proof fn cost_sum_push(entries: Seq<(u64, u64)>, key: u64, cost: u64)
49    ensures
50        cost_sum_to(entries.push((key, cost)), entries.len() as int + 1)
51            == cost_sum_to(entries, entries.len() as int) + cost as int,
52{
53    cost_sum_push_prefix(entries, (key, cost), entries.len() as int);
54    assert(entries.push((key, cost))[entries.len() as int].1 == cost);
55}
56
57/// An allocation snapshot: the accepted node set plus the running cost / budget
58/// figures, over a node universe `0..num_nodes` bounded by `capacity`.
59pub struct AllocationSnapshot {
60    /// |Nodes|: the node universe is the index range `0..num_nodes`.
61    pub num_nodes: u64,
62    /// ResourceRegistry component mapping accepted nodes to their costs.
63    pub registry: crate::primitives::resource_registry::ResourceRegistry<u64, u64>,
64    /// Budget component charged by the registered costs.
65    pub budget: crate::primitives::budget::Budget,
66}
67
68impl AllocationSnapshot {
69    // ── Specifications ──────────────────────────────────────────────────
70
71    /// `accepted ⊆ Nodes`: every accepted id is a valid node index.
72    pub open spec fn accepted_subset_nodes(&self) -> bool {
73        forall|i: int|
74            0 <= i < self.registry.entries.len()
75                ==> #[trigger] self.registry.entries@[i].0 < self.num_nodes
76    }
77
78    /// `accepted` is a set: no duplicate node ids. This encodes the TLA+ set
79    /// variable and makes the AcceptNode `n ∉ accepted` guard enforceable.
80    pub open spec fn accepted_distinct(&self) -> bool {
81        self.registry.unique_mapping()
82    }
83
84    /// Every ResourceRegistry value is an admitted positive node cost.
85    pub open spec fn costs_valid(&self) -> bool {
86        forall|i: int| 0 <= i < self.registry.entries.len()
87            ==> #[trigger] self.registry.entries@[i].1 > 0
88    }
89
90    /// TLA+ `TypeInvariant` (the structural clauses; the Nat clauses are carried
91    /// by the u64 typing of total_cost / budget_remaining).
92    pub open spec fn type_invariant(&self) -> bool {
93        self.accepted_subset_nodes() && self.accepted_distinct() && self.costs_valid()
94    }
95
96    /// TLA+ `BudgetConsistency`.
97    pub open spec fn budget_consistency(&self) -> bool {
98        &&& self.budget.safety_invariant()
99        &&& self.budget.reserved == 0
100        &&& self.budget.pending_eviction == 0
101        &&& self.budget.allocated as int
102            == cost_sum_to(self.registry.entries@, self.registry.entries.len() as int)
103    }
104
105    /// `n ∈ accepted`.
106    pub open spec fn contains(&self, n: u64) -> bool {
107        self.registry.contains_key(n)
108    }
109
110    // ── Init (TLA+ Init) ────────────────────────────────────────────────
111
112    /// Construct the empty snapshot: nothing accepted, full budget remaining.
113    /// Realises the TLA+ `Init` predicate and establishes both invariants.
114    pub fn new(capacity: u64, num_nodes: u64) -> (s: AllocationSnapshot)
115        ensures
116            s.num_nodes == num_nodes,
117            s.registry.entries@.len() == 0,
118            s.budget.capacity == capacity,
119            s.budget.allocated == 0,
120            s.type_invariant(),
121            s.budget_consistency(),
122    {
123        AllocationSnapshot {
124            num_nodes,
125            registry: crate::primitives::resource_registry::ResourceRegistry::new(),
126            budget: crate::primitives::budget::Budget::new(capacity),
127        }
128    }
129
130    // ── Membership (executable) ─────────────────────────────────────────
131
132    /// Executable membership test; links to the `contains` spec so callers can
133    /// discharge the `n ∉ accepted` precondition of `accept_node`.
134    pub fn contains_exec(&self, n: u64) -> (b: bool)
135        requires self.registry.unique_mapping(),
136        ensures
137            b == self.contains(n),
138    {
139        match self.registry.lookup(n) {
140            Some(_) => true,
141            None => false,
142        }
143    }
144
145    // ── AcceptNode (TLA+ AcceptNode) ────────────────────────────────────
146
147    /// Accept node `n` at cost `node_cost`. Realises the TLA+ `AcceptNode`
148    /// action: its three guards are `requires`, and both invariants are
149    /// re-established as `ensures` (the inductive preservation step).
150    pub fn accept_node(&mut self, n: u64, node_cost: u64)
151        requires
152            old(self).type_invariant(),
153            old(self).budget_consistency(),
154            n < old(self).num_nodes,                  // n ∈ Nodes
155            !old(self).contains(n),                   // n ∉ accepted
156            1 <= node_cost,                           // c is positive
157            old(self).budget.used() + node_cost as int <= old(self).budget.capacity as int,
158        ensures
159            final(self).num_nodes == old(self).num_nodes,
160            final(self).registry.entries@
161                == old(self).registry.entries@.push((n, node_cost)),
162            final(self).budget.capacity == old(self).budget.capacity,
163            final(self).budget.allocated == old(self).budget.allocated + node_cost,
164            final(self).contains(n),
165            final(self).type_invariant(),
166            final(self).budget_consistency(),
167    {
168        let _accepted = self.budget.try_allocate(node_cost);
169        assert(_accepted);
170        let ghost prior_entries = self.registry.entries@;
171        self.registry.register(n, node_cost);
172        proof { cost_sum_push(prior_entries, n, node_cost); }
173        // Re-establish the set invariant: the pushed element n was absent
174        // (precondition) and is a valid node, so distinctness and the subset
175        // bound both carry to the extended sequence.
176        assert(self.contains(n)) by {
177            assert(self.registry.maps_to(n, node_cost));
178        }
179    }
180}
181
182// ── capture (fold a whole acceptance sequence into a snapshot) ───────────
183
184/// Build a finished snapshot by folding a sequence of (node, cost) requests
185/// through the guarded `AcceptNode` action: a request is accepted iff it is a
186/// fresh, valid node whose cost fits the remaining budget (exactly the TLA+
187/// guards), otherwise it is skipped. The returned snapshot is immutable and
188/// satisfies both invariants. `nodes[i]` is paired with `costs[i]`.
189pub fn capture(capacity: u64, num_nodes: u64, nodes: &[u64], costs: &[u64])
190    -> (s: AllocationSnapshot)
191    requires
192        nodes@.len() == costs@.len(),
193    ensures
194        s.budget.capacity == capacity,
195        s.num_nodes == num_nodes,
196        s.type_invariant(),
197        s.budget_consistency(),
198{
199    let mut s = AllocationSnapshot::new(capacity, num_nodes);
200    let n_reqs = nodes.len();
201    let mut i: usize = 0;
202    while i < n_reqs
203        invariant
204            i <= n_reqs,
205            n_reqs == nodes@.len(),
206            nodes@.len() == costs@.len(),
207            s.budget.capacity == capacity,
208            s.num_nodes == num_nodes,
209            s.type_invariant(),
210            s.budget_consistency(),
211        decreases n_reqs - i,
212    {
213        let n = nodes[i];
214        let c = costs[i];
215        let available = s.budget.available();
216        if n < num_nodes && 1 <= c && c <= available && !s.contains_exec(n) {
217            s.accept_node(n, c);
218        }
219        i = i + 1;
220    }
221    s
222}
223
224}