Implement bounded typed data and identified interface contracts

This commit is contained in:
2026-09-06 03:44:09 +03:00
parent 1230ab8a3e
commit 0b58107097
15 changed files with 3127 additions and 177 deletions
+120 -94
View File
@@ -388,68 +388,10 @@ pub fn validate(p: &Project) -> Vec<Issue> {
}
if let Some(shared) = e.shared() {
match shared {
SharedBody::DataType {
definition: DataTypeDefinition::Enumeration { literals },
} => {
let mut seen = BTreeSet::new();
for literal in literals {
text(&mut out, e.id, "literals", literal);
if !seen.insert(literal) {
error(
&mut out,
e.id,
"duplicate_literal",
"literals",
"Enumeration literals must be unique.",
);
}
}
}
SharedBody::DataField {
type_ref,
minimum,
maximum,
unit,
} => {
if maximum.is_some_and(|max| max < *minimum) {
error(
&mut out,
e.id,
"invalid_multiplicity",
"maximum",
"Maximum multiplicity must be at least minimum, or unbounded.",
);
}
text(&mut out, e.id, "unit", unit);
if let TypeRef::Named(id) = type_ref {
expect_element(p, &mut out, e.id, *id, "type_ref", |k| k == Kind::DataType);
}
if let Some(parent) = e.parent.and_then(|id| p.elements.get(&id))
&& parent.kind() == Kind::DataType
&& !matches!(
parent.shared(),
Some(SharedBody::DataType {
definition: DataTypeDefinition::Record
})
)
{
error(
&mut out,
e.id,
"field_owner",
"parent",
"Fields under a data type require a Record type.",
);
}
}
SharedBody::Interface { items } => {
check_ids(&mut out, e.id, "items", items);
for id in items {
expect_element(p, &mut out, e.id, *id, "items", |k| {
k == Kind::ExchangeItem
});
}
}
SharedBody::DataType { .. }
| SharedBody::DataField { .. }
| SharedBody::Interface { .. }
| SharedBody::ExchangeItem { .. } => crate::data::validate_element(p, e, &mut out),
SharedBody::Port { .. } => crate::ports::validate_port(p, e, &mut out),
SharedBody::Scenario {
viewpoint,
@@ -643,9 +585,11 @@ pub fn validate(p: &Project) -> Vec<Issue> {
text(&mut out, r.id, "name", &r.name);
text(&mut out, r.id, "description", &r.description);
validate_relation(p, r, &mut out);
crate::data::validate_relation(p, r, &mut out);
crate::ports::validate_boundary_interface_requirements(p, r, &mut out);
validate_relation_context(p, r, &mut out);
}
crate::data::validate_record_cycles(p, &mut out);
for (key, d) in &p.diagrams {
if *key != d.id {
error(
@@ -1436,23 +1380,10 @@ fn enclosing_loops(p: &Project, occurrence: Id) -> BTreeSet<Id> {
}
/// Fields carried by an exchange, including fields of nested named records.
pub fn carried_fields(p: &Project, relation: &Relation) -> BTreeSet<Id> {
let mut pending = relation.body.items().to_vec();
let mut seen = BTreeSet::new();
let mut fields = BTreeSet::new();
while let Some(owner) = pending.pop() {
if !seen.insert(owner) {
continue;
}
for field in p.children(owner) {
if let Some(SharedBody::DataField { type_ref, .. }) = field.shared() {
fields.insert(field.id);
if let TypeRef::Named(id) = type_ref {
pending.push(*id);
}
}
}
}
fields
crate::data::carried_field_paths(p, relation)
.into_iter()
.map(|f| f.field)
.collect()
}
fn occurrence_uses_lifeline(body: &OccurrenceBody, line: Id) -> bool {
match body {
@@ -1683,36 +1614,125 @@ fn validate_message(p: &Project, e: &Element, body: &OccurrenceBody, out: &mut V
}
}
}
let allowed = exchange
.and_then(|id| p.relations.get(&id))
.map(|r| carried_fields(p, r));
let relation = exchange.and_then(|id| p.relations.get(&id));
if message_kind == &MessageKind::Call
&& relation.is_some_and(|r| {
!r.body
.items()
.iter()
.any(|id| crate::data::item_mechanism(p, *id) == Some(ExchangeMechanism::Operation))
})
{
warning(
out,
e.id,
"call_without_operation",
"exchange",
"This generic call carries no Operation item; define an operation contract when appropriate.",
);
}
let mut fields = BTreeSet::new();
for value in values {
expect_element(p, out, e.id, value.field, "values.field", |kind| {
kind == Kind::DataField
});
for hop in &value.path {
expect_element(p, out, e.id, *hop, "values.path", |kind| {
kind == Kind::DataField
});
}
text(out, e.id, "values.expression", &value.expression);
if !fields.insert(value.field) {
if !fields.insert((&value.path, value.field)) {
error(
out,
e.id,
"duplicate_message_value",
"values",
"Each carried field has at most one contextual value annotation in a message.",
"Each qualified carried field has at most one contextual value annotation.",
);
}
if allowed
.as_ref()
.is_some_and(|allowed| !allowed.contains(&value.field))
{
if value.path.len() > crate::data::MAX_VALUE_PATH_DEPTH {
error(
out,
e.id,
"message_value_field",
"values.field",
"Value annotations must reference fields carried by this message's exchange, including nested record fields.",
"message_value_path_limit",
"values.path",
"The value path exceeds the bounded authoring depth.",
);
}
if let Some(r) = relation {
if let Some(path) = crate::data::carried_value_path(p, r, &value.path, value.field) {
if crate::data::item_mechanism(p, path.item) == Some(ExchangeMechanism::Operation) {
let permitted = match message_kind {
MessageKind::Call => matches!(
path.direction,
ParameterDirection::Unspecified
| ParameterDirection::In
| ParameterDirection::InOut
),
MessageKind::Reply => matches!(
path.direction,
ParameterDirection::Unspecified
| ParameterDirection::Out
| ParameterDirection::InOut
| ParameterDirection::Return
),
_ => true,
};
if !permitted {
error(
out,
e.id,
"message_parameter_phase",
"values.path",
"Call values address In/InOut parameters; replies address Out/InOut/Return parameters.",
);
}
if path.direction == ParameterDirection::Unspecified {
warning(
out,
e.id,
"message_parameter_direction",
"values.path",
"The containing operation parameter has no direction yet.",
);
}
}
} else {
let legacy = value
.path
.is_empty()
.then(|| crate::data::resolve_legacy_field_path(p, r, value.field).ok())
.flatten();
if matches!(
legacy,
Some(
crate::data::LegacyFieldPathResolution::Unique(_)
| crate::data::LegacyFieldPathResolution::Ambiguous
)
) {
warning(
out,
e.id,
if legacy == Some(crate::data::LegacyFieldPathResolution::Ambiguous) {
"message_value_ambiguous_path"
} else {
"message_value_missing_path"
},
"values.path",
"This legacy nested field annotation needs an explicit containing parameter path; its expression is preserved.",
);
} else {
error(
out,
e.id,
"message_value_field",
"values.path",
"The qualified field must be carried by the exchange, following Value record fields only.",
);
}
}
}
if value.expression.trim().is_empty() {
warning(
out,
@@ -2130,7 +2150,11 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
};
if a.layer() != b.layer()
|| !r.kind().permits(a.kind(), b.kind())
|| (a.id == b.id && r.kind() != RelationKind::StateTransition)
|| (a.id == b.id
&& !matches!(
r.kind(),
RelationKind::StateTransition | RelationKind::DataAssociation
))
{
error(
out,
@@ -2381,8 +2405,10 @@ pub fn can_parent(p: &Project, child_kind: Kind, layer: Layer, parent: Option<Id
owner.layer() == layer
&& parent_permits(owner.kind(), child_kind)
&& match owner.shared() {
Some(SharedBody::DataType { definition }) if child_kind == Kind::DataField => {
*definition == DataTypeDefinition::Record
Some(SharedBody::DataType { definition, .. })
if child_kind == Kind::DataField =>
{
matches!(definition, DataTypeDefinition::Record { .. })
}
Some(SharedBody::Occurrence { body, .. }) if child_kind == Kind::Occurrence => {
matches!(