use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::ir::{Block, Chart, Exec, ExecKind, Param, Span, State, StateElement, StateKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InitialSource {
Attribute,
Element,
FirstChild,
None,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedInitial<'a> {
pub targets: Vec<&'a str>,
pub source: InitialSource,
pub body: Option<&'a Block>,
}
pub fn resolve_initial(state: &State) -> ResolvedInitial<'_> {
if state.kind() != StateKind::Compound {
return ResolvedInitial {
targets: Vec::new(),
source: InitialSource::None,
body: None,
};
}
if !state.initial.is_empty() {
return ResolvedInitial {
targets: state.initial.iter().map(String::as_str).collect(),
source: InitialSource::Attribute,
body: None,
};
}
if let Some(init) = &state.initial_element {
return ResolvedInitial {
targets: init.transition.targets.iter().map(String::as_str).collect(),
source: InitialSource::Element,
body: Some(&init.transition.body),
};
}
first_child(&state.children)
}
pub fn resolve_root_initial(chart: &Chart) -> ResolvedInitial<'_> {
if !chart.initial.is_empty() {
return ResolvedInitial {
targets: chart.initial.iter().map(String::as_str).collect(),
source: InitialSource::Attribute,
body: None,
};
}
first_child(&chart.states)
}
fn first_child(children: &[State]) -> ResolvedInitial<'_> {
match children
.iter()
.find(|c| !matches!(c.element, StateElement::History(_)))
{
Some(c) => ResolvedInitial {
targets: alloc::vec![c.id.as_str()],
source: InitialSource::FirstChild,
body: None,
},
None => ResolvedInitial {
targets: Vec::new(),
source: InitialSource::None,
body: None,
},
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NodeRef {
Root,
State {
id: String,
},
Transition {
source: String,
index: usize,
},
Initial {
state: String,
},
Data {
id: String,
},
Invoke {
state: String,
index: usize,
},
Exec {
state: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ViolationKind {
EmptyChart,
MissingId,
DuplicateStateId {
id: String,
},
UnknownTarget {
target: String,
},
IllegalInitialTarget {
target: String,
},
InitialAttributeAndElement,
InitialOnNonCompound,
IllegalInitialTransition,
HistoryTransitionCount,
IllegalHistoryTransition,
IllegalHistoryTarget {
target: String,
},
HistoryAtRoot,
HistoryContent,
FinalContent {
what: String,
},
DoneDataOutsideFinal,
ParallelHistoryOnly,
StateHistoryOnly,
IllegalTargetSet {
first: String,
second: String,
},
HistoryDefaultCycle {
history: String,
},
DuplicateDataId {
id: String,
},
ConflictingAttributes {
element: String,
attributes: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Violation {
pub kind: ViolationKind,
pub node: NodeRef,
pub span: Span,
}
impl core::fmt::Display for Violation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?} at {:?}", self.kind, self.node)
}
}
pub(crate) struct Flat<'a> {
pub states: Vec<&'a State>,
pub parent: Vec<Option<usize>>,
pub last: Vec<usize>,
pub by_id: BTreeMap<&'a str, usize>,
}
impl<'a> Flat<'a> {
pub(crate) fn new(chart: &'a Chart) -> Self {
let mut f = Flat {
states: Vec::new(),
parent: Vec::new(),
last: Vec::new(),
by_id: BTreeMap::new(),
};
for s in &chart.states {
f.push(s, None);
}
f
}
fn push(&mut self, s: &'a State, parent: Option<usize>) -> usize {
let i = self.states.len();
self.states.push(s);
self.parent.push(parent);
self.last.push(i);
self.by_id.entry(s.id.as_str()).or_insert(i);
for c in &s.children {
let l = self.push(c, Some(i));
self.last[i] = l;
}
self.last[i]
}
pub(crate) fn is_descendant(&self, d: usize, a: usize) -> bool {
a < d && d <= self.last[a]
}
}
pub fn validate(chart: &Chart) -> Result<(), Vec<Violation>> {
let flat = Flat::new(chart);
let mut v = Vec::new();
let mut push = |kind: ViolationKind, node: NodeRef, span: Span| {
v.push(Violation { kind, node, span });
};
if chart.states.is_empty() {
push(ViolationKind::EmptyChart, NodeRef::Root, chart.span);
}
let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
for s in &flat.states {
if s.id.is_empty() {
push(
ViolationKind::MissingId,
NodeRef::State { id: String::new() },
s.span,
);
continue;
}
let n = seen.entry(s.id.as_str()).or_insert(0);
*n += 1;
if *n == 2 {
push(
ViolationKind::DuplicateStateId { id: s.id.clone() },
NodeRef::State { id: s.id.clone() },
s.span,
);
}
}
for t in &chart.initial {
if !flat.by_id.contains_key(t.as_str()) {
push(
ViolationKind::UnknownTarget { target: t.clone() },
NodeRef::Root,
chart.span,
);
}
}
check_target_set(&flat, &chart.initial, NodeRef::Root, chart.span, &mut push);
for s in &chart.states {
if matches!(s.element, StateElement::History(_)) {
push(
ViolationKind::HistoryAtRoot,
NodeRef::State { id: s.id.clone() },
s.span,
);
}
}
check_history_defaults(&flat, &mut push);
let mut data_seen: BTreeMap<&str, usize> = BTreeMap::new();
let all_data = chart
.data
.iter()
.chain(flat.states.iter().flat_map(|s| s.data.iter()));
for d in all_data {
let n = data_seen.entry(d.id.as_str()).or_insert(0);
*n += 1;
if *n == 2 {
push(
ViolationKind::DuplicateDataId { id: d.id.clone() },
NodeRef::Data { id: d.id.clone() },
d.span,
);
}
let given = [d.src.is_some() || d.content.is_some(), d.expr.is_some()]
.iter()
.filter(|b| **b)
.count();
if given > 1 {
push(
conflict("data", "src, expr, content"),
NodeRef::Data { id: d.id.clone() },
d.span,
);
}
}
for (i, s) in flat.states.iter().enumerate() {
let sid = || NodeRef::State { id: s.id.clone() };
let kind = s.kind();
let has_initial = !s.initial.is_empty() || s.initial_element.is_some();
if has_initial && kind != StateKind::Compound {
push(ViolationKind::InitialOnNonCompound, sid(), s.span);
}
if !s.initial.is_empty() && s.initial_element.is_some() {
push(
ViolationKind::InitialAttributeAndElement,
NodeRef::Initial {
state: s.id.clone(),
},
s.span,
);
}
let init_node = || NodeRef::Initial {
state: s.id.clone(),
};
for t in &s.initial {
match flat.by_id.get(t.as_str()) {
None => push(
ViolationKind::UnknownTarget { target: t.clone() },
init_node(),
s.span,
),
Some(&ti) if !flat.is_descendant(ti, i) => push(
ViolationKind::IllegalInitialTarget { target: t.clone() },
init_node(),
s.span,
),
_ => {}
}
}
check_target_set(&flat, &s.initial, init_node(), s.span, &mut push);
if let Some(init) = &s.initial_element {
let tr = &init.transition;
if !tr.events.is_empty() || tr.cond.is_some() || tr.targets.is_empty() {
push(
ViolationKind::IllegalInitialTransition,
init_node(),
init.span,
);
}
for t in &tr.targets {
match flat.by_id.get(t.as_str()) {
None => push(
ViolationKind::UnknownTarget { target: t.clone() },
init_node(),
init.span,
),
Some(&ti) if !flat.is_descendant(ti, i) => push(
ViolationKind::IllegalInitialTarget { target: t.clone() },
init_node(),
init.span,
),
_ => {}
}
}
check_target_set(&flat, &tr.targets, init_node(), init.span, &mut push);
check_block(&tr.body, &s.id, &mut push);
}
for (ti, t) in s.transitions.iter().enumerate() {
let tnode = || NodeRef::Transition {
source: s.id.clone(),
index: ti,
};
for target in &t.targets {
if !flat.by_id.contains_key(target.as_str()) {
push(
ViolationKind::UnknownTarget {
target: target.clone(),
},
tnode(),
t.span,
);
}
}
check_target_set(&flat, &t.targets, tnode(), t.span, &mut push);
check_block(&t.body, &s.id, &mut push);
}
match kind {
StateKind::History(_) => {
if s.transitions.len() > 1 {
push(ViolationKind::HistoryTransitionCount, sid(), s.span);
}
if let Some(t) = s.transitions.first() {
if !t.events.is_empty() || t.cond.is_some() || t.targets.is_empty() {
push(
ViolationKind::IllegalHistoryTransition,
NodeRef::Transition {
source: s.id.clone(),
index: 0,
},
t.span,
);
}
if let Some(p) = flat.parent[i] {
for target in &t.targets {
if let Some(&ti) = flat.by_id.get(target.as_str())
&& !flat.is_descendant(ti, p)
{
push(
ViolationKind::IllegalHistoryTarget {
target: target.clone(),
},
NodeRef::Transition {
source: s.id.clone(),
index: 0,
},
t.span,
);
}
}
}
}
if !s.children.is_empty()
|| !s.onentry.is_empty()
|| !s.onexit.is_empty()
|| !s.invokes.is_empty()
|| !s.data.is_empty()
{
push(ViolationKind::HistoryContent, sid(), s.span);
}
}
StateKind::Final => {
let mut what = Vec::new();
if !s.children.is_empty() {
what.push("child states");
}
if !s.transitions.is_empty() {
what.push("transitions");
}
if !s.invokes.is_empty() {
what.push("invokes");
}
if !s.data.is_empty() {
what.push("data");
}
if !what.is_empty() {
push(
ViolationKind::FinalContent {
what: what.join(", "),
},
sid(),
s.span,
);
}
}
StateKind::Parallel | StateKind::Compound
if !s.children.is_empty()
&& s.children
.iter()
.all(|c| matches!(c.element, StateElement::History(_))) =>
{
let k = if kind == StateKind::Parallel {
ViolationKind::ParallelHistoryOnly
} else {
ViolationKind::StateHistoryOnly
};
push(k, sid(), s.span);
}
_ => {}
}
if s.donedata.is_some() && kind != StateKind::Final {
push(ViolationKind::DoneDataOutsideFinal, sid(), s.span);
}
if let Some(dd) = &s.donedata {
if dd.content.is_some() && !dd.params.is_empty() {
push(conflict("donedata", "content, param"), sid(), dd.span);
}
check_params(&dd.params, &s.id, &mut push);
}
for b in s.onentry.iter().chain(s.onexit.iter()) {
check_block(b, &s.id, &mut push);
}
for (ii, inv) in s.invokes.iter().enumerate() {
let inode = || NodeRef::Invoke {
state: s.id.clone(),
index: ii,
};
let pairs = [
(
inv.type_.is_some() && inv.typeexpr.is_some(),
"type, typeexpr",
),
(inv.src.is_some() && inv.srcexpr.is_some(), "src, srcexpr"),
(
inv.id.is_some() && inv.idlocation.is_some(),
"id, idlocation",
),
(
inv.content.as_ref().is_some_and(|c| {
inv.srcexpr.is_some() || (inv.src.is_some() && c.expr.is_some())
}),
"content, src",
),
(
!inv.params.is_empty() && !inv.namelist.is_empty(),
"param, namelist",
),
];
for (bad, attrs) in pairs {
if bad {
push(conflict("invoke", attrs), inode(), inv.span);
}
}
check_params(&inv.params, &s.id, &mut push);
check_block(&inv.finalize, &s.id, &mut push);
}
}
if v.is_empty() { Ok(()) } else { Err(v) }
}
fn conflict(element: &str, attributes: &str) -> ViolationKind {
ViolationKind::ConflictingAttributes {
element: element.to_string(),
attributes: attributes.to_string(),
}
}
fn history_default_edges(flat: &Flat<'_>, h: usize) -> Vec<usize> {
let st = flat.states[h];
let targets: Vec<&str> = match (st.transitions.first(), flat.parent[h]) {
(Some(t), _) => t.targets.iter().map(String::as_str).collect(),
(None, Some(p)) => resolve_initial(flat.states[p]).targets,
(None, None) => Vec::new(),
};
targets
.iter()
.filter_map(|t| flat.by_id.get(t).copied())
.filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
.collect()
}
fn check_history_defaults(flat: &Flat<'_>, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
let histories: Vec<usize> = (0..flat.states.len())
.filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
.collect();
for &h in &histories {
let mut seen = alloc::vec![false; flat.states.len()];
let mut stack = history_default_edges(flat, h);
let mut cyclic = false;
while let Some(x) = stack.pop() {
if x == h {
cyclic = true;
break;
}
if !core::mem::replace(&mut seen[x], true) {
stack.extend(history_default_edges(flat, x));
}
}
if cyclic {
let id = flat.states[h].id.clone();
push(
ViolationKind::HistoryDefaultCycle {
history: id.clone(),
},
NodeRef::State { id },
flat.states[h].span,
);
}
}
}
fn check_target_set(
flat: &Flat<'_>,
targets: &[String],
node: NodeRef,
span: Span,
push: &mut impl FnMut(ViolationKind, NodeRef, Span),
) {
let idx: Vec<(usize, &String)> = targets
.iter()
.filter_map(|t| flat.by_id.get(t.as_str()).map(|i| (*i, t)))
.filter_map(|(i, t)| {
if matches!(flat.states[i].element, StateElement::History(_)) {
flat.parent[i].map(|p| (p, t))
} else {
Some((i, t))
}
})
.collect();
for a in 0..idx.len() {
for b in (a + 1)..idx.len() {
let (x, xs) = idx[a];
let (y, ys) = idx[b];
let ok = if x == y || flat.is_descendant(x, y) || flat.is_descendant(y, x) {
false
} else {
let mut anc = flat.parent[x];
loop {
match anc {
Some(p) if flat.is_descendant(y, p) => {
break flat.states[p].kind() == StateKind::Parallel;
}
Some(p) => anc = flat.parent[p],
None => break false,
}
}
};
if !ok {
push(
ViolationKind::IllegalTargetSet {
first: xs.clone(),
second: ys.clone(),
},
node.clone(),
span,
);
}
}
}
}
fn check_params(
params: &[Param],
state: &str,
push: &mut impl FnMut(ViolationKind, NodeRef, Span),
) {
for p in params {
if p.expr.is_some() == p.location.is_some() {
push(
conflict("param", "expr, location"),
NodeRef::Exec {
state: state.to_string(),
},
p.span,
);
}
}
}
fn check_block(block: &Block, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
for e in block {
check_exec(e, state, push);
}
}
fn check_exec(e: &Exec, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
let node = || NodeRef::Exec {
state: state.to_string(),
};
match &e.kind {
ExecKind::Send(s) => {
let pairs = [
(
s.event.is_some() && s.eventexpr.is_some(),
"event, eventexpr",
),
(
s.target.is_some() && s.targetexpr.is_some(),
"target, targetexpr",
),
(s.type_.is_some() && s.typeexpr.is_some(), "type, typeexpr"),
(s.id.is_some() && s.idlocation.is_some(), "id, idlocation"),
(
s.delay.is_some() && s.delayexpr.is_some(),
"delay, delayexpr",
),
(
s.content.is_some() && (!s.params.is_empty() || !s.namelist.is_empty()),
"content, param, namelist",
),
];
for (bad, attrs) in pairs {
if bad {
push(conflict("send", attrs), node(), e.span);
}
}
check_params(&s.params, state, push);
}
ExecKind::Cancel { sendid, sendidexpr } if sendid.is_some() == sendidexpr.is_some() => {
push(conflict("cancel", "sendid, sendidexpr"), node(), e.span);
}
ExecKind::Assign { expr, content, .. } if expr.is_some() && content.is_some() => {
push(conflict("assign", "expr, content"), node(), e.span);
}
ExecKind::If { branches } => {
for (bi, br) in branches.iter().enumerate() {
let is_else = br.cond.is_none();
if is_else && bi + 1 != branches.len() {
push(
conflict("if", &format!("else before branch {}", bi + 1)),
node(),
br.span,
);
}
if bi == 0 && is_else {
push(conflict("if", "cond"), node(), br.span);
}
check_block(&br.body, state, push);
}
}
ExecKind::Foreach { body, item, .. } => {
if item.is_empty() {
push(conflict("foreach", "item"), node(), e.span);
}
check_block(body, state, push);
}
_ => {}
}
}