493 lines
17 KiB
Rust
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;
|