1use 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 pub is_sm_boundary: bool,
127 pub sm_with: Vec<(String, String)>,
128 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 pub meta: Value,
165}
166
167impl Machine {
168 pub fn get(&self, n: NodeId) -> &StateDef {
169 &self.states[n]
170 }
171
172 pub fn resolve(&self, raw: &str) -> Option<NodeId> {
174 resolve_ref(&self.by_name, raw)
175 }
176}
177
178pub 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#[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 ®ion.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
361fn 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 let idx = by_name[path.rsplit('.').next().unwrap_or("top")];
373 let kind = states[idx].kind;
374
375 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 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, ®ion.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 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 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 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 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 ®ion.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
630pub 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, ®istry, &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}