1use alloc::collections::BTreeMap;
7use alloc::format;
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11use crate::ir::{Block, Chart, Exec, ExecKind, Param, Span, State, StateElement, StateKind};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum InitialSource {
16 Attribute,
18 Element,
20 FirstChild,
22 None,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct ResolvedInitial<'a> {
29 pub targets: Vec<&'a str>,
31 pub source: InitialSource,
33 pub body: Option<&'a Block>,
35}
36
37pub fn resolve_initial(state: &State) -> ResolvedInitial<'_> {
41 if state.kind() != StateKind::Compound {
42 return ResolvedInitial {
43 targets: Vec::new(),
44 source: InitialSource::None,
45 body: None,
46 };
47 }
48 if !state.initial.is_empty() {
49 return ResolvedInitial {
50 targets: state.initial.iter().map(String::as_str).collect(),
51 source: InitialSource::Attribute,
52 body: None,
53 };
54 }
55 if let Some(init) = &state.initial_element {
56 return ResolvedInitial {
57 targets: init.transition.targets.iter().map(String::as_str).collect(),
58 source: InitialSource::Element,
59 body: Some(&init.transition.body),
60 };
61 }
62 first_child(&state.children)
63}
64
65pub fn resolve_root_initial(chart: &Chart) -> ResolvedInitial<'_> {
67 if !chart.initial.is_empty() {
68 return ResolvedInitial {
69 targets: chart.initial.iter().map(String::as_str).collect(),
70 source: InitialSource::Attribute,
71 body: None,
72 };
73 }
74 first_child(&chart.states)
75}
76
77fn first_child(children: &[State]) -> ResolvedInitial<'_> {
78 match children
79 .iter()
80 .find(|c| !matches!(c.element, StateElement::History(_)))
81 {
82 Some(c) => ResolvedInitial {
83 targets: alloc::vec![c.id.as_str()],
84 source: InitialSource::FirstChild,
85 body: None,
86 },
87 None => ResolvedInitial {
88 targets: Vec::new(),
89 source: InitialSource::None,
90 body: None,
91 },
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum NodeRef {
98 Root,
100 State {
102 id: String,
104 },
105 Transition {
107 source: String,
109 index: usize,
111 },
112 Initial {
114 state: String,
116 },
117 Data {
119 id: String,
121 },
122 Invoke {
124 state: String,
126 index: usize,
128 },
129 Exec {
131 state: String,
133 },
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
138pub enum ViolationKind {
139 EmptyChart,
141 MissingId,
143 DuplicateStateId {
145 id: String,
147 },
148 UnknownTarget {
150 target: String,
152 },
153 IllegalInitialTarget {
156 target: String,
158 },
159 InitialAttributeAndElement,
161 InitialOnNonCompound,
163 IllegalInitialTransition,
165 HistoryTransitionCount,
167 IllegalHistoryTransition,
169 IllegalHistoryTarget {
171 target: String,
173 },
174 HistoryAtRoot,
176 HistoryContent,
178 FinalContent {
180 what: String,
182 },
183 DoneDataOutsideFinal,
185 ParallelHistoryOnly,
187 StateHistoryOnly,
189 IllegalTargetSet {
191 first: String,
193 second: String,
195 },
196 HistoryDefaultCycle {
201 history: String,
203 },
204 DuplicateDataId {
206 id: String,
208 },
209 ConflictingAttributes {
211 element: String,
213 attributes: String,
215 },
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct Violation {
221 pub kind: ViolationKind,
223 pub node: NodeRef,
225 pub span: Span,
227}
228
229impl core::fmt::Display for Violation {
230 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
231 write!(f, "{:?} at {:?}", self.kind, self.node)
232 }
233}
234
235pub(crate) struct Flat<'a> {
237 pub states: Vec<&'a State>,
239 pub parent: Vec<Option<usize>>,
241 pub last: Vec<usize>,
243 pub by_id: BTreeMap<&'a str, usize>,
245}
246
247impl<'a> Flat<'a> {
248 pub(crate) fn new(chart: &'a Chart) -> Self {
249 let mut f = Flat {
250 states: Vec::new(),
251 parent: Vec::new(),
252 last: Vec::new(),
253 by_id: BTreeMap::new(),
254 };
255 for s in &chart.states {
256 f.push(s, None);
257 }
258 f
259 }
260
261 fn push(&mut self, s: &'a State, parent: Option<usize>) -> usize {
262 let i = self.states.len();
263 self.states.push(s);
264 self.parent.push(parent);
265 self.last.push(i);
266 self.by_id.entry(s.id.as_str()).or_insert(i);
267 for c in &s.children {
268 let l = self.push(c, Some(i));
269 self.last[i] = l;
270 }
271 self.last[i]
272 }
273
274 pub(crate) fn is_descendant(&self, d: usize, a: usize) -> bool {
276 a < d && d <= self.last[a]
277 }
278}
279
280pub fn validate(chart: &Chart) -> Result<(), Vec<Violation>> {
283 let flat = Flat::new(chart);
284 let mut v = Vec::new();
285 let mut push = |kind: ViolationKind, node: NodeRef, span: Span| {
286 v.push(Violation { kind, node, span });
287 };
288
289 if chart.states.is_empty() {
290 push(ViolationKind::EmptyChart, NodeRef::Root, chart.span);
291 }
292
293 let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
295 for s in &flat.states {
296 if s.id.is_empty() {
297 push(
298 ViolationKind::MissingId,
299 NodeRef::State { id: String::new() },
300 s.span,
301 );
302 continue;
303 }
304 let n = seen.entry(s.id.as_str()).or_insert(0);
305 *n += 1;
306 if *n == 2 {
307 push(
308 ViolationKind::DuplicateStateId { id: s.id.clone() },
309 NodeRef::State { id: s.id.clone() },
310 s.span,
311 );
312 }
313 }
314
315 for t in &chart.initial {
317 if !flat.by_id.contains_key(t.as_str()) {
318 push(
319 ViolationKind::UnknownTarget { target: t.clone() },
320 NodeRef::Root,
321 chart.span,
322 );
323 }
324 }
325 check_target_set(&flat, &chart.initial, NodeRef::Root, chart.span, &mut push);
326 for s in &chart.states {
327 if matches!(s.element, StateElement::History(_)) {
328 push(
329 ViolationKind::HistoryAtRoot,
330 NodeRef::State { id: s.id.clone() },
331 s.span,
332 );
333 }
334 }
335
336 check_history_defaults(&flat, &mut push);
337
338 let mut data_seen: BTreeMap<&str, usize> = BTreeMap::new();
340 let all_data = chart
341 .data
342 .iter()
343 .chain(flat.states.iter().flat_map(|s| s.data.iter()));
344 for d in all_data {
345 let n = data_seen.entry(d.id.as_str()).or_insert(0);
346 *n += 1;
347 if *n == 2 {
348 push(
349 ViolationKind::DuplicateDataId { id: d.id.clone() },
350 NodeRef::Data { id: d.id.clone() },
351 d.span,
352 );
353 }
354 let given = [d.src.is_some() || d.content.is_some(), d.expr.is_some()]
357 .iter()
358 .filter(|b| **b)
359 .count();
360 if given > 1 {
361 push(
362 conflict("data", "src, expr, content"),
363 NodeRef::Data { id: d.id.clone() },
364 d.span,
365 );
366 }
367 }
368
369 for (i, s) in flat.states.iter().enumerate() {
370 let sid = || NodeRef::State { id: s.id.clone() };
371 let kind = s.kind();
372
373 let has_initial = !s.initial.is_empty() || s.initial_element.is_some();
375 if has_initial && kind != StateKind::Compound {
376 push(ViolationKind::InitialOnNonCompound, sid(), s.span);
377 }
378 if !s.initial.is_empty() && s.initial_element.is_some() {
379 push(
380 ViolationKind::InitialAttributeAndElement,
381 NodeRef::Initial {
382 state: s.id.clone(),
383 },
384 s.span,
385 );
386 }
387 let init_node = || NodeRef::Initial {
388 state: s.id.clone(),
389 };
390 for t in &s.initial {
391 match flat.by_id.get(t.as_str()) {
392 None => push(
393 ViolationKind::UnknownTarget { target: t.clone() },
394 init_node(),
395 s.span,
396 ),
397 Some(&ti) if !flat.is_descendant(ti, i) => push(
398 ViolationKind::IllegalInitialTarget { target: t.clone() },
399 init_node(),
400 s.span,
401 ),
402 _ => {}
403 }
404 }
405 check_target_set(&flat, &s.initial, init_node(), s.span, &mut push);
406 if let Some(init) = &s.initial_element {
407 let tr = &init.transition;
408 if !tr.events.is_empty() || tr.cond.is_some() || tr.targets.is_empty() {
409 push(
410 ViolationKind::IllegalInitialTransition,
411 init_node(),
412 init.span,
413 );
414 }
415 for t in &tr.targets {
416 match flat.by_id.get(t.as_str()) {
417 None => push(
418 ViolationKind::UnknownTarget { target: t.clone() },
419 init_node(),
420 init.span,
421 ),
422 Some(&ti) if !flat.is_descendant(ti, i) => push(
423 ViolationKind::IllegalInitialTarget { target: t.clone() },
424 init_node(),
425 init.span,
426 ),
427 _ => {}
428 }
429 }
430 check_target_set(&flat, &tr.targets, init_node(), init.span, &mut push);
431 check_block(&tr.body, &s.id, &mut push);
432 }
433
434 for (ti, t) in s.transitions.iter().enumerate() {
436 let tnode = || NodeRef::Transition {
437 source: s.id.clone(),
438 index: ti,
439 };
440 for target in &t.targets {
441 if !flat.by_id.contains_key(target.as_str()) {
442 push(
443 ViolationKind::UnknownTarget {
444 target: target.clone(),
445 },
446 tnode(),
447 t.span,
448 );
449 }
450 }
451 check_target_set(&flat, &t.targets, tnode(), t.span, &mut push);
452 check_block(&t.body, &s.id, &mut push);
453 }
454
455 match kind {
457 StateKind::History(_) => {
458 if s.transitions.len() > 1 {
459 push(ViolationKind::HistoryTransitionCount, sid(), s.span);
460 }
461 if let Some(t) = s.transitions.first() {
462 if !t.events.is_empty() || t.cond.is_some() || t.targets.is_empty() {
463 push(
464 ViolationKind::IllegalHistoryTransition,
465 NodeRef::Transition {
466 source: s.id.clone(),
467 index: 0,
468 },
469 t.span,
470 );
471 }
472 if let Some(p) = flat.parent[i] {
473 for target in &t.targets {
474 if let Some(&ti) = flat.by_id.get(target.as_str())
475 && !flat.is_descendant(ti, p)
476 {
477 push(
478 ViolationKind::IllegalHistoryTarget {
479 target: target.clone(),
480 },
481 NodeRef::Transition {
482 source: s.id.clone(),
483 index: 0,
484 },
485 t.span,
486 );
487 }
488 }
489 }
490 }
491 if !s.children.is_empty()
492 || !s.onentry.is_empty()
493 || !s.onexit.is_empty()
494 || !s.invokes.is_empty()
495 || !s.data.is_empty()
496 {
497 push(ViolationKind::HistoryContent, sid(), s.span);
498 }
499 }
500 StateKind::Final => {
501 let mut what = Vec::new();
502 if !s.children.is_empty() {
503 what.push("child states");
504 }
505 if !s.transitions.is_empty() {
506 what.push("transitions");
507 }
508 if !s.invokes.is_empty() {
509 what.push("invokes");
510 }
511 if !s.data.is_empty() {
512 what.push("data");
513 }
514 if !what.is_empty() {
515 push(
516 ViolationKind::FinalContent {
517 what: what.join(", "),
518 },
519 sid(),
520 s.span,
521 );
522 }
523 }
524 StateKind::Parallel | StateKind::Compound
525 if !s.children.is_empty()
526 && s.children
527 .iter()
528 .all(|c| matches!(c.element, StateElement::History(_))) =>
529 {
530 let k = if kind == StateKind::Parallel {
534 ViolationKind::ParallelHistoryOnly
535 } else {
536 ViolationKind::StateHistoryOnly
537 };
538 push(k, sid(), s.span);
539 }
540 _ => {}
541 }
542 if s.donedata.is_some() && kind != StateKind::Final {
543 push(ViolationKind::DoneDataOutsideFinal, sid(), s.span);
544 }
545 if let Some(dd) = &s.donedata {
546 if dd.content.is_some() && !dd.params.is_empty() {
547 push(conflict("donedata", "content, param"), sid(), dd.span);
548 }
549 check_params(&dd.params, &s.id, &mut push);
550 }
551
552 for b in s.onentry.iter().chain(s.onexit.iter()) {
554 check_block(b, &s.id, &mut push);
555 }
556 for (ii, inv) in s.invokes.iter().enumerate() {
557 let inode = || NodeRef::Invoke {
558 state: s.id.clone(),
559 index: ii,
560 };
561 let pairs = [
562 (
563 inv.type_.is_some() && inv.typeexpr.is_some(),
564 "type, typeexpr",
565 ),
566 (inv.src.is_some() && inv.srcexpr.is_some(), "src, srcexpr"),
567 (
568 inv.id.is_some() && inv.idlocation.is_some(),
569 "id, idlocation",
570 ),
571 (
575 inv.content.as_ref().is_some_and(|c| {
576 inv.srcexpr.is_some() || (inv.src.is_some() && c.expr.is_some())
577 }),
578 "content, src",
579 ),
580 (
581 !inv.params.is_empty() && !inv.namelist.is_empty(),
582 "param, namelist",
583 ),
584 ];
585 for (bad, attrs) in pairs {
586 if bad {
587 push(conflict("invoke", attrs), inode(), inv.span);
588 }
589 }
590 check_params(&inv.params, &s.id, &mut push);
591 check_block(&inv.finalize, &s.id, &mut push);
592 }
593 }
594
595 if v.is_empty() { Ok(()) } else { Err(v) }
596}
597
598fn conflict(element: &str, attributes: &str) -> ViolationKind {
599 ViolationKind::ConflictingAttributes {
600 element: element.to_string(),
601 attributes: attributes.to_string(),
602 }
603}
604
605fn history_default_edges(flat: &Flat<'_>, h: usize) -> Vec<usize> {
609 let st = flat.states[h];
610 let targets: Vec<&str> = match (st.transitions.first(), flat.parent[h]) {
611 (Some(t), _) => t.targets.iter().map(String::as_str).collect(),
612 (None, Some(p)) => resolve_initial(flat.states[p]).targets,
613 (None, None) => Vec::new(),
614 };
615 targets
616 .iter()
617 .filter_map(|t| flat.by_id.get(t).copied())
618 .filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
619 .collect()
620}
621
622fn check_history_defaults(flat: &Flat<'_>, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
626 let histories: Vec<usize> = (0..flat.states.len())
627 .filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
628 .collect();
629 for &h in &histories {
630 let mut seen = alloc::vec![false; flat.states.len()];
631 let mut stack = history_default_edges(flat, h);
632 let mut cyclic = false;
633 while let Some(x) = stack.pop() {
634 if x == h {
635 cyclic = true;
636 break;
637 }
638 if !core::mem::replace(&mut seen[x], true) {
639 stack.extend(history_default_edges(flat, x));
640 }
641 }
642 if cyclic {
643 let id = flat.states[h].id.clone();
644 push(
645 ViolationKind::HistoryDefaultCycle {
646 history: id.clone(),
647 },
648 NodeRef::State { id },
649 flat.states[h].span,
650 );
651 }
652 }
653}
654
655fn check_target_set(
660 flat: &Flat<'_>,
661 targets: &[String],
662 node: NodeRef,
663 span: Span,
664 push: &mut impl FnMut(ViolationKind, NodeRef, Span),
665) {
666 let idx: Vec<(usize, &String)> = targets
667 .iter()
668 .filter_map(|t| flat.by_id.get(t.as_str()).map(|i| (*i, t)))
669 .filter_map(|(i, t)| {
670 if matches!(flat.states[i].element, StateElement::History(_)) {
671 flat.parent[i].map(|p| (p, t))
673 } else {
674 Some((i, t))
675 }
676 })
677 .collect();
678 for a in 0..idx.len() {
679 for b in (a + 1)..idx.len() {
680 let (x, xs) = idx[a];
681 let (y, ys) = idx[b];
682 let ok = if x == y || flat.is_descendant(x, y) || flat.is_descendant(y, x) {
683 false
684 } else {
685 let mut anc = flat.parent[x];
687 loop {
688 match anc {
689 Some(p) if flat.is_descendant(y, p) => {
690 break flat.states[p].kind() == StateKind::Parallel;
691 }
692 Some(p) => anc = flat.parent[p],
693 None => break false,
694 }
695 }
696 };
697 if !ok {
698 push(
699 ViolationKind::IllegalTargetSet {
700 first: xs.clone(),
701 second: ys.clone(),
702 },
703 node.clone(),
704 span,
705 );
706 }
707 }
708 }
709}
710
711fn check_params(
712 params: &[Param],
713 state: &str,
714 push: &mut impl FnMut(ViolationKind, NodeRef, Span),
715) {
716 for p in params {
717 if p.expr.is_some() == p.location.is_some() {
718 push(
719 conflict("param", "expr, location"),
720 NodeRef::Exec {
721 state: state.to_string(),
722 },
723 p.span,
724 );
725 }
726 }
727}
728
729fn check_block(block: &Block, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
730 for e in block {
731 check_exec(e, state, push);
732 }
733}
734
735fn check_exec(e: &Exec, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
736 let node = || NodeRef::Exec {
737 state: state.to_string(),
738 };
739 match &e.kind {
740 ExecKind::Send(s) => {
741 let pairs = [
742 (
743 s.event.is_some() && s.eventexpr.is_some(),
744 "event, eventexpr",
745 ),
746 (
747 s.target.is_some() && s.targetexpr.is_some(),
748 "target, targetexpr",
749 ),
750 (s.type_.is_some() && s.typeexpr.is_some(), "type, typeexpr"),
751 (s.id.is_some() && s.idlocation.is_some(), "id, idlocation"),
752 (
753 s.delay.is_some() && s.delayexpr.is_some(),
754 "delay, delayexpr",
755 ),
756 (
757 s.content.is_some() && (!s.params.is_empty() || !s.namelist.is_empty()),
758 "content, param, namelist",
759 ),
760 ];
761 for (bad, attrs) in pairs {
762 if bad {
763 push(conflict("send", attrs), node(), e.span);
764 }
765 }
766 check_params(&s.params, state, push);
767 }
768 ExecKind::Cancel { sendid, sendidexpr } if sendid.is_some() == sendidexpr.is_some() => {
769 push(conflict("cancel", "sendid, sendidexpr"), node(), e.span);
770 }
771 ExecKind::Assign { expr, content, .. } if expr.is_some() && content.is_some() => {
772 push(conflict("assign", "expr, content"), node(), e.span);
773 }
774 ExecKind::If { branches } => {
775 for (bi, br) in branches.iter().enumerate() {
776 let is_else = br.cond.is_none();
777 if is_else && bi + 1 != branches.len() {
778 push(
779 conflict("if", &format!("else before branch {}", bi + 1)),
780 node(),
781 br.span,
782 );
783 }
784 if bi == 0 && is_else {
785 push(conflict("if", "cond"), node(), br.span);
786 }
787 check_block(&br.body, state, push);
788 }
789 }
790 ExecKind::Foreach { body, item, .. } => {
791 if item.is_empty() {
792 push(conflict("foreach", "item"), node(), e.span);
793 }
794 check_block(body, state, push);
795 }
796 _ => {}
797 }
798}