//! Read-only lifecycle geometry. Operand screen order is never execution order. use oa_core::{FragmentOperator, Id, Kind, MessageKind, OccurrenceBody, Project, SharedBody}; use std::collections::{BTreeMap, BTreeSet}; pub(super) mod render; /// Maps already recorded by the sequence renderer. Rows carry their owning sequence. pub(crate) struct Geometry<'a> { pub rows: &'a BTreeMap, pub scopes: &'a BTreeMap, } #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct Span { pub start: f32, pub end: f32, } impl Span { fn valid(self) -> bool { self.start.is_finite() && self.end.is_finite() && self.start <= self.end } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Life { Alive, Absent, Unresolved, } #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct Interval { pub scope: Id, pub span: Span, pub life: Life, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum MarkerKind { Birth, Death, } #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct Marker { pub occurrence: Id, pub scope: Id, pub y: f32, pub kind: MarkerKind, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum UncertaintyReason { Conditional, Repeated, Parallel, MissingGeometry, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct Uncertainty { pub scope: Id, pub reason: UncertaintyReason, } #[derive(Debug, Clone, PartialEq)] pub(crate) struct LaneLifetime { pub lifeline: Id, pub intervals: Vec, pub markers: Vec, pub uncertainties: Vec, } /// Stable lane-ID order; source/model/history and caller-owned geometry are untouched. /// An opaque ScenarioUse is a local use, not a caller birth/death contract. pub(crate) fn project_lifetimes( project: &Project, scenario: Id, geometry: &Geometry<'_>, ) -> Vec { if project .elements .get(&scenario) .is_none_or(|e| e.kind() != Kind::Scenario) { return vec![]; } project .elements .values() .filter(|e| e.kind() == Kind::Lifeline && e.parent == Some(scenario)) .map(|e| { let mut projection = Projection { project, geometry, lane: e.id, visiting: BTreeSet::new(), result: LaneLifetime { lifeline: e.id, intervals: vec![], markers: vec![], uncertainties: vec![], }, }; let initial = if projection.contains(scenario, Some(MarkerKind::Birth)) { Life::Unresolved } else { Life::Alive }; projection.sequence(scenario, initial); projection.result }) .collect() } struct Projection<'a, 'g> { project: &'a Project, geometry: &'a Geometry<'g>, lane: Id, visiting: BTreeSet, result: LaneLifetime, } impl Projection<'_, '_> { fn body(&self, id: Id) -> Option<&OccurrenceBody> { match self.project.elements.get(&id)?.shared()? { SharedBody::Occurrence { body, .. } => Some(body), _ => None, } } fn event(&self, id: Id) -> Option { match self.body(id)? { OccurrenceBody::Exchange { receiver: Some(line), message_kind, .. } if *line == self.lane => match message_kind { MessageKind::Create => Some(MarkerKind::Birth), MessageKind::Delete => Some(MarkerKind::Death), _ => None, }, _ => None, } } fn contains(&self, scope: Id, kind: Option) -> bool { // Local structural walk only: no guards, iteration simulation or reuse expansion. let mut pending = vec![scope]; let mut seen = BTreeSet::new(); while let Some(id) = pending.pop() { if !seen.insert(id) { continue; } if self .event(id) .is_some_and(|event| kind.is_none_or(|k| event == k)) { return true; } if let Some(steps) = self.project.elements.get(&id).and_then(|e| e.steps()) { pending.extend(steps); } if let Some(OccurrenceBody::Fragment { branches, .. }) = self.body(id) { pending.extend(branches); } } false } fn span(&self, scope: Id) -> Option { let &(start, end) = self.geometry.scopes.get(&scope)?; let span = Span { start, end }; span.valid().then_some(span) } fn uncertain(&mut self, scope: Id, reason: UncertaintyReason) { let value = Uncertainty { scope, reason }; if !self.result.uncertainties.contains(&value) { self.result.uncertainties.push(value); } } fn interval(&mut self, scope: Id, start: f32, end: f32, life: Life) { if start < end && start.is_finite() && end.is_finite() { self.result.intervals.push(Interval { scope, span: Span { start, end }, life, }); } } fn sequence(&mut self, scope: Id, entry: Life) -> Life { let Some(span) = self.span(scope) else { self.uncertain(scope, UncertaintyReason::MissingGeometry); return Life::Unresolved; }; if !self.visiting.insert(scope) { self.uncertain(scope, UncertaintyReason::MissingGeometry); return Life::Unresolved; } let steps = self .project .elements .get(&scope) .and_then(|e| e.steps()) .cloned() .unwrap_or_default(); // A creation directly in this sequence fixes its local birth boundary. // A creation only in a nested operand cannot fix its ancestor's entry. let mut life = if steps .iter() .any(|&id| self.event(id) == Some(MarkerKind::Birth)) { Life::Absent } else { entry }; let mut cursor = span.start; for id in steps { let event = self.event(id); let fragment = match self.body(id) { Some(OccurrenceBody::Fragment { operator, branches }) => { Some((*operator, branches.clone())) } _ => None, }; if event.is_none() && fragment.is_none() { continue; } let y = self .geometry .rows .get(&id) .filter(|(owner, y)| { *owner == scope && y.is_finite() && *y >= cursor && *y <= span.end }) .map(|(_, y)| *y); let Some(y) = y else { self.uncertain(id, UncertaintyReason::MissingGeometry); life = Life::Unresolved; continue; }; self.interval(scope, cursor, y, life); cursor = y; if let Some(kind) = event { self.result.markers.push(Marker { occurrence: id, scope, y, kind, }); life = match kind { MarkerKind::Birth => Life::Alive, MarkerKind::Death => Life::Absent, }; } if let Some((operator, branches)) = fragment { let entry = life; let mut missing = false; for branch in branches { let bounds = self .span(branch) .filter(|s| s.start >= cursor && s.end <= span.end); if let Some(bounds) = bounds { // Every sibling inherits the same entry, never its displayed predecessor's exit. self.sequence(branch, entry); cursor = bounds.end; } else { self.uncertain(branch, UncertaintyReason::MissingGeometry); missing = true; } } if self.contains(id, None) { let reason = match operator { FragmentOperator::Alternative | FragmentOperator::Optional => { UncertaintyReason::Conditional } FragmentOperator::Loop => UncertaintyReason::Repeated, FragmentOperator::Parallel => UncertaintyReason::Parallel, }; self.uncertain(id, reason); life = Life::Unresolved; } else if missing { life = Life::Unresolved; } } } self.interval(scope, cursor, span.end, life); self.visiting.remove(&scope); life } } #[cfg(test)] #[path = "scenario_lifecycle/tests.rs"] mod tests;