528 lines
15 KiB
Rust
528 lines
15 KiB
Rust
//! Source-only native fixture generator for issue 21; root compiles/runs it.
|
|
//! Uses actual oa-core validation, operation resolution, and whole-Project source round trips.
|
|
//! Usage: nested-call-bound-fixture.exe NEW_OUTPUT_DIRECTORY
|
|
use oa_core::{operations, validation, *};
|
|
use std::{fs, path::Path};
|
|
|
|
fn id(n: u128) -> Id {
|
|
Id::from_u128(0x21b00d00000040008000000000000000 + n)
|
|
}
|
|
fn element(
|
|
p: &mut Project,
|
|
layer: Layer,
|
|
n: u128,
|
|
kind: Kind,
|
|
name: &str,
|
|
parent: Option<Id>,
|
|
) -> Id {
|
|
let mut e = p
|
|
.make_element(kind, layer, name, parent)
|
|
.unwrap_or_else(|| {
|
|
panic!("No generic constructor for {layer:?} {kind:?} '{name}' (fixture ID {n})")
|
|
});
|
|
e.id = id(n);
|
|
e.alias = format!("bound_{n}");
|
|
assert!(p.elements.insert(e.id, e).is_none());
|
|
id(n)
|
|
}
|
|
fn shared(p: &mut Project, element: Id, body: SharedBody) {
|
|
*p.elements.get_mut(&element).unwrap().shared_mut().unwrap() = body;
|
|
}
|
|
fn component_port(p: &mut Project, n: u128, owner: Id, name: &str) -> Id {
|
|
let mut port = ports::make_port(
|
|
p,
|
|
id(n),
|
|
owner,
|
|
name,
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
},
|
|
)
|
|
.expect("Explicit component-port contract and owner");
|
|
port.alias = format!("bound_{n}");
|
|
assert!(p.elements.insert(port.id, port).is_none());
|
|
id(n)
|
|
}
|
|
fn relation(p: &mut Project, n: u128, name: &str, body: RelationBody) -> Id {
|
|
let r = Relation {
|
|
id: id(n),
|
|
alias: format!("bound_{n}"),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
body,
|
|
};
|
|
assert!(p.relations.insert(r.id, r).is_none());
|
|
id(n)
|
|
}
|
|
fn parameter(
|
|
p: &mut Project,
|
|
layer: Layer,
|
|
n: u128,
|
|
owner: Id,
|
|
name: &str,
|
|
direction: ParameterDirection,
|
|
) -> Id {
|
|
let field = element(p, layer, n, Kind::DataField, name, Some(owner));
|
|
shared(
|
|
p,
|
|
field,
|
|
SharedBody::DataField {
|
|
type_ref: TypeRef::Real,
|
|
minimum: 1,
|
|
maximum: Some(1),
|
|
unit: "kPa".into(),
|
|
direction,
|
|
storage: FieldStorage::Value,
|
|
default: None,
|
|
},
|
|
);
|
|
field
|
|
}
|
|
#[derive(Clone, Copy)]
|
|
struct Contract {
|
|
layer: Layer,
|
|
requester: Id,
|
|
responder: Id,
|
|
channel: Id,
|
|
operation: Option<Id>,
|
|
input: Id,
|
|
output: Id,
|
|
}
|
|
fn oa_contract(p: &mut Project) -> Contract {
|
|
let layer = Layer::Oa;
|
|
let requester = element(
|
|
p,
|
|
layer,
|
|
100,
|
|
Kind::Activity,
|
|
"Review pressure demand",
|
|
None,
|
|
);
|
|
let responder = element(p, layer, 101, Kind::Activity, "Inspect pressure", None);
|
|
let item = element(p, layer, 110, Kind::ExchangeItem, "Check pressure", None);
|
|
shared(
|
|
p,
|
|
item,
|
|
SharedBody::ExchangeItem {
|
|
kind: ExchangeItemKind::Information,
|
|
mechanism: ExchangeMechanism::Operation,
|
|
supertype: None,
|
|
},
|
|
);
|
|
let input = parameter(
|
|
p,
|
|
layer,
|
|
111,
|
|
item,
|
|
"requested_pressure",
|
|
ParameterDirection::In,
|
|
);
|
|
let output = parameter(
|
|
p,
|
|
layer,
|
|
112,
|
|
item,
|
|
"measured_pressure",
|
|
ParameterDirection::Return,
|
|
);
|
|
let channel = relation(
|
|
p,
|
|
120,
|
|
"Pressure check",
|
|
RelationBody::Interaction {
|
|
source: requester,
|
|
target: responder,
|
|
items: vec![item],
|
|
},
|
|
);
|
|
Contract {
|
|
layer,
|
|
requester,
|
|
responder,
|
|
channel,
|
|
operation: None,
|
|
input,
|
|
output,
|
|
}
|
|
}
|
|
fn sa_contract(p: &mut Project) -> Contract {
|
|
let layer = Layer::Sa;
|
|
let requester = element(p, layer, 200, Kind::SystemActor, "Operator console", None);
|
|
let responder = element(p, layer, 201, Kind::System, "Pressure service", None);
|
|
let caller_port = component_port(p, 202, requester, "Client socket");
|
|
let provider_port = component_port(p, 203, responder, "Service socket");
|
|
let item = element(p, layer, 210, Kind::ExchangeItem, "Check pressure", None);
|
|
shared(
|
|
p,
|
|
item,
|
|
SharedBody::ExchangeItem {
|
|
kind: ExchangeItemKind::Information,
|
|
mechanism: ExchangeMechanism::Operation,
|
|
supertype: None,
|
|
},
|
|
);
|
|
let input = parameter(
|
|
p,
|
|
layer,
|
|
211,
|
|
item,
|
|
"requested_pressure",
|
|
ParameterDirection::In,
|
|
);
|
|
let output = parameter(
|
|
p,
|
|
layer,
|
|
212,
|
|
item,
|
|
"measured_pressure",
|
|
ParameterDirection::Return,
|
|
);
|
|
let interface = element(p, layer, 213, Kind::Interface, "Pressure check API", None);
|
|
shared(p, interface, SharedBody::Interface { supertype: None });
|
|
let allocation = relation(
|
|
p,
|
|
220,
|
|
"Synchronous pressure check",
|
|
RelationBody::InterfaceItemAllocation {
|
|
source: interface,
|
|
target: item,
|
|
order: 0,
|
|
transmission: TransmissionProtocol::Synchronous,
|
|
acquisition: AcquisitionProtocol::Unspecified,
|
|
},
|
|
);
|
|
relation(
|
|
p,
|
|
221,
|
|
"Console requires check",
|
|
RelationBody::InterfaceRequirement {
|
|
source: caller_port,
|
|
target: interface,
|
|
},
|
|
);
|
|
relation(
|
|
p,
|
|
222,
|
|
"Service provides check",
|
|
RelationBody::InterfaceProvision {
|
|
source: provider_port,
|
|
target: interface,
|
|
},
|
|
);
|
|
let channel = relation(
|
|
p,
|
|
223,
|
|
"Pressure channel",
|
|
RelationBody::BoundaryExchange {
|
|
source: caller_port,
|
|
target: provider_port,
|
|
oriented: false,
|
|
items: vec![item],
|
|
},
|
|
);
|
|
Contract {
|
|
layer,
|
|
requester,
|
|
responder,
|
|
channel,
|
|
operation: Some(allocation),
|
|
input,
|
|
output,
|
|
}
|
|
}
|
|
struct Case {
|
|
owner: Id,
|
|
caller: Id,
|
|
callee: Id,
|
|
call: Id,
|
|
inner: Id,
|
|
inner_reply: Id,
|
|
reply: Id,
|
|
}
|
|
fn message(
|
|
p: &mut Project,
|
|
contract: Contract,
|
|
n: u128,
|
|
owner: Id,
|
|
name: &str,
|
|
endpoints: (Id, Id),
|
|
reply_and_value: (Option<Id>, &str),
|
|
) -> Id {
|
|
let (reply, expression) = reply_and_value;
|
|
let message = element(p, contract.layer, n, Kind::Occurrence, name, Some(owner));
|
|
shared(
|
|
p,
|
|
message,
|
|
SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
operation: if reply.is_none() {
|
|
contract.operation.map(|allocation| OperationUse {
|
|
allocation: Some(allocation),
|
|
})
|
|
} else {
|
|
None
|
|
},
|
|
exchange: Some(contract.channel),
|
|
sender: Some(endpoints.0),
|
|
receiver: Some(endpoints.1),
|
|
message_kind: if reply.is_some() {
|
|
MessageKind::Reply
|
|
} else {
|
|
MessageKind::Call
|
|
},
|
|
reply_to: reply,
|
|
values: vec![MessageValue {
|
|
field: if reply.is_some() {
|
|
contract.output
|
|
} else {
|
|
contract.input
|
|
},
|
|
path: vec![],
|
|
expression: expression.into(),
|
|
}],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
},
|
|
);
|
|
message
|
|
}
|
|
fn scenario(
|
|
p: &mut Project,
|
|
contract: Contract,
|
|
base: u128,
|
|
name: &str,
|
|
callback: Option<Id>,
|
|
) -> Case {
|
|
let owner = element(p, contract.layer, base, Kind::Scenario, name, None);
|
|
shared(
|
|
p,
|
|
owner,
|
|
SharedBody::Scenario {
|
|
viewpoint: if contract.layer == Layer::Oa {
|
|
ScenarioViewpoint::OperationalActivity
|
|
} else {
|
|
ScenarioViewpoint::BoundaryInterface
|
|
},
|
|
context: None,
|
|
precondition: "Two pressure checks are reviewed before the outer reply".into(),
|
|
postcondition: "Each reply retains its own requested and measured values".into(),
|
|
steps: vec![],
|
|
},
|
|
);
|
|
let caller = element(
|
|
p,
|
|
contract.layer,
|
|
base + 1,
|
|
Kind::Lifeline,
|
|
"L1 Console",
|
|
Some(owner),
|
|
);
|
|
let callee = element(
|
|
p,
|
|
contract.layer,
|
|
base + 2,
|
|
Kind::Lifeline,
|
|
"L2 Inspector",
|
|
Some(owner),
|
|
);
|
|
shared(
|
|
p,
|
|
caller,
|
|
SharedBody::Lifeline {
|
|
represented: Some(contract.requester),
|
|
},
|
|
);
|
|
shared(
|
|
p,
|
|
callee,
|
|
SharedBody::Lifeline {
|
|
represented: Some(contract.responder),
|
|
},
|
|
);
|
|
// Reverse UUID order deliberately: chronological order comes from steps.
|
|
let call = message(
|
|
p,
|
|
contract,
|
|
base + 40,
|
|
owner,
|
|
"C1 420 kPa",
|
|
(caller, callee),
|
|
(None, "420.0"),
|
|
);
|
|
let inner_contract = Contract {
|
|
channel: callback.unwrap_or(contract.channel),
|
|
..contract
|
|
};
|
|
let inner_endpoints = if callback.is_some() {
|
|
(callee, caller)
|
|
} else {
|
|
(caller, callee)
|
|
};
|
|
let inner = message(
|
|
p,
|
|
inner_contract,
|
|
base + 30,
|
|
owner,
|
|
"C2 380 kPa",
|
|
inner_endpoints,
|
|
(None, "380.0"),
|
|
);
|
|
let inner_reply = message(
|
|
p,
|
|
inner_contract,
|
|
base + 60,
|
|
owner,
|
|
"R2 378 kPa",
|
|
(inner_endpoints.1, inner_endpoints.0),
|
|
(Some(inner), "378.0"),
|
|
);
|
|
let reply = message(
|
|
p,
|
|
contract,
|
|
base + 50,
|
|
owner,
|
|
"R1 418 kPa",
|
|
(callee, caller),
|
|
(Some(call), "418.0"),
|
|
);
|
|
*p.elements.get_mut(&owner).unwrap().steps_mut().unwrap() =
|
|
vec![call, inner, inner_reply, reply];
|
|
Case {
|
|
owner,
|
|
caller,
|
|
callee,
|
|
call,
|
|
inner,
|
|
inner_reply,
|
|
reply,
|
|
}
|
|
}
|
|
fn assert_case(p: &Project, case: &Case, contract: Contract) {
|
|
assert_eq!(
|
|
p.elements[&case.owner].steps().unwrap(),
|
|
&vec![case.call, case.inner, case.inner_reply, case.reply]
|
|
);
|
|
for (message, invocation, expression) in [
|
|
(case.call, case.call, "420.0"),
|
|
(case.inner, case.inner, "380.0"),
|
|
(case.inner_reply, case.inner, "378.0"),
|
|
(case.reply, case.call, "418.0"),
|
|
] {
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Exchange {
|
|
values,
|
|
exchange,
|
|
sender,
|
|
receiver,
|
|
..
|
|
},
|
|
..
|
|
}) = p.elements[&message].shared()
|
|
else {
|
|
panic!("Missing message");
|
|
};
|
|
assert!(exchange.is_some() && sender.is_some() && receiver.is_some());
|
|
assert_eq!(values.len(), 1);
|
|
assert_eq!(values[0].expression, expression);
|
|
if let Some(allocation) = contract.operation {
|
|
let operations::OperationResolution::Selected(proof) =
|
|
operations::resolve(p, message).unwrap()
|
|
else {
|
|
panic!("Operation is not actually resolved");
|
|
};
|
|
assert_eq!(proof.invocation, invocation);
|
|
assert_eq!(proof.allocation, allocation);
|
|
assert_eq!(proof.caller_port, id(202));
|
|
assert_eq!(proof.provider_port, id(203));
|
|
} else {
|
|
assert_eq!(
|
|
operations::resolve(p, message).unwrap(),
|
|
operations::OperationResolution::Ordinary
|
|
);
|
|
}
|
|
}
|
|
}
|
|
fn main() {
|
|
let args: Vec<_> = std::env::args().collect();
|
|
assert_eq!(args.len(), 2, "Pass a NEW output directory");
|
|
let directory = Path::new(&args[1]);
|
|
assert!(
|
|
!directory.exists(),
|
|
"Refuse to replace existing native evidence"
|
|
);
|
|
let mut p = Project::empty();
|
|
p.id = id(1);
|
|
p.alias = "bound_nested_calls".into();
|
|
p.name = "Bound nested pressure checks".into();
|
|
p.description = "Native activation acceptance. Requested 420/380 kPa, measured 418/378 kPa. Unicode readback: Δ pressure · 戻り値.".into();
|
|
let oa = oa_contract(&mut p);
|
|
let sa = sa_contract(&mut p);
|
|
let oa_case = scenario(&mut p, oa, 1000, "OA Nested pressure checks", None);
|
|
let sa_case = scenario(&mut p, sa, 2000, "SA Nested pressure checks", None);
|
|
let callback = relation(
|
|
&mut p,
|
|
121,
|
|
"Demand review callback",
|
|
RelationBody::Interaction {
|
|
source: oa.responder,
|
|
target: oa.requester,
|
|
items: vec![id(110)],
|
|
},
|
|
);
|
|
let reentrant = scenario(
|
|
&mut p,
|
|
oa,
|
|
3000,
|
|
"OA Reentrant pressure review",
|
|
Some(callback),
|
|
);
|
|
let issues = validation::validate(&p);
|
|
let errors: Vec<_> = issues
|
|
.iter()
|
|
.filter(|i| i.severity == validation::Severity::Error)
|
|
.collect();
|
|
assert!(
|
|
errors.is_empty(),
|
|
"Actual core rejects fixture: {errors:#?}"
|
|
);
|
|
for (case, contract) in [(&oa_case, oa), (&sa_case, sa), (&reentrant, oa)] {
|
|
assert_case(&p, case, contract);
|
|
}
|
|
let canonical = language::format(&p);
|
|
let reparsed =
|
|
language::parse(&canonical).expect("Canonical source must parse through actual core");
|
|
assert_eq!(
|
|
reparsed, p,
|
|
"Whole-Project equality, not only ordered steps"
|
|
);
|
|
assert_eq!(language::format(&reparsed), canonical);
|
|
let mut manifest = String::from("scenario\towner\tL1\tL2\tC1\tC2\tR2\tR1\n");
|
|
for case in [&oa_case, &sa_case, &reentrant] {
|
|
manifest.push_str(&format!(
|
|
"{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
|
|
p.named(case.owner),
|
|
case.owner,
|
|
case.caller,
|
|
case.callee,
|
|
case.call,
|
|
case.inner,
|
|
case.inner_reply,
|
|
case.reply
|
|
));
|
|
}
|
|
fs::create_dir(directory).unwrap();
|
|
fs::write(directory.join("bound-nested-calls.arcadia"), canonical).unwrap();
|
|
fs::write(directory.join("scenario-identities.tsv"), manifest).unwrap();
|
|
fs::write(directory.join("diagnostics.txt"), format!("{issues:#?}\n")).unwrap();
|
|
fs::write(directory.join("README.md"), "Open bound-nested-calls.arcadia. In Behavior select OA Nested pressure checks, SA Nested pressure checks, or OA Reentrant pressure review. Each has two lifelines and four rows ordered C1,C2,R2,R1. C1 requests 420.0 kPa and R1 returns 418.0; C2 requests 380.0 and R2 returns 378.0. The SA messages resolve an actual synchronous interface-operation allocation; Replies derive it from their exact Calls. OA uses a bound Interaction carrying an Operation item without SA-only OperationUse. The reentrant case uses an explicit reverse Interaction for C2/R2. Check unselected bars, both reply ends, each bar's selection, Properties values/correlation, source parity and save/reopen. scenario-identities.tsv lists every UI target UUID. diagnostics.txt records completeness warnings. Generator validation and roundtrip are not desktop paint or native acceptance.\n").unwrap();
|
|
println!(
|
|
"PASS: {} elements, {} relations, three bound scenarios, zero Errors, {} warnings; whole-Project roundtrip and exact SA operation resolution",
|
|
p.elements.len(),
|
|
p.relations.len(),
|
|
issues.len()
|
|
);
|
|
}
|