Skip to main content

automation_structures/
execution_api.rs

1//! Checked public entry points for reusable execution modalities.
2
3use crate::modalities::fork_join::ForkJoin as ForkJoinCarrier;
4use crate::modalities::sequential::Sequential as SequentialCarrier;
5use crate::modalities::step_graph::StepGraph as StepGraphCarrier;
6use crate::modalities::stream_graph::StreamGraph as StreamGraphCarrier;
7use vstd::prelude::*;
8
9verus! {
10
11/// Invalid sequential-execution configuration.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum SequentialBuildError {
15    /// Sequential execution requires at least one step.
16    NoSteps,
17    /// The value domain must contain at least one value.
18    EmptyValueDomain,
19    /// The initial value is outside the configured domain.
20    InitialValueOutOfRange,
21}
22
23/// A totally ordered, finite-step execution modality.
24pub struct Sequential {
25    inner: SequentialCarrier,
26}
27
28impl Sequential {
29    #[verifier::type_invariant]
30    closed spec fn well_formed(&self) -> bool { self.inner.inv() }
31
32    /// Validate and construct an inactive sequential execution.
33    pub fn new(steps: usize, value_domain_size: u64, initial_value: u64)
34        -> (result: Result<Self, SequentialBuildError>) {
35        if steps == 0 { return Err(SequentialBuildError::NoSteps); }
36        if value_domain_size == 0 { return Err(SequentialBuildError::EmptyValueDomain); }
37        if initial_value >= value_domain_size {
38            return Err(SequentialBuildError::InitialValueOutOfRange);
39        }
40        Ok(Self { inner: SequentialCarrier::new(steps, value_domain_size, initial_value) })
41    }
42
43    /// Total number of steps.
44    pub fn steps(&self) -> usize { self.inner.steps }
45
46    /// Number of completed steps.
47    pub fn completed(&self) -> usize { self.inner.pc }
48
49    /// Current carried value.
50    pub fn value(&self) -> u64 { self.inner.value }
51
52    /// Whether a step is active.
53    pub fn is_active(&self) -> bool { self.inner.active }
54
55    /// Whether all steps are complete and inactive.
56    pub fn is_done(&self) -> bool { self.inner.pc == self.inner.steps && !self.inner.active }
57
58    /// Read a completed-step value by execution order.
59    #[expect(clippy::indexing_slicing, reason = "the branch proves the history index is in bounds")]
60    pub fn history(&self, index: usize) -> Option<u64> {
61        if index < self.inner.history.len() { Some(self.inner.history[index]) } else { None }
62    }
63
64    /// Begin the next step if execution is inactive and incomplete.
65    #[must_use]
66    pub fn begin_step(&mut self) -> (accepted: bool) {
67        proof { use_type_invariant(&*self); }
68        let mut carrier = sequential_sentinel();
69        core::mem::swap(&mut self.inner, &mut carrier);
70        let accepted = carrier.begin_step();
71        core::mem::swap(&mut self.inner, &mut carrier);
72        accepted
73    }
74
75    /// Complete the active step with a value in the configured domain.
76    #[must_use]
77    pub fn complete_step(&mut self, next_value: u64) -> (accepted: bool) {
78        proof { use_type_invariant(&*self); }
79        let mut carrier = sequential_sentinel();
80        core::mem::swap(&mut self.inner, &mut carrier);
81        let accepted = carrier.complete_step(next_value);
82        core::mem::swap(&mut self.inner, &mut carrier);
83        accepted
84    }
85}
86
87/// Invalid fork-join configuration.
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89#[non_exhaustive]
90pub enum ForkJoinBuildError {
91    /// The value domain must contain at least one value.
92    EmptyValueDomain,
93    /// The initial worker value is outside the configured domain.
94    InitialValueOutOfRange,
95}
96
97/// One worker's fork-join lifecycle state.
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum WorkerState {
100    /// The worker may be started.
101    Ready,
102    /// The worker is running.
103    Running,
104    /// The worker has produced its value.
105    Complete,
106}
107
108/// The global fork-join phase.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum ForkJoinPhase {
111    /// Workers may be started and completed.
112    Fork,
113    /// All workers are complete and the barrier has committed.
114    Join,
115    /// A stable output snapshot has been produced.
116    Done,
117}
118
119/// A barriered fork-join execution with a stable output snapshot.
120pub struct ForkJoin {
121    inner: ForkJoinCarrier,
122}
123
124impl ForkJoin {
125    #[verifier::type_invariant]
126    closed spec fn well_formed(&self) -> bool { self.inner.inv() }
127
128    /// Validate and construct a fork-join execution.
129    pub fn new(workers: usize, value_domain_size: u64, initial_value: u64)
130        -> (result: Result<Self, ForkJoinBuildError>) {
131        if value_domain_size == 0 { return Err(ForkJoinBuildError::EmptyValueDomain); }
132        if initial_value >= value_domain_size {
133            return Err(ForkJoinBuildError::InitialValueOutOfRange);
134        }
135        Ok(Self { inner: ForkJoinCarrier::new(workers, value_domain_size, initial_value) })
136    }
137
138    /// Number of workers.
139    pub fn len(&self) -> usize { self.inner.wstate.len() }
140
141    /// Whether no workers are configured.
142    pub fn is_empty(&self) -> bool { self.inner.wstate.is_empty() }
143
144    /// Current global phase.
145    pub fn phase(&self) -> ForkJoinPhase {
146        if self.inner.phase == 0 { ForkJoinPhase::Fork }
147        else if self.inner.phase == 1 { ForkJoinPhase::Join }
148        else { ForkJoinPhase::Done }
149    }
150
151    /// Whether the output snapshot is ready.
152    pub fn output_ready(&self) -> bool { self.inner.output_ready }
153
154    /// Read one worker lifecycle state.
155    #[expect(clippy::indexing_slicing, reason = "the branch proves the worker index is in bounds")]
156    pub fn worker_state(&self, worker: usize) -> Option<WorkerState> {
157        if worker >= self.inner.wstate.len() { return None; }
158        Some(if self.inner.wstate[worker] == 0 { WorkerState::Ready }
159            else if self.inner.wstate[worker] == 1 { WorkerState::Running }
160            else { WorkerState::Complete })
161    }
162
163    /// Read one worker's current value.
164    #[expect(clippy::indexing_slicing, reason = "the branch proves the worker index is in bounds")]
165    pub fn worker_value(&self, worker: usize) -> Option<u64> {
166        if worker < self.inner.wvalue.len() { Some(self.inner.wvalue[worker]) } else { None }
167    }
168
169    /// Read one stable output value after output production.
170    #[expect(clippy::indexing_slicing, reason = "the branch proves the output index is in bounds")]
171    pub fn output(&self, worker: usize) -> Option<u64> {
172        if self.inner.output_ready && worker < self.inner.output_snapshot.len() {
173            Some(self.inner.output_snapshot[worker])
174        } else { None }
175    }
176
177    /// Start one ready worker during the fork phase.
178    #[must_use]
179    pub fn start_worker(&mut self, worker: usize) -> (accepted: bool) {
180        proof { use_type_invariant(&*self); }
181        let mut carrier = fork_join_sentinel();
182        core::mem::swap(&mut self.inner, &mut carrier);
183        let accepted = carrier.start_worker(worker);
184        core::mem::swap(&mut self.inner, &mut carrier);
185        accepted
186    }
187
188    /// Complete one running worker with an in-domain value.
189    #[must_use]
190    pub fn complete_worker(&mut self, worker: usize, value: u64) -> (accepted: bool) {
191        proof { use_type_invariant(&*self); }
192        let mut carrier = fork_join_sentinel();
193        core::mem::swap(&mut self.inner, &mut carrier);
194        let accepted = carrier.complete_worker(worker, value);
195        core::mem::swap(&mut self.inner, &mut carrier);
196        accepted
197    }
198
199    /// Commit the barrier when every worker is complete.
200    #[must_use]
201    pub fn barrier(&mut self) -> (accepted: bool) {
202        proof { use_type_invariant(&*self); }
203        let mut carrier = fork_join_sentinel();
204        core::mem::swap(&mut self.inner, &mut carrier);
205        let accepted = carrier.barrier();
206        core::mem::swap(&mut self.inner, &mut carrier);
207        accepted
208    }
209
210    /// Produce the immutable output snapshot from the joined worker values.
211    #[must_use]
212    pub fn produce_output(&mut self) -> (accepted: bool) {
213        proof { use_type_invariant(&*self); }
214        let mut carrier = fork_join_sentinel();
215        core::mem::swap(&mut self.inner, &mut carrier);
216        let accepted = carrier.produce_output();
217        core::mem::swap(&mut self.inner, &mut carrier);
218        accepted
219    }
220}
221
222/// Invalid step-graph configuration.
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224#[non_exhaustive]
225pub enum StepGraphBuildError {
226    /// At least one edge endpoint is outside the node universe.
227    EdgeEndpointOutOfRange,
228    /// Duplicate edges are not admitted.
229    DuplicateEdge,
230}
231
232/// One step's lifecycle state.
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub enum StepState {
235    /// A predecessor is incomplete.
236    NotReady,
237    /// All predecessors are complete.
238    Ready,
239    /// The step is running.
240    Running,
241    /// The step is complete.
242    Complete,
243}
244
245/// A predecessor-governed directed step graph.
246pub struct StepGraph {
247    inner: StepGraphCarrier,
248}
249
250impl StepGraph {
251    #[verifier::type_invariant]
252    closed spec fn well_formed(&self) -> bool { self.inner.inv() }
253
254    /// Validate edges and construct initial readiness states.
255    pub fn new(num_nodes: usize, edges: Vec<(usize, usize)>)
256        -> (result: Result<Self, StepGraphBuildError>) {
257        if !step_edges_valid(&edges, num_nodes) {
258            return Err(StepGraphBuildError::EdgeEndpointOutOfRange);
259        }
260        if !step_edges_distinct(&edges) { return Err(StepGraphBuildError::DuplicateEdge); }
261        Ok(Self { inner: StepGraphCarrier::new(num_nodes, edges) })
262    }
263
264    /// Number of steps.
265    pub fn len(&self) -> usize { self.inner.num_nodes }
266
267    /// Whether the graph has no steps.
268    pub fn is_empty(&self) -> bool { self.inner.num_nodes == 0 }
269
270    /// Number of directed predecessor edges.
271    pub fn edge_count(&self) -> usize { self.inner.edges.len() }
272
273    /// Read one directed predecessor edge.
274    #[expect(clippy::indexing_slicing, reason = "the branch proves the edge index is in bounds")]
275    pub fn edge(&self, index: usize) -> Option<(usize, usize)> {
276        if index < self.inner.edges.len() { Some(self.inner.edges[index]) } else { None }
277    }
278
279    /// Read one step lifecycle state.
280    #[expect(clippy::indexing_slicing, reason = "the branch proves the node index is in bounds")]
281    pub fn state(&self, node: usize) -> Option<StepState> {
282        if node >= self.inner.nstate.len() { return None; }
283        Some(if self.inner.nstate[node] == 0 { StepState::NotReady }
284            else if self.inner.nstate[node] == 1 { StepState::Ready }
285            else if self.inner.nstate[node] == 2 { StepState::Running }
286            else { StepState::Complete })
287    }
288
289    /// Promote a blocked node after every predecessor completes.
290    #[must_use]
291    pub fn become_ready(&mut self, node: usize) -> (accepted: bool) {
292        proof { use_type_invariant(&*self); }
293        let mut carrier = step_graph_sentinel();
294        core::mem::swap(&mut self.inner, &mut carrier);
295        let accepted = carrier.become_ready(node);
296        core::mem::swap(&mut self.inner, &mut carrier);
297        accepted
298    }
299
300    /// Start one ready step.
301    #[must_use]
302    pub fn start(&mut self, node: usize) -> (accepted: bool) {
303        proof { use_type_invariant(&*self); }
304        let mut carrier = step_graph_sentinel();
305        core::mem::swap(&mut self.inner, &mut carrier);
306        let accepted = carrier.start_running(node);
307        core::mem::swap(&mut self.inner, &mut carrier);
308        accepted
309    }
310
311    /// Complete one running step.
312    #[must_use]
313    pub fn complete(&mut self, node: usize) -> (accepted: bool) {
314        proof { use_type_invariant(&*self); }
315        let mut carrier = step_graph_sentinel();
316        core::mem::swap(&mut self.inner, &mut carrier);
317        let accepted = carrier.complete_node(node);
318        core::mem::swap(&mut self.inner, &mut carrier);
319        accepted
320    }
321
322    /// Whether every step is complete.
323    #[expect(clippy::indexing_slicing, reason = "the loop proves the state index is in bounds")]
324    #[expect(clippy::arithmetic_side_effects, reason = "the loop proves the cursor remains within the vector")]
325    pub fn is_done(&self) -> bool {
326        let mut index = 0;
327        while index < self.inner.nstate.len()
328            invariant index <= self.inner.nstate.len(),
329            decreases self.inner.nstate.len() - index,
330        {
331            if self.inner.nstate[index] != 3 { return false; }
332            index = index + 1;
333        }
334        true
335    }
336}
337
338/// Invalid stream-graph configuration.
339#[derive(Clone, Copy, Debug, Eq, PartialEq)]
340#[non_exhaustive]
341pub enum StreamGraphBuildError {
342    /// Only three- and four-stage chains are currently represented.
343    UnsupportedChainLength,
344    /// Every inter-stage queue needs positive capacity.
345    ZeroCapacity,
346    /// The record value domain must be nonempty.
347    EmptyRecordDomain,
348}
349
350/// A bounded three- or four-stage FIFO stream graph.
351pub struct StreamGraph {
352    inner: StreamGraphCarrier,
353}
354
355impl StreamGraph {
356    #[verifier::type_invariant]
357    closed spec fn well_formed(&self) -> bool { self.inner.inv() }
358
359    /// Validate and construct an empty stream graph.
360    pub fn new(chain_length: usize, capacity: usize, max_inputs: usize, record_domain_size: u64)
361        -> (result: Result<Self, StreamGraphBuildError>) {
362        if chain_length != 3 && chain_length != 4 {
363            return Err(StreamGraphBuildError::UnsupportedChainLength);
364        }
365        if capacity == 0 { return Err(StreamGraphBuildError::ZeroCapacity); }
366        if record_domain_size == 0 { return Err(StreamGraphBuildError::EmptyRecordDomain); }
367        Ok(Self { inner: StreamGraphCarrier::new(
368            chain_length, capacity, max_inputs, record_domain_size,
369        ) })
370    }
371
372    /// Number of execution stages.
373    pub fn chain_length(&self) -> usize { self.inner.chain_length }
374
375    /// Per-edge FIFO capacity.
376    pub fn capacity(&self) -> usize { self.inner.capacity }
377
378    /// Records admitted at the source.
379    pub fn ingested(&self) -> usize { self.inner.ingested }
380
381    /// Records consumed at the sink.
382    pub fn emitted(&self) -> usize { self.inner.emitted }
383
384    /// Current depth of the first queue.
385    pub fn first_queue_len(&self) -> usize { self.inner.q1.len() }
386
387    /// Current depth of the second queue.
388    pub fn second_queue_len(&self) -> usize { self.inner.q2.len() }
389
390    /// Current depth of the optional third queue.
391    pub fn third_queue_len(&self) -> usize { self.inner.q3.len() }
392
393    /// Admit one source record if its value, input bound, and backpressure permit it.
394    #[must_use]
395    pub fn ingest(&mut self, value: u64) -> (accepted: bool) {
396        proof { use_type_invariant(&*self); }
397        let mut carrier = stream_graph_sentinel();
398        core::mem::swap(&mut self.inner, &mut carrier);
399        let accepted = carrier.source_ingest(value);
400        core::mem::swap(&mut self.inner, &mut carrier);
401        accepted
402    }
403
404    /// Transfer one FIFO record across the first internal stage.
405    #[must_use]
406    pub fn advance_first(&mut self) -> (accepted: bool) {
407        proof { use_type_invariant(&*self); }
408        let mut carrier = stream_graph_sentinel();
409        core::mem::swap(&mut self.inner, &mut carrier);
410        let accepted = carrier.middle2_fire();
411        core::mem::swap(&mut self.inner, &mut carrier);
412        accepted
413    }
414
415    /// Transfer one FIFO record across the optional four-stage link.
416    #[must_use]
417    pub fn advance_second(&mut self) -> (accepted: bool) {
418        proof { use_type_invariant(&*self); }
419        let mut carrier = stream_graph_sentinel();
420        core::mem::swap(&mut self.inner, &mut carrier);
421        let accepted = carrier.middle3_fire();
422        core::mem::swap(&mut self.inner, &mut carrier);
423        accepted
424    }
425
426    /// Consume and return the next FIFO record at the sink.
427    #[expect(clippy::indexing_slicing, reason = "the queue guards prove the sink head is present")]
428    pub fn consume(&mut self) -> (value: Option<u64>) {
429        proof { use_type_invariant(&*self); }
430        let value = if self.inner.chain_length == 3 {
431            if self.inner.q2.is_empty() { return None; }
432            self.inner.q2[0]
433        } else {
434            if self.inner.q3.is_empty() { return None; }
435            self.inner.q3[0]
436        };
437        let mut carrier = stream_graph_sentinel();
438        core::mem::swap(&mut self.inner, &mut carrier);
439        let accepted = carrier.sink_consume();
440        if !accepted {
441            core::mem::swap(&mut self.inner, &mut carrier);
442            return None;
443        }
444        core::mem::swap(&mut self.inner, &mut carrier);
445        Some(value)
446    }
447
448    /// Whether the input bound is reached and every queue is drained.
449    pub fn is_done(&self) -> bool {
450        self.inner.ingested == self.inner.max_inputs
451            && self.inner.q1.is_empty()
452            && self.inner.q2.is_empty()
453            && self.inner.q3.is_empty()
454    }
455}
456
457#[expect(clippy::indexing_slicing, reason = "the loop proves the edge index is in bounds")]
458#[expect(clippy::arithmetic_side_effects, reason = "the loop proves the cursor remains within the vector")]
459#[expect(clippy::ptr_arg, reason = "Verus sequence-view contracts are stated over Vec in this checked boundary")]
460fn step_edges_valid(edges: &Vec<(usize, usize)>, num_nodes: usize) -> (valid: bool)
461    ensures valid == StepGraphCarrier::edges_valid(edges@, num_nodes),
462{
463    let mut index = 0;
464    while index < edges.len()
465        invariant
466            index <= edges.len(),
467            forall|i: int| 0 <= i < index ==>
468                #[trigger] edges@[i].0 < num_nodes && edges@[i].1 < num_nodes,
469        decreases edges.len() - index,
470    {
471        if edges[index].0 >= num_nodes || edges[index].1 >= num_nodes {
472            assert(!StepGraphCarrier::edges_valid(edges@, num_nodes));
473            return false;
474        }
475        index = index + 1;
476    }
477    true
478}
479
480#[expect(clippy::indexing_slicing, reason = "the nested loops prove both edge indices are in bounds")]
481#[expect(clippy::arithmetic_side_effects, reason = "the loops prove both cursors remain within the vector")]
482#[expect(clippy::ptr_arg, reason = "Verus sequence-view contracts are stated over Vec in this checked boundary")]
483fn step_edges_distinct(edges: &Vec<(usize, usize)>) -> (distinct: bool)
484    ensures distinct == StepGraphCarrier::edges_distinct(edges@),
485{
486    let mut left = 0;
487    while left < edges.len()
488        invariant
489            left <= edges.len(),
490            forall|i: int, j: int| 0 <= i < left && 0 <= j < edges.len() && i != j
491                ==> #[trigger] edges@[i] != #[trigger] edges@[j],
492        decreases edges.len() - left,
493    {
494        let mut right = left + 1;
495        while right < edges.len()
496            invariant
497                left < edges.len(),
498                left + 1 <= right <= edges.len(),
499                forall|j: int| left < j < right ==> edges@[left as int] != edges@[j],
500            decreases edges.len() - right,
501        {
502            if edges[left].0 == edges[right].0 && edges[left].1 == edges[right].1 {
503                assert(!StepGraphCarrier::edges_distinct(edges@));
504                return false;
505            }
506            right = right + 1;
507        }
508        left = left + 1;
509    }
510    true
511}
512
513fn sequential_sentinel() -> (carrier: SequentialCarrier)
514    ensures carrier.inv(),
515{ SequentialCarrier::new(1, 1, 0) }
516
517fn fork_join_sentinel() -> (carrier: ForkJoinCarrier)
518    ensures carrier.inv(),
519{ ForkJoinCarrier::new(0, 1, 0) }
520
521fn step_graph_sentinel() -> (carrier: StepGraphCarrier)
522    ensures carrier.inv(),
523{
524    let edges: Vec<(usize, usize)> = Vec::new();
525    StepGraphCarrier::new(0, edges)
526}
527
528fn stream_graph_sentinel() -> (carrier: StreamGraphCarrier)
529    ensures carrier.inv(),
530{ StreamGraphCarrier::new(3, 1, 0, 1) }
531
532}
533
534macro_rules! impl_error {
535    ($type:ty, { $($variant:path => $message:literal),+ $(,)? }) => {
536        impl core::fmt::Display for $type {
537            fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
538                formatter.write_str(match self { $($variant => $message),+ })
539            }
540        }
541        impl std::error::Error for $type {}
542    };
543}
544
545macro_rules! impl_observational_debug {
546    ($type:ty, $name:literal, $($field:literal => $method:ident),+ $(,)?) => {
547        impl core::fmt::Debug for $type {
548            fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
549                let mut state = formatter.debug_struct($name);
550                $(state.field($field, &self.$method());)+
551                state.finish()
552            }
553        }
554    };
555}
556
557impl_observational_debug!(Sequential, "Sequential",
558    "steps" => steps,
559    "completed" => completed,
560    "value" => value,
561    "active" => is_active,
562    "done" => is_done,
563);
564impl_observational_debug!(ForkJoin, "ForkJoin",
565    "len" => len,
566    "phase" => phase,
567    "output_ready" => output_ready,
568);
569impl_observational_debug!(StepGraph, "StepGraph",
570    "len" => len,
571    "edge_count" => edge_count,
572    "done" => is_done,
573);
574impl_observational_debug!(StreamGraph, "StreamGraph",
575    "chain_length" => chain_length,
576    "capacity" => capacity,
577    "ingested" => ingested,
578    "emitted" => emitted,
579    "first_queue_len" => first_queue_len,
580    "second_queue_len" => second_queue_len,
581    "third_queue_len" => third_queue_len,
582    "done" => is_done,
583);
584
585impl_error!(SequentialBuildError, {
586    Self::NoSteps => "sequential execution requires at least one step",
587    Self::EmptyValueDomain => "sequential value domain is empty",
588    Self::InitialValueOutOfRange => "initial sequential value is outside its domain",
589});
590impl_error!(ForkJoinBuildError, {
591    Self::EmptyValueDomain => "fork-join value domain is empty",
592    Self::InitialValueOutOfRange => "initial worker value is outside its domain",
593});
594impl_error!(StepGraphBuildError, {
595    Self::EdgeEndpointOutOfRange => "step-graph edge endpoint is outside the node universe",
596    Self::DuplicateEdge => "step graph contains a duplicate edge",
597});
598impl_error!(StreamGraphBuildError, {
599    Self::UnsupportedChainLength => "stream graph supports only three- or four-stage chains",
600    Self::ZeroCapacity => "stream-graph queue capacity must be positive",
601    Self::EmptyRecordDomain => "stream-graph record domain is empty",
602});