//! 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) { // 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, 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, ) -> 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, correlations: &BTreeMap, ) { 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, lines: &[Line], ) -> BTreeMap { 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, receiver: Id, scope: Id, y: f32, exclude: Option, ) -> 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;