Skip to main content

terminals_core/substrate/
layout.rs

1//! ProjectionLayout — Compile-time memory layout for ComputeAtoms.
2//!
3//! Determines byte offsets and stride from a set of active projections.
4//! Once constructed, the layout is frozen (immutable). Amb-governed
5//! composition happens BEFORE layout creation; deterministic access after.
6
7use super::projection::ProjectionId;
8use super::splat::SplatProjection;
9use super::kuramoto::KuramotoProjection;
10use super::expert::ExpertProjection;
11use super::graph::GraphProjection;
12use super::thermal::ThermalProjection;
13use super::projection::Projection;
14
15/// A frozen memory layout: which projections are active, at what offsets.
16#[derive(Debug, Clone)]
17pub struct ProjectionLayout {
18    /// Active projections with their byte offsets, in order.
19    pub entries: Vec<(ProjectionId, usize)>,
20    /// Total bytes per atom (sum of all projection byte sizes).
21    pub stride: usize,
22}
23
24impl ProjectionLayout {
25    /// Create a layout from a set of projection IDs.
26    /// Order is canonical (Splat, Kuramoto, Expert, Graph) regardless of input order.
27    pub fn from_projections(ids: &[ProjectionId]) -> Self {
28        // Canonical ordering for deterministic layout
29        let canonical = [
30            ProjectionId::Splat,
31            ProjectionId::Kuramoto,
32            ProjectionId::Expert,
33            ProjectionId::Graph,
34            ProjectionId::Thermal,
35        ];
36
37        let mut entries = Vec::new();
38        let mut offset = 0usize;
39
40        for &proj_id in &canonical {
41            if ids.contains(&proj_id) {
42                let size = Self::projection_byte_size(proj_id);
43                entries.push((proj_id, offset));
44                offset += size;
45            }
46        }
47
48        Self {
49            entries,
50            stride: offset,
51        }
52    }
53
54    /// Full 5-projection layout (void instantiation — all projections present).
55    pub fn full() -> Self {
56        Self::from_projections(&[
57            ProjectionId::Splat,
58            ProjectionId::Kuramoto,
59            ProjectionId::Expert,
60            ProjectionId::Graph,
61            ProjectionId::Thermal,
62        ])
63    }
64
65    /// Minimal layout: only Kuramoto + Splat (e.g., landing page).
66    pub fn minimal() -> Self {
67        Self::from_projections(&[ProjectionId::Splat, ProjectionId::Kuramoto])
68    }
69
70    /// Get byte offset for a projection, or None if not in layout.
71    pub fn offset_of(&self, id: ProjectionId) -> Option<usize> {
72        self.entries.iter().find(|(pid, _)| *pid == id).map(|(_, off)| *off)
73    }
74
75    /// Whether a projection is active in this layout.
76    pub fn has(&self, id: ProjectionId) -> bool {
77        self.entries.iter().any(|(pid, _)| *pid == id)
78    }
79
80    /// Number of active projections.
81    pub fn count(&self) -> usize {
82        self.entries.len()
83    }
84
85    fn projection_byte_size(id: ProjectionId) -> usize {
86        match id {
87            ProjectionId::Splat => SplatProjection::byte_size(),
88            ProjectionId::Kuramoto => KuramotoProjection::byte_size(),
89            ProjectionId::Expert => ExpertProjection::byte_size(),
90            ProjectionId::Graph => GraphProjection::byte_size(),
91            ProjectionId::Thermal => ThermalProjection::byte_size(),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_full_layout_stride() {
102        let layout = ProjectionLayout::full();
103        // Splat(1536) + Kuramoto(12) + Expert(12) + Graph(32) + Thermal(20) = 1612
104        assert_eq!(layout.stride, 1612);
105        assert_eq!(layout.count(), 5);
106    }
107
108    #[test]
109    fn test_minimal_layout() {
110        let layout = ProjectionLayout::minimal();
111        // Splat(1536) + Kuramoto(12) = 1548
112        assert_eq!(layout.stride, 1548);
113        assert_eq!(layout.count(), 2);
114        assert!(layout.has(ProjectionId::Splat));
115        assert!(layout.has(ProjectionId::Kuramoto));
116        assert!(!layout.has(ProjectionId::Expert));
117    }
118
119    #[test]
120    fn test_offset_canonical_order() {
121        let layout = ProjectionLayout::full();
122        assert_eq!(layout.offset_of(ProjectionId::Splat), Some(0));
123        assert_eq!(layout.offset_of(ProjectionId::Kuramoto), Some(1536));
124        assert_eq!(layout.offset_of(ProjectionId::Expert), Some(1548));
125        assert_eq!(layout.offset_of(ProjectionId::Graph), Some(1560));
126    }
127
128    #[test]
129    fn test_order_independent() {
130        // Regardless of input order, layout should be canonical
131        let a = ProjectionLayout::from_projections(&[ProjectionId::Graph, ProjectionId::Splat]);
132        let b = ProjectionLayout::from_projections(&[ProjectionId::Splat, ProjectionId::Graph]);
133        assert_eq!(a.stride, b.stride);
134        assert_eq!(a.offset_of(ProjectionId::Splat), b.offset_of(ProjectionId::Splat));
135    }
136}