Skip to main content

vertep_runtime/
chart.rs

1//! Chart versions, the chart registry, `vt:` extension lowering and migrations.
2//!
3//! A chart version is the content address of the chart as written (`vertep_rdf::chart_version`
4//! over the IR before lowering), so the same chart loaded from SCXML or YAML has one version.
5//! The registry keeps every version by IRI, the versions of each chart type in registration
6//! order (the last is the latest) and the declared migrations.
7//!
8//! Before compiling, [`lower`] rewrites the runtime's `vt:` extensions into plain SCXML:
9//!
10//! - `<vt:request target=".." event=".." timeout="5s" reply="s1" ontimeout="s2"/>` in a state:
11//!   a `<send>` of type [`vocab::REQUEST_SEND_TYPE`] on entry, a generated waiting substate, a
12//!   transition on `vt:reply` and one on `error.timeout`. The runtime assigns the correlation id
13//!   and journals the timeout as a durable timer.
14//! - `<vt:reminder name=".." period=".." event=".."/>` (or `cancel="true"`) in executable
15//!   content: a `<send>` of type [`vocab::REMINDER_SEND_TYPE`].
16//! - `<vt:migrate to=".." migration=".."/>` in executable content: a `<send>` of type
17//!   [`vocab::MIGRATE_SEND_TYPE`], handled as a self-migration request.
18//!
19//! `vt:defer` (on states), `vt:unhandled` (on `<scxml>`) and `vt:restart*` (on `<invoke>`) are
20//! read from the IR without rewriting it.
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::sync::Arc;
24
25use vertep_core::compile::{CompiledChart, ROOT, StateIdx};
26use vertep_core::datamodel::Value;
27use vertep_core::ir::{
28    Block, Chart, Exec, ExecKind, Extension, ExtensionValue, Send as SendElem, State, StateElement,
29    Transition, VT_NS,
30};
31use vertep_core::kernel::History;
32use vertep_core::session::Session;
33use vertep_rdf::ChartVersion;
34
35use crate::vocab;
36
37/// What to do with an external event that enables no transition and is not deferred.
38// @tg: implements:: [[openspec:messaging#Unhandled Event Policy]]
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
40pub enum UnhandledPolicy {
41    /// Drop it, as the SCXML algorithm does (default for plain sessions).
42    Discard,
43    /// Record it as a `vt:DeadLetter` (default for virtual actors).
44    DeadLetter,
45    /// Raise `error.unhandled` in the chart with the event as data.
46    Error,
47}
48
49impl UnhandledPolicy {
50    /// The name used in `vt:unhandled` and in journal entries.
51    pub fn as_str(self) -> &'static str {
52        match self {
53            UnhandledPolicy::Discard => "discard",
54            UnhandledPolicy::DeadLetter => "dead-letter",
55            UnhandledPolicy::Error => "error",
56        }
57    }
58
59    /// The policy named `s`.
60    pub fn parse(s: &str) -> Option<Self> {
61        match s.trim() {
62            "discard" => Some(UnhandledPolicy::Discard),
63            "dead-letter" => Some(UnhandledPolicy::DeadLetter),
64            "error" => Some(UnhandledPolicy::Error),
65            _ => None,
66        }
67    }
68}
69
70/// When an invoked actor is restarted after it fails.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum RestartMode {
73    /// Never (the SCXML default).
74    Never,
75    /// After a failure.
76    OnFailure,
77    /// After a failure and after a normal completion, which then raises no `done.invoke`.
78    Always,
79}
80
81/// A `vt:restart` supervision policy of an `<invoke>`.
82// @tg: implements:: [[openspec:actor-model#Supervision With Restart]]
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct RestartPolicy {
85    /// When to restart.
86    pub mode: RestartMode,
87    /// Restarts allowed within the window; one more failure gives up.
88    pub limit: u32,
89    /// The window, in milliseconds.
90    pub window_ms: i64,
91    /// The first backoff; the n-th restart waits `base * 2^(n-1)`.
92    pub backoff_ms: i64,
93}
94
95impl Default for RestartPolicy {
96    fn default() -> Self {
97        RestartPolicy {
98            mode: RestartMode::Never,
99            limit: 3,
100            window_ms: 60_000,
101            backoff_ms: 100,
102        }
103    }
104}
105
106impl RestartPolicy {
107    /// The backoff before the `n`-th restart (1-based).
108    pub fn backoff(&self, n: u32) -> i64 {
109        self.backoff_ms
110            .saturating_mul(1i64 << n.saturating_sub(1).min(30))
111    }
112}
113
114/// Parses a duration: a number followed by `ms`, `s`, `min`, `h` or `d`, or a bare number of
115/// milliseconds.
116pub fn parse_duration(text: &str) -> Option<i64> {
117    let t = text.trim();
118    let units: [(&str, f64); 5] = [
119        ("ms", 1.0),
120        ("min", 60_000.0),
121        ("s", 1_000.0),
122        ("h", 3_600_000.0),
123        ("d", 86_400_000.0),
124    ];
125    for (suffix, factor) in units {
126        if let Some(n) = t.strip_suffix(suffix) {
127            let v: f64 = n.trim().parse().ok()?;
128            return (v >= 0.0).then(|| (v * factor).round() as i64);
129        }
130    }
131    t.parse::<i64>().ok().filter(|v| *v >= 0)
132}
133
134/// The attributes of the start tag of an extension element's serialized XML.
135pub fn element_attributes(xml: &str) -> BTreeMap<String, String> {
136    let mut out = BTreeMap::new();
137    let Some(start) = xml.find('<') else {
138        return out;
139    };
140    let rest = &xml[start + 1..];
141    let end = rest.find('>').unwrap_or(rest.len());
142    let tag = rest[..end].trim_end_matches('/');
143    let mut chars = tag.char_indices().peekable();
144    // skip the element name
145    while let Some((_, c)) = chars.peek() {
146        if c.is_whitespace() {
147            break;
148        }
149        chars.next();
150    }
151    let bytes = tag;
152    let mut i = chars.peek().map_or(bytes.len(), |(i, _)| *i);
153    while i < bytes.len() {
154        let rest = &bytes[i..];
155        let trimmed = rest.trim_start();
156        i += rest.len() - trimmed.len();
157        let Some(eq) = trimmed.find('=') else { break };
158        let name = trimmed[..eq].trim().to_owned();
159        let after = trimmed[eq + 1..].trim_start();
160        let Some(q) = after.chars().next().filter(|c| *c == '"' || *c == '\'') else {
161            break;
162        };
163        let body = &after[1..];
164        let Some(close) = body.find(q) else { break };
165        out.insert(
166            name.rsplit(':').next().unwrap_or(&name).to_owned(),
167            unescape(&body[..close]),
168        );
169        let consumed = (trimmed.len() - after.len()) + 1 + close + 1;
170        i += consumed;
171    }
172    out
173}
174
175fn unescape(s: &str) -> String {
176    s.replace("&lt;", "<")
177        .replace("&gt;", ">")
178        .replace("&quot;", "\"")
179        .replace("&apos;", "'")
180        .replace("&amp;", "&")
181}
182
183fn vt_attr<'a>(exts: &'a [Extension], local: &str) -> Option<&'a str> {
184    exts.iter().find_map(|e| match &e.value {
185        ExtensionValue::Attribute(v) if e.ns == VT_NS && e.local == local => Some(v.as_str()),
186        _ => None,
187    })
188}
189
190fn vt_elements<'a>(
191    exts: &'a [Extension],
192    local: &'a str,
193) -> impl Iterator<Item = BTreeMap<String, String>> + 'a {
194    exts.iter().filter_map(move |e| match &e.value {
195        ExtensionValue::Element(xml) if e.ns == VT_NS && e.local == local => {
196            Some(element_attributes(xml))
197        }
198        _ => None,
199    })
200}
201
202/// Why a chart's `vt:` extensions could not be lowered.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct LowerError(pub String);
205
206impl core::fmt::Display for LowerError {
207    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208        write!(f, "invalid vt: extension: {}", self.0)
209    }
210}
211
212/// The send type of a `vt:request` with an optional timeout.
213// @tg: implements:: [[openspec:messaging#Request Timeout]]
214pub fn request_type(timeout_ms: Option<i64>) -> String {
215    match timeout_ms {
216        Some(t) => format!("{};timeout={t}", vocab::REQUEST_SEND_TYPE),
217        None => vocab::REQUEST_SEND_TYPE.to_owned(),
218    }
219}
220
221/// The parameters of a `vt:` send type: `base;k=v;k2=v2` gives `(base, {k: v, k2: v2})`.
222pub fn split_type(type_: &str) -> (&str, BTreeMap<String, String>) {
223    let mut parts = type_.split(';');
224    let base = parts.next().unwrap_or_default();
225    let params = parts
226        .map(|p| {
227            let (k, v) = p.split_once('=').unwrap_or((p, ""));
228            (k.to_owned(), v.to_owned())
229        })
230        .collect();
231    (base, params)
232}
233
234/// Rewrites the runtime's `vt:` extensions into plain SCXML (see the module docs), returning
235/// the lowered chart and the send types it introduced.
236///
237/// # Errors
238///
239/// [`LowerError`] for a `vt:request` without `event` or with a malformed timeout, or a
240/// `vt:reminder` without `name`.
241// @tg: implements:: [[openspec:messaging#Non-Blocking Request]]
242pub fn lower(chart: &Chart) -> Result<(Chart, BTreeSet<String>), LowerError> {
243    let mut out = chart.clone();
244    let mut types = BTreeSet::new();
245    for s in &mut out.states {
246        lower_state(s, &mut types)?;
247    }
248    Ok((out, types))
249}
250
251fn lower_state(s: &mut State, types: &mut BTreeSet<String>) -> Result<(), LowerError> {
252    let requests: Vec<_> = vt_elements(&s.extensions, "request").collect();
253    for (k, r) in requests.iter().enumerate() {
254        let event = r
255            .get("event")
256            .ok_or_else(|| LowerError(format!("vt:request in state {} has no event", s.id)))?;
257        let timeout = match r.get("timeout") {
258            Some(t) => Some(parse_duration(t).ok_or_else(|| {
259                LowerError(format!("vt:request in state {}: bad timeout {t:?}", s.id))
260            })?),
261            None => None,
262        };
263        let ty = request_type(timeout);
264        types.insert(ty.clone());
265        let send = SendElem {
266            event: Some(event.clone()),
267            target: r.get("target").cloned(),
268            type_: Some(ty),
269            ..SendElem::default()
270        };
271        s.onentry
272            .push(vec![Exec::new(ExecKind::Send(Box::new(send)))]);
273        if k == 0 && s.element == StateElement::State && s.children.is_empty() {
274            s.children.push(State::new(
275                format!("{}.vt-waiting", s.id),
276                StateElement::State,
277            ));
278        }
279        if let Some(target) = r.get("reply") {
280            s.transitions.push(Transition {
281                events: vec![vocab::REPLY_EVENT.to_owned()],
282                targets: vec![target.clone()],
283                ..Transition::default()
284            });
285        }
286        if let Some(target) = r.get("ontimeout") {
287            s.transitions.push(Transition {
288                events: vec![vocab::TIMEOUT_EVENT.to_owned()],
289                targets: vec![target.clone()],
290                ..Transition::default()
291            });
292        }
293    }
294    for b in s.onentry.iter_mut().chain(s.onexit.iter_mut()) {
295        lower_block(b, types)?;
296    }
297    for t in &mut s.transitions {
298        lower_block(&mut t.body, types)?;
299    }
300    for c in &mut s.children {
301        lower_state(c, types)?;
302    }
303    Ok(())
304}
305
306fn lower_block(b: &mut Block, types: &mut BTreeSet<String>) -> Result<(), LowerError> {
307    for e in b.iter_mut() {
308        match &mut e.kind {
309            ExecKind::If { branches } => {
310                for br in branches {
311                    lower_block(&mut br.body, types)?;
312                }
313            }
314            ExecKind::Foreach { body, .. } => lower_block(body, types)?,
315            ExecKind::Extension(x) if x.ns == VT_NS => {
316                let ExtensionValue::Element(xml) = &x.value else {
317                    continue;
318                };
319                let a = element_attributes(xml);
320                let send = match x.local.as_str() {
321                    "reminder" => {
322                        let name = a
323                            .get("name")
324                            .ok_or_else(|| LowerError("vt:reminder has no name".into()))?;
325                        let ty = if a.get("cancel").is_some_and(|c| c == "true") {
326                            format!("{};name={name};cancel=true", vocab::REMINDER_SEND_TYPE)
327                        } else {
328                            let period = a
329                                .get("period")
330                                .and_then(|p| parse_duration(p))
331                                .filter(|p| *p > 0)
332                                .ok_or_else(|| {
333                                    LowerError(format!("vt:reminder {name}: bad period"))
334                                })?;
335                            format!("{};name={name};period={period}", vocab::REMINDER_SEND_TYPE)
336                        };
337                        SendElem {
338                            event: Some(a.get("event").cloned().unwrap_or_else(|| name.clone())),
339                            type_: Some(ty),
340                            ..SendElem::default()
341                        }
342                    }
343                    "migrate" => {
344                        let to = a
345                            .get("to")
346                            .ok_or_else(|| LowerError("vt:migrate has no `to`".into()))?;
347                        let m = a.get("migration").cloned().unwrap_or_default();
348                        SendElem {
349                            event: Some("vt:migrate".into()),
350                            type_: Some(format!(
351                                "{};to={to};migration={m}",
352                                vocab::MIGRATE_SEND_TYPE
353                            )),
354                            ..SendElem::default()
355                        }
356                    }
357                    _ => continue,
358                };
359                if let Some(t) = &send.type_ {
360                    types.insert(t.clone());
361                }
362                e.kind = ExecKind::Send(Box::new(send));
363            }
364            _ => {}
365        }
366    }
367    Ok(())
368}
369
370/// A registered chart version: the IR as written, the compiled lowered chart and what the
371/// runtime reads from its `vt:` extensions.
372pub struct ChartInfo {
373    /// The version IRI (content address of [`ChartInfo::ir`]).
374    pub version: ChartVersion,
375    /// The chart type this version was registered under, if any.
376    pub chart_type: Option<String>,
377    /// The chart as written.
378    pub ir: Chart,
379    /// The lowered, compiled chart that runs.
380    pub compiled: CompiledChart,
381    /// The `vt:` send types the lowering introduced.
382    pub vt_send_types: BTreeSet<String>,
383    /// `vt:defer` descriptors per state index.
384    pub defers: BTreeMap<StateIdx, Vec<String>>,
385    /// The declared `vt:unhandled` policy.
386    pub unhandled: Option<UnhandledPolicy>,
387}
388
389impl std::fmt::Debug for ChartInfo {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        f.debug_struct("ChartInfo")
392            .field("version", &self.version.as_str())
393            .field("chart_type", &self.chart_type)
394            .finish_non_exhaustive()
395    }
396}
397
398/// Why a chart could not be registered.
399#[derive(Clone, Debug, PartialEq, Eq)]
400pub enum RegisterError {
401    /// The chart type is not an absolute IRI.
402    BadType(String),
403    /// A `vt:` extension is malformed.
404    Lower(LowerError),
405    /// The lowered chart failed validation.
406    Invalid(Vec<String>),
407    /// An `<invoke>` targets a virtual actor, which is never invokable.
408    InvokesVirtualActor {
409        /// The invoking state.
410        state: String,
411        /// The `src` naming a virtual actor.
412        src: String,
413    },
414    /// The chart's datamodel refuses to instantiate it, such as a typed CEL chart with type
415    /// errors.
416    Refused {
417        /// The datamodel name.
418        datamodel: String,
419        /// Every problem, each with its location in the chart.
420        problems: Vec<String>,
421    },
422}
423
424impl core::fmt::Display for RegisterError {
425    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
426        match self {
427            RegisterError::BadType(t) => write!(f, "chart type {t:?} is not an absolute IRI"),
428            RegisterError::Lower(e) => write!(f, "{e}"),
429            RegisterError::Invalid(v) => write!(f, "chart is invalid: {}", v.join("; ")),
430            RegisterError::InvokesVirtualActor { state, src } => write!(
431                f,
432                "state {state} invokes the virtual actor {src}; virtual actors are not invokable"
433            ),
434            RegisterError::Refused {
435                datamodel,
436                problems,
437            } => write!(
438                f,
439                "the {datamodel} datamodel refuses the chart: {}",
440                problems.join("; ")
441            ),
442        }
443    }
444}
445
446impl std::error::Error for RegisterError {}
447
448impl ChartInfo {
449    /// Lowers, compiles and analyses `chart`.
450    ///
451    /// # Errors
452    ///
453    /// [`RegisterError`] when lowering or validation fails.
454    pub fn new(chart: Chart, chart_type: Option<String>) -> Result<Self, RegisterError> {
455        let version = vertep_rdf::chart_version(&chart);
456        let (lowered, vt_send_types) = lower(&chart).map_err(RegisterError::Lower)?;
457        let compiled = CompiledChart::new(lowered)
458            .map_err(|v| RegisterError::Invalid(v.iter().map(|x| format!("{x:?}")).collect()))?;
459        let mut defers = BTreeMap::new();
460        collect_defers(&compiled.chart().states, &compiled, &mut defers);
461        let unhandled = vt_attr(&chart.extensions, "unhandled").and_then(UnhandledPolicy::parse);
462        Ok(ChartInfo {
463            version,
464            chart_type,
465            ir: chart,
466            compiled,
467            vt_send_types,
468            defers,
469            unhandled,
470        })
471    }
472
473    /// The event descriptors deferred by the states active in `session`.
474    // @tg: implements:: [[openspec:messaging#Deferred Events]]
475    pub fn deferred(&self, session: &Session) -> Vec<String> {
476        let mut out = Vec::new();
477        for (idx, ds) in &self.defers {
478            if session.configuration.contains(*idx) {
479                out.extend(ds.iter().cloned());
480            }
481        }
482        out
483    }
484
485    /// The `vt:restart` policy of the `index`-th `<invoke>` of state `state`.
486    pub fn restart_policy(&self, state: &str, index: usize) -> RestartPolicy {
487        let mut p = RestartPolicy::default();
488        let Some(inv) = find_state(&self.ir.states, state).and_then(|s| s.invokes.get(index))
489        else {
490            return p;
491        };
492        let x = &inv.extensions;
493        p.mode = match vt_attr(x, "restart") {
494            Some("on-failure") => RestartMode::OnFailure,
495            Some("always") => RestartMode::Always,
496            _ => RestartMode::Never,
497        };
498        if let Some(l) = vt_attr(x, "restart-limit").and_then(|v| v.trim().parse().ok()) {
499            p.limit = l;
500        }
501        if let Some(w) = vt_attr(x, "restart-window").and_then(parse_duration) {
502            p.window_ms = w;
503        }
504        if let Some(b) = vt_attr(x, "restart-backoff").and_then(parse_duration) {
505            p.backoff_ms = b;
506        }
507        p
508    }
509
510    /// The active state ids of `session`, in document order.
511    pub fn active(&self, session: &Session) -> Vec<String> {
512        session.active(&self.compiled)
513    }
514}
515
516fn collect_defers(
517    states: &[State],
518    chart: &CompiledChart,
519    out: &mut BTreeMap<StateIdx, Vec<String>>,
520) {
521    for s in states {
522        if let Some(d) = vt_attr(&s.extensions, "defer")
523            && let Some(i) = chart.index_of(&s.id)
524        {
525            out.insert(i, d.split_whitespace().map(str::to_owned).collect());
526        }
527        collect_defers(&s.children, chart, out);
528    }
529}
530
531/// The state with identifier `id`.
532pub fn find_state<'a>(states: &'a [State], id: &str) -> Option<&'a State> {
533    for s in states {
534        if s.id == id {
535            return Some(s);
536        }
537        if let Some(f) = find_state(&s.children, id) {
538            return Some(f);
539        }
540    }
541    None
542}
543
544/// Every `<invoke>` with a static `src`, as (state id, src).
545pub fn static_invoke_sources(states: &[State], out: &mut Vec<(String, String)>) {
546    for s in states {
547        for inv in &s.invokes {
548            if let Some(src) = &inv.src {
549                out.push((s.id.clone(), src.clone()));
550            }
551        }
552        static_invoke_sources(&s.children, out);
553    }
554}
555
556/// A datamodel transform of a migration.
557pub type Transform = Arc<dyn Fn(Value) -> Result<Value, String> + Send + Sync>;
558
559/// A declared migration between two chart versions: a configuration mapping and a datamodel
560/// transform. Configurations not listed explicitly map state by state through
561/// [`Migration::states`], and a state not listed there maps to the state with the same id in the
562/// new version, if it has one (the derived identity mapping).
563// @tg: implements:: [[openspec:chart-versioning#Explicit Migration]]
564#[derive(Clone)]
565pub struct Migration {
566    /// The migration's IRI.
567    pub iri: String,
568    /// The version it migrates from.
569    pub from: ChartVersion,
570    /// The version it migrates to.
571    pub to: ChartVersion,
572    /// Explicit configuration mappings (old active state ids to new active state ids).
573    pub configurations: Vec<(BTreeSet<String>, BTreeSet<String>)>,
574    /// State renames applied to configurations without an explicit mapping.
575    pub states: BTreeMap<String, String>,
576    /// The datamodel transform.
577    pub transform: Transform,
578}
579
580impl std::fmt::Debug for Migration {
581    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582        f.debug_struct("Migration")
583            .field("iri", &self.iri)
584            .field("from", &self.from.as_str())
585            .field("to", &self.to.as_str())
586            .finish_non_exhaustive()
587    }
588}
589
590impl Migration {
591    /// A migration with only the derived identity mapping and an identity transform.
592    pub fn identity(iri: impl Into<String>, from: ChartVersion, to: ChartVersion) -> Self {
593        Migration {
594            iri: iri.into(),
595            from,
596            to,
597            configurations: Vec::new(),
598            states: BTreeMap::new(),
599            transform: Arc::new(Ok),
600        }
601    }
602
603    /// This migration with an explicit configuration mapping.
604    pub fn map_configuration<I: IntoIterator<Item = S>, S: Into<String>>(
605        mut self,
606        from: I,
607        to: I,
608    ) -> Self {
609        self.configurations.push((
610            from.into_iter().map(Into::into).collect(),
611            to.into_iter().map(Into::into).collect(),
612        ));
613        self
614    }
615
616    /// This migration with a state rename.
617    pub fn rename_state(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
618        self.states.insert(from.into(), to.into());
619        self
620    }
621
622    /// This migration with a datamodel transform.
623    pub fn with_transform(
624        mut self,
625        f: impl Fn(Value) -> Result<Value, String> + Send + Sync + 'static,
626    ) -> Self {
627        self.transform = Arc::new(f);
628        self
629    }
630
631    /// The new configuration for `old`, or `None` when unmapped.
632    pub fn map(&self, old: &BTreeSet<String>, to: &ChartInfo) -> Option<BTreeSet<String>> {
633        let core: BTreeSet<String> = old.iter().filter(|s| !is_generated(s)).cloned().collect();
634        if let Some((_, n)) = self.configurations.iter().find(|(o, _)| *o == core) {
635            return Some(n.clone());
636        }
637        let mut out = BTreeSet::new();
638        for s in &core {
639            let n = self.states.get(s).unwrap_or(s);
640            to.compiled.index_of(n)?;
641            out.insert(n.clone());
642        }
643        Some(out)
644    }
645}
646
647fn is_generated(id: &str) -> bool {
648    id.ends_with(".vt-waiting")
649}
650
651/// The configurations reachable in `chart`, ignoring guards (an over-approximation): every
652/// legal configuration of its state structure, as sets of state ids.
653///
654/// # Errors
655///
656/// The number of configurations found so far when it exceeds `limit`.
657// @tg: implements:: [[openspec:chart-versioning#Migration Compatibility Check]]
658pub fn reachable_configurations(
659    chart: &Chart,
660    limit: usize,
661) -> Result<Vec<BTreeSet<String>>, usize> {
662    let mut all = Vec::new();
663    for s in &chart.states {
664        if matches!(s.element, StateElement::History(_)) {
665            continue;
666        }
667        all.extend(configs(s, limit)?);
668        if all.len() > limit {
669            return Err(all.len());
670        }
671    }
672    Ok(all)
673}
674
675fn configs(s: &State, limit: usize) -> Result<Vec<BTreeSet<String>>, usize> {
676    let kids: Vec<&State> = s
677        .children
678        .iter()
679        .filter(|c| !matches!(c.element, StateElement::History(_)))
680        .collect();
681    let mut out: Vec<BTreeSet<String>> = Vec::new();
682    if kids.is_empty() {
683        out.push(BTreeSet::from([s.id.clone()]));
684    } else if s.element == StateElement::Parallel {
685        out.push(BTreeSet::from([s.id.clone()]));
686        for k in kids {
687            let sub = configs(k, limit)?;
688            let mut next = Vec::with_capacity(out.len() * sub.len());
689            for a in &out {
690                for b in &sub {
691                    let mut c = a.clone();
692                    c.extend(b.iter().cloned());
693                    next.push(c);
694                }
695                if next.len() > limit {
696                    return Err(next.len());
697                }
698            }
699            out = next;
700        }
701    } else {
702        for k in kids {
703            for mut c in configs(k, limit)? {
704                c.insert(s.id.clone());
705                out.push(c);
706            }
707            if out.len() > limit {
708                return Err(out.len());
709            }
710        }
711    }
712    Ok(out)
713}
714
715/// The configurations of `from` that `m` leaves unmapped into `to`; empty when the migration
716/// passes the compatibility check.
717pub fn unmapped_configurations(
718    m: &Migration,
719    from: &ChartInfo,
720    to: &ChartInfo,
721) -> Result<Vec<BTreeSet<String>>, usize> {
722    Ok(reachable_configurations(&from.ir, 100_000)?
723        .into_iter()
724        .filter(|c| m.map(c, to).is_none())
725        .collect())
726}
727
728/// The session under `to` for active state ids `ids`, carrying the counters of `old`: compound
729/// states without an active child get their default entry, history and invocations start empty.
730pub fn migrated_session(old: &Session, ids: &BTreeSet<String>, to: &ChartInfo) -> Session {
731    let c = &to.compiled;
732    let mut config = c.empty_set();
733    for id in ids {
734        if let Some(i) = c.index_of(id) {
735            config.insert(i);
736            let mut p = c.state(i).parent;
737            while let Some(a) = p {
738                if a == ROOT {
739                    break;
740                }
741                config.insert(a);
742                p = c.state(a).parent;
743            }
744        }
745    }
746    complete(c, ROOT, &mut config);
747    let mut data = config.clone();
748    data.insert(ROOT);
749    let mut options = old.options.clone();
750    for t in &to.vt_send_types {
751        if !options.send_types.contains(t) {
752            options.send_types.push(t.clone());
753        }
754    }
755    Session {
756        sessionid: old.sessionid.clone(),
757        status: old.status,
758        configuration: config,
759        history: History::new(c.len()),
760        internal_queue: Default::default(),
761        invocations: Vec::new(),
762        data_initialized: data,
763        send_counter: old.send_counter,
764        invoke_counter: old.invoke_counter,
765        macrosteps: old.macrosteps,
766        options,
767    }
768}
769
770fn complete(c: &CompiledChart, s: StateIdx, config: &mut vertep_core::bitset::StateSet) {
771    let st = c.state(s);
772    if st.children.is_empty() {
773        return;
774    }
775    let parallel = st.kind == vertep_core::ir::StateKind::Parallel;
776    let active: Vec<StateIdx> = st
777        .children
778        .iter()
779        .copied()
780        .filter(|k| config.contains(*k))
781        .collect();
782    if parallel {
783        for k in st.children.clone() {
784            config.insert(k);
785            complete(c, k, config);
786        }
787    } else if active.is_empty() {
788        if let Some(&k) = c.default_entry(s).first().or(st.children.first()) {
789            config.insert(k);
790            let mut p = c.state(k).parent;
791            while let Some(a) = p {
792                if a == s {
793                    break;
794                }
795                config.insert(a);
796                p = c.state(a).parent;
797            }
798            complete(c, k, config);
799        }
800    } else {
801        for k in active {
802            complete(c, k, config);
803        }
804    }
805}
806
807/// Every chart version an actor may run, keyed by version IRI, the versions of each chart type
808/// in registration order, and the declared migrations.
809// @tg: implements:: [[openspec:chart-versioning#Chart Registry]]
810#[derive(Default)]
811pub struct ChartRegistry {
812    versions: BTreeMap<String, Arc<ChartInfo>>,
813    types: BTreeMap<String, Vec<String>>,
814    migrations: BTreeMap<String, Migration>,
815    /// Addresses bound to a chart type and key without the `<type>/<key>` form (DIDs).
816    bindings: BTreeMap<String, (String, String)>,
817}
818
819impl std::fmt::Debug for ChartRegistry {
820    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
821        f.debug_struct("ChartRegistry")
822            .field("versions", &self.versions.keys().collect::<Vec<_>>())
823            .field("types", &self.types)
824            .finish_non_exhaustive()
825    }
826}
827
828impl ChartRegistry {
829    /// Adds a version (idempotent) and, with a type, makes it the type's latest version.
830    pub fn insert(&mut self, info: ChartInfo) -> Arc<ChartInfo> {
831        let key = info.version.as_str().to_owned();
832        let ty = info.chart_type.clone();
833        let info = self
834            .versions
835            .entry(key.clone())
836            .or_insert_with(|| Arc::new(info))
837            .clone();
838        if let Some(t) = ty {
839            let list = self.types.entry(t).or_default();
840            list.retain(|v| *v != key);
841            list.push(key);
842        }
843        info
844    }
845
846    /// The version `iri`.
847    pub fn get(&self, iri: &str) -> Option<Arc<ChartInfo>> {
848        self.versions.get(iri).cloned()
849    }
850
851    /// The latest version registered for chart type `ty`.
852    pub fn latest(&self, ty: &str) -> Option<Arc<ChartInfo>> {
853        self.types
854            .get(ty)
855            .and_then(|l| l.last())
856            .and_then(|v| self.get(v))
857    }
858
859    /// The versions registered for chart type `ty`, oldest first.
860    pub fn versions_of(&self, ty: &str) -> Vec<String> {
861        self.types.get(ty).cloned().unwrap_or_default()
862    }
863
864    /// Every registered chart type.
865    pub fn types(&self) -> impl Iterator<Item = &String> {
866        self.types.keys()
867    }
868
869    /// The chart type and key of a virtual actor address (`<type>/<key>`), for the longest
870    /// registered type that prefixes it, or the type and key the address is bound to.
871    pub fn type_of(&self, address: &str) -> Option<(String, String)> {
872        if let Some(b) = self.bindings.get(address) {
873            return Some(b.clone());
874        }
875        self.types
876            .keys()
877            .filter_map(|t| {
878                let rest = address.strip_prefix(t.as_str())?.strip_prefix('/')?;
879                Some((t, rest))
880            })
881            .max_by_key(|(t, _)| t.len())
882            .and_then(|(t, k)| Some((t.clone(), crate::address::percent_decode(k)?)))
883    }
884
885    /// Binds `address` to the virtual actor of `chart_type` with `key`, for addresses that do
886    /// not have the `<type>/<key>` form, such as an instance whose address is its DID.
887    pub fn bind(&mut self, address: &str, chart_type: &str, key: &str) {
888        self.bindings
889            .insert(address.to_owned(), (chart_type.to_owned(), key.to_owned()));
890    }
891
892    /// Removes version `iri`; the caller has checked that no non-final actor uses it.
893    pub fn remove(&mut self, iri: &str) -> bool {
894        for l in self.types.values_mut() {
895            l.retain(|v| v != iri);
896        }
897        self.versions.remove(iri).is_some()
898    }
899
900    /// Declares a migration.
901    pub fn add_migration(&mut self, m: Migration) {
902        self.migrations.insert(m.iri.clone(), m);
903    }
904
905    /// The migration `iri`.
906    pub fn migration(&self, iri: &str) -> Option<Migration> {
907        self.migrations.get(iri).cloned()
908    }
909}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914
915    #[test]
916    fn durations() {
917        assert_eq!(parse_duration("100ms"), Some(100));
918        assert_eq!(parse_duration("5s"), Some(5_000));
919        assert_eq!(parse_duration("10min"), Some(600_000));
920        assert_eq!(parse_duration("1h"), Some(3_600_000));
921        assert_eq!(parse_duration("250"), Some(250));
922        assert_eq!(parse_duration("x"), None);
923    }
924
925    #[test]
926    fn attributes_of_an_extension_element() {
927        let a = element_attributes(
928            r#"<vt:request xmlns:vt="https://w3id.org/vertep/vt#" target="urn:b" event='price.get' timeout="5s"/>"#,
929        );
930        assert_eq!(a.get("target").map(String::as_str), Some("urn:b"));
931        assert_eq!(a.get("event").map(String::as_str), Some("price.get"));
932        assert_eq!(a.get("timeout").map(String::as_str), Some("5s"));
933        assert_eq!(
934            split_type("x;a=1;b=2").1.get("b").map(String::as_str),
935            Some("2")
936        );
937    }
938
939    #[test]
940    fn backoff_doubles() {
941        let p = RestartPolicy {
942            backoff_ms: 100,
943            ..RestartPolicy::default()
944        };
945        assert_eq!((p.backoff(1), p.backoff(2), p.backoff(3)), (100, 200, 400));
946    }
947}