minerva 0.2.0

Causal ordering for distributed systems
//! Native-epoch holdback behind the adoption witness.

use core::num::NonZeroU64;

use crate::metis::dot::Dot;
use crate::metis::{Adopted, Gate, Ideal};

/// A durable epoch identity: lineage generation plus winning declaration dot.
///
/// The generation prevents a declaration tuple reused after horizon eviction
/// from aliasing traffic for the retired epoch. Both parts carry their own
/// laws on the type: the generation is one-based and the declaration is a
/// [`Dot`] (ruling R-91), so a held address never re-proves either.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EpochAddress {
    generation: NonZeroU64,
    declaration: Dot,
}

impl EpochAddress {
    pub(super) const fn new(generation: NonZeroU64, declaration: Dot) -> Self {
        Self {
            generation,
            declaration,
        }
    }

    /// The monotonically increasing lineage generation.
    #[must_use]
    pub const fn generation(self) -> u64 {
        self.generation.get()
    }

    /// The declaration's causal identity within its parent epoch.
    #[must_use]
    pub const fn declaration(self) -> Dot {
        self.declaration
    }

    /// Rehydrates a durable or wire address after validating its identity
    /// parts.
    ///
    /// The epoch machine starts lineage at generation one and declarations
    /// are dots, whose counters are one-based. Keeping this check on the
    /// nominal type prevents each consumer codec from reconstructing those
    /// invariants. The refusals are exactly the codec contract's two
    /// (R-8): station zero remains a decodable station id, and its refusal
    /// stays the consumer's authenticated-boundary law (ruling R-91).
    ///
    /// # Errors
    ///
    /// [`InvalidEpochAddress`] when `generation` is zero or `declaration`
    /// carries Minerva's non-dot counter.
    pub const fn try_from_parts(
        generation: u64,
        declaration: (u32, u64),
    ) -> Result<Self, InvalidEpochAddress> {
        let Some(generation) = NonZeroU64::new(generation) else {
            return Err(InvalidEpochAddress::ZeroGeneration);
        };
        match Dot::from_parts(declaration.0, declaration.1) {
            Ok(declaration) => Ok(Self {
                generation,
                declaration,
            }),
            Err(_) => Err(InvalidEpochAddress::ZeroDeclarationCounter {
                station: declaration.0,
            }),
        }
    }
}

impl core::fmt::Display for EpochAddress {
    /// Prints the address triple: `(generation, station, counter)` --- the
    /// raw-parts spelling from before the S346 re-typing. Refusal evidence
    /// formats the derived `Debug`; this is the compact human spelling.
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "({}, {}, {})",
            self.generation,
            self.declaration.station(),
            self.declaration.counter()
        )
    }
}

/// A decoded epoch address does not name a possible lineage declaration.
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum InvalidEpochAddress {
    /// Epoch lineage starts at generation one.
    #[error("epoch generation zero is invalid")]
    ZeroGeneration,
    /// A declaration is a dot and therefore has a nonzero counter.
    #[error("epoch declaration at station {station} has the non-dot counter zero")]
    ZeroDeclarationCounter {
        /// Station attached to the malformed declaration.
        station: u32,
    },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn durable_address_parts_rehydrate_only_when_generation_and_counter_are_nonzero() {
        let address = EpochAddress::try_from_parts(7, (3, 11)).unwrap();
        assert_eq!(address.generation(), 7);
        assert_eq!(address.declaration(), Dot::from_parts(3, 11).unwrap());
        assert_eq!(
            EpochAddress::try_from_parts(0, (3, 11)),
            Err(InvalidEpochAddress::ZeroGeneration)
        );
        assert_eq!(
            EpochAddress::try_from_parts(7, (3, 0)),
            Err(InvalidEpochAddress::ZeroDeclarationCounter { station: 3 })
        );
        // Station zero stays a decodable station id: the nonzero-station
        // rule is the consumer's boundary law, not this type's (R-91).
        let wide = EpochAddress::try_from_parts(7, (0, 1)).unwrap();
        assert_eq!(wide.declaration().station(), 0);
    }
}

/// Holds native traffic until this replica adopts its declaration.
///
/// One buffer spans one transition. Before adoption every event waits;
/// after adoption events for the winner release and every competing address
/// is terminally stale. Reusing a buffer for another transition is therefore
/// deliberately impossible.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EpochGate;

/// Adoption attempted on a buffer already fixed to another declaration.
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("epoch buffer already adopted {adopted:?}; cannot adopt {requested:?}")]
pub struct EpochAdoptionMismatch {
    /// The declaration already fixed in the buffer.
    pub adopted: EpochAddress,
    /// The competing declaration the caller attempted to adopt.
    pub requested: EpochAddress,
}

impl Gate for EpochGate {
    type Dep = EpochAddress;
    type Progress = Option<EpochAddress>;

    fn deliverable(progress: &Self::Progress, _sender: u32, dep: &Self::Dep) -> bool {
        progress.as_ref() == Some(dep)
    }

    fn advance(progress: &mut Self::Progress, _sender: u32, dep: &Self::Dep) {
        if progress.is_none() {
            *progress = Some(*dep);
        }
    }

    fn stale(progress: &Self::Progress, _sender: u32, dep: &Self::Dep) -> bool {
        progress.is_some_and(|adopted| adopted != *dep)
    }
}

impl<T> Ideal<T, EpochGate> {
    /// Opens this one-transition buffer for the fixed winning declaration.
    ///
    /// Idempotent for the same declaration. A competing declaration is a
    /// typed refusal and cannot replace the monotone adoption witness.
    ///
    /// # Errors
    ///
    /// Returns [`EpochAdoptionMismatch`] when this buffer already adopted a
    /// different declaration.
    pub fn adopt_epoch(&mut self, adopted: &Adopted) -> Result<(), EpochAdoptionMismatch> {
        let requested = adopted.address();
        if let Some(adopted) = *self.progress()
            && adopted != requested
        {
            return Err(EpochAdoptionMismatch { adopted, requested });
        }
        self.advance_progress(requested.declaration().station(), &requested);
        Ok(())
    }
}