653 lines
24 KiB
Rust
653 lines
24 KiB
Rust
use crate::*;
|
|
use std::collections::BTreeSet;
|
|
|
|
/// Every live reference in a typed element, including structural ownership.
|
|
pub fn element_references(e: &Element) -> Vec<Id> {
|
|
let mut copy = e.clone();
|
|
let mut refs = Vec::new();
|
|
visit_element_refs(&mut copy, |id| refs.push(*id));
|
|
refs
|
|
}
|
|
pub fn relation_references(r: &Relation) -> Vec<Id> {
|
|
let mut copy = r.clone();
|
|
let mut refs = Vec::new();
|
|
visit_relation_refs(&mut copy, |id| refs.push(*id));
|
|
refs
|
|
}
|
|
pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
|
if let Some(parent) = &mut e.parent {
|
|
visit(parent);
|
|
}
|
|
match &mut e.body {
|
|
ElementBody::Oa(OaBody::Process { steps })
|
|
| ElementBody::Sa(SaBody::FunctionalChain { steps }) => {
|
|
for id in steps {
|
|
visit(id);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(shared) = e.shared_mut() {
|
|
match shared {
|
|
SharedBody::ExchangeCategory { exchanges, .. } => {
|
|
for id in exchanges {
|
|
visit(id);
|
|
}
|
|
}
|
|
SharedBody::DataField {
|
|
type_ref: TypeRef::Named(id),
|
|
..
|
|
} => visit(id),
|
|
SharedBody::Interface { supertype }
|
|
| SharedBody::ExchangeItem { supertype, .. }
|
|
| SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype },
|
|
..
|
|
} => {
|
|
if let Some(id) = supertype {
|
|
visit(id);
|
|
}
|
|
}
|
|
SharedBody::Event {
|
|
body: EventBody::Signal { items },
|
|
} => {
|
|
for id in items {
|
|
visit(id);
|
|
}
|
|
}
|
|
SharedBody::Port { contract, legacy } => {
|
|
match contract {
|
|
PortContract::FunctionInput { incoming_items } => {
|
|
incoming_items.iter_mut().for_each(&mut visit)
|
|
}
|
|
PortContract::FunctionOutput { outgoing_items } => {
|
|
outgoing_items.iter_mut().for_each(&mut visit)
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(id) = legacy.as_mut().and_then(|old| old.interface.as_mut()) {
|
|
visit(id);
|
|
}
|
|
}
|
|
SharedBody::Scenario { context, steps, .. } => {
|
|
if let Some(id) = context {
|
|
visit(id);
|
|
}
|
|
for id in steps {
|
|
visit(id);
|
|
}
|
|
}
|
|
SharedBody::Lifeline {
|
|
represented: Some(id),
|
|
} => visit(id),
|
|
SharedBody::Occurrence { body, .. } => match body {
|
|
OccurrenceBody::Exchange {
|
|
exchange,
|
|
sender,
|
|
receiver,
|
|
reply_to,
|
|
values,
|
|
operation,
|
|
..
|
|
} => {
|
|
if let Some(id) = operation
|
|
.as_mut()
|
|
.and_then(|operation| operation.allocation.as_mut())
|
|
{
|
|
visit(id);
|
|
}
|
|
for id in [exchange, sender, receiver, reply_to].into_iter().flatten() {
|
|
visit(id);
|
|
}
|
|
for value in values {
|
|
for id in &mut value.path {
|
|
visit(id);
|
|
}
|
|
visit(&mut value.field);
|
|
}
|
|
}
|
|
OccurrenceBody::Action { action, lifeline } => {
|
|
for id in [action, lifeline].into_iter().flatten() {
|
|
visit(id);
|
|
}
|
|
}
|
|
OccurrenceBody::SequenceReference { sequence } => {
|
|
if let Some(id) = sequence {
|
|
visit(id);
|
|
}
|
|
}
|
|
OccurrenceBody::Control { .. } => {}
|
|
OccurrenceBody::ScenarioUse { scenario, bindings } => {
|
|
if let Some(id) = scenario {
|
|
visit(id);
|
|
}
|
|
for binding in bindings {
|
|
visit(&mut binding.referenced);
|
|
visit(&mut binding.local);
|
|
}
|
|
}
|
|
OccurrenceBody::State { state, lifeline } => {
|
|
for id in [state, lifeline].into_iter().flatten() {
|
|
visit(id);
|
|
}
|
|
}
|
|
OccurrenceBody::Fragment { branches, .. } => {
|
|
for id in branches {
|
|
visit(id);
|
|
}
|
|
}
|
|
OccurrenceBody::Branch { occurrences, .. } => {
|
|
for id in occurrences {
|
|
visit(id);
|
|
}
|
|
}
|
|
},
|
|
SharedBody::Constraint { constrained, .. } => {
|
|
for id in constrained {
|
|
visit(id);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
pub fn visit_relation_refs(r: &mut Relation, mut visit: impl FnMut(&mut Id)) {
|
|
let (mut source, mut target) = r.body.endpoints();
|
|
visit(&mut source);
|
|
visit(&mut target);
|
|
r.body.set_endpoints(source, target);
|
|
match &mut r.body {
|
|
RelationBody::ContextualLink {
|
|
owner,
|
|
source,
|
|
target,
|
|
purpose,
|
|
} => {
|
|
visit(owner);
|
|
for id in source.references.iter_mut().chain(&mut target.references) {
|
|
visit(id);
|
|
}
|
|
match purpose {
|
|
ContextualLinkPurpose::Exchange { exchange } => visit(exchange),
|
|
ContextualLinkPurpose::CommonWork { work } => visit(work),
|
|
ContextualLinkPurpose::Precedence { .. } => {}
|
|
}
|
|
}
|
|
RelationBody::StateTransition {
|
|
triggers, effects, ..
|
|
} => {
|
|
for id in triggers.iter_mut().chain(effects) {
|
|
visit(id);
|
|
}
|
|
}
|
|
RelationBody::Interaction { items, .. }
|
|
| RelationBody::BoundaryExchange { items, .. }
|
|
| RelationBody::CommunicationMean { items, .. } => {
|
|
for id in items {
|
|
visit(id);
|
|
}
|
|
}
|
|
RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
items,
|
|
..
|
|
} => {
|
|
for id in [source_port, target_port] {
|
|
visit(id);
|
|
}
|
|
for id in items {
|
|
visit(id);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
/// Every semantic/presentation reference in a diagram, excluding its own identities.
|
|
pub fn diagram_references(d: &Diagram) -> Vec<Id> {
|
|
let mut copy = d.clone();
|
|
let mut refs = Vec::new();
|
|
visit_diagram_refs(&mut copy, |id| refs.push(*id));
|
|
refs
|
|
}
|
|
|
|
pub fn visit_diagram_refs(d: &mut Diagram, mut visit: impl FnMut(&mut Id)) {
|
|
d.view_options.visit_references(&mut visit);
|
|
match &mut d.projection {
|
|
ProjectionKind::StateMachine { machine } => visit(machine),
|
|
ProjectionKind::Scenario { scenario } => visit(scenario),
|
|
_ => {}
|
|
}
|
|
if let Some(layout) = &mut d.layout {
|
|
layout.visit_references(&mut visit);
|
|
}
|
|
for route in &mut d.routes {
|
|
visit(&mut route.edge);
|
|
}
|
|
for node in d.nodes.values_mut() {
|
|
for anchor in &mut node.port_anchors {
|
|
visit(&mut anchor.port);
|
|
}
|
|
visit(&mut node.element);
|
|
}
|
|
}
|
|
|
|
/// Preflight reference-key collisions before the caller changes object identities.
|
|
/// Fresh targets are allowed. This is not whole-project admission: unrelated
|
|
/// invalid records remain repairable within a transaction. Future typed reference
|
|
/// domains must compose their own preflight here before any remap mutation.
|
|
pub fn check_remap(p: &Project, old: Id, new: Id) -> Result<(), ModelError> {
|
|
if old == new {
|
|
return Ok(());
|
|
}
|
|
let remap = |id: Id| if id == old { new } else { id };
|
|
for diagram in p.diagrams.values() {
|
|
let Some(DiagramLayout::Scenario(layout)) = &diagram.layout else {
|
|
continue;
|
|
};
|
|
let mut lanes = std::collections::BTreeMap::new();
|
|
for lane in &layout.lanes {
|
|
let affected = lane.lifeline == old;
|
|
if let Some(previous_affected) = lanes.insert(remap(lane.lifeline), affected)
|
|
&& (affected || previous_affected)
|
|
{
|
|
return Err(ModelError(
|
|
"Identity remap would collide with a Scenario lane position.".into(),
|
|
));
|
|
}
|
|
}
|
|
let mut messages = std::collections::BTreeMap::new();
|
|
for message in &layout.messages {
|
|
let mut address = message.address.clone();
|
|
let affected = address.occurrence == old || address.references.contains(&old);
|
|
address.occurrence = remap(address.occurrence);
|
|
for reference in &mut address.references {
|
|
*reference = remap(*reference);
|
|
}
|
|
if let Some(previous_affected) = messages.insert(address, affected)
|
|
&& (affected || previous_affected)
|
|
{
|
|
return Err(ModelError(
|
|
"Identity remap would collide with a Scenario message position.".into(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remap_references(p: &mut Project, old: Id, new: Id) -> Result<(), ModelError> {
|
|
check_remap(p, old, new)?;
|
|
for e in p.elements.values_mut() {
|
|
visit_element_refs(e, |id| {
|
|
if *id == old {
|
|
*id = new;
|
|
}
|
|
});
|
|
}
|
|
for r in p.relations.values_mut() {
|
|
visit_relation_refs(r, |id| {
|
|
if *id == old {
|
|
*id = new;
|
|
}
|
|
});
|
|
}
|
|
for d in p.diagrams.values_mut() {
|
|
visit_diagram_refs(d, |id| {
|
|
if *id == old {
|
|
*id = new;
|
|
}
|
|
});
|
|
}
|
|
for a in p.artifact_links.values_mut() {
|
|
if a.owner == old {
|
|
a.owner = new;
|
|
}
|
|
}
|
|
for t in p.traces.values_mut() {
|
|
if t.oa == old {
|
|
t.oa = new;
|
|
}
|
|
if t.sa == old {
|
|
t.sa = new;
|
|
}
|
|
if let Some(baseline) = t.baseline.as_mut().and_then(|b| b.semantic.as_mut()) {
|
|
crate::semantic::visit_semantic_references(baseline, |id| {
|
|
if *id == old {
|
|
*id = new;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
/// IDs removed by a cascade. Cross-layer traced counterparts are never included.
|
|
pub fn deletion_impact(p: &Project, id: Id) -> BTreeSet<Id> {
|
|
deletion_impact_inner(p, id, false)
|
|
}
|
|
fn deletion_impact_inner(p: &Project, id: Id, defer_exchange_allocations: bool) -> BTreeSet<Id> {
|
|
let mut removed = BTreeSet::from([id]);
|
|
loop {
|
|
let before = removed.len();
|
|
for e in p.elements.values() {
|
|
let dependent_type = matches!(e.shared(),Some(SharedBody::DataField{type_ref:TypeRef::Named(t),..}) if removed.contains(t));
|
|
if e.parent.is_some_and(|parent| removed.contains(&parent)) || dependent_type {
|
|
removed.insert(e.id);
|
|
}
|
|
if matches!(e.shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::Exchange{exchange:Some(r),..},..}) if removed.contains(r))
|
|
{
|
|
removed.insert(e.id);
|
|
}
|
|
}
|
|
for r in p.relations.values() {
|
|
let port_removed = matches!(&r.body, RelationBody::FunctionalExchange { source_port, target_port, .. } if removed.contains(source_port) || removed.contains(target_port));
|
|
let work_allocation_removed = r.kind() == RelationKind::PortAllocation
|
|
&& p.relations.values().any(|allocation| {
|
|
allocation.kind() == RelationKind::Allocation
|
|
&& removed.contains(&allocation.id)
|
|
&& p.elements.get(&r.source()).and_then(|e| e.parent)
|
|
== Some(allocation.source())
|
|
&& p.elements.get(&r.target()).and_then(|e| e.parent)
|
|
== Some(allocation.target())
|
|
});
|
|
if removed.contains(&r.source())
|
|
|| removed.contains(&r.target())
|
|
|| (r.kind() == RelationKind::ContextualLink
|
|
&& relation_references(r).iter().any(|id| removed.contains(id)))
|
|
|| port_removed
|
|
|| work_allocation_removed
|
|
{
|
|
removed.insert(r.id);
|
|
}
|
|
}
|
|
// Exchange allocation depends on both explicit endpoint allocations.
|
|
for r in p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::ExchangeAllocation)
|
|
{
|
|
let (Some(boundary), Some(work)) =
|
|
(p.relations.get(&r.source()), p.relations.get(&r.target()))
|
|
else {
|
|
continue;
|
|
};
|
|
let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = &work.body
|
|
else {
|
|
continue;
|
|
};
|
|
if !defer_exchange_allocations
|
|
&& p.relations.values().any(|allocation| {
|
|
allocation.kind() == RelationKind::PortAllocation
|
|
&& removed.contains(&allocation.id)
|
|
&& [boundary.source(), boundary.target()].contains(&allocation.source())
|
|
&& [*source_port, *target_port].contains(&allocation.target())
|
|
})
|
|
&& crate::ports::missing_exchange_port_allocations(p, r, &removed)
|
|
{
|
|
removed.insert(r.id);
|
|
}
|
|
}
|
|
for d in p.diagrams.values() {
|
|
if d.projection
|
|
.state_machine()
|
|
.or_else(|| d.projection.scenario())
|
|
.is_some_and(|owner| removed.contains(&owner))
|
|
{
|
|
removed.insert(d.id);
|
|
}
|
|
for n in d.nodes.values() {
|
|
if removed.contains(&d.id) || removed.contains(&n.element) {
|
|
removed.insert(n.id);
|
|
}
|
|
}
|
|
}
|
|
for t in p.traces.values() {
|
|
if removed.contains(&t.oa) || removed.contains(&t.sa) {
|
|
removed.insert(t.id);
|
|
}
|
|
}
|
|
for a in p.artifact_links.values() {
|
|
if removed.contains(&a.owner) {
|
|
removed.insert(a.id);
|
|
}
|
|
}
|
|
if removed.len() == before {
|
|
break;
|
|
}
|
|
}
|
|
removed
|
|
}
|
|
pub fn delete_cascade(p: &mut Project, id: Id) {
|
|
let removed = deletion_impact(p, id);
|
|
remove_objects(p, removed);
|
|
}
|
|
/// Preserve conditional exchange-allocation dependents until the transaction
|
|
/// has had the opportunity to supply explicit replacement allocation facts.
|
|
pub(crate) fn delete_cascade_deferred(p: &mut Project, id: Id, pending: &mut BTreeSet<Id>) {
|
|
let complete = deletion_impact(p, id);
|
|
let removed = deletion_impact_inner(p, id, true);
|
|
// An explicitly removed relation cannot pass its conditional cascade on
|
|
// to a new relation that later reuses the same identity in this batch.
|
|
pending.retain(|id| !removed.contains(id));
|
|
pending.extend(
|
|
complete
|
|
.difference(&removed)
|
|
.filter(|id| {
|
|
p.relations
|
|
.get(id)
|
|
.is_some_and(|r| r.kind() == RelationKind::ExchangeAllocation)
|
|
})
|
|
.copied(),
|
|
);
|
|
remove_objects(p, removed);
|
|
}
|
|
pub(crate) fn finish_deletions(p: &mut Project, pending: &BTreeSet<Id>) {
|
|
let none = BTreeSet::new();
|
|
for id in pending {
|
|
if p.relations.get(id).is_some_and(|r| {
|
|
r.kind() == RelationKind::ExchangeAllocation
|
|
&& crate::ports::missing_exchange_port_allocations(p, r, &none)
|
|
}) {
|
|
delete_cascade(p, *id);
|
|
}
|
|
}
|
|
}
|
|
fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
|
let removed_names: std::collections::BTreeMap<_, _> = removed
|
|
.iter()
|
|
.map(|id| (*id, p.named(*id).to_string()))
|
|
.collect();
|
|
p.elements.retain(|id, _| !removed.contains(id));
|
|
p.relations.retain(|id, _| !removed.contains(id));
|
|
p.diagrams.retain(|id, _| !removed.contains(id));
|
|
p.traces.retain(|id, _| !removed.contains(id));
|
|
p.artifact_links.retain(|id, _| !removed.contains(id));
|
|
for d in p.diagrams.values_mut() {
|
|
d.view_options.retain_references(|id| !removed.contains(id));
|
|
crate::scenario_layout::prune_deleted(d, &removed);
|
|
d.nodes.retain(|id, _| !removed.contains(id));
|
|
d.routes.retain(|route| !removed.contains(&route.edge));
|
|
for node in d.nodes.values_mut() {
|
|
node.port_anchors
|
|
.retain(|anchor| !removed.contains(&anchor.port));
|
|
}
|
|
}
|
|
let clear = |v: &mut Option<Id>| {
|
|
if v.is_some_and(|id| removed.contains(&id)) {
|
|
*v = None;
|
|
}
|
|
};
|
|
for e in p.elements.values_mut() {
|
|
if let Some(steps) = e.steps_mut() {
|
|
steps.retain(|id| !removed.contains(id));
|
|
}
|
|
if let Some(shared) = e.shared_mut() {
|
|
match shared {
|
|
SharedBody::Interface { supertype }
|
|
| SharedBody::ExchangeItem { supertype, .. }
|
|
| SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype },
|
|
..
|
|
} => clear(supertype),
|
|
SharedBody::Event {
|
|
body: EventBody::Signal { items },
|
|
} => items.retain(|id| !removed.contains(id)),
|
|
SharedBody::Port { contract, legacy } => {
|
|
match contract {
|
|
PortContract::FunctionInput { incoming_items } => {
|
|
incoming_items.retain(|id| !removed.contains(id))
|
|
}
|
|
PortContract::FunctionOutput { outgoing_items } => {
|
|
outgoing_items.retain(|id| !removed.contains(id))
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(old) = legacy {
|
|
clear(&mut old.interface);
|
|
}
|
|
}
|
|
SharedBody::Scenario { context, .. } => clear(context),
|
|
SharedBody::Lifeline { represented } => clear(represented),
|
|
SharedBody::ExchangeCategory {
|
|
exchanges: constrained,
|
|
..
|
|
}
|
|
| SharedBody::Constraint { constrained, .. } => {
|
|
constrained.retain(|id| !removed.contains(id))
|
|
}
|
|
SharedBody::Occurrence { body, .. } => match body {
|
|
OccurrenceBody::Exchange {
|
|
exchange,
|
|
sender,
|
|
receiver,
|
|
reply_to,
|
|
values,
|
|
operation,
|
|
..
|
|
} => {
|
|
if let Some(operation) = operation {
|
|
clear(&mut operation.allocation);
|
|
}
|
|
clear(exchange);
|
|
clear(sender);
|
|
clear(receiver);
|
|
clear(reply_to);
|
|
values.retain(|value| {
|
|
!removed.contains(&value.field)
|
|
&& value.path.iter().all(|id| !removed.contains(id))
|
|
});
|
|
}
|
|
OccurrenceBody::Action { action, lifeline } => {
|
|
clear(action);
|
|
clear(lifeline);
|
|
}
|
|
OccurrenceBody::SequenceReference { sequence } => clear(sequence),
|
|
OccurrenceBody::Control { .. } => {}
|
|
OccurrenceBody::ScenarioUse { scenario, bindings } => {
|
|
clear(scenario);
|
|
bindings.retain(|binding| {
|
|
!removed.contains(&binding.referenced)
|
|
&& !removed.contains(&binding.local)
|
|
});
|
|
}
|
|
OccurrenceBody::State { state, lifeline } => {
|
|
clear(state);
|
|
clear(lifeline);
|
|
}
|
|
OccurrenceBody::Fragment { branches, .. } => {
|
|
branches.retain(|id| !removed.contains(id))
|
|
}
|
|
OccurrenceBody::Branch { .. } => {}
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
for r in p.relations.values_mut() {
|
|
match &mut r.body {
|
|
RelationBody::StateTransition {
|
|
triggers,
|
|
effects,
|
|
trigger,
|
|
..
|
|
} => {
|
|
let deleted: Vec<_> = triggers
|
|
.iter()
|
|
.filter(|id| removed.contains(id))
|
|
.filter_map(|id| removed_names.get(id).cloned())
|
|
.collect();
|
|
triggers.retain(|id| !removed.contains(id));
|
|
effects.retain(|id| !removed.contains(id));
|
|
if !deleted.is_empty() && triggers.is_empty() && trigger.trim().is_empty() {
|
|
*trigger = format!(
|
|
"Removed trigger: {}. Choose a replacement trigger.",
|
|
deleted.join(", ")
|
|
);
|
|
}
|
|
}
|
|
RelationBody::Interaction { items, .. }
|
|
| RelationBody::BoundaryExchange { items, .. }
|
|
| RelationBody::CommunicationMean { items, .. } => {
|
|
items.retain(|id| !removed.contains(id))
|
|
}
|
|
RelationBody::FunctionalExchange { items, .. } => {
|
|
items.retain(|id| !removed.contains(id));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
// Removing an item or a containing payload field can disconnect a still-live
|
|
// record field from an exchange. Its contextual annotation is no longer valid.
|
|
let allowed: std::collections::BTreeMap<_, _> = p
|
|
.elements
|
|
.values()
|
|
.filter_map(|e| match e.shared() {
|
|
Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(id), ..
|
|
},
|
|
..
|
|
}) => p.relations.get(id).map(|r| {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = e.shared()
|
|
else {
|
|
unreachable!()
|
|
};
|
|
let retained: Vec<_> = values
|
|
.iter()
|
|
.filter(|value| {
|
|
crate::data::carried_value_path(p, r, &value.path, value.field).is_some()
|
|
|| (value.path.is_empty()
|
|
&& crate::data::carried_field_paths(p, r)
|
|
.iter()
|
|
.any(|f| f.field == value.field))
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
(e.id, retained)
|
|
}),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
for (id, retained) in allowed {
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = p.elements.get_mut(&id).and_then(Element::shared_mut)
|
|
{
|
|
*values = retained;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "references/scenario_layout_tests.rs"]
|
|
mod scenario_layout_tests;
|