1use std::collections::{BTreeMap, BTreeSet, HashSet};
5use std::sync::Arc;
6
7use oxrdf::{Literal, NamedNode, Triple};
8use serde::{Deserialize, Serialize};
9use vertep_core::datamodel::{
10 Datamodel, DatamodelRegistry, Environment, Event, EventType, SessionInfo, Value,
11};
12use vertep_core::session::{
13 self, Effect, Input, InvokeRequest, Options, SCXML_EVENT_PROCESSOR, Session, StartInput,
14 StepError,
15};
16use vertep_core::trace::MacrostepTrace;
17
18use crate::address::{self, Address};
19use crate::chart::{ChartInfo, UnhandledPolicy, split_type};
20use crate::journal::{
21 Effects, InputRecord, ReminderSpec, SendRecord, SnapshotData, TimerRecord, digest,
22 timer_triples,
23};
24use crate::json;
25use crate::vocab::{self as vt, node};
26
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct InvocationLog {
31 pub request: InvokeRequest,
33 pub events: Vec<(Event, i64)>,
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
42pub struct RuntimeState {
43 pub timers: BTreeMap<String, TimerRecord>,
45 pub requests: BTreeMap<String, Option<String>>,
47 pub answered: BTreeSet<String>,
49 pub timed_out: BTreeSet<String>,
51 pub cancelled: BTreeSet<String>,
53 pub invocations: BTreeMap<String, InvocationLog>,
55}
56
57#[derive(Clone, Debug, PartialEq)]
59pub enum MachineError {
60 Terminated,
62 Step(String),
64 Snapshot(String),
66}
67
68impl core::fmt::Display for MachineError {
69 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70 match self {
71 MachineError::Terminated => f.write_str("the session has terminated"),
72 MachineError::Step(e) => write!(f, "step failed: {e}"),
73 MachineError::Snapshot(e) => write!(f, "snapshot cannot be restored: {e}"),
74 }
75 }
76}
77
78pub(crate) struct Ctx<'a> {
80 pub registry: &'a dyn DatamodelRegistry,
81 pub ioprocessors: Value,
82 pub send_types: Vec<String>,
83 pub invoke_types: Vec<String>,
84}
85
86#[derive(Debug, Default)]
88pub(crate) struct Applied {
89 pub effects: Effects,
90 pub invokes: Vec<InvokeRequest>,
91 pub cancel_invokes: Vec<String>,
92 pub migrations: Vec<(String, String)>,
93}
94
95pub(crate) struct Machine {
97 pub address: Address,
98 pub info: Arc<ChartInfo>,
99 pub session: Session,
100 pub dm: Box<dyn Datamodel>,
101 pub rt: RuntimeState,
102}
103
104impl Machine {
105 fn options(info: &ChartInfo, ctx: &Ctx<'_>) -> Options {
106 let mut o = Options::default();
107 for t in ctx.send_types.iter().chain(info.vt_send_types.iter()) {
108 if !o.send_types.contains(t) {
109 o.send_types.push(t.clone());
110 }
111 }
112 for t in &ctx.invoke_types {
113 if !o.invoke_types.contains(t) {
114 o.invoke_types.push(t.clone());
115 }
116 }
117 o
118 }
119
120 fn new_dm(info: &ChartInfo, ctx: &Ctx<'_>) -> Result<Box<dyn Datamodel>, MachineError> {
121 vertep_core::null::create_datamodel_for(ctx.registry, info.compiled.chart())
122 .map_err(|e| MachineError::Step(format!("{e:?}")))
123 }
124
125 fn info(address: &Address, info: &ChartInfo, ctx: &Ctx<'_>) -> SessionInfo {
126 SessionInfo {
127 sessionid: address.as_str().to_owned(),
128 name: info.ir.name.clone(),
129 ioprocessors: ctx.ioprocessors.clone(),
130 }
131 }
132
133 pub fn start(
135 address: &Address,
136 info: Arc<ChartInfo>,
137 ctx: &Ctx<'_>,
138 env: Environment,
139 ) -> Result<(Machine, Vec<Effect>, MacrostepTrace), MachineError> {
140 let mut dm = Self::new_dm(&info, ctx)?;
141 let start = StartInput {
142 info: Self::info(address, &info, ctx),
143 env,
144 options: Self::options(&info, ctx),
145 data: Vec::new(),
146 sessions: None,
147 };
148 let o = session::start(&info.compiled, start, dm.as_mut())
149 .map_err(|e| MachineError::Step(format!("{e:?}")))?;
150 Ok((
151 Machine {
152 address: address.clone(),
153 info,
154 session: o.session,
155 dm,
156 rt: RuntimeState::default(),
157 },
158 o.effects,
159 o.trace,
160 ))
161 }
162
163 pub fn from_snapshot(
165 address: &Address,
166 info: Arc<ChartInfo>,
167 ctx: &Ctx<'_>,
168 s: &SnapshotData,
169 ) -> Result<Machine, MachineError> {
170 let mut dm = Self::new_dm(&info, ctx)?;
171 dm.init_session(&Self::info(address, &info, ctx))
172 .map_err(|e| MachineError::Snapshot(e.message))?;
173 let value =
174 json::from_text(&s.datamodel).map_err(|e| MachineError::Snapshot(e.to_string()))?;
175 dm.restore(&value)
176 .map_err(|e| MachineError::Snapshot(e.message))?;
177 let session: Session =
178 serde_json::from_str(&s.session).map_err(|e| MachineError::Snapshot(e.to_string()))?;
179 let rt: RuntimeState =
180 serde_json::from_str(&s.runtime).map_err(|e| MachineError::Snapshot(e.to_string()))?;
181 Ok(Machine {
182 address: address.clone(),
183 info,
184 session,
185 dm,
186 rt,
187 })
188 }
189
190 pub fn active(&self) -> Vec<String> {
192 self.info.active(&self.session)
193 }
194
195 pub fn digest(&self) -> String {
197 digest(
198 &self.active(),
199 &self.dm.snapshot(),
200 self.session.is_terminated(),
201 )
202 }
203
204 pub fn can(&self, registry: &dyn DatamodelRegistry, event: &Event) -> bool {
208 if self.session.is_terminated() {
209 return false;
210 }
211 if self
212 .info
213 .deferred(&self.session)
214 .iter()
215 .any(|d| vertep_core::kernel::descriptor_matches(d, &event.name))
216 {
217 return true;
218 }
219 let Ok(mut dm) =
220 vertep_core::null::create_datamodel_for(registry, self.info.compiled.chart())
221 else {
222 return false;
223 };
224 let info = SessionInfo {
225 sessionid: self.address.as_str().to_owned(),
226 name: self.info.ir.name.clone(),
227 ioprocessors: Value::Undefined,
228 };
229 if dm.init_session(&info).is_err()
230 || dm.restore(&self.dm.snapshot()).is_err()
231 || dm.check_event(event).is_err()
232 {
233 return false;
234 }
235 dm.set_event(event);
236 let chart = &self.info.compiled;
237 let q = ActiveIn {
238 chart,
239 session: &self.session,
240 };
241 !vertep_core::kernel::select_transitions(
242 chart,
243 &self.session.configuration,
244 &self.session.history,
245 Some(&event.name),
246 |t| {
247 chart
248 .transition(t)
249 .cond
250 .as_deref()
251 .is_none_or(|c| dm.eval_cond(c, &q).unwrap_or(false))
252 },
253 )
254 .is_empty()
255 }
256
257 pub fn snapshot(&self, iri: &NamedNode, position: u64) -> SnapshotData {
259 SnapshotData {
260 iri: iri.as_str().to_owned(),
261 actor: self.address.as_str().to_owned(),
262 position,
263 version: self.info.version.as_str().to_owned(),
264 digest: self.digest(),
265 session: serde_json::to_string(&self.session).unwrap_or_default(),
266 datamodel: json::to_text(&self.dm.snapshot()),
267 runtime: serde_json::to_string(&self.rt).unwrap_or_default(),
268 }
269 }
270
271 pub fn state_triples(&self, chart_type: Option<(&str, &str)>) -> HashSet<Triple> {
274 let a = self.address.node().clone();
275 let mut out = HashSet::new();
276 let mut add = |p: &str, o: oxrdf::Term| {
277 out.insert(Triple::new(a.clone(), node(p), o));
278 };
279 add(vt::RDF_TYPE, node(vt::ACTOR_CLASS).into());
280 add(vt::RDF_TYPE, node(vt::VIRTUAL_ACTOR).into());
281 add(vt::CHART_VERSION, self.info.version.named_node().into());
282 if let Some((t, k)) = chart_type
283 && let Ok(t) = NamedNode::new(t)
284 {
285 add(vt::CHART_TYPE, t.into());
286 add(vt::KEY, Literal::new_simple_literal(k).into());
287 }
288 add(
289 vt::STATUS,
290 node(if self.session.is_terminated() {
291 vt::TERMINATED
292 } else {
293 vt::RUNNING
294 })
295 .into(),
296 );
297 for id in self.active() {
298 add(vt::IN_STATE, state_node(&self.info, &id).into());
299 }
300 add(
301 vt::DATAMODEL_STATE,
302 Literal::new_simple_literal(json::to_text(&self.dm.snapshot())).into(),
303 );
304 for t in self.rt.timers.values() {
305 out.extend(timer_triples(t));
306 }
307 out
308 }
309
310 pub fn apply(
313 &mut self,
314 input: &InputRecord,
315 position: u64,
316 ts: i64,
317 policy: UnhandledPolicy,
318 start_effects: Vec<Effect>,
319 start_trace: Option<&MacrostepTrace>,
320 ) -> Result<Applied, MachineError> {
321 let entry = self.address.entry_iri(position);
322 let env = Environment {
323 now_ms: ts,
324 seed: address::seed_of(&entry),
325 };
326 let mut out = Applied::default();
327 let ev = &input.event;
328 if let Some(t) = &input.timer
330 && self.rt.timers.get(t).is_some_and(|r| r.reminder.is_none())
331 {
332 self.rt.timers.remove(t);
333 }
334 if let Some(c) = &input.correlation {
335 if ev.name == vt::REPLY_EVENT
336 && let Some(timeout) = self.rt.requests.remove(c)
337 {
338 self.rt.answered.insert(c.clone());
339 out.effects.answers = Some(c.clone());
340 if let Some(t) = timeout
341 && self.rt.timers.remove(&t).is_some()
342 {
343 self.rt.cancelled.insert(t.clone());
344 out.effects.cancels.push(t);
345 }
346 } else if ev.name == vt::TIMEOUT_EVENT && self.rt.requests.remove(c).is_some() {
347 self.rt.timed_out.insert(c.clone());
348 }
349 }
350 if let Some(id) = ev.name.strip_prefix("done.invoke.") {
351 self.rt.invocations.remove(id);
352 }
353 let mut traces: Vec<MacrostepTrace> = start_trace.into_iter().cloned().collect();
354 let step = |m: &mut Machine, e: Event| -> Result<session::Outcome, MachineError> {
355 session::step(
356 &m.info.compiled,
357 m.session.clone(),
358 Input::event(e, env),
359 m.dm.as_mut(),
360 )
361 .map_err(|e| match e {
362 StepError::Terminated => MachineError::Terminated,
363 other => MachineError::Step(format!("{other:?}")),
364 })
365 };
366 let admitted = vertep_core::datamodel::admit(self.dm.as_ref(), ev.clone());
369 let o = step(self, admitted)?;
370 let unhandled = o
371 .trace
372 .microsteps
373 .iter()
374 .find(|m| m.event.as_deref() == Some(ev.name.as_str()))
375 .is_some_and(|m| m.transitions.is_empty());
376 self.session = o.session;
377 let mut raw = start_effects;
378 raw.extend(o.effects);
379 traces.push(o.trace);
380 if unhandled {
381 out.effects.unhandled = Some(policy);
382 if policy == UnhandledPolicy::Error && !self.session.is_terminated() {
383 let data = Value::Object(vec![
384 ("name".into(), Value::String(ev.name.clone())),
385 ("data".into(), ev.data.clone()),
386 ]);
387 let e = Event {
388 name: vt::UNHANDLED_EVENT.into(),
389 type_: EventType::Platform,
390 data,
391 ..Event::default()
392 };
393 let o2 = step(self, e)?;
394 self.session = o2.session;
395 raw.extend(o2.effects);
396 traces.push(o2.trace);
397 }
398 }
399 for t in &traces {
400 for m in &t.microsteps {
401 for tr in &m.transitions {
402 let frag = format!(
403 "{}:t{}",
404 vertep_rdf::project::state_fragment(&tr.source),
405 tr.index
406 );
407 let iri = vertep_rdf::iri::node_iri(self.info.version.as_str(), &frag);
408 if !out.effects.transitions.contains(&iri) {
409 out.effects.transitions.push(iri);
410 }
411 }
412 }
413 }
414 out.effects.transitions.sort();
415 self.collect(raw, &entry, position, ts, input, &mut out);
416 out.effects.terminated = self.session.is_terminated();
417 if out.effects.terminated {
418 let reminders: Vec<String> = self
419 .rt
420 .timers
421 .iter()
422 .filter(|(_, t)| t.reminder.is_some())
423 .map(|(k, _)| k.clone())
424 .collect();
425 for r in reminders {
426 self.rt.timers.remove(&r);
427 self.rt.cancelled.insert(r.clone());
428 out.effects.cancels.push(r);
429 }
430 }
431 Ok(out)
432 }
433
434 fn recipient(&self, target: Option<&str>) -> Option<String> {
435 match target {
436 None | Some("#_internal") => Some(self.address.as_str().to_owned()),
437 Some("#_parent") => None,
438 Some(t) => {
439 if let Some(sid) = t.strip_prefix("#_scxml_") {
440 Address::parse(sid).ok().map(|a| a.as_str().to_owned())
441 } else if let Some(id) = t.strip_prefix("#_") {
442 Some(Address::invoked(&self.address, id).as_str().to_owned())
443 } else {
444 Address::parse(t).ok().map(|a| a.as_str().to_owned())
445 }
446 }
447 }
448 }
449
450 fn collect(
451 &mut self,
452 raw: Vec<Effect>,
453 entry: &NamedNode,
454 position: u64,
455 ts: i64,
456 input: &InputRecord,
457 out: &mut Applied,
458 ) {
459 let mut requests = 0usize;
460 for e in raw {
461 match e {
462 Effect::Send(req) => {
463 let (base, params) = split_type(&req.type_);
464 if base == vt::REMINDER_SEND_TYPE {
465 let name = params.get("name").cloned().unwrap_or_default();
466 let existing: Vec<String> = self
467 .rt
468 .timers
469 .iter()
470 .filter(|(_, t)| t.reminder.as_ref().is_some_and(|r| r.name == name))
471 .map(|(k, _)| k.clone())
472 .collect();
473 for k in existing {
474 self.rt.timers.remove(&k);
475 self.rt.cancelled.insert(k.clone());
476 out.effects.cancels.push(k);
477 }
478 if params.contains_key("cancel") {
479 continue;
480 }
481 let period: i64 = params
482 .get("period")
483 .and_then(|p| p.parse().ok())
484 .unwrap_or(1)
485 .max(1);
486 let t = TimerRecord {
487 iri: address::timer_iri(entry, &format!("reminder:{name}"))
488 .as_str()
489 .to_owned(),
490 actor: self.address.as_str().to_owned(),
491 send_id: name.clone(),
492 due_ms: ts.saturating_add(period),
493 event: self.origin_event(req.event),
494 recipient: self.address.as_str().to_owned(),
495 reminder: Some(ReminderSpec {
496 name,
497 period_ms: period,
498 }),
499 correlation: None,
500 };
501 self.rt.timers.insert(t.iri.clone(), t.clone());
502 out.effects.timers.push(t);
503 continue;
504 }
505 if base == vt::MIGRATE_SEND_TYPE {
506 out.migrations.push((
507 params.get("to").cloned().unwrap_or_default(),
508 params.get("migration").cloned().unwrap_or_default(),
509 ));
510 continue;
511 }
512 let recipient = self.recipient(req.target.as_deref());
513 if let Some(id) = req.target.as_deref().and_then(|t| t.strip_prefix("#_"))
514 && let Some(log) = self.rt.invocations.get_mut(id)
515 {
516 log.events.push((req.event.clone(), ts));
517 }
518 let correlation = if base == vt::REQUEST_SEND_TYPE {
519 let c = self.address.correlation_id(position, requests);
520 requests += 1;
521 out.effects.requests.push(c.clone());
522 let timeout = params.get("timeout").and_then(|t| t.parse::<i64>().ok());
523 let timer = timeout.map(|t| {
524 let rec = TimerRecord {
525 iri: address::timer_iri(entry, &format!("timeout:{c}"))
526 .as_str()
527 .to_owned(),
528 actor: self.address.as_str().to_owned(),
529 send_id: format!("timeout:{c}"),
530 due_ms: ts.saturating_add(t),
531 event: self.origin_event(Event {
532 name: vt::TIMEOUT_EVENT.into(),
533 type_: EventType::Platform,
534 data: Value::Object(vec![(
535 "correlation".into(),
536 Value::String(c.clone()),
537 )]),
538 ..Event::default()
539 }),
540 recipient: self.address.as_str().to_owned(),
541 reminder: None,
542 correlation: Some(c.clone()),
543 };
544 self.rt.timers.insert(rec.iri.clone(), rec.clone());
545 out.effects.timers.push(rec.clone());
546 rec.iri
547 });
548 self.rt.requests.insert(c.clone(), timer);
549 Some(c)
550 } else if req.event.name == vt::REPLY_EVENT {
551 input.correlation.clone()
552 } else {
553 None
554 };
555 let event = self.origin_event(req.event);
556 if req.delay_ms > 0 {
557 let t = TimerRecord {
558 iri: address::timer_iri(entry, &req.sendid).as_str().to_owned(),
559 actor: self.address.as_str().to_owned(),
560 send_id: req.sendid.clone(),
561 due_ms: ts
562 .saturating_add(i64::try_from(req.delay_ms).unwrap_or(i64::MAX)),
563 event,
564 recipient: recipient.unwrap_or_else(|| req.target.unwrap_or_default()),
565 reminder: None,
566 correlation,
567 };
568 self.rt.timers.insert(t.iri.clone(), t.clone());
569 out.effects.timers.push(t);
570 } else {
571 let k = out.effects.sends.len();
572 out.effects.sends.push(SendRecord {
573 iri: address::send_iri(entry, k).as_str().to_owned(),
574 send_id: req.sendid,
575 event,
576 target: req.target,
577 recipient,
578 type_: req.type_,
579 correlation,
580 });
581 }
582 }
583 Effect::Cancel { sendid } => {
584 let found: Vec<String> = self
585 .rt
586 .timers
587 .iter()
588 .filter(|(_, t)| t.send_id == sendid && t.reminder.is_none())
589 .map(|(k, _)| k.clone())
590 .collect();
591 for k in found {
592 self.rt.timers.remove(&k);
593 self.rt.cancelled.insert(k.clone());
594 out.effects.cancels.push(k);
595 }
596 }
597 Effect::Invoke(req) => {
598 out.effects
599 .invokes
600 .push(serde_json::to_string(&req).unwrap_or_default());
601 self.rt.invocations.insert(
602 req.invokeid.clone(),
603 InvocationLog {
604 request: req.clone(),
605 events: Vec::new(),
606 },
607 );
608 out.invokes.push(req);
609 }
610 Effect::CancelInvoke { invokeid } => {
611 self.rt.invocations.remove(&invokeid);
612 out.cancel_invokes.push(invokeid);
613 }
614 Effect::Forward { invokeid, event } => {
615 let k = out.effects.sends.len();
616 if let Some(log) = self.rt.invocations.get_mut(&invokeid) {
617 log.events.push((event.clone(), ts));
618 }
619 out.effects.sends.push(SendRecord {
620 iri: address::send_iri(entry, k).as_str().to_owned(),
621 send_id: format!("forward:{k}"),
622 event: self.origin_event(event),
623 target: Some(format!("#_{invokeid}")),
624 recipient: Some(
625 Address::invoked(&self.address, &invokeid)
626 .as_str()
627 .to_owned(),
628 ),
629 type_: SCXML_EVENT_PROCESSOR.to_owned(),
630 correlation: None,
631 });
632 }
633 Effect::Log { .. } | Effect::SessionDone { .. } => {}
634 }
635 }
636 }
637
638 fn origin_event(&self, mut e: Event) -> Event {
640 e.origin = Some(self.address.as_str().to_owned());
641 e.origintype = Some(SCXML_EVENT_PROCESSOR.to_owned());
642 e.originverified = false;
644 e
645 }
646}
647
648struct ActiveIn<'a> {
650 chart: &'a vertep_core::compile::CompiledChart,
651 session: &'a Session,
652}
653
654impl vertep_core::datamodel::StateQuery for ActiveIn<'_> {
655 fn in_state(&self, id: &str) -> bool {
656 self.session.is_active(self.chart, id)
657 }
658
659 fn active_states(&self) -> Option<Vec<String>> {
660 Some(self.session.active(self.chart))
661 }
662}
663
664pub fn state_node(info: &ChartInfo, id: &str) -> NamedNode {
666 NamedNode::new_unchecked(vertep_rdf::iri::node_iri(
667 info.version.as_str(),
668 &vertep_rdf::project::state_fragment(id),
669 ))
670}