terminals-core 0.1.0

Core runtime primitives for Terminals OS: phase dynamics, AXON wire protocol, substrate engine, and sematonic types
Documentation
//! ProjectionLayout — Compile-time memory layout for ComputeAtoms.
//!
//! Determines byte offsets and stride from a set of active projections.
//! Once constructed, the layout is frozen (immutable). Amb-governed
//! composition happens BEFORE layout creation; deterministic access after.

use super::projection::ProjectionId;
use super::splat::SplatProjection;
use super::kuramoto::KuramotoProjection;
use super::expert::ExpertProjection;
use super::graph::GraphProjection;
use super::thermal::ThermalProjection;
use super::projection::Projection;

/// A frozen memory layout: which projections are active, at what offsets.
#[derive(Debug, Clone)]
pub struct ProjectionLayout {
    /// Active projections with their byte offsets, in order.
    pub entries: Vec<(ProjectionId, usize)>,
    /// Total bytes per atom (sum of all projection byte sizes).
    pub stride: usize,
}

impl ProjectionLayout {
    /// Create a layout from a set of projection IDs.
    /// Order is canonical (Splat, Kuramoto, Expert, Graph) regardless of input order.
    pub fn from_projections(ids: &[ProjectionId]) -> Self {
        // Canonical ordering for deterministic layout
        let canonical = [
            ProjectionId::Splat,
            ProjectionId::Kuramoto,
            ProjectionId::Expert,
            ProjectionId::Graph,
            ProjectionId::Thermal,
        ];

        let mut entries = Vec::new();
        let mut offset = 0usize;

        for &proj_id in &canonical {
            if ids.contains(&proj_id) {
                let size = Self::projection_byte_size(proj_id);
                entries.push((proj_id, offset));
                offset += size;
            }
        }

        Self {
            entries,
            stride: offset,
        }
    }

    /// Full 5-projection layout (void instantiation — all projections present).
    pub fn full() -> Self {
        Self::from_projections(&[
            ProjectionId::Splat,
            ProjectionId::Kuramoto,
            ProjectionId::Expert,
            ProjectionId::Graph,
            ProjectionId::Thermal,
        ])
    }

    /// Minimal layout: only Kuramoto + Splat (e.g., landing page).
    pub fn minimal() -> Self {
        Self::from_projections(&[ProjectionId::Splat, ProjectionId::Kuramoto])
    }

    /// Get byte offset for a projection, or None if not in layout.
    pub fn offset_of(&self, id: ProjectionId) -> Option<usize> {
        self.entries.iter().find(|(pid, _)| *pid == id).map(|(_, off)| *off)
    }

    /// Whether a projection is active in this layout.
    pub fn has(&self, id: ProjectionId) -> bool {
        self.entries.iter().any(|(pid, _)| *pid == id)
    }

    /// Number of active projections.
    pub fn count(&self) -> usize {
        self.entries.len()
    }

    fn projection_byte_size(id: ProjectionId) -> usize {
        match id {
            ProjectionId::Splat => SplatProjection::byte_size(),
            ProjectionId::Kuramoto => KuramotoProjection::byte_size(),
            ProjectionId::Expert => ExpertProjection::byte_size(),
            ProjectionId::Graph => GraphProjection::byte_size(),
            ProjectionId::Thermal => ThermalProjection::byte_size(),
        }
    }
}

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

    #[test]
    fn test_full_layout_stride() {
        let layout = ProjectionLayout::full();
        // Splat(1536) + Kuramoto(12) + Expert(12) + Graph(32) + Thermal(20) = 1612
        assert_eq!(layout.stride, 1612);
        assert_eq!(layout.count(), 5);
    }

    #[test]
    fn test_minimal_layout() {
        let layout = ProjectionLayout::minimal();
        // Splat(1536) + Kuramoto(12) = 1548
        assert_eq!(layout.stride, 1548);
        assert_eq!(layout.count(), 2);
        assert!(layout.has(ProjectionId::Splat));
        assert!(layout.has(ProjectionId::Kuramoto));
        assert!(!layout.has(ProjectionId::Expert));
    }

    #[test]
    fn test_offset_canonical_order() {
        let layout = ProjectionLayout::full();
        assert_eq!(layout.offset_of(ProjectionId::Splat), Some(0));
        assert_eq!(layout.offset_of(ProjectionId::Kuramoto), Some(1536));
        assert_eq!(layout.offset_of(ProjectionId::Expert), Some(1548));
        assert_eq!(layout.offset_of(ProjectionId::Graph), Some(1560));
    }

    #[test]
    fn test_order_independent() {
        // Regardless of input order, layout should be canonical
        let a = ProjectionLayout::from_projections(&[ProjectionId::Graph, ProjectionId::Splat]);
        let b = ProjectionLayout::from_projections(&[ProjectionId::Splat, ProjectionId::Graph]);
        assert_eq!(a.stride, b.stride);
        assert_eq!(a.offset_of(ProjectionId::Splat), b.offset_of(ProjectionId::Splat));
    }
}