1use super::ast::{AstLayer, SttpAst};
2use super::lexer::{Token, TokenKind, tokenize};
3use super::lexicon::{LAYER_ORDER, LAYER_STOP_MARKER, LayerKind};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ParserState {
7 Start,
8 InProvenance,
9 InEnvelope,
10 InContent,
11 InMetrics,
12 Done,
13 Error,
14}
15
16#[derive(Debug, Clone)]
17pub struct LayerParseOutcome<'a> {
18 pub provenance: Option<&'a str>,
19 pub provenance_span: Option<super::lexer::Span>,
20 pub envelope: Option<&'a str>,
21 pub envelope_span: Option<super::lexer::Span>,
22 pub content: Option<&'a str>,
23 pub content_span: Option<super::lexer::Span>,
24 pub metrics: Option<&'a str>,
25 pub metrics_span: Option<super::lexer::Span>,
26 pub strict_spine: bool,
27 pub state: ParserState,
28 pub diagnostics: Vec<String>,
29}
30
31pub struct SttpLayerStateMachine;
32
33impl SttpLayerStateMachine {
34 pub fn parse<'a>(raw: &'a str) -> LayerParseOutcome<'a> {
35 let mut diagnostics = Vec::new();
36 let tokens = tokenize(raw);
37
38 let provenance = extract_layer(raw, &tokens, LayerKind::Provenance);
39 let envelope = extract_layer(raw, &tokens, LayerKind::Envelope);
40 let content = extract_layer(raw, &tokens, LayerKind::Content);
41 let metrics = extract_layer(raw, &tokens, LayerKind::Metrics);
42
43 let provenance_span = provenance.as_ref().map(|v| v.span);
44 let envelope_span = envelope.as_ref().map(|v| v.span);
45 let content_span = content.as_ref().map(|v| v.span);
46 let metrics_span = metrics.as_ref().map(|v| v.span);
47
48 let provenance = provenance.as_ref().map(|v| v.slice);
49 let envelope = envelope.as_ref().map(|v| v.slice);
50 let content = content.as_ref().map(|v| v.slice);
51 let metrics = metrics.as_ref().map(|v| v.slice);
52
53 let strict_spine = is_strict_spine(raw);
54 if !strict_spine {
55 diagnostics.push("non_strict_spine_recovered_tolerantly".to_string());
56 }
57
58 if provenance.is_none() {
59 diagnostics.push("missing_layer_provenance".to_string());
60 }
61 if envelope.is_none() {
62 diagnostics.push("missing_layer_envelope".to_string());
63 }
64 if content.is_none() {
65 diagnostics.push("missing_layer_content".to_string());
66 }
67 if metrics.is_none() {
68 diagnostics.push("missing_layer_metrics".to_string());
69 }
70
71 let state = if metrics.is_some() {
72 ParserState::Done
73 } else if content.is_some() {
74 ParserState::InContent
75 } else if envelope.is_some() {
76 ParserState::InEnvelope
77 } else if provenance.is_some() {
78 ParserState::InProvenance
79 } else {
80 ParserState::Error
81 };
82
83 LayerParseOutcome {
84 provenance,
85 provenance_span,
86 envelope,
87 envelope_span,
88 content,
89 content_span,
90 metrics,
91 metrics_span,
92 strict_spine,
93 state,
94 diagnostics,
95 }
96 }
97
98 pub fn to_ast(raw: &str, parsed: &LayerParseOutcome<'_>) -> SttpAst {
99 let make_layer = |kind: LayerKind, source: Option<&str>| {
100 source.and_then(|slice| {
101 let start = raw.find(slice)?;
102 let end = start + slice.len();
103 Some(AstLayer {
104 kind,
105 source: slice.to_string(),
106 start,
107 end,
108 })
109 })
110 };
111
112 SttpAst {
113 provenance: make_layer(LayerKind::Provenance, parsed.provenance),
114 envelope: make_layer(LayerKind::Envelope, parsed.envelope),
115 content: make_layer(LayerKind::Content, parsed.content),
116 metrics: make_layer(LayerKind::Metrics, parsed.metrics),
117 strict_spine: parsed.strict_spine,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy)]
123struct LayerSlice<'a> {
124 slice: &'a str,
125 span: super::lexer::Span,
126}
127
128fn is_strict_spine(raw: &str) -> bool {
129 let mut last = 0usize;
130 for kind in LAYER_ORDER {
131 let marker = kind.marker();
132 let Some(relative) = raw[last..].find(marker) else {
133 return false;
134 };
135 last += relative + marker.len();
136 }
137
138 true
139}
140
141fn extract_layer<'a>(raw: &'a str, tokens: &[Token], kind: LayerKind) -> Option<LayerSlice<'a>> {
142 let layer_start_kind = layer_start_token_kind(kind);
143 let start_token_index = tokens.iter().position(|t| t.kind == layer_start_kind)?;
144 let start_token = tokens[start_token_index];
145 let start = tokens[start_token_index].span.start;
146
147 let end = tokens
148 .iter()
149 .skip(start_token_index + 1)
150 .find(|t| t.kind == TokenKind::LayerEnd)
151 .map(|t| t.span.end)
152 .or_else(|| {
153 let value_start = start + kind.marker().len();
154 raw[value_start..]
155 .find(LAYER_STOP_MARKER)
156 .map(|relative| value_start + relative + LAYER_STOP_MARKER.len())
157 })?;
158
159 let slice = raw.get(start..end)?;
160 Some(LayerSlice {
161 slice,
162 span: super::lexer::Span {
163 start,
164 end,
165 line: start_token.span.line,
166 column: start_token.span.column,
167 },
168 })
169}
170
171fn layer_start_token_kind(kind: LayerKind) -> TokenKind {
172 match kind {
173 LayerKind::Provenance => TokenKind::ProvenanceStart,
174 LayerKind::Envelope => TokenKind::EnvelopeStart,
175 LayerKind::Content => TokenKind::ContentStart,
176 LayerKind::Metrics => TokenKind::MetricsStart,
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn should_detect_strict_spine() {
186 let raw = r#"
187⊕⟨ { trigger: manual } ⟩
188⦿⟨ { timestamp: "2026-03-05T06:30:00Z", tier: raw } ⟩
189◈⟨ { a(.99): b } ⟩
190⍉⟨ { rho: 0.1, kappa: 0.2, psi: 0.3 } ⟩
191"#;
192
193 let parsed = SttpLayerStateMachine::parse(raw);
194 assert!(parsed.strict_spine);
195 assert_eq!(parsed.state, ParserState::Done);
196 assert!(parsed.provenance.is_some());
197 assert!(parsed.envelope.is_some());
198 assert!(parsed.content.is_some());
199 assert!(parsed.metrics.is_some());
200 }
201
202 #[test]
203 fn should_recover_with_missing_layer() {
204 let raw = r#"
205⊕⟨ { trigger: manual } ⟩
206⦿⟨ { timestamp: "2026-03-05T06:30:00Z", tier: raw } ⟩
207⍉⟨ { rho: 0.1, kappa: 0.2, psi: 0.3 } ⟩
208"#;
209
210 let parsed = SttpLayerStateMachine::parse(raw);
211 assert!(!parsed.strict_spine);
212 assert_eq!(parsed.state, ParserState::Done);
213 assert!(parsed.content.is_none());
214 assert!(parsed.metrics.is_some());
215 assert!(parsed.diagnostics.len() >= 2);
216 assert!(
217 parsed
218 .diagnostics
219 .iter()
220 .any(|d| d == "missing_layer_content")
221 );
222 }
223
224 #[test]
225 fn should_project_ast_with_spans() {
226 let raw = r#"
227⊕⟨ { trigger: manual } ⟩
228⦿⟨ { timestamp: "2026-03-05T06:30:00Z", tier: raw } ⟩
229◈⟨ { topic(.99): x } ⟩
230⍉⟨ { rho: 0.1, kappa: 0.2, psi: 0.3 } ⟩
231"#;
232
233 let parsed = SttpLayerStateMachine::parse(raw);
234 let ast = SttpLayerStateMachine::to_ast(raw, &parsed);
235
236 assert!(ast.provenance.is_some());
237 assert!(ast.envelope.is_some());
238 assert!(ast.content.is_some());
239 assert!(ast.metrics.is_some());
240
241 let content = ast.content.expect("content layer should exist");
242 assert!(content.start < content.end);
243 assert!(content.source.contains("topic(.99)"));
244 }
245}