Dependent Session Protocols in Separation Logic from First Principles (Functional Pearl)

We develop an account of dependent session protocols in concurrent separation logic for a functional language with message-passing. Inspired by minimalistic session calculi, we present a layered design: starting from mutable references, we build one-shot channels, session channels, and imperative channels. Whereas previous work on dependent session protocols in concurrent separation logic required advanced mechanisms such as recursive domain equations and higher-order ghost state, we only require the most basic mechanisms to verify that our one-shot channels satisfy one-shot protocols, and subsequently treat their specification as a black box on top of which we define dependent session protocols. This has a number of advantages in terms of simplicity, elegance, and flexibility: support for subprotocols and guarded recursion automatically transfers from the one-shot protocols to the dependent session protocols, and we easily obtain various forms of channel closing. Because the meta theory of our results is so simple, we are able to give all definitions as part of this paper, and mechanize all our results using the Iris framework in less than 1000 lines of Coq.


INTRODUCTION
Message passing is a commonly used abstraction for concurrent programming, with languages such as Erlang and Go having native support for it, and languages such as Java, Scala, Rust, and C# having library support.Session types offer powerful type systems for message passing concurrency [Honda 1993;Honda et al. 1998], and have been extended with a number of exciting features: (1) Dependent protocols: The key ingredient of a session type system is the notion of a session protocol, which describes what data should be exchanged.For example, the session protocol !Z.!Z.?B.end expresses that two integers are sent, after which a Boolean is received, and the channel is closed.In vanilla session types, protocols were meant to specify the types of the exchanged data.They cannot be used to express that the right values are exchanged (i.e., functional correctness), nor to express data-dependent protocols where the remaining protocol can depend on prior messages.
There have been two lines of work to extend session protocols with logical conditions to remedy this shortcoming.Bocchi et al. [2010]; Toninho et al. [2011]; Zhou et al. [2020]; Thiemann and Vasconcelos [2020] develop type systems that combine concepts from the theory of dependent and refinement types with session types.Lozes and Villard [2012]; Craciun et al. [2015]; Hinrichsen et al. [2020] develop program logics that combine concurrent separation logic [O'Hearn 2004;Brookes 2004] with concepts from session types.Separation logic (instead of a type system) is used to enforce affine use of a channel library, and Hoare triple specifications (instead of typing rules) are provided for channel operations.
(2) Integration in functional languages: While session types were originally developed in the context of -calculus, a tempting direction is to combine session types with functional programming.In such languages, session-typed channels are considered first-class data, and can be stored in data types and sent over channels (similar to first-class mutable references in ML).The GV family by Gay and Vasconcelos [2010]; Wadler [2012] extends linear lambdacalculus with channels.The SILL family by Toninho et al. [2013]; Pfenning and Griffith [2015]; Toninho [2015] uses a monadic embedding of session types into an unrestricted language.
(3) Session channels as a library: Session types are typically a language feature, but a recent trend is to embed channels with session types as a library in an existing language [Hu et al. 2008;Scalas and Yoshida 2016;Pucella and Tov 2008].Often, either the host language or the encoding supports substructural types, to enforce the affine use of session channels [Kokke and Dardha 2021;Lindley and Morris 2016;Jespersen et al. 2015;Chen et al. 2022].(4) Minimalistic calculi: Session-typed languages add a large number of additional constructs to the types and expressions of their base languages.Already in the early days of session types, Kobayashi [2002] showed that session types can be encoded into -types; an approach formalized by Dardha et al. [2012Dardha et al. [ , 2017]], and applied to GV-style languages by Jacobs [2022].
To our knowledge, there is no prior work that combines all five features under a single roof.The goal of this functional pearl is thus to do exactly that.We will develop an account of dependent session protocols for a GV-style language in a concurrent separation logic.We start from first principles, enabling us to take a minimalistic approach.Our results have been mechanized in the Coq proof assistant using the Iris framework for concurrent separation logic [Jung et al. 2015[Jung et al. , 2016;;Krebbers et al. 2017a;Jung et al. 2018;Krebbers et al. 2018Krebbers et al. , 2017b].In the remainder of the introduction, we give a teaser of our approach and list some of our key insights.
Key idea #1: Implicit buffers through one-shot channels.The first step to formalizing a language with message-passing concurrency is to decide on the semantics of channels.A common approach is to use an asynchronous semantics where the sender enqueues the messages in a buffer, from which the receiver dequeues them.In such a semantics, the receive operation can block if no message is present, but the send operation will always succeed immediately.To model the notion of a buffer, one typically incorporates a linked list in the formal definition of the language, and extends the language with operations to send (enqueue) and receive (dequeue) messages.
To be minimalistic, we want to avoid having to explicitly model the notion of a linked list in our semantics.Inspired by Kobayashi [2002]; Dardha et al. [2017]; Jacobs [2022]  of one-shot channels.These come with functions new1 (), which creates a new channel; send1 , which send a message on channel (without blocking); and recv1 , which receives a message from (blocks until a message has been sent).On top of the one-shot channels, we define regular multi-shot session channels.For example, the send operation of session channels is defined as: This operation not only sends the message , but also creates a new channel ′ for the remainder of the communication, and sends the new channel paired with the message.While there is no explicit notion of a buffer or linked-list in the semantics of one-shot channels, nor in the definition of session channels, we will show that the buffer arises implicitly from the preceding definition.
Key idea #2: Dependent session protocols via one-shot protocols.Program logics for message-passing concurrency typically come with a channel points-to connective p, which provides unique ownership of a channel endpoint that has to obey to a protocol p.These protocols typically have a sequenced structure, describing a dependent session of multiple exchanges.An example of a dependent separation protocol in the Actris logic by Hinrichsen et al. [2020Hinrichsen et al. [ , 2022] ] is end.This protocol expresses that two natural numbers ≤ are sent, and the difference − is returned.Similar to our desire for avoiding the need to explicitly model the buffers that underpin channels as linked lists, we would like to avoid having to inductively define such dependent session protocols.In our system, the channel points-to connective for the one-shot channels is simply (tag, ), where tag ∈ {Send, Recv} and is a predicate over the exchanged value.While our protocols only describe a single message, dependent session protocols that can describe session channels are simply defined as combinators.This is achieved by recursively using the channel points-to connective for describing the channel continuation inside the base protocol .Due to Iris's support for impredicativity [Svendsen and Birkedal 2014], we can use its fixpoint combinator to define recursive (and dependent) protocols by guarded recursion.
Key idea #3: Layered session channel library design and verification.We implement session channels in terms of one-shot channels, and our dependent session protocols as combinators of one-shot protocols, but we wish to go further by layering our design-from below and above.The layered design is shown in Fig. 1.
From below, we do not start with a language that has channels as primitive.We build on top of a functional language with mutable references as found in languages of the ML family (with allocation, deallocation, store and load).One-shot channels are implemented on top of primitive mutable references, and verified using (Iris's) separation logic rules for the verification of concurrent programs with mutable shared-memory references.Building on top of a language with mutable references has other tangible benefits.First, we can write and verify programs that transfer data by reference.Second, we can define both functional versions of session channels (that return a new endpoint) and imperative versions of channel endpoints (that mutate the channel).
From above, we demonstrate the flexibility of our solution by implementing multiple methods for closing a session.Session types and protocols are often terminated with an explicit end-tag, and it is non-trivial to extend the range of termination tags in settings where the protocols are defined inductively.Since our session channels are defined as combinators on top of the one-shot channels-that do not inherently include a method for closing-we can freely choose how to close our channels, after the fact.Initially, we implement asymmetric closing, where one endpoint initiates the closing of a channel (protocol !end), while the other waits and actually deallocates the memory backing the channel (protocol ?end).We later provide two alternatives with a self-dual end protocol: symmetrically closing the channel with a the same closing operation on both endpoints, where the last call deallocated the channel, and a combined send-close operation, which sends a last message but does not create a continuation channel.
Key idea #4: Mechanization using a subset of Iris.Our layered design proved beneficial for the meta theory and mechanization of our results.We only need the usual points-to connective ℓ ↦ → for ownership of locations ℓ with value in separation logic, a simple form of ghost state (unique tokens), and Iris's impredicative invariants.By comparison, the Actris logic by Hinrichsen et al. [2020Hinrichsen et al. [ , 2022] ] relies on a non-trivial model of recursive protocols using the technique from America and Rutten [1989] for solving recursive domain equations, and uses Iris's mechanism for higher-order ghost state [Jung et al. 2016] to define its channel points-to connective p.Since the meta theory of our results is so simple, we are able to give all definitions as part of this paper (there is no appendix) and mechanize all our results in less than 1000 lines of Coq.

Contributions. This paper makes the following contributions:
• A layered implementation of higher-order shared-memory session channels, starting from mutable references, on which we build one-shot channels, session channels, and imperative channels ( §2) • A layered development of separation logic specifications for our channels.We start from a small subset of Iris, developing specifications for one-shot channels, which are then treated as a black box upon which we build high-level dependent separation protocols ( §3) • Support for subprotocols ( §3.3) and guarded recursion ( §4), which transfers automatically from one-shot protocols to dependent session protocols.• A demonstration of the extensibility obtained by building on first principles, through various methods for closing session channels ( §5) • A small and intuitive mechanization in the Coq proof assistant, comprised of less than 1000 lines of Coq code ( §7).The paper is annotated with mechanization icons ( ) that link to the relevant Coq code, and a cross-reference sheet is provided ( § A).

LAYERED IMPLEMENTATION OF CHANNELS
In this section we will implement message passing channels in terms of low-level operations.We build these channels in several layers: • We start by describing the base language and its low-level operations ( §2.1).
• We then build a library of one-shot channels ( §2.2).
• On top of this we build functional multi-shot session channels ( §2.3).
• As a final layer, we have imperative session channels ( §2.4).
• We show that linked lists (buffers) implicitly emerge ( §2.5).In the subsequent §3, we develop specifications and proof for each of the layers, and demonstrate how to verify the correctness of the example.

Base Language
We use HeapLang, a low-level concurrent language that comes with the Iris separation logic framework, as our base language.HeapLang has the purely functional operations that one would expect, such as arithmetic and conditionals, and also includes products and sums.For the purpose of this paper, the following operations on mutable memory locations are the most relevant:

ref
Allocate a new memory location that initially stores value .

! ℓ
Read the value from memory location ℓ. ℓ ← Write value to location ℓ. free ℓ Free the memory location ℓ.HeapLang additionally includes a primitive for spawning a new thread: The program is allowed to refer to variables in the surrounding lexical context.The following is a grammar of the most notable constructs that we will use:

One-Shot Channels
At the base of our development lie one-shot channels, which communicate a single message from a sender to a receiver.The API consists of the following operations: new1 () Create and return a new one-shot channel .send1 Send message on channel (non-blocking).recv1 Receive message from channel (blocks until a message is sent).
The channels are one-shot; only one value is sent over the channel, after which point the channel is deallocated as a part of recv1 .
Example of using one-shot channels.These channels enable us to set up a communication between child and parent threads as in the following example: The main thread creates a one-shot channel , which is shared between the main thread and a forked-off thread.The forked-off thread then dynamically allocates a reference to 42, and sends the location over the channel.Finally, the main thread receives the reference, reads it, and asserts that the stored value is 42.To communicate several times, we could share several channels, but an interesting alternative style that allows unbounded communication is to send a new channel along with the message, as we shall see in §2.3.
In the HeapLang semantics, assert gets stuck if the condition is false.Safety (the fact that the assert does not fail) crucially depends on the forked-off thread not modifying the reference after it has sent it.This example is safe as the exclusive permission to write and read the reference first belongs to the forked-off thread, after which it is transferred to the main thread.We verify this safe transfer of ownership in §3.2.This goes beyond standard session types due to reference ownership and the verification of the assert.
Implementation of one-shot channels.In our development, channels are not primitive but implemented in terms of low-level mutable references.A channel is represented as a mutable reference that initially contains the value None.To send a value to the channel, we set the mutable reference to Some .To receive from the channel, we read the value of the mutable reference in a loop, until we see the None change to Some .We then deallocate the mutable reference, and return .This gives us the following implementation: This implementation shows that safety also depends on the fact that clients only call recv1 once, and does not call send1 after a completed recv1.These would otherwise result in a double-free and use-after-free, which get stuck in the HeapLang semantics.
HeapLang has a sequentially consistent memory model.In a weaker memory model, the store/load instructions should use release/acquire memory order options (or stronger).Similar to most literature on Iris-with the exception of papers specifically focused on weak memory [Mével and Jourdan 2021;Kaiser et al. 2017;Dang et al. 2020]-we ignore these concerns.

Session Channels
A session channel facilitates sequences of messages between two channel endpoints, which is useful for implementing client-server style concurrency.
The new function allocates an initial one-shot channel and returns it as the session channel.The send function allocates a new one-shot channel, and sends it along the original channel with the given message , after which the new channel is returned.The recv function receives the value and continuation channel pair using the original one-shot channel receive function.The close function sends a final termination flag, without allocating a new one-shot channel, to terminate the session.The wait function receives the final termination flag, which deallocates the channel.
For session channels to be used safely-i.e., to not cause memory errors such as use-after-free or double-free-it is crucial that channel endpoints are used in a dual way.That is, if there is a send on one endpoint, there should be a matching receive on the other endpoint, and vice versa.Similarly, a close should match up with a wait.We discuss other options for closing channels in §5.
Example of using session channels.An example of using the session channels is as follows: assert(! = 42) Here, the main thread initially creates a session channel , which is shared between the main thread and forked-off 'worker' thread.The main thread dynamically allocates a reference to 40, after which it sends the reference over the channel.The worker thread receives the reference, adds 2 to it, and sends a flag back, to signal that the reference has been updated.The main thread receives the flag and then reads the updated value stored in the reference, and asserts that it is 42.Finally, the main thread sends the closing signal, which is received by the worker thread.Each operation on the channel binds the channel continuation to an overshadowing name , to intuitively capture that they keep working on the same session.
Similar to the example presented in §2.1, this program is safe if the assert succeeds and there are no memory errors due to improper use of the channel API.Intuitively, this example achieves safe access to the reference via ownership delegation over the channel.We verify this in §3.4.

Imperative Channels
Although session channels are more convenient to use than one-shot channels, they still require us to continuously pass around new channel references.On top of session channels we therefore define imperative channels, which have a traditional imperative channel API: new_imp () Create a new imperative channel, and return a pair ( 1 , 2 ) of two endpoints..send( ) Send message on channel .Return nothing.

.recv()
Receive a message from channel .Return only the message.

.close()
Send termination message and close the channel.

.wait()
Wait for termination message and close the channel.
-create channel between main and worker -start the worker thread -receive count and answer reference -sum received numbers -signal that we are done -wait for closing signal -mutable reference to store the sum -we will send 100 numbers to be summed into -send the numbers 1..100 -wait until the worker is done -send closing signal -assert that the received answer is correct We implement imperative channels in terms of session channels by storing a session channel in a mutable reference: .close() ≜ close (! ); free .wait()≜ wait (! ); free

Emerging Linked List Buffers
We demonstrate the imperative API with the example from Fig. 2. The example creates a channel to communicate between the main thread and the forked-off 'worker' thread.The main thread allocates a reference and sends the message (100, ) to the worker thread, which indicates that the main thread is going to send 100 further number messages to the worker thread.The worker thread receives each of these numbers, and mutates to keep track of their sum.Finally, the worker thread sends an empty acknowledgment message () to the main thread, indicating that it is done with and will not mutate further.The main thread closes the session by sending the closing signal, which the worker thread waits for.The main thread then reads the value of the sum from , and asserts that it is correctly computed.
The linked structures that emerge during execution are displayed in Fig. 3.In the picture, the main thread has sent the numbers [1, . . ., 9], while the worker thread has so far only received [1, 2, 3].At run time, the worker thread will have a reference to 2 , which points to the head of a linked list structure.When the worker thread receives the next message (4), it updates 2 to point to the next linked list element, and adds the value of the message to .The main thread also has a reference to , but it will not use it until the worker thread has sent the completion signal back, to avoid race conditions.Instead, the main thread is still busy working on the other end of the linked list.Each time the main thread sends a message, it allocates a new memory location, puts its message into the tail, and updates the tail of the existing linked list to point to the new location.This emergence of the linked list occurs because the send operation allocates a new one-shot channel, represented as a memory location, and sends it along with the message.At a lower level of abstraction, this results in a linked list buffer of messages, where each message is a pair of a value and a continuation channel.
If the worker thread were to catch up with the main thread, it would wait until it sees a message.When the main thread is done, it tries to receive a message using the last linked list node it has created, which is initially still empty.When the client reaches that node, it puts the acknowledgment () into it, signaling that the main thread may now read from .1More generally, the threads switch roles when the polarity of the protocol changes: the thread that used to consume list cells now creates new list cells, and vice versa.
Note that the emergence of the buffer as a bi-directional linked list is somewhat implicit.We have built several layers of channels, but at no point did we have to think about the linked-list run-time structure as a whole.We will see a similar phenomenon when doing the proofs: we never need to think about the run-time structure as a whole.Instead, we will develop specifications in a layered way, following the layers of the implementation.
In the remainder of this paper, we will develop specifications for these different layers (corresponding to § 2.1 to 2.4), and prove the correctness of the channel implementations with respect to these specifications.We can then use the specifications to verify this example in §3.5.

LAYERED SPECIFICATIONS AND VERIFICATION
As the reader may have noticed, the implementations in the preceding section are untyped.Rather than assigning types to the channel APIs, we will provide separation logic specifications.These allow us to prove functional correctness of programs that make use of the channel API.We prove partial correctness, which guarantees that if a program satisfies a separation logic specification with trivial precondition, then the program is safe, i.e., does not get stuck in the semantics due to run-time type errors, use-after-free or double-free bugs, or failing assert expressions.In terms of session types, our result should be compared with type safety and session fidelity.As is standard in papers that use Iris, we do not prove deadlock freedom or termination (which would only be true when assuming a fair scheduler as the spin-loop in recv1 could otherwise trivially loop).

The Iris Separation Logic
To specify and verify the channel implementations and example clients, we use the Iris separation logic.Fig. 4 shows the grammar and a selection of rules of the subset of Iris that we use.Iris provides a program logic for HeapLang with Hoare-triples { } { }, which express that given the precondition ( : iProp), the program ( : Expr) is safe to execute, and yields the postcondition ( :Val → iProp).We often write Points-to: Iris is a separation logic [O'Hearn et al. 2001], meaning that propositions assert ownership over resources, such as references.This is made precise by the separation logic connectives, such as the separating conjunction * , which describes that the propositions and holds for separate parts of the heap.In particular, this lets us derive exclusivity of references; it is impossible to separately own the same reference: ℓ ↦ → * ℓ ↦ → − * False.Here − * is the "separating implication" connective.It acts similarly to the regular implication, but for separation logic.
Separation logic facilitates modular verification, by virtue of the framing rule Ht-frame, which states that we can verify programs in the presence of separate resources .Non-structured concurrency is supported by the Ht-fork rule.Finally, Iris enjoys the conventional rules for mutable references Ht-alloc, Ht-load, Ht-store, and Ht-free, which respectively allow allocating, reading, updating, and freeing mutable references.
We use Iris's impredicative invariants , ghost state tokens tok , and later modality ⊲ .We further discuss the meaning and importance of these connectives throughout the section.

One-Shot Channels
In Fig. 5 we show separation logic specifications for the one-shot channel implementation from §2.2.These specifications make use of one-shot protocols that describe the protocol for a one-shot channel.As a one-shot channel communicates a value, the protocol will carry a predicate describing which values are allowed to be communicated with that channel.Additionally, the protocol says whether we are allowed to send or receive.Therefore, we represent one-shot protocols as a pair (tag, ) where tag ∈ {Send, Recv} and ∈ Val → iProp.The predicate is a separation logic predicate, so that protocols can express transfer of ownership.
To link protocols to actual channels, we shall define a channel points-to predicate base (tag, ).The channel points-to provides unique ownership of one end of the channel and says that channel satisfies protocol (tag, ).The channel points-to is analogous to the normal points-to ℓ ↦ → of separation logic, in the sense that a points-to assertion is required to verify an invocation of a channel operation.The definition can be found in Fig. 6, but we will first discuss how it is used in the Hoare rules for the channel operations.
When we create a new channel using new1 (), we may choose the protocol predicate , and we get two channel points-tos: base (Send, ) and base (Recv, ).Note that we get both channel points-tos for the same channel , because the same memory location is used for both ends of the channel, and the two channel points-tos represent ownership of the two ends of the channel, which give two different views of the same memory location.As we shall see in §3.2, this is achieved by moving the ownership of the primitive heap points-to of the memory location into an invariant, which allows us to share it.In accordance with session types, and to state the specification of new1 () in a symmetric manner (Fig. 5), we introduce the dual function on protocols, given by (Send, ) ≜ (Recv, ) and (Recv, ) ≜ (Send, ).
Once we have the two channel-point-to predicates we may give one of them to another thread, and keep one of them in the current thread.This way we ensure that two threads use the protocol to agree on how the channel will be used.
We may then use the send1 and recv1 operations to perform the communication.The send1 operation requires ownership of base (Send, ) as well as in its precondition.Dually, the recv1 operation requires ownership of base (Recv, ) in its precondition.Its postcondition guarantees that recv1 returns a value that satisfies . With these specifications we can verify the example presented in §2.2 with the following protocol: This protocol expresses that the exchanged value is a location ℓ.We transfer the ownership of the exchanged reference ℓ along with the message.With this, we can symbolically apply the one-shot channel specifications, and finally assert that the value read from the received reference is 42.
Verifying the implementation with respect to the specification.We now prove that the one-shot channel implementation satisfies its specification.To do this, we define the channel pointsto base in terms of Iris logic primitives (namely, ordinary points-to, ghost state and invariants).We then prove that the specifications for new1,send1 and recv1 follow from the rules of Iris.We first present the two key concepts from Iris needed for our proof: ghost state and invariants.
Ghost state.Ghost state is logical state that we can use to logically coordinate between parallel threads.Compared to the standard approach to ghost state in concurrency verification [Owicki and Gries 1976], ghost state in Iris is not part of the program text.It is introduced and manipulated solely in proofs.Just as the physical heap keeps track of the values of memory locations, Iris has a ghost heap that keeps track of the values of ghost locations.In our case we only need the very simplest form of ghost state: we need pure ownership over ghost heap locations; we do not need to store further information in the ghost locations.Given the ghost location , we have the ghost resource tok , which is analogous to ℓ ↦ → (), i.e., a location that points to a unit value.It may seem a bit puzzling that ghost locations that do not store any interesting contents can be helpful in a proof.The key is that ghost locations have the same exclusivity as memory locations.That is, we have the Tok-excl rule that says it is impossible to have ownership of two ghost locations with the same name: tok * tok − * False.We shall see why this is useful in a moment.Finally, we can always allocate new pieces of ghost state, using the Ht-ghost-alloc rule.
Invariants.The points-to resource ℓ ↦ → is an affine resource, and cannot be duplicated.This is a problem for verifying concurrent programs, where we would like to use the same memory location from multiple threads: when we fork off a child thread, we would like to keep ownership over the memory location in both the main thread and the child thread.
To solve this issue, concurrent separation logic has the notion of invariants.At any moment in the proof where we have ownership over ∈ iProp, we can choose to establish as an invariant, denoted ∈ iProp.This is formally described by the Ht-inv-alloc rule.The advantage of an invariant is that it can be freely duplicated, i.e., − * * .In turn, we cannot directly access the inside the invariant.Instead, we can only temporarily access it when the program takes an atomic step, such as a memory load !ℓ or store ℓ ← .After the atomic step has happened, we must immediately put back into the invariant.This is formally described by the Ht-inv-open-close rule, where the resources ⊲ are the resources that are temporarily removed from the invariant.In the precondition of the rule, we obtain access to the resources ⊲ taken out of the invariant, and in the postcondition we have to give back the resources ⊲ , which represents putting them back into the invariant.The proposition inside an invariant is typically a disjunction of several states, where the states may assert ownership over memory locations using ℓ ↦ → , and may assert that has certain properties in that state.A state may also assert ownership over ghost resources.
Iris's invariants are impredicative [Svendsen and Birkedal 2014], which effectively lets us nest invariants inside of invariants, because ∈ iProp for every ∈ iProp, including = .Nesting of invariants is critical for the verification of our session channels, as will be covered in §3.4.To maintain soundness of the Iris logic, resources extracted from an invariant are guarded by a later modality ⊲ [Nakano 2000;Appel et al. 2007].This later can be seen in the Ht-inv-open-close rule.Resources ⊲ behind a later modality can only be used after the program does the next step of execution.This is formally expressed by the Ht-later-frame rule, which states that one can frame resources under a later, if the program has not terminated.Another means of stripping laters is if the guarded resources are timeless (Ht-later-timeless).Pure propositions, reference ownership (ℓ ↦ → ) and ghost ownership (tok ) are timeless, which means when we open an invariant, we can immediately remove the later from these connectives.
The one-shot channel invariant.To verify the one-shot channels, we need to define the connective base , whose key ingredient is an invariant.To explain the invariant, we start with a key observation.The one-shot channel can be in three different states: (1) no message has been sent (ℓ ↦ → None), (2) a message has been sent but not received (ℓ ↦ → Some ), and (3) the message has been both sent and received (ℓ has been deallocated).These states are reflected in the invariant chan_inv 1 2 ℓ defined in Fig. 6.The arguments 1 and 2 are two ghost locations, whereas ℓ is the physical memory location where the channel is located, and is the predicate associated with the protocol.The invariant captures each state with a separate disjunct.By virtue of the exclusion of the ghost resources, it is then possible to exclude possible states, based on local ghost ownership.
In particular, if one owns tok 1 , the invariant must be in the first state (as the other states assert ownership of the token).Similarly, if one owns tok 2 , the invariant cannot be in the final state.The proof then follows by letting the sender own tok 1 and the receiver own tok 2 , to let them locally determine which state the invariant is in, by the exclusivity rule of the ghost resources.
More formally, with the invariant in place, we can define the channel points-to base (tag, ), as presented in Fig. 6.The definition captures (1) that is a reference ( = ℓ), (2) that the invariant is established ( chan_inv 1 2 ℓ ), (3) that the endpoint has ownership of either tok 1 or tok 2 , if they are the sender or receiver, respectively.The later modalities (⊲) in the definition of base are needed to support infinite protocols via guarded recursion ( §4).
Initially, when creating a channel, we establish the invariant in the first state, using the Ht-invalloc rule.We then duplicate the invariant, and create base (Send, ) and base (Recv, ) using the two copies of the invariant, as well as tok 1 and tok 2 , respectively, which are created by two applications of the Ht-ghost-alloc rule.
When the sender wants to send their message , they temporarily open the invariant using the Ht-inv-open-close rule, and determine that they are in the first state, based on their tok 1 token.They then get ownership over the reference ℓ ↦ → None.The sender then modifies the location to contain the sent value Some , and transfers the ownership back into the invariant.The sender also puts the token tok 1 into the invariant, as well as the resources captured by the protocol.The invariant is restored in the second state.
When the receiver wants to receive, it temporarily opens up the invariant, using the Ht-inv-openclose rule, to get ownership over the reference.It reads the location, and if the value is None, it determines that it is in the first state, and so it loops.Once a value Some is read, it is determined that we are in the second state, and so the receiver deallocates the reference.The receiver additionally takes the resource out of the invariant, and re-establishes the invariant by putting its token tok 2 into the invariant, which restores it in the third state.
The rule for new1 is then proven as follows.We obtain ownership over the location ℓ ↦ → None because new1 allocates the reference.We also allocate two new ghost locations tok 1 and tok 2 obtaining the identifiers 1 and 2 .We establish the invariant using the first disjunct, by putting ℓ ↦ → None into the invariant, and allocate it with the Ht-inv-alloc rule.We then duplicate the invariant, and create base (Send, ) and base (Recv, ) using the two copies of the invariant, as well as tok 1 and tok 2 , respectively.

Subprotocols
We define a subprotocol relation on dependent separation protocols as introduced by Actris [Hinrichsen et al. 2022], analogous to subtyping on session types [Gay and Hole 2005].Whereas subtyping between session types is established by subtyping between the messages, the subprotocol relation between protocols is established by implications between the separation logic predicates. 2he subprotocol relation is denoted ⊑ where , are protocols, and is defined as follows: This relation is reflexive and transitive, and ⊑ iff ⊑ .We layer subprotocols on top of our specification for one-shot channels by defining a new channel points-to that is explicitly closed under subprotocols: We do not use a superscript on because we consider it to be the main channel points-to, whereas we view base as an internal notion.This channel points-to satisfies a subsumption like rule: ( ) * ⊲( ⊑ ) − * ( ), which is proved by transitivity of ⊑.The use of the later modality (⊲) is discussed in §4.
We can prove versions of the specifications for new1, send1, and recv1 for .These proofs are straightforward, because we can prove these specifications using the existing specifications for base from Fig. 5, by using ⊑ at appropriate points to convert a 1 into 2 or vice versa.In particular, we apply this conversion in the send rule just before sending the message, and in the receive rule just after receiving the message.We also trivially have ( base ) − * ( ), which is used to prove the new1 rule for .

Session Channels
Now that we have established the specifications for the one-shot channels, we move on to the next layer: multi-shot session channels.A prominent approach to specifying and verifying multi-shot channels is the concept of session types [Honda 1993], which lets a user ascribe session channel endpoints with a sequence of obligations to send or receive messages of certain types.More recently, the session type approach has been adopted in the separation logic setting [Craciun et al. 2015;Hinrichsen et al. 2022].One such adaptation is Dependent Separation Protocols [Hinrichsen et al. 2022].Rather than ascribing types to each exchange, dependent separation protocols ascribe logical variables, physical values, and propositions.The dependent separation protocols and the specifications for the session channels can be seen in Fig. 7.
The dependent separation protocols consists of four constructors: !⟨ ⟩{ }. p, ?⟨ ⟩{ }. p, !end, and ?end.The first two constructors describe the permission to send or receive the logical variable , the value , and the resources , respectively, after which they follow the protocol tail p.Here, binds into all of the remaining constituents.We often omit the binder when it is of the unit type: e.g., !⟨ ⟩{ }. p.We similarly often omit the proposition if it is True: e.g., !⟨ ⟩. p.The last two constructors specify that the protocol has ended, meaning that no further operations can be made on the channel, and the channel can be closed.We further detail alternative specifications for closing and deallocation in §5.
The protocols are subject to the same notion of duality, as presented in § 3.2.The dual of a protocol is the same sequence of obligations, where the polarity has been flipped, i.e., all sends (!) become receives (?), and vice versa, as made precise by the rules of the figure.Finally, we use the same channel endpoint ownership p as for the one-shot channels, as the dependent separation protocols share the same type as the one-shot protocols, as will be seen momentarily.
The dependent separation protocols can be used to specify and verify session channels.As an example, the following dependent separation protocol specifies the interactions of the prog_add example from §2.3:The protocol says that one must first send a reference to a number (captured by the logical variable (ℓ, ) : Loc × Z)), along with the ownership of the reference ℓ ↦ → .Afterwards, the updated reference can be reacquired, followed by the protocol termination.The dual of the protocol is ?((ℓ, ) : The notion of duality is used in the specification for new.The specification states that we obtain separate exclusive ownership of the returned endpoint , one with a freely picked protocol p and the other with its dual p.This mimics the intuition from the one-shot channel, in which one endpoint had to release the specified resources, while the other could acquire them.The specification for send states that in order to send, the channel endpoint must have a sending protocol, and we must give up the specified resources , for a specific instantiation of the variable .Additionally, the sent value must correspond to the protocol, for the variable instantiation .As a result, the returned channel endpoint follows the protocol tail ′ p , for the same variable instantiation.Conversely, the specification for recv states that we can receive if the channel endpoint has a receiving protocol.As a result we obtain an instance of the logical variable , and the resources specified by the protocol .Additionally, the returned value is exactly the one specified by the protocol , and the new endpoint follows the protocol tail ′ p .The prog_add example can now be verified using the prot_add protocol.
Verification of the session channel specifications.The definitions of the dependent separation protocols and the specification rules presented in Fig. 7 are derived directly on top of the one-shot channel definitions and specifications.In particular, the type of dependent separation protocols is the same as the one for the one-shot channel protocols, namely Prot.The definition of the receiving protocol is as follows: The recv_prot constructor takes four arguments, and constructs a receiving one-shot channel protocol.In particular the constructor takes the type of its logical variable , the exchanged value , the exchanged proposition , and the protocol tail p.The latter three arguments all abstract over the protocol variable, which is existentially quantified in the protocol body.The second projection captures that the actual exchanged value is a tuple of the value specified by the protocol ( ), and the continuation ( ).It additionally includes ownership of the resources specified by the protocol ( ), and finally a one-shot channel ownership, of the continuation with the protocol tail ( (p )).The notation ?( : ) ⟨ ⟩{ }. p then simply lets us instantiate the receiving constructor, without explicitly repeating the variable abstraction for the three constituents.
The duality function of the session channels is the same as the one for the one-shot channel.We define the sending constructor in terms of the receiving one, using the duality function as follows: To specify the close and wait operations we define two session protocols: Finally, the channel endpoint ownership p is identical to the one for the one-shot channels, as the type of the protocols are the same, they simply carry channel continuations now.This immediate reuse of the one-shot ownership is made possible by the higher-order nature of Iris.In particular, the internal invariant of the endpoint ownership refers to the session protocols, which internally includes a nested endpoint ownership, and so on.By virtue of the step-indexing of Iris, this is sound as we always take a step for each unfolding of the nested invariants.
With these definitions the soundness of the session channel specifications (Fig. 7) follow almost immediately from the sound specifications of the one-shot channel operations send1 and recv1.
Subprotocols for session protocols.We have a notion of subprotocols for one-shot protocols ( §3.3), but what about dependent session protocols?Because we have defined session protocols as particular forms of one-shot protocols, we get the appropriate notion of subprotocols for session protocols for free.The following lemmas for session subprotocols (and the imperative derivation on top of them) are already true and easily derived from the subprotocol rules in §3.3: At a high level, these lemmas state that a session protocol is a subprotocol of another, if for each logical message in the first protocol, there exists an appropriate logical message in the second protocol, such that we have a separating implication between separation logic assertions, and the tails of the protocols are in a subprotocol relationship.The stated lemmas are somewhat stronger than this high-level description; for instance, the user of the lemmas gets access to the assertion 1 1 before having to provide the corresponding logical message 2 for the other protocol.As an example, this strengthening allows one to perform a form of framing of resources within a protocol: if a resource is provided by an earlier send and needed by a later receive, we can frame these two resources (i.e., remove both from the protocol by canceling them out).This property can be illustrated by the following rules: !⟨ ⟩{ }. ?⟨ ⟩{ }. p ⊑ !⟨ ⟩{ * }. ?⟨ ⟩{ * }. p ? ⟨ ⟩{ * }. !⟨ ⟩{ * }. p ⊑ ?⟨ ⟩{ }. !⟨ ⟩{ }. p

Imperative Channels
Because our session channels create new pointers at each step, they return new channels, and are thus inconvenient to work with.For that reason, we have our final layer: the imperative channels from § 2.4.These channels put a session channel in a mutable reference, so that we can use the same mutable reference throughout and use mutating operations to change the reference to a new session channel upon send and receive operations.To handle these channels, we introduce a new channel points-to imp p.The specifications for the imperative channels can be found in Fig. 8.We note a couple of differences with respect to the session channels: • The new_imp operation returns a pair of channels now, so the points-to connectives in the postcondition are for the two components of the pair.• The send operation does not return a value.The new channel points-to in the postcondition refers to the original channel instead.• The recv operation only returns one value-the message.The channel points-to in the postcondition once again refers to the original channel.
Verifying the imperative channel specifications.To verify the session channels we first define a new connective for channel endpoint ownership:  The new imperative channel ownership connective imp p simply lifts the original connective ′ p to assert ownership of a mutable reference.With this definition in hand, verifying the specification is trivial.We simply use the Iris rule for allocating, reading, and updating the reference, along with the specifications for the original channel endpoint ownership, to resolve the operations on the channel.
Because the new channel-points-to is defined in terms of the old one, the results of subprotocols easily lift to the imperative channels.
Verifying the example.We now explain how these specifications can be used to verify the example from Fig. 2. The example starts by allocating a new channel, so we use the specification for new_imp.In order to use this specification, we have to choose the session protocol p.We use the following protocol: The protocol prot_sum says that we will first send the pair ( , ) of a number and a location, and the assertion that ↦ → 0. We then continue with the protocol prot_sum ′ 0 , which is recursively defined.Its first argument keeps track of the sum of the messages sent so far, and the second argument keeps track of how many messages we still have to send.When the counter = 0, we stop sending and instead receive a unit value, as well as the assertion that ↦ → , i.e., the sum of the messages sent.
After the channel allocation, we have 1 imp prot_sum and 2 imp prot_sum.We verify the first interaction using the first step of prot_sum.We prove the loops correct using induction: the main thread does induction on 100, and the child thread induction on the received message (which will be 100, but the child thread does not know this).After the final synchronization, the ownership over has been transferred back to the main thread.According to the protocol, the location points to the value 1 + 2 + • • • + 100, which is equal to 5050 by mathematical reasoning.
As the reader can see, the reasoning about the pointer structure of the buffers is completely encapsulated in the higher-level session specifications.The nondeterminism present due to the asynchronous semantics of the send operation does not need to be reasoned about explicitly: although the depth of the linked list buffer changes non-deterministically according to the thread scheduling of the sends and receives, the proof does not explicitly reason about this at all.

GUARDED RECURSION
As we have seen in the example in §3.4,we can already create some recursive protocols by employing recursion over natural numbers (or other inductively-defined data types in Coq).Recursion over natural numbers lets us verify the example from Fig. 2 where one side sends a number , and then sends further messages.Although recursion on inductive types is powerful, it does not allow us to create protocols for truly infinite interactions with services that run forever.We can create protocols that support truly infinite interactions with Iris's operator for guarded recursion.
Iris models guarded recursion via step-indexing [Appel and McAllester 2001;Ahmed 2004], meaning that separation logic propositions iProp are internally monotone predicates of a natural number , the step index.Intuitively, the meaning of such a proposition is given by taking the limit to ever higher step indices.This allows us to model infinite protocols as a step-indexed protocol of unboundedly increasing depth.Iris does not expose the step index to the user of the logic, so we cannot define protocols by direct recursion over .Instead, Iris provides a logical account of step-indexing [Appel et al. 2007;Dreyer et al. 2011] through the later modality ⊲ [Nakano 2000], and a guarded recursion operator .
for constructing recursive predicates.The must be contractive in the sense that recursive occurrences of in must only occur under a later ⊲.This ensures that creating such a recursive predicate does not result in any logical paradoxes.Our protocols Prot ≜ (Send | Recv) × (Val → iProp) contain separation logic predicates over values, so we can make direct use of Iris's guarded recursion mechanism to define recursive protocols.
The reader may have noticed that we have already inserted the later modality ⊲ in certain places in our definitions, such as in the definition of base ( §3.2).This is to make sure that base is contractive in , which in turn means that !⟨ ⟩{ }. p and ?⟨ ⟩{ }. p are contractive in p.
We are therefore able to take guarded fixpoints of protocols, to create unbounded or infinite protocols, such as the following recursive variant of prot_add: A second component of guarded recursion is Iris's support for Löb induction.Löb induction allows us to verify unbounded or infinitely recursive programs that use recursive protocols.Ordinary induction only gives us an induction hypothesis for recursive calls where some measure is decreasing, and hence only works for terminating loops.Löb induction, on the other hand, gives us an induction hypothesis for any recursive call (not necessarily decreasing), but this induction hypothesis will be guarded under a later (⊲).These laters maintain logical consistency, but the resources guarded by them may only be accessed after the next primitive program step.In this manner, Löb induction allows us to verify partial correctness of a program that sends a stream of messages in an infinite tail-recursive loop, by instantiating the channel with the preceding recursive protocol.
The recursive protocols combined with Löb induction allow us to verify recursive programs such as the following recursive variant of the prog_add program from §2.3: Here, rec f = is a recursive function, where the recursive occurrence is bound to .Verifying the program is straightforward.Notably, the main thread unfolds the recursive protocol prot_add_rec twice, to verify its code.The forked-off thread is resolved using Löb induction.It unfolds the recursive protocol once, verifies one iteration, after which it uses the Löb induction hypothesis to verify the recursive call.Similar to Actris [Hinrichsen et al. 2022, §9.1], recursion is not only permitted via the tail p, but also via the proposition in the protocols ?⟨ ⟩{ }. p and !⟨ ⟩{ }. p, making it possible to construct recursive protocols such as p. !⟨ ⟩{ p}.!end.We are allowed to construct such protocols because p is contractive in p.Also similar to Actris [Hinrichsen et al. 2022, §6.4], we can use Löb induction to prove that an infinitely recursive protocol is a subprotocol of another.The later modalities (⊲) in the rules for subprotocols (page 17) make it possible to remove a later from the Löb induction hypothesis.The same approach applies to protocols such as p. !⟨ ⟩{ p}.!end because the subsumption rule ( ) * ⊲( ⊑ ) − * ( ) contains a later modality.Making recursion and Löb induction interact properly requires careful placement of later modalities in the definitions of the channel points-to connectives.For example, to prove the subsumption rule ( ) * ⊲( ⊑ ) − * ( ) for other forms of channel closure in §5, we need to consider the case that = end and ≠ end.We only obtain ⊲ False from ⊲( ⊑ ), instead of an immediate contradiction (⊲ False is not equivalent to False).Due to the later modalities in , however, ⊲ False is sufficient to complete the proof.3

SELF-DUAL END
In the preceding sections, we had separate close and wait operations, with dual !end and ?end protocols.In this section we investigate alternative operations to deallocate or close a channel, which result in a self-dual end protocol.We have two different options for achieving this: • Symmetric close.Define one close operation, with protocol end, that both sides call, which dynamically determines who deallocates the channel ( §5.1) • Send-close.Define a combined send-close operation that sends the last message and closes the channel.The other side performs a recv that obtains no continuation channel ( §5.2).

Symmetric Close
Suppose that we want only one sym_close operation, that both sides of the channel call.Because the channel consists of one memory location, we need to dynamically decide which caller gets to free the memory.We use compare-and-swap to achieve this effect: sym_close ≜ if CAS( , None, Some()) then () else free To see how this works, consider two parallel close operations on the same channel: sym_close ∥ sym_close .The thread that does its CAS first will successfully set from None to Some(), and return () from its sym_close.The second thread will then fail its CAS, since the value stored in is no longer None.It will then go to the else branch and free .
To verify this version of close, we need to make a change to our notion of protocols.So far, our protocols have all been one-shot protocols ∈ Prot ≜ (Send | Recv) × (Val → iProp) under the hood; even the protocols !end, ?end ∈ Prot.For the symmetric sym_close, this does not work.We now have to explicitly distinguish end in the protocols: where ∈ Prot We also need to extend duality with end ≜ end and subprotocols with end ⊑ end.
With this additional protocol, we have the following specification for sym_close:

Close operation:
{ sym end} sym_close {True} Because our set of protocols has been extended, we need an extended channel points-to sym , which we define as follows: Here, the following protocol is stored inside our invariant: ) ∨ (ℓ ↦ → Some() * (tok 1 ∨ tok 2 ) one side has closed ) Like the one-shot send-receive protocol, this protocol uses two tokens tok 1 and tok 2 , which belong to the two sym end assertions.Initially, the invariant states that the location ℓ points to None.When one side has successfully closed, the invariant states that ℓ points to Some(), and the invariant has collected the token of the side that has called close first (because this is nondeterministic, the invariant uses a disjunction tok 1 ∨ tok 2 ).When both sides have closed, the invariant has both tokens, and no memory points-to (because the memory location has been deallocated).As before, we add later modalities (⊲) in front of = ℓ and tok 1 to support infinite protocols via guarded recursion ( §4).With these definitions, we can prove the Hoare specification for the symmetric sym_close in a similar way we verified send1 and recv1.

Send-Close
From an operational point of view, the previous two methods for channel closing are a tiny bit disappointing, because for the last step, a memory location is allocated but not used to communicate any useful message.In this section we develop a channel closing mechanism where the close operation is integrated with the last message send.
This may sound strange at first sight, but upon investigating how channel closing typically works in examples, it hopefully starts to make more sense.Consider an example where party A is communicating a stream of messages to another party B, and A may at every point decide to end the stream.This can be accomplished by sending an additional Boolean along with each message, which determines whether this is the last message or not.When it is the last message, the sender does not allocate a continuation channel, and sends () in place of the continuation channel.When the receiver receives a message, they have to inspect the Boolean to determine whether they got a continuation channel or not.This saves one memory allocation and synchronization compared to the previous methods.Similarly, in the example of Fig. 2, we can eliminate the last interaction and synchronization by integrating the final acknowledgment with the closing of the channel.
While this saving is minor, we argue in favor of it for aesthetic reasons.If one wants to implement the one-shot API on top of the previous session channel API (i.e., the other way around compared to what we have done so far), then a single shot communication would involve one real communication and then one extra allocation and communication to close the channel.We now present a channel closing mechanism with which one can implement one-shot channels on top of session channels with no additional synchronizations or allocations.Therefore, with this channel closing mechanism, session channels become a purely logical layer over one-shot channels.The implementation of this closing mechanism is very simple, namely the following send_close operation: There is no corresponding wait operation for the other side: as send_close simply does not allocate a continuation channel, the other side can use recv, which already deallocates the memory location.For the specification and verification of send_close, we use the same Prot end protocols: ∈ Prot end ::= end | where ∈ Prot We extend duality with end ≜ end and subprotocols with end ⊑ end.As before, we define a new channel points-to, this time for the send-close version: For the end protocol, the channel points-to asserts that there is no channel, i.e., the channel is a unit value instead of a pointer to a memory location (this could also be implemented as a null pointer).These are the specifications for the channel operations with send_close: Send-close: { scl (! ⟨ ⟩{ }. p) * } send_close ( ) {True} where p = end Receive: { scl (? ⟨ ⟩{ }. p)} recv { .∃ , ′ .= ( , ′ ) * ′ scl p * } The send operation now requires that the tail p is not end, whereas the send_close operation requires that p is end.The specification of recv does not concern itself with end.Instead, the received message will contain information about whether the protocol ended or not (such as a Boolean, as described previously).Using logical reasoning about the message, we can then conclude whether the tail protocol p is end or not.If it is, then we obtain ′ = (), and we do not need to do anything.If it is not end, we obtain ′ scl p and can continue the protocol.
Unlike close with symmetric channel closing from §5.1, the send_close operation has been defined in terms of send1.The proofs of the specifications therefore also follow straightforwardly from the specifications of send1 and recv1, unlike the proofs for symmetric channel closing.

OTHER SUPPORTED FEATURES
In this section, we briefly discuss some other features of our framework.Similar to Actris, we get these features for free by building on top of Iris: Delegation and channel passing.We support delegation, i.e., sending channels over channels as messages, due to Iris's support for impredicative (i.e., nested) invariants.This allows the channel points-to resource to be used in a protocol such as !⟨ ⟩{ }.This protocol enables us to send a channel as well as its associated channel points-to over another channel, which then allows the receiver to use the received channel at protocol .Choice protocols.We support choice protocols, where a thread can choose between multiple different continuation protocols.This can be encoded as a special case of dependent session protocols, where the sender makes the choice by sending a Boolean value, and the continuation protocol is chosen based on the value of the Boolean: 1 ⊕ 2 ≜ !⟨ ⟩{True}.if then 1 else 2 .Shared memory.Channels are not the only way to communicate information between threads: we can also use shared memory directly.We can use all of the features of Iris to reason about shared memory, we can send mutable references as messages over channels (as in Fig. 2), and we can store channels in mutable references.
Locks and shared sessions.We support the combination of locks with channel communication.For instance, we can use a lock to protect a channel endpoint, which can then be used by multiple threads.This is useful for implementing shared sessions, where multiple threads can send and receive messages on the same channel endpoint, which is common in client-server protocols.

MECHANIZATION
The implementations of channels ( §2), the proof that they satisfy their separation logic specifications ( §3), the different methods for closing channels ( §5), and the verification of all the examples have been fully mechanized using the Coq proof assistant [Coq Team 2021], making use of the Iris separation logic framework.
The mechanization follows the layered design as presented in Fig. 1.The layered design allows our proofs to be simpler compared to previous work on Actris [Hinrichsen et al. 2020].Only the proofs for one-shot operations new1, send1, recv1 (and the symmetric sym_close) involve concurrent separation logic concepts such as ghost state and invariants.All the other proofs are done on top of these specifications, treating the one-shot operations as a black box.
Our protocol definitions are simple compared to Actris.We do not need to solve an intricate recursive domain equation [Hinrichsen et al. 2022, §9.7].At no point do we have to reason about more than one cell in the buffer structure; the multi-shot session protocols simply emerge automatically using composition.Despite this simplification to the Actris model, the different extensions such as subprotocols, guarded recursion, and the different forms of channel closing work seamlessly together.For instance, we can show that an infinitely recursive protocol is a subprotocol of another infinitely recursive protocol, by using guarded recursion and Löb induction.
In total, our Coq mechanization consists of less than 1000 lines of Coq code (including the verification of all examples).The mechanization is referenced throughout the paper by -symbols.The mechanization has also been archived on Zenodo [Jacobs et al. 2023].

RELATED WORK
The origins of our line of work trace back to session types.More directly, our work is inspired by encodings of session types in terms of one-shot synchronization in particular [Kobayashi 2002;Dardha et al. 2017;Jacobs 2022].Our work is also directly related to dependent protocols and program logics for session protocols.Most notable is the work on Actris [Hinrichsen et al. 2020[Hinrichsen et al. , 2022]], which introduced the notion of dependent separation protocols, which we use to specify our session channels.We go over each of these points in more detail below.
One-shot channels.The encoding of session channels in terms of sequenced one-shot channels originated in the -calculus.This encoding sends a continuation channel in each message, so that the communication can continue.Kobayashi [2002] showed that session types can be encoded into -types, and Dardha et al. [2012Dardha et al. [ , 2017] ] later extended Kobayashi [2002]'s approach.Jacobs [2022] presented a bidirectional version in a -calculus.
Similar one-shot primitives have also been used in the implementation of message passing libraries, such as in the work of Scalas and Yoshida [2016]; Padovani [2017]; Kokke and Dardha [2021]; Niehren et al. [2006].Our implementation of session channels in terms of one-shot channels uses a similar strategy.
Unlike this earlier work, which is either untyped or type-based, we use session protocols in separation logic to verify (partial) functional correctness.Our one-shot channels are not primitive and not built-in to the language, but implemented in terms of low-level memory operations.We take inspiration from the preceding work and subsequently build session channels on top of one-shot channels, and we build session protocols on top of one-shot protocols.
Dependent protocols and session logics.Bocchi et al. [2010] and Toninho et al. [2011] both developed version of (multi-party) session types which incorporate logical binders into the protocols, alongside a first-order decidable assertion language for specifying properties about them.Later, Toninho and Yoshida [2018] and Thiemann and Vasconcelos [2020] expanded on this work by allowing similar binders determine the structure of the remaining protocol, similar to what we do in § 2.4.Compared to our work, their assertion languages are limited in the sense that they cannot describe the delegation of resources (e.g., sending a reference to another thread).Later work [Craciun et al. 2015;Costea et al. 2018] addressed the issue of specifying resource delegation, through the development of a session logic, based in separation logic.Their logic allows ascribing channel endpoints with protocols, which in turn can specify resources to be shared, such as other channel endpoints.Compared to our work, they do not support binders, which for one means that they cannot specify protocols referring the dynamically allocated references, like we do in §2.2.Actris protocols support both binders, delegation, and protocols referring to dynamically allocated references and ghost resources [Hinrichsen et al. 2020], as our protocols do.
Actris.Actris introduced a shared-memory implementation of higher-order session channels, and the notion of dependent separation protocols for the verification of message passing concurrency using program logics, mechanized on top of Iris.Our work focuses primarily on developing a framework in the style of Actris, but with a focus on layered design, elegance, and simplicity.This results in the following key differences between Actris and our work: • Actris channels implement bi-directional communication using a pair of buffers that are protected by a lock.Our one-shot channels are implemented directly using load and store memory operations, and our session channels and imperative channels are implemented in terms of one-shot channels.• As a result of this, Actris's dependent separation protocols are defined by solving an intricate recursive domain equation.By contrast, our definition of Prot ≜ (Send | Recv) × (Val → iProp) is itself non-recursive, yet Actris-style dependent separation protocols can be defined as inhabitants of Prot, and automatically support recursive protocols.• Our notion of subprotocols for one-shot channels is very simple and non-recursive, but automatically lifts to (recursive) session protocols, because session protocols are defined as one-shot protocols.Actris's notion of subprotocols is recursive and more complicated than ours, but also stronger: Actris's implementation of channels with a pair of buffers admits swapping sends over receives (akin to asynchronous subtyping [Mostrous et al. 2009;Mostrous and Yoshida 2015]).Such a transformation is not sound for our single-buffer implementation of channels.• We achieve a simpler approach by making use of nested invariants, but Actris's solution gave rise to the "Actris ghost theory" [Hinrichsen et al. 2022, §9.4] for reasoning about session protocols in a way that is disconnected from specific implementations.The Actris ghost theory has been used to develop specifications based on dependent session protocols for distributed systems [Gondelman et al. 2023].• Actris contains a number of convenience features, such as multi-binders and associated tactics, to ease verification of message passing programs in Coq.While such features can be integrated in our Coq development, we preferred to keep the protocols (and verification thereof) simpler, to focus on the layering of channel variants.Even so, our single-binders can simulate multi-binders using tuples, as has been demonstrated throughout the paper.• While Actris relies on a garbage collector for channel deallocation, we present several manually memory managed solutions for channel closing.
In short, Actris has more features (asynchronous subtyping, ghost theory) and a more convenient implementation in Coq (multi-binders, tactics), but our design achieves the key feature of Actris (dependent separation protocols) in a conceptually simpler and layered manner: once we have defined and verified one-shot channels (which are quite simple and require only the simplest form of ghost resources to verify), we treat them as a black box and develop Actris-style protocols with relative ease and without any further use of ghost state or invariants.An application of Actris is the verification of the soundness of a session type system via the method of semantic typing [Hinrichsen et al. 2021].Since our separation logic specifications for session channels are the same as Actris's, a similar result could be achieved with our development.
Imperative session channels.Related to §2.4, there has also been work on type systems for imperative channels, which free the user from having to thread channel variables through their program Saffrich and Thiemann [2022b,a].The advantage of a type system compared to a program logic is that type checking is automatic, but an advantage of a program logic is its ability to verify functional correctness.Hinrichsen et al. [2021] combines advantages of both approaches via the method of semantic typing in Iris, which allows one to combine separation logic verification for intricate parts of the program, and type checking for the rest.
Fig. 5. Separation logic specifications for one-shot channels.

Fig. 6 .
Fig. 6.The channel invariant and channel points-to definition.
Fig. 1.Layered design of our development.
Session channels have the following API:In this section we demonstrate how one-shot channels can be used to implement session channels.The session channels are obtained by allocating and exchanging a new one-shot channel whenever a value is sent.The new one-shot channel is then used as a continuation of the session.The session Proc.ACM Program.Lang., Vol. 7, No. ICFP, Article 214.Publication date: August 2023.