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
357/// A fresh session value for `chart`, before the start macrostep.
358fn new_session(chart: &CompiledChart, sessionid: String, options: Options) -> Session {
359    Session {
360        sessionid,
361        status: Status::Running,
362        configuration: chart.empty_set(),
363        history: History::new(chart.len()),
364        internal_queue: VecDeque::new(),
365        invocations: Vec::new(),
366        data_initialized: chart.empty_set(),
367        send_counter: 0,
368        invoke_counter: 0,
369        macrosteps: 0,
370        options,
371    }
372}
373
374/// Start a session: initialize the datamodel (all data under early binding, top-level data under
375/// late binding), run the top-level script, enter the initial configuration and run macrosteps
376/// until stable. `dm` must be a fresh instance of the datamodel the chart names.
377// @tg: implements:: [[openspec:scxml-interpretation#Session Initialization]]
378// @tg: implements:: [[openspec:datamodel#Data Binding Modes]]
379// @tg: implements:: [[openspec:datamodel#System Variables]] {confidence: partial, note: "the core passes system variables and _event to the datamodel"}
380pub fn start(
381    chart: &CompiledChart,
382    input: StartInput,
383    dm: &mut dyn Datamodel,
384) -> Result<Outcome, StartError> {
385    if dm.name() != chart.datamodel() {
386        return Err(StartError::DatamodelMismatch {
387            chart: chart.datamodel().to_string(),
388            given: dm.name().to_string(),
389        });
390    }
391    dm.init_session(&input.info)
392        .map_err(StartError::Datamodel)?;
393    dm.set_environment(&input.env);
394    let session = new_session(chart, input.info.sessionid.clone(), input.options);
395    let invoker_data = input.data;
396    let mut d = Driver::new(
397        chart,
398        session,
399        dm,
400        TraceInput::Start {
401            sessionid: input.info.sessionid,
402        },
403    );
404    d.sessions = input.sessions;
405    let budget = d.s.options.microstep_budget;
406    match d.run_start(&invoker_data) {
407        Ok(()) => Ok(d.finish()),
408        Err(Budget) => Err(StartError::BudgetExceeded {
409            budget,
410            partial: Box::new(d.finish()),
411        }),
412    }
413}
414
415/// Process one input: run one macrostep for an external event, or terminate on a cancel request.
416/// Equal sessions, inputs and datamodel states give equal results.
417// @tg: implements:: [[openspec:scxml-interpretation#Pure Step Function]]
418// @tg: implements:: [[openspec:scxml-interpretation#Macrostep Execution]]
419pub fn step(
420    chart: &CompiledChart,
421    session: Session,
422    input: Input,
423    dm: &mut dyn Datamodel,
424) -> Result<Outcome, StepError> {
425    if session.status == Status::Terminated {
426        return Err(StepError::Terminated);
427    }
428    dm.set_environment(&input.env);
429    let trace_input = match &input.kind {
430        InputKind::Event(e) => TraceInput::Event(e.clone()),
431        InputKind::Cancel => TraceInput::Cancel,
432    };
433    let budget = session.options.microstep_budget;
434    let mut d = Driver::new(chart, session, dm, trace_input);
435    d.sessions = input.sessions;
436    let r = match input.kind {
437        InputKind::Cancel => {
438            d.exit_interpreter(true);
439            Ok(())
440        }
441        InputKind::Event(ev) => d.run_external(ev),
442    };
443    match r {
444        Ok(()) => Ok(d.finish()),
445        Err(Budget) => Err(StepError::BudgetExceeded {
446            budget,
447            partial: Box::new(d.finish()),
448        }),
449    }
450}
451
452/// The microstep budget ran out.
453struct Budget;
454
455/// Executable content stopped at a failing element (the error event is already queued).
456struct Stop;
457
458/// The configuration as seen by `In()`.
459struct Active<'a> {
460    chart: &'a CompiledChart,
461    config: &'a StateSet,
462}
463
464impl StateQuery for Active<'_> {
465    fn in_state(&self, id: &str) -> bool {
466        self.chart
467            .index_of(id)
468            .is_some_and(|i| self.config.contains(i))
469    }
470
471    fn active_states(&self) -> Option<Vec<String>> {
472        Some(
473            self.chart
474                .states()
475                .iter()
476                .enumerate()
477                .filter(|(i, _)| *i != 0 && self.config.contains(*i))
478                .map(|(_, s)| s.id.clone())
479                .collect(),
480        )
481    }
482}
483
484/// Message of an `error.execution` the core raises itself, not from a datamodel failure.
485pub const EXECUTION_ERROR_MESSAGE: &str = "execution error";
486/// Message of an `error.communication` for a target the core knows is unreachable.
487pub const COMMUNICATION_ERROR_MESSAGE: &str = "target session does not exist";
488
489/// A platform error event whose data is `{kind, message}`, matching the predefined `error.*`
490/// payload type. `kind` is the part of the name after `error.`.
491fn platform_error(name: &str, message: &str, sendid: Option<String>) -> Event {
492    let kind = name.strip_prefix("error.").unwrap_or(name);
493    Event {
494        name: name.to_string(),
495        type_: EventType::Platform,
496        sendid,
497        data: Value::Object(Vec::from([
498            (String::from("kind"), Value::String(kind.to_string())),
499            (String::from("message"), Value::String(message.to_string())),
500        ])),
501        ..Event::default()
502    }
503}
504
505/// The datamodel as the driver calls it: forwards every operation and remembers the error of
506/// the most recent call, so the `error.execution` it causes can carry its message.
507struct Capture<'d> {
508    inner: &'d mut dyn Datamodel,
509    last: Option<EvalError>,
510}
511
512impl Capture<'_> {
513    fn note<T>(&mut self, r: Result<T, EvalError>) -> Result<T, EvalError> {
514        self.last = r.as_ref().err().cloned();
515        r
516    }
517}
518
519impl Datamodel for Capture<'_> {
520    fn name(&self) -> &str {
521        self.inner.name()
522    }
523    fn init_session(&mut self, info: &SessionInfo) -> Result<(), EvalError> {
524        let r = self.inner.init_session(info);
525        self.note(r)
526    }
527    fn set_environment(&mut self, env: &Environment) {
528        self.inner.set_environment(env)
529    }
530    fn declare(&mut self, id: &str) -> Result<(), EvalError> {
531        let r = self.inner.declare(id);
532        self.note(r)
533    }
534    fn set_event(&mut self, event: &Event) {
535        self.inner.set_event(event)
536    }
537    fn check_event(&self, event: &Event) -> Result<(), EvalError> {
538        self.inner.check_event(event)
539    }
540    fn eval_cond(&mut self, expr: &str, states: &dyn StateQuery) -> Result<bool, EvalError> {
541        let r = self.inner.eval_cond(expr, states);
542        self.note(r)
543    }
544    fn eval_value(&mut self, expr: &str, states: &dyn StateQuery) -> Result<Value, EvalError> {
545        let r = self.inner.eval_value(expr, states);
546        self.note(r)
547    }
548    fn eval_content(&mut self, content: &str) -> Result<Value, EvalError> {
549        let r = self.inner.eval_content(content);
550        self.note(r)
551    }
552    fn assign_expr(
553        &mut self,
554        location: &str,
555        expr: &str,
556        states: &dyn StateQuery,
557    ) -> Result<(), EvalError> {
558        let r = self.inner.assign_expr(location, expr, states);
559        self.note(r)
560    }
561    fn assign_value(&mut self, location: &str, value: Value) -> Result<(), EvalError> {
562        let r = self.inner.assign_value(location, value);
563        self.note(r)
564    }
565    fn bind_var(&mut self, name: &str, value: Value) -> Result<(), EvalError> {
566        let r = self.inner.bind_var(name, value);
567        self.note(r)
568    }
569    fn is_location(&mut self, location: &str) -> bool {
570        self.last = None;
571        self.inner.is_location(location)
572    }
573    fn exec_script(&mut self, script: &str, states: &dyn StateQuery) -> Result<(), EvalError> {
574        let r = self.inner.exec_script(script, states);
575        self.note(r)
576    }
577    fn foreach_items(
578        &mut self,
579        array: &str,
580        states: &dyn StateQuery,
581    ) -> Result<Vec<Value>, EvalError> {
582        let r = self.inner.foreach_items(array, states);
583        self.note(r)
584    }
585    fn snapshot(&self) -> Value {
586        self.inner.snapshot()
587    }
588    fn restore(&mut self, snapshot: &Value) -> Result<(), EvalError> {
589        let r = self.inner.restore(snapshot);
590        self.note(r)
591    }
592}
593
594/// Parse a CSS2 time (`5s`, `250ms`, `1.5s`, or a bare number of milliseconds) into
595/// milliseconds, without floating point.
596pub fn parse_delay(text: &str) -> Option<u64> {
597    let t = text.trim();
598    let (num, factor) = if let Some(n) = t.strip_suffix("ms") {
599        (n.trim(), 1u64)
600    } else if let Some(n) = t.strip_suffix('s') {
601        (n.trim(), 1000u64)
602    } else {
603        (t, 1u64)
604    };
605    if num.is_empty() {
606        return None;
607    }
608    let (int, frac) = match num.split_once('.') {
609        Some((i, f)) => (i, f),
610        None => (num, ""),
611    };
612    if (int.is_empty() && frac.is_empty())
613        || !int.bytes().all(|b| b.is_ascii_digit())
614        || !frac.bytes().all(|b| b.is_ascii_digit())
615    {
616        return None;
617    }
618    let mut ms: u64 = 0;
619    for b in int.bytes() {
620        ms = ms.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
621    }
622    ms = ms.checked_mul(factor)?;
623    // Fractional part, truncated to whole milliseconds.
624    let mut scale = factor;
625    for b in frac.bytes() {
626        scale /= 10;
627        if scale == 0 {
628            break;
629        }
630        ms = ms.checked_add(u64::from(b - b'0') * scale)?;
631    }
632    Some(ms)
633}
634
635fn value_to_string(v: Value) -> Result<String, EvalError> {
636    match v {
637        Value::String(s) => Ok(s),
638        Value::Bool(b) => Ok(if b { "true" } else { "false" }.to_string()),
639        Value::Number(n) => {
640            if n.is_finite() && n == (n as i64) as f64 {
641                Ok(format!("{}", n as i64))
642            } else {
643                Ok(format!("{n}"))
644            }
645        }
646        other => Err(EvalError::new(format!("expected a string, got {other:?}"))),
647    }
648}
649
650/// Executes one session's macrosteps against a datamodel.
651struct Driver<'c, 'd> {
652    chart: &'c CompiledChart,
653    s: Session,
654    dm: Capture<'d>,
655    effects: Vec<Effect>,
656    to_invoke: StateSet,
657    running: bool,
658    terminated_now: bool,
659    microsteps: Vec<MicrostepTrace>,
660    used: u32,
661    input: TraceInput,
662    /// The reachable session identifiers of the input, if the host listed them.
663    sessions: Option<Vec<String>>,
664}
665
666impl<'c, 'd> Driver<'c, 'd> {
667    fn new(
668        chart: &'c CompiledChart,
669        s: Session,
670        dm: &'d mut dyn Datamodel,
671        input: TraceInput,
672    ) -> Self {
673        Driver {
674            chart,
675            s,
676            dm: Capture {
677                inner: dm,
678                last: None,
679            },
680            effects: Vec::new(),
681            to_invoke: chart.empty_set(),
682            running: true,
683            terminated_now: false,
684            microsteps: Vec::new(),
685            used: 0,
686            input,
687            sessions: None,
688        }
689    }
690
691    fn finish(mut self) -> Outcome {
692        let trace = MacrostepTrace {
693            index: self.s.macrosteps,
694            input: self.input,
695            microsteps: self.microsteps,
696            configuration: self.chart.ids(&self.s.configuration),
697            effects: self.effects.clone(),
698            terminated: self.terminated_now,
699        };
700        self.s.macrosteps += 1;
701        Outcome {
702            session: self.s,
703            effects: self.effects,
704            trace,
705        }
706    }
707
708    // ----- datamodel helpers -------------------------------------------------------------
709
710    /// Queue `error.execution`; its message is the datamodel error of the most recent
711    /// datamodel call when that call failed, otherwise [`EXECUTION_ERROR_MESSAGE`].
712    fn queue_error(&mut self, sendid: Option<String>) {
713        let last = self.dm.last.take();
714        let message = last
715            .as_ref()
716            .map_or(EXECUTION_ERROR_MESSAGE, |e| e.message.as_str());
717        self.s
718            .internal_queue
719            .push_back(platform_error("error.execution", message, sendid));
720    }
721
722    fn eval_value(&mut self, expr: &str) -> Result<Value, EvalError> {
723        let q = Active {
724            chart: self.chart,
725            config: &self.s.configuration,
726        };
727        self.dm.eval_value(expr, &q)
728    }
729
730    fn eval_string(&mut self, expr: &str) -> Result<String, EvalError> {
731        self.eval_value(expr).and_then(value_to_string)
732    }
733
734    fn eval_cond(&mut self, expr: &str) -> Result<bool, EvalError> {
735        let q = Active {
736            chart: self.chart,
737            config: &self.s.configuration,
738        };
739        self.dm.eval_cond(expr, &q)
740    }
741
742    fn eval_payload(
743        &mut self,
744        namelist: &[String],
745        params: &[Param],
746        content: Option<&Content>,
747    ) -> Result<Value, EvalError> {
748        if let Some(c) = content {
749            return match (&c.expr, &c.body) {
750                (Some(x), _) => self.eval_value(x),
751                (None, Some(b)) => self.dm.eval_content(b),
752                (None, None) => Ok(Value::Undefined),
753            };
754        }
755        if namelist.is_empty() && params.is_empty() {
756            return Ok(Value::Undefined);
757        }
758        let mut pairs = Vec::new();
759        for name in namelist {
760            let v = self.eval_value(name)?;
761            pairs.push((name.clone(), v));
762        }
763        for p in params {
764            let v = match (&p.expr, &p.location) {
765                (Some(x), _) => self.eval_value(x)?,
766                (None, Some(l)) => self.eval_value(l)?,
767                (None, None) => Value::Undefined,
768            };
769            pairs.push((p.name.clone(), v));
770        }
771        Ok(Value::Object(pairs))
772    }
773
774    fn init_data(&mut self, items: &'c [Data]) {
775        self.init_data_with(items, &[]);
776    }
777
778    /// Initialize data items; an item named in `given` takes that value (invoke parameters).
779    fn init_data_with(&mut self, items: &'c [Data], given: &[(String, Value)]) {
780        for d in items {
781            let r = if let Some((_, v)) = given.iter().find(|(n, _)| *n == d.id) {
782                self.dm.assign_value(&d.id, v.clone())
783            } else if let Some(e) = &d.expr {
784                self.eval_value(e)
785                    .and_then(|v| self.dm.assign_value(&d.id, v))
786            } else if let Some(c) = &d.content {
787                self.dm
788                    .eval_content(c)
789                    .and_then(|v| self.dm.assign_value(&d.id, v))
790            } else if let Some(src) = &d.src {
791                Err(EvalError::new(format!("data src {src} was not resolved")))
792            } else {
793                Ok(())
794            };
795            if r.is_err() {
796                self.queue_error(None);
797            }
798        }
799    }
800
801    // ----- executable content -------------------------------------------------------------
802
803    /// Run a block; an error queues `error.execution` and stops the rest of the block.
804    // @tg: implements:: [[openspec:scxml-interpretation#Execution Errors]]
805    fn exec_block(&mut self, block: &'c [Exec]) -> Result<(), Stop> {
806        for e in block {
807            self.exec(e)?;
808        }
809        Ok(())
810    }
811
812    fn fail<T>(&mut self, sendid: Option<String>) -> Result<T, Stop> {
813        self.queue_error(sendid);
814        Err(Stop)
815    }
816
817    fn exec(&mut self, e: &'c Exec) -> Result<(), Stop> {
818        match &e.kind {
819            ExecKind::Raise { event } => {
820                self.s
821                    .internal_queue
822                    .push_back(Event::named(event.clone(), EventType::Internal));
823                Ok(())
824            }
825            ExecKind::Send(send) => self.exec_send(send),
826            ExecKind::Cancel { sendid, sendidexpr } => {
827                let id = match (sendid, sendidexpr) {
828                    (Some(s), _) => s.clone(),
829                    (None, Some(x)) => match self.eval_string(x) {
830                        Ok(s) => s,
831                        Err(_) => return self.fail(None),
832                    },
833                    (None, None) => return self.fail(None),
834                };
835                self.effects.push(Effect::Cancel { sendid: id });
836                Ok(())
837            }
838            ExecKind::Log { label, expr } => {
839                let value = match expr {
840                    Some(x) => match self.eval_value(x) {
841                        Ok(v) => Some(v),
842                        Err(_) => return self.fail(None),
843                    },
844                    None => None,
845                };
846                self.effects.push(Effect::Log {
847                    label: label.clone(),
848                    value,
849                });
850                Ok(())
851            }
852            ExecKind::Assign {
853                location,
854                expr,
855                content,
856            } => {
857                let r = match (expr, content) {
858                    (Some(x), _) => {
859                        let q = Active {
860                            chart: self.chart,
861                            config: &self.s.configuration,
862                        };
863                        self.dm.assign_expr(location, x, &q)
864                    }
865                    (None, Some(c)) => self
866                        .dm
867                        .eval_content(c)
868                        .and_then(|v| self.dm.assign_value(location, v)),
869                    (None, None) => self.dm.assign_value(location, Value::Undefined),
870                };
871                if r.is_err() {
872                    return self.fail(None);
873                }
874                Ok(())
875            }
876            ExecKind::Script(script) => {
877                let r = match &script.body {
878                    Some(body) => {
879                        let q = Active {
880                            chart: self.chart,
881                            config: &self.s.configuration,
882                        };
883                        self.dm.exec_script(body, &q)
884                    }
885                    None => Err(EvalError::new("script source was not resolved")),
886                };
887                if r.is_err() {
888                    return self.fail(None);
889                }
890                Ok(())
891            }
892            ExecKind::If { branches } => {
893                for br in branches {
894                    let take = match &br.cond {
895                        None => true,
896                        Some(c) => match self.eval_cond(c) {
897                            Ok(b) => b,
898                            // The condition counts as false, error.execution is queued, and the
899                            // `<if>` element has failed, so the enclosing block stops.
900                            Err(_) => return self.fail(None),
901                        },
902                    };
903                    if take {
904                        return self.exec_block(&br.body);
905                    }
906                }
907                Ok(())
908            }
909            ExecKind::Foreach {
910                array,
911                item,
912                index,
913                body,
914            } => {
915                let items = {
916                    let q = Active {
917                        chart: self.chart,
918                        config: &self.s.configuration,
919                    };
920                    self.dm.foreach_items(array, &q)
921                };
922                let items = match items {
923                    Ok(v) => v,
924                    Err(_) => return self.fail(None),
925                };
926                for (i, v) in items.into_iter().enumerate() {
927                    if self.dm.bind_var(item, v).is_err() {
928                        return self.fail(None);
929                    }
930                    if let Some(ix) = index
931                        && self.dm.bind_var(ix, Value::Number(i as f64)).is_err()
932                    {
933                        return self.fail(None);
934                    }
935                    self.exec_block(body)?;
936                }
937                Ok(())
938            }
939            // Executable content from other namespaces is ignored.
940            ExecKind::Extension(_) => Ok(()),
941        }
942    }
943
944    fn exec_send(&mut self, send: &'c Send) -> Result<(), Stop> {
945        // 1. Identifier, stored at `idlocation` before anything else is evaluated, so error
946        //    events raised below can carry it.
947        let (sendid, visible) = match (&send.id, &send.idlocation) {
948            (Some(id), _) => (id.clone(), true),
949            (None, loc) => {
950                self.s.send_counter += 1;
951                let id = format!("{}.send.{}", self.s.sessionid, self.s.send_counter);
952                if let Some(loc) = loc
953                    && self
954                        .dm
955                        .assign_value(loc, Value::String(id.clone()))
956                        .is_err()
957                {
958                    return self.fail(Some(id));
959                }
960                (id, loc.is_some())
961            }
962        };
963        let err_id = Some(sendid.clone());
964        // 2. Event name, target, type and delay.
965        let name = match (&send.event, &send.eventexpr) {
966            (Some(n), _) => n.clone(),
967            (None, Some(x)) => match self.eval_string(x) {
968                Ok(n) => n,
969                Err(_) => return self.fail(err_id),
970            },
971            (None, None) => String::new(),
972        };
973        let target = match (&send.target, &send.targetexpr) {
974            (Some(t), _) => Some(t.clone()),
975            (None, Some(x)) => match self.eval_string(x) {
976                Ok(t) => Some(t),
977                Err(_) => return self.fail(err_id),
978            },
979            (None, None) => None,
980        };
981        let type_ = match (&send.type_, &send.typeexpr) {
982            (Some(t), _) => t.clone(),
983            (None, Some(x)) => match self.eval_string(x) {
984                Ok(t) => t,
985                Err(_) => return self.fail(err_id),
986            },
987            (None, None) => SCXML_EVENT_PROCESSOR.to_string(),
988        };
989        let delay_text = match (&send.delay, &send.delayexpr) {
990            (Some(d), _) => Some(d.clone()),
991            (None, Some(x)) => match self.eval_string(x) {
992                Ok(d) => Some(d),
993                Err(_) => return self.fail(err_id),
994            },
995            (None, None) => None,
996        };
997        let delay_ms = match delay_text {
998            None => 0,
999            Some(t) => match parse_delay(&t) {
1000                Some(ms) => ms,
1001                None => return self.fail(err_id),
1002            },
1003        };
1004        // 3. Data.
1005        let data = match self.eval_payload(&send.namelist, &send.params, send.content.as_ref()) {
1006            Ok(v) => v,
1007            Err(_) => return self.fail(err_id),
1008        };
1009        // 4. Routing. An unsupported type or an illegal SCXML target is error.execution
1010        //    (W3C SCXML 6.2.4); failures to deliver are reported by the host as
1011        //    error.communication.
1012        if !self.s.options.send_types.contains(&type_) {
1013            return self.fail(err_id);
1014        }
1015        let scxml = type_ == SCXML_EVENT_PROCESSOR || type_ == "scxml";
1016        if scxml
1017            && let Some(t) = &target
1018            && !(t.starts_with("#_") || t.contains(':'))
1019        {
1020            return self.fail(err_id);
1021        }
1022        // A session the host says does not exist cannot be dispatched to: error.communication
1023        // goes on the internal queue at once and the block continues (W3C SCXML 6.2.4).
1024        if scxml
1025            && delay_ms == 0
1026            && let (Some(t), Some(known)) = (&target, &self.sessions)
1027            && let Some(id) = t.strip_prefix("#_scxml_")
1028            && !known.iter().any(|k| k == id)
1029        {
1030            self.s.internal_queue.push_back(platform_error(
1031                "error.communication",
1032                COMMUNICATION_ERROR_MESSAGE,
1033                err_id,
1034            ));
1035            return Ok(());
1036        }
1037        let exposed = if visible { Some(sendid.clone()) } else { None };
1038        if target.as_deref() == Some("#_internal") && delay_ms == 0 {
1039            self.s.internal_queue.push_back(Event {
1040                name,
1041                type_: EventType::Internal,
1042                sendid: exposed,
1043                data,
1044                ..Event::default()
1045            });
1046            return Ok(());
1047        }
1048        let event = Event {
1049            name,
1050            type_: EventType::External,
1051            sendid: exposed,
1052            origin: if scxml {
1053                Some(format!("#_scxml_{}", self.s.sessionid))
1054            } else {
1055                None
1056            },
1057            origintype: if scxml {
1058                Some(SCXML_EVENT_PROCESSOR.to_string())
1059            } else {
1060                None
1061            },
1062            invokeid: None,
1063            data,
1064            ..Event::default()
1065        };
1066        self.effects.push(Effect::Send(SendRequest {
1067            sendid,
1068            event,
1069            target,
1070            type_,
1071            delay_ms,
1072        }));
1073        Ok(())
1074    }
1075
1076    // ----- selection, microsteps, macrosteps ---------------------------------------------
1077
1078    fn select(&mut self, event: Option<&str>) -> Vec<TransIdx> {
1079        let chart = self.chart;
1080        let config = &self.s.configuration;
1081        let history = &self.s.history;
1082        let queue = &mut self.s.internal_queue;
1083        let dm = &mut self.dm;
1084        kernel::select_transitions(chart, config, history, event, |t| {
1085            let Some(cond) = chart.transition(t).cond.as_deref() else {
1086                return true;
1087            };
1088            let q = Active { chart, config };
1089            match dm.eval_cond(cond, &q) {
1090                Ok(b) => b,
1091                Err(e) => {
1092                    queue.push_back(platform_error("error.execution", &e.message, None));
1093                    false
1094                }
1095            }
1096        })
1097    }
1098
1099    fn record(&mut self, event: Option<String>, ts: &[TransIdx]) {
1100        let transitions = ts
1101            .iter()
1102            .filter_map(|t| {
1103                let tr = self.chart.transition(*t);
1104                tr.index.map(|index| TransitionRef {
1105                    source: self.chart.state(tr.source).id.clone(),
1106                    index,
1107                })
1108            })
1109            .collect();
1110        self.microsteps.push(MicrostepTrace { event, transitions });
1111    }
1112
1113    /// Charge one microstep record (a selection that fired transitions, or an event consumed
1114    /// without enabling any) against the budget. Counting the latter too bounds a macrostep in
1115    /// which every consumed event queues another one, such as a failing eventless guard
1116    /// re-raising `error.execution` forever.
1117    fn charge(&mut self) -> Result<(), Budget> {
1118        if self.used >= self.s.options.microstep_budget {
1119            return Err(Budget);
1120        }
1121        self.used += 1;
1122        Ok(())
1123    }
1124
1125    fn microstep(&mut self, event: Option<String>, ts: &[TransIdx]) -> Result<(), Budget> {
1126        self.charge()?;
1127        self.record(event, ts);
1128        let chart = self.chart;
1129        let domains = kernel::transition_domains(chart, &self.s.history, ts);
1130        self.exit_states(ts);
1131        for &t in ts {
1132            let _ = self.exec_block(&chart.transition(t).body);
1133        }
1134        self.enter_states(ts, &domains);
1135        Ok(())
1136    }
1137
1138    // @tg: implements:: [[openspec:scxml-interpretation#Exit And Entry Order]]
1139    fn exit_states(&mut self, ts: &[TransIdx]) {
1140        let chart = self.chart;
1141        let exit = kernel::compute_exit_set(chart, &self.s.configuration, &self.s.history, ts);
1142        self.to_invoke.difference_with(&exit);
1143        kernel::record_history(chart, &self.s.configuration, &exit, &mut self.s.history);
1144        for s in exit.to_vec_rev() {
1145            for b in &chart.state(s).onexit {
1146                let _ = self.exec_block(b);
1147            }
1148            self.cancel_invocations(s);
1149            self.s.configuration.remove(s);
1150        }
1151    }
1152
1153    fn cancel_invocations(&mut self, s: StateIdx) {
1154        let mut keep = Vec::new();
1155        for inv in core::mem::take(&mut self.s.invocations) {
1156            if inv.state == s {
1157                if !inv.completed {
1158                    self.effects.push(Effect::CancelInvoke {
1159                        invokeid: inv.invokeid,
1160                    });
1161                }
1162            } else {
1163                keep.push(inv);
1164            }
1165        }
1166        self.s.invocations = keep;
1167    }
1168
1169    // @tg: implements:: [[openspec:scxml-interpretation#Final States And Done Events]]
1170    fn enter_states(&mut self, ts: &[TransIdx], domains: &[Option<StateIdx>]) {
1171        let chart = self.chart;
1172        let plan = kernel::compute_entry_set(chart, &self.s.history, ts, domains);
1173        let late = chart.binding() == Binding::Late;
1174        for s in plan.states.to_vec() {
1175            self.s.configuration.insert(s);
1176            self.to_invoke.insert(s);
1177            let st = chart.state(s);
1178            if late && !self.s.data_initialized.contains(s) {
1179                self.s.data_initialized.insert(s);
1180                self.init_data(&st.data);
1181            }
1182            for b in &st.onentry {
1183                let _ = self.exec_block(b);
1184            }
1185            if plan.default_entry.contains(s)
1186                && let Some(it) = st.initial
1187            {
1188                let _ = self.exec_block(&chart.transition(it).body);
1189            }
1190            if let Some(&(_, t)) = plan.default_history_content.iter().find(|(p, _)| *p == s) {
1191                let _ = self.exec_block(&chart.transition(t).body);
1192            }
1193            if st.kind == StateKind::Final {
1194                let parent = st.parent.unwrap_or(ROOT);
1195                if parent == ROOT {
1196                    self.running = false;
1197                } else {
1198                    let data = self.done_data(s);
1199                    let pid = &chart.state(parent).id;
1200                    self.s.internal_queue.push_back(Event {
1201                        name: format!("done.state.{pid}"),
1202                        type_: EventType::Platform,
1203                        data,
1204                        ..Event::default()
1205                    });
1206                    if let Some(gp) = chart.state(parent).parent
1207                        && gp != ROOT
1208                        && chart.state(gp).kind == StateKind::Parallel
1209                        && chart
1210                            .state(gp)
1211                            .children
1212                            .iter()
1213                            .all(|c| kernel::is_in_final_state(chart, &self.s.configuration, *c))
1214                    {
1215                        let gid = &chart.state(gp).id;
1216                        self.s.internal_queue.push_back(Event::named(
1217                            format!("done.state.{gid}"),
1218                            EventType::Platform,
1219                        ));
1220                    }
1221                }
1222            }
1223        }
1224    }
1225
1226    fn done_data(&mut self, s: StateIdx) -> Value {
1227        let chart = self.chart;
1228        let Some(dd) = &chart.state(s).donedata else {
1229            return Value::Undefined;
1230        };
1231        match self.eval_payload(&[], &dd.params, dd.content.as_ref()) {
1232            Ok(v) => v,
1233            Err(_) => {
1234                self.queue_error(None);
1235                Value::Undefined
1236            }
1237        }
1238    }
1239
1240    /// The inner loop of `mainEventLoop` plus the invokes at the end of the macrostep, repeated
1241    /// while invoking raised internal events.
1242    fn run_macrostep(&mut self) -> Result<(), Budget> {
1243        loop {
1244            while self.running {
1245                let mut enabled = self.select(None);
1246                let mut name = None;
1247                if enabled.is_empty() {
1248                    match self.s.internal_queue.pop_front() {
1249                        None => break,
1250                        Some(ev) => {
1251                            self.dm.set_event(&ev);
1252                            enabled = self.select(Some(&ev.name));
1253                            if enabled.is_empty() {
1254                                self.charge()?;
1255                                self.record(Some(ev.name), &[]);
1256                                continue;
1257                            }
1258                            name = Some(ev.name);
1259                        }
1260                    }
1261                }
1262                self.microstep(name, &enabled)?;
1263            }
1264            if !self.running {
1265                self.exit_interpreter(false);
1266                return Ok(());
1267            }
1268            self.start_invokes();
1269            if self.s.internal_queue.is_empty() {
1270                return Ok(());
1271            }
1272        }
1273    }
1274
1275    fn run_start(&mut self, invoker_data: &[(String, Value)]) -> Result<(), Budget> {
1276        let chart = self.chart;
1277        // Create every data item, then initialize them per the binding mode.
1278        for s in 0..chart.len() {
1279            for d in &chart.state(s).data {
1280                if self.dm.declare(&d.id).is_err() {
1281                    self.queue_error(None);
1282                }
1283            }
1284        }
1285        match chart.binding() {
1286            Binding::Early => {
1287                for s in 0..chart.len() {
1288                    let given = if s == ROOT { invoker_data } else { &[] };
1289                    self.init_data_with(&chart.state(s).data, given);
1290                    self.s.data_initialized.insert(s);
1291                }
1292            }
1293            Binding::Late => {
1294                self.init_data_with(&chart.state(ROOT).data, invoker_data);
1295                self.s.data_initialized.insert(ROOT);
1296            }
1297        }
1298        if let Some(script) = &chart.chart().script {
1299            let r = match &script.body {
1300                Some(body) => {
1301                    let q = Active {
1302                        chart,
1303                        config: &self.s.configuration,
1304                    };
1305                    self.dm.exec_script(body, &q)
1306                }
1307                None => Err(EvalError::new("script source was not resolved")),
1308            };
1309            if r.is_err() {
1310                self.queue_error(None);
1311            }
1312        }
1313        if let Some(it) = chart.state(ROOT).initial {
1314            let domains = kernel::transition_domains(chart, &self.s.history, &[it]);
1315            self.enter_states(&[it], &domains);
1316        }
1317        self.run_macrostep()
1318    }
1319
1320    // @tg: implements:: [[openspec:scxml-interpretation#Invoke Lifecycle]]
1321    fn run_external(&mut self, ev: Event) -> Result<(), Budget> {
1322        // Every event the host delivers is processed. Ignoring events from a cancelled
1323        // invocation is the host's job when it receives them (W3C SCXML 6.4: once cancelled,
1324        // the processor MUST NOT insert the invoked session's events into the external queue);
1325        // events queued before the cancellation, such as the `done.invoke.<id>` of a child that
1326        // finished first, are still processed (IRP test 236). Only active invocations run
1327        // `finalize` or get the event forwarded.
1328        if let Some(rest) = ev.name.strip_prefix("done.invoke.") {
1329            for inv in self.s.invocations.iter_mut() {
1330                if inv.invokeid == rest || ev.invokeid.as_deref() == Some(inv.invokeid.as_str()) {
1331                    inv.completed = true;
1332                }
1333            }
1334        }
1335        self.dm.set_event(&ev);
1336        let mut invs = self.s.invocations.clone();
1337        invs.sort_by_key(|i| (i.state, i.index));
1338        let chart = self.chart;
1339        for inv in invs {
1340            if ev.invokeid.as_deref() == Some(inv.invokeid.as_str()) {
1341                let finalize = &chart.state(inv.state).invokes[inv.index].finalize;
1342                let _ = self.exec_block(finalize);
1343            }
1344            if inv.autoforward && !inv.completed {
1345                self.effects.push(Effect::Forward {
1346                    invokeid: inv.invokeid.clone(),
1347                    event: ev.clone(),
1348                });
1349            }
1350        }
1351        let enabled = self.select(Some(&ev.name));
1352        if enabled.is_empty() {
1353            self.record(Some(ev.name), &[]);
1354        } else {
1355            self.microstep(Some(ev.name), &enabled)?;
1356        }
1357        self.run_macrostep()
1358    }
1359
1360    fn start_invokes(&mut self) {
1361        let chart = self.chart;
1362        for s in self.to_invoke.to_vec() {
1363            for (i, inv) in chart.state(s).invokes.iter().enumerate() {
1364                self.invoke(s, i, inv);
1365            }
1366        }
1367        self.to_invoke.clear();
1368    }
1369
1370    fn invoke(&mut self, s: StateIdx, index: usize, inv: &'c Invoke) {
1371        let chart = self.chart;
1372        let state_id = &chart.state(s).id;
1373        let invokeid = match &inv.id {
1374            Some(id) => id.clone(),
1375            None => {
1376                self.s.invoke_counter += 1;
1377                let id = format!("{}.{}", state_id, self.s.invoke_counter);
1378                if let Some(loc) = &inv.idlocation
1379                    && self
1380                        .dm
1381                        .assign_value(loc, Value::String(id.clone()))
1382                        .is_err()
1383                {
1384                    self.queue_error(None);
1385                    return;
1386                }
1387                id
1388            }
1389        };
1390        let type_ = match (&inv.type_, &inv.typeexpr) {
1391            (Some(t), _) => Ok(t.clone()),
1392            (None, Some(x)) => self.eval_string(x),
1393            (None, None) => Ok(SCXML_INVOKE_TYPE.to_string()),
1394        };
1395        let src = match (&inv.src, &inv.srcexpr) {
1396            (Some(t), _) => Ok(Some(t.clone())),
1397            (None, Some(x)) => self.eval_string(x).map(Some),
1398            (None, None) => Ok(None),
1399        };
1400        let content = match &inv.content {
1401            Some(Content { expr: Some(x), .. }) => self.eval_value(x).map(Some),
1402            Some(Content {
1403                expr: None,
1404                body: Some(b),
1405                ..
1406            }) => Ok(Some(Value::String(b.clone()))),
1407            _ => Ok(None),
1408        };
1409        let params = self.eval_payload(&inv.namelist, &inv.params, None);
1410        let (Ok(type_), Ok(src), Ok(content), Ok(params)) = (type_, src, content, params) else {
1411            self.queue_error(None);
1412            return;
1413        };
1414        if !self.s.options.invoke_types.contains(&type_) {
1415            self.queue_error(None);
1416            return;
1417        }
1418        self.s.invocations.push(Invocation {
1419            state: s,
1420            index,
1421            invokeid: invokeid.clone(),
1422            autoforward: inv.autoforward,
1423            completed: false,
1424        });
1425        self.effects.push(Effect::Invoke(InvokeRequest {
1426            invokeid,
1427            state: state_id.clone(),
1428            type_,
1429            src,
1430            content,
1431            params,
1432            autoforward: inv.autoforward,
1433        }));
1434    }
1435
1436    /// `exitInterpreter`: exit every active state in exit order, cancel invocations and emit
1437    /// one session-done effect.
1438    // @tg: implements:: [[openspec:scxml-interpretation#Session Termination]]
1439    fn exit_interpreter(&mut self, cancelled: bool) {
1440        let chart = self.chart;
1441        let mut data = Value::Undefined;
1442        for s in self.s.configuration.to_vec_rev() {
1443            for b in &chart.state(s).onexit {
1444                let _ = self.exec_block(b);
1445            }
1446            self.cancel_invocations(s);
1447            self.s.configuration.remove(s);
1448            if chart.state(s).kind == StateKind::Final && chart.state(s).parent == Some(ROOT) {
1449                data = self.done_data(s);
1450            }
1451        }
1452        self.effects.push(Effect::SessionDone { data, cancelled });
1453        self.s.internal_queue.clear();
1454        self.to_invoke.clear();
1455        self.s.status = Status::Terminated;
1456        self.running = false;
1457        self.terminated_now = true;
1458    }
1459}