typed-protocols-1.0.0.0: src/Network/TypedProtocol/Proofs.hs
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE TypeFamilies #-}
-- This is already implied by the -Wall in the .cabal file, but lets just be
-- completely explicit about it too, since we rely on the completeness
-- checking in the cases below for the completeness of our proofs.
{-# OPTIONS_GHC -Wincomplete-patterns #-}
-- | Proofs and helpful testing utilities.
--
module Network.TypedProtocol.Proofs
( -- * Connect proofs
connect
, connectPipelined
, TerminalStates (..)
-- * Pipelining proofs
-- | Additional proofs specific to the pipelining features
, forgetPipelined
, promoteToPipelined
-- ** Pipeline proof helpers
, Queue (..)
, enqueue
-- ** Auxiliary functions
, pipelineInterleaving
) where
import Data.Singletons
import Network.TypedProtocol.Core
import Network.TypedProtocol.Lemmas
import Network.TypedProtocol.Peer
-- | The 'connect' function takes two peers that agree on a protocol and runs
-- them in lock step, until (and if) they complete.
--
-- The 'connect' function serves a few purposes.
--
-- * The fact we can define this function at at all proves some minimal
-- sanity property of the typed protocol framework.
--
-- * It demonstrates that all protocols defined in the framework can be run
-- with synchronous communication rather than requiring buffered communication.
--
-- * It is useful for testing peer implementations against each other in a
-- minimalistic setting.
--
connect
:: forall ps (pr :: PeerRole) (initSt :: ps) m a b.
(Monad m, SingI pr)
=> Peer ps pr NonPipelined initSt m a
-- ^ a peer
-> Peer ps (FlipAgency pr) NonPipelined initSt m b
-- ^ a peer with flipped agency
-> m (a, b, TerminalStates ps)
-- ^ peers results and an evidence of their termination
connect = go
where
singPeerRole :: Sing pr
singPeerRole = sing
go :: forall (st :: ps).
Peer ps pr NonPipelined st m a
-> Peer ps (FlipAgency pr) NonPipelined st m b
-> m (a, b, TerminalStates ps)
go (Done ReflNobodyAgency a) (Done ReflNobodyAgency b) =
return (a, b, terminals)
where
terminals :: TerminalStates ps
terminals = TerminalStates (stateToken :: StateToken st)
(stateToken :: StateToken st)
go (Effect a ) b = a >>= \a' -> go a' b
go a (Effect b) = b >>= \b' -> go a b'
go (Yield _ msg a) (Await _ b) = go a (b msg)
go (Await _ a) (Yield _ msg b) = go (a msg) b
-- By appealing to the proofs about agency for this protocol we can
-- show that these other cases are impossible
go (Yield reflA _ _) (Yield reflB _ _) =
case exclusionLemma_ClientAndServerHaveAgency singPeerRole reflA reflB of
ReflNobodyHasAgency -> case reflA of {}
go (Await reflA _) (Await reflB _) =
case exclusionLemma_ClientAndServerHaveAgency singPeerRole reflA reflB of
ReflNobodyHasAgency -> case reflA of {}
go (Done reflA _) (Yield reflB _ _) =
case terminationLemma_2 singPeerRole reflB reflA of
ReflNobodyHasAgency -> case reflB of {}
go (Done reflA _) (Await reflB _) =
case terminationLemma_2 singPeerRole reflB reflA of
ReflNobodyHasAgency -> case reflB of {}
go (Yield reflA _ _) (Done reflB _) =
case terminationLemma_1 singPeerRole reflA reflB of
ReflNobodyHasAgency -> case reflA of {}
go (Await reflA _) (Done reflB _) =
case terminationLemma_1 singPeerRole reflA reflB of
ReflNobodyHasAgency -> case reflA of {}
-- | The terminal states for the protocol. Used in 'connect' and
-- 'connectPipelined' to return the states in which the peers terminated.
--
data TerminalStates ps where
TerminalStates
:: forall ps (st :: ps).
(StateAgency st ~ NobodyAgency)
=> StateToken st
-- ^ state termination evidence for the first peer
-> StateToken st
-- ^ state termination evidence for the second peer
-> TerminalStates ps
--
-- Remove Pipelining
--
-- | A size indexed queue. This is useful for proofs, including
-- 'connectPipelined' but also as so-called @direct@ functions for running a
-- client and server wrapper directly against each other.
--
data Queue (n :: N) a where
EmptyQ :: Queue Z a
ConsQ :: a -> Queue n a -> Queue (S n) a
-- | At an element to the end of a 'Queue'. This is not intended to be
-- efficient. It is only for proofs and tests.
--
enqueue :: a -> Queue n a -> Queue (S n) a
enqueue a EmptyQ = ConsQ a EmptyQ
enqueue a (ConsQ b q) = ConsQ b (enqueue a q)
-- | Proof that we have a total conversion from pipelined peers to regular
-- peers. This is a sanity property that shows that pipelining did not give
-- us extra expressiveness or to break the protocol state machine.
--
forgetPipelined
:: forall ps (pr :: PeerRole) (st :: ps) m a.
Functor m
=> [Bool]
-- ^ interleaving choices for pipelining allowed by `Collect` primitive. False
-- values or `[]` give no pipelining.
-> PeerPipelined ps pr st m a
-> Peer ps pr NonPipelined st m a
forgetPipelined cs0 (PeerPipelined peer) = goSender EmptyQ cs0 peer
where
goSender :: forall st' n c.
Queue n c
-> [Bool]
-> Peer ps pr ('Pipelined n c) st' m a
-> Peer ps pr 'NonPipelined st' m a
goSender EmptyQ _cs (Done refl k) = Done refl k
goSender q cs (Effect k) = Effect (goSender q cs <$> k)
goSender q cs (Yield refl m k) = Yield refl m (goSender q cs k)
goSender q cs (Await refl k) = Await refl (goSender q cs <$> k)
goSender q cs (YieldPipelined refl m r k) = Yield refl m (goReceiver q cs k r)
goSender q (True:cs') (Collect (Just k) _) = goSender q cs' k
goSender (ConsQ x q) (_:cs) (Collect _ k) = goSender q cs (k x)
goSender (ConsQ x q) cs@[] (Collect _ k) = goSender q cs (k x)
goReceiver :: forall stCurrent stNext n c.
Queue n c
-> [Bool]
-> Peer ps pr ('Pipelined (S n) c) stNext m a
-> Receiver ps pr stCurrent stNext m c
-> Peer ps pr 'NonPipelined stCurrent m a
goReceiver q cs s (ReceiverDone x) = goSender (enqueue x q) cs s
goReceiver q cs s (ReceiverEffect k) = Effect (goReceiver q cs s <$> k)
goReceiver q cs s (ReceiverAwait refl k) = Await refl (goReceiver q cs s . k)
-- | Promote a peer to a pipelined one.
--
-- This is a right inverse of `forgetPipelined`, e.g.
--
-- >>> forgetPipelined . promoteToPipelined = id
--
promoteToPipelined
:: forall ps (pr :: PeerRole) st m a.
Functor m
=> Peer ps pr NonPipelined st m a
-- ^ a peer
-> PeerPipelined ps pr st m a
-- ^ a pipelined peer
promoteToPipelined p = PeerPipelined (go p)
where
go :: forall st' c.
Peer ps pr NonPipelined st' m a
-> Peer ps pr (Pipelined Z c) st' m a
go (Effect k) = Effect $ go <$> k
go (Yield refl msg k) = Yield refl msg (go k)
go (Await refl k) = Await refl (go . k)
go (Done refl k) = Done refl k
-- | Analogous to 'connect' but also for pipelined peers.
--
-- Since pipelining allows multiple possible interleavings, we provide a
-- @[Bool]@ parameter to control the choices. Each @True@ will trigger picking
-- the first choice in the @SenderCollect@ construct (if possible), leading
-- to more results outstanding. This can also be interpreted as a greater
-- pipeline depth, or more messages in-flight.
--
-- This can be exercised using a QuickCheck style generator.
--
connectPipelined
:: forall ps (pr :: PeerRole)
(st :: ps) m a b.
(Monad m, SingI pr)
=> [Bool]
-- ^ an interleaving
-> PeerPipelined ps pr st m a
-- ^ a pipelined peer
-> Peer ps (FlipAgency pr) NonPipelined st m b
-- ^ a non-pipelined peer with fliped agency
-> m (a, b, TerminalStates ps)
-- ^ peers results and an evidence of their termination
connectPipelined csA a b =
connect (forgetPipelined csA a) b
-- | A reference specification for interleaving of requests and responses
-- with pipelining, where the environment can choose whether a response is
-- available yet.
--
-- This also supports bounded choice where the maximum number of outstanding
-- in-flight responses is limited.
--
pipelineInterleaving :: Int -- ^ Bound on outstanding responses
-> [Bool] -- ^ Pipelining choices
-> [req] -> [resp] -> [Either req resp]
pipelineInterleaving omax cs0 reqs0 resps0 =
go 0 cs0 (zip [0 :: Int ..] reqs0)
(zip [0 :: Int ..] resps0)
where
go o (c:cs) reqs@((reqNo, req) :reqs')
resps@((respNo,resp):resps')
| respNo == reqNo = Left req : go (o+1) (c:cs) reqs' resps
| c && o < omax = Left req : go (o+1) cs reqs' resps
| otherwise = Right resp : go (o-1) cs reqs resps'
go o [] reqs@((reqNo, req) :reqs')
resps@((respNo,resp):resps')
| respNo == reqNo = Left req : go (o+1) [] reqs' resps
| otherwise = Right resp : go (o-1) [] reqs resps'
go _ _ [] resps = map (Right . snd) resps
go _ _ (_:_) [] = error "pipelineInterleaving: not enough responses"