Skip to main content

sark_core/duplex/
machine.rs

1//! Affine duplex transition machine.
2use core::marker;
3
4use o3::buffer::{resident, segments};
5use sark_http::payload::owned;
6
7use crate::duplex::{self, admit};
8
9const INPUT_FINISH_PENDING: u8 = 1;
10const INPUT_FINISH_HELD: u8 = 1 << 1;
11const INITIAL_ACTIVATION: u8 = 1 << 2;
12
13#[derive(Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15enum Phase {
16    Idle,
17    Turn,
18    Wait,
19    Activate,
20    FinishOutput,
21    FinishSession,
22    QueuedWait,
23    QueuedActivate,
24    QueuedFinishOutput,
25    QueuedFinishSession,
26    Finished,
27}
28
29impl Phase {
30    const fn action(self) -> Option<Staged> {
31        let (dir, resume) = match self {
32            Self::Wait => (duplex::Directive::Wait, Resume::Idle),
33            Self::Activate => (duplex::Directive::Activate, Resume::Idle),
34            Self::FinishOutput => (duplex::Directive::FinishOutput, Resume::Idle),
35            Self::FinishSession => (duplex::Directive::FinishSession, Resume::Idle),
36            Self::QueuedWait => (duplex::Directive::Wait, Resume::Turn),
37            Self::QueuedActivate => (duplex::Directive::Activate, Resume::Turn),
38            Self::QueuedFinishOutput => (duplex::Directive::FinishOutput, Resume::Turn),
39            Self::QueuedFinishSession => (duplex::Directive::FinishSession, Resume::Turn),
40            Self::Idle | Self::Turn | Self::Finished => return None,
41        };
42        Some(Staged {
43            directive: dir,
44            resume,
45        })
46    }
47
48    const fn from_action(dir: duplex::Directive, resume: Resume) -> Self {
49        match (dir, resume) {
50            (duplex::Directive::Wait, Resume::Idle) => Self::Wait,
51            (duplex::Directive::Activate, Resume::Idle) => Self::Activate,
52            (duplex::Directive::FinishOutput, Resume::Idle) => Self::FinishOutput,
53            (duplex::Directive::FinishSession, Resume::Idle) => Self::FinishSession,
54            (duplex::Directive::Wait, Resume::Turn) => Self::QueuedWait,
55            (duplex::Directive::Activate, Resume::Turn) => Self::QueuedActivate,
56            (duplex::Directive::FinishOutput, Resume::Turn) => Self::QueuedFinishOutput,
57            (duplex::Directive::FinishSession, Resume::Turn) => Self::QueuedFinishSession,
58        }
59    }
60}
61
62#[derive(Clone, Copy)]
63#[repr(u8)]
64enum Resume {
65    Idle,
66    Turn,
67}
68
69#[derive(Clone, Copy)]
70struct Staged {
71    directive: duplex::Directive,
72    resume: Resume,
73}
74
75/// Exact disposition of the session output half.
76#[derive(Clone, Copy, PartialEq, Eq)]
77#[repr(u8)]
78pub enum Output {
79    Open,
80    Finished,
81}
82
83#[derive(Clone, Copy)]
84struct Next {
85    settled: After,
86    flags: u8,
87    output: Output,
88}
89
90impl Next {
91    const fn after(&self) -> After {
92        match self.settled {
93            After::Finished => After::Finished,
94            After::Turn => After::Turn,
95            After::Idle if self.flags & INPUT_FINISH_PENDING != 0 => After::Turn,
96            After::Idle => After::Idle,
97        }
98    }
99
100    const fn reduce(mut flags: u8, mut output: Output, staged: Staged) -> Self {
101        let settled = match staged.directive {
102            duplex::Directive::Wait => match staged.resume {
103                Resume::Idle => After::Idle,
104                Resume::Turn => After::Turn,
105            },
106            duplex::Directive::Activate => After::Turn,
107            duplex::Directive::FinishOutput => {
108                output = Output::Finished;
109                if flags == INPUT_FINISH_HELD {
110                    flags = 0;
111                    After::Finished
112                } else {
113                    match staged.resume {
114                        Resume::Idle => After::Idle,
115                        Resume::Turn => After::Turn,
116                    }
117                }
118            }
119            duplex::Directive::FinishSession => {
120                flags = 0;
121                output = Output::Finished;
122                After::Finished
123            }
124        };
125        Self {
126            settled,
127            flags,
128            output,
129        }
130    }
131}
132
133/// Exact session owner with one mutually exclusive input/output slot.
134pub struct Machine<'d, D: duplex::Session<'d>> {
135    /// Released before the session that produced it.
136    pending: Option<segments::Nonempty<resident::Shared<'d>>>,
137    session: D,
138    phase: Phase,
139    flags: u8,
140    output: Output,
141}
142
143/// Exclusive vacant input slot bound to one machine borrow.
144#[must_use = "commit the input owner or keep it at the carrier boundary"]
145pub struct Input<'a, 'd, D: duplex::Session<'d>>(&'a mut Machine<'d, D>);
146/// Exact availability of the session's one callback input slot.
147pub enum Access<T> {
148    Vacant(T),
149    Blocked,
150}
151/// Exact current progress of the session machine.
152#[derive(Clone, Copy, PartialEq, Eq)]
153#[repr(u8)]
154pub enum Progress {
155    Idle,
156    Turn,
157    Finished,
158}
159/// Exact retention of an observed orderly input finish.
160#[derive(Clone, Copy, PartialEq, Eq)]
161#[repr(u8)]
162pub enum Hold {
163    Released,
164    Held,
165}
166impl<'d, D: duplex::Session<'d>> Input<'_, 'd, D> {
167    pub fn commit(self, input: owned::Segments<'d>) {
168        if let Some(input) = input.take() {
169            self.0.pending = Some(input);
170        }
171    }
172}
173/// Validated orderly-input finish bound to one machine borrow.
174#[must_use = "commit the validated input finish"]
175pub struct Finish<'a, 'd, D: duplex::Session<'d>>(&'a mut Machine<'d, D>);
176
177impl<'d, D: duplex::Session<'d>> Finish<'_, 'd, D> {
178    pub fn commit(self) {
179        self.0.flags |= INPUT_FINISH_PENDING | INPUT_FINISH_HELD;
180    }
181}
182
183/// Transport disposition causally adjacent to one admitted output.
184#[derive(Clone, Copy, PartialEq, Eq)]
185#[repr(u8)]
186pub enum End {
187    Open,
188    FinishOutput,
189    FinishSession,
190}
191
192/// Projected state produced by the same reducer as the physical commit.
193#[derive(Clone, Copy, PartialEq, Eq)]
194#[repr(u8)]
195pub enum After {
196    Idle,
197    Turn,
198    Finished,
199}
200
201/// Protocol-neutral result of one machine transaction.
202pub enum Transaction<R, E> {
203    Idle,
204    Committed { receipt: R, after: After },
205    Pending { receipt: R },
206    Rejected(E),
207    Violation(Violation),
208}
209
210/// A session/carrier contract violation detected at the canonical boundary.
211#[derive(Clone, Copy, PartialEq, Eq)]
212pub enum Violation {
213    OutputAfterFinish,
214    PrefixCapacity { written: usize, capacity: usize },
215    PrefixPending,
216}
217
218/// Rejection of one orderly peer-input terminal transition.
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum FinishError {
221    Finished,
222    Closed,
223}
224
225impl<'d, D: duplex::Session<'d>> Machine<'d, D> {
226    pub fn new(inner: D) -> Self {
227        Self {
228            session: inner,
229            pending: None,
230            phase: Phase::Turn,
231            flags: INITIAL_ACTIVATION,
232            output: Output::Open,
233        }
234    }
235
236    pub(super) fn with_finish(inner: D) -> Self {
237        Self {
238            flags: INPUT_FINISH_PENDING | INPUT_FINISH_HELD | INITIAL_ACTIVATION,
239            ..Self::new(inner)
240        }
241    }
242
243    /// Validates the one callback slot and binds vacancy to this borrow.
244    pub fn input(&mut self) -> Access<Input<'_, 'd, D>> {
245        match self.access() {
246            Access::Vacant(()) => Access::Vacant(Input(self)),
247            Access::Blocked => Access::Blocked,
248        }
249    }
250
251    pub const fn access(&self) -> Access<()> {
252        match (&self.pending, self.phase) {
253            (None, Phase::Idle | Phase::Turn) => Access::Vacant(()),
254            (None | Some(_), _) => Access::Blocked,
255        }
256    }
257
258    pub const fn progress(&self) -> Progress {
259        if self.pending.is_some() || self.flags & INPUT_FINISH_PENDING != 0 {
260            return Progress::Turn;
261        }
262        match self.phase {
263            Phase::Idle => Progress::Idle,
264            Phase::Turn
265            | Phase::Wait
266            | Phase::Activate
267            | Phase::FinishOutput
268            | Phase::FinishSession
269            | Phase::QueuedWait
270            | Phase::QueuedActivate
271            | Phase::QueuedFinishOutput
272            | Phase::QueuedFinishSession => Progress::Turn,
273            Phase::Finished => Progress::Finished,
274        }
275    }
276
277    pub const fn output(&self) -> &Output {
278        &self.output
279    }
280
281    pub const fn hold(&self) -> Hold {
282        if self.flags & INPUT_FINISH_HELD == 0 {
283            Hold::Released
284        } else {
285            Hold::Held
286        }
287    }
288
289    /// Binds an orderly-input finish while preserving the caller's exact owner
290    /// on both transition outcomes.
291    pub fn finish<I>(&mut self, owner: I) -> Result<(Finish<'_, 'd, D>, I), (FinishError, I)> {
292        match self.finish_error() {
293            Some(error) => Err((error, owner)),
294            None => Ok((Finish(self), owner)),
295        }
296    }
297
298    /// Joins at most one session callback to one native admission attempt.
299    pub fn transact<R, E>(
300        &mut self,
301        output: &mut [u8],
302        scratch: &mut Vec<u8>,
303        admit: impl for<'turn> FnOnce(
304            usize,
305            admit::Emission<'turn, 'd, D>,
306        ) -> admit::Admission<'turn, 'd, D, R, E>,
307    ) -> Transaction<R, E> {
308        let (written, staged) = match self.phase.action() {
309            Some(staged) => (0, staged),
310            None => {
311                let pending = self.pending.is_some();
312                let turn = matches!(self.phase, Phase::Turn);
313                let finish = self.flags & INPUT_FINISH_PENDING != 0;
314                if !pending && !turn && !finish {
315                    return Transaction::Idle;
316                }
317                // Only initial activation precedes FIN; later activation remains queued.
318                let activate = turn && (!finish || self.flags & INITIAL_ACTIVATION != 0);
319                let resume = if turn && (pending || !activate) {
320                    Resume::Turn
321                } else {
322                    Resume::Idle
323                };
324                let (written, action) = if let Some(input) = self.pending.take() {
325                    self.session
326                        .receive(owned::Segments::from(input), output, scratch)
327                } else if activate {
328                    self.flags &= !INITIAL_ACTIVATION;
329                    self.session.activate(output, scratch)
330                } else {
331                    self.flags &= !INPUT_FINISH_PENDING;
332                    self.session.finish(output, scratch)
333                };
334                let (output, dir) = action.into_parts();
335                self.pending = output.and_then(owned::Segments::take);
336                self.phase = Phase::from_action(dir, resume);
337                (
338                    written,
339                    Staged {
340                        directive: dir,
341                        resume,
342                    },
343                )
344            }
345        };
346        if written > output.len() {
347            self.terminate();
348            return Transaction::Violation(Violation::PrefixCapacity {
349                written,
350                capacity: output.len(),
351            });
352        }
353        if matches!(self.output, Output::Finished) && (written != 0 || self.pending.is_some()) {
354            self.terminate();
355            return Transaction::Violation(Violation::OutputAfterFinish);
356        }
357        let end = match (self.output, staged.directive) {
358            (Output::Finished, _)
359            | (Output::Open, duplex::Directive::Wait | duplex::Directive::Activate) => End::Open,
360            (Output::Open, duplex::Directive::FinishOutput) => End::FinishOutput,
361            (Output::Open, duplex::Directive::FinishSession) => End::FinishSession,
362        };
363        let next = Next::reduce(self.flags, self.output, staged);
364        let after = next.after();
365        let payload = match self.pending.take() {
366            Some(output) => admit::Payload::Present {
367                output,
368                permit: admit::Permit {
369                    owner: marker::PhantomData,
370                    mode: marker::PhantomData,
371                },
372            },
373            None => admit::Payload::Vacant(admit::Permit {
374                owner: marker::PhantomData,
375                mode: marker::PhantomData,
376            }),
377        };
378        let emit = admit::Emission {
379            payload,
380            end,
381            after,
382        };
383
384        let admit::Admission {
385            decision,
386            owner: marker::PhantomData,
387        } = admit(written, emit);
388        match decision {
389            admit::Decision::Pending { .. } | admit::Decision::Blocked { .. } if written != 0 => {
390                self.terminate();
391                Transaction::Violation(Violation::PrefixPending)
392            }
393            admit::Decision::Committed { receipt: proof } => {
394                self.phase = match next.settled {
395                    After::Idle => Phase::Idle,
396                    After::Turn => Phase::Turn,
397                    After::Finished => Phase::Finished,
398                };
399                self.flags = next.flags;
400                self.output = next.output;
401                Transaction::Committed {
402                    receipt: proof,
403                    after,
404                }
405            }
406            admit::Decision::Pending {
407                remainder: rest,
408                receipt: proof,
409            } => {
410                self.pending = Some(rest);
411                Transaction::Pending { receipt: proof }
412            }
413            admit::Decision::Blocked { receipt: proof } => Transaction::Pending { receipt: proof },
414            admit::Decision::Rejected { error } => {
415                self.terminate();
416                Transaction::Rejected(error)
417            }
418            admit::Decision::Returned {
419                error,
420                remainder: rest,
421            } => {
422                drop(rest);
423                self.terminate();
424                Transaction::Rejected(error)
425            }
426        }
427    }
428
429    fn terminate(&mut self) {
430        self.pending = None;
431        self.phase = Phase::Finished;
432        self.flags = 0;
433        self.output = Output::Finished;
434    }
435
436    fn finish_error(&self) -> Option<FinishError> {
437        match self.progress() {
438            Progress::Finished => Some(FinishError::Closed),
439            Progress::Idle | Progress::Turn
440                if self.flags & (INPUT_FINISH_PENDING | INPUT_FINISH_HELD) != 0 =>
441            {
442                Some(FinishError::Finished)
443            }
444            Progress::Idle | Progress::Turn => None,
445        }
446    }
447
448    /// Retires the session once at the adapter's observable close boundary.
449    pub fn retire(&mut self) {
450        if matches!(self.phase, Phase::Finished) {
451            return;
452        }
453        self.terminate();
454    }
455}
456
457const _: () = {
458    assert!(core::mem::size_of::<Resume>() == core::mem::size_of::<bool>());
459    assert!(core::mem::align_of::<Resume>() == core::mem::align_of::<bool>());
460    assert!(core::mem::size_of::<Next>() == 3);
461    assert!(core::mem::align_of::<Next>() == 1);
462    assert!(core::mem::size_of::<After>() == core::mem::size_of::<u8>());
463    assert!(core::mem::align_of::<After>() == core::mem::align_of::<u8>());
464    assert!(core::mem::size_of::<Output>() == core::mem::size_of::<u8>());
465    assert!(core::mem::align_of::<Output>() == core::mem::align_of::<u8>());
466    assert!(
467        core::mem::size_of::<Machine<'static, duplex::Never>>()
468            == core::mem::size_of::<Option<segments::Nonempty<resident::Shared<'static>>>>()
469                + core::mem::size_of::<usize>()
470    );
471    assert!(
472        core::mem::size_of::<Input<'static, 'static, duplex::Never>>()
473            == core::mem::size_of::<usize>()
474    );
475    assert!(
476        core::mem::size_of::<Finish<'static, 'static, duplex::Never>>()
477            == core::mem::size_of::<usize>()
478    );
479};