Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/scenario_activation.rs
T

493 lines
17 KiB
Rust

//! Derived call geometry. Correlation and branch ownership come from the model;
//! columns separate overlapping drawings and do not assert an execution thread.
use super::*;
const WIDTH: f32 = 12.;
const OFFSET: f32 = 6.;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Finish {
Reply(Id),
Boundary,
/// Still open at this scope boundary; the visible span is not an isolated stub.
Continuation,
Open,
/// The receiver was deleted without answering this call.
Terminated(Id),
/// Lifetime clipping ended this visible piece before its logical terminal.
Clipped,
}
#[derive(Clone, Debug, PartialEq)]
pub(super) struct Segment {
pub call: Id,
pub receiver: Id,
pub scope: Id,
pub rect: Rectangle,
pub column: usize,
pub finish: Finish,
}
impl Segment {
fn bottom(&self) -> f32 {
self.rect.y + self.rect.height
}
fn overlaps(&self, other: &Self) -> bool {
self.receiver == other.receiver
&& self.scope == other.scope
&& self.rect.y < other.bottom()
&& other.rect.y < self.bottom()
}
fn edge(&self, right: bool) -> f32 {
self.rect.x + if right { self.rect.width } else { 0. }
}
}
impl Scene {
pub(super) fn layout_activations(&mut self, p: &Project, lanes: &BTreeMap<Id, f32>) {
// Logical endpoints precede clipping and remain available even when a
// lifetime hides a whole execution or terminates it before its Reply.
let correlations = logical_correlations(p, &self.rows, &self.lines);
let mut calls: Vec<_> = self
.rows
.iter()
.filter_map(|(&call, &(scope, y))| {
let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind: MessageKind::Call,
receiver: Some(receiver),
..
},
..
}) = p.elements.get(&call).and_then(|e| e.shared())
else {
return None;
};
Some((call, *receiver, scope, y, *lanes.get(receiver)?))
})
.collect();
// Rows are laid out in semantic order. UUID order must not choose which
// rectangle obscures another (especially after source reidentification).
calls.sort_by(|a, b| a.3.total_cmp(&b.3).then_with(|| a.0.cmp(&b.0)));
for &(call, receiver, scope, _, x) in &calls {
let first = self.activations.len();
let first_cue = self.open_calls.len();
self.activation_sequence(p, scope, call, receiver, x, None);
let column = self.activations[first..]
.iter()
.flat_map(|segment| {
self.activations[..first]
.iter()
.filter(move |other| segment.overlaps(other))
})
.map(|other| other.column + 1)
.max()
.unwrap_or(0);
for segment in &mut self.activations[first..] {
segment.column = column;
segment.rect.x += column as f32 * OFFSET;
}
for cue in &mut self.open_calls[first_cue..] {
cue.x += column as f32 * OFFSET;
}
}
// Shallow executions paint first; columns cannot collide within a scope.
self.activations.sort_by(|a, b| {
a.column
.cmp(&b.column)
.then_with(|| a.rect.y.total_cmp(&b.rect.y))
.then_with(|| a.call.cmp(&b.call))
});
self.anchor_activation_messages(p, lanes, &correlations);
}
fn activation_segment(
&mut self,
call: Id,
receiver: Id,
scope: Id,
x: f32,
span: std::ops::Range<f32>,
finish: Finish,
) {
// Cues describe unanswered logical paths, including wholly invisible
// ones. Never derive them only from the surviving paint rectangles.
if matches!(
finish,
Finish::Open | Finish::Continuation | Finish::Terminated(_)
) {
self.open_calls
.push(Point::new(x + WIDTH / 2. + 4., span.end - 7.));
}
for alive in self
.lifecycle
.alive_spans(receiver, scope, span.start, span.end)
{
self.activations.push(Segment {
call,
receiver,
scope,
rect: Rectangle::new(
Point::new(x - WIDTH / 2., alive.start),
Size::new(WIDTH, alive.end - alive.start),
),
column: 0,
finish: if alive.end == span.end {
finish
} else {
Finish::Clipped
},
});
}
}
/// Follow one exact correlation. A segment belongs to one ordered scope;
/// vertically stacked operands must not become a single chronological span.
fn activation_sequence(
&mut self,
p: &Project,
owner: Id,
call: Id,
receiver: Id,
x: f32,
mut start: Option<f32>,
) -> Outcome {
let entered_open = start.is_some();
let mut split = false;
let mut may_be_terminated = false;
for id in p.elements[&owner].steps().into_iter().flatten() {
let Some(&(_, y)) = self.rows.get(id) else {
continue;
};
if *id == call {
// A reflective call is received at the lower end of its loop.
start = Some(
self.lines
.iter()
.find(|line| line.id == call)
.and_then(|line| line.points.last())
.map_or(y, |point| point.y),
);
}
let Some(top) = start else {
continue;
};
match p.elements[id].shared() {
Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind: MessageKind::Delete,
receiver: Some(deleted),
..
},
..
}) if *deleted == receiver => {
self.activation_segment(
call,
receiver,
owner,
x,
top..y,
Finish::Terminated(*id),
);
start = None;
may_be_terminated = true;
}
Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind: MessageKind::Reply,
reply_to: Some(correlation),
..
},
..
}) if *correlation == call => {
// Execution ends when its reply is sent, including a self reply.
self.activation_segment(call, receiver, owner, x, top..y, Finish::Reply(*id));
start = None;
}
Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { operator, branches },
..
}) => {
split = true;
self.activation_segment(call, receiver, owner, x, top..y, Finish::Boundary);
let bottom = self
.boxes
.iter()
.find(|b| b.id == *id && !b.title.is_empty())
.map(|b| b.rect.y + b.rect.height)
.unwrap_or(y);
let outcomes: Vec<_> = branches
.iter()
.filter_map(|branch| {
let &(top, _) = self.sequence_bounds.get(branch)?;
Some(self.activation_sequence(p, *branch, call, receiver, x, Some(top)))
})
.collect();
let open = match operator {
FragmentOperator::Alternative => {
outcomes.is_empty() || outcomes.iter().any(|outcome| outcome.open)
}
FragmentOperator::Parallel => outcomes.iter().all(|outcome| outcome.open),
FragmentOperator::Optional | FragmentOperator::Loop => true,
};
if outcomes.iter().any(|outcome| outcome.may_be_terminated) {
// Sibling bars retain their own entry paths, but a
// conditional death cannot become a definite parent
// execution again after a later Create.
self.open_calls
.push(Point::new(x + WIDTH / 2. + 4., bottom - 7.));
may_be_terminated = true;
start = None;
} else {
start = open.then_some(bottom);
}
}
_ => {}
}
}
if let Some(top) = start {
let bottom = if entered_open || split {
self.sequence_bounds
.get(&owner)
.map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.))
} else {
top + 26.
};
let finish = if entered_open || split {
Finish::Continuation
} else {
Finish::Open
};
self.activation_segment(call, receiver, owner, x, top..bottom, finish);
}
Outcome {
open: start.is_some(),
may_be_terminated,
}
}
fn anchor_activation_messages(
&mut self,
p: &Project,
lanes: &BTreeMap<Id, f32>,
correlations: &BTreeMap<Id, Correlation>,
) {
let segments = &self.activations;
for line in &mut self.lines {
let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
sender: Some(sender),
receiver: Some(receiver),
message_kind,
reply_to,
..
},
..
}) = p.elements.get(&line.id).and_then(|e| e.shared())
else {
continue;
};
if !matches!(
message_kind,
MessageKind::Call | MessageKind::Reply | MessageKind::Asynchronous
) {
continue;
}
let (Some(&from), Some(&to), Some(&(scope, _))) = (
lanes.get(sender),
lanes.get(receiver),
self.rows.get(&line.id),
) else {
continue;
};
let send_y = line.points[0].y;
let receive_y = line.points.last().unwrap().y;
let reflective = sender == receiver;
let source = if *message_kind == MessageKind::Reply {
reply_to.and_then(|call| {
segments.iter().find(|a| {
a.call == call
&& a.scope == scope
&& a.finish == Finish::Reply(line.id)
&& a.bottom() == send_y
})
})
} else {
active_segment(
p,
segments,
correlations,
*sender,
scope,
send_y,
Some(line.id),
)
};
let target = if *message_kind == MessageKind::Call {
segments
.iter()
.find(|a| a.call == line.id && a.scope == scope && a.rect.y == receive_y)
} else {
active_segment(
p,
segments,
correlations,
*receiver,
scope,
receive_y,
*reply_to,
)
};
let start = source.map_or(from, |a| a.edge(reflective || to > from));
let end = target.map_or(to, |a| a.edge(reflective || from > to));
if reflective {
let outside = start.max(end) + 60.;
line.points = vec![
Point::new(start, send_y),
Point::new(outside, send_y),
Point::new(outside, receive_y),
Point::new(end, receive_y),
];
} else {
line.points = vec![Point::new(start, send_y), Point::new(end, receive_y)];
}
line.label_at.x = start.min(end) + 12.;
}
}
}
struct Outcome {
open: bool,
may_be_terminated: bool,
}
fn logical_correlations(
p: &Project,
rows: &BTreeMap<Id, (Id, f32)>,
lines: &[Line],
) -> BTreeMap<Id, Correlation> {
let mut result = BTreeMap::new();
let points: BTreeMap<_, _> = lines.iter().map(|line| (line.id, &line.points)).collect();
for (&id, &(_, y)) in rows {
if matches!(
p.elements.get(&id).and_then(|e| e.shared()),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
message_kind: MessageKind::Call,
..
},
..
})
) {
result.insert(
id,
Correlation {
start: points.get(&id).and_then(|p| p.last()).map_or(y, |p| p.y),
replies: vec![],
},
);
}
}
for (&id, &(scope, y)) in rows {
if let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind: MessageKind::Reply,
reply_to: Some(call),
..
},
..
}) = p.elements.get(&id).and_then(|e| e.shared())
&& let Some(correlation) = result.get_mut(call)
{
let sent = points.get(&id).and_then(|p| p.first()).map_or(y, |p| p.y);
correlation.replies.push((scope, sent));
}
}
result
}
/// A caller has no implicit execution just because it sent a message. Only an
/// actually drawn, known-duration execution can supply its anchor. Crossed
/// intervals provide no unambiguous innermost execution, so fall back to the lane.
fn active_segment<'a>(
p: &Project,
segments: &'a [Segment],
correlations: &BTreeMap<Id, Correlation>,
receiver: Id,
scope: Id,
y: f32,
exclude: Option<Id>,
) -> Option<&'a Segment> {
let candidates: Vec<_> = segments
.iter()
.filter(|a| {
a.receiver == receiver
&& a.scope == scope
&& Some(a.call) != exclude
&& a.rect.y <= y
&& y < a.bottom()
&& a.finish != Finish::Open
})
.collect();
let inner = candidates.iter().max_by_key(|a| a.column).copied()?;
candidates
.iter()
.all(|a| {
a.call == inner.call
|| correlations[&a.call].contains(p, scope, &correlations[&inner.call])
})
.then_some(inner)
}
struct Correlation {
start: f32,
replies: Vec<(Id, f32)>,
}
impl Correlation {
fn contains(&self, p: &Project, scope: Id, inner: &Self) -> bool {
// Segment clipping at a fragment can make crossed calls appear nested.
// Compare actual correlated replies on compatible scope paths instead.
let relevant = |&(reply_scope, _): &(Id, f32)| {
ancestor(p, scope, reply_scope) || ancestor(p, reply_scope, scope)
};
let mut compared = false;
for &(inner_scope, inner_end) in inner.replies.iter().filter(|reply| relevant(reply)) {
let mut matched = false;
for &(outer_scope, outer_end) in self.replies.iter().filter(|reply| relevant(reply)) {
if ancestor(p, inner_scope, outer_scope) || ancestor(p, outer_scope, inner_scope) {
// Alternative operands are not chronological. Compare only
// returns that can belong to the same enclosing branch path.
if outer_end < inner_end {
return false;
}
matched = true;
}
}
if !matched {
return false;
}
compared = true;
}
self.start <= inner.start && compared
}
}
fn ancestor(p: &Project, expected: Id, mut scope: Id) -> bool {
for _ in 0..=p.elements.len() {
if scope == expected {
return true;
}
let Some(parent) = p.elements.get(&scope).and_then(|e| e.parent) else {
return false;
};
scope = parent;
}
false
}
#[cfg(test)]
mod tests;