use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Payload(pub String);
impl Payload {
pub fn new(name: impl Into<String>) -> Self {
Payload(name.into())
}
}
impl fmt::Display for Payload {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SessionType {
End,
Send {
payload: Payload,
credit: Option<u64>,
cont: Box<SessionType>,
},
Recv {
payload: Payload,
credit: Option<u64>,
cont: Box<SessionType>,
},
Select(BTreeMap<String, SessionType>),
Branch(BTreeMap<String, SessionType>),
Rec(String, Box<SessionType>),
Var(String),
}
impl SessionType {
pub fn send(payload: impl Into<String>, then: SessionType) -> Self {
SessionType::Send {
payload: Payload::new(payload),
credit: None,
cont: Box::new(then),
}
}
pub fn recv(payload: impl Into<String>, then: SessionType) -> Self {
SessionType::Recv {
payload: Payload::new(payload),
credit: None,
cont: Box::new(then),
}
}
pub fn send_credit(payload: impl Into<String>, n: u64, then: SessionType) -> Self {
SessionType::Send {
payload: Payload::new(payload),
credit: Some(n),
cont: Box::new(then),
}
}
pub fn recv_credit(payload: impl Into<String>, n: u64, then: SessionType) -> Self {
SessionType::Recv {
payload: Payload::new(payload),
credit: Some(n),
cont: Box::new(then),
}
}
pub fn select(branches: impl IntoIterator<Item = (String, SessionType)>) -> Self {
SessionType::Select(branches.into_iter().collect())
}
pub fn branch(branches: impl IntoIterator<Item = (String, SessionType)>) -> Self {
SessionType::Branch(branches.into_iter().collect())
}
pub fn rec(var: impl Into<String>, body: SessionType) -> Self {
SessionType::Rec(var.into(), Box::new(body))
}
pub fn var(name: impl Into<String>) -> Self {
SessionType::Var(name.into())
}
pub fn dual(&self) -> SessionType {
match self {
SessionType::End => SessionType::End,
SessionType::Send { payload, credit, cont } => SessionType::Recv {
payload: payload.clone(),
credit: *credit,
cont: Box::new(cont.dual()),
},
SessionType::Recv { payload, credit, cont } => SessionType::Send {
payload: payload.clone(),
credit: *credit,
cont: Box::new(cont.dual()),
},
SessionType::Select(m) => SessionType::Branch(dual_map(m)),
SessionType::Branch(m) => SessionType::Select(dual_map(m)),
SessionType::Rec(x, b) => SessionType::Rec(x.clone(), Box::new(b.dual())),
SessionType::Var(x) => SessionType::Var(x.clone()),
}
}
fn subst(&self, var: &str, repl: &SessionType) -> SessionType {
match self {
SessionType::End => SessionType::End,
SessionType::Send { payload, credit, cont } => SessionType::Send {
payload: payload.clone(),
credit: *credit,
cont: Box::new(cont.subst(var, repl)),
},
SessionType::Recv { payload, credit, cont } => SessionType::Recv {
payload: payload.clone(),
credit: *credit,
cont: Box::new(cont.subst(var, repl)),
},
SessionType::Select(m) => SessionType::Select(subst_map(m, var, repl)),
SessionType::Branch(m) => SessionType::Branch(subst_map(m, var, repl)),
SessionType::Rec(x, b) => {
if x == var {
self.clone() } else {
SessionType::Rec(x.clone(), Box::new(b.subst(var, repl)))
}
}
SessionType::Var(x) => {
if x == var {
repl.clone()
} else {
self.clone()
}
}
}
}
pub fn unfold_head(&self) -> SessionType {
let mut t = self.clone();
while let SessionType::Rec(x, b) = t {
let whole = SessionType::Rec(x.clone(), b.clone());
t = b.subst(&x, &whole);
}
t
}
pub fn equiv(&self, other: &SessionType) -> bool {
let mut assumed: Vec<(SessionType, SessionType)> = Vec::new();
equiv_inner(self, other, &mut assumed)
}
pub fn is_dual_to(&self, peer: &SessionType) -> bool {
peer.equiv(&self.dual())
}
pub fn with_credit(&self, n: u64) -> SessionType {
match self {
SessionType::End => SessionType::End,
SessionType::Send { payload, cont, .. } => SessionType::Send {
payload: payload.clone(),
credit: Some(n),
cont: Box::new(cont.with_credit(n)),
},
SessionType::Recv { payload, cont, .. } => SessionType::Recv {
payload: payload.clone(),
credit: Some(n),
cont: Box::new(cont.with_credit(n)),
},
SessionType::Select(m) => SessionType::Select(
m.iter().map(|(l, s)| (l.clone(), s.with_credit(n))).collect(),
),
SessionType::Branch(m) => SessionType::Branch(
m.iter().map(|(l, s)| (l.clone(), s.with_credit(n))).collect(),
),
SessionType::Rec(x, b) => SessionType::Rec(x.clone(), Box::new(b.with_credit(n))),
SessionType::Var(x) => SessionType::Var(x.clone()),
}
}
pub fn has_send_at_zero(&self) -> Option<Payload> {
match self {
SessionType::End => None,
SessionType::Send { payload, credit: Some(0), .. } => Some(payload.clone()),
SessionType::Send { cont, .. } | SessionType::Recv { cont, .. } => cont.has_send_at_zero(),
SessionType::Select(m) | SessionType::Branch(m) => {
m.values().find_map(|s| s.has_send_at_zero())
}
SessionType::Rec(_, b) => b.has_send_at_zero(),
SessionType::Var(_) => None,
}
}
pub fn credit_analyse(&self, budget: u64) -> Result<(), CreditError> {
if let Some(p) = self.has_send_at_zero() {
return Err(CreditError::SendAtZero { payload: p });
}
let _final = credit_walk(self, budget as i64, budget as i64)?;
Ok(())
}
pub fn recurring_paths(&self, x: &str) -> Vec<(u64, u64)> {
let mut out = Vec::new();
recurring_paths_into(self, x, 0, 0, &mut out);
out
}
pub fn credit_delta(&self, x: &str) -> (u64, u64) {
self.recurring_paths(x)
.into_iter()
.max_by_key(|(s, r)| *s as i64 - *r as i64)
.unwrap_or((0, 0))
}
pub fn projects_to_sse(&self) -> bool {
self.has_polarity(Polarity::Producer)
}
pub fn projects_to_sse_consumer(&self) -> bool {
self.has_polarity(Polarity::Consumer)
}
pub fn has_polarity(&self, p: Polarity) -> bool {
match (self, p) {
(SessionType::End, _) => true,
(SessionType::Var(_), _) => true,
(SessionType::Send { cont, .. }, Polarity::Producer) => cont.has_polarity(p),
(SessionType::Recv { cont, .. }, Polarity::Consumer) => cont.has_polarity(p),
(SessionType::Select(arms), Polarity::Producer) => {
arms.values().all(|s| s.has_polarity(p))
}
(SessionType::Branch(arms), Polarity::Consumer) => {
arms.values().all(|s| s.has_polarity(p))
}
(SessionType::Rec(_, body), _) => body.has_polarity(p),
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Polarity {
Producer,
Consumer,
}
impl Polarity {
pub fn flip(self) -> Self {
match self {
Polarity::Producer => Polarity::Consumer,
Polarity::Consumer => Polarity::Producer,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CreditError {
SendAtZero { payload: Payload },
BurstOverflow { payload: Payload, budget: u64, burst: u64 },
LoopUnsustainable { sends_per_iter: u64, recvs_per_iter: u64 },
}
impl fmt::Display for CreditError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CreditError::SendAtZero { payload } => {
write!(f, "send `{payload}` at credit n=0 has no typing rule (D2, §4.2)")
}
CreditError::BurstOverflow { payload, budget, burst } => write!(
f,
"credit-window overflow at send `{payload}`: the protocol requires a \
send-burst of {burst} but the socket's `credit({budget})` cannot absorb it"
),
CreditError::LoopUnsustainable { sends_per_iter, recvs_per_iter } => write!(
f,
"recursive body is unsustainable: Δ = {sends_per_iter} - {recvs_per_iter} > 0 \
(no finite credit window keeps unbounded iteration in flight)"
),
}
}
}
fn credit_walk(t: &SessionType, available: i64, budget: i64) -> Result<i64, CreditError> {
match t {
SessionType::End => Ok(available),
SessionType::Send { payload, cont, .. } => {
let next = available - 1;
if next < 0 {
return Err(CreditError::BurstOverflow {
payload: payload.clone(),
budget: budget as u64,
burst: (budget - available + 1) as u64,
});
}
credit_walk(cont, next, budget)
}
SessionType::Recv { cont, .. } => {
let next = (available + 1).min(budget);
credit_walk(cont, next, budget)
}
SessionType::Select(m) | SessionType::Branch(m) => {
let mut worst = available;
for arm in m.values() {
let post = credit_walk(arm, available, budget)?;
if post < worst {
worst = post;
}
}
Ok(worst)
}
SessionType::Rec(x, body) => {
for (s, r) in body.recurring_paths(x) {
if s > r {
return Err(CreditError::LoopUnsustainable {
sends_per_iter: s,
recvs_per_iter: r,
});
}
}
credit_walk(body, available, budget)
}
SessionType::Var(_) => {
Ok(available)
}
}
}
fn recurring_paths_into(t: &SessionType, x: &str, s: u64, r: u64, out: &mut Vec<(u64, u64)>) {
match t {
SessionType::End => {} SessionType::Var(y) if y == x => out.push((s, r)),
SessionType::Var(_) => {} SessionType::Send { cont, .. } => recurring_paths_into(cont, x, s + 1, r, out),
SessionType::Recv { cont, .. } => recurring_paths_into(cont, x, s, r + 1, out),
SessionType::Select(m) | SessionType::Branch(m) => {
for arm in m.values() {
recurring_paths_into(arm, x, s, r, out);
}
}
SessionType::Rec(y, body) if y != x => recurring_paths_into(body, x, s, r, out),
SessionType::Rec(_, _) => {} }
}
fn dual_map(m: &BTreeMap<String, SessionType>) -> BTreeMap<String, SessionType> {
m.iter().map(|(l, s)| (l.clone(), s.dual())).collect()
}
fn subst_map(m: &BTreeMap<String, SessionType>, var: &str, repl: &SessionType) -> BTreeMap<String, SessionType> {
m.iter().map(|(l, s)| (l.clone(), s.subst(var, repl))).collect()
}
fn equiv_inner(s: &SessionType, t: &SessionType, assumed: &mut Vec<(SessionType, SessionType)>) -> bool {
if assumed.iter().any(|(x, y)| x == s && y == t) {
return true;
}
assumed.push((s.clone(), t.clone()));
match (s.unfold_head(), t.unfold_head()) {
(SessionType::End, SessionType::End) => true,
(
SessionType::Send { payload: a, credit: ca, cont: sk },
SessionType::Send { payload: b, credit: cb, cont: tk },
) => a == b && ca == cb && equiv_inner(&sk, &tk, assumed),
(
SessionType::Recv { payload: a, credit: ca, cont: sk },
SessionType::Recv { payload: b, credit: cb, cont: tk },
) => a == b && ca == cb && equiv_inner(&sk, &tk, assumed),
(SessionType::Select(m1), SessionType::Select(m2)) => equiv_maps(&m1, &m2, assumed),
(SessionType::Branch(m1), SessionType::Branch(m2)) => equiv_maps(&m1, &m2, assumed),
(SessionType::Var(x), SessionType::Var(y)) => x == y,
_ => false,
}
}
fn equiv_maps(
m1: &BTreeMap<String, SessionType>,
m2: &BTreeMap<String, SessionType>,
assumed: &mut Vec<(SessionType, SessionType)>,
) -> bool {
if m1.len() != m2.len() || !m1.keys().all(|l| m2.contains_key(l)) {
return false;
}
m1.iter().all(|(l, s1)| equiv_inner(s1, &m2[l], assumed))
}
impl fmt::Display for SessionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionType::End => f.write_str("end"),
SessionType::Send { payload, credit, cont } => match credit {
Some(n) => write!(f, "!^{n}{payload}.{cont}"),
None => write!(f, "!{payload}.{cont}"),
},
SessionType::Recv { payload, credit, cont } => match credit {
Some(n) => write!(f, "?^{n}{payload}.{cont}"),
None => write!(f, "?{payload}.{cont}"),
},
SessionType::Select(m) => write_choice(f, "+", m),
SessionType::Branch(m) => write_choice(f, "&", m),
SessionType::Rec(x, b) => write!(f, "rec {x}.{b}"),
SessionType::Var(x) => f.write_str(x),
}
}
}
fn write_choice(f: &mut fmt::Formatter<'_>, sym: &str, m: &BTreeMap<String, SessionType>) -> fmt::Result {
write!(f, "{sym}{{")?;
for (i, (l, s)) in m.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{l}: {s}")?;
}
f.write_str("}")
}
#[cfg(test)]
mod tests {
use super::*;
fn sel(pairs: &[(&str, SessionType)]) -> SessionType {
SessionType::select(pairs.iter().map(|(l, s)| (l.to_string(), s.clone())))
}
fn brn(pairs: &[(&str, SessionType)]) -> SessionType {
SessionType::branch(pairs.iter().map(|(l, s)| (l.to_string(), s.clone())))
}
#[test]
fn dual_swaps_send_recv_and_keeps_payload() {
let s = SessionType::send("Int", SessionType::End);
assert_eq!(s.dual(), SessionType::recv("Int", SessionType::End));
assert_eq!(SessionType::recv("Int", SessionType::End).dual(), s);
}
#[test]
fn dual_swaps_select_branch() {
let s = sel(&[("a", SessionType::End), ("b", SessionType::send("T", SessionType::End))]);
let d = s.dual();
assert!(matches!(d, SessionType::Branch(_)));
assert_eq!(d, brn(&[("a", SessionType::End), ("b", SessionType::recv("T", SessionType::End))]));
}
#[test]
fn duality_is_an_involution() {
let samples = vec![
SessionType::End,
SessionType::send("A", SessionType::recv("B", SessionType::End)),
sel(&[("x", SessionType::End), ("y", SessionType::recv("Q", SessionType::End))]),
SessionType::rec(
"X",
SessionType::send("Msg", brn(&[("more", SessionType::var("X")), ("done", SessionType::End)])),
),
];
for s in samples {
assert!(s.dual().dual().equiv(&s), "(S⊥)⊥ ≢ S for {s}");
}
}
#[test]
fn connection_law_holds_for_dual_and_fails_otherwise() {
let s = SessionType::send("Q", SessionType::recv("R", SessionType::End));
assert!(s.is_dual_to(&s.dual()), "a session must be dual to its own dual");
assert!(!s.is_dual_to(&s));
let wrong = SessionType::recv("Q", SessionType::send("WRONG", SessionType::End));
assert!(!s.is_dual_to(&wrong));
}
#[test]
fn equirecursive_fold_unfold_equality() {
let folded = SessionType::rec("X", SessionType::send("A", SessionType::var("X")));
let unfolded = SessionType::send("A", folded.clone());
assert!(folded.equiv(&unfolded));
assert!(unfolded.equiv(&folded));
}
#[test]
fn equality_is_insensitive_to_bound_variable_name() {
let x = SessionType::rec("X", SessionType::send("A", SessionType::var("X")));
let y = SessionType::rec("Y", SessionType::send("A", SessionType::var("Y")));
assert!(x.equiv(&y), "α-equivalent recursive sessions must be equal");
}
#[test]
fn equality_reflexive_and_rejects_real_differences() {
let s = sel(&[("a", SessionType::send("T", SessionType::End)), ("b", SessionType::End)]);
assert!(s.equiv(&s));
assert!(!SessionType::send("T", SessionType::End).equiv(&SessionType::recv("T", SessionType::End)));
assert!(!SessionType::send("A", SessionType::End).equiv(&SessionType::send("B", SessionType::End)));
let s2 = sel(&[("a", SessionType::send("T", SessionType::End)), ("c", SessionType::End)]);
assert!(!s.equiv(&s2));
assert!(!sel(&[("a", SessionType::End)]).equiv(&brn(&[("a", SessionType::End)])));
}
#[test]
fn connection_law_holds_for_recursive_dialogue() {
let client = SessionType::rec(
"X",
sel(&[
(
"ask",
SessionType::send(
"Utterance",
brn(&[("token", SessionType::recv("Token", SessionType::var("X"))), ("done", SessionType::End)]),
),
),
("cancel", SessionType::End),
]),
);
assert!(client.is_dual_to(&client.dual()));
assert!(!client.is_dual_to(&client));
assert!(client.equiv(&client));
}
#[test]
fn display_is_readable() {
let s = SessionType::send("Int", SessionType::recv("Bool", SessionType::End));
assert_eq!(s.to_string(), "!Int.?Bool.end");
assert_eq!(SessionType::rec("X", SessionType::var("X")).to_string(), "rec X.X");
}
#[test]
fn dual_preserves_credit_index() {
let s = SessionType::send_credit("Msg", 7, SessionType::End);
assert_eq!(s.dual(), SessionType::recv_credit("Msg", 7, SessionType::End));
assert!(s.dual().dual().equiv(&s));
}
#[test]
fn equality_distinguishes_credit_index() {
let a = SessionType::send_credit("T", 1, SessionType::End);
let b = SessionType::send_credit("T", 2, SessionType::End);
assert!(!a.equiv(&b));
let unbounded = SessionType::send("T", SessionType::End);
assert!(!a.equiv(&unbounded));
}
#[test]
fn with_credit_stamps_every_send_and_recv() {
let bare = SessionType::send(
"A",
SessionType::recv("B", SessionType::send("C", SessionType::End)),
);
let stamped = bare.with_credit(4);
let expected = SessionType::send_credit(
"A",
4,
SessionType::recv_credit("B", 4, SessionType::send_credit("C", 4, SessionType::End)),
);
assert_eq!(stamped, expected);
assert_eq!(stamped.with_credit(4), stamped);
}
#[test]
fn has_send_at_zero_finds_the_unprovable_send() {
let bad = SessionType::recv(
"Q",
SessionType::send_credit("Boom", 0, SessionType::End),
);
assert_eq!(bad.has_send_at_zero(), Some(Payload::new("Boom")));
let ok = SessionType::send_credit("A", 3, SessionType::End);
assert_eq!(ok.has_send_at_zero(), None);
let choice = sel(&[("ask", SessionType::send_credit("X", 0, SessionType::End))]);
assert_eq!(choice.has_send_at_zero(), Some(Payload::new("X")));
}
#[test]
fn credit_analyse_accepts_a_straight_line_protocol_within_budget() {
let s = SessionType::send("A", SessionType::send("B", SessionType::End));
assert!(s.credit_analyse(2).is_ok());
}
#[test]
fn credit_analyse_rejects_burst_overflow() {
let s = SessionType::send(
"A",
SessionType::send("B", SessionType::send("C", SessionType::End)),
);
match s.credit_analyse(2) {
Err(CreditError::BurstOverflow { payload, budget: 2, .. }) => {
assert_eq!(payload, Payload::new("C"));
}
other => panic!("expected BurstOverflow, got {other:?}"),
}
}
#[test]
fn credit_analyse_rejects_explicit_send_at_zero() {
let s = SessionType::send_credit("X", 0, SessionType::End);
match s.credit_analyse(8) {
Err(CreditError::SendAtZero { payload }) => assert_eq!(payload, Payload::new("X")),
other => panic!("expected SendAtZero, got {other:?}"),
}
}
#[test]
fn credit_analyse_rejects_unsustainable_loop() {
let s = SessionType::rec(
"X",
SessionType::send(
"A",
SessionType::send("B", SessionType::recv("Ack", SessionType::var("X"))),
),
);
match s.credit_analyse(100) {
Err(CreditError::LoopUnsustainable { sends_per_iter: 2, recvs_per_iter: 1 }) => {}
other => panic!("expected LoopUnsustainable(2,1), got {other:?}"),
}
}
#[test]
fn credit_analyse_accepts_a_balanced_loop() {
let s = SessionType::rec(
"X",
SessionType::send("A", SessionType::recv("Ack", SessionType::var("X"))),
);
assert!(s.credit_analyse(1).is_ok());
assert!(s.credit_analyse(8).is_ok());
}
#[test]
fn credit_analyse_walks_choice_arms_worst_case() {
let s = sel(&[
("ask", SessionType::send("A", SessionType::send("B", SessionType::End))),
("quit", SessionType::End),
]);
assert!(s.credit_analyse(2).is_ok()); assert!(matches!(
s.credit_analyse(1),
Err(CreditError::BurstOverflow { .. })
));
}
#[test]
fn credit_delta_counts_per_iteration() {
let body = SessionType::send(
"A",
SessionType::send("B", SessionType::recv("Ack", SessionType::var("X"))),
);
assert_eq!(body.credit_delta("X"), (2, 1));
let non_recurring = SessionType::send("A", SessionType::End);
assert_eq!(non_recurring.credit_delta("X"), (0, 0));
let body_chat = sel(&[
(
"ask",
SessionType::send("U", SessionType::recv("Tok", SessionType::var("X"))),
),
("cancel", SessionType::End),
]);
assert_eq!(body_chat.credit_delta("X"), (1, 1));
}
#[test]
fn pure_send_chain_is_in_the_sse_producer_fragment() {
let s = SessionType::send("A", SessionType::send("B", SessionType::End));
assert!(s.projects_to_sse());
assert!(s.dual().projects_to_sse_consumer());
}
#[test]
fn any_recv_disqualifies_the_producer_fragment() {
let s = SessionType::send("Q", SessionType::recv("Ack", SessionType::End));
assert!(!s.projects_to_sse());
assert!(!s.dual().projects_to_sse_consumer());
}
#[test]
fn branch_disqualifies_the_producer_fragment() {
let s = SessionType::branch([("ack".into(), SessionType::End)]);
assert!(!s.projects_to_sse());
assert!(s.dual().projects_to_sse());
}
#[test]
fn select_is_in_the_producer_fragment_iff_all_arms_are() {
let ok = sel(&[
("a", SessionType::send("A", SessionType::End)),
("b", SessionType::End),
]);
assert!(ok.projects_to_sse());
let bad = sel(&[
("a", SessionType::send("A", SessionType::End)),
("b", SessionType::recv("Q", SessionType::End)),
]);
assert!(!bad.projects_to_sse());
}
#[test]
fn recursive_sse_token_stream_is_in_the_producer_fragment() {
let s = SessionType::rec(
"X",
SessionType::send("Token", SessionType::var("X")),
);
assert!(s.projects_to_sse());
assert!(s.dual().projects_to_sse_consumer());
}
#[test]
fn the_two_polarities_partition_the_sse_projectable_space() {
let samples_producer: Vec<SessionType> = vec![
SessionType::End,
SessionType::send("A", SessionType::End),
sel(&[("x", SessionType::End), ("y", SessionType::send("T", SessionType::End))]),
SessionType::rec("X", SessionType::send("T", SessionType::var("X"))),
];
for s in samples_producer {
assert!(s.projects_to_sse(), "{s} should project to SSE producer");
assert!(s.dual().projects_to_sse_consumer(), "{s}⊥ should project to SSE consumer");
}
let samples_non_sse: Vec<SessionType> = vec![
SessionType::recv("A", SessionType::send("B", SessionType::End)),
SessionType::send("A", SessionType::recv("B", SessionType::End)),
brn(&[("x", SessionType::End)]),
];
for s in samples_non_sse {
assert!(!s.projects_to_sse(), "{s} should NOT project to SSE producer");
}
}
#[test]
fn polarity_flip_is_an_involution() {
assert_eq!(Polarity::Producer.flip(), Polarity::Consumer);
assert_eq!(Polarity::Consumer.flip(), Polarity::Producer);
for p in [Polarity::Producer, Polarity::Consumer] {
assert_eq!(p.flip().flip(), p);
}
}
#[test]
fn credit_analyse_is_total_on_realistic_chat_dialogue() {
let client = SessionType::rec(
"X",
sel(&[
(
"ask",
SessionType::send(
"Utterance",
brn(&[
("token", SessionType::recv("Token", SessionType::var("X"))),
("done", SessionType::End),
]),
),
),
("cancel", SessionType::End),
]),
);
assert!(client.credit_analyse(4).is_ok());
assert!(client.dual().credit_analyse(4).is_ok());
}
}