Skip to main content

vertep_core/
session.rs

1//! Sessions and the pure `start` and `step` functions (W3C SCXML Appendix D, `interpret` and
2//! `mainEventLoop`).
3//!
4//! A [`Session`] is a value: the configuration as a bitset over states in document order, the
5//! history table, the internal queue, active invocations, the send and invoke counters, the
6//! late-binding status of data items and the options. [`start`] and [`step`] take a session (or
7//! create one) and an [`Input`], and return the next session, the [`Effect`]s to perform and a
8//! [`MacrostepTrace`]. They perform no I/O, read no clock and use no randomness; delays are
9//! durations inside effects and the time and seed of each input reach the datamodel only through
10//! [`Datamodel::set_environment`].
11//!
12//! The datamodel is the one mutable component kept outside the value (decision D1); it is passed
13//! as `&mut dyn Datamodel`. Executable content and condition evaluation live in this module and
14//! call it; the transition selection and entry/exit computations are delegated to the
15//! trait-object-free [`crate::kernel`].
16
17use alloc::boxed::Box;
18use alloc::collections::VecDeque;
19use alloc::format;
20use alloc::string::{String, ToString};
21use alloc::vec::Vec;
22
23use crate::bitset::StateSet;
24use crate::compile::{CompiledChart, ROOT, StateIdx, TransIdx};
25use crate::datamodel::{
26    Datamodel, Environment, EvalError, Event, EventType, SessionInfo, StateQuery, Value,
27};
28use crate::ir::{Binding, Content, Data, Exec, ExecKind, Invoke, Param, Send, StateKind};
29use crate::kernel::{self, History};
30use crate::trace::{MacrostepTrace, MicrostepTrace, TraceInput, TransitionRef};
31
32/// The default number of microsteps one macrostep may take before it is cut off.
33pub const DEFAULT_MICROSTEP_BUDGET: u32 = 10_000;
34/// The type URI of the SCXML Event I/O Processor.
35pub const SCXML_EVENT_PROCESSOR: &str = "http://www.w3.org/TR/scxml/#SCXMLEventProcessor";
36/// The type URI of SCXML invocations.
37pub const SCXML_INVOKE_TYPE: &str = "http://www.w3.org/TR/scxml/";
38
39/// Interpreter options, fixed at session start.
40#[derive(Clone, Debug, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct Options {
43    /// The maximum number of microsteps per macrostep: selections that fire transitions and
44    /// internal events consumed without enabling any (every [`MicrostepTrace`] record).
45    pub microstep_budget: u32,
46    /// `send` types the host can deliver; any other type raises `error.execution`.
47    pub send_types: Vec<String>,
48    /// `invoke` types the host can start; any other type raises `error.execution`.
49    pub invoke_types: Vec<String>,
50}
51
52impl Default for Options {
53    fn default() -> Self {
54        Options {
55            microstep_budget: DEFAULT_MICROSTEP_BUDGET,
56            send_types: alloc::vec![SCXML_EVENT_PROCESSOR.to_string(), "scxml".to_string()],
57            invoke_types: alloc::vec![
58                SCXML_INVOKE_TYPE.to_string(),
59                "http://www.w3.org/TR/scxml".to_string(),
60                "scxml".to_string(),
61            ],
62        }
63    }
64}
65
66/// Whether the session still accepts events.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69pub enum Status {
70    /// Waiting for the next external event.
71    Running,
72    /// Ended by a top-level final state or a cancel request.
73    Terminated,
74}
75
76/// A started invocation.
77#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct Invocation {
80    /// The invoking state.
81    pub state: StateIdx,
82    /// Index of the `<invoke>` in the state's invokes.
83    pub index: usize,
84    /// The invoke identifier.
85    pub invokeid: String,
86    /// Whether external events are forwarded to it.
87    pub autoforward: bool,
88    /// Whether `done.invoke.<id>` was received; a completed invocation is not cancelled.
89    pub completed: bool,
90}
91
92/// The state of one session, as a value.
93///
94/// The internal queue is part of the value: `raise`, `send` to `#_internal` and platform events
95/// (`error.*`, `done.state.*`) feed it, while external events arrive only as [`Input`]s, so
96/// internal events are always processed before the next external one.
97// @tg: implements:: [[openspec:scxml-interpretation#Internal And External Queues]]
98#[derive(Clone, Debug, PartialEq)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub struct Session {
101    /// `_sessionid`.
102    pub sessionid: String,
103    /// Running or terminated.
104    pub status: Status,
105    /// Active states (never the root).
106    pub configuration: StateSet,
107    /// Recorded history values.
108    pub history: History,
109    /// The internal event queue.
110    pub internal_queue: VecDeque<Event>,
111    /// Started, not yet cancelled invocations.
112    pub invocations: Vec<Invocation>,
113    /// States whose data items have been initialized (late binding); the root is set at start.
114    pub data_initialized: StateSet,
115    /// Counter for generated send identifiers.
116    pub send_counter: u64,
117    /// Counter for generated invoke identifiers.
118    pub invoke_counter: u64,
119    /// Number of macrosteps completed.
120    pub macrosteps: u64,
121    /// Options fixed at start.
122    pub options: Options,
123}
124
125impl Session {
126    /// Whether the session has ended.
127    pub fn is_terminated(&self) -> bool {
128        self.status == Status::Terminated
129    }
130
131    /// Active state identifiers in document order.
132    pub fn active(&self, chart: &CompiledChart) -> Vec<String> {
133        chart.ids(&self.configuration)
134    }
135
136    /// Whether the state with this identifier is active.
137    pub fn is_active(&self, chart: &CompiledChart, id: &str) -> bool {
138        chart
139            .index_of(id)
140            .is_some_and(|i| self.configuration.contains(i))
141    }
142}
143
144/// The input that starts a session.
145#[derive(Clone, Debug, Default, PartialEq)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub struct StartInput {
148    /// System variables.
149    pub info: SessionInfo,
150    /// Time and seed for the start macrostep.
151    pub env: Environment,
152    /// Interpreter options.
153    pub options: Options,
154    /// Values for top-level data items supplied by the invoking session (the evaluated
155    /// `namelist` and `<param>` of `<invoke>`, W3C SCXML 6.4.4): a top-level data item whose
156    /// identifier is listed here is initialized to the value instead of its `expr`, content or
157    /// `src`. Names that match no top-level data item are ignored.
158    #[cfg_attr(feature = "serde", serde(default))]
159    pub data: Vec<(String, Value)>,
160    /// The session identifiers the SCXML Event I/O Processor can deliver to while this input
161    /// is processed, when the host knows them. With a list, an immediate `<send>` to
162    /// `#_scxml_<id>` naming no listed session queues `error.communication` (with the send id)
163    /// on the internal queue at once, as W3C SCXML 6.2.4 requires (IRP test 496); with `None`
164    /// every such target goes to the host, which reports delivery failures back as an
165    /// `error.communication` input.
166    #[cfg_attr(feature = "serde", serde(default))]
167    pub sessions: Option<Vec<String>>,
168}
169
170/// What an input asks the session to do.
171#[derive(Clone, Debug, PartialEq)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173pub enum InputKind {
174    /// Process an external event.
175    Event(Event),
176    /// Cancel the session (the parent's or host's cancel request).
177    Cancel,
178}
179
180/// One input to [`step`]: an external event or a cancel request, plus its environment.
181#[derive(Clone, Debug, PartialEq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183pub struct Input {
184    /// Time and seed while processing this input.
185    pub env: Environment,
186    /// The request.
187    pub kind: InputKind,
188    /// The session identifiers the SCXML Event I/O Processor can deliver to while this input
189    /// is processed, when the host knows them. With a list, an immediate `<send>` to
190    /// `#_scxml_<id>` naming no listed session queues `error.communication` (with the send id)
191    /// on the internal queue at once, as W3C SCXML 6.2.4 requires (IRP test 496); with `None`
192    /// every such target goes to the host, which reports delivery failures back as an
193    /// `error.communication` input.
194    #[cfg_attr(feature = "serde", serde(default))]
195    pub sessions: Option<Vec<String>>,
196}
197
198impl Input {
199    /// An external event.
200    pub fn event(event: Event, env: Environment) -> Self {
201        Input {
202            env,
203            kind: InputKind::Event(event),
204            sessions: None,
205        }
206    }
207
208    /// A cancel request.
209    pub fn cancel(env: Environment) -> Self {
210        Input {
211            env,
212            kind: InputKind::Cancel,
213            sessions: None,
214        }
215    }
216}
217
218/// A request to deliver an event, produced by `<send>`.
219#[derive(Clone, Debug, PartialEq)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
221pub struct SendRequest {
222    /// The send identifier (declared, or generated from the session id and a counter).
223    pub sendid: String,
224    /// The event to deliver, with `sendid` set when the chart declared or stored the identifier,
225    /// and `origin`/`origintype` set for the SCXML processor.
226    pub event: Event,
227    /// The evaluated target; `None` means the session's own external queue.
228    pub target: Option<String>,
229    /// The evaluated Event I/O Processor type.
230    pub type_: String,
231    /// Delay before delivery, in milliseconds.
232    pub delay_ms: u64,
233}
234
235/// A request to start an invocation, produced at the end of a macrostep.
236#[derive(Clone, Debug, PartialEq)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238pub struct InvokeRequest {
239    /// The invoke identifier.
240    pub invokeid: String,
241    /// The invoking state identifier.
242    pub state: String,
243    /// The evaluated type.
244    pub type_: String,
245    /// The evaluated source, if any.
246    pub src: Option<String>,
247    /// Inline content: the evaluated `expr`, or the raw inline body as a string.
248    pub content: Option<Value>,
249    /// Evaluated `namelist` and `<param>` values as an object, or undefined when none.
250    pub params: Value,
251    /// Whether external events are forwarded to it.
252    pub autoforward: bool,
253}
254
255/// An interaction with the outside world, returned by the step instead of performed.
256// @tg: implements:: [[openspec:scxml-interpretation#Send And Cancel Effects]]
257#[derive(Clone, Debug, PartialEq)]
258#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
259pub enum Effect {
260    /// Deliver an event, possibly after a delay.
261    Send(SendRequest),
262    /// Cancel a pending delayed send.
263    Cancel {
264        /// The send identifier.
265        sendid: String,
266    },
267    /// Start an invocation.
268    Invoke(InvokeRequest),
269    /// Cancel an invocation whose state was exited. From then on the host must not deliver
270    /// events from that invocation; events it already queued are still delivered.
271    CancelInvoke {
272        /// The invoke identifier.
273        invokeid: String,
274    },
275    /// Forward an external event to an `autoforward` invocation.
276    Forward {
277        /// The invoke identifier.
278        invokeid: String,
279        /// The event.
280        event: Event,
281    },
282    /// `<log>`.
283    Log {
284        /// The label.
285        label: Option<String>,
286        /// The evaluated expression.
287        value: Option<Value>,
288    },
289    /// The session ended.
290    SessionDone {
291        /// The evaluated `donedata` of the top-level final state, or undefined.
292        data: Value,
293        /// True when ended by a cancel request rather than a top-level final state.
294        cancelled: bool,
295    },
296}
297
298/// The result of a macrostep.
299#[derive(Clone, Debug, PartialEq)]
300pub struct Outcome {
301    /// The session after the macrostep.
302    pub session: Session,
303    /// Effects in the order they were produced.
304    pub effects: Vec<Effect>,
305    /// The macrostep record.
306    pub trace: MacrostepTrace,
307}
308
309/// Why a session could not start.
310#[derive(Clone, Debug, PartialEq)]
311pub enum StartError {
312    /// The chart names a datamodel the host did not register.
313    UnsupportedDatamodel {
314        /// The datamodel name.
315        name: String,
316    },
317    /// The datamodel passed in is not the one the chart names.
318    DatamodelMismatch {
319        /// The chart's datamodel.
320        chart: String,
321        /// The datamodel given.
322        given: String,
323    },
324    /// The datamodel refused to initialize the session.
325    Datamodel(EvalError),
326    /// The datamodel refused to instantiate the chart, with every problem it found (a
327    /// statically checked datamodel refuses a chart with type errors).
328    ChartRefused {
329        /// The datamodel name.
330        datamodel: String,
331        /// Every problem, each with its location in the chart.
332        problems: Vec<String>,
333    },
334    /// The start macrostep did not stabilize within the microstep budget.
335    BudgetExceeded {
336        /// The budget.
337        budget: u32,
338        /// The session and effects as left when the budget ran out.
339        partial: Box<Outcome>,
340    },
341}
342
343/// Why a step was rejected or cut short.
344#[derive(Clone, Debug, PartialEq)]
345pub enum StepError {
346    /// The session has terminated and accepts no further input.
347    Terminated,
348    /// The macrostep did not stabilize within the microstep budget.
349    BudgetExceeded {
350        /// The budget.
351        budget: u32,
352        /// The session and effects as left when the budget ran out.
353        partial: Box<Outcome>,
354    },
355}
356
357impl core::fmt::Display for StartError {
358    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
359        match self {
360            StartError::UnsupportedDatamodel { name } => {
361                write!(f, "unsupported datamodel `{name}`")
362            }
363            StartError::DatamodelMismatch { chart, given } => {
364                write!(
365                    f,
366                    "the chart names datamodel `{chart}` but `{given}` was given"
367                )
368            }
369            StartError::Datamodel(e) => write!(f, "the datamodel failed to initialize: {e}"),
370            StartError::ChartRefused {
371                datamodel,
372                problems,
373            } => {
374                write!(f, "datamodel `{datamodel}` refused the chart")?;
375                for p in problems {
376                    write!(f, "\n{p}")?;
377                }
378                Ok(())
379            }
380            StartError::BudgetExceeded { budget, .. } => {
381                write!(
382                    f,
383                    "the start macrostep exceeded the microstep budget of {budget}"
384                )
385            }
386        }
387    }
388}
389
390impl core::error::Error for StartError {
391    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
392        match self {
393            StartError::Datamodel(e) => Some(e),
394            _ => None,
395        }
396    }
397}
398
399impl core::fmt::Display for StepError {
400    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
401        match self {
402            StepError::Terminated => f.write_str("the session has terminated"),
403            StepError::BudgetExceeded { budget, .. } => {
404                write!(f, "the macrostep exceeded the microstep budget of {budget}")
405            }
406        }
407    }
408}
409
410impl core::error::Error for StepError {}
411
412/// A fresh session value for `chart`, before the start macrostep.
413fn new_session(chart: &CompiledChart, sessionid: String, options: Options) -> Session {
414    Session {
415        sessionid,
416        status: Status::Running,
417        configuration: chart.empty_set(),
418        history: History::new(chart.len()),
419        internal_queue: VecDeque::new(),
420        invocations: Vec::new(),
421        data_initialized: chart.empty_set(),
422        send_counter: 0,
423        invoke_counter: 0,
424        macrosteps: 0,
425        options,
426    }
427}
428
429/// Start a session: initialize the datamodel (all data under early binding, top-level data under
430/// late binding), run the top-level script, enter the initial configuration and run macrosteps
431/// until stable. `dm` must be a fresh instance of the datamodel the chart names.
432// @tg: implements:: [[openspec:scxml-interpretation#Session Initialization]]
433// @tg: implements:: [[openspec:datamodel#Data Binding Modes]]
434// @tg: implements:: [[openspec:datamodel#System Variables]] {confidence: partial, note: "the core passes system variables and _event to the datamodel"}
435pub fn start(
436    chart: &CompiledChart,
437    input: StartInput,
438    dm: &mut dyn Datamodel,
439) -> Result<Outcome, StartError> {
440    if dm.name() != chart.datamodel() {
441        return Err(StartError::DatamodelMismatch {
442            chart: chart.datamodel().to_string(),
443            given: dm.name().to_string(),
444        });
445    }
446    dm.init_session(&input.info)
447        .map_err(StartError::Datamodel)?;
448    dm.set_environment(&input.env);
449    let session = new_session(chart, input.info.sessionid.clone(), input.options);
450    let invoker_data = input.data;
451    let mut d = Driver::new(
452        chart,
453        session,
454        dm,
455        TraceInput::Start {
456            sessionid: input.info.sessionid,
457        },
458    );
459    d.sessions = input.sessions;
460    let budget = d.s.options.microstep_budget;
461    match d.run_start(&invoker_data) {
462        Ok(()) => Ok(d.finish()),
463        Err(Budget) => Err(StartError::BudgetExceeded {
464            budget,
465            partial: Box::new(d.finish()),
466        }),
467    }
468}
469
470/// Process one input: run one macrostep for an external event, or terminate on a cancel request.
471/// Equal sessions, inputs and datamodel states give equal results.
472// @tg: implements:: [[openspec:scxml-interpretation#Pure Step Function]]
473// @tg: implements:: [[openspec:scxml-interpretation#Macrostep Execution]]
474pub fn step(
475    chart: &CompiledChart,
476    session: Session,
477    input: Input,
478    dm: &mut dyn Datamodel,
479) -> Result<Outcome, StepError> {
480    if session.status == Status::Terminated {
481        return Err(StepError::Terminated);
482    }
483    dm.set_environment(&input.env);
484    let trace_input = match &input.kind {
485        InputKind::Event(e) => TraceInput::Event(e.clone()),
486        InputKind::Cancel => TraceInput::Cancel,
487    };
488    let budget = session.options.microstep_budget;
489    let mut d = Driver::new(chart, session, dm, trace_input);
490    d.sessions = input.sessions;
491    let r = match input.kind {
492        InputKind::Cancel => {
493            d.exit_interpreter(true);
494            Ok(())
495        }
496        InputKind::Event(ev) => d.run_external(ev),
497    };
498    match r {
499        Ok(()) => Ok(d.finish()),
500        Err(Budget) => Err(StepError::BudgetExceeded {
501            budget,
502            partial: Box::new(d.finish()),
503        }),
504    }
505}
506
507/// The microstep budget ran out.
508struct Budget;
509
510/// Executable content stopped at a failing element (the error event is already queued).
511struct Stop;
512
513/// The configuration as seen by `In()`.
514struct Active<'a> {
515    chart: &'a CompiledChart,
516    config: &'a StateSet,
517}
518
519impl StateQuery for Active<'_> {
520    fn in_state(&self, id: &str) -> bool {
521        self.chart
522            .index_of(id)
523            .is_some_and(|i| self.config.contains(i))
524    }
525
526    fn active_states(&self) -> Option<Vec<String>> {
527        Some(
528            self.chart
529                .states()
530                .iter()
531                .enumerate()
532                .filter(|(i, _)| *i != 0 && self.config.contains(*i))
533                .map(|(_, s)| s.id.clone())
534                .collect(),
535        )
536    }
537}
538
539/// Message of an `error.execution` the core raises itself, not from a datamodel failure.
540pub const EXECUTION_ERROR_MESSAGE: &str = "execution error";
541/// Message of an `error.communication` for a target the core knows is unreachable.
542pub const COMMUNICATION_ERROR_MESSAGE: &str = "target session does not exist";
543
544/// A platform error event whose data is `{kind, message}`, matching the predefined `error.*`
545/// payload type. `kind` is the part of the name after `error.`.
546fn platform_error(name: &str, message: &str, sendid: Option<String>) -> Event {
547    let kind = name.strip_prefix("error.").unwrap_or(name);
548    Event {
549        name: name.to_string(),
550        type_: EventType::Platform,
551        sendid,
552        data: Value::Object(Vec::from([
553            (String::from("kind"), Value::String(kind.to_string())),
554            (String::from("message"), Value::String(message.to_string())),
555        ])),
556        ..Event::default()
557    }
558}
559
560/// The datamodel as the driver calls it: forwards every operation and remembers the error of
561/// the most recent call, so the `error.execution` it causes can carry its message.
562struct Capture<'d> {
563    inner: &'d mut dyn Datamodel,
564    last: Option<EvalError>,
565}
566
567impl Capture<'_> {
568    fn note<T>(&mut self, r: Result<T, EvalError>) -> Result<T, EvalError> {
569        self.last = r.as_ref().err().cloned();
570        r
571    }
572}
573
574impl Datamodel for Capture<'_> {
575    fn name(&self) -> &str {
576        self.inner.name()
577    }
578    fn init_session(&mut self, info: &SessionInfo) -> Result<(), EvalError> {
579        let r = self.inner.init_session(info);
580        self.note(r)
581    }
582    fn set_environment(&mut self, env: &Environment) {
583        self.inner.set_environment(env)
584    }
585    fn declare(&mut self, id: &str) -> Result<(), EvalError> {
586        let r = self.inner.declare(id);
587        self.note(r)
588    }
589    fn set_event(&mut self, event: &Event) {
590        self.inner.set_event(event)
591    }
592    fn check_event(&self, event: &Event) -> Result<(), EvalError> {
593        self.inner.check_event(event)
594    }
595    fn eval_cond(&mut self, expr: &str, states: &dyn StateQuery) -> Result<bool, EvalError> {
596        let r = self.inner.eval_cond(expr, states);
597        self.note(r)
598    }
599    fn eval_value(&mut self, expr: &str, states: &dyn StateQuery) -> Result<Value, EvalError> {
600        let r = self.inner.eval_value(expr, states);
601        self.note(r)
602    }
603    fn eval_content(&mut self, content: &str) -> Result<Value, EvalError> {
604        let r = self.inner.eval_content(content);
605        self.note(r)
606    }
607    fn assign_expr(
608        &mut self,
609        location: &str,
610        expr: &str,
611        states: &dyn StateQuery,
612    ) -> Result<(), EvalError> {
613        let r = self.inner.assign_expr(location, expr, states);
614        self.note(r)
615    }
616    fn assign_value(&mut self, location: &str, value: Value) -> Result<(), EvalError> {
617        let r = self.inner.assign_value(location, value);
618        self.note(r)
619    }
620    fn bind_var(&mut self, name: &str, value: Value) -> Result<(), EvalError> {
621        let r = self.inner.bind_var(name, value);
622        self.note(r)
623    }
624    fn is_location(&mut self, location: &str) -> bool {
625        self.last = None;
626        self.inner.is_location(location)
627    }
628    fn exec_script(&mut self, script: &str, states: &dyn StateQuery) -> Result<(), EvalError> {
629        let r = self.inner.exec_script(script, states);
630        self.note(r)
631    }
632    fn foreach_items(
633        &mut self,
634        array: &str,
635        states: &dyn StateQuery,
636    ) -> Result<Vec<Value>, EvalError> {
637        let r = self.inner.foreach_items(array, states);
638        self.note(r)
639    }
640    fn snapshot(&self) -> Value {
641        self.inner.snapshot()
642    }
643    fn restore(&mut self, snapshot: &Value) -> Result<(), EvalError> {
644        let r = self.inner.restore(snapshot);
645        self.note(r)
646    }
647}
648
649/// Parse a CSS2 time (`5s`, `250ms`, `1.5s`, or a bare number of milliseconds) into
650/// milliseconds, without floating point.
651pub fn parse_delay(text: &str) -> Option<u64> {
652    let t = text.trim();
653    let (num, factor) = if let Some(n) = t.strip_suffix("ms") {
654        (n.trim(), 1u64)
655    } else if let Some(n) = t.strip_suffix('s') {
656        (n.trim(), 1000u64)
657    } else {
658        (t, 1u64)
659    };
660    if num.is_empty() {
661        return None;
662    }
663    let (int, frac) = match num.split_once('.') {
664        Some((i, f)) => (i, f),
665        None => (num, ""),
666    };
667    if (int.is_empty() && frac.is_empty())
668        || !int.bytes().all(|b| b.is_ascii_digit())
669        || !frac.bytes().all(|b| b.is_ascii_digit())
670    {
671        return None;
672    }
673    let mut ms: u64 = 0;
674    for b in int.bytes() {
675        ms = ms.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
676    }
677    ms = ms.checked_mul(factor)?;
678    // Fractional part, truncated to whole milliseconds.
679    let mut scale = factor;
680    for b in frac.bytes() {
681        scale /= 10;
682        if scale == 0 {
683            break;
684        }
685        ms = ms.checked_add(u64::from(b - b'0') * scale)?;
686    }
687    Some(ms)
688}
689
690fn value_to_string(v: Value) -> Result<String, EvalError> {
691    match v {
692        Value::String(s) => Ok(s),
693        Value::Bool(b) => Ok(if b { "true" } else { "false" }.to_string()),
694        Value::Number(n) => {
695            if n.is_finite() && n == (n as i64) as f64 {
696                Ok(format!("{}", n as i64))
697            } else {
698                Ok(format!("{n}"))
699            }
700        }
701        other => Err(EvalError::new(format!("expected a string, got {other:?}"))),
702    }
703}
704
705/// Executes one session's macrosteps against a datamodel.
706struct Driver<'c, 'd> {
707    chart: &'c CompiledChart,
708    s: Session,
709    dm: Capture<'d>,
710    effects: Vec<Effect>,
711    to_invoke: StateSet,
712    running: bool,
713    terminated_now: bool,
714    microsteps: Vec<MicrostepTrace>,
715    used: u32,
716    input: TraceInput,
717    /// The reachable session identifiers of the input, if the host listed them.
718    sessions: Option<Vec<String>>,
719}
720
721impl<'c, 'd> Driver<'c, 'd> {
722    fn new(
723        chart: &'c CompiledChart,
724        s: Session,
725        dm: &'d mut dyn Datamodel,
726        input: TraceInput,
727    ) -> Self {
728        Driver {
729            chart,
730            s,
731            dm: Capture {
732                inner: dm,
733                last: None,
734            },
735            effects: Vec::new(),
736            to_invoke: chart.empty_set(),
737            running: true,
738            terminated_now: false,
739            microsteps: Vec::new(),
740            used: 0,
741            input,
742            sessions: None,
743        }
744    }
745
746    fn finish(mut self) -> Outcome {
747        let trace = MacrostepTrace {
748            index: self.s.macrosteps,
749            input: self.input,
750            microsteps: self.microsteps,
751            configuration: self.chart.ids(&self.s.configuration),
752            effects: self.effects.clone(),
753            terminated: self.terminated_now,
754        };
755        self.s.macrosteps += 1;
756        Outcome {
757            session: self.s,
758            effects: self.effects,
759            trace,
760        }
761    }
762
763    // ----- datamodel helpers -------------------------------------------------------------
764
765    /// Queue `error.execution`; its message is the datamodel error of the most recent
766    /// datamodel call when that call failed, otherwise [`EXECUTION_ERROR_MESSAGE`].
767    fn queue_error(&mut self, sendid: Option<String>) {
768        let last = self.dm.last.take();
769        let message = last
770            .as_ref()
771            .map_or(EXECUTION_ERROR_MESSAGE, |e| e.message.as_str());
772        self.s
773            .internal_queue
774            .push_back(platform_error("error.execution", message, sendid));
775    }
776
777    fn eval_value(&mut self, expr: &str) -> Result<Value, EvalError> {
778        let q = Active {
779            chart: self.chart,
780            config: &self.s.configuration,
781        };
782        self.dm.eval_value(expr, &q)
783    }
784
785    fn eval_string(&mut self, expr: &str) -> Result<String, EvalError> {
786        self.eval_value(expr).and_then(value_to_string)
787    }
788
789    fn eval_cond(&mut self, expr: &str) -> Result<bool, EvalError> {
790        let q = Active {
791            chart: self.chart,
792            config: &self.s.configuration,
793        };
794        self.dm.eval_cond(expr, &q)
795    }
796
797    fn eval_payload(
798        &mut self,
799        namelist: &[String],
800        params: &[Param],
801        content: Option<&Content>,
802    ) -> Result<Value, EvalError> {
803        if let Some(c) = content {
804            return match (&c.expr, &c.body) {
805                (Some(x), _) => self.eval_value(x),
806                (None, Some(b)) => self.dm.eval_content(b),
807                (None, None) => Ok(Value::Undefined),
808            };
809        }
810        if namelist.is_empty() && params.is_empty() {
811            return Ok(Value::Undefined);
812        }
813        let mut pairs = Vec::new();
814        for name in namelist {
815            let v = self.eval_value(name)?;
816            pairs.push((name.clone(), v));
817        }
818        for p in params {
819            let v = match (&p.expr, &p.location) {
820                (Some(x), _) => self.eval_value(x)?,
821                (None, Some(l)) => self.eval_value(l)?,
822                (None, None) => Value::Undefined,
823            };
824            pairs.push((p.name.clone(), v));
825        }
826        Ok(Value::Object(pairs))
827    }
828
829    fn init_data(&mut self, items: &'c [Data]) {
830        self.init_data_with(items, &[]);
831    }
832
833    /// Initialize data items; an item named in `given` takes that value (invoke parameters).
834    fn init_data_with(&mut self, items: &'c [Data], given: &[(String, Value)]) {
835        for d in items {
836            let r = if let Some((_, v)) = given.iter().find(|(n, _)| *n == d.id) {
837                self.dm.assign_value(&d.id, v.clone())
838            } else if let Some(e) = &d.expr {
839                self.eval_value(e)
840                    .and_then(|v| self.dm.assign_value(&d.id, v))
841            } else if let Some(c) = &d.content {
842                self.dm
843                    .eval_content(c)
844                    .and_then(|v| self.dm.assign_value(&d.id, v))
845            } else if let Some(src) = &d.src {
846                Err(EvalError::new(format!("data src {src} was not resolved")))
847            } else {
848                Ok(())
849            };
850            if r.is_err() {
851                self.queue_error(None);
852            }
853        }
854    }
855
856    // ----- executable content -------------------------------------------------------------
857
858    /// Run a block; an error queues `error.execution` and stops the rest of the block.
859    // @tg: implements:: [[openspec:scxml-interpretation#Execution Errors]]
860    fn exec_block(&mut self, block: &'c [Exec]) -> Result<(), Stop> {
861        for e in block {
862            self.exec(e)?;
863        }
864        Ok(())
865    }
866
867    fn fail<T>(&mut self, sendid: Option<String>) -> Result<T, Stop> {
868        self.queue_error(sendid);
869        Err(Stop)
870    }
871
872    fn exec(&mut self, e: &'c Exec) -> Result<(), Stop> {
873        match &e.kind {
874            ExecKind::Raise { event } => {
875                self.s
876                    .internal_queue
877                    .push_back(Event::named(event.clone(), EventType::Internal));
878                Ok(())
879            }
880            ExecKind::Send(send) => self.exec_send(send),
881            ExecKind::Cancel { sendid, sendidexpr } => {
882                let id = match (sendid, sendidexpr) {
883                    (Some(s), _) => s.clone(),
884                    (None, Some(x)) => match self.eval_string(x) {
885                        Ok(s) => s,
886                        Err(_) => return self.fail(None),
887                    },
888                    (None, None) => return self.fail(None),
889                };
890                self.effects.push(Effect::Cancel { sendid: id });
891                Ok(())
892            }
893            ExecKind::Log { label, expr } => {
894                let value = match expr {
895                    Some(x) => match self.eval_value(x) {
896                        Ok(v) => Some(v),
897                        Err(_) => return self.fail(None),
898                    },
899                    None => None,
900                };
901                self.effects.push(Effect::Log {
902                    label: label.clone(),
903                    value,
904                });
905                Ok(())
906            }
907            ExecKind::Assign {
908                location,
909                expr,
910                content,
911            } => {
912                let r = match (expr, content) {
913                    (Some(x), _) => {
914                        let q = Active {
915                            chart: self.chart,
916                            config: &self.s.configuration,
917                        };
918                        self.dm.assign_expr(location, x, &q)
919                    }
920                    (None, Some(c)) => self
921                        .dm
922                        .eval_content(c)
923                        .and_then(|v| self.dm.assign_value(location, v)),
924                    (None, None) => self.dm.assign_value(location, Value::Undefined),
925                };
926                if r.is_err() {
927                    return self.fail(None);
928                }
929                Ok(())
930            }
931            ExecKind::Script(script) => {
932                let r = match &script.body {
933                    Some(body) => {
934                        let q = Active {
935                            chart: self.chart,
936                            config: &self.s.configuration,
937                        };
938                        self.dm.exec_script(body, &q)
939                    }
940                    None => Err(EvalError::new("script source was not resolved")),
941                };
942                if r.is_err() {
943                    return self.fail(None);
944                }
945                Ok(())
946            }
947            ExecKind::If { branches } => {
948                for br in branches {
949                    let take = match &br.cond {
950                        None => true,
951                        Some(c) => match self.eval_cond(c) {
952                            Ok(b) => b,
953                            // The condition counts as false, error.execution is queued, and the
954                            // `<if>` element has failed, so the enclosing block stops.
955                            Err(_) => return self.fail(None),
956                        },
957                    };
958                    if take {
959                        return self.exec_block(&br.body);
960                    }
961                }
962                Ok(())
963            }
964            ExecKind::Foreach {
965                array,
966                item,
967                index,
968                body,
969            } => {
970                let items = {
971                    let q = Active {
972                        chart: self.chart,
973                        config: &self.s.configuration,
974                    };
975                    self.dm.foreach_items(array, &q)
976                };
977                let items = match items {
978                    Ok(v) => v,
979                    Err(_) => return self.fail(None),
980                };
981                for (i, v) in items.into_iter().enumerate() {
982                    if self.dm.bind_var(item, v).is_err() {
983                        return self.fail(None);
984                    }
985                    if let Some(ix) = index
986                        && self.dm.bind_var(ix, Value::Number(i as f64)).is_err()
987                    {
988                        return self.fail(None);
989                    }
990                    self.exec_block(body)?;
991                }
992                Ok(())
993            }
994            // Executable content from other namespaces is ignored.
995            ExecKind::Extension(_) => Ok(()),
996        }
997    }
998
999    fn exec_send(&mut self, send: &'c Send) -> Result<(), Stop> {
1000        // 1. Identifier, stored at `idlocation` before anything else is evaluated, so error
1001        //    events raised below can carry it.
1002        let (sendid, visible) = match (&send.id, &send.idlocation) {
1003            (Some(id), _) => (id.clone(), true),
1004            (None, loc) => {
1005                self.s.send_counter += 1;
1006                let id = format!("{}.send.{}", self.s.sessionid, self.s.send_counter);
1007                if let Some(loc) = loc
1008                    && self
1009                        .dm
1010                        .assign_value(loc, Value::String(id.clone()))
1011                        .is_err()
1012                {
1013                    return self.fail(Some(id));
1014                }
1015                (id, loc.is_some())
1016            }
1017        };
1018        let err_id = Some(sendid.clone());
1019        // 2. Event name, target, type and delay.
1020        let name = match (&send.event, &send.eventexpr) {
1021            (Some(n), _) => n.clone(),
1022            (None, Some(x)) => match self.eval_string(x) {
1023                Ok(n) => n,
1024                Err(_) => return self.fail(err_id),
1025            },
1026            (None, None) => String::new(),
1027        };
1028        let target = match (&send.target, &send.targetexpr) {
1029            (Some(t), _) => Some(t.clone()),
1030            (None, Some(x)) => match self.eval_string(x) {
1031                Ok(t) => Some(t),
1032                Err(_) => return self.fail(err_id),
1033            },
1034            (None, None) => None,
1035        };
1036        let type_ = match (&send.type_, &send.typeexpr) {
1037            (Some(t), _) => t.clone(),
1038            (None, Some(x)) => match self.eval_string(x) {
1039                Ok(t) => t,
1040                Err(_) => return self.fail(err_id),
1041            },
1042            (None, None) => SCXML_EVENT_PROCESSOR.to_string(),
1043        };
1044        let delay_text = match (&send.delay, &send.delayexpr) {
1045            (Some(d), _) => Some(d.clone()),
1046            (None, Some(x)) => match self.eval_string(x) {
1047                Ok(d) => Some(d),
1048                Err(_) => return self.fail(err_id),
1049            },
1050            (None, None) => None,
1051        };
1052        let delay_ms = match delay_text {
1053            None => 0,
1054            Some(t) => match parse_delay(&t) {
1055                Some(ms) => ms,
1056                None => return self.fail(err_id),
1057            },
1058        };
1059        // 3. Data.
1060        let data = match self.eval_payload(&send.namelist, &send.params, send.content.as_ref()) {
1061            Ok(v) => v,
1062            Err(_) => return self.fail(err_id),
1063        };
1064        // 4. Routing. An unsupported type or an illegal SCXML target is error.execution
1065        //    (W3C SCXML 6.2.4); failures to deliver are reported by the host as
1066        //    error.communication.
1067        if !self.s.options.send_types.contains(&type_) {
1068            return self.fail(err_id);
1069        }
1070        let scxml = type_ == SCXML_EVENT_PROCESSOR || type_ == "scxml";
1071        if scxml
1072            && let Some(t) = &target
1073            && !(t.starts_with("#_") || t.contains(':'))
1074        {
1075            return self.fail(err_id);
1076        }
1077        // A session the host says does not exist cannot be dispatched to: error.communication
1078        // goes on the internal queue at once and the block continues (W3C SCXML 6.2.4).
1079        if scxml
1080            && delay_ms == 0
1081            && let (Some(t), Some(known)) = (&target, &self.sessions)
1082            && let Some(id) = t.strip_prefix("#_scxml_")
1083            && !known.iter().any(|k| k == id)
1084        {
1085            self.s.internal_queue.push_back(platform_error(
1086                "error.communication",
1087                COMMUNICATION_ERROR_MESSAGE,
1088                err_id,
1089            ));
1090            return Ok(());
1091        }
1092        let exposed = if visible { Some(sendid.clone()) } else { None };
1093        if target.as_deref() == Some("#_internal") && delay_ms == 0 {
1094            self.s.internal_queue.push_back(Event {
1095                name,
1096                type_: EventType::Internal,
1097                sendid: exposed,
1098                data,
1099                ..Event::default()
1100            });
1101            return Ok(());
1102        }
1103        let event = Event {
1104            name,
1105            type_: EventType::External,
1106            sendid: exposed,
1107            origin: if scxml {
1108                Some(format!("#_scxml_{}", self.s.sessionid))
1109            } else {
1110                None
1111            },
1112            origintype: if scxml {
1113                Some(SCXML_EVENT_PROCESSOR.to_string())
1114            } else {
1115                None
1116            },
1117            invokeid: None,
1118            data,
1119            ..Event::default()
1120        };
1121        self.effects.push(Effect::Send(SendRequest {
1122            sendid,
1123            event,
1124            target,
1125            type_,
1126            delay_ms,
1127        }));
1128        Ok(())
1129    }
1130
1131    // ----- selection, microsteps, macrosteps ---------------------------------------------
1132
1133    fn select(&mut self, event: Option<&str>) -> Vec<TransIdx> {
1134        let chart = self.chart;
1135        let config = &self.s.configuration;
1136        let history = &self.s.history;
1137        let queue = &mut self.s.internal_queue;
1138        let dm = &mut self.dm;
1139        kernel::select_transitions(chart, config, history, event, |t| {
1140            let Some(cond) = chart.transition(t).cond.as_deref() else {
1141                return true;
1142            };
1143            let q = Active { chart, config };
1144            match dm.eval_cond(cond, &q) {
1145                Ok(b) => b,
1146                Err(e) => {
1147                    queue.push_back(platform_error("error.execution", &e.message, None));
1148                    false
1149                }
1150            }
1151        })
1152    }
1153
1154    fn record(&mut self, event: Option<String>, ts: &[TransIdx]) {
1155        let transitions = ts
1156            .iter()
1157            .filter_map(|t| {
1158                let tr = self.chart.transition(*t);
1159                tr.index.map(|index| TransitionRef {
1160                    source: self.chart.state(tr.source).id.clone(),
1161                    index,
1162                })
1163            })
1164            .collect();
1165        self.microsteps.push(MicrostepTrace { event, transitions });
1166    }
1167
1168    /// Charge one microstep record (a selection that fired transitions, or an event consumed
1169    /// without enabling any) against the budget. Counting the latter too bounds a macrostep in
1170    /// which every consumed event queues another one, such as a failing eventless guard
1171    /// re-raising `error.execution` forever.
1172    fn charge(&mut self) -> Result<(), Budget> {
1173        if self.used >= self.s.options.microstep_budget {
1174            return Err(Budget);
1175        }
1176        self.used += 1;
1177        Ok(())
1178    }
1179
1180    fn microstep(&mut self, event: Option<String>, ts: &[TransIdx]) -> Result<(), Budget> {
1181        self.charge()?;
1182        self.record(event, ts);
1183        let chart = self.chart;
1184        let domains = kernel::transition_domains(chart, &self.s.history, ts);
1185        self.exit_states(ts);
1186        for &t in ts {
1187            let _ = self.exec_block(&chart.transition(t).body);
1188        }
1189        self.enter_states(ts, &domains);
1190        Ok(())
1191    }
1192
1193    // @tg: implements:: [[openspec:scxml-interpretation#Exit And Entry Order]]
1194    fn exit_states(&mut self, ts: &[TransIdx]) {
1195        let chart = self.chart;
1196        let exit = kernel::compute_exit_set(chart, &self.s.configuration, &self.s.history, ts);
1197        self.to_invoke.difference_with(&exit);
1198        kernel::record_history(chart, &self.s.configuration, &exit, &mut self.s.history);
1199        for s in exit.to_vec_rev() {
1200            for b in &chart.state(s).onexit {
1201                let _ = self.exec_block(b);
1202            }
1203            self.cancel_invocations(s);
1204            self.s.configuration.remove(s);
1205        }
1206    }
1207
1208    fn cancel_invocations(&mut self, s: StateIdx) {
1209        let mut keep = Vec::new();
1210        for inv in core::mem::take(&mut self.s.invocations) {
1211            if inv.state == s {
1212                if !inv.completed {
1213                    self.effects.push(Effect::CancelInvoke {
1214                        invokeid: inv.invokeid,
1215                    });
1216                }
1217            } else {
1218                keep.push(inv);
1219            }
1220        }
1221        self.s.invocations = keep;
1222    }
1223
1224    // @tg: implements:: [[openspec:scxml-interpretation#Final States And Done Events]]
1225    fn enter_states(&mut self, ts: &[TransIdx], domains: &[Option<StateIdx>]) {
1226        let chart = self.chart;
1227        let plan = kernel::compute_entry_set(chart, &self.s.history, ts, domains);
1228        let late = chart.binding() == Binding::Late;
1229        for s in plan.states.to_vec() {
1230            self.s.configuration.insert(s);
1231            self.to_invoke.insert(s);
1232            let st = chart.state(s);
1233            if late && !self.s.data_initialized.contains(s) {
1234                self.s.data_initialized.insert(s);
1235                self.init_data(&st.data);
1236            }
1237            for b in &st.onentry {
1238                let _ = self.exec_block(b);
1239            }
1240            if plan.default_entry.contains(s)
1241                && let Some(it) = st.initial
1242            {
1243                let _ = self.exec_block(&chart.transition(it).body);
1244            }
1245            if let Some(&(_, t)) = plan.default_history_content.iter().find(|(p, _)| *p == s) {
1246                let _ = self.exec_block(&chart.transition(t).body);
1247            }
1248            if st.kind == StateKind::Final {
1249                let parent = st.parent.unwrap_or(ROOT);
1250                if parent == ROOT {
1251                    self.running = false;
1252                } else {
1253                    let data = self.done_data(s);
1254                    let pid = &chart.state(parent).id;
1255                    self.s.internal_queue.push_back(Event {
1256                        name: format!("done.state.{pid}"),
1257                        type_: EventType::Platform,
1258                        data,
1259                        ..Event::default()
1260                    });
1261                    if let Some(gp) = chart.state(parent).parent
1262                        && gp != ROOT
1263                        && chart.state(gp).kind == StateKind::Parallel
1264                        && chart
1265                            .state(gp)
1266                            .children
1267                            .iter()
1268                            .all(|c| kernel::is_in_final_state(chart, &self.s.configuration, *c))
1269                    {
1270                        let gid = &chart.state(gp).id;
1271                        self.s.internal_queue.push_back(Event::named(
1272                            format!("done.state.{gid}"),
1273                            EventType::Platform,
1274                        ));
1275                    }
1276                }
1277            }
1278        }
1279    }
1280
1281    fn done_data(&mut self, s: StateIdx) -> Value {
1282        let chart = self.chart;
1283        let Some(dd) = &chart.state(s).donedata else {
1284            return Value::Undefined;
1285        };
1286        match self.eval_payload(&[], &dd.params, dd.content.as_ref()) {
1287            Ok(v) => v,
1288            Err(_) => {
1289                self.queue_error(None);
1290                Value::Undefined
1291            }
1292        }
1293    }
1294
1295    /// The inner loop of `mainEventLoop` plus the invokes at the end of the macrostep, repeated
1296    /// while invoking raised internal events.
1297    fn run_macrostep(&mut self) -> Result<(), Budget> {
1298        loop {
1299            while self.running {
1300                let mut enabled = self.select(None);
1301                let mut name = None;
1302                if enabled.is_empty() {
1303                    match self.s.internal_queue.pop_front() {
1304                        None => break,
1305                        Some(ev) => {
1306                            self.dm.set_event(&ev);
1307                            enabled = self.select(Some(&ev.name));
1308                            if enabled.is_empty() {
1309                                self.charge()?;
1310                                self.record(Some(ev.name), &[]);
1311                                continue;
1312                            }
1313                            name = Some(ev.name);
1314                        }
1315                    }
1316                }
1317                self.microstep(name, &enabled)?;
1318            }
1319            if !self.running {
1320                self.exit_interpreter(false);
1321                return Ok(());
1322            }
1323            self.start_invokes();
1324            if self.s.internal_queue.is_empty() {
1325                return Ok(());
1326            }
1327        }
1328    }
1329
1330    fn run_start(&mut self, invoker_data: &[(String, Value)]) -> Result<(), Budget> {
1331        let chart = self.chart;
1332        // Create every data item, then initialize them per the binding mode.
1333        for s in 0..chart.len() {
1334            for d in &chart.state(s).data {
1335                if self.dm.declare(&d.id).is_err() {
1336                    self.queue_error(None);
1337                }
1338            }
1339        }
1340        match chart.binding() {
1341            Binding::Early => {
1342                for s in 0..chart.len() {
1343                    let given = if s == ROOT { invoker_data } else { &[] };
1344                    self.init_data_with(&chart.state(s).data, given);
1345                    self.s.data_initialized.insert(s);
1346                }
1347            }
1348            Binding::Late => {
1349                self.init_data_with(&chart.state(ROOT).data, invoker_data);
1350                self.s.data_initialized.insert(ROOT);
1351            }
1352        }
1353        if let Some(script) = &chart.chart().script {
1354            let r = match &script.body {
1355                Some(body) => {
1356                    let q = Active {
1357                        chart,
1358                        config: &self.s.configuration,
1359                    };
1360                    self.dm.exec_script(body, &q)
1361                }
1362                None => Err(EvalError::new("script source was not resolved")),
1363            };
1364            if r.is_err() {
1365                self.queue_error(None);
1366            }
1367        }
1368        if let Some(it) = chart.state(ROOT).initial {
1369            let domains = kernel::transition_domains(chart, &self.s.history, &[it]);
1370            self.enter_states(&[it], &domains);
1371        }
1372        self.run_macrostep()
1373    }
1374
1375    // @tg: implements:: [[openspec:scxml-interpretation#Invoke Lifecycle]]
1376    fn run_external(&mut self, ev: Event) -> Result<(), Budget> {
1377        // Every event the host delivers is processed. Ignoring events from a cancelled
1378        // invocation is the host's job when it receives them (W3C SCXML 6.4: once cancelled,
1379        // the processor MUST NOT insert the invoked session's events into the external queue);
1380        // events queued before the cancellation, such as the `done.invoke.<id>` of a child that
1381        // finished first, are still processed (IRP test 236). Only active invocations run
1382        // `finalize` or get the event forwarded.
1383        if let Some(rest) = ev.name.strip_prefix("done.invoke.") {
1384            for inv in self.s.invocations.iter_mut() {
1385                if inv.invokeid == rest || ev.invokeid.as_deref() == Some(inv.invokeid.as_str()) {
1386                    inv.completed = true;
1387                }
1388            }
1389        }
1390        self.dm.set_event(&ev);
1391        let mut invs = self.s.invocations.clone();
1392        invs.sort_by_key(|i| (i.state, i.index));
1393        let chart = self.chart;
1394        for inv in invs {
1395            if ev.invokeid.as_deref() == Some(inv.invokeid.as_str()) {
1396                let finalize = &chart.state(inv.state).invokes[inv.index].finalize;
1397                let _ = self.exec_block(finalize);
1398            }
1399            if inv.autoforward && !inv.completed {
1400                self.effects.push(Effect::Forward {
1401                    invokeid: inv.invokeid.clone(),
1402                    event: ev.clone(),
1403                });
1404            }
1405        }
1406        let enabled = self.select(Some(&ev.name));
1407        if enabled.is_empty() {
1408            self.record(Some(ev.name), &[]);
1409        } else {
1410            self.microstep(Some(ev.name), &enabled)?;
1411        }
1412        self.run_macrostep()
1413    }
1414
1415    fn start_invokes(&mut self) {
1416        let chart = self.chart;
1417        for s in self.to_invoke.to_vec() {
1418            for (i, inv) in chart.state(s).invokes.iter().enumerate() {
1419                self.invoke(s, i, inv);
1420            }
1421        }
1422        self.to_invoke.clear();
1423    }
1424
1425    fn invoke(&mut self, s: StateIdx, index: usize, inv: &'c Invoke) {
1426        let chart = self.chart;
1427        let state_id = &chart.state(s).id;
1428        let invokeid = match &inv.id {
1429            Some(id) => id.clone(),
1430            None => {
1431                self.s.invoke_counter += 1;
1432                let id = format!("{}.{}", state_id, self.s.invoke_counter);
1433                if let Some(loc) = &inv.idlocation
1434                    && self
1435                        .dm
1436                        .assign_value(loc, Value::String(id.clone()))
1437                        .is_err()
1438                {
1439                    self.queue_error(None);
1440                    return;
1441                }
1442                id
1443            }
1444        };
1445        let type_ = match (&inv.type_, &inv.typeexpr) {
1446            (Some(t), _) => Ok(t.clone()),
1447            (None, Some(x)) => self.eval_string(x),
1448            (None, None) => Ok(SCXML_INVOKE_TYPE.to_string()),
1449        };
1450        let src = match (&inv.src, &inv.srcexpr) {
1451            (Some(t), _) => Ok(Some(t.clone())),
1452            (None, Some(x)) => self.eval_string(x).map(Some),
1453            (None, None) => Ok(None),
1454        };
1455        let content = match &inv.content {
1456            Some(Content { expr: Some(x), .. }) => self.eval_value(x).map(Some),
1457            Some(Content {
1458                expr: None,
1459                body: Some(b),
1460                ..
1461            }) => Ok(Some(Value::String(b.clone()))),
1462            _ => Ok(None),
1463        };
1464        let params = self.eval_payload(&inv.namelist, &inv.params, None);
1465        let (Ok(type_), Ok(src), Ok(content), Ok(params)) = (type_, src, content, params) else {
1466            self.queue_error(None);
1467            return;
1468        };
1469        if !self.s.options.invoke_types.contains(&type_) {
1470            self.queue_error(None);
1471            return;
1472        }
1473        self.s.invocations.push(Invocation {
1474            state: s,
1475            index,
1476            invokeid: invokeid.clone(),
1477            autoforward: inv.autoforward,
1478            completed: false,
1479        });
1480        self.effects.push(Effect::Invoke(InvokeRequest {
1481            invokeid,
1482            state: state_id.clone(),
1483            type_,
1484            src,
1485            content,
1486            params,
1487            autoforward: inv.autoforward,
1488        }));
1489    }
1490
1491    /// `exitInterpreter`: exit every active state in exit order, cancel invocations and emit
1492    /// one session-done effect.
1493    // @tg: implements:: [[openspec:scxml-interpretation#Session Termination]]
1494    fn exit_interpreter(&mut self, cancelled: bool) {
1495        let chart = self.chart;
1496        let mut data = Value::Undefined;
1497        for s in self.s.configuration.to_vec_rev() {
1498            for b in &chart.state(s).onexit {
1499                let _ = self.exec_block(b);
1500            }
1501            self.cancel_invocations(s);
1502            self.s.configuration.remove(s);
1503            if chart.state(s).kind == StateKind::Final && chart.state(s).parent == Some(ROOT) {
1504                data = self.done_data(s);
1505            }
1506        }
1507        self.effects.push(Effect::SessionDone { data, cancelled });
1508        self.s.internal_queue.clear();
1509        self.to_invoke.clear();
1510        self.s.status = Status::Terminated;
1511        self.running = false;
1512        self.terminated_now = true;
1513    }
1514}
1515
1516#[cfg(test)]
1517mod error_tests {
1518    use super::*;
1519    use alloc::string::ToString;
1520    use alloc::vec;
1521
1522    #[test]
1523    fn start_error_displays_and_boxes() {
1524        let e = StartError::ChartRefused {
1525            datamodel: "cel".into(),
1526            problems: vec!["state `s` › cond: not bool".into()],
1527        };
1528        assert_eq!(
1529            e.to_string(),
1530            "datamodel `cel` refused the chart\nstate `s` › cond: not bool"
1531        );
1532        let e = StartError::Datamodel(EvalError::new("boom"));
1533        assert_eq!(e.to_string(), "the datamodel failed to initialize: boom");
1534        assert_eq!(core::error::Error::source(&e).unwrap().to_string(), "boom");
1535        fn run() -> Result<(), alloc::boxed::Box<dyn core::error::Error>> {
1536            Err(StartError::UnsupportedDatamodel { name: "x".into() })?
1537        }
1538        assert_eq!(run().unwrap_err().to_string(), "unsupported datamodel `x`");
1539    }
1540
1541    #[test]
1542    fn step_error_displays_and_boxes() {
1543        fn run() -> Result<(), alloc::boxed::Box<dyn core::error::Error>> {
1544            Err(StepError::Terminated)?
1545        }
1546        assert_eq!(run().unwrap_err().to_string(), "the session has terminated");
1547    }
1548}