Reuse published lifecycle bridge and renderer checks from peer checkpoint 7aa007b. Preserve logical correlation independently from clipped paint spans, terminate executions on receiver death, and retain conservative sibling uncertainty without resurrection.
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;
|