use std::collections::{HashMap, HashSet};
use chappi_syntax::{Atom, Diagnostic, Form, FormKind, Span};
use crate::diagnostic::error;
use crate::model::forms_semantically_equal;
use crate::{CognitiveState, Node, NodeKind};
type FocusDecodeResult<T> = Result<T, Box<Diagnostic>>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FocusStream {
pub id: String,
pub frames: Vec<FocusFrame>,
}
#[derive(Clone, Debug)]
pub struct FocusFrame {
pub stream_id: String,
pub sequence: u64,
pub owner: String,
pub transition: TransitionKind,
pub previous_sequence: Option<u64>,
pub foreground: Vec<String>,
pub background: Vec<String>,
pub residue: Vec<String>,
pub trigger: Form,
pub span: Span,
}
impl PartialEq for FocusFrame {
fn eq(&self, other: &Self) -> bool {
self.stream_id == other.stream_id
&& self.sequence == other.sequence
&& self.owner == other.owner
&& self.transition == other.transition
&& self.previous_sequence == other.previous_sequence
&& bucket_eq(&self.foreground, &other.foreground)
&& bucket_eq(&self.background, &other.background)
&& bucket_eq(&self.residue, &other.residue)
&& forms_semantically_equal(&self.trigger, &other.trigger)
}
}
impl Eq for FocusFrame {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransitionKind {
Evoked,
Associated,
Interrupted,
Returned,
Reframed,
Resolved,
}
impl TransitionKind {
pub(crate) fn decode(value: &str) -> Option<Self> {
match value {
"evoked" => Some(Self::Evoked),
"associated" => Some(Self::Associated),
"interrupted" => Some(Self::Interrupted),
"returned" => Some(Self::Returned),
"reframed" => Some(Self::Reframed),
"resolved" => Some(Self::Resolved),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Evoked => "evoked",
Self::Associated => "associated",
Self::Interrupted => "interrupted",
Self::Returned => "returned",
Self::Reframed => "reframed",
Self::Resolved => "resolved",
}
}
}
pub(crate) fn decode_focus(items: &[Form], span: Span) -> FocusDecodeResult<FocusFrame> {
if items.len() != 10 {
return Err(error(
"invalid-focus-arity",
span,
vec!["focus".into()],
"a focus frame must contain stream, sequence, owner, transition, previous sequence, fg, bg, residue, and trigger",
None,
)
.into());
}
let stream_id = required_symbol(&items[1], "stream ID", &["focus", "stream"])?;
let sequence = required_u64(&items[2], "sequence", &["focus", &stream_id, "sequence"])?;
let owner = required_symbol(
&items[3],
"owner entity ID",
&["focus", &stream_id, "owner"],
)?;
let transition_text = required_symbol(
&items[4],
"transition",
&["focus", &stream_id, "transition"],
)?;
let Some(transition) = TransitionKind::decode(&transition_text) else {
return Err(error(
"invalid-focus-transition",
items[4].span,
vec!["focus".into(), stream_id.clone(), "transition".into()],
"transition must be `evoked`, `associated`, `interrupted`, `returned`, `reframed`, or `resolved`",
None,
)
.into());
};
let previous_sequence = match symbol(&items[5]) {
Some("none") => None,
_ => Some(required_u64(
&items[5],
"previous sequence",
&["focus", &stream_id, "previous_sequence"],
)?),
};
Ok(FocusFrame {
stream_id: stream_id.clone(),
sequence,
owner,
transition,
previous_sequence,
foreground: decode_bucket(&items[6], "fg", &stream_id)?,
background: decode_bucket(&items[7], "bg", &stream_id)?,
residue: decode_bucket(&items[8], "residue", &stream_id)?,
trigger: items[9].clone(),
span,
})
}
fn decode_bucket(
form: &Form,
expected_head: &str,
stream_id: &str,
) -> FocusDecodeResult<Vec<String>> {
let FormKind::List(items) = &form.kind else {
return Err(invalid_bucket(form, expected_head, stream_id));
};
if items.first().and_then(symbol) != Some(expected_head) {
return Err(invalid_bucket(form, expected_head, stream_id));
}
let mut ids = items[1..]
.iter()
.map(|item| {
required_symbol(
item,
"focus node reference",
&["focus", stream_id, expected_head],
)
})
.collect::<Result<Vec<_>, _>>()?;
ids.sort();
Ok(ids)
}
fn bucket_eq(left: &[String], right: &[String]) -> bool {
let mut left = left.iter().map(String::as_str).collect::<Vec<_>>();
let mut right = right.iter().map(String::as_str).collect::<Vec<_>>();
left.sort_unstable();
right.sort_unstable();
left == right
}
fn invalid_bucket(form: &Form, expected_head: &str, stream_id: &str) -> Box<Diagnostic> {
error(
"invalid-focus-bucket",
form.span,
vec!["focus".into(), stream_id.into(), expected_head.into()],
format!("focus bucket must be `({expected_head} <node-id>...)`"),
None,
)
.into()
}
fn required_symbol(form: &Form, name: &str, path: &[&str]) -> FocusDecodeResult<String> {
symbol(form).map(ToOwned::to_owned).ok_or_else(|| {
error(
"expected-symbol",
form.span,
path.iter().map(ToString::to_string).collect(),
format!("{name} must be a symbol"),
None,
)
.into()
})
}
fn required_u64(form: &Form, name: &str, path: &[&str]) -> FocusDecodeResult<u64> {
let FormKind::Atom(Atom::Integer(value)) = &form.kind else {
return Err(invalid_integer(form, name, path));
};
value.parse().map_err(|_| invalid_integer(form, name, path))
}
fn invalid_integer(form: &Form, name: &str, path: &[&str]) -> Box<Diagnostic> {
error(
"invalid-focus-sequence-number",
form.span,
path.iter().map(ToString::to_string).collect(),
format!("{name} must be a non-negative 64-bit integer"),
None,
)
.into()
}
fn symbol(form: &Form) -> Option<&str> {
match &form.kind {
FormKind::Atom(Atom::Symbol(value)) => Some(value),
_ => None,
}
}
pub(crate) fn validate_focus_streams(
state: &CognitiveState,
nodes_by_id: &HashMap<&str, &Node>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut seen_streams = HashSet::new();
for stream in &state.focus_streams {
if stream.frames.is_empty() {
diagnostics.push(error(
"empty-focus-stream",
empty_span(),
vec!["focus".into(), stream.id.clone()],
format!(
"focus stream `{}` must contain at least one frame",
stream.id
),
None,
));
}
if !seen_streams.insert(stream.id.as_str()) {
let span = stream
.frames
.first()
.map_or_else(empty_span, |frame| frame.span);
diagnostics.push(error(
"duplicate-focus-stream",
span,
vec!["focus".into(), stream.id.clone()],
format!("focus stream `{}` is defined more than once", stream.id),
None,
));
}
for (index, frame) in stream.frames.iter().enumerate() {
if frame.stream_id != stream.id {
diagnostics.push(error(
"focus-stream-mismatch",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
],
format!(
"frame names stream `{}` but is stored in stream `{}`",
frame.stream_id, stream.id
),
None,
));
}
let expected = index as u64;
if index == 0 {
if frame.sequence != 0
|| frame.transition != TransitionKind::Evoked
|| frame.previous_sequence.is_some()
{
diagnostics.push(error(
"invalid-focus-start",
frame.span,
vec!["focus".into(), stream.id.clone(), frame.sequence.to_string()],
"the first frame must have sequence 0, transition `evoked`, and previous `none`",
None,
));
}
} else if frame.sequence != expected || frame.previous_sequence != Some(expected - 1) {
diagnostics.push(error(
"invalid-focus-sequence",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
],
format!(
"focus frame must have sequence {expected} and name previous sequence {}",
expected - 1
),
None,
));
}
match nodes_by_id.get(frame.owner.as_str()) {
None => diagnostics.push(error(
"undefined-focus-owner",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
"owner".into(),
],
format!(
"focus owner `{}` does not name an existing entity node",
frame.owner
),
None,
)),
Some(node) if !matches!(node.kind, NodeKind::Entity { .. }) => {
diagnostics.push(error(
"invalid-focus-owner-kind",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
"owner".into(),
],
format!("focus owner `{}` names a non-entity node", frame.owner),
None,
))
}
Some(_) => {}
}
let mut all_seen = HashSet::new();
for (bucket_name, bucket) in [
("fg", &frame.foreground),
("bg", &frame.background),
("residue", &frame.residue),
] {
let mut bucket_seen = HashSet::new();
for reference in bucket {
if !bucket_seen.insert(reference.as_str()) {
diagnostics.push(error(
"duplicate-focus-reference",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
bucket_name.into(),
],
format!(
"focus bucket `{bucket_name}` contains `{reference}` more than once"
),
None,
));
continue;
}
if !all_seen.insert(reference.as_str()) {
diagnostics.push(error(
"overlapping-focus-reference",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
bucket_name.into(),
],
format!("node `{reference}` occurs in more than one focus bucket"),
None,
));
}
if !nodes_by_id.contains_key(reference.as_str()) {
diagnostics.push(error(
"undefined-focus-reference",
frame.span,
vec![
"focus".into(),
stream.id.clone(),
frame.sequence.to_string(),
bucket_name.into(),
],
format!("focus reference `{reference}` does not name an existing node"),
None,
));
}
}
}
}
}
}
fn empty_span() -> Span {
Span {
start: 0,
end: 0,
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 1,
}
}