1use std::collections::{HashMap, HashSet};
2
3use chappi_syntax::{Atom, Diagnostic, Form, FormKind, Span};
4
5use crate::diagnostic::error;
6use crate::model::forms_semantically_equal;
7use crate::{CognitiveState, Node, NodeKind};
8
9type FocusDecodeResult<T> = Result<T, Box<Diagnostic>>;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct FocusStream {
13 pub id: String,
14 pub frames: Vec<FocusFrame>,
15}
16
17#[derive(Clone, Debug)]
18pub struct FocusFrame {
19 pub stream_id: String,
20 pub sequence: u64,
21 pub owner: String,
22 pub transition: TransitionKind,
23 pub previous_sequence: Option<u64>,
24 pub foreground: Vec<String>,
25 pub background: Vec<String>,
26 pub residue: Vec<String>,
27 pub trigger: Form,
28 pub span: Span,
29}
30
31impl PartialEq for FocusFrame {
32 fn eq(&self, other: &Self) -> bool {
33 self.stream_id == other.stream_id
34 && self.sequence == other.sequence
35 && self.owner == other.owner
36 && self.transition == other.transition
37 && self.previous_sequence == other.previous_sequence
38 && bucket_eq(&self.foreground, &other.foreground)
39 && bucket_eq(&self.background, &other.background)
40 && bucket_eq(&self.residue, &other.residue)
41 && forms_semantically_equal(&self.trigger, &other.trigger)
42 }
43}
44
45impl Eq for FocusFrame {}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub enum TransitionKind {
49 Evoked,
50 Associated,
51 Interrupted,
52 Returned,
53 Reframed,
54 Resolved,
55}
56
57impl TransitionKind {
58 pub(crate) fn decode(value: &str) -> Option<Self> {
59 match value {
60 "evoked" => Some(Self::Evoked),
61 "associated" => Some(Self::Associated),
62 "interrupted" => Some(Self::Interrupted),
63 "returned" => Some(Self::Returned),
64 "reframed" => Some(Self::Reframed),
65 "resolved" => Some(Self::Resolved),
66 _ => None,
67 }
68 }
69
70 pub fn as_str(self) -> &'static str {
71 match self {
72 Self::Evoked => "evoked",
73 Self::Associated => "associated",
74 Self::Interrupted => "interrupted",
75 Self::Returned => "returned",
76 Self::Reframed => "reframed",
77 Self::Resolved => "resolved",
78 }
79 }
80}
81
82pub(crate) fn decode_focus(items: &[Form], span: Span) -> FocusDecodeResult<FocusFrame> {
83 if items.len() != 10 {
84 return Err(error(
85 "invalid-focus-arity",
86 span,
87 vec!["focus".into()],
88 "a focus frame must contain stream, sequence, owner, transition, previous sequence, fg, bg, residue, and trigger",
89 None,
90 )
91 .into());
92 }
93
94 let stream_id = required_symbol(&items[1], "stream ID", &["focus", "stream"])?;
95 let sequence = required_u64(&items[2], "sequence", &["focus", &stream_id, "sequence"])?;
96 let owner = required_symbol(
97 &items[3],
98 "owner entity ID",
99 &["focus", &stream_id, "owner"],
100 )?;
101 let transition_text = required_symbol(
102 &items[4],
103 "transition",
104 &["focus", &stream_id, "transition"],
105 )?;
106 let Some(transition) = TransitionKind::decode(&transition_text) else {
107 return Err(error(
108 "invalid-focus-transition",
109 items[4].span,
110 vec!["focus".into(), stream_id.clone(), "transition".into()],
111 "transition must be `evoked`, `associated`, `interrupted`, `returned`, `reframed`, or `resolved`",
112 None,
113 )
114 .into());
115 };
116 let previous_sequence = match symbol(&items[5]) {
117 Some("none") => None,
118 _ => Some(required_u64(
119 &items[5],
120 "previous sequence",
121 &["focus", &stream_id, "previous_sequence"],
122 )?),
123 };
124
125 Ok(FocusFrame {
126 stream_id: stream_id.clone(),
127 sequence,
128 owner,
129 transition,
130 previous_sequence,
131 foreground: decode_bucket(&items[6], "fg", &stream_id)?,
132 background: decode_bucket(&items[7], "bg", &stream_id)?,
133 residue: decode_bucket(&items[8], "residue", &stream_id)?,
134 trigger: items[9].clone(),
135 span,
136 })
137}
138
139fn decode_bucket(
140 form: &Form,
141 expected_head: &str,
142 stream_id: &str,
143) -> FocusDecodeResult<Vec<String>> {
144 let FormKind::List(items) = &form.kind else {
145 return Err(invalid_bucket(form, expected_head, stream_id));
146 };
147 if items.first().and_then(symbol) != Some(expected_head) {
148 return Err(invalid_bucket(form, expected_head, stream_id));
149 }
150 let mut ids = items[1..]
151 .iter()
152 .map(|item| {
153 required_symbol(
154 item,
155 "focus node reference",
156 &["focus", stream_id, expected_head],
157 )
158 })
159 .collect::<Result<Vec<_>, _>>()?;
160 ids.sort();
161 Ok(ids)
162}
163
164fn bucket_eq(left: &[String], right: &[String]) -> bool {
165 let mut left = left.iter().map(String::as_str).collect::<Vec<_>>();
166 let mut right = right.iter().map(String::as_str).collect::<Vec<_>>();
167 left.sort_unstable();
168 right.sort_unstable();
169 left == right
170}
171
172fn invalid_bucket(form: &Form, expected_head: &str, stream_id: &str) -> Box<Diagnostic> {
173 error(
174 "invalid-focus-bucket",
175 form.span,
176 vec!["focus".into(), stream_id.into(), expected_head.into()],
177 format!("focus bucket must be `({expected_head} <node-id>...)`"),
178 None,
179 )
180 .into()
181}
182
183fn required_symbol(form: &Form, name: &str, path: &[&str]) -> FocusDecodeResult<String> {
184 symbol(form).map(ToOwned::to_owned).ok_or_else(|| {
185 error(
186 "expected-symbol",
187 form.span,
188 path.iter().map(ToString::to_string).collect(),
189 format!("{name} must be a symbol"),
190 None,
191 )
192 .into()
193 })
194}
195
196fn required_u64(form: &Form, name: &str, path: &[&str]) -> FocusDecodeResult<u64> {
197 let FormKind::Atom(Atom::Integer(value)) = &form.kind else {
198 return Err(invalid_integer(form, name, path));
199 };
200 value.parse().map_err(|_| invalid_integer(form, name, path))
201}
202
203fn invalid_integer(form: &Form, name: &str, path: &[&str]) -> Box<Diagnostic> {
204 error(
205 "invalid-focus-sequence-number",
206 form.span,
207 path.iter().map(ToString::to_string).collect(),
208 format!("{name} must be a non-negative 64-bit integer"),
209 None,
210 )
211 .into()
212}
213
214fn symbol(form: &Form) -> Option<&str> {
215 match &form.kind {
216 FormKind::Atom(Atom::Symbol(value)) => Some(value),
217 _ => None,
218 }
219}
220
221pub(crate) fn validate_focus_streams(
222 state: &CognitiveState,
223 nodes_by_id: &HashMap<&str, &Node>,
224 diagnostics: &mut Vec<Diagnostic>,
225) {
226 let mut seen_streams = HashSet::new();
227 for stream in &state.focus_streams {
228 if stream.frames.is_empty() {
229 diagnostics.push(error(
230 "empty-focus-stream",
231 empty_span(),
232 vec!["focus".into(), stream.id.clone()],
233 format!(
234 "focus stream `{}` must contain at least one frame",
235 stream.id
236 ),
237 None,
238 ));
239 }
240 if !seen_streams.insert(stream.id.as_str()) {
241 let span = stream
242 .frames
243 .first()
244 .map_or_else(empty_span, |frame| frame.span);
245 diagnostics.push(error(
246 "duplicate-focus-stream",
247 span,
248 vec!["focus".into(), stream.id.clone()],
249 format!("focus stream `{}` is defined more than once", stream.id),
250 None,
251 ));
252 }
253 for (index, frame) in stream.frames.iter().enumerate() {
254 if frame.stream_id != stream.id {
255 diagnostics.push(error(
256 "focus-stream-mismatch",
257 frame.span,
258 vec![
259 "focus".into(),
260 stream.id.clone(),
261 frame.sequence.to_string(),
262 ],
263 format!(
264 "frame names stream `{}` but is stored in stream `{}`",
265 frame.stream_id, stream.id
266 ),
267 None,
268 ));
269 }
270 let expected = index as u64;
271 if index == 0 {
272 if frame.sequence != 0
273 || frame.transition != TransitionKind::Evoked
274 || frame.previous_sequence.is_some()
275 {
276 diagnostics.push(error(
277 "invalid-focus-start",
278 frame.span,
279 vec!["focus".into(), stream.id.clone(), frame.sequence.to_string()],
280 "the first frame must have sequence 0, transition `evoked`, and previous `none`",
281 None,
282 ));
283 }
284 } else if frame.sequence != expected || frame.previous_sequence != Some(expected - 1) {
285 diagnostics.push(error(
286 "invalid-focus-sequence",
287 frame.span,
288 vec![
289 "focus".into(),
290 stream.id.clone(),
291 frame.sequence.to_string(),
292 ],
293 format!(
294 "focus frame must have sequence {expected} and name previous sequence {}",
295 expected - 1
296 ),
297 None,
298 ));
299 }
300
301 match nodes_by_id.get(frame.owner.as_str()) {
302 None => diagnostics.push(error(
303 "undefined-focus-owner",
304 frame.span,
305 vec![
306 "focus".into(),
307 stream.id.clone(),
308 frame.sequence.to_string(),
309 "owner".into(),
310 ],
311 format!(
312 "focus owner `{}` does not name an existing entity node",
313 frame.owner
314 ),
315 None,
316 )),
317 Some(node) if !matches!(node.kind, NodeKind::Entity { .. }) => {
318 diagnostics.push(error(
319 "invalid-focus-owner-kind",
320 frame.span,
321 vec![
322 "focus".into(),
323 stream.id.clone(),
324 frame.sequence.to_string(),
325 "owner".into(),
326 ],
327 format!("focus owner `{}` names a non-entity node", frame.owner),
328 None,
329 ))
330 }
331 Some(_) => {}
332 }
333
334 let mut all_seen = HashSet::new();
335 for (bucket_name, bucket) in [
336 ("fg", &frame.foreground),
337 ("bg", &frame.background),
338 ("residue", &frame.residue),
339 ] {
340 let mut bucket_seen = HashSet::new();
341 for reference in bucket {
342 if !bucket_seen.insert(reference.as_str()) {
343 diagnostics.push(error(
344 "duplicate-focus-reference",
345 frame.span,
346 vec![
347 "focus".into(),
348 stream.id.clone(),
349 frame.sequence.to_string(),
350 bucket_name.into(),
351 ],
352 format!(
353 "focus bucket `{bucket_name}` contains `{reference}` more than once"
354 ),
355 None,
356 ));
357 continue;
358 }
359 if !all_seen.insert(reference.as_str()) {
360 diagnostics.push(error(
361 "overlapping-focus-reference",
362 frame.span,
363 vec![
364 "focus".into(),
365 stream.id.clone(),
366 frame.sequence.to_string(),
367 bucket_name.into(),
368 ],
369 format!("node `{reference}` occurs in more than one focus bucket"),
370 None,
371 ));
372 }
373 if !nodes_by_id.contains_key(reference.as_str()) {
374 diagnostics.push(error(
375 "undefined-focus-reference",
376 frame.span,
377 vec![
378 "focus".into(),
379 stream.id.clone(),
380 frame.sequence.to_string(),
381 bucket_name.into(),
382 ],
383 format!("focus reference `{reference}` does not name an existing node"),
384 None,
385 ));
386 }
387 }
388 }
389 }
390 }
391}
392
393fn empty_span() -> Span {
394 Span {
395 start: 0,
396 end: 0,
397 start_line: 1,
398 start_column: 1,
399 end_line: 1,
400 end_column: 1,
401 }
402}