Skip to main content

lsp_max/runtime/typestate/
mod.rs

1//! Typestate machine types for AccessAdmissionLaw.
2
3use crate::runtime::sha256::{sha256, validate_and_reconstruct_chain_checked};
4use std::fmt::{Debug, Formatter};
5use std::marker::PhantomData;
6
7pub struct DeterministicSnapshot {
8    pub id: lsp_max_protocol::SnapshotId,
9    pub timestamp: u64,
10}
11
12impl DeterministicSnapshot {
13    pub fn new() -> Self {
14        let timestamp = std::time::SystemTime::now()
15            .duration_since(std::time::UNIX_EPOCH)
16            .unwrap_or(std::time::Duration::ZERO)
17            .as_secs();
18        Self {
19            id: lsp_max_protocol::SnapshotId(format!("snap-{}", timestamp)),
20            timestamp,
21        }
22    }
23}
24
25impl Default for DeterministicSnapshot {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31/// Represents the compile-time law governing state transitions and protocol behavior.
32pub trait Law {
33    type Error;
34}
35
36/// The specific admission law defined in the OWL ontology.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct AccessAdmissionLaw;
39
40impl Law for AccessAdmissionLaw {
41    type Error = &'static str;
42}
43
44/// Represents a distinct phase in the lifecycle of the machine.
45pub trait Phase {}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub struct Uninitialized;
49impl Phase for Uninitialized {}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub struct Initializing;
53impl Phase for Initializing {}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub struct Initialized;
57impl Phase for Initialized {}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub struct ShutDown;
61impl Phase for ShutDown {}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub struct Exited;
65impl Phase for Exited {}
66
67/// Represents the inner data/state carried during a particular phase.
68pub trait Data {}
69
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
71pub struct EmptyData {
72    pub client_capabilities: Option<serde_json::Value>,
73    pub server_capabilities: Option<serde_json::Value>,
74}
75impl Data for EmptyData {}
76
77#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
78pub struct InitializingData {
79    pub client_capabilities: serde_json::Value,
80}
81impl Data for InitializingData {}
82
83#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
84pub struct InitializedData {
85    pub client_capabilities: serde_json::Value,
86    pub server_capabilities: serde_json::Value,
87}
88impl Data for InitializedData {}
89
90/// The zero-cost typestate machine container.
91pub struct Machine<L: Law, P: Phase, D: Data> {
92    pub _law: PhantomData<L>,
93    pub phase: P,
94    pub data: D,
95}
96
97impl<L: Law, P: Phase + Debug, D: Data + Debug> Debug for Machine<L, P, D> {
98    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("Machine")
100            .field("phase", &self.phase)
101            .field("data", &self.data)
102            .finish()
103    }
104}
105
106impl<L: Law, P: Phase + Clone, D: Data + Clone> Clone for Machine<L, P, D> {
107    fn clone(&self) -> Self {
108        Self {
109            _law: PhantomData,
110            phase: self.phase.clone(),
111            data: self.data.clone(),
112        }
113    }
114}
115
116impl<L: Law, P: Phase, D: Data> Machine<L, P, D> {
117    /// Create a new typestate machine.
118    pub const fn new(phase: P, data: D) -> Self {
119        Self {
120            _law: PhantomData,
121            phase,
122            data,
123        }
124    }
125}
126
127/// Error type for receipt chain validation failures during replay.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum ChainError {
130    /// The chain is empty when it must not be.
131    EmptyHistory,
132    /// Cryptographic hash mismatch between the declared and computed hash.
133    HashMismatch {
134        index: usize,
135        expected: String,
136        got: String,
137    },
138    /// Receipt ID at a given index does not match the expected value.
139    ReceiptIdMismatch { index: usize, detail: String },
140    /// Chain is too short to reconstruct the target phase.
141    InsufficientHistory { required: usize, got: usize },
142    /// History contains more receipts than the protocol allows.
143    ExcessHistory { extra: usize },
144    /// JSON embedded in a receipt ID could not be parsed.
145    ParseError { index: usize, detail: String },
146}
147
148impl std::fmt::Display for ChainError {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            ChainError::EmptyHistory => write!(f, "Receipt chain is empty"),
152            ChainError::HashMismatch {
153                index,
154                expected,
155                got,
156            } => write!(
157                f,
158                "Hash mismatch at index {}: expected '{}', got '{}'",
159                index, expected, got
160            ),
161            ChainError::ReceiptIdMismatch { index, detail } => {
162                write!(f, "Receipt ID mismatch at index {}: {}", index, detail)
163            }
164            ChainError::InsufficientHistory { required, got } => write!(
165                f,
166                "Insufficient history: required {} receipts, got {}",
167                required, got
168            ),
169            ChainError::ExcessHistory { extra } => {
170                write!(f, "History contains {} unexpected extra receipts", extra)
171            }
172            ChainError::ParseError { index, detail } => {
173                write!(f, "Parse error at index {}: {}", index, detail)
174            }
175        }
176    }
177}
178
179/// Convert a `validate_and_reconstruct_chain_checked` string error into a `ChainError`.
180fn chain_err_from_str(e: String) -> ChainError {
181    if e.contains("must not be empty") || e.contains("History must not be empty") {
182        ChainError::EmptyHistory
183    } else if e.contains("Hash mismatch") {
184        let index = e
185            .split_whitespace()
186            .find_map(|w| w.trim_end_matches(':').parse::<usize>().ok())
187            .unwrap_or(0);
188        ChainError::HashMismatch {
189            index,
190            expected: String::new(),
191            got: e,
192        }
193    } else if e.contains("Insufficient history") {
194        ChainError::InsufficientHistory {
195            required: 0,
196            got: 0,
197        }
198    } else if e.contains("unexpected") {
199        ChainError::ExcessHistory { extra: 0 }
200    } else {
201        ChainError::ReceiptIdMismatch {
202            index: 0,
203            detail: e,
204        }
205    }
206}
207
208/// Enforces the Admit -> Receipt -> Exit -> Replay operational theorem stages.
209pub trait TypestateKernel<L: Law, P: Phase, D: Data> {
210    type Input;
211    type OutputPhase: Phase;
212    type OutputData: Data;
213    type Receipt;
214
215    /// Validate the input message or action against the Law.
216    fn validate(&self, input: &Self::Input) -> Result<(), L::Error>;
217
218    /// Select the next state phase depending on the input.
219    fn select(&self, input: &Self::Input) -> Self::OutputPhase;
220
221    /// Admit the input message and transition the machine into the target typestate.
222    fn admit(
223        self,
224        input: Self::Input,
225    ) -> Result<Machine<L, Self::OutputPhase, Self::OutputData>, L::Error>;
226
227    /// Produce a deterministic execution receipt containing the transition metadata.
228    fn receipt(&self) -> Self::Receipt;
229
230    /// Destroy/Exit the current phase and yield the underlying data.
231    fn exit(self) -> D;
232
233    /// Reconstruct the machine state by replaying a ledger of historic receipts.
234    /// Returns `Err(ChainError)` when the history is corrupted, malformed, or insufficient.
235    fn replay(history: Vec<Self::Receipt>) -> Result<Self, ChainError>
236    where
237        Self: Sized;
238}
239
240// ==========================================
241// Transition Definitions (Admit and Consume)
242// ==========================================
243
244mod machine;
245
246#[cfg(test)]
247mod tests;