minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use alloc::string::String;
use alloc::vec::Vec;

use super::page::{BlockDot, Page};
use super::{PARAGRAPH, TABLE};

/// One rendered block under the read-side conjunction rule: a block renders
/// as content only when every pair the content needs has arrived, and every
/// other cross-pair state is *classified*, never fabricated. This is
/// candidate answer (a) from the stage-A scoping (caller discipline, no new
/// type), and the coherence scripts test whether it suffices.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Rendered {
    /// Kind reads `paragraph` and the paragraph content has arrived.
    Paragraph { block: BlockDot, text: String },
    /// Kind reads `table` and the table content has arrived.
    Table { block: BlockDot, rows: usize },
    /// Concurrent kind writes survive as siblings: the conflict is surfaced
    /// whole, and which to display is the caller's policy (the kairotic tag
    /// is the designed discriminator, horizons axis 8).
    MultiKind {
        block: BlockDot,
        kinds: Vec<&'static str>,
    },
    /// Fields arrived but the selected kind's content map holds nothing for
    /// this block (or the kind register itself is empty): render withholds
    /// rather than fabricating. The recorded limit rides here: canonical
    /// form drops bottom stores, so a *genuinely empty* block has no
    /// content-map key and classifies exactly like an undelivered one;
    /// absence cannot say "empty", the recipe's fourth hurt (pinned by
    /// `test_empty_block_is_ambiguous_with_undelivered_content`).
    KindPending { block: BlockDot },
    /// Order arrived ahead of the field pair: a visible block dot with no
    /// fields at all, the ghost the naive render shows as a blank block.
    Ghost { block: BlockDot },
}

impl Page {
    /// The conjunction render beside its deterministic cost: the classified
    /// block list and the number of cross-pair map lookups it performed
    /// (fields get, kind register read, and one content-map get per selected
    /// kind), the per-render counter the measurement report records.
    #[must_use]
    pub fn render_counted(&self) -> (Vec<Rendered>, usize) {
        let mut rendered = Vec::new();
        let mut lookups = 0usize;
        for block in self.block_order() {
            lookups += 1; // fields map get
            if !self.fields_present(block) {
                rendered.push(Rendered::Ghost { block });
                continue;
            }
            lookups += 1; // kind register read
            let kinds = self.kinds(block);
            match kinds.as_slice() {
                [] => rendered.push(Rendered::KindPending { block }),
                [kind] => {
                    lookups += 1; // the selected kind's content-map get
                    rendered.push(self.render_kind(block, kind));
                }
                _ => rendered.push(Rendered::MultiKind { block, kinds }),
            }
        }
        (rendered, lookups)
    }

    /// The conjunction render alone.
    #[must_use]
    pub fn render(&self) -> Vec<Rendered> {
        self.render_counted().0
    }

    /// Renders a single-kind block: content when the kind's pair has arrived,
    /// [`Rendered::KindPending`] when it has not (or the tag names a kind
    /// this recipe has no map for).
    fn render_kind(&self, block: BlockDot, kind: &str) -> Rendered {
        if kind == PARAGRAPH && self.para_present(block) {
            return Rendered::Paragraph {
                block,
                text: self.para_text(block),
            };
        }
        if kind == TABLE && self.table_present(block) {
            return Rendered::Table {
                block,
                rows: self.table_rows(block),
            };
        }
        Rendered::KindPending { block }
    }

    /// The naive render's anomaly count: how many order-visible blocks the
    /// conjunction rule had to classify as [`Rendered::Ghost`] or
    /// [`Rendered::KindPending`] (states a render without the rule would
    /// fabricate as blank blocks). Zero on a quiesced page.
    #[must_use]
    pub fn anomalous_blocks(&self) -> usize {
        self.render()
            .iter()
            .filter(|entry| matches!(entry, Rendered::Ghost { .. } | Rendered::KindPending { .. }))
            .count()
    }
}