Skip to main content

vertep_core/
kernel.rs

1//! The selection and entry/exit kernel of the W3C SCXML interpretation algorithm (Appendix D).
2//!
3//! Every function here is a pure function over a [`CompiledChart`], a configuration
4//! ([`StateSet`]) and the recorded [`History`]. The names follow Appendix D:
5//! [`select_transitions`] (`selectTransitions` and `selectEventlessTransitions`),
6//! [`remove_conflicting_transitions`], [`compute_exit_set`], [`compute_entry_set`]
7//! (`computeEntrySet`, `addDescendantStatesToEnter`, `addAncestorStatesToEnter`),
8//! [`transition_domain`] (`getTransitionDomain`), [`find_lcca`], [`effective_targets`]
9//! (`getEffectiveTargetStates`), [`record_history`] (the history step of `exitStates`) and
10//! [`is_in_final_state`].
11//!
12//! # Aeneas-friendly boundary
13//!
14//! The kernel stays inside the Rust subset that Aeneas translates
15//! ([[constraints#Aeneas supports a Rust subset]]): it uses no trait objects, no interior
16//! mutability (`Cell`, `RefCell`), no `unsafe`, no async and no I/O. Data is plain vectors,
17//! bitsets of words and `usize` indices; iteration is over owned index vectors. The only
18//! abstraction is the generic condition oracle `FnMut(TransIdx) -> bool` taken by
19//! [`select_transitions`], which is monomorphized, never dynamically dispatched.
20//!
21//! Executable content and condition evaluation are outside the kernel: the session driver in
22//! [`crate::session`] calls the [`crate::datamodel::Datamodel`] trait object and applies the
23//! kernel's plans (exit order, entry order, default entries, default history content) one
24//! state at a time, so the order of datamodel requests is fully determined by the kernel's
25//! output. That order is what the datamodel tape ([`crate::tape`]) records for the Lean oracle.
26
27use alloc::vec::Vec;
28
29use crate::bitset::StateSet;
30use crate::compile::{CompiledChart, Domain, ROOT, StateIdx, TransIdx};
31use crate::ir::StateKind;
32
33/// The history value table: for each history state, the states recorded when its parent was
34/// last exited, or `None` when it was never exited.
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct History {
38    values: Vec<Option<StateSet>>,
39}
40
41impl History {
42    /// An empty table for a chart of `n` states.
43    pub fn new(n: usize) -> Self {
44        History {
45            values: alloc::vec![None; n],
46        }
47    }
48
49    /// The recorded value of history state `h`.
50    pub fn get(&self, h: StateIdx) -> Option<&StateSet> {
51        self.values.get(h).and_then(|v| v.as_ref())
52    }
53
54    /// Record a value for history state `h`.
55    pub fn set(&mut self, h: StateIdx, value: StateSet) {
56        if h >= self.values.len() {
57            self.values.resize(h + 1, None);
58        }
59        self.values[h] = Some(value);
60    }
61}
62
63/// Whether one event descriptor matches an event name (W3C SCXML 3.12.1): `*` matches every
64/// event; otherwise, after dropping a trailing `.*` or `.`, the descriptor must equal the name or
65/// be a prefix of it that ends at a `.` token boundary.
66// @tg: implements:: [[openspec:scxml-interpretation#Event Descriptor Matching]]
67pub fn descriptor_matches(descriptor: &str, name: &str) -> bool {
68    let d = descriptor.trim();
69    if d == "*" {
70        return true;
71    }
72    let d = d.strip_suffix(".*").unwrap_or(d);
73    let d = d.strip_suffix('.').unwrap_or(d);
74    if d.is_empty() {
75        return false;
76    }
77    if name == d {
78        return true;
79    }
80    name.len() > d.len() && name.starts_with(d) && name.as_bytes()[d.len()] == b'.'
81}
82
83/// Whether any of a transition's descriptors matches the event name.
84pub fn name_match(descriptors: &[alloc::string::String], name: &str) -> bool {
85    descriptors.iter().any(|d| descriptor_matches(d, name))
86}
87
88/// Whether a state is atomic: it has no child states (history children do not count).
89pub fn is_atomic(chart: &CompiledChart, s: StateIdx) -> bool {
90    s != ROOT
91        && chart.state(s).children.is_empty()
92        && !matches!(chart.state(s).kind, StateKind::History(_))
93}
94
95/// The proper ancestors of `s` from its parent upward, stopping before `stop`; with no `stop`,
96/// up to and including the root.
97pub fn proper_ancestors(
98    chart: &CompiledChart,
99    s: StateIdx,
100    stop: Option<StateIdx>,
101) -> Vec<StateIdx> {
102    let mut out = Vec::new();
103    let mut cur = chart.state(s).parent;
104    while let Some(p) = cur {
105        if Some(p) == stop {
106            break;
107        }
108        out.push(p);
109        cur = chart.state(p).parent;
110    }
111    out
112}
113
114fn push_unique(v: &mut Vec<StateIdx>, s: StateIdx) {
115    if !v.contains(&s) {
116        v.push(s);
117    }
118}
119
120/// `getEffectiveTargetStates`: the targets of `t` with history states replaced by their recorded
121/// states, or by the effective targets of their default transition when nothing is recorded.
122pub fn effective_targets(chart: &CompiledChart, history: &History, t: TransIdx) -> Vec<StateIdx> {
123    effective_targets_within(chart, history, t, chart.len())
124}
125
126/// [`effective_targets`] following at most `fuel` history default transitions. Validation
127/// rejects cyclic history defaults, so the bound never cuts a well-formed chart short; it only
128/// guarantees termination.
129fn effective_targets_within(
130    chart: &CompiledChart,
131    history: &History,
132    t: TransIdx,
133    fuel: usize,
134) -> Vec<StateIdx> {
135    let mut out = Vec::new();
136    for &s in &chart.transition(t).targets {
137        if matches!(chart.state(s).kind, StateKind::History(_)) {
138            match history.get(s) {
139                Some(v) => {
140                    for x in v.to_vec() {
141                        push_unique(&mut out, x);
142                    }
143                }
144                None => {
145                    if let Some(&dt) = chart.state(s).transitions.first()
146                        && fuel > 0
147                    {
148                        for x in effective_targets_within(chart, history, dt, fuel - 1) {
149                            push_unique(&mut out, x);
150                        }
151                    }
152                }
153            }
154        } else {
155            push_unique(&mut out, s);
156        }
157    }
158    out
159}
160
161/// `findLCCA`: the nearest proper ancestor of `head` that is compound (or the root) and has every
162/// state of `tail` as a descendant.
163pub fn find_lcca(chart: &CompiledChart, head: StateIdx, tail: &[StateIdx]) -> StateIdx {
164    for anc in proper_ancestors(chart, head, None) {
165        if chart.state(anc).kind == StateKind::Compound
166            && tail.iter().all(|s| chart.is_descendant(*s, anc))
167        {
168            return anc;
169        }
170    }
171    ROOT
172}
173
174/// `getTransitionDomain`, computed from scratch.
175pub fn compute_domain(chart: &CompiledChart, history: &History, t: TransIdx) -> Option<StateIdx> {
176    let tstates = effective_targets(chart, history, t);
177    if tstates.is_empty() {
178        return None;
179    }
180    let tr = chart.transition(t);
181    if tr.internal
182        && chart.state(tr.source).kind == StateKind::Compound
183        && tstates.iter().all(|s| chart.is_descendant(*s, tr.source))
184    {
185        return Some(tr.source);
186    }
187    Some(find_lcca(chart, tr.source, &tstates))
188}
189
190/// `getTransitionDomain`, using the precomputed domain when it does not depend on history.
191// @tg: implements:: [[openspec:scxml-interpretation#Internal Transitions]]
192pub fn transition_domain(
193    chart: &CompiledChart,
194    history: &History,
195    t: TransIdx,
196) -> Option<StateIdx> {
197    match chart.transition(t).domain {
198        Domain::Targetless => None,
199        Domain::Static(d) => Some(d),
200        Domain::Dynamic => compute_domain(chart, history, t),
201    }
202}
203
204/// `computeExitSet`: the active states that are proper descendants of the domains of the
205/// targeted transitions in `ts`.
206pub fn compute_exit_set(
207    chart: &CompiledChart,
208    config: &StateSet,
209    history: &History,
210    ts: &[TransIdx],
211) -> StateSet {
212    let mut out = chart.empty_set();
213    for &t in ts {
214        if chart.transition(t).targets.is_empty() {
215            continue;
216        }
217        if let Some(d) = transition_domain(chart, history, t) {
218            for s in config.to_vec() {
219                if chart.is_descendant(s, d) {
220                    out.insert(s);
221                }
222            }
223        }
224    }
225    out
226}
227
228/// `removeConflictingTransitions`: keep a transition unless its exit set intersects that of a
229/// transition kept earlier; a transition whose source descends from the earlier one's source
230/// replaces it, otherwise the earlier one wins.
231pub fn remove_conflicting_transitions(
232    chart: &CompiledChart,
233    config: &StateSet,
234    history: &History,
235    enabled: &[TransIdx],
236) -> Vec<TransIdx> {
237    let mut filtered: Vec<TransIdx> = Vec::new();
238    for &t1 in enabled {
239        let ex1 = compute_exit_set(chart, config, history, &[t1]);
240        let mut preempted = false;
241        let mut to_remove: Vec<TransIdx> = Vec::new();
242        for &t2 in &filtered {
243            let ex2 = compute_exit_set(chart, config, history, &[t2]);
244            if ex1.intersects(&ex2) {
245                if chart.is_descendant(chart.transition(t1).source, chart.transition(t2).source) {
246                    to_remove.push(t2);
247                } else {
248                    preempted = true;
249                    break;
250                }
251            }
252        }
253        if !preempted {
254            filtered.retain(|t| !to_remove.contains(t));
255            filtered.push(t1);
256        }
257    }
258    filtered
259}
260
261/// `selectTransitions` (with `Some(event)`) and `selectEventlessTransitions` (with `None`): visit
262/// the active atomic states in document order and, for each, take the first transition in
263/// document order on the state or its nearest ancestor that matches the event and whose
264/// condition holds; then remove conflicts. `cond` is called, in this order, for each candidate
265/// transition that has a condition, and answers whether it holds.
266// @tg: implements:: [[openspec:scxml-interpretation#Optimal Enabled Transition Set]]
267pub fn select_transitions<F: FnMut(TransIdx) -> bool>(
268    chart: &CompiledChart,
269    config: &StateSet,
270    history: &History,
271    event: Option<&str>,
272    mut cond: F,
273) -> Vec<TransIdx> {
274    let mut enabled: Vec<TransIdx> = Vec::new();
275    for state in config.to_vec() {
276        if !is_atomic(chart, state) {
277            continue;
278        }
279        let mut chain = alloc::vec![state];
280        chain.extend(proper_ancestors(chart, state, None));
281        'outer: for s in chain {
282            for &t in &chart.state(s).transitions {
283                let tr = chart.transition(t);
284                let matches = match event {
285                    None => tr.events.is_empty(),
286                    Some(name) => !tr.events.is_empty() && name_match(&tr.events, name),
287                };
288                if matches && (tr.cond.is_none() || cond(t)) {
289                    if !enabled.contains(&t) {
290                        enabled.push(t);
291                    }
292                    break 'outer;
293                }
294            }
295        }
296    }
297    remove_conflicting_transitions(chart, config, history, &enabled)
298}
299
300/// The history step of `exitStates`: for every state in the exit set with history children,
301/// record the active atomic descendants (deep) or active children (shallow).
302pub fn record_history(
303    chart: &CompiledChart,
304    config: &StateSet,
305    exit_set: &StateSet,
306    history: &mut History,
307) {
308    for s in exit_set.to_vec() {
309        for &h in &chart.state(s).history {
310            let deep = matches!(
311                chart.state(h).kind,
312                StateKind::History(crate::ir::HistoryType::Deep)
313            );
314            let mut v = chart.empty_set();
315            for s0 in config.to_vec() {
316                let keep = if deep {
317                    is_atomic(chart, s0) && chart.is_descendant(s0, s)
318                } else {
319                    chart.state(s0).parent == Some(s)
320                };
321                if keep {
322                    v.insert(s0);
323                }
324            }
325            history.set(h, v);
326        }
327    }
328}
329
330/// The result of `computeEntrySet`.
331#[derive(Clone, Debug, PartialEq, Eq)]
332pub struct EntryPlan {
333    /// States to enter; enter them in ascending (document) order.
334    pub states: StateSet,
335    /// Compound states entered by default, whose default-entry content runs after `onentry`.
336    pub default_entry: StateSet,
337    /// For history parents entered through a history's default transition: the parent and that
338    /// transition, whose content runs after the parent's `onentry`.
339    pub default_history_content: Vec<(StateIdx, TransIdx)>,
340}
341
342/// The domain of every transition in `ts`, in order, under the history recorded so far.
343pub fn transition_domains(
344    chart: &CompiledChart,
345    history: &History,
346    ts: &[TransIdx],
347) -> Vec<Option<StateIdx>> {
348    ts.iter()
349        .map(|&t| transition_domain(chart, history, t))
350        .collect()
351}
352
353/// `computeEntrySet`. `domains` are the transitions' domains as [`compute_exit_set`] saw them,
354/// before `exitStates` recorded history (see [`transition_domains`]): Appendix D recomputes
355/// them here from the history just recorded, which can move a domain below a state the exit
356/// left (a deep history of a parallel state with one region), and that state was then never
357/// entered again.
358// @tg: implements:: [[openspec:scxml-interpretation#History Restoration]]
359pub fn compute_entry_set(
360    chart: &CompiledChart,
361    history: &History,
362    ts: &[TransIdx],
363    domains: &[Option<StateIdx>],
364) -> EntryPlan {
365    let mut p = EntryPlan {
366        states: chart.empty_set(),
367        default_entry: chart.empty_set(),
368        default_history_content: Vec::new(),
369    };
370    for (i, &t) in ts.iter().enumerate() {
371        let ancestor = domains.get(i).copied().flatten();
372        let floor = ancestor.unwrap_or(ROOT);
373        for &s in &chart.transition(t).targets {
374            add_descendants(chart, history, s, &mut p, chart.len(), floor);
375        }
376        for s in effective_targets(chart, history, t) {
377            add_ancestors(chart, history, s, ancestor, &mut p, chart.len(), floor);
378        }
379    }
380    p
381}
382
383/// `addDescendantStatesToEnter`. `fuel` bounds the history default transitions followed (see
384/// [`effective_targets_within`]); every other recursion goes strictly deeper in the tree.
385///
386/// `floor` is the domain of the transition being entered; see [`add_ancestors`].
387fn add_descendants(
388    chart: &CompiledChart,
389    history: &History,
390    state: StateIdx,
391    p: &mut EntryPlan,
392    fuel: usize,
393    floor: StateIdx,
394) {
395    let st = chart.state(state);
396    if matches!(st.kind, StateKind::History(_)) {
397        let parent = st.parent.unwrap_or(ROOT);
398        match history.get(state) {
399            Some(v) => {
400                let v = v.to_vec();
401                for &s in &v {
402                    add_descendants(chart, history, s, p, fuel, floor);
403                }
404                for &s in &v {
405                    add_ancestors(chart, history, s, Some(parent), p, fuel, floor);
406                }
407            }
408            None => {
409                if let Some(&dt) = st.transitions.first()
410                    && fuel > 0
411                {
412                    p.default_history_content.retain(|(par, _)| *par != parent);
413                    p.default_history_content.push((parent, dt));
414                    for &s in &chart.transition(dt).targets {
415                        add_descendants(chart, history, s, p, fuel - 1, floor);
416                    }
417                    for &s in &chart.transition(dt).targets {
418                        add_ancestors(chart, history, s, Some(parent), p, fuel - 1, floor);
419                    }
420                }
421            }
422        }
423        return;
424    }
425    p.states.insert(state);
426    match st.kind {
427        StateKind::Compound => {
428            p.default_entry.insert(state);
429            if let Some(it) = st.initial {
430                for &s in &chart.transition(it).targets {
431                    add_descendants(chart, history, s, p, fuel, floor);
432                }
433                for &s in &chart.transition(it).targets {
434                    add_ancestors(chart, history, s, Some(state), p, fuel, floor);
435                }
436            }
437        }
438        StateKind::Parallel => {
439            for &child in &st.children {
440                if !p.states.any_in_range(child + 1, chart.state(child).last) {
441                    add_descendants(chart, history, child, p, fuel, floor);
442                }
443            }
444        }
445        _ => {}
446    }
447}
448
449/// `addAncestorStatesToEnter`, entering only proper descendants of `floor`, the domain of the
450/// transition being entered: the exit left the domain and everything above it active. A
451/// history state restores its states with their ancestors up to its parent, and the domain can
452/// lie below that parent (a default or recorded value inside a region of a parallel state
453/// below the parent, reached from inside that region); Appendix D then entered the active
454/// states between the domain and the parent again and completed their parallel ancestors'
455/// other regions by default next to the regions still active.
456fn add_ancestors(
457    chart: &CompiledChart,
458    history: &History,
459    state: StateIdx,
460    ancestor: Option<StateIdx>,
461    p: &mut EntryPlan,
462    fuel: usize,
463    floor: StateIdx,
464) {
465    for anc in proper_ancestors(chart, state, ancestor) {
466        if anc == ROOT || !chart.is_descendant(anc, floor) {
467            continue;
468        }
469        p.states.insert(anc);
470        if chart.state(anc).kind == StateKind::Parallel {
471            for &child in &chart.state(anc).children {
472                if !p.states.any_in_range(child + 1, chart.state(child).last) {
473                    add_descendants(chart, history, child, p, fuel, floor);
474                }
475            }
476        }
477    }
478}
479
480/// `isInFinalState`: a compound state with an active final child, or a parallel state all of
481/// whose regions are in a final state.
482pub fn is_in_final_state(chart: &CompiledChart, config: &StateSet, s: StateIdx) -> bool {
483    let st = chart.state(s);
484    match st.kind {
485        StateKind::Compound => st
486            .children
487            .iter()
488            .any(|c| chart.state(*c).kind == StateKind::Final && config.contains(*c)),
489        StateKind::Parallel => st
490            .children
491            .iter()
492            .all(|c| is_in_final_state(chart, config, *c)),
493        _ => false,
494    }
495}