Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
346 lines
11 KiB
Rust
346 lines
11 KiB
Rust
//! One operation resolver for admission, forms and scenario labels.
|
|
use crate::*;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ResolvedOperation {
|
|
/// The actual Call/Asynchronous declaration, also when resolving its Reply.
|
|
pub invocation: Id,
|
|
pub allocation: Id,
|
|
pub interface: Id,
|
|
pub item: Id,
|
|
pub caller_port: Id,
|
|
pub provider_port: Id,
|
|
pub transmission: TransmissionProtocol,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum OperationResolution {
|
|
Ordinary,
|
|
Unresolved { invocation: Id, reason: String },
|
|
Selected(ResolvedOperation),
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
|
#[error("{message}")]
|
|
pub struct OperationError {
|
|
pub code: &'static str,
|
|
pub message: String,
|
|
}
|
|
fn error(code: &'static str, message: &str) -> OperationError {
|
|
OperationError {
|
|
code,
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn resolve(p: &Project, occurrence: Id) -> Result<OperationResolution, OperationError> {
|
|
let e = p
|
|
.elements
|
|
.get(&occurrence)
|
|
.ok_or_else(|| error("operation_context", "The message no longer exists."))?;
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
operation,
|
|
message_kind,
|
|
reply_to,
|
|
..
|
|
},
|
|
..
|
|
}) = e.shared()
|
|
else {
|
|
return Ok(OperationResolution::Ordinary);
|
|
};
|
|
let invocation = if *message_kind == MessageKind::Reply {
|
|
if operation.is_some() {
|
|
return Err(error(
|
|
"reply_operation",
|
|
"A Reply derives its operation from its exact Call; it cannot store a second selection.",
|
|
));
|
|
}
|
|
let Some(call) = reply_to else {
|
|
return Ok(OperationResolution::Ordinary);
|
|
};
|
|
if !matches!(
|
|
p.elements.get(call).and_then(Element::shared),
|
|
Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
message_kind: MessageKind::Call,
|
|
..
|
|
},
|
|
..
|
|
})
|
|
) {
|
|
return Ok(OperationResolution::Ordinary);
|
|
}
|
|
*call
|
|
} else {
|
|
occurrence
|
|
};
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
body @ OccurrenceBody::Exchange {
|
|
operation: Some(operation),
|
|
message_kind,
|
|
exchange,
|
|
sender,
|
|
receiver,
|
|
..
|
|
},
|
|
..
|
|
}) = p.elements.get(&invocation).and_then(Element::shared)
|
|
else {
|
|
return Ok(OperationResolution::Ordinary);
|
|
};
|
|
if p.layer_of(invocation) != Some(Layer::Sa)
|
|
|| !matches!(message_kind, MessageKind::Call | MessageKind::Asynchronous)
|
|
|| validation::ancestor_of_kind(p, invocation, Kind::Scenario)
|
|
.and_then(|id| p.elements.get(&id))
|
|
.and_then(Element::shared)
|
|
.is_none_or(|body| {
|
|
!matches!(
|
|
body,
|
|
SharedBody::Scenario {
|
|
viewpoint: ScenarioViewpoint::BoundaryInterface,
|
|
..
|
|
}
|
|
)
|
|
})
|
|
{
|
|
return Err(error(
|
|
"operation_context",
|
|
"Operation use belongs to a Call or Asynchronous message in an SA BoundaryInterface scenario.",
|
|
));
|
|
}
|
|
let unresolved = |reason: &str| {
|
|
Ok(OperationResolution::Unresolved {
|
|
invocation,
|
|
reason: reason.into(),
|
|
})
|
|
};
|
|
if exchange
|
|
.and_then(|id| p.relations.get(&id))
|
|
.is_some_and(|r| r.kind() != RelationKind::BoundaryExchange)
|
|
{
|
|
return Err(error(
|
|
"operation_channel",
|
|
"Operation use requires an explicit boundary exchange channel.",
|
|
));
|
|
}
|
|
let Some(allocation) = operation.allocation else {
|
|
return unresolved(
|
|
"Choose the interface operation allocation; existing value expressions are preserved.",
|
|
);
|
|
};
|
|
let Some(relation) = p.relations.get(&allocation) else {
|
|
return Err(error(
|
|
"operation_allocation",
|
|
"The selected interface operation allocation no longer exists.",
|
|
));
|
|
};
|
|
let RelationBody::InterfaceItemAllocation {
|
|
source: interface,
|
|
target: item,
|
|
transmission,
|
|
..
|
|
} = relation.body
|
|
else {
|
|
return Err(error(
|
|
"operation_allocation",
|
|
"Choose an InterfaceItemAllocation, not an item or another relationship.",
|
|
));
|
|
};
|
|
if p.layer_of(allocation) != Some(Layer::Sa)
|
|
|| !matches!(
|
|
p.elements.get(&item).and_then(Element::shared),
|
|
Some(SharedBody::ExchangeItem {
|
|
kind: ExchangeItemKind::Information,
|
|
mechanism: ExchangeMechanism::Operation,
|
|
..
|
|
})
|
|
)
|
|
{
|
|
return Err(error(
|
|
"operation_allocation",
|
|
"The selected SA allocation must declare an Information item with Operation mechanism.",
|
|
));
|
|
}
|
|
let Some(channel) = exchange.and_then(|id| p.relations.get(&id)) else {
|
|
return Err(error(
|
|
"operation_channel",
|
|
"A selected operation requires an existing boundary exchange carrying its item; clear the allocation to retain unresolved intent.",
|
|
));
|
|
};
|
|
if channel.kind() != RelationKind::BoundaryExchange
|
|
|| !channel
|
|
.body
|
|
.items()
|
|
.iter()
|
|
.any(|carried| data::item_satisfies(p, *carried, item))
|
|
{
|
|
return Err(error(
|
|
"operation_channel",
|
|
"The boundary exchange must carry the selected operation item or an admitted subtype.",
|
|
));
|
|
}
|
|
if sender.is_none() || receiver.is_none() {
|
|
return Err(error(
|
|
"operation_lifelines",
|
|
"A selected operation requires actual caller and provider lifelines; clear the allocation to retain unresolved intent.",
|
|
));
|
|
}
|
|
let (caller_port, provider_port) = match validation::resolve_message_endpoints(p, body) {
|
|
validation::MessageEndpointResolution::Resolved { source, target } => (source, target),
|
|
validation::MessageEndpointResolution::Conflict => {
|
|
return Err(error(
|
|
"operation_direction",
|
|
"The selected lifelines conflict with the boundary channel's actual caller/provider direction.",
|
|
));
|
|
}
|
|
_ => {
|
|
return Err(error(
|
|
"operation_direction",
|
|
"A selected operation requires a proven actual caller/provider channel direction; clear the allocation to retain unresolved intent.",
|
|
));
|
|
}
|
|
};
|
|
for port in [caller_port, provider_port] {
|
|
if !matches!(
|
|
p.elements.get(&port).and_then(ports::port_contract),
|
|
Some(PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
..
|
|
})
|
|
) {
|
|
return Err(error(
|
|
"operation_ports",
|
|
"Operation proof requires concrete Standard component ports.",
|
|
));
|
|
}
|
|
}
|
|
let exposes = |interface| {
|
|
data::effective_interface_allocations(p, interface)
|
|
.iter()
|
|
.any(|r| r.id == allocation)
|
|
};
|
|
let providers: Vec<_> = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| {
|
|
r.kind() == RelationKind::InterfaceProvision
|
|
&& r.source() == provider_port
|
|
&& exposes(r.target())
|
|
})
|
|
.map(Relation::target)
|
|
.collect();
|
|
let valid = p.relations.values().any(|r| {
|
|
r.kind() == RelationKind::InterfaceRequirement
|
|
&& r.source() == caller_port
|
|
&& exposes(r.target())
|
|
&& providers
|
|
.iter()
|
|
.any(|provided| data::interface_satisfies(p, *provided, r.target()))
|
|
});
|
|
if !valid {
|
|
return Err(error(
|
|
"operation_roles",
|
|
"The actual receiver port must provide, and sender port require, compatible interfaces exposing this exact allocation.",
|
|
));
|
|
}
|
|
if !matches!(
|
|
(message_kind, transmission),
|
|
(_, TransmissionProtocol::Unspecified)
|
|
| (MessageKind::Call, TransmissionProtocol::Synchronous)
|
|
| (
|
|
MessageKind::Asynchronous,
|
|
TransmissionProtocol::Asynchronous
|
|
)
|
|
) {
|
|
return Err(error(
|
|
"operation_transmission",
|
|
"Calls require synchronous transmission; asynchronous operation messages require asynchronous transmission.",
|
|
));
|
|
}
|
|
let selected = ResolvedOperation {
|
|
invocation,
|
|
allocation,
|
|
interface,
|
|
item,
|
|
caller_port,
|
|
provider_port,
|
|
transmission,
|
|
};
|
|
if occurrence != invocation {
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
exchange: reply_exchange,
|
|
..
|
|
},
|
|
..
|
|
}) = e.shared()
|
|
else {
|
|
unreachable!()
|
|
};
|
|
if let Some(response) = reply_exchange.and_then(|id| p.relations.get(&id))
|
|
&& response.id != channel.id
|
|
&& !response
|
|
.body
|
|
.items()
|
|
.iter()
|
|
.any(|carried| data::item_satisfies(p, *carried, item))
|
|
{
|
|
return Err(error(
|
|
"operation_reply_channel",
|
|
"A separate reply channel must carry the Call's selected operation item or an admitted subtype, including parameterless returns.",
|
|
));
|
|
}
|
|
}
|
|
Ok(OperationResolution::Selected(selected))
|
|
}
|
|
|
|
/// Valid selections under the proposed message context, without mutating the model.
|
|
pub fn eligible_allocations(p: &Project, occurrence: Id) -> Vec<Id> {
|
|
let mut candidate = p.clone();
|
|
let ids: Vec<_> = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::InterfaceItemAllocation)
|
|
.map(|r| r.id)
|
|
.collect();
|
|
ids.into_iter()
|
|
.filter(|id| {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { operation, .. },
|
|
..
|
|
}) = candidate
|
|
.elements
|
|
.get_mut(&occurrence)
|
|
.and_then(Element::shared_mut)
|
|
else {
|
|
return false;
|
|
};
|
|
*operation = Some(OperationUse {
|
|
allocation: Some(*id),
|
|
});
|
|
matches!(
|
|
resolve(&candidate, occurrence),
|
|
Ok(OperationResolution::Selected(_))
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Reuse the existing bounded field-path engine with exactly one selected item.
|
|
pub fn signature_relation(operation: &ResolvedOperation) -> Relation {
|
|
Relation {
|
|
id: operation.allocation,
|
|
alias: String::new(),
|
|
name: String::new(),
|
|
description: String::new(),
|
|
body: RelationBody::Interaction {
|
|
source: operation.caller_port,
|
|
target: operation.provider_port,
|
|
items: vec![operation.item],
|
|
},
|
|
}
|
|
}
|