286 lines
9.1 KiB
Rust
286 lines
9.1 KiB
Rust
//! 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<Id, (Id, f32)>,
|
|
pub scopes: &'a BTreeMap<Id, (f32, f32)>,
|
|
}
|
|
|
|
#[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<Interval>,
|
|
pub markers: Vec<Marker>,
|
|
pub uncertainties: Vec<Uncertainty>,
|
|
}
|
|
|
|
/// 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<LaneLifetime> {
|
|
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<Id>,
|
|
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<MarkerKind> {
|
|
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<MarkerKind>) -> 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<Span> {
|
|
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;
|