use crate::ir_nodes::{IRProgram, IRSession, IRSessionRole, IRSessionStep};
use crate::session::{Payload, SessionType};
fn lower_steps(steps: &[IRSessionStep]) -> SessionType {
let Some((first, rest)) = steps.split_first() else {
return SessionType::End;
};
match first.op.as_str() {
"send" => SessionType::send(first.message_type.clone(), lower_steps(rest)),
"receive" => SessionType::recv(first.message_type.clone(), lower_steps(rest)),
"loop" => SessionType::var("X"),
"end" => SessionType::End,
"select" => SessionType::select(branch_types(first)),
"branch" => SessionType::branch(branch_types(first)),
"interrupt" => {
let arm = |label: &str| {
first
.branches
.iter()
.find(|b| b.label == label)
.map(|b| lower_steps(&b.steps))
.unwrap_or(SessionType::End)
};
SessionType::Interrupt {
signal: Payload::new(first.message_type.clone()),
body: Box::new(arm("body")),
handler: Box::new(arm("handler")),
}
}
"resume" => SessionType::Resume,
_ => lower_steps(rest),
}
}
fn branch_types(step: &IRSessionStep) -> Vec<(String, SessionType)> {
step.branches
.iter()
.map(|b| (b.label.clone(), lower_steps(&b.steps)))
.collect()
}
fn contains_loop(steps: &[IRSessionStep]) -> bool {
steps.iter().any(|s| {
s.op == "loop" || s.branches.iter().any(|b| contains_loop(&b.steps))
})
}
pub fn session_type_of_role(role: &IRSessionRole) -> SessionType {
let body = lower_steps(&role.steps);
if contains_loop(&role.steps) {
SessionType::rec("X", body)
} else {
body
}
}
pub fn server_schema(ir: &IRProgram, session_name: &str) -> Option<SessionType> {
let session: &IRSession = ir.sessions.iter().find(|s| s.name == session_name)?;
let role = session.roles.first()?;
Some(session_type_of_role(role))
}
pub fn schema_for_socket(ir: &IRProgram, socket_name: &str) -> Option<SessionType> {
let socket = ir.sockets.iter().find(|s| s.name == socket_name)?;
server_schema(ir, &socket.protocol)
}
pub fn credit_for_socket(ir: &IRProgram, socket_name: &str) -> Option<u64> {
ir.sockets
.iter()
.find(|s| s.name == socket_name)
.and_then(|s| s.backpressure_credit)
.map(|c| if c < 0 { 0 } else { c as u64 })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir_nodes::{IRSessionBranch, IRSocket};
fn step(op: &str, msg: &str) -> IRSessionStep {
IRSessionStep {
node_type: "session_step",
source_line: 0,
source_column: 0,
op: op.into(),
message_type: msg.into(),
branches: Vec::new(),
binder: String::new(),
resumable: false,
}
}
fn role(name: &str, steps: Vec<IRSessionStep>) -> IRSessionRole {
IRSessionRole {
node_type: "session_role",
source_line: 0,
source_column: 0,
name: name.into(),
steps,
}
}
fn program(sessions: Vec<IRSession>, sockets: Vec<IRSocket>) -> IRProgram {
let mut ir = IRProgram::new();
ir.sessions = sessions;
ir.sockets = sockets;
ir
}
fn session(name: &str, roles: Vec<IRSessionRole>) -> IRSession {
IRSession {
node_type: "session",
source_line: 0,
source_column: 0,
name: name.into(),
roles,
}
}
fn socket(name: &str, protocol: &str, credit: Option<i64>) -> IRSocket {
IRSocket {
node_type: "socket",
source_line: 0,
source_column: 0,
name: name.into(),
protocol: protocol.into(),
backpressure_credit: credit,
reconnect: false,
legal_basis: None,
}
}
#[test]
fn the_declared_protocol_is_what_compiles() {
let s = session(
"Trade",
vec![
role("broker", vec![step("receive", "Order"), step("send", "Fill"), step("end", "")]),
role("client", vec![step("send", "Order"), step("receive", "Fill"), step("end", "")]),
],
);
let ir = program(vec![s], vec![socket("Wire", "Trade", None)]);
let schema = schema_for_socket(&ir, "Wire").expect("the declared protocol must resolve");
let expected = SessionType::recv(
"Order",
SessionType::send("Fill", SessionType::End),
);
assert_eq!(
schema, expected,
"the runtime must enforce `?Order.!Fill.end` — the protocol the adopter WROTE. \
Enterprise substituted a hardcoded chat schema here, so a proven-dual protocol was \
deployed and a different one was enforced"
);
}
#[test]
fn the_server_schema_is_dual_to_the_client_role() {
let broker = role(
"broker",
vec![step("receive", "Order"), step("send", "Fill"), step("end", "")],
);
let client = role(
"client",
vec![step("send", "Order"), step("receive", "Fill"), step("end", "")],
);
let server_ty = session_type_of_role(&broker);
let client_ty = session_type_of_role(&client);
assert!(
server_ty.is_dual_to(&client_ty),
"the compiled roles must be dual — the same law the type-checker proves"
);
}
#[test]
fn a_looping_role_gets_its_recursion_point() {
let r = role(
"echo",
vec![step("receive", "Msg"), step("send", "Msg"), step("loop", "")],
);
let ty = session_type_of_role(&r);
assert_eq!(
ty,
SessionType::rec(
"X",
SessionType::recv("Msg", SessionType::send("Msg", SessionType::var("X")))
)
);
}
#[test]
fn select_and_branch_compile_their_arms() {
let mut sel = step("select", "");
sel.branches = vec![
IRSessionBranch {
node_type: "session_branch",
label: "buy".into(),
steps: vec![step("send", "Buy"), step("end", "")],
},
IRSessionBranch {
node_type: "session_branch",
label: "quit".into(),
steps: vec![step("end", "")],
},
];
let ty = session_type_of_role(&role("r", vec![sel]));
match ty {
SessionType::Select(arms) => {
assert_eq!(arms.len(), 2);
assert_eq!(arms["buy"], SessionType::send("Buy", SessionType::End));
assert_eq!(arms["quit"], SessionType::End);
}
other => panic!("expected Select, got {other:?}"),
}
}
#[test]
fn an_undeclared_protocol_resolves_to_nothing() {
let ir = program(vec![], vec![socket("Wire", "GhostProtocol", None)]);
assert!(schema_for_socket(&ir, "Wire").is_none());
assert!(schema_for_socket(&ir, "NoSuchSocket").is_none());
}
#[test]
fn the_declared_credit_reaches_the_runtime() {
let ir = program(vec![], vec![socket("Wire", "P", Some(8))]);
assert_eq!(credit_for_socket(&ir, "Wire"), Some(8));
let ir2 = program(vec![], vec![socket("Wire", "P", Some(-3))]);
assert_eq!(credit_for_socket(&ir2, "Wire"), Some(0));
}
}