Skip to main content

chappi_core/
focus.rs

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}