Skip to main content

vertep_core/
validate.rs

1//! Well-formedness validation and initial-state resolution.
2//!
3//! [`validate`] checks every rule and returns all violations together, each naming the offending
4//! node; a chart that passes can be compiled with [`crate::compile::CompiledChart::new`] and run.
5
6use 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/// Where a resolved default entry came from.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum InitialSource {
16    /// The `initial` attribute.
17    Attribute,
18    /// The transition of the `<initial>` element.
19    Element,
20    /// The first child state in document order.
21    FirstChild,
22    /// The state has no default entry (it is not compound, or has no child states).
23    None,
24}
25
26/// The resolved default entry of a compound state or of the root.
27#[derive(Clone, Debug, PartialEq)]
28pub struct ResolvedInitial<'a> {
29    /// The identifiers of the default entry targets, in document order of the source.
30    pub targets: Vec<&'a str>,
31    /// Where the targets came from.
32    pub source: InitialSource,
33    /// The executable content of the `<initial>` transition, if it came from an element.
34    pub body: Option<&'a Block>,
35}
36
37/// Resolve the default entry of a state: the `initial` attribute, else the `<initial>` element's
38/// transition, else the first child state (not a history pseudo-state) in document order.
39// @tg: implements:: [[openspec:chart-model#Initial State Resolution]]
40pub 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
65/// Resolve the default entry of the root: its `initial` attribute, else its first child state.
66pub 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/// The node a violation is about.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum NodeRef {
98    /// The `<scxml>` root.
99    Root,
100    /// A state, by identifier (possibly empty when the identifier is missing).
101    State {
102        /// The state identifier.
103        id: String,
104    },
105    /// A transition, by source state and index in its transition list.
106    Transition {
107        /// The source state identifier.
108        source: String,
109        /// Index in `State::transitions`.
110        index: usize,
111    },
112    /// The `initial` attribute or `<initial>` element of a state.
113    Initial {
114        /// The owning state identifier.
115        state: String,
116    },
117    /// A data item, by identifier.
118    Data {
119        /// The data identifier.
120        id: String,
121    },
122    /// An invoke, by owning state and index.
123    Invoke {
124        /// The owning state identifier.
125        state: String,
126        /// Index in `State::invokes`.
127        index: usize,
128    },
129    /// An executable content element inside a state (its handlers, transitions or invokes).
130    Exec {
131        /// The owning state identifier.
132        state: String,
133    },
134}
135
136/// What is wrong.
137#[derive(Clone, Debug, PartialEq, Eq)]
138pub enum ViolationKind {
139    /// The chart has no child states.
140    EmptyChart,
141    /// A state has no identifier (run [`crate::identity::assign_generated_ids`] first).
142    MissingId,
143    /// Two states share an identifier.
144    DuplicateStateId {
145        /// The repeated identifier.
146        id: String,
147    },
148    /// A target names no state.
149    UnknownTarget {
150        /// The unknown identifier.
151        target: String,
152    },
153    /// An `initial` attribute or `<initial>` transition targets a state that is not a proper
154    /// descendant of the owning state.
155    IllegalInitialTarget {
156        /// The offending target.
157        target: String,
158    },
159    /// A state has both an `initial` attribute and an `<initial>` element.
160    InitialAttributeAndElement,
161    /// An `initial` attribute or `<initial>` element on a state that is not compound.
162    InitialOnNonCompound,
163    /// An `<initial>` transition with an event or a condition, or without a target.
164    IllegalInitialTransition,
165    /// A history state with more than one transition.
166    HistoryTransitionCount,
167    /// A history default transition with an event or a condition, or without a target.
168    IllegalHistoryTransition,
169    /// A history default transition target outside the history's parent.
170    IllegalHistoryTarget {
171        /// The offending target.
172        target: String,
173    },
174    /// A history state that is a child of the root.
175    HistoryAtRoot,
176    /// A history state with content other than its default transition.
177    HistoryContent,
178    /// A final state with content other than `onentry`, `onexit` and `donedata`.
179    FinalContent {
180        /// What it contains.
181        what: String,
182    },
183    /// `donedata` on a state that is not final.
184    DoneDataOutsideFinal,
185    /// A parallel state whose children are all history states.
186    ParallelHistoryOnly,
187    /// A `<state>` whose children are all history states.
188    StateHistoryOnly,
189    /// The targets of one transition cannot be active together.
190    IllegalTargetSet {
191        /// The first target of the conflicting pair.
192        first: String,
193        /// The second target of the conflicting pair.
194        second: String,
195    },
196    /// Default history entry never reaches a real state: following the default transitions of
197    /// history states (a history state without one defaults to its parent's `initial`) leads
198    /// back to this history state, for example `<state initial="h">` whose history child `h`
199    /// has no default transition.
200    HistoryDefaultCycle {
201        /// The history state on the cycle.
202        history: String,
203    },
204    /// Two data items share an identifier.
205    DuplicateDataId {
206        /// The repeated identifier.
207        id: String,
208    },
209    /// An element has attributes or children that exclude each other, or lacks a required one.
210    ConflictingAttributes {
211        /// The element name.
212        element: String,
213        /// The attributes involved.
214        attributes: String,
215    },
216}
217
218/// One well-formedness violation.
219#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct Violation {
221    /// What is wrong.
222    pub kind: ViolationKind,
223    /// The offending node.
224    pub node: NodeRef,
225    /// Source location of the offending node, if known.
226    pub span: Span,
227}
228
229impl core::fmt::Display for NodeRef {
230    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
231        match self {
232            NodeRef::Root => f.write_str("the root"),
233            NodeRef::State { id } => write!(f, "state `{id}`"),
234            NodeRef::Transition { source, index } => {
235                write!(f, "transition {index} of state `{source}`")
236            }
237            NodeRef::Initial { state } => write!(f, "the initial of state `{state}`"),
238            NodeRef::Data { id } => write!(f, "data `{id}`"),
239            NodeRef::Invoke { state, index } => write!(f, "invoke {index} of state `{state}`"),
240            NodeRef::Exec { state } => write!(f, "executable content in state `{state}`"),
241        }
242    }
243}
244
245impl core::fmt::Display for ViolationKind {
246    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247        match self {
248            ViolationKind::EmptyChart => f.write_str("the chart has no states"),
249            ViolationKind::MissingId => f.write_str("a state has no identifier"),
250            ViolationKind::DuplicateStateId { id } => write!(f, "duplicate state id `{id}`"),
251            ViolationKind::UnknownTarget { target } => write!(f, "unknown target `{target}`"),
252            ViolationKind::IllegalInitialTarget { target } => {
253                write!(f, "initial target `{target}` is not a proper descendant")
254            }
255            ViolationKind::InitialAttributeAndElement => {
256                f.write_str("both an initial attribute and an <initial> element")
257            }
258            ViolationKind::InitialOnNonCompound => f.write_str("initial on a non-compound state"),
259            ViolationKind::IllegalInitialTransition => {
260                f.write_str("an <initial> transition needs a target and no event or condition")
261            }
262            ViolationKind::HistoryTransitionCount => {
263                f.write_str("a history state has more than one transition")
264            }
265            ViolationKind::IllegalHistoryTransition => {
266                f.write_str("a history default transition needs a target and no event or condition")
267            }
268            ViolationKind::IllegalHistoryTarget { target } => {
269                write!(
270                    f,
271                    "history default target `{target}` is outside the history's parent"
272                )
273            }
274            ViolationKind::HistoryAtRoot => f.write_str("a history state at the root"),
275            ViolationKind::HistoryContent => {
276                f.write_str("a history state with content other than its default transition")
277            }
278            ViolationKind::FinalContent { what } => write!(f, "a final state contains {what}"),
279            ViolationKind::DoneDataOutsideFinal => f.write_str("donedata on a non-final state"),
280            ViolationKind::ParallelHistoryOnly => {
281                f.write_str("a parallel state whose children are all history states")
282            }
283            ViolationKind::StateHistoryOnly => {
284                f.write_str("a state whose children are all history states")
285            }
286            ViolationKind::IllegalTargetSet { first, second } => {
287                write!(
288                    f,
289                    "targets `{first}` and `{second}` cannot be active together"
290                )
291            }
292            ViolationKind::HistoryDefaultCycle { history } => {
293                write!(
294                    f,
295                    "the default entry of history `{history}` never reaches a state"
296                )
297            }
298            ViolationKind::DuplicateDataId { id } => write!(f, "duplicate data id `{id}`"),
299            ViolationKind::ConflictingAttributes {
300                element,
301                attributes,
302            } => write!(f, "<{element}>: conflicting or missing {attributes}"),
303        }
304    }
305}
306
307impl core::fmt::Display for Violation {
308    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
309        write!(f, "{}: {}", self.node, self.kind)
310    }
311}
312
313impl core::error::Error for Violation {}
314
315/// Every well-formedness violation of a chart, as one error value.
316///
317/// [`crate::compile::CompiledChart::new`] returns the violations as a `Vec<Violation>`; wrap
318/// them with `map_err(Violations)` to use `?` into `Box<dyn Error>`. Displays one violation
319/// per line.
320// @tg: implements:: [[decisions#Core errors are standard errors]]
321#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct Violations(pub Vec<Violation>);
323
324impl From<Vec<Violation>> for Violations {
325    fn from(v: Vec<Violation>) -> Self {
326        Violations(v)
327    }
328}
329
330impl core::fmt::Display for Violations {
331    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
332        for (i, v) in self.0.iter().enumerate() {
333            if i > 0 {
334                f.write_str("\n")?;
335            }
336            write!(f, "{v}")?;
337        }
338        Ok(())
339    }
340}
341
342impl core::error::Error for Violations {}
343
344/// Flat pre-order view of the state tree used by validation and compilation.
345pub(crate) struct Flat<'a> {
346    /// States in document order.
347    pub states: Vec<&'a State>,
348    /// Parent index, `None` for children of the root.
349    pub parent: Vec<Option<usize>>,
350    /// Index of the last descendant of each state (itself when it has none).
351    pub last: Vec<usize>,
352    /// Identifier to first index.
353    pub by_id: BTreeMap<&'a str, usize>,
354}
355
356impl<'a> Flat<'a> {
357    pub(crate) fn new(chart: &'a Chart) -> Self {
358        let mut f = Flat {
359            states: Vec::new(),
360            parent: Vec::new(),
361            last: Vec::new(),
362            by_id: BTreeMap::new(),
363        };
364        for s in &chart.states {
365            f.push(s, None);
366        }
367        f
368    }
369
370    fn push(&mut self, s: &'a State, parent: Option<usize>) -> usize {
371        let i = self.states.len();
372        self.states.push(s);
373        self.parent.push(parent);
374        self.last.push(i);
375        self.by_id.entry(s.id.as_str()).or_insert(i);
376        for c in &s.children {
377            let l = self.push(c, Some(i));
378            self.last[i] = l;
379        }
380        self.last[i]
381    }
382
383    /// Whether `d` is a proper descendant of `a`.
384    pub(crate) fn is_descendant(&self, d: usize, a: usize) -> bool {
385        a < d && d <= self.last[a]
386    }
387}
388
389/// Validate a chart, returning every violation found.
390// @tg: implements:: [[openspec:chart-model#Well-Formedness Validation]]
391pub fn validate(chart: &Chart) -> Result<(), Vec<Violation>> {
392    let flat = Flat::new(chart);
393    let mut v = Vec::new();
394    let mut push = |kind: ViolationKind, node: NodeRef, span: Span| {
395        v.push(Violation { kind, node, span });
396    };
397
398    if chart.states.is_empty() {
399        push(ViolationKind::EmptyChart, NodeRef::Root, chart.span);
400    }
401
402    // Identifiers.
403    let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
404    for s in &flat.states {
405        if s.id.is_empty() {
406            push(
407                ViolationKind::MissingId,
408                NodeRef::State { id: String::new() },
409                s.span,
410            );
411            continue;
412        }
413        let n = seen.entry(s.id.as_str()).or_insert(0);
414        *n += 1;
415        if *n == 2 {
416            push(
417                ViolationKind::DuplicateStateId { id: s.id.clone() },
418                NodeRef::State { id: s.id.clone() },
419                s.span,
420            );
421        }
422    }
423
424    // Root initial.
425    for t in &chart.initial {
426        if !flat.by_id.contains_key(t.as_str()) {
427            push(
428                ViolationKind::UnknownTarget { target: t.clone() },
429                NodeRef::Root,
430                chart.span,
431            );
432        }
433    }
434    check_target_set(&flat, &chart.initial, NodeRef::Root, chart.span, &mut push);
435    for s in &chart.states {
436        if matches!(s.element, StateElement::History(_)) {
437            push(
438                ViolationKind::HistoryAtRoot,
439                NodeRef::State { id: s.id.clone() },
440                s.span,
441            );
442        }
443    }
444
445    check_history_defaults(&flat, &mut push);
446
447    // Data identifiers.
448    let mut data_seen: BTreeMap<&str, usize> = BTreeMap::new();
449    let all_data = chart
450        .data
451        .iter()
452        .chain(flat.states.iter().flat_map(|s| s.data.iter()));
453    for d in all_data {
454        let n = data_seen.entry(d.id.as_str()).or_insert(0);
455        *n += 1;
456        if *n == 2 {
457            push(
458                ViolationKind::DuplicateDataId { id: d.id.clone() },
459                NodeRef::Data { id: d.id.clone() },
460                d.span,
461            );
462        }
463        // A resolved `src` keeps both the reference and the content it resolved to (the
464        // reader's convention), so `src` with `content` is one source, not two.
465        let given = [d.src.is_some() || d.content.is_some(), d.expr.is_some()]
466            .iter()
467            .filter(|b| **b)
468            .count();
469        if given > 1 {
470            push(
471                conflict("data", "src, expr, content"),
472                NodeRef::Data { id: d.id.clone() },
473                d.span,
474            );
475        }
476    }
477
478    for (i, s) in flat.states.iter().enumerate() {
479        let sid = || NodeRef::State { id: s.id.clone() };
480        let kind = s.kind();
481
482        // Initial attribute and element.
483        let has_initial = !s.initial.is_empty() || s.initial_element.is_some();
484        if has_initial && kind != StateKind::Compound {
485            push(ViolationKind::InitialOnNonCompound, sid(), s.span);
486        }
487        if !s.initial.is_empty() && s.initial_element.is_some() {
488            push(
489                ViolationKind::InitialAttributeAndElement,
490                NodeRef::Initial {
491                    state: s.id.clone(),
492                },
493                s.span,
494            );
495        }
496        let init_node = || NodeRef::Initial {
497            state: s.id.clone(),
498        };
499        for t in &s.initial {
500            match flat.by_id.get(t.as_str()) {
501                None => push(
502                    ViolationKind::UnknownTarget { target: t.clone() },
503                    init_node(),
504                    s.span,
505                ),
506                Some(&ti) if !flat.is_descendant(ti, i) => push(
507                    ViolationKind::IllegalInitialTarget { target: t.clone() },
508                    init_node(),
509                    s.span,
510                ),
511                _ => {}
512            }
513        }
514        check_target_set(&flat, &s.initial, init_node(), s.span, &mut push);
515        if let Some(init) = &s.initial_element {
516            let tr = &init.transition;
517            if !tr.events.is_empty() || tr.cond.is_some() || tr.targets.is_empty() {
518                push(
519                    ViolationKind::IllegalInitialTransition,
520                    init_node(),
521                    init.span,
522                );
523            }
524            for t in &tr.targets {
525                match flat.by_id.get(t.as_str()) {
526                    None => push(
527                        ViolationKind::UnknownTarget { target: t.clone() },
528                        init_node(),
529                        init.span,
530                    ),
531                    Some(&ti) if !flat.is_descendant(ti, i) => push(
532                        ViolationKind::IllegalInitialTarget { target: t.clone() },
533                        init_node(),
534                        init.span,
535                    ),
536                    _ => {}
537                }
538            }
539            check_target_set(&flat, &tr.targets, init_node(), init.span, &mut push);
540            check_block(&tr.body, &s.id, &mut push);
541        }
542
543        // Transitions.
544        for (ti, t) in s.transitions.iter().enumerate() {
545            let tnode = || NodeRef::Transition {
546                source: s.id.clone(),
547                index: ti,
548            };
549            for target in &t.targets {
550                if !flat.by_id.contains_key(target.as_str()) {
551                    push(
552                        ViolationKind::UnknownTarget {
553                            target: target.clone(),
554                        },
555                        tnode(),
556                        t.span,
557                    );
558                }
559            }
560            check_target_set(&flat, &t.targets, tnode(), t.span, &mut push);
561            check_block(&t.body, &s.id, &mut push);
562        }
563
564        // Kind-specific rules.
565        match kind {
566            StateKind::History(_) => {
567                if s.transitions.len() > 1 {
568                    push(ViolationKind::HistoryTransitionCount, sid(), s.span);
569                }
570                if let Some(t) = s.transitions.first() {
571                    if !t.events.is_empty() || t.cond.is_some() || t.targets.is_empty() {
572                        push(
573                            ViolationKind::IllegalHistoryTransition,
574                            NodeRef::Transition {
575                                source: s.id.clone(),
576                                index: 0,
577                            },
578                            t.span,
579                        );
580                    }
581                    if let Some(p) = flat.parent[i] {
582                        for target in &t.targets {
583                            if let Some(&ti) = flat.by_id.get(target.as_str())
584                                && !flat.is_descendant(ti, p)
585                            {
586                                push(
587                                    ViolationKind::IllegalHistoryTarget {
588                                        target: target.clone(),
589                                    },
590                                    NodeRef::Transition {
591                                        source: s.id.clone(),
592                                        index: 0,
593                                    },
594                                    t.span,
595                                );
596                            }
597                        }
598                    }
599                }
600                if !s.children.is_empty()
601                    || !s.onentry.is_empty()
602                    || !s.onexit.is_empty()
603                    || !s.invokes.is_empty()
604                    || !s.data.is_empty()
605                {
606                    push(ViolationKind::HistoryContent, sid(), s.span);
607                }
608            }
609            StateKind::Final => {
610                let mut what = Vec::new();
611                if !s.children.is_empty() {
612                    what.push("child states");
613                }
614                if !s.transitions.is_empty() {
615                    what.push("transitions");
616                }
617                if !s.invokes.is_empty() {
618                    what.push("invokes");
619                }
620                if !s.data.is_empty() {
621                    what.push("data");
622                }
623                if !what.is_empty() {
624                    push(
625                        ViolationKind::FinalContent {
626                            what: what.join(", "),
627                        },
628                        sid(),
629                        s.span,
630                    );
631                }
632            }
633            StateKind::Parallel | StateKind::Compound
634                if !s.children.is_empty()
635                    && s.children
636                        .iter()
637                        .all(|c| matches!(c.element, StateElement::History(_))) =>
638            {
639                // A `<state>` whose only children are history states is atomic in W3C SCXML
640                // but compound by `State::kind`; such charts are rejected rather than run
641                // under either reading.
642                let k = if kind == StateKind::Parallel {
643                    ViolationKind::ParallelHistoryOnly
644                } else {
645                    ViolationKind::StateHistoryOnly
646                };
647                push(k, sid(), s.span);
648            }
649            _ => {}
650        }
651        if s.donedata.is_some() && kind != StateKind::Final {
652            push(ViolationKind::DoneDataOutsideFinal, sid(), s.span);
653        }
654        if let Some(dd) = &s.donedata {
655            if dd.content.is_some() && !dd.params.is_empty() {
656                push(conflict("donedata", "content, param"), sid(), dd.span);
657            }
658            check_params(&dd.params, &s.id, &mut push);
659        }
660
661        // Executable content and invokes.
662        for b in s.onentry.iter().chain(s.onexit.iter()) {
663            check_block(b, &s.id, &mut push);
664        }
665        for (ii, inv) in s.invokes.iter().enumerate() {
666            let inode = || NodeRef::Invoke {
667                state: s.id.clone(),
668                index: ii,
669            };
670            let pairs = [
671                (
672                    inv.type_.is_some() && inv.typeexpr.is_some(),
673                    "type, typeexpr",
674                ),
675                (inv.src.is_some() && inv.srcexpr.is_some(), "src, srcexpr"),
676                (
677                    inv.id.is_some() && inv.idlocation.is_some(),
678                    "id, idlocation",
679                ),
680                // A resolved `src` keeps the reference and the document it resolved to as an
681                // inline `<content>` body, so only `srcexpr`, or `src` with a `<content expr>`,
682                // conflicts with content.
683                (
684                    inv.content.as_ref().is_some_and(|c| {
685                        inv.srcexpr.is_some() || (inv.src.is_some() && c.expr.is_some())
686                    }),
687                    "content, src",
688                ),
689                (
690                    !inv.params.is_empty() && !inv.namelist.is_empty(),
691                    "param, namelist",
692                ),
693            ];
694            for (bad, attrs) in pairs {
695                if bad {
696                    push(conflict("invoke", attrs), inode(), inv.span);
697                }
698            }
699            check_params(&inv.params, &s.id, &mut push);
700            check_block(&inv.finalize, &s.id, &mut push);
701        }
702    }
703
704    if v.is_empty() { Ok(()) } else { Err(v) }
705}
706
707fn conflict(element: &str, attributes: &str) -> ViolationKind {
708    ViolationKind::ConflictingAttributes {
709        element: element.to_string(),
710        attributes: attributes.to_string(),
711    }
712}
713
714/// The history states the default entry of history state `h` targets: its default transition's
715/// history targets, or, without one, the history targets of its parent's `initial` attribute or
716/// `<initial>` transition (which is what compilation synthesizes as the default).
717fn history_default_edges(flat: &Flat<'_>, h: usize) -> Vec<usize> {
718    let st = flat.states[h];
719    let targets: Vec<&str> = match (st.transitions.first(), flat.parent[h]) {
720        (Some(t), _) => t.targets.iter().map(String::as_str).collect(),
721        (None, Some(p)) => resolve_initial(flat.states[p]).targets,
722        (None, None) => Vec::new(),
723    };
724    targets
725        .iter()
726        .filter_map(|t| flat.by_id.get(t).copied())
727        .filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
728        .collect()
729}
730
731/// Default history entry must terminate: no history state may reach itself through
732/// [`history_default_edges`]. Without this rule entering such a history state would recurse
733/// forever.
734fn check_history_defaults(flat: &Flat<'_>, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
735    let histories: Vec<usize> = (0..flat.states.len())
736        .filter(|&i| matches!(flat.states[i].element, StateElement::History(_)))
737        .collect();
738    for &h in &histories {
739        let mut seen = alloc::vec![false; flat.states.len()];
740        let mut stack = history_default_edges(flat, h);
741        let mut cyclic = false;
742        while let Some(x) = stack.pop() {
743            if x == h {
744                cyclic = true;
745                break;
746            }
747            if !core::mem::replace(&mut seen[x], true) {
748                stack.extend(history_default_edges(flat, x));
749            }
750        }
751        if cyclic {
752            let id = flat.states[h].id.clone();
753            push(
754                ViolationKind::HistoryDefaultCycle {
755                    history: id.clone(),
756                },
757                NodeRef::State { id },
758                flat.states[h].span,
759            );
760        }
761    }
762}
763
764/// Targets of one transition must be able to be active together: no target may be an ancestor of
765/// another, and every pair must meet below a parallel state. A history target stands for the
766/// whole content of its parent, so it takes its parent's position: it excludes its siblings,
767/// their descendants and other history states of the same parent.
768fn check_target_set(
769    flat: &Flat<'_>,
770    targets: &[String],
771    node: NodeRef,
772    span: Span,
773    push: &mut impl FnMut(ViolationKind, NodeRef, Span),
774) {
775    let idx: Vec<(usize, &String)> = targets
776        .iter()
777        .filter_map(|t| flat.by_id.get(t.as_str()).map(|i| (*i, t)))
778        .filter_map(|(i, t)| {
779            if matches!(flat.states[i].element, StateElement::History(_)) {
780                // A history state at the root is reported on its own.
781                flat.parent[i].map(|p| (p, t))
782            } else {
783                Some((i, t))
784            }
785        })
786        .collect();
787    for a in 0..idx.len() {
788        for b in (a + 1)..idx.len() {
789            let (x, xs) = idx[a];
790            let (y, ys) = idx[b];
791            let ok = if x == y || flat.is_descendant(x, y) || flat.is_descendant(y, x) {
792                false
793            } else {
794                // Lowest common proper ancestor must be a parallel state.
795                let mut anc = flat.parent[x];
796                loop {
797                    match anc {
798                        Some(p) if flat.is_descendant(y, p) => {
799                            break flat.states[p].kind() == StateKind::Parallel;
800                        }
801                        Some(p) => anc = flat.parent[p],
802                        None => break false,
803                    }
804                }
805            };
806            if !ok {
807                push(
808                    ViolationKind::IllegalTargetSet {
809                        first: xs.clone(),
810                        second: ys.clone(),
811                    },
812                    node.clone(),
813                    span,
814                );
815            }
816        }
817    }
818}
819
820fn check_params(
821    params: &[Param],
822    state: &str,
823    push: &mut impl FnMut(ViolationKind, NodeRef, Span),
824) {
825    for p in params {
826        if p.expr.is_some() == p.location.is_some() {
827            push(
828                conflict("param", "expr, location"),
829                NodeRef::Exec {
830                    state: state.to_string(),
831                },
832                p.span,
833            );
834        }
835    }
836}
837
838fn check_block(block: &Block, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
839    for e in block {
840        check_exec(e, state, push);
841    }
842}
843
844fn check_exec(e: &Exec, state: &str, push: &mut impl FnMut(ViolationKind, NodeRef, Span)) {
845    let node = || NodeRef::Exec {
846        state: state.to_string(),
847    };
848    match &e.kind {
849        ExecKind::Send(s) => {
850            let pairs = [
851                (
852                    s.event.is_some() && s.eventexpr.is_some(),
853                    "event, eventexpr",
854                ),
855                (
856                    s.target.is_some() && s.targetexpr.is_some(),
857                    "target, targetexpr",
858                ),
859                (s.type_.is_some() && s.typeexpr.is_some(), "type, typeexpr"),
860                (s.id.is_some() && s.idlocation.is_some(), "id, idlocation"),
861                (
862                    s.delay.is_some() && s.delayexpr.is_some(),
863                    "delay, delayexpr",
864                ),
865                (
866                    s.content.is_some() && (!s.params.is_empty() || !s.namelist.is_empty()),
867                    "content, param, namelist",
868                ),
869            ];
870            for (bad, attrs) in pairs {
871                if bad {
872                    push(conflict("send", attrs), node(), e.span);
873                }
874            }
875            check_params(&s.params, state, push);
876        }
877        ExecKind::Cancel { sendid, sendidexpr } if sendid.is_some() == sendidexpr.is_some() => {
878            push(conflict("cancel", "sendid, sendidexpr"), node(), e.span);
879        }
880        ExecKind::Assign { expr, content, .. } if expr.is_some() && content.is_some() => {
881            push(conflict("assign", "expr, content"), node(), e.span);
882        }
883        ExecKind::If { branches } => {
884            for (bi, br) in branches.iter().enumerate() {
885                let is_else = br.cond.is_none();
886                if is_else && bi + 1 != branches.len() {
887                    push(
888                        conflict("if", &format!("else before branch {}", bi + 1)),
889                        node(),
890                        br.span,
891                    );
892                }
893                if bi == 0 && is_else {
894                    push(conflict("if", "cond"), node(), br.span);
895                }
896                check_block(&br.body, state, push);
897            }
898        }
899        ExecKind::Foreach { body, item, .. } => {
900            if item.is_empty() {
901                push(conflict("foreach", "item"), node(), e.span);
902            }
903            check_block(body, state, push);
904        }
905        _ => {}
906    }
907}
908
909#[cfg(test)]
910mod error_tests {
911    use super::*;
912    use alloc::string::ToString;
913    use alloc::vec;
914
915    fn unknown(source: &str, target: &str) -> Violation {
916        Violation {
917            kind: ViolationKind::UnknownTarget {
918                target: target.into(),
919            },
920            node: NodeRef::Transition {
921                source: source.into(),
922                index: 0,
923            },
924            span: Span::default(),
925        }
926    }
927
928    #[test]
929    fn violation_displays_node_and_kind() {
930        let v = unknown("a", "nowhere");
931        assert_eq!(
932            v.to_string(),
933            "transition 0 of state `a`: unknown target `nowhere`"
934        );
935        let _: &dyn core::error::Error = &v;
936    }
937
938    #[test]
939    fn violations_display_one_per_line_and_box_with_question_mark() {
940        let vs = vec![unknown("a", "x"), unknown("b", "y")];
941        let w = Violations::from(vs.clone());
942        assert_eq!(w.0, vs);
943        assert_eq!(
944            w.to_string(),
945            "transition 0 of state `a`: unknown target `x`\ntransition 0 of state `b`: unknown target `y`"
946        );
947        fn run(vs: Vec<Violation>) -> Result<(), alloc::boxed::Box<dyn core::error::Error>> {
948            Err(Violations(vs))?
949        }
950        assert!(run(vs).unwrap_err().to_string().contains("`y`"));
951    }
952}