Skip to main content

determa_state/
machine.rs

1//! The resolved machine: a flat state table (indexed by [`NodeId`]) with all
2//! `transition_to` references resolved to [`NodeId`]s, event scopes parsed, and
3//! actions/initials/choices expanded. Built once from [`crate::model::RawMachine`].
4//!
5//! Building is two-phase: first every state node is registered (completing the
6//! name table), then a second walk resolves all targets — which may be forward
7//! references to states defined later in the YAML.
8
9use crate::model::{self, Action, RawMachine, StateNode};
10use crate::value::Value;
11use std::collections::{BTreeMap, HashMap};
12
13pub type NodeId = usize;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Scope {
17    Internal,
18    Local,
19    Global,
20}
21
22impl Scope {
23    pub fn parse(s: Option<&str>) -> Scope {
24        match s {
25            Some("local") => Scope::Local,
26            Some("global") => Scope::Global,
27            _ => Scope::Internal,
28        }
29    }
30}
31
32#[derive(Debug, Clone)]
33pub struct PayloadField {
34    pub ty: String,
35    pub required: bool,
36    pub default: Option<Value>,
37}
38
39#[derive(Debug, Clone)]
40pub struct EventDecl {
41    pub scope: Scope,
42    pub payload: Vec<(String, PayloadField)>,
43}
44
45#[derive(Debug, Clone)]
46pub struct EsvDecl {
47    pub ty: String,
48    pub init: Option<Value>,
49    pub external: bool,
50}
51
52#[derive(Debug, Clone)]
53pub struct TransitionDef {
54    pub target: Option<NodeId>,
55    pub guard: Option<String>,
56    pub action: Vec<Action>,
57    pub internal: bool,
58    pub local: bool,
59    pub raw_target: String,
60}
61
62#[derive(Debug, Clone)]
63pub struct InitialDef {
64    pub target: NodeId,
65    pub guard: Option<String>,
66    pub action: Vec<Action>,
67}
68
69#[derive(Debug, Clone)]
70pub struct ChoiceBranchDef {
71    pub target: NodeId,
72    pub guard: Option<String>,
73    pub action: Vec<Action>,
74}
75
76#[derive(Debug, Clone)]
77pub struct AfterDef {
78    pub duration_ms: u64,
79    pub target: Option<NodeId>,
80    pub guard: Option<String>,
81    pub action: Vec<Action>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum StateKind {
86    Simple,
87    Composite,
88    Orthogonal,
89    Final,
90    Choice,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum HistoryKind {
95    None,
96    Shallow,
97    Deep,
98}
99
100#[derive(Debug, Clone)]
101pub struct RegionDef {
102    pub initial: InitialDef,
103    pub states: Vec<NodeId>,
104}
105
106#[derive(Debug, Clone)]
107pub struct StateDef {
108    pub id: String,
109    pub path: String,
110    pub parent: Option<NodeId>,
111    pub depth: usize,
112    pub kind: StateKind,
113    pub esvs: Vec<(String, EsvDecl)>,
114    pub entry: Vec<Action>,
115    pub exit: Vec<Action>,
116    pub initial: Option<InitialDef>,
117    pub children: Vec<NodeId>,
118    pub regions: Vec<RegionDef>,
119    pub on_events: BTreeMap<String, Vec<TransitionDef>>,
120    pub after: Vec<AfterDef>,
121    pub defer: Vec<String>,
122    pub history: HistoryKind,
123    pub choice: Option<Vec<ChoiceBranchDef>>,
124    pub region_of: Option<(NodeId, usize)>,
125    /// Inlined submachine root (esv-scope boundary, SPEC §5.6.1).
126    pub is_sm_boundary: bool,
127    pub sm_with: Vec<(String, String)>,
128    /// Opaque state annotations (SPEC §4.5); informative only.
129    pub meta: Value,
130}
131
132impl StateDef {
133    pub fn is_orthogonal(&self) -> bool {
134        matches!(self.kind, StateKind::Orthogonal)
135    }
136    pub fn has_history(&self) -> bool {
137        !matches!(self.history, HistoryKind::None)
138    }
139}
140
141#[derive(Debug, Clone)]
142pub struct MigrationDef {
143    pub from: i64,
144    pub to: i64,
145    pub when: Option<String>,
146    pub state_map: BTreeMap<String, String>,
147    pub esvs: Vec<Action>,
148}
149
150#[derive(Debug, Clone)]
151pub struct Machine {
152    pub id: String,
153    pub version: i64,
154    pub format: i64,
155    pub contracts: Vec<String>,
156    pub subscribe: Vec<String>,
157    pub events: BTreeMap<String, EventDecl>,
158    pub migrations: Vec<MigrationDef>,
159    pub states: Vec<StateDef>,
160    pub by_name: HashMap<String, NodeId>,
161    pub by_path: HashMap<String, NodeId>,
162    pub top: NodeId,
163    /// Opaque machine-level annotations (SPEC §4.1); informative only.
164    pub meta: Value,
165}
166
167impl Machine {
168    pub fn get(&self, n: NodeId) -> &StateDef {
169        &self.states[n]
170    }
171
172    /// Resolve a reference: dotted walks from top; bare looks up the unique name.
173    pub fn resolve(&self, raw: &str) -> Option<NodeId> {
174        resolve_ref(&self.by_name, raw)
175    }
176}
177
178/// Parse a duration like "30s", "5m", "10ms", "1h" into milliseconds.
179pub fn parse_duration(s: &str) -> Result<u64, String> {
180    let err = || format!("invalid duration '{s}'");
181    let split = s.find(|c: char| !c.is_ascii_digit()).ok_or_else(err)?;
182    let (num, unit) = s.split_at(split);
183    let n: u64 = num.parse().map_err(|_| err())?;
184    match unit {
185        "ms" => Ok(n),
186        "s" => n.checked_mul(1000).ok_or_else(err),
187        "m" => n.checked_mul(60_000).ok_or_else(err),
188        "h" => n.checked_mul(3_600_000).ok_or_else(err),
189        _ => Err(err()),
190    }
191}
192
193fn resolve_ref(by_name: &HashMap<String, NodeId>, raw: &str) -> Option<NodeId> {
194    if raw.is_empty() {
195        return None;
196    }
197    if raw.contains('.') {
198        let mut cur: Option<NodeId> = None;
199        for seg in raw.split('.') {
200            if seg == "top" {
201                cur = Some(0);
202                continue;
203            }
204            cur = by_name.get(seg).copied();
205            if cur.is_none() {
206                return None;
207            }
208        }
209        cur
210    } else {
211        by_name.get(raw).copied()
212    }
213}
214
215fn infer_kind(node: &StateNode) -> StateKind {
216    match node.ty.as_deref() {
217        Some("composite") => StateKind::Composite,
218        Some("orthogonal") => StateKind::Orthogonal,
219        Some("final") => StateKind::Final,
220        Some("simple") => StateKind::Simple,
221        _ => {
222            if node.choice.is_some() {
223                StateKind::Choice
224            } else if node.regions.is_some() {
225                StateKind::Orthogonal
226            } else if node.states.is_some() {
227                StateKind::Composite
228            } else {
229                StateKind::Simple
230            }
231        }
232    }
233}
234
235// ---------------------------------------------------------------------------
236// Phase 1: register all nodes (kind/esvs/entry/exit/defer/history + structure).
237
238#[allow(clippy::too_many_arguments)]
239fn register(
240    states: &mut Vec<StateDef>,
241    by_name: &mut HashMap<String, NodeId>,
242    by_path: &mut HashMap<String, NodeId>,
243    errors: &mut Vec<String>,
244    id: String,
245    path: String,
246    parent: Option<NodeId>,
247    node: &StateNode,
248) -> NodeId {
249    let depth = parent.map(|p| states[p].depth + 1).unwrap_or(0);
250    let kind = infer_kind(node);
251    let history = match node.history.as_deref() {
252        Some("shallow") => HistoryKind::Shallow,
253        Some("deep") => HistoryKind::Deep,
254        _ => HistoryKind::None,
255    };
256
257    let esvs = node
258        .esvs
259        .as_ref()
260        .map(|m| {
261            m.iter()
262                .map(|(name, e)| {
263                    let init = e.init.as_ref().and_then(|raw| match model::esv_init_value(&e.ty, raw) {
264                        Ok(v) => Some(v),
265                        Err(msg) => {
266                            errors.push(format!("esv '{path}.{name}': {msg}"));
267                            None
268                        }
269                    });
270                    (
271                        name.clone(),
272                        EsvDecl {
273                            ty: e.ty.clone(),
274                            init,
275                            external: e.external.unwrap_or(false),
276                        },
277                    )
278                })
279                .collect()
280        })
281        .unwrap_or_default();
282
283    let entry = parse_actions_or_err(errors, &path, "entry", node.entry.as_ref());
284    let exit = parse_actions_or_err(errors, &path, "exit", node.exit.as_ref());
285
286    let idx = states.len();
287    states.push(StateDef {
288        id: id.clone(),
289        path: path.clone(),
290        parent,
291        depth,
292        kind,
293        esvs,
294        entry,
295        exit,
296        initial: None,
297        children: Vec::new(),
298        regions: Vec::new(),
299        on_events: BTreeMap::new(),
300        after: Vec::new(),
301        defer: node.defer.clone().unwrap_or_default(),
302        history,
303        choice: None,
304        region_of: None,
305        is_sm_boundary: node.is_sm_boundary,
306        sm_with: node.sm_with.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
307        meta: node
308            .meta
309            .as_ref()
310            .map(|v| Value::from_yaml(v))
311            .unwrap_or(Value::Map(BTreeMap::new())),
312    });
313
314    if by_name.contains_key(&id) {
315        errors.push(format!("duplicate state name '{id}'"));
316    }
317    by_name.insert(id, idx);
318    by_path.insert(path.clone(), idx);
319
320    let mut children = Vec::new();
321    let mut regions: Vec<RegionDef> = Vec::new();
322    match kind {
323        StateKind::Composite => {
324            if node.initial.is_none() {
325                errors.push(format!("composite '{path}' requires an initial transition"));
326            }
327            if let Some(map) = &node.states {
328                for (cname, cnode) in map {
329                    let cpath = format!("{path}.{cname}");
330                    let cid = register(states, by_name, by_path, errors, cname.clone(), cpath, Some(idx), cnode);
331                    children.push(cid);
332                }
333            }
334        }
335        StateKind::Orthogonal => {
336            if let Some(regs) = &node.regions {
337                for (ri, region) in regs.iter().enumerate() {
338                    let mut rstates = Vec::new();
339                    for (cname, cnode) in &region.states {
340                        let cpath = format!("{path}.{cname}");
341                        let cid = register(states, by_name, by_path, errors, cname.clone(), cpath, Some(idx), cnode);
342                        states[cid].region_of = Some((idx, ri));
343                        rstates.push(cid);
344                    }
345                    regions.push(RegionDef {
346                        initial: InitialDef { target: idx, guard: None, action: Vec::new() },
347                        states: rstates,
348                    });
349                }
350            } else {
351                errors.push(format!("orthogonal '{path}' requires regions"));
352            }
353        }
354        _ => {}
355    }
356    states[idx].children = children;
357    states[idx].regions = regions;
358    idx
359}
360
361// ---------------------------------------------------------------------------
362// Phase 2: resolve transitions/initials/afters/choices now that names are complete.
363
364fn fill(
365    states: &mut Vec<StateDef>,
366    by_name: &HashMap<String, NodeId>,
367    errors: &mut Vec<String>,
368    path: &str,
369    node: &StateNode,
370) {
371    // find this node's id
372    let idx = by_name[path.rsplit('.').next().unwrap_or("top")];
373    let kind = states[idx].kind;
374
375    // composite initial
376    if kind == StateKind::Composite {
377        if let Some(init) = &node.initial {
378            let action = parse_actions_or_err(errors, path, "initial", init.action.as_ref());
379            match resolve_ref(by_name, &init.transition_to) {
380                Some(t) => states[idx].initial = Some(InitialDef {
381                    target: t,
382                    guard: init.guard.clone(),
383                    action,
384                }),
385                None => errors.push(format!(
386                    "initial of '{path}' references unknown state '{}'",
387                    init.transition_to
388                )),
389            }
390        }
391    }
392    // orthogonal region initials
393    if kind == StateKind::Orthogonal {
394        if let Some(regs) = &node.regions {
395            for (ri, region) in regs.iter().enumerate() {
396                let action = parse_actions_or_err(errors, path, "initial", region.initial.action.as_ref());
397                match resolve_ref(by_name, &region.initial.transition_to) {
398                    Some(t) => states[idx].regions[ri].initial = InitialDef {
399                        target: t,
400                        guard: region.initial.guard.clone(),
401                        action,
402                    },
403                    None => errors.push(format!(
404                        "region initial of '{path}' references unknown state '{}'",
405                        region.initial.transition_to
406                    )),
407                }
408            }
409        }
410    }
411
412    // on_events
413    if let Some(oe) = &node.on_events {
414        let mut map = BTreeMap::new();
415        for (ev, tol) in oe {
416            let list = match tol {
417                model::TransitionOrList::One(t) => vec![t.clone()],
418                model::TransitionOrList::Many(v) => v.clone(),
419            };
420            let defs = list
421                .iter()
422                .map(|t| {
423                    let raw_target = t.transition_to.clone().unwrap_or_default();
424                    let target = if raw_target.is_empty() {
425                        None
426                    } else {
427                        match resolve_ref(by_name, &raw_target) {
428                            Some(id) => Some(id),
429                            None => {
430                                errors.push(format!(
431                                    "transition '{ev}' in '{path}' references unknown state '{raw_target}'"
432                                ));
433                                None
434                            }
435                        }
436                    };
437                    TransitionDef {
438                        target,
439                        guard: t.guard.clone(),
440                        action: parse_actions_or_err(errors, path, "transition", t.action.as_ref()),
441                        internal: t.internal.unwrap_or(false),
442                        local: t.local.unwrap_or(false),
443                        raw_target,
444                    }
445                })
446                .collect();
447            map.insert(ev.clone(), defs);
448        }
449        states[idx].on_events = map;
450    }
451
452    // after
453    if let Some(after) = &node.after {
454        states[idx].after = after
455            .iter()
456            .map(|a| {
457                let duration_ms = match parse_duration(&a.duration) {
458                    Ok(d) => d,
459                    Err(e) => {
460                        errors.push(format!("after in '{path}': {e}"));
461                        0
462                    }
463                };
464                let target = a.transition_to.as_deref().and_then(|r| {
465                    resolve_ref(by_name, r).or_else(|| {
466                        errors.push(format!(
467                            "after in '{path}' references unknown state '{r}'"
468                        ));
469                        None
470                    })
471                });
472                AfterDef {
473                    duration_ms,
474                    target,
475                    guard: a.guard.clone(),
476                    action: parse_actions_or_err(errors, path, "after", a.action.as_ref()),
477                }
478            })
479            .collect();
480    }
481
482    // choice
483    if let Some(choice) = &node.choice {
484        states[idx].choice = Some(
485            choice
486                .iter()
487                .map(|br| {
488                    let target = match resolve_ref(by_name, &br.transition_to) {
489                        Some(id) => id,
490                        None => {
491                            errors.push(format!(
492                                "choice in '{path}' references unknown state '{}'",
493                                br.transition_to
494                            ));
495                            idx
496                        }
497                    };
498                    ChoiceBranchDef {
499                        target,
500                        guard: br.guard.clone(),
501                        action: parse_actions_or_err(errors, path, "choice", br.action.as_ref()),
502                    }
503                })
504                .collect(),
505        );
506    }
507
508    // recurse into children
509    match kind {
510        StateKind::Composite => {
511            if let Some(map) = &node.states {
512                for (cname, cnode) in map {
513                    fill(states, by_name, errors, &format!("{path}.{cname}"), cnode);
514                }
515            }
516        }
517        StateKind::Orthogonal => {
518            if let Some(regs) = &node.regions {
519                for region in regs {
520                    for (cname, cnode) in &region.states {
521                        fill(states, by_name, errors, &format!("{path}.{cname}"), cnode);
522                    }
523                }
524            }
525        }
526        _ => {}
527    }
528}
529
530fn parse_actions_or_err(
531    errors: &mut Vec<String>,
532    path: &str,
533    what: &str,
534    raw: Option<&serde_yaml::Value>,
535) -> Vec<Action> {
536    match model::parse_actions(raw) {
537        Ok(a) => a,
538        Err(e) => {
539            errors.push(format!("{what} in '{path}': {e}"));
540            Vec::new()
541        }
542    }
543}
544
545pub fn build(raw: &RawMachine) -> Result<Machine, Vec<String>> {
546    let mut states: Vec<StateDef> = Vec::new();
547    let mut by_name: HashMap<String, NodeId> = HashMap::new();
548    let mut by_path: HashMap<String, NodeId> = HashMap::new();
549    let mut errors: Vec<String> = Vec::new();
550
551    register(
552        &mut states,
553        &mut by_name,
554        &mut by_path,
555        &mut errors,
556        "top".to_string(),
557        "top".to_string(),
558        None,
559        &raw.top,
560    );
561
562    fill(&mut states, &by_name, &mut errors, "top", &raw.top);
563
564    if !errors.is_empty() {
565        return Err(errors);
566    }
567
568    let mut events = BTreeMap::new();
569    if let Some(ev) = &raw.events {
570        for (k, v) in ev {
571            let scope = Scope::parse(v.scope.as_deref());
572            let payload = v
573                .payload
574                .as_ref()
575                .map(|m| {
576                    m.iter()
577                        .map(|(name, pf)| {
578                            (
579                                name.clone(),
580                                PayloadField {
581                                    ty: pf.ty.clone(),
582                                    required: pf.required.unwrap_or(false),
583                                    default: pf.default.as_ref().map(Value::from_yaml),
584                                },
585                            )
586                        })
587                        .collect()
588                })
589                .unwrap_or_default();
590            events.insert(k.clone(), EventDecl { scope, payload });
591        }
592    }
593
594    let migrations = raw
595        .migrations
596        .as_ref()
597        .map(|ms| {
598            ms.iter()
599                .map(|m| MigrationDef {
600                    from: m.from,
601                    to: m.to,
602                    when: m.when.clone(),
603                    state_map: m.state_map.clone().unwrap_or_default(),
604                    esvs: model::parse_actions(m.esvs.as_ref()).unwrap_or_default(),
605                })
606                .collect()
607        })
608        .unwrap_or_default();
609
610    Ok(Machine {
611        id: raw.id.clone(),
612        version: raw.version.unwrap_or(1),
613        format: raw.format.unwrap_or(1),
614        contracts: raw.contracts.clone(),
615        subscribe: raw.subscribe.clone(),
616        events,
617        migrations,
618        states,
619        by_name,
620        by_path,
621        top: 0,
622        meta: raw
623            .meta
624            .as_ref()
625            .map(|v| Value::from_yaml(v))
626            .unwrap_or(Value::Map(BTreeMap::new())),
627    })
628}
629
630/// Build a set of definitions, inlining `submachine:` references (SPEC §5.6.1)
631/// using a registry of all docs (def id -> raw machine). Returns one resolved
632/// [`Machine`] per input doc, in order, or the collected errors.
633pub fn resolve_definitions(
634    docs: &[RawMachine],
635) -> Result<Vec<Machine>, Vec<crate::loader::LoadError>> {
636    use crate::loader::LoadError;
637    use std::collections::{HashMap, HashSet};
638
639    let registry: HashMap<String, crate::model::StateNode> = docs
640        .iter()
641        .map(|d| (d.id.clone(), d.top.clone()))
642        .collect();
643    let mut out = Vec::new();
644    let mut errs: Vec<LoadError> = Vec::new();
645    for d in docs {
646        let inlined_top = match crate::model::inline_submachines(&d.top, &registry, &HashSet::new()) {
647            Ok(t) => t,
648            Err(e) => {
649                errs.extend(e);
650                continue;
651            }
652        };
653        let mut inlined = d.clone();
654        inlined.top = inlined_top;
655        match build(&inlined) {
656            Ok(m) => out.push(m),
657            Err(es) => {
658                for e in es {
659                    errs.push(LoadError {
660                        path: format!("machine '{}'", d.id),
661                        message: e,
662                    });
663                }
664            }
665        }
666    }
667    if !errs.is_empty() {
668        return Err(errs);
669    }
670    Ok(out)
671}