Skip to main content

automation_structures/compositions/
signal.rs

1// AuditSink-backed Signal named composition.
2//
3// This is the executable construction recorded by
4// SignalFromAuditSink.tla:
5//
6//   current_value = last AuditSink operation, or initial_value
7//   pending(l)    = cursor(l) < audit length
8//   notified(l)   = audit length > 0 && cursor(l) == audit length
9//
10// AuditSink owns append capacity and chain state. CF-002 Cursor owns each
11// retained listener position. Signal adds only change detection and the fused
12// listener catch-up transition. It stores no parallel pending or notified set.
13
14use crate::connectives::cursor::Cursor;
15use crate::primitives::audit_sink::AuditSink;
16#[expect(
17    unused_imports,
18    reason = "ChainOperation is used by ghost specifications erased by rustc"
19)]
20use crate::primitives::audit_sink::ChainOperation;
21use vstd::prelude::*;
22
23verus! {
24
25/// Erased one-listener logical view used by fused Signal realizations.
26pub ghost struct SignalModel {
27    /// Current signal value.
28    pub current_value: u64,
29    /// Whether any actual change has been retained.
30    pub change_observed: bool,
31    /// Whether the selected listener trails the current change head.
32    pub pending: bool,
33    /// Whether the selected listener has observed the current change head.
34    pub notified: bool,
35}
36
37/// The one-listener projection preserves Signal's delivery-state invariants.
38pub open spec fn model_valid(model: SignalModel) -> bool {
39    &&& !(model.pending && model.notified)
40    &&& (!model.change_observed ==> !model.pending && !model.notified)
41    &&& (model.change_observed ==> model.pending || model.notified)
42}
43
44/// Initial Signal projection before an observed value change.
45pub open spec fn model_initial(model: SignalModel, initial_value: u64) -> bool {
46    &&& model_valid(model)
47    &&& model.current_value == initial_value
48    &&& !model.change_observed
49    &&& !model.pending
50    &&& !model.notified
51}
52
53/// One real value change creates exactly one pending notification.
54pub open spec fn model_set_value(
55    pre: SignalModel,
56    post: SignalModel,
57    value: u64,
58) -> bool {
59    &&& model_valid(pre)
60    &&& model_valid(post)
61    &&& value != pre.current_value
62    &&& post.current_value == value
63    &&& post.change_observed
64    &&& post.pending
65    &&& !post.notified
66}
67
68/// Delivery moves the one listener from pending to notified.
69pub open spec fn model_notify(pre: SignalModel, post: SignalModel) -> bool {
70    &&& model_valid(pre)
71    &&& model_valid(post)
72    &&& pre.pending
73    &&& post.current_value == pre.current_value
74    &&& post.change_observed == pre.change_observed
75    &&& !post.pending
76    &&& post.notified
77}
78
79/// A fused physical wake may perform Signal's change and delivery actions atomically.
80pub open spec fn fused_delivery(value: u64) -> bool {
81    value != 0
82}
83
84/// Every admitted fused wake has a witness through the two Signal actions.
85pub proof fn fused_delivery_has_action_witness(value: u64)
86    requires fused_delivery(value),
87    ensures exists|initial: SignalModel, pending: SignalModel, notified: SignalModel|
88        model_initial(initial, 0)
89            && model_set_value(initial, pending, value)
90            && model_notify(pending, notified),
91{
92    let initial = SignalModel {
93        current_value: 0,
94        change_observed: false,
95        pending: false,
96        notified: false,
97    };
98    let pending = SignalModel {
99        current_value: value,
100        change_observed: true,
101        pending: true,
102        notified: false,
103    };
104    let notified = SignalModel {
105        current_value: value,
106        change_observed: true,
107        pending: false,
108        notified: true,
109    };
110    assert(model_initial(initial, 0));
111    assert(model_set_value(initial, pending, value));
112    assert(model_notify(pending, notified));
113}
114
115/// A change-detecting Signal composed from AuditSink and per-listener Cursor.
116pub struct Signal {
117    /// Value used before the first retained change.
118    pub initial_value: u64,
119    /// Exclusive upper bound of the value domain.
120    pub num_values: u64,
121    /// Number of listener cursors.
122    pub num_listeners: usize,
123    /// Owner of retained value changes.
124    pub audit: AuditSink,
125    /// Per-listener progress owners.
126    pub cursors: Vec<Cursor>,
127}
128
129impl Signal {
130    /// The current value projected from the audit head or the initial value.
131    pub open spec fn current_value_spec(&self) -> u64 {
132        if self.audit.log@.len() == 0 {
133            self.initial_value
134        } else {
135            self.audit.log@[self.audit.log@.len() - 1].operation
136        }
137    }
138
139    /// Whether one listener trails the current audit head.
140    pub open spec fn pending_spec(&self, listener: int) -> bool {
141        0 <= listener < self.cursors@.len()
142            && self.cursors@[listener].position < self.audit.log@.len()
143    }
144
145    /// Whether one listener has caught up to a nonempty audit head.
146    pub open spec fn notified_spec(&self, listener: int) -> bool {
147        &&& 0 <= listener < self.cursors@.len()
148        &&& self.audit.log@.len() > 0
149        &&& self.cursors@[listener].position == self.audit.log@.len()
150    }
151
152    /// Maintained construction invariant.
153    pub open spec fn inv(&self) -> bool {
154        &&& self.num_values > 0
155        &&& self.initial_value < self.num_values
156        &&& self.num_listeners == self.cursors@.len()
157        &&& self.audit.inv()
158        &&& forall|i: int| 0 <= i < self.audit.log@.len()
159            ==> #[trigger] self.audit.log@[i].operation < self.num_values
160        &&& forall|i: int| 0 <= i < self.cursors@.len()
161            ==> #[trigger] self.cursors@[i].position <= self.audit.log@.len()
162    }
163
164    /// Construct an empty change log and one zero cursor per listener.
165    #[expect(clippy::arithmetic_side_effects, reason = "the loop invariant and guard prove the listener index increment remains in range")]
166    pub fn new(
167        initial_value: u64,
168        num_values: u64,
169        num_listeners: usize,
170        max_changes: usize,
171    ) -> (signal: Self)
172        requires
173            num_values > 0,
174            initial_value < num_values,
175        ensures
176            signal.inv(),
177            signal.initial_value == initial_value,
178            signal.num_values == num_values,
179            signal.num_listeners == num_listeners,
180            signal.audit.max_log_len == max_changes,
181            signal.audit.log@.len() == 0,
182            signal.current_value_spec() == initial_value,
183            forall|i: int| 0 <= i < signal.cursors@.len()
184                ==> #[trigger] signal.cursors@[i].position == 0,
185    {
186        let audit = AuditSink::new(max_changes);
187        let mut cursors: Vec<Cursor> = Vec::new();
188        let mut index: usize = 0;
189        while index < num_listeners
190            invariant
191                index <= num_listeners,
192                cursors@.len() == index,
193                forall|i: int| 0 <= i < cursors@.len()
194                    ==> #[trigger] cursors@[i].position == 0,
195            decreases num_listeners - index,
196        {
197            cursors.push(Cursor::new(0));
198            index += 1;
199        }
200        Self {
201            initial_value,
202            num_values,
203            num_listeners,
204            audit,
205            cursors,
206        }
207    }
208
209    /// Read the current value projection.
210    #[expect(clippy::indexing_slicing, reason = "the nonempty branch proves the audit index is in bounds")]
211    #[expect(clippy::arithmetic_side_effects, reason = "the nonempty branch proves the audit length can be decremented")]
212    pub fn current_value(&self) -> (value: u64)
213        requires self.inv(),
214        ensures
215            value == self.current_value_spec(),
216            value < self.num_values,
217    {
218        if self.audit.log.is_empty() {
219            self.initial_value
220        } else {
221            self.audit.log[self.audit.log.len() - 1].operation
222        }
223    }
224
225    /// Report whether at least one value change has been recorded.
226    pub fn has_changes(&self) -> (changed: bool)
227        requires self.inv(),
228        ensures changed == (self.audit.log@.len() > 0),
229    {
230        !self.audit.log.is_empty()
231    }
232
233    /// Report whether `set_value` is enabled for the supplied value.
234    pub fn can_set_value(&self, value: u64) -> (enabled: bool)
235        requires self.inv(),
236        ensures enabled == (value < self.num_values
237            && value != self.current_value_spec()
238            && self.audit.log@.len() < self.audit.max_log_len),
239    {
240        value < self.num_values
241            && value != self.current_value()
242            && self.audit.log.len() < self.audit.max_log_len
243    }
244
245    /// Read whether one listener is pending.
246    #[expect(clippy::indexing_slicing, reason = "the caller supplies an in-range listener")]
247    pub fn is_pending(&self, listener: usize) -> (pending: bool)
248        requires
249            self.inv(),
250            listener < self.cursors.len(),
251        ensures pending == self.pending_spec(listener as int),
252    {
253        self.cursors[listener].position < self.audit.log.len()
254    }
255
256    /// Read whether one listener has caught up to a nonempty head.
257    #[expect(clippy::indexing_slicing, reason = "the caller supplies an in-range listener")]
258    pub fn is_notified(&self, listener: usize) -> (notified: bool)
259        requires
260            self.inv(),
261            listener < self.cursors.len(),
262        ensures notified == self.notified_spec(listener as int),
263    {
264        !self.audit.log.is_empty()
265            && self.cursors[listener].position == self.audit.log.len()
266    }
267
268    /// Append one real value change through the AuditSink owner.
269    pub fn set_value(&mut self, value: u64)
270        requires
271            old(self).inv(),
272            value < old(self).num_values,
273            value != old(self).current_value_spec(),
274            old(self).audit.log@.len() < old(self).audit.max_log_len,
275        ensures
276            final(self).inv(),
277            final(self).initial_value == old(self).initial_value,
278            final(self).num_values == old(self).num_values,
279            final(self).num_listeners == old(self).num_listeners,
280            final(self).audit.operator == old(self).audit.operator,
281            final(self).audit.max_log_len == old(self).audit.max_log_len,
282            final(self).audit.log@.len() == old(self).audit.log@.len() + 1,
283            final(self).audit.last_hash
284                == old(self).audit.operator.combine_spec(old(self).audit.last_hash, value),
285            final(self).audit.log@[old(self).audit.log@.len() as int].operation == value,
286            final(self).audit.log@[old(self).audit.log@.len() as int].prev_hash
287                == old(self).audit.last_hash,
288            forall|index: int| 0 <= index < old(self).audit.log@.len() ==>
289                #[trigger] final(self).audit.log@[index] == old(self).audit.log@[index],
290            final(self).current_value_spec() == value,
291            final(self).cursors@ == old(self).cursors@,
292    {
293        let ghost prior_log = self.audit.log@;
294        let ghost prior_cursors = self.cursors@;
295        let _accepted = self.audit.record(value);
296        assert(_accepted);
297        assert(self.audit.log@.len() == prior_log.len() + 1);
298        assert(self.audit.log@[prior_log.len() as int].operation == value);
299        assert(self.current_value_spec() == value);
300        assert(self.cursors@ == prior_cursors);
301        assert forall|i: int| 0 <= i < self.audit.log@.len()
302            implies #[trigger] self.audit.log@[i].operation < self.num_values by {
303            if i < prior_log.len() {
304                assert(self.audit.log@[i] == prior_log[i]);
305            } else {
306                assert(i == prior_log.len());
307            }
308        }
309        assert forall|i: int| 0 <= i < self.cursors@.len()
310            implies #[trigger] self.cursors@[i].position <= self.audit.log@.len() by {
311            assert(self.cursors@[i].position <= prior_log.len());
312        }
313    }
314
315    /// Advance one pending listener exactly to the current audit head.
316    #[expect(clippy::indexing_slicing, reason = "the caller and invariant prove the listener index is in bounds")]
317    pub fn notify_listener(&mut self, listener: usize)
318        requires
319            old(self).inv(),
320            listener < old(self).cursors.len(),
321            old(self).cursors@[listener as int].position < old(self).audit.log@.len(),
322        ensures
323            final(self).inv(),
324            final(self).initial_value == old(self).initial_value,
325            final(self).num_values == old(self).num_values,
326            final(self).num_listeners == old(self).num_listeners,
327            final(self).audit == old(self).audit,
328            final(self).cursors@
329                == old(self).cursors@.update(
330                    listener as int,
331                    Cursor { position: old(self).audit.log@.len() as usize },
332                ),
333    {
334        let head = self.audit.log.len();
335        let ghost prior_cursors = self.cursors@;
336        let mut advanced = Cursor::new(self.cursors[listener].position);
337        advanced.advance_to(head);
338        self.cursors.set(listener, advanced);
339        assert(self.cursors@ == prior_cursors.update(listener as int, self.cursors@[listener as int]));
340        assert forall|i: int| 0 <= i < self.cursors@.len()
341            implies #[trigger] self.cursors@[i].position <= self.audit.log@.len() by {
342            if i == listener as int {
343                assert(self.cursors@[i].position == self.audit.log@.len());
344            } else {
345                assert(self.cursors@[i] == prior_cursors[i]);
346            }
347        }
348    }
349}
350
351}