use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::bitset::StateSet;
use crate::compile::{CompiledChart, ROOT, StateIdx, TransIdx};
use crate::datamodel::{
Datamodel, Environment, EvalError, Event, EventType, SessionInfo, StateQuery, Value,
};
use crate::ir::{Binding, Content, Data, Exec, ExecKind, Invoke, Param, Send, StateKind};
use crate::kernel::{self, History};
use crate::trace::{MacrostepTrace, MicrostepTrace, TraceInput, TransitionRef};
pub const DEFAULT_MICROSTEP_BUDGET: u32 = 10_000;
pub const SCXML_EVENT_PROCESSOR: &str = "http://www.w3.org/TR/scxml/#SCXMLEventProcessor";
pub const SCXML_INVOKE_TYPE: &str = "http://www.w3.org/TR/scxml/";
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Options {
pub microstep_budget: u32,
pub send_types: Vec<String>,
pub invoke_types: Vec<String>,
}
impl Default for Options {
fn default() -> Self {
Options {
microstep_budget: DEFAULT_MICROSTEP_BUDGET,
send_types: alloc::vec![SCXML_EVENT_PROCESSOR.to_string(), "scxml".to_string()],
invoke_types: alloc::vec![
SCXML_INVOKE_TYPE.to_string(),
"http://www.w3.org/TR/scxml".to_string(),
"scxml".to_string(),
],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Status {
Running,
Terminated,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Invocation {
pub state: StateIdx,
pub index: usize,
pub invokeid: String,
pub autoforward: bool,
pub completed: bool,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Session {
pub sessionid: String,
pub status: Status,
pub configuration: StateSet,
pub history: History,
pub internal_queue: VecDeque<Event>,
pub invocations: Vec<Invocation>,
pub data_initialized: StateSet,
pub send_counter: u64,
pub invoke_counter: u64,
pub macrosteps: u64,
pub options: Options,
}
impl Session {
pub fn is_terminated(&self) -> bool {
self.status == Status::Terminated
}
pub fn active(&self, chart: &CompiledChart) -> Vec<String> {
chart.ids(&self.configuration)
}
pub fn is_active(&self, chart: &CompiledChart, id: &str) -> bool {
chart
.index_of(id)
.is_some_and(|i| self.configuration.contains(i))
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StartInput {
pub info: SessionInfo,
pub env: Environment,
pub options: Options,
#[cfg_attr(feature = "serde", serde(default))]
pub data: Vec<(String, Value)>,
#[cfg_attr(feature = "serde", serde(default))]
pub sessions: Option<Vec<String>>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum InputKind {
Event(Event),
Cancel,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Input {
pub env: Environment,
pub kind: InputKind,
#[cfg_attr(feature = "serde", serde(default))]
pub sessions: Option<Vec<String>>,
}
impl Input {
pub fn event(event: Event, env: Environment) -> Self {
Input {
env,
kind: InputKind::Event(event),
sessions: None,
}
}
pub fn cancel(env: Environment) -> Self {
Input {
env,
kind: InputKind::Cancel,
sessions: None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SendRequest {
pub sendid: String,
pub event: Event,
pub target: Option<String>,
pub type_: String,
pub delay_ms: u64,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InvokeRequest {
pub invokeid: String,
pub state: String,
pub type_: String,
pub src: Option<String>,
pub content: Option<Value>,
pub params: Value,
pub autoforward: bool,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Effect {
Send(SendRequest),
Cancel {
sendid: String,
},
Invoke(InvokeRequest),
CancelInvoke {
invokeid: String,
},
Forward {
invokeid: String,
event: Event,
},
Log {
label: Option<String>,
value: Option<Value>,
},
SessionDone {
data: Value,
cancelled: bool,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct Outcome {
pub session: Session,
pub effects: Vec<Effect>,
pub trace: MacrostepTrace,
}
#[derive(Clone, Debug, PartialEq)]
pub enum StartError {
UnsupportedDatamodel {
name: String,
},
DatamodelMismatch {
chart: String,
given: String,
},
Datamodel(EvalError),
ChartRefused {
datamodel: String,
problems: Vec<String>,
},
BudgetExceeded {
budget: u32,
partial: Box<Outcome>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum StepError {
Terminated,
BudgetExceeded {
budget: u32,
partial: Box<Outcome>,
},
}
impl core::fmt::Display for StartError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
StartError::UnsupportedDatamodel { name } => {
write!(f, "unsupported datamodel `{name}`")
}
StartError::DatamodelMismatch { chart, given } => {
write!(
f,
"the chart names datamodel `{chart}` but `{given}` was given"
)
}
StartError::Datamodel(e) => write!(f, "the datamodel failed to initialize: {e}"),
StartError::ChartRefused {
datamodel,
problems,
} => {
write!(f, "datamodel `{datamodel}` refused the chart")?;
for p in problems {
write!(f, "\n{p}")?;
}
Ok(())
}
StartError::BudgetExceeded { budget, .. } => {
write!(
f,
"the start macrostep exceeded the microstep budget of {budget}"
)
}
}
}
}
impl core::error::Error for StartError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
StartError::Datamodel(e) => Some(e),
_ => None,
}
}
}
impl core::fmt::Display for StepError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
StepError::Terminated => f.write_str("the session has terminated"),
StepError::BudgetExceeded { budget, .. } => {
write!(f, "the macrostep exceeded the microstep budget of {budget}")
}
}
}
}
impl core::error::Error for StepError {}
fn new_session(chart: &CompiledChart, sessionid: String, options: Options) -> Session {
Session {
sessionid,
status: Status::Running,
configuration: chart.empty_set(),
history: History::new(chart.len()),
internal_queue: VecDeque::new(),
invocations: Vec::new(),
data_initialized: chart.empty_set(),
send_counter: 0,
invoke_counter: 0,
macrosteps: 0,
options,
}
}
pub fn start(
chart: &CompiledChart,
input: StartInput,
dm: &mut dyn Datamodel,
) -> Result<Outcome, StartError> {
if dm.name() != chart.datamodel() {
return Err(StartError::DatamodelMismatch {
chart: chart.datamodel().to_string(),
given: dm.name().to_string(),
});
}
dm.init_session(&input.info)
.map_err(StartError::Datamodel)?;
dm.set_environment(&input.env);
let session = new_session(chart, input.info.sessionid.clone(), input.options);
let invoker_data = input.data;
let mut d = Driver::new(
chart,
session,
dm,
TraceInput::Start {
sessionid: input.info.sessionid,
},
);
d.sessions = input.sessions;
let budget = d.s.options.microstep_budget;
match d.run_start(&invoker_data) {
Ok(()) => Ok(d.finish()),
Err(Budget) => Err(StartError::BudgetExceeded {
budget,
partial: Box::new(d.finish()),
}),
}
}
pub fn step(
chart: &CompiledChart,
session: Session,
input: Input,
dm: &mut dyn Datamodel,
) -> Result<Outcome, StepError> {
if session.status == Status::Terminated {
return Err(StepError::Terminated);
}
dm.set_environment(&input.env);
let trace_input = match &input.kind {
InputKind::Event(e) => TraceInput::Event(e.clone()),
InputKind::Cancel => TraceInput::Cancel,
};
let budget = session.options.microstep_budget;
let mut d = Driver::new(chart, session, dm, trace_input);
d.sessions = input.sessions;
let r = match input.kind {
InputKind::Cancel => {
d.exit_interpreter(true);
Ok(())
}
InputKind::Event(ev) => d.run_external(ev),
};
match r {
Ok(()) => Ok(d.finish()),
Err(Budget) => Err(StepError::BudgetExceeded {
budget,
partial: Box::new(d.finish()),
}),
}
}
struct Budget;
struct Stop;
struct Active<'a> {
chart: &'a CompiledChart,
config: &'a StateSet,
}
impl StateQuery for Active<'_> {
fn in_state(&self, id: &str) -> bool {
self.chart
.index_of(id)
.is_some_and(|i| self.config.contains(i))
}
fn active_states(&self) -> Option<Vec<String>> {
Some(
self.chart
.states()
.iter()
.enumerate()
.filter(|(i, _)| *i != 0 && self.config.contains(*i))
.map(|(_, s)| s.id.clone())
.collect(),
)
}
}
pub const EXECUTION_ERROR_MESSAGE: &str = "execution error";
pub const COMMUNICATION_ERROR_MESSAGE: &str = "target session does not exist";
fn platform_error(name: &str, message: &str, sendid: Option<String>) -> Event {
let kind = name.strip_prefix("error.").unwrap_or(name);
Event {
name: name.to_string(),
type_: EventType::Platform,
sendid,
data: Value::Object(Vec::from([
(String::from("kind"), Value::String(kind.to_string())),
(String::from("message"), Value::String(message.to_string())),
])),
..Event::default()
}
}
struct Capture<'d> {
inner: &'d mut dyn Datamodel,
last: Option<EvalError>,
}
impl Capture<'_> {
fn note<T>(&mut self, r: Result<T, EvalError>) -> Result<T, EvalError> {
self.last = r.as_ref().err().cloned();
r
}
}
impl Datamodel for Capture<'_> {
fn name(&self) -> &str {
self.inner.name()
}
fn init_session(&mut self, info: &SessionInfo) -> Result<(), EvalError> {
let r = self.inner.init_session(info);
self.note(r)
}
fn set_environment(&mut self, env: &Environment) {
self.inner.set_environment(env)
}
fn declare(&mut self, id: &str) -> Result<(), EvalError> {
let r = self.inner.declare(id);
self.note(r)
}
fn set_event(&mut self, event: &Event) {
self.inner.set_event(event)
}
fn check_event(&self, event: &Event) -> Result<(), EvalError> {
self.inner.check_event(event)
}
fn eval_cond(&mut self, expr: &str, states: &dyn StateQuery) -> Result<bool, EvalError> {
let r = self.inner.eval_cond(expr, states);
self.note(r)
}
fn eval_value(&mut self, expr: &str, states: &dyn StateQuery) -> Result<Value, EvalError> {
let r = self.inner.eval_value(expr, states);
self.note(r)
}
fn eval_content(&mut self, content: &str) -> Result<Value, EvalError> {
let r = self.inner.eval_content(content);
self.note(r)
}
fn assign_expr(
&mut self,
location: &str,
expr: &str,
states: &dyn StateQuery,
) -> Result<(), EvalError> {
let r = self.inner.assign_expr(location, expr, states);
self.note(r)
}
fn assign_value(&mut self, location: &str, value: Value) -> Result<(), EvalError> {
let r = self.inner.assign_value(location, value);
self.note(r)
}
fn bind_var(&mut self, name: &str, value: Value) -> Result<(), EvalError> {
let r = self.inner.bind_var(name, value);
self.note(r)
}
fn is_location(&mut self, location: &str) -> bool {
self.last = None;
self.inner.is_location(location)
}
fn exec_script(&mut self, script: &str, states: &dyn StateQuery) -> Result<(), EvalError> {
let r = self.inner.exec_script(script, states);
self.note(r)
}
fn foreach_items(
&mut self,
array: &str,
states: &dyn StateQuery,
) -> Result<Vec<Value>, EvalError> {
let r = self.inner.foreach_items(array, states);
self.note(r)
}
fn snapshot(&self) -> Value {
self.inner.snapshot()
}
fn restore(&mut self, snapshot: &Value) -> Result<(), EvalError> {
let r = self.inner.restore(snapshot);
self.note(r)
}
}
pub fn parse_delay(text: &str) -> Option<u64> {
let t = text.trim();
let (num, factor) = if let Some(n) = t.strip_suffix("ms") {
(n.trim(), 1u64)
} else if let Some(n) = t.strip_suffix('s') {
(n.trim(), 1000u64)
} else {
(t, 1u64)
};
if num.is_empty() {
return None;
}
let (int, frac) = match num.split_once('.') {
Some((i, f)) => (i, f),
None => (num, ""),
};
if (int.is_empty() && frac.is_empty())
|| !int.bytes().all(|b| b.is_ascii_digit())
|| !frac.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let mut ms: u64 = 0;
for b in int.bytes() {
ms = ms.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
}
ms = ms.checked_mul(factor)?;
let mut scale = factor;
for b in frac.bytes() {
scale /= 10;
if scale == 0 {
break;
}
ms = ms.checked_add(u64::from(b - b'0') * scale)?;
}
Some(ms)
}
fn value_to_string(v: Value) -> Result<String, EvalError> {
match v {
Value::String(s) => Ok(s),
Value::Bool(b) => Ok(if b { "true" } else { "false" }.to_string()),
Value::Number(n) => {
if n.is_finite() && n == (n as i64) as f64 {
Ok(format!("{}", n as i64))
} else {
Ok(format!("{n}"))
}
}
other => Err(EvalError::new(format!("expected a string, got {other:?}"))),
}
}
struct Driver<'c, 'd> {
chart: &'c CompiledChart,
s: Session,
dm: Capture<'d>,
effects: Vec<Effect>,
to_invoke: StateSet,
running: bool,
terminated_now: bool,
microsteps: Vec<MicrostepTrace>,
used: u32,
input: TraceInput,
sessions: Option<Vec<String>>,
}
impl<'c, 'd> Driver<'c, 'd> {
fn new(
chart: &'c CompiledChart,
s: Session,
dm: &'d mut dyn Datamodel,
input: TraceInput,
) -> Self {
Driver {
chart,
s,
dm: Capture {
inner: dm,
last: None,
},
effects: Vec::new(),
to_invoke: chart.empty_set(),
running: true,
terminated_now: false,
microsteps: Vec::new(),
used: 0,
input,
sessions: None,
}
}
fn finish(mut self) -> Outcome {
let trace = MacrostepTrace {
index: self.s.macrosteps,
input: self.input,
microsteps: self.microsteps,
configuration: self.chart.ids(&self.s.configuration),
effects: self.effects.clone(),
terminated: self.terminated_now,
};
self.s.macrosteps += 1;
Outcome {
session: self.s,
effects: self.effects,
trace,
}
}
fn queue_error(&mut self, sendid: Option<String>) {
let last = self.dm.last.take();
let message = last
.as_ref()
.map_or(EXECUTION_ERROR_MESSAGE, |e| e.message.as_str());
self.s
.internal_queue
.push_back(platform_error("error.execution", message, sendid));
}
fn eval_value(&mut self, expr: &str) -> Result<Value, EvalError> {
let q = Active {
chart: self.chart,
config: &self.s.configuration,
};
self.dm.eval_value(expr, &q)
}
fn eval_string(&mut self, expr: &str) -> Result<String, EvalError> {
self.eval_value(expr).and_then(value_to_string)
}
fn eval_cond(&mut self, expr: &str) -> Result<bool, EvalError> {
let q = Active {
chart: self.chart,
config: &self.s.configuration,
};
self.dm.eval_cond(expr, &q)
}
fn eval_payload(
&mut self,
namelist: &[String],
params: &[Param],
content: Option<&Content>,
) -> Result<Value, EvalError> {
if let Some(c) = content {
return match (&c.expr, &c.body) {
(Some(x), _) => self.eval_value(x),
(None, Some(b)) => self.dm.eval_content(b),
(None, None) => Ok(Value::Undefined),
};
}
if namelist.is_empty() && params.is_empty() {
return Ok(Value::Undefined);
}
let mut pairs = Vec::new();
for name in namelist {
let v = self.eval_value(name)?;
pairs.push((name.clone(), v));
}
for p in params {
let v = match (&p.expr, &p.location) {
(Some(x), _) => self.eval_value(x)?,
(None, Some(l)) => self.eval_value(l)?,
(None, None) => Value::Undefined,
};
pairs.push((p.name.clone(), v));
}
Ok(Value::Object(pairs))
}
fn init_data(&mut self, items: &'c [Data]) {
self.init_data_with(items, &[]);
}
fn init_data_with(&mut self, items: &'c [Data], given: &[(String, Value)]) {
for d in items {
let r = if let Some((_, v)) = given.iter().find(|(n, _)| *n == d.id) {
self.dm.assign_value(&d.id, v.clone())
} else if let Some(e) = &d.expr {
self.eval_value(e)
.and_then(|v| self.dm.assign_value(&d.id, v))
} else if let Some(c) = &d.content {
self.dm
.eval_content(c)
.and_then(|v| self.dm.assign_value(&d.id, v))
} else if let Some(src) = &d.src {
Err(EvalError::new(format!("data src {src} was not resolved")))
} else {
Ok(())
};
if r.is_err() {
self.queue_error(None);
}
}
}
fn exec_block(&mut self, block: &'c [Exec]) -> Result<(), Stop> {
for e in block {
self.exec(e)?;
}
Ok(())
}
fn fail<T>(&mut self, sendid: Option<String>) -> Result<T, Stop> {
self.queue_error(sendid);
Err(Stop)
}
fn exec(&mut self, e: &'c Exec) -> Result<(), Stop> {
match &e.kind {
ExecKind::Raise { event } => {
self.s
.internal_queue
.push_back(Event::named(event.clone(), EventType::Internal));
Ok(())
}
ExecKind::Send(send) => self.exec_send(send),
ExecKind::Cancel { sendid, sendidexpr } => {
let id = match (sendid, sendidexpr) {
(Some(s), _) => s.clone(),
(None, Some(x)) => match self.eval_string(x) {
Ok(s) => s,
Err(_) => return self.fail(None),
},
(None, None) => return self.fail(None),
};
self.effects.push(Effect::Cancel { sendid: id });
Ok(())
}
ExecKind::Log { label, expr } => {
let value = match expr {
Some(x) => match self.eval_value(x) {
Ok(v) => Some(v),
Err(_) => return self.fail(None),
},
None => None,
};
self.effects.push(Effect::Log {
label: label.clone(),
value,
});
Ok(())
}
ExecKind::Assign {
location,
expr,
content,
} => {
let r = match (expr, content) {
(Some(x), _) => {
let q = Active {
chart: self.chart,
config: &self.s.configuration,
};
self.dm.assign_expr(location, x, &q)
}
(None, Some(c)) => self
.dm
.eval_content(c)
.and_then(|v| self.dm.assign_value(location, v)),
(None, None) => self.dm.assign_value(location, Value::Undefined),
};
if r.is_err() {
return self.fail(None);
}
Ok(())
}
ExecKind::Script(script) => {
let r = match &script.body {
Some(body) => {
let q = Active {
chart: self.chart,
config: &self.s.configuration,
};
self.dm.exec_script(body, &q)
}
None => Err(EvalError::new("script source was not resolved")),
};
if r.is_err() {
return self.fail(None);
}
Ok(())
}
ExecKind::If { branches } => {
for br in branches {
let take = match &br.cond {
None => true,
Some(c) => match self.eval_cond(c) {
Ok(b) => b,
Err(_) => return self.fail(None),
},
};
if take {
return self.exec_block(&br.body);
}
}
Ok(())
}
ExecKind::Foreach {
array,
item,
index,
body,
} => {
let items = {
let q = Active {
chart: self.chart,
config: &self.s.configuration,
};
self.dm.foreach_items(array, &q)
};
let items = match items {
Ok(v) => v,
Err(_) => return self.fail(None),
};
for (i, v) in items.into_iter().enumerate() {
if self.dm.bind_var(item, v).is_err() {
return self.fail(None);
}
if let Some(ix) = index
&& self.dm.bind_var(ix, Value::Number(i as f64)).is_err()
{
return self.fail(None);
}
self.exec_block(body)?;
}
Ok(())
}
ExecKind::Extension(_) => Ok(()),
}
}
fn exec_send(&mut self, send: &'c Send) -> Result<(), Stop> {
let (sendid, visible) = match (&send.id, &send.idlocation) {
(Some(id), _) => (id.clone(), true),
(None, loc) => {
self.s.send_counter += 1;
let id = format!("{}.send.{}", self.s.sessionid, self.s.send_counter);
if let Some(loc) = loc
&& self
.dm
.assign_value(loc, Value::String(id.clone()))
.is_err()
{
return self.fail(Some(id));
}
(id, loc.is_some())
}
};
let err_id = Some(sendid.clone());
let name = match (&send.event, &send.eventexpr) {
(Some(n), _) => n.clone(),
(None, Some(x)) => match self.eval_string(x) {
Ok(n) => n,
Err(_) => return self.fail(err_id),
},
(None, None) => String::new(),
};
let target = match (&send.target, &send.targetexpr) {
(Some(t), _) => Some(t.clone()),
(None, Some(x)) => match self.eval_string(x) {
Ok(t) => Some(t),
Err(_) => return self.fail(err_id),
},
(None, None) => None,
};
let type_ = match (&send.type_, &send.typeexpr) {
(Some(t), _) => t.clone(),
(None, Some(x)) => match self.eval_string(x) {
Ok(t) => t,
Err(_) => return self.fail(err_id),
},
(None, None) => SCXML_EVENT_PROCESSOR.to_string(),
};
let delay_text = match (&send.delay, &send.delayexpr) {
(Some(d), _) => Some(d.clone()),
(None, Some(x)) => match self.eval_string(x) {
Ok(d) => Some(d),
Err(_) => return self.fail(err_id),
},
(None, None) => None,
};
let delay_ms = match delay_text {
None => 0,
Some(t) => match parse_delay(&t) {
Some(ms) => ms,
None => return self.fail(err_id),
},
};
let data = match self.eval_payload(&send.namelist, &send.params, send.content.as_ref()) {
Ok(v) => v,
Err(_) => return self.fail(err_id),
};
if !self.s.options.send_types.contains(&type_) {
return self.fail(err_id);
}
let scxml = type_ == SCXML_EVENT_PROCESSOR || type_ == "scxml";
if scxml
&& let Some(t) = &target
&& !(t.starts_with("#_") || t.contains(':'))
{
return self.fail(err_id);
}
if scxml
&& delay_ms == 0
&& let (Some(t), Some(known)) = (&target, &self.sessions)
&& let Some(id) = t.strip_prefix("#_scxml_")
&& !known.iter().any(|k| k == id)
{
self.s.internal_queue.push_back(platform_error(
"error.communication",
COMMUNICATION_ERROR_MESSAGE,
err_id,
));
return Ok(());
}
let exposed = if visible { Some(sendid.clone()) } else { None };
if target.as_deref() == Some("#_internal") && delay_ms == 0 {
self.s.internal_queue.push_back(Event {
name,
type_: EventType::Internal,
sendid: exposed,
data,
..Event::default()
});
return Ok(());
}
let event = Event {
name,
type_: EventType::External,
sendid: exposed,
origin: if scxml {
Some(format!("#_scxml_{}", self.s.sessionid))
} else {
None
},
origintype: if scxml {
Some(SCXML_EVENT_PROCESSOR.to_string())
} else {
None
},
invokeid: None,
data,
..Event::default()
};
self.effects.push(Effect::Send(SendRequest {
sendid,
event,
target,
type_,
delay_ms,
}));
Ok(())
}
fn select(&mut self, event: Option<&str>) -> Vec<TransIdx> {
let chart = self.chart;
let config = &self.s.configuration;
let history = &self.s.history;
let queue = &mut self.s.internal_queue;
let dm = &mut self.dm;
kernel::select_transitions(chart, config, history, event, |t| {
let Some(cond) = chart.transition(t).cond.as_deref() else {
return true;
};
let q = Active { chart, config };
match dm.eval_cond(cond, &q) {
Ok(b) => b,
Err(e) => {
queue.push_back(platform_error("error.execution", &e.message, None));
false
}
}
})
}
fn record(&mut self, event: Option<String>, ts: &[TransIdx]) {
let transitions = ts
.iter()
.filter_map(|t| {
let tr = self.chart.transition(*t);
tr.index.map(|index| TransitionRef {
source: self.chart.state(tr.source).id.clone(),
index,
})
})
.collect();
self.microsteps.push(MicrostepTrace { event, transitions });
}
fn charge(&mut self) -> Result<(), Budget> {
if self.used >= self.s.options.microstep_budget {
return Err(Budget);
}
self.used += 1;
Ok(())
}
fn microstep(&mut self, event: Option<String>, ts: &[TransIdx]) -> Result<(), Budget> {
self.charge()?;
self.record(event, ts);
let chart = self.chart;
let domains = kernel::transition_domains(chart, &self.s.history, ts);
self.exit_states(ts);
for &t in ts {
let _ = self.exec_block(&chart.transition(t).body);
}
self.enter_states(ts, &domains);
Ok(())
}
fn exit_states(&mut self, ts: &[TransIdx]) {
let chart = self.chart;
let exit = kernel::compute_exit_set(chart, &self.s.configuration, &self.s.history, ts);
self.to_invoke.difference_with(&exit);
kernel::record_history(chart, &self.s.configuration, &exit, &mut self.s.history);
for s in exit.to_vec_rev() {
for b in &chart.state(s).onexit {
let _ = self.exec_block(b);
}
self.cancel_invocations(s);
self.s.configuration.remove(s);
}
}
fn cancel_invocations(&mut self, s: StateIdx) {
let mut keep = Vec::new();
for inv in core::mem::take(&mut self.s.invocations) {
if inv.state == s {
if !inv.completed {
self.effects.push(Effect::CancelInvoke {
invokeid: inv.invokeid,
});
}
} else {
keep.push(inv);
}
}
self.s.invocations = keep;
}
fn enter_states(&mut self, ts: &[TransIdx], domains: &[Option<StateIdx>]) {
let chart = self.chart;
let plan = kernel::compute_entry_set(chart, &self.s.history, ts, domains);
let late = chart.binding() == Binding::Late;
for s in plan.states.to_vec() {
self.s.configuration.insert(s);
self.to_invoke.insert(s);
let st = chart.state(s);
if late && !self.s.data_initialized.contains(s) {
self.s.data_initialized.insert(s);
self.init_data(&st.data);
}
for b in &st.onentry {
let _ = self.exec_block(b);
}
if plan.default_entry.contains(s)
&& let Some(it) = st.initial
{
let _ = self.exec_block(&chart.transition(it).body);
}
if let Some(&(_, t)) = plan.default_history_content.iter().find(|(p, _)| *p == s) {
let _ = self.exec_block(&chart.transition(t).body);
}
if st.kind == StateKind::Final {
let parent = st.parent.unwrap_or(ROOT);
if parent == ROOT {
self.running = false;
} else {
let data = self.done_data(s);
let pid = &chart.state(parent).id;
self.s.internal_queue.push_back(Event {
name: format!("done.state.{pid}"),
type_: EventType::Platform,
data,
..Event::default()
});
if let Some(gp) = chart.state(parent).parent
&& gp != ROOT
&& chart.state(gp).kind == StateKind::Parallel
&& chart
.state(gp)
.children
.iter()
.all(|c| kernel::is_in_final_state(chart, &self.s.configuration, *c))
{
let gid = &chart.state(gp).id;
self.s.internal_queue.push_back(Event::named(
format!("done.state.{gid}"),
EventType::Platform,
));
}
}
}
}
}
fn done_data(&mut self, s: StateIdx) -> Value {
let chart = self.chart;
let Some(dd) = &chart.state(s).donedata else {
return Value::Undefined;
};
match self.eval_payload(&[], &dd.params, dd.content.as_ref()) {
Ok(v) => v,
Err(_) => {
self.queue_error(None);
Value::Undefined
}
}
}
fn run_macrostep(&mut self) -> Result<(), Budget> {
loop {
while self.running {
let mut enabled = self.select(None);
let mut name = None;
if enabled.is_empty() {
match self.s.internal_queue.pop_front() {
None => break,
Some(ev) => {
self.dm.set_event(&ev);
enabled = self.select(Some(&ev.name));
if enabled.is_empty() {
self.charge()?;
self.record(Some(ev.name), &[]);
continue;
}
name = Some(ev.name);
}
}
}
self.microstep(name, &enabled)?;
}
if !self.running {
self.exit_interpreter(false);
return Ok(());
}
self.start_invokes();
if self.s.internal_queue.is_empty() {
return Ok(());
}
}
}
fn run_start(&mut self, invoker_data: &[(String, Value)]) -> Result<(), Budget> {
let chart = self.chart;
for s in 0..chart.len() {
for d in &chart.state(s).data {
if self.dm.declare(&d.id).is_err() {
self.queue_error(None);
}
}
}
match chart.binding() {
Binding::Early => {
for s in 0..chart.len() {
let given = if s == ROOT { invoker_data } else { &[] };
self.init_data_with(&chart.state(s).data, given);
self.s.data_initialized.insert(s);
}
}
Binding::Late => {
self.init_data_with(&chart.state(ROOT).data, invoker_data);
self.s.data_initialized.insert(ROOT);
}
}
if let Some(script) = &chart.chart().script {
let r = match &script.body {
Some(body) => {
let q = Active {
chart,
config: &self.s.configuration,
};
self.dm.exec_script(body, &q)
}
None => Err(EvalError::new("script source was not resolved")),
};
if r.is_err() {
self.queue_error(None);
}
}
if let Some(it) = chart.state(ROOT).initial {
let domains = kernel::transition_domains(chart, &self.s.history, &[it]);
self.enter_states(&[it], &domains);
}
self.run_macrostep()
}
fn run_external(&mut self, ev: Event) -> Result<(), Budget> {
if let Some(rest) = ev.name.strip_prefix("done.invoke.") {
for inv in self.s.invocations.iter_mut() {
if inv.invokeid == rest || ev.invokeid.as_deref() == Some(inv.invokeid.as_str()) {
inv.completed = true;
}
}
}
self.dm.set_event(&ev);
let mut invs = self.s.invocations.clone();
invs.sort_by_key(|i| (i.state, i.index));
let chart = self.chart;
for inv in invs {
if ev.invokeid.as_deref() == Some(inv.invokeid.as_str()) {
let finalize = &chart.state(inv.state).invokes[inv.index].finalize;
let _ = self.exec_block(finalize);
}
if inv.autoforward && !inv.completed {
self.effects.push(Effect::Forward {
invokeid: inv.invokeid.clone(),
event: ev.clone(),
});
}
}
let enabled = self.select(Some(&ev.name));
if enabled.is_empty() {
self.record(Some(ev.name), &[]);
} else {
self.microstep(Some(ev.name), &enabled)?;
}
self.run_macrostep()
}
fn start_invokes(&mut self) {
let chart = self.chart;
for s in self.to_invoke.to_vec() {
for (i, inv) in chart.state(s).invokes.iter().enumerate() {
self.invoke(s, i, inv);
}
}
self.to_invoke.clear();
}
fn invoke(&mut self, s: StateIdx, index: usize, inv: &'c Invoke) {
let chart = self.chart;
let state_id = &chart.state(s).id;
let invokeid = match &inv.id {
Some(id) => id.clone(),
None => {
self.s.invoke_counter += 1;
let id = format!("{}.{}", state_id, self.s.invoke_counter);
if let Some(loc) = &inv.idlocation
&& self
.dm
.assign_value(loc, Value::String(id.clone()))
.is_err()
{
self.queue_error(None);
return;
}
id
}
};
let type_ = match (&inv.type_, &inv.typeexpr) {
(Some(t), _) => Ok(t.clone()),
(None, Some(x)) => self.eval_string(x),
(None, None) => Ok(SCXML_INVOKE_TYPE.to_string()),
};
let src = match (&inv.src, &inv.srcexpr) {
(Some(t), _) => Ok(Some(t.clone())),
(None, Some(x)) => self.eval_string(x).map(Some),
(None, None) => Ok(None),
};
let content = match &inv.content {
Some(Content { expr: Some(x), .. }) => self.eval_value(x).map(Some),
Some(Content {
expr: None,
body: Some(b),
..
}) => Ok(Some(Value::String(b.clone()))),
_ => Ok(None),
};
let params = self.eval_payload(&inv.namelist, &inv.params, None);
let (Ok(type_), Ok(src), Ok(content), Ok(params)) = (type_, src, content, params) else {
self.queue_error(None);
return;
};
if !self.s.options.invoke_types.contains(&type_) {
self.queue_error(None);
return;
}
self.s.invocations.push(Invocation {
state: s,
index,
invokeid: invokeid.clone(),
autoforward: inv.autoforward,
completed: false,
});
self.effects.push(Effect::Invoke(InvokeRequest {
invokeid,
state: state_id.clone(),
type_,
src,
content,
params,
autoforward: inv.autoforward,
}));
}
fn exit_interpreter(&mut self, cancelled: bool) {
let chart = self.chart;
let mut data = Value::Undefined;
for s in self.s.configuration.to_vec_rev() {
for b in &chart.state(s).onexit {
let _ = self.exec_block(b);
}
self.cancel_invocations(s);
self.s.configuration.remove(s);
if chart.state(s).kind == StateKind::Final && chart.state(s).parent == Some(ROOT) {
data = self.done_data(s);
}
}
self.effects.push(Effect::SessionDone { data, cancelled });
self.s.internal_queue.clear();
self.to_invoke.clear();
self.s.status = Status::Terminated;
self.running = false;
self.terminated_now = true;
}
}
#[cfg(test)]
mod error_tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn start_error_displays_and_boxes() {
let e = StartError::ChartRefused {
datamodel: "cel".into(),
problems: vec!["state `s` › cond: not bool".into()],
};
assert_eq!(
e.to_string(),
"datamodel `cel` refused the chart\nstate `s` › cond: not bool"
);
let e = StartError::Datamodel(EvalError::new("boom"));
assert_eq!(e.to_string(), "the datamodel failed to initialize: boom");
assert_eq!(core::error::Error::source(&e).unwrap().to_string(), "boom");
fn run() -> Result<(), alloc::boxed::Box<dyn core::error::Error>> {
Err(StartError::UnsupportedDatamodel { name: "x".into() })?
}
assert_eq!(run().unwrap_err().to_string(), "unsupported datamodel `x`");
}
#[test]
fn step_error_displays_and_boxes() {
fn run() -> Result<(), alloc::boxed::Box<dyn core::error::Error>> {
Err(StepError::Terminated)?
}
assert_eq!(run().unwrap_err().to_string(), "the session has terminated");
}
}