use {
crate::{trace_tree::TraceTree, TraceRecord, Visitor},
colorful::Colorful,
fibril_core::{Command, Event, Id, Step},
std::{
collections::VecDeque,
fmt::Debug,
panic::{catch_unwind, AssertUnwindSafe},
},
vector_clock::VectorClock,
};
pub(crate) struct Actor<M> {
behavior: Box<dyn Step<M>>,
clock: VectorClock,
id: Id,
inbox_by_src: Vec<VecDeque<(M, VectorClock)>>,
#[allow(clippy::type_complexity)]
next_event: fn(&mut Actor<M>) -> Option<(VectorClock, Event<M>)>,
trace_tree: TraceTree<M>,
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub enum RunResult<M> {
Complete,
Incomplete,
Panic {
message: String,
minimal_trace: Vec<TraceRecord<M>>,
},
}
pub struct VerifierConfig<M> {
behaviors: Vec<Box<dyn Step<M>>>,
}
impl<M> VerifierConfig<M> {
fn new(cfg_fn: &impl Fn(&mut VerifierConfig<M>)) -> Self {
let mut cfg = VerifierConfig {
behaviors: Vec::new(),
};
cfg_fn(&mut cfg);
cfg
}
pub fn spawn(&mut self, behavior: impl Step<M> + 'static) -> Id {
let id = self.behaviors.len().into();
self.behaviors.push(Box::new(behavior));
id
}
}
pub struct Verifier<M> {
actors: Vec<Actor<M>>,
#[allow(clippy::type_complexity)]
cfg_fn: Box<dyn Fn(&mut VerifierConfig<M>)>,
next_prefix: Vec<(Id, Event<M>, VectorClock)>,
trace_records: Vec<TraceRecord<M>>,
visitors: Vec<Box<dyn Visitor<M>>>,
}
impl<M> Verifier<M>
where
M: Clone + Debug + PartialEq,
{
pub fn assert_no_panic(&mut self) {
match self.run() {
RunResult::Complete => (),
RunResult::Incomplete => panic!("Too many representative traces."),
RunResult::Panic {
message,
minimal_trace,
} => {
println!("Minimal trace reaching panic:");
let mut i = 1;
for r in &minimal_trace {
println!("\t{i}. {r}");
i += 1;
}
panic!("^ {message}");
}
}
}
pub fn assert_panic(&mut self) -> (String, Vec<TraceRecord<M>>) {
match self.run() {
RunResult::Complete => panic!("Done, but expected an actor to panic."),
RunResult::Incomplete => panic!("Too many representative traces."),
RunResult::Panic {
message,
minimal_trace,
} => (message, minimal_trace),
}
}
fn find_next_reversible_race(&mut self) -> Option<Vec<(Id, Event<M>, VectorClock)>>
where
M: Clone + PartialEq,
{
for (i, ri) in self.trace_records.iter().enumerate().rev() {
let (src_i, _msg_i) = if let Event::RecvOk(src, msg) = &ri.event {
(src, msg)
} else {
continue; };
for (j, rj) in self.trace_records.iter().enumerate().take(i).rev() {
if rj.id != ri.id {
continue; }
let (src_j, _msg_j) = if let Event::RecvOk(src, msg) = &rj.event {
(src, msg)
} else {
continue; };
if ri.event_clock >= rj.clock {
continue; }
if src_i == src_j {
continue; }
if self.trace_records.iter().take(i).skip(j + 1).any(|r| {
if let Event::RecvOk(src_r, _msg_r) = &r.event {
src_i == src_r
} else {
false
}
}) {
continue; }
if self.actors[rj.id].trace_tree.visited(
self.trace_records
.iter()
.take(j)
.filter(|r| r.id == rj.id)
.map(|r| &r.event),
&ri.event,
) {
continue; }
let mut race: Vec<_> = self
.trace_records
.iter()
.take(j)
.map(|r| (r.id, r.event.clone(), r.event_clock.clone()))
.collect();
for r in self.trace_records.iter().skip(j + 1).take(i - j - 1) {
if r.clock <= ri.event_clock {
race.push((r.id, r.event.clone(), r.event_clock.clone()));
}
}
race.push((ri.id, ri.event.clone(), ri.event_clock.clone()));
if std::env::var("FIBRIL_DEBUG").is_ok() {
println!("Trace records with pending reversal:");
for (k, r) in self.trace_records.iter().enumerate() {
let msg = format!(
"{k: >3}{}. {r}",
if k == i {
" (i)"
} else if k == j {
" (j)"
} else {
""
}
);
if k == i || k == j {
println!("{}", msg.color(colorful::Color::Red));
} else {
println!("{msg}");
}
}
println!("Reversal prefix:");
for (k, (pid, event, event_clock)) in race.iter().enumerate() {
let msg = format!(
"{k: >3}{}. {event:?}@{event_clock} → {pid}",
if k == j { " (j)" } else { "" }
);
if k >= j {
println!("{}", msg.color(colorful::Color::Red));
} else {
println!("{msg}");
}
}
}
return Some(race);
}
}
if std::env::var("FIBRIL_DEBUG").is_ok() {
println!("Trace records with no pending reversal:");
for (k, r) in self.trace_records.iter().enumerate() {
println!("{k: >3}. {r}");
}
}
None
}
pub fn new(cfg_fn: impl Fn(&mut VerifierConfig<M>) + 'static) -> Self {
let cfg = VerifierConfig::new(&cfg_fn);
let count = cfg.behaviors.len();
let mut actors = Vec::with_capacity(count);
for (id, behavior) in cfg.behaviors.into_iter().enumerate() {
actors.push(Actor {
behavior,
clock: VectorClock::new_with_len(count),
id: id.into(),
inbox_by_src: vec![VecDeque::new(); count],
next_event: |actor| Some((VectorClock::new(), Event::SpawnOk(actor.id))),
trace_tree: TraceTree::new(),
});
}
Verifier {
actors,
cfg_fn: Box::new(cfg_fn),
next_prefix: Vec::new(),
trace_records: Vec::new(),
visitors: Vec::new(),
}
}
fn next_step(&mut self) -> Option<(Id, VectorClock, Event<M>)> {
for id in 0..self.actors.len() {
let actor = &mut self.actors[id];
if let Some((event_clock, event)) = (actor.next_event)(actor) {
return Some((Id::from(id), event_clock, event));
}
}
None
}
fn reset_actors(&mut self) {
let mut cfg = VerifierConfig::new(&self.cfg_fn);
for (id, behavior) in cfg.behaviors.drain(..).enumerate() {
let actor = &mut self.actors[id];
actor.behavior = behavior;
actor.clock.reset();
for inbox in &mut actor.inbox_by_src {
inbox.clear();
}
actor.next_event = |actor| Some((VectorClock::new(), Event::SpawnOk(actor.id)));
actor.trace_tree.reset_cursor();
}
self.trace_records.clear();
}
pub fn run(&mut self) -> RunResult<M> {
let mut trace_count = 0;
while trace_count < 1024 * 1024 {
trace_count += 1;
if std::env::var("FIBRIL_DEBUG").is_ok() || trace_count % 4096 == 0 {
println!("\n=== Maximal {} ===", trace_count);
}
let prefix = self.next_prefix.drain(..).collect();
if let Err(panic) = catch_unwind(AssertUnwindSafe(|| self.run_until_maximal(prefix))) {
let message = if let Some(panic) = panic.downcast_ref::<&'static str>() {
panic.to_string()
} else if let Some(panic) = panic.downcast_ref::<String>() {
panic.clone()
} else {
"UNKNOWN".to_string()
};
let last_trace_record = self.trace_records.last_mut().unwrap();
last_trace_record.command = Command::Panic(message.clone());
for v in &mut self.visitors {
v.on_maximal(&self.trace_records);
}
let final_clock = &self.trace_records.last().unwrap().clock;
return RunResult::Panic {
message,
minimal_trace: self
.trace_records
.iter()
.filter(|r| &r.clock <= final_clock) .cloned()
.collect(),
};
}
self.next_prefix = match self.find_next_reversible_race() {
None => return RunResult::Complete,
Some(race) => race,
};
}
RunResult::Incomplete
}
fn run_until_maximal(&mut self, prefix: Vec<(Id, Event<M>, VectorClock)>) {
self.reset_actors();
for (id, event, event_clock) in prefix {
if let Event::RecvOk(src, expected_msg) = &event {
let actor = &mut self.actors[id];
let inbox = &mut actor.inbox_by_src[*src];
let (m, m_clock) = match inbox.pop_front() {
None => panic!("- Inbox empty. id={id:?}"),
Some(pair) => pair,
};
assert_eq!(expected_msg, &m);
actor.clock.merge_in(&m_clock);
}
self.step(id, event, event_clock);
}
while let Some((id, event_clock, event)) = self.next_step() {
self.step(id, event, event_clock);
}
for v in &mut self.visitors {
v.on_maximal(&self.trace_records);
}
}
fn step(&mut self, id: Id, event: Event<M>, event_clock: VectorClock) {
let actors = &mut self.actors;
let actor = &mut actors[id];
actor.clock.increment(id.into());
self.trace_records.push(TraceRecord {
event: event.clone(),
event_clock,
id,
command: Command::Panic("PLACEHOLDER".to_string()),
clock: actor.clock.clone(),
});
let record = self.trace_records.last_mut().unwrap();
let command = actor.behavior.step(event);
actors[id].next_event = match &command {
Command::Exit => |_| None,
Command::Panic(_) => |_| None,
Command::Recv => |actor| {
for (src, inbox) in actor.inbox_by_src.iter_mut().enumerate() {
let (m, m_clock) = match inbox.pop_front() {
None => continue,
Some(pair) => pair,
};
actor.clock.merge_in(&m_clock);
return Some((m_clock, Event::RecvOk(src.into(), m)));
}
None
},
Command::Send(dst, m) => {
let m_clock = actor.clock.clone();
actors[*dst].inbox_by_src[id].push_back((m.clone(), m_clock));
|_| Some((VectorClock::new(), Event::SendOk))
}
_ => unimplemented!(),
};
record.command = command;
actors[id].trace_tree.visit(record.clone());
}
pub fn visitor(mut self, visitor: impl Visitor<M> + 'static) -> Self {
self.visitors.push(Box::new(visitor));
self
}
}
#[cfg(test)]
mod test {
use {
super::*,
crate::{assert_trace, TraceRecordingVisitor},
fibril::Fiber,
};
#[test]
fn does_not_reverse_dependent_recv_ok() {
let (record, replay) = TraceRecordingVisitor::new_with_replay();
let mut verifier = Verifier::new(|cfg| {
let server = cfg.spawn(Fiber::new(|sdk| {
sdk.recv();
sdk.send(sdk.id(), "FROM SERVER");
sdk.recv();
}));
cfg.spawn(Fiber::new(move |sdk| {
sdk.send(server, "FROM CLIENT");
}));
})
.visitor(record);
verifier.assert_no_panic();
let traces = replay();
assert_eq!(traces.len(), 1);
assert_trace![
traces[0],
"SpawnOk(:0)@<> → :0 → Recv@<1 0>",
"SpawnOk(:1)@<> → :1 → Send(:0, \"FROM CLIENT\")@<0 1>",
"RecvOk(:1, \"FROM CLIENT\")@<0 1> → :0 → Send(:0, \"FROM SERVER\")@<2 1>",
"SendOk@<> → :0 → Recv@<3 1>",
"RecvOk(:0, \"FROM SERVER\")@<2 1> → :0 → Exit@<4 1>",
"SendOk@<> → :1 → Exit@<0 2>",
];
}
#[test]
fn reverses_recv_ok_enabled_before_earlier_racing_step_clock() {
let (record, replay) = TraceRecordingVisitor::new_with_replay();
let mut verifier = Verifier::new(|cfg| {
let registry = cfg.spawn(Fiber::new(|sdk| {
sdk.recv();
sdk.recv();
sdk.recv();
}));
let worker1 = cfg.spawn(Fiber::new(move |sdk| {
sdk.recv();
sdk.send(registry, "W1");
}));
let worker2 = cfg.spawn(Fiber::new(move |sdk| {
sdk.recv();
sdk.send(registry, "W2");
}));
cfg.spawn(Fiber::new(move |sdk| {
sdk.send(registry, "CLIENT");
sdk.send(worker1, "GO");
sdk.send(worker2, "GO");
}));
})
.visitor(record);
verifier.assert_no_panic();
let traces = replay();
assert_eq!(traces.len(), 6);
assert_trace![
traces[0],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0 0>",
"SpawnOk(:1)@<> → :1 → Recv@<0 1 0 0>",
"SpawnOk(:2)@<> → :2 → Recv@<0 0 1 0>",
"SpawnOk(:3)@<> → :3 → Send(:0, \"CLIENT\")@<0 0 0 1>",
"RecvOk(:3, \"CLIENT\")@<0 0 0 1> → :0 → Recv@<2 0 0 1>",
"SendOk@<> → :3 → Send(:1, \"GO\")@<0 0 0 2>",
"RecvOk(:3, \"GO\")@<0 0 0 2> → :1 → Send(:0, \"W1\")@<0 2 0 2>",
"RecvOk(:1, \"W1\")@<0 2 0 2> → :0 → Recv@<3 2 0 2>",
"SendOk@<> → :1 → Exit@<0 3 0 2>",
"SendOk@<> → :3 → Send(:2, \"GO\")@<0 0 0 3>",
"RecvOk(:3, \"GO\")@<0 0 0 3> → :2 → Send(:0, \"W2\")@<0 0 2 3>",
"RecvOk(:2, \"W2\")@<0 0 2 3> → :0 → Exit@<4 2 2 3>",
"SendOk@<> → :2 → Exit@<0 0 3 3>",
"SendOk@<> → :3 → Exit@<0 0 0 4>",
];
assert_trace![
traces[1],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0 0>",
"SpawnOk(:1)@<> → :1 → Recv@<0 1 0 0>",
"SpawnOk(:2)@<> → :2 → Recv@<0 0 1 0>",
"SpawnOk(:3)@<> → :3 → Send(:0, \"CLIENT\")@<0 0 0 1>",
"RecvOk(:3, \"CLIENT\")@<0 0 0 1> → :0 → Recv@<2 0 0 1>",
"SendOk@<> → :3 → Send(:1, \"GO\")@<0 0 0 2>",
"RecvOk(:3, \"GO\")@<0 0 0 2> → :1 → Send(:0, \"W1\")@<0 2 0 2>",
"SendOk@<> → :3 → Send(:2, \"GO\")@<0 0 0 3>",
"RecvOk(:3, \"GO\")@<0 0 0 3> → :2 → Send(:0, \"W2\")@<0 0 2 3>",
"RecvOk(:2, \"W2\")@<0 0 2 3> → :0 → Recv@<3 0 2 3>",
"RecvOk(:1, \"W1\")@<0 2 0 2> → :0 → Exit@<4 2 2 3>",
"SendOk@<> → :1 → Exit@<0 3 0 2>",
"SendOk@<> → :2 → Exit@<0 0 3 3>",
"SendOk@<> → :3 → Exit@<0 0 0 4>",
];
assert_trace![
traces[2],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0 0>",
"SpawnOk(:1)@<> → :1 → Recv@<0 1 0 0>",
"SpawnOk(:2)@<> → :2 → Recv@<0 0 1 0>",
"SpawnOk(:3)@<> → :3 → Send(:0, \"CLIENT\")@<0 0 0 1>",
"SendOk@<> → :3 → Send(:1, \"GO\")@<0 0 0 2>",
"RecvOk(:3, \"GO\")@<0 0 0 2> → :1 → Send(:0, \"W1\")@<0 2 0 2>",
"RecvOk(:1, \"W1\")@<0 2 0 2> → :0 → Recv@<2 2 0 2>",
"RecvOk(:3, \"CLIENT\")@<0 0 0 1> → :0 → Recv@<3 2 0 2>",
"SendOk@<> → :1 → Exit@<0 3 0 2>",
"SendOk@<> → :3 → Send(:2, \"GO\")@<0 0 0 3>",
"RecvOk(:3, \"GO\")@<0 0 0 3> → :2 → Send(:0, \"W2\")@<0 0 2 3>",
"RecvOk(:2, \"W2\")@<0 0 2 3> → :0 → Exit@<4 2 2 3>",
"SendOk@<> → :2 → Exit@<0 0 3 3>",
"SendOk@<> → :3 → Exit@<0 0 0 4>",
];
assert_trace![
traces[3],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0 0>",
"SpawnOk(:1)@<> → :1 → Recv@<0 1 0 0>",
"SpawnOk(:2)@<> → :2 → Recv@<0 0 1 0>",
"SpawnOk(:3)@<> → :3 → Send(:0, \"CLIENT\")@<0 0 0 1>",
"SendOk@<> → :3 → Send(:1, \"GO\")@<0 0 0 2>",
"RecvOk(:3, \"GO\")@<0 0 0 2> → :1 → Send(:0, \"W1\")@<0 2 0 2>",
"RecvOk(:1, \"W1\")@<0 2 0 2> → :0 → Recv@<2 2 0 2>",
"SendOk@<> → :3 → Send(:2, \"GO\")@<0 0 0 3>",
"RecvOk(:3, \"GO\")@<0 0 0 3> → :2 → Send(:0, \"W2\")@<0 0 2 3>",
"RecvOk(:2, \"W2\")@<0 0 2 3> → :0 → Recv@<3 2 2 3>",
"RecvOk(:3, \"CLIENT\")@<0 0 0 1> → :0 → Exit@<4 2 2 3>",
"SendOk@<> → :1 → Exit@<0 3 0 2>",
"SendOk@<> → :2 → Exit@<0 0 3 3>",
"SendOk@<> → :3 → Exit@<0 0 0 4>",
];
assert_trace![
traces[4],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0 0>",
"SpawnOk(:1)@<> → :1 → Recv@<0 1 0 0>",
"SpawnOk(:2)@<> → :2 → Recv@<0 0 1 0>",
"SpawnOk(:3)@<> → :3 → Send(:0, \"CLIENT\")@<0 0 0 1>",
"SendOk@<> → :3 → Send(:1, \"GO\")@<0 0 0 2>",
"RecvOk(:3, \"GO\")@<0 0 0 2> → :1 → Send(:0, \"W1\")@<0 2 0 2>",
"SendOk@<> → :3 → Send(:2, \"GO\")@<0 0 0 3>",
"RecvOk(:3, \"GO\")@<0 0 0 3> → :2 → Send(:0, \"W2\")@<0 0 2 3>",
"RecvOk(:2, \"W2\")@<0 0 2 3> → :0 → Recv@<2 0 2 3>",
"RecvOk(:1, \"W1\")@<0 2 0 2> → :0 → Recv@<3 2 2 3>",
"RecvOk(:3, \"CLIENT\")@<0 0 0 1> → :0 → Exit@<4 2 2 3>",
"SendOk@<> → :1 → Exit@<0 3 0 2>",
"SendOk@<> → :2 → Exit@<0 0 3 3>",
"SendOk@<> → :3 → Exit@<0 0 0 4>",
];
assert_trace![
traces[5],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0 0>",
"SpawnOk(:1)@<> → :1 → Recv@<0 1 0 0>",
"SpawnOk(:2)@<> → :2 → Recv@<0 0 1 0>",
"SpawnOk(:3)@<> → :3 → Send(:0, \"CLIENT\")@<0 0 0 1>",
"SendOk@<> → :3 → Send(:1, \"GO\")@<0 0 0 2>",
"RecvOk(:3, \"GO\")@<0 0 0 2> → :1 → Send(:0, \"W1\")@<0 2 0 2>",
"SendOk@<> → :3 → Send(:2, \"GO\")@<0 0 0 3>",
"RecvOk(:3, \"GO\")@<0 0 0 3> → :2 → Send(:0, \"W2\")@<0 0 2 3>",
"RecvOk(:2, \"W2\")@<0 0 2 3> → :0 → Recv@<2 0 2 3>",
"RecvOk(:3, \"CLIENT\")@<0 0 0 1> → :0 → Recv@<3 0 2 3>",
"RecvOk(:1, \"W1\")@<0 2 0 2> → :0 → Exit@<4 2 2 3>",
"SendOk@<> → :1 → Exit@<0 3 0 2>",
"SendOk@<> → :2 → Exit@<0 0 3 3>",
"SendOk@<> → :3 → Exit@<0 0 0 4>",
];
}
#[test]
fn respects_network_queuing_from_self_send() {
let (record, replay) = TraceRecordingVisitor::new_with_replay();
let mut verifier = Verifier::new(|cfg| {
cfg.spawn(Fiber::new(|sdk| {
sdk.send(sdk.id(), "FIRST IN LINE");
sdk.send(sdk.id(), "SECOND IN LINE");
sdk.recv();
sdk.recv();
}));
})
.visitor(record);
verifier.assert_no_panic();
let traces = replay();
assert_eq!(traces.len(), 1); assert_trace![
traces[0],
"SpawnOk(:0)@<> → :0 → Send(:0, \"FIRST IN LINE\")@<1>",
"SendOk@<> → :0 → Send(:0, \"SECOND IN LINE\")@<2>",
"SendOk@<> → :0 → Recv@<3>",
"RecvOk(:0, \"FIRST IN LINE\")@<1> → :0 → Recv@<4>",
"RecvOk(:0, \"SECOND IN LINE\")@<2> → :0 → Exit@<5>",
];
}
#[test]
fn respects_network_queuing_from_client_send() {
let (record, replay) = TraceRecordingVisitor::new_with_replay();
let mut verifier = Verifier::new(|cfg| {
let server = cfg.spawn(Fiber::new(|sdk| {
sdk.recv();
sdk.recv();
sdk.recv();
}));
cfg.spawn(Fiber::new(move |sdk| {
sdk.send(server, "FIRST IN LINE FROM C1");
sdk.send(server, "SECOND IN LINE FROM C1");
}));
cfg.spawn(Fiber::new(move |sdk| {
sdk.send(server, "CONCURRENT FROM C2");
}));
})
.visitor(record);
verifier.assert_no_panic();
let traces = replay();
assert_eq!(traces.len(), 3);
assert_trace![
traces[0],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0>",
"SpawnOk(:1)@<> → :1 → Send(:0, \"FIRST IN LINE FROM C1\")@<0 1 0>",
"RecvOk(:1, \"FIRST IN LINE FROM C1\")@<0 1 0> → :0 → Recv@<2 1 0>",
"SendOk@<> → :1 → Send(:0, \"SECOND IN LINE FROM C1\")@<0 2 0>",
"RecvOk(:1, \"SECOND IN LINE FROM C1\")@<0 2 0> → :0 → Recv@<3 2 0>",
"SendOk@<> → :1 → Exit@<0 3 0>",
"SpawnOk(:2)@<> → :2 → Send(:0, \"CONCURRENT FROM C2\")@<0 0 1>",
"RecvOk(:2, \"CONCURRENT FROM C2\")@<0 0 1> → :0 → Exit@<4 2 1>",
"SendOk@<> → :2 → Exit@<0 0 2>",
];
assert_trace![
traces[1],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0>",
"SpawnOk(:1)@<> → :1 → Send(:0, \"FIRST IN LINE FROM C1\")@<0 1 0>",
"RecvOk(:1, \"FIRST IN LINE FROM C1\")@<0 1 0> → :0 → Recv@<2 1 0>",
"SendOk@<> → :1 → Send(:0, \"SECOND IN LINE FROM C1\")@<0 2 0>",
"SpawnOk(:2)@<> → :2 → Send(:0, \"CONCURRENT FROM C2\")@<0 0 1>",
"RecvOk(:2, \"CONCURRENT FROM C2\")@<0 0 1> → :0 → Recv@<3 1 1>",
"RecvOk(:1, \"SECOND IN LINE FROM C1\")@<0 2 0> → :0 → Exit@<4 2 1>",
"SendOk@<> → :1 → Exit@<0 3 0>",
"SendOk@<> → :2 → Exit@<0 0 2>",
];
assert_trace![
traces[2],
"SpawnOk(:0)@<> → :0 → Recv@<1 0 0>",
"SpawnOk(:1)@<> → :1 → Send(:0, \"FIRST IN LINE FROM C1\")@<0 1 0>",
"SpawnOk(:2)@<> → :2 → Send(:0, \"CONCURRENT FROM C2\")@<0 0 1>",
"RecvOk(:2, \"CONCURRENT FROM C2\")@<0 0 1> → :0 → Recv@<2 0 1>",
"RecvOk(:1, \"FIRST IN LINE FROM C1\")@<0 1 0> → :0 → Recv@<3 1 1>",
"SendOk@<> → :1 → Send(:0, \"SECOND IN LINE FROM C1\")@<0 2 0>",
"RecvOk(:1, \"SECOND IN LINE FROM C1\")@<0 2 0> → :0 → Exit@<4 2 1>",
"SendOk@<> → :1 → Exit@<0 3 0>",
"SendOk@<> → :2 → Exit@<0 0 2>",
];
}
}