automation_structures/compositions/
signal.rs1use 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
25pub ghost struct SignalModel {
27 pub current_value: u64,
29 pub change_observed: bool,
31 pub pending: bool,
33 pub notified: bool,
35}
36
37pub 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
44pub 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
53pub 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
68pub 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
79pub open spec fn fused_delivery(value: u64) -> bool {
81 value != 0
82}
83
84pub 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
115pub struct Signal {
117 pub initial_value: u64,
119 pub num_values: u64,
121 pub num_listeners: usize,
123 pub audit: AuditSink,
125 pub cursors: Vec<Cursor>,
127}
128
129impl Signal {
130 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 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 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 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 #[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 #[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 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 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 #[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 #[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 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 #[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}