Ferrite: A Judgmental Embedding of Session Types in Rust
aa r X i v : . [ c s . P L ] S e p Ferrite: A Judgmental Embedding of Session Types in Rust
RUOFEI CHEN,
Independent Researcher, Germany
STEPHANIE BALZER,
Carnegie Mellon University, USAThis article introduces Ferrite , a shallow embedding of session types in Rust. In contrast to existing ses-sion type libraries and embeddings for mainstram languages Ferrite not only supports linear session typesbut also shared session types. Shared session types allow sharing (aliasing) of channels while preservingsession fidelity (preservation) using type modalities for acquiring and releasing sessions. Ferrite adopts apropositions as types approach and encodes typing derivations as Rust functions, with the proof of success-ful type-checking manifesting as a Rust program. The encoding resides entirely within the safe fragment ofRust and makes use of type-level features to support an arbitrary-length linear resource context and recursivesession types. ACM Reference Format:
Ruofei Chen and Stephanie Balzer. 2020. Ferrite: A Judgmental Embedding of Session Types in Rust. 1, 1(September 2020), 33 pages.
Message-passing concurrency is a dominant concurrency paradigm, adopted by mainstream lan-guages such as Erlang, Scala, Go, and Rust, putting the slogan “to share memory by communicatingrather than communicating by sharing memory” [1, 2] into practice. In this setting, messages areexchanged over channels, which can be shared among several senders and recipients. Figure 1provides an example in Rust. It sketches the main communication paths in Servo’s canvas com-ponent [3], with some code simplified. Servo is a browser engine under development that usesmessage-passing to parallelize tasks, such as DOM traversal and layout painting, which are exe-cuted sequentially in existing web browsers. The canvas component provides 2D graphic renderingservices, allowing its clients to create new canvases and perform operations on a canvas such asmoving the cursor, drawing lines and rectangles.The component is implemented by the
CanvasPaintThread , whose function start contains the maincommunication loop running in a separate thread (lines 9–17). This loop processes client requestsreceived along canvas_msg_receiver and create_receiver , the receiving endpoints of the channelscreated prior to spawning the loop (lines 7–8). The channels are typed with the enumerations
ConstellationCanvasMsg and
CanvasMsg , defining messages for creating and terminating the canvascomponent and for executing operations on an individual canvas, respectively. Canvases are iden-tified by an id, which is generated upon canvas creation (line 16) and stored in the thread’s hashmap canvases (line 4). Should a client request an invalid id, the failed assertion expect("Bogus canvasid") (line 20) will result in a panic! , causing the canvas component to crash and clients to deadlockwhen waiting for a response from the component. Such a reaction can be the result of a clientterminating a canvas (line 13) while other clients are still trying to communicate with it.Although he code in Figure 1 uses a clever combination of enumerations to type channels andownership to rule out races on the data sent along channels, the Rust type system is not expressiveenough to enforce adherence to the intended protocol of message exchange and existence of a GitHub repository for Ferrite: https://github.com/maybevoid/ferriteAuthors’ addresses: Ruofei Chen, Independent Researcher, Germany, [email protected]; Stephanie Balzer,Carnegie Mellon University, USA.2020. XXXX-XXXX/2020/9-ART $15.00https://doi.org/ , Vol. 1, No. 1, Article . Publication date: September 2020.
Ruofei Chen and Stephanie Balzer enum CanvasMsg { Canvas2d(Canvas2dMsg, CanvasId), Close(CanvasId) } enum Canvas2dMsg { MoveTo(Point2D), LineTo(Point2D), ... } enum ConstellationCanvasMsg { Create { id_sender: Sender
Session types [4–6] were introduced to express the protocols of message exchange and theiradherence at compile-time. The discovery of a Curry-Howard correspondence between linear logicand the session-typed π -calculus [7–11] gave session types a strong logical foundation, resultingin a linear treatment of channels and thus assurance of a communication partner. More recently,linear session types have been extended with shared session types to accommodate safe sharing(i.e., aliasing) of channels [12–14], addressing the limitations of an exclusively linear treatment ofchannels and increasing the scope of applicability of session types to a multi-client scenario, suchas the one in Figure 1.For example, using linear and shared session types we can capture the protocols implicit inFigure 1 as follows: Canvas = ( Canvas2dMsg ⊲ Canvas ) N ϵ ConstellationCanvas = ↑ SL Size2D ⊲ CanvasId ⊳ Canvas ⊗↓ SL ConstellationCanvas
The linear session type
Canvas prescribes the protocol for performing operations on an individualcanvas, the shared session type
ConstellationCanvas the protocol for creating a new canvas. Thissetup allows multiple clients to concurrently create new canvases, but ensures that at any pointin time there exists only one client per canvas. We use the language
SILL R , a formal session typelanguage based on SILL S [12] that we extend with Rust constructs. Table 1 provides an overview of SILL R ’s connectives. These are the usual linear connectives for receiving and sending values andchannels ( ⊲ , ⊳ , ⊸ , ⊗ ) as well as external and internal choice ( N , ⊕ ). An external choice leavesthe choice between its left and right option to the client, an internal choice leaves it to the provider.For example, Canvas provides the client the choice between sending a value of enumeration type
Canvas2dMsg , after which the canvas recurs, or closing the canvas. Readers familiar with classicallinear logic session types [8] may notice the absence of linear negation.
SILL R adopts an intuition-istic, sequent calculus-based formulation [7], which avoids explicit dualization of a connective byproviding left and right rules. , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 3 Table 1. Overview and semantics of session types in SILL R and Ferrite.SILL R Ferrite Description ϵ End
Terminate session. τ ⊲ A ReceiveValue
Receive value of type τ , then continue as session type A . τ ⊳ A SendValue
Send value of type τ , then continue as session type A . A ⊸ B ReceiveChannel
Receive channel of session type A , then continue as session type B . A ⊗ B SendChannel
Send channel of session type A , then continue as session type B . A N B ExternalChoice
Receive label inl or inr , then continue as session type A or B , resp. A ⊕ B InternalChoice
Send label inl or inr , then continue as session type A or B , resp. ↑ SL A LinearToShared
Accept an acquire, then continue as a linear session type A . ↓ SL S SharedToLinear
Initiate a release, then continue as a shared session type S . type Canvas = Fix
Additionally,
SILL R comprises connectives to safely share channels ( ↑ SL A , ↓ SL S ). Since shared chan-nels have a sharing semantics as opposed to a copying semantics , as is the case for the linear expo-nential, it is crucial for safety to ensure that the multiple clients interact with the shared componentin mutual exclusion of each other. To this end, an acquire-release semantics is adopted for sharedcomponents such that a shared component must first be acquired prior to any interaction. When aclient acquires a shared component by sending an acquire request along the component’s sharedchannel, it obtains a linear channel to the component, becoming its unique owner. Once the clientis done interacting with the component, it releases the linear channel, relinquishing the owner-ship and being left only with a shared channel to the component. Key to type safety is to establishacquire-release not as a mere programming idiom but to manifest it in the type structure [12] suchthat session types prescribe when to acquire and when to release. This is achieved with the modal-ities ↑ SL A and ↓ SL S , denoting the begin and end of a critical section, respectively. For example, thesession type ConstellationCanvas prescribes that a client must first acquire the canvas componentbefore it can ask for a canvas to be created by sending values for the canvas’ size (
Size2D ) and id(
CanvasId ). The component then returns to the client a linear channel to the new canvas (
Canvas ),initiates a release ( ↓ SL ), and recurs to be available to the next client.The benefits of session types for software development have led to the introduction of sessiontype libraries and embeddings for languages such as Java [15, 16], Scala [17], Haskell [18–21],OCaml [22, 23], and Rust [24, 25]. This article introduces Ferrite , a shallow embedding of sessiontypes in Rust. Ferrite allows programmers to specify linear and shared session types and writemessage-passing programs that adhere to the specified protocol. Protocol adherence is guaranteedstatically by the Rust compiler. Figure 2 shows the corresponding session type declarations forthe above session types
Canvas and
ConstellationCanvas in Ferrite. Table 1 provides a mappingbetween
SILL R and Ferrite constructs. As we discuss in detail in Sections 2 and 5, Ferrite introducesa Fix type operator to support recursive session types; the type argument Z denotes the base caseof the type application and thus the recursion point. In case of a shared recursive session typessuch as ConstellationCanvas , Z combines recursion with a release ( SharedToLinear ). , Vol. 1, No. 1, Article . Publication date: September 2020. Ruofei Chen and Stephanie Balzer
A key contribution of Ferrite is the support of shared session types. Existing solutions focus onenforcing a linear treatment of sessions with varying guarantees, ranging from partial to dynamicor static. Enforcing linearity statically in an affine host language posed a considerable challengefor the development of Ferrite. We adopt the idea of lenses [26, 27] from prior work [20, 23] tosupport an arbitrary-length linear typing context with random access, but avoid explicit dualiza-tion of session types for higher-order channels thanks to our intuitionistic formalization. Anotherdistinguishing characteristics of Ferrite is its propositions as types approach. Building on the Curry-Howard correspondence between linear logic and the session-typed π -calculus [7, 8], Ferrite en-codes SILL R typing judgments and derivations as Rust functions. A successfully type-checked Fer-rite program thus manifests in a Rust program that is the actual SILL S typing derivation and thusthe proof of protocol adherence.In summary, Ferrite is an embedded domain-specific language (EDSL) for writing session-typedprograms in Rust, which supports • shared and linear session types using adjoint modalities for acquiring and releasing sessions, • arbitrary recursive session types using type-level recursion, • arbitrary-length linear context using lenses from profunctor optics to support random access, • input and output of channels (a.k.a.higher-order channels) in addition to values, and • managed concurrency, shielding programmers from channel creation and thread allocation.Remarkably, the Ferrite code base remains entirely within the safe fragment of Rust. Outline:
Section 2 provides a summary of the key ideas underlying Ferrite, with subsequentsections refining those. Section 3 introduces the Ferrite type system, focusing on its judgmentalembedding and enforcement of linearity. Section 4 elaborates on Ferrite’s dynamics, detailing theuse of Rust channels to implement Ferrite channels. Section 5 explains how Ferrite addresses Rust’slimited support of recursive data types to allow the definition of arbitrary recursive and sharedsession types. Section 6 provides a discussion of Ferrite’s guarantees, design choices, and direc-tions for future work, and Section 7 reviews related work. The Ferrite code base with examplesis provided as supplementary materials. For convenient look-up of definitions we also include anappendix. We plan to submit Ferrite as an artifact.
This section highlights the key ideas underlying Ferrite. Subsequent sections provide further de-tails. In SILL R , the formal session type language that we use in this article to study linear and sharedsession types, a program type-checks if there exists a derivation using the SILL R typing rules.Central to a typing rule is the notion of a typing judgment . For SILL R , we use the judgment Γ ; ∆ ⊢ expr :: A to denote that expression expr has session type A , given the typing of free shared and linear chan-nel variables in contexts Γ and ∆ , respectively. Γ is a structural context, which permits exchange,weakening, and contraction. ∆ is a linear context, which only permits exchange but neither weak-ening nor contraction. The significance of the absence of weakening and contraction is that itbecomes possible to “drop a resource” (premature closure) and “duplicate a resource” (aliasing),respectively.For example, to type-check session termination, SILL R defines the following typing rules: Γ ; ∆ ⊢ cont :: A Γ ; ∆ , a : ϵ ⊢ wait a ; cont :: A (T - ϵ L ) Γ ; · ⊢ terminate :: ϵ (T- ϵ R ) , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 5 Table 2. Judgmental embedding of SILL R in Ferrite.SILL R Ferrite Description Γ ; · ⊢ A Session < C, A >
Typing judgment for top-level session (i.e., closed program). Γ ; ∆ ⊢ A PartialSession < C, A >
Typing judgment for partial session. ∆ C : Context
Linear context; explicitly encoded. Γ - Shared context; delegated to Rust, but with clone semantics. A A : Procotol
Session type.
As mentioned in the previous section, we get a left and right rule for each connective because
SILL R adopts an intuitionistic, sequent calculus-based formulation [7]. Whereas the left rule de-scribes the session from the point of view of the client , the right rule describes the session fromthe point of view of the provider . We read the rules bottom-up, with the meaning that the premisedenotes the continuation of the session. The left rule (T - ϵ L ) indicates that the client is waiting forthe linear session offered along channel a to terminate, and continues with its continuation cont once a is closed. The linear channel a is no longer available to the continuation due to the absenceof contraction. The right rule ( T- ϵ R ) indicates termination of the provider and thus has no contin-uation. The absence of weakening forces the linear context ∆ to be empty, making sure that allresources are consumed.Given the notions of a typing judgment, typing rule, and typing derivation, we can get the Rustcompiler to type-check SILL R programs by encoding these notions as Rust programs. The basicidea underlying this encoding can be schematically described as follows: Γ ; ∆ ⊢ cont :: A Γ ; ∆ ⊢ expr ; cont :: A fn expr < C1: Context, C2: Context, A1: Protocol, A2: Protocol >( cont : PartialSession < C2, A2 > )-> PartialSession < C1, A1 > On the left we show a
SILL R typing rule, on the right its encoding in Ferrite. Ferrite encodes a SILL R typing judgment as the Rust type PartialSession
PartialSession
SILL R judgment with an empty linearcontext. A immediate encoding of the linear context C would be a type-level list C = ( A , ( A , ( ..., ( A N − )))) of session types A i with an appropriate update operation. This encoding has the advantage that itallows the context to be of arbitrary length, but the disadvantage that it imposes a fixed order onthe context’s elements, disallowing exchange . A further challenge for the encoding is the supportof arbitrary channel names: programmers should be able to freely choose the names of channelvariables and not be forced to comply with a predetermined naming scheme. Next, we sketch the , Vol. 1, No. 1, Article . Publication date: September 2020. Ruofei Chen and Stephanie Balzer high-level idea of how Ferrite supports an arbitrary-length linear context with random access toprogrammer-named channels while using an ordered, type-level list internally.An important abstraction in pursuit of this goal is the notion of a lens [26, 27], present in ear-lier work [20, 23], that generalizes access and modification of a data structure’s component. Wecombine this abstraction with de Bruijn levels as nameless indexes into an ordered, type-level listrepresentation of the linear context. Given an inductive trait definition of natural numbers as zero( Z ) and successor ( S
ContextLens
N: Nat such that the session type B1 at the N-th position in the linear context C is replaced withthe session type B2 and the update linear context becomes the associated type N::Target . Schemat-ically this encoding can be captured as follows: Γ , i : N ; ∆ , N : B , ∆ ′ ⊢ cont :: A Γ , i : N ; ∆ , N : B , ∆ ′ ⊢ expr ( i ) ; cont :: A fn expr < N, C, A1: Protocol, B1: Protocol, ... >( i: N, cont: PartialSession < C, A2 > )-> PartialSession < N::Target, A1 >where N : ContextLens < C, B1, B2 > We note that the index N amounts to the type of the variable i that the programmer chooses as aname for a channel in the linear context. Ferrite takes care of the mapping, thus supporting randomaccess to programmer-named channels. The above encoding is simplified to illustrate the key idea,sections 3.2 and 3.3 provide further details, including the support of higher-order channels. Rust’s support for recursive types is limited to recursive struct definitions of a known size. Tocircumvent this restriction and support arbitrary recursive session types, Ferrite introduces a type-level fixed-point combinator
Fix
F::Applied . Schematically we cancapture this encoding as follows; Section 5.1 provides further details: trait TypeApp < X > { type Applied; } struct Fix < F : TypeApp < Fix < F > > >{ unfix : Box < F::Applied > }
Recursive types are also vital for encoding shared session types. In line with [14], Ferrite restrictsshared session types to be recursive, making sure that a shared component is continuously avail-able. To guarantee preservation, recursive session types must be strictly equi-synchronizing [12, 14],requiring an acquired session to be released to the same type at which it was previously acquired.Ferrite easily enforces this invariant by defining a specialized trait
SharedTypeApp which omits animplementation for
End . Section 5.2 provides further details on the encoding.
This section introduces the statics of Ferrite, detailing the encoding of
SILL R typing judgments andrules and linear context in Ferrite. Section 5 discusses recursive and shared session types. A distinguishing characteristics of Ferrite is its propositions as types approach, yielding a directcorrespondence between
SILL R notions and their Ferrite encodings. We introduced this correspon-dence in Section 2.1 (see Table 2), next we discuss it in more detail. To this end, let’s consider thetyping of value input. We remind the reader of Table 1 in Section 1, which provides a mappingbetween SILL R and Ferrite session types. The interested reader can find a corresponding mappingon the term level in Table 3 in the appendix. , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 7 Γ , a : τ ; ∆ ⊢ K :: A Γ ; ∆ ⊢ a ← receive _ value ; K :: τ ⊲ A ( T ⊲ R ) fn receive_value < T, C: Context, A: Protocol >( cont : impl FnOnce( T ) -> PartialSession < C, A > )-> PartialSession < C, ReceiveValue < T, A > > The
SILL R right rule ( T ⊲ R ) types the expression a ← receive _ value ; K as the session type τ ⊲ A andthe continuation K as the session type A . Ferrite encodes this rule as the function receive_value ,parameterized by a value type T ( τ ), a linear context C ( ∆ ), and an offered session type A . Thefunction yields a value of type PartialSession< C, ReceiveValue
FnOnce(T) -> PartialSession
PartialSession
SILL R . A Rust value of type PartialSession
SILL R typing judgment. The typeparameters C and A are constrained to implement the traits Context and
Protocol , two other Ferriteconstructs representing a linear context and linear session type, respectively: struct PartialSession< C: Context, A: Protocol > { ... } trait Context { ... }trait Protocol { ... }
For each
SILL R session type, Ferrite defines a corresponding Rust struct that implements the trait Protocol , yielding the listing shown in Table 1. Corresponding implementations for ϵ ( End ) and τ ⊲ A ( ReceiveValue
ReceiveValue
Protocol is propagated to theinner session type, requiring A to implement Protocol too: struct End { ... }impl Protocol for End { ... } struct ReceiveValue < T, A > { ... };impl < A: Protocol > forReceiveValue < T, A > { ... }
Whereas Ferrite delegates the handling of the shared context Γ to Rust with the obligation thatshared channels implement Rust’s trait Clone to permit contraction, it encodes the linear context ∆ explicitly. Being affine, the Rust type system permits weakening, a structural property rejected bylinear logic. Ferrite encodes a linear context as a type-level list of the form (A0, (A1, ())) , with allits type elements A i implementing Protocol . Using the unit type () for the empty list and the tupleconstructor (_,_) for the cons cell, we can implement the Context trait inductively as follows: impl Context for () { ... }impl < A: Protocol, C: Context > Context for ( A, C ) { ... }
The use of a type-level list for encoding the linear context has the advantage that it allows thecontext to be of arbitrary length. Its disadvantage is that it imposes an order on the context’selements, disallowing exchange. In the next two sections, we discuss how to make up for the lossof exchange and support random access to context elements using lenses. In prepration of thisdiscussion, we find it helpful to introduce a variant of
SILL R typing rules that use an orderedcontext Σ instead of the linear context ∆ and a lens for random access to the elements in Σ . Wecall this variant SILL R Σ and label SILL R Σ typing rules with the subscript Σ to set them apart fromregular SILL R typing rules. In SILL R Σ , the context Σ is inductively defined as follows, mirroringthe inductive definition of Context : · ctx A sessiontype Σ ctx ( A , Σ ) ctx , Vol. 1, No. 1, Article . Publication date: September 2020. Ruofei Chen and Stephanie Balzer
To represent a closed program, that is a program without any free variables, Ferrite defines atype alias
Session for
PartialSession
A complete session type program in Ferrite is thus of type
Session and amounts to the
SILL R typing derivation proving that the program adheres to the defined protocol.As an illustration of how to program in Ferrite we use a “hello world”-style session type programthat receives a string value as input, prints it to the screen, and then terminates. On the left, weshow the corresponding program in SILL R , on the right in Ferrite. The session type offered is of SILL R type String ⊲ ϵ , which translates into the Ferrite type ReceiveValue
Ferrite program is available in appendix B.1.
Next, we discuss how we establish exchange for our encoding of a linear context using the notionof a lens. In this section, we focus on updating the type of a channel in the linear context due toprotocol transition, in the next section we address the addition or removal of a channel to andfrom the linear context to account for higher-order channel constructs such as ⊸ and ⊗ .We illustrate the update of the type of a channel in the linear context based on the left rule forvalue input. The rule updates the type of channel a from τ ⊲ A in the conclusion to A in the premiseas a consequence of sending a value of type τ along channel a : Γ ; ∆ , a : A ⊢ K :: B Γ , x : τ ; ∆ , a : τ ⊲ A ⊢ send _ value _ to a x ; K :: B (T ⊲ L ) Following the same approach that we used for embedding ( T ⊲ R ) in the previous section, wecan sketch the encoding of ( T ⊲ L ) as the function send_value_to shown below, with a few type holesprefixed with ? to be filled in: fn send_value_to < T, A: Protocol, ... >( l: ?L, x: T, cont: PartialSession < ?C2, A > )-> PartialSession < ?C1, A > In addition to the type holes ?C1 and ?C2 for the linear contexts ∆ , a : τ ⊲ A and ∆ , a : A for theconclusion and premise of the rule, respectively, the encoding introduces the type hole ?L as ameans to access the channel a inside the linear contexts ?C1 and ?C2 .A naive approach to filling in the type holes would simply be to plug in (ReceiveValue
ContextLens traitis defined with three type parameters C , A1 , and A2 corresponding to Σ , A , and A ′ of ϕ , respectively.The updated context Σ ′ is defined as an associated type Target because it is determined by the othertype parameters C , A1 , and A2 . trait ContextLens < C: Context, A1: Protocol, A2: Protocol >{ type Target: Context; ... } Using the trait
ContextLens we can now encode the inference rule ( T Σ ⊲ L ) as shown below. The type L of the first argument l implements the trait ContextLens
PhantomData
ContextLens using Z and S
SILL R Σ shown above. The base case Z implements the ContextLens for any linear contextin the form of (A1, C) . The inductive case
S
ContextLens< (B, C), A1, A2 > forany B , provided that N implements the ContextLens< C, A1, A2 > . The
Target associated type for
S
N::Target prepended with B . , Vol. 1, No. 1, Article . Publication date: September 2020. impl < A1: Protocol, A2: Protocol,C: Context >ContextLens < (A1, C), A1, A2 >for Z{ type Target = ( A2, C ); ... } impl < A1: Protocol, A2: Protocol,B: Protocol, C: Context,N: ContextLens < C, A1, A2 > >ContextLens < (B, C), A1, A2 >for S < N >{ type Target = ( B, N::Target ); ... } The current definition of a context lens works well for accessing a channel in a linear context toupdate its session type. However we have not addressed how channels can be removed from or added to the linear context can be done using context lenses. These operations are required toaccount for higher-order channel constructs such as ⊗ and ⊸ or session termination. To support channel removal, we introduce a special empty element ∅ to denotethe absence of a channel at a particular position in the linear context Σ . Below, we show the SILL R inference rule ( T L ) for termination, which uses exchange for locating the channel l : ϵ forremoval from ∆ , and contrast it with the corresponding SILL R Σ rule ( T Σ L ), which uses a contextlens to update l : ϵ in Σ to l : ∅ in Σ ′ . Γ ; ∆ ⊢ K :: A Γ ; ∆ , l : ϵ ⊢ wait l ; K :: A (T L ) Γ , n : N ; Σ ′ ⊢ K :: A N ⇒ ϕ ( Σ , ϵ , ∅ , Σ ′ ) Γ , n : N ; Σ ⊢ wait n ; K :: A (T Σ L ) Ferrite implements ∅ as the Empty struct. To allow
Empty to be present in a linear context, weintroduce a new
Slot trait and make both
Empty and
Protocol implement
Slot . The original definitionof
Context is then updated to allow types that implement
Slot instead of
Protocol . struct Empty { }trait Slot { ... } impl Slot for Empty { ... }impl < A: Protocol > Slot for A { ... } Using
Empty , it is straightforward to implement rule ( T Σ L ) using a context lens that replaces achannel of session type End with the
Empty slot: fn wait < C: Context, A: Protocol, N: ContextLens < C, End, Empty > >( n: N, cont: PartialSession < N::Target, A > )-> PartialSession < C, A >
The function wait does not really remove a slot from a linear context, but merely replaces theslot with
Empty . As a result, an empty linear context may contain any number of
Empty slots, suchas (Empty, (Empty, ())) . We introduce a new
EmptyContext trait to abstract over the different formsan empty linear context can take and provide an inductive definition as its implementation, withthe empty list () as the base case. The inductive case (Empty, C) then is an empty linear context, if C is an empty context. Using the definition of an empty context, the right rule ( T Σ R ) can then beeasily encoded as the function terminate shown below: trait EmptyContext : Context { ... }fn terminate < C: EmptyContext >() -> PartialSession < C, End > impl EmptyContext for () { ... }impl < C: EmptyContext >EmptyContext for ( Empty, C ) { ... } The Ferrite function wait removes a channel from the linear context by replacing itwith ∅ . The function receive_channel , on the other hand, adds a new channel to the linear context.The SILL R right rule ( T ⊸ R ) for channel input is shown below. It binds the received channel ofsession type A to the channel variable a and adds it to the linear context ∆ of the continuation. Γ ; ∆ , a : A ⊢ K :: B Γ ; ∆ ⊢ a ← receive _ channel ; K :: A ⊸ B (T ⊸ R ) To encode ( T ⊸ R ) in SILL R Σ , we need to first think of how to add a new channel to a linearcontext Σ . From the previous section we know that context lenses are implemented as natural , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 11 numbers, serving as an index for accessing channels by their position in a linear context. Withthis in mind we want to avoid adding a new channel A to the front of a linear context Σ to form ( A , Σ ) , as it would invalidate any existing context lenses used in the continuation. Instead, we wantto define an operation for appending a new channel A to the end of a linear context Σ .The append operation in SILL R Σ can be defined inductively by the rules shown below. We usethe judgment Σ ∗ Σ ′ (cid:17) Σ ′′ to denote that the result of appending Σ to Σ ′ is Σ ′′ . · ∗ Σ (cid:17) Σ Σ ∗ Σ ′ (cid:17) Σ ′′ ( A , Σ ) ∗ Σ ′ (cid:17) ( A , Σ ′′ ) Similarly in Ferrite, the append operation is defined as the
AppendContext trait shown below. Thetrait
AppendContext is parameterized by a linear context C , has Context as its super-trait, and an asso-ciated type
Appended . If a linear context C1 implements the trait AppendContext
AppendContext follows the same inductive definition as in
SILL R Σ , with the empty list () implementing the base case and the cons cell (A, C) implementingthe inductive case. trait AppendContext < C: Context > : Context{ type Appended: Context; ... }impl < C: Context >AppendContext < C > for (){ type Appended = C; ... } impl < A: Slot, C2: Context,C1: AppendContext < C2 > >AppendContext < C2 >for ( A, C1 ){ type Appended = ( A, C1::Appended ); ... } Using
AppendContext , a channel B can be appended to the end of a linear context C , if C implements AppendContext<(B, ())> . The new linear context after the append operation is then given in theassociated type
C::Appended . At this point we know that the channel B can be found in C::Appended ,but there is not yet any way to access channel B in C::Appended . To provide access, a context lenshas first to be generated. We observe that the position of channel B in C::Appended is the same as thelength of the original linear context C . In other words, the context lens for channel B in C::Appended can be generated by getting the length of C .We first provide an inductive definition for the length of a linear context Σ in SILL R Σ using thetype-level natural numbers defined earlier and | Σ | to denote the size of the type-level list Σ . | · | (cid:17) Z | Σ | (cid:17) N |( A , Σ )| (cid:17) S ( N ) In Ferrite, the length operation can be implemented by adding an associated type
Length to the
Context trait. Then the implementation of
Context for () and (A, C) simply follows the same inductivedefinition as shown above in SILL R Σ . trait Context { type Length; ... }impl Context for () { type Length = Z; ... } impl < A: Slot, C: Context > Context for (A, C){ type Length = S < C::Length >; ... } With the append and length operations in place, we can now define the right rule ( T Σ ⊸ R ) in SILL R Σ as shown below. The rule uses Σ ∗ ( A , ·) to append the channel A to Σ and identifies Σ ′ asthe result. Other than that it also uses | Σ | to determine the length of Σ as N . The structural context Γ has a new variable a of type N added to it, and Σ ′ is used as the linear context. Γ , a : N ; Σ ′ ⊢ K :: B | Σ | (cid:17) N Σ ∗ ( A , ·) (cid:17) Σ ′ Γ ; Σ ⊢ a ← receive _ channel ; K :: A ⊸ B (T Σ ⊸ R ) Ferrite encodes the right rule ( T Σ ⊸ R ) as the receive_channel function shown below. The functionis parameterized by a linear context C implementing AppendContext to append the session type A to C . The continuation argument cont is a closure that is given a context lens C::Length , and returnsa
PartialSession with
C::Appended as its linear context. The function returns a
PartialSession with C being the linear context and offers a session of type ReceiveChannel . , Vol. 1, No. 1, Article . Publication date: September 2020. fn receive_channel< A: Protocol, B: Protocol, C: AppendContext <( A, () )> >( cont: impl FnOnce ( C::Length ) -> PartialSession < C::Appended, B > )-> PartialSession < C, ReceiveChannel < A, B > > The use of
ContextLens and send_value_to can be demonstrated with an example client shown be-low. The program hello_client is written to communicate with the hello_provider program definedearlier in section 3.1. The communication is achieved by having hello_client offer the session type
ReceiveChannel < ReceiveValue
ReceiveValue
Ferrite program is available in appendix B.1.
At this point we have defined the necessary constructs to build and type check both hello_provider and hello_client , but the two are separate Ferrite programs that are yet to be linked with eachother and executed. Ferrite provides a special construct apply_channel to facilitate such linking.The typing rule for apply_channel is as follows: Γ ; · ⊢ f :: A ⊸ B Γ ; · ⊢ a :: A Γ ; · ⊢ apply _ channel f a :: B (T-app) fn apply_channel < A: Protocol, B: Protocol >( f: Session < ReceiveChannel < A, B > >,a: Session < A > )-> Session < B > The function apply_channel is defined as a construct that brings together two continuations f and a . The program f acts as the client expecting to receive a channel from a provider offering sessiontype A and then continues as session type B . The program a acts as the provider offering sessiontype A . Both f and a are linked by apply_channel , which sends the channel offered by a to f and thencontinues as f . The function apply_channel restricts f and a to be complete Ferrite programs withempty linear contexts, i.e., f and a cannot be closures with free channel variables.To actually execute an entire Ferrite application, Ferrite provides the function run_session . Thefunction run_session accepts a top-level Ferrite program of type Session
End cannot be executed until they are linked to amain Ferrite program of type
Session
Readers familiar with linear logic-based session types [7, 8] may wonderhow apply_channel is related to cut . The cut rule is defined as follows, using the
SILL R syntax: Γ ; ∆ ⊢ a :: A Γ ; ∆ , x : A ⊢ b :: B Γ ; ∆ , ∆ ⊢ x ← cut a ; b :: B (T-cut) Compared to apply_channel , cut is less restrictive as it accepts arbitrary linear contexts ∆ and ∆ ,including non-empty ones, and an arbitrary session type A for the first premise. However, as weshow in Appendix B.2, apply_channel is actually derivable using cut and does, as a result, not limitthe expressiveness of Ferrite.The benefit of supporting apply_channel rather than cut is the enforced linearization by introduc-ing channels into a context subsequently, which uniquely determines the type-level list used inter-nally in Ferrite for the linear context. Supporting cut directly in Ferrite would require the splittingof contexts for the premises of the cut rule, a type equation for which no unique solution may exist.For example, the equation Σ ∗ Σ (cid:17) ( A , ( A , ( A , ()))) has the solutions Σ (cid:17) ( A , ( A , (∅ , ()))) and Σ (cid:17) (∅ , (∅ , ( A , ()))) , Σ ′ (cid:17) ( A , (∅ , ( A , ()))) and Σ ′ (cid:17) (∅ , ( A , (∅ , ()))) , and Σ ′′ (cid:17) (∅ , ( A , ( A , ()))) and Σ ′′ (cid:17) ( A , (∅ , (∅ , ()))) for splitting the context such that | Σ | = | Σ | = Section 3 introduced the type system of Ferrite, based on the constructs
End , ReceiveValue , and
ReceiveChannel . This section revisits those constructs and fills in the missing implementations tomake the constructs executable, amounting to the dynamic semantics of Ferrite.
Ferrite uses
Rust channels as the basic building blocks for session type channels. A Rust channelis a pair of a sender and receiver, of type
Sender
and
Receiver
, respectively, denoting the twoendpoints of the channel. The type parameter P is the payload type of the Rust channel, indicatingthe type of values that can be communicated over the channel.Rust channels can be used for communication between two or more processes. For the commu-nication to happen, we first need to decide what the payload type P should be and how to distributethe two endpoints among the processes. Internally, Ferrite adopts the convention to always givethe sending endpoint of a Rust channel to the provider, and the receiving endpoint to the client.We illustrate Ferrite’s use of Rust channels based on the example below. The example shows aprovider int_provider that offers to send an integer value and a client that is willing to receive aninteger value. We choose i32 as the payload type, allowing the provider to send a value of that typeto the client. fn int_provider ( sender: Sender
Sender
Given this brief introduction to Rust channels and the use of channel nesting for polarity rever-sal, we next address how these ideas are combined to implement the dynamics of Ferrite sessiontypes. Basically, the dynamics of a Ferrite session type is implemented by a Rust channel whosepayload is of type
Protocol . We recall from Section 3.1 that all protocols except for
End are param-eterized with the session type of their continuation. To provide an implementation that properlyreflects the state transitions of a protocol induced by communication, it is essential to create a freshchannel to be used for communication by the continuation. For each implementation of a sessiontype, it must be determined what the payload type of the channel to be used for the continuationshould be. Again, the convention is to associate the sending endpoint with the provider, whichmay necessitate channel nesting if polarity reversal is required.We illustrate this idea below, showing the implementations of the Ferrite protocols
SendValue
ReceiveValue
ReceiveValue
Endpoint and
Endpoints for the
Slot and
Context traits, respectively, as shown below. From the client’s perspective, a non-empty slot ofsession type A has the Endpoint type
Receiver . The
Endpoints type of a
Context is then a type levellist of slot
Endpoint s. trait Slot { type Endpoint : Send; }impl Slot for Empty{ type Endpoint = (); }impl < A: Protocol > Slot for A{ type Endpoint = Receiver; } trait Context { type Endpoints; ... }impl Context for () { type Endpoints = (); ... }impl < A: Slot, R: Context > Contextfor ( A, R ) { type Endpoints =( A::Endpoint, R::Endpoints ); ... } Ferrite generates session type programs by composing
PartialSession objects generated by termconstructors such as send_value . The
PartialSession struct contains an internal executor field asshown below, for executing the constructed Ferrite program. The executor is a Rust async clo-sure that accepts two arguments – the endpoints for the linear context
C::Endpoints and the senderfor the offered session type
Ferrite keeps the executor field private within the library to prevent end users from constructingnew
PartialSession values or running the executor closure. This is because the creation and exe-cution of
PartialSession may be unsafe. The code below shows two examples of unsafe (i.e., non-linear) usage of
PartialSession . On the left, a Ferrite program p1 of type Session< SendValue
PartialSession is used with care within Ferrite to ensure that linearity is enforced in the implemen-tation. Externally, the run_session is provided for executing Ferrite programs of type
Session
SendValue . The function accepts a value x of type T to be sent, and a continuation cont of type PartialSession
PartialSession constructor is called to create the return value of type
PartialSession< C, SendValue
C::Endpoints , and the second argument sender1 of type
SendValue
Many real world applications, such as web services, databases, instant messaging, and onlinegames, are recursive in nature. As a result, it is essential for Ferrite to support recursive sessiontypes to allow the expression of the communication protocols of these applications. In this section,we report on Rust’s limited support for recursive types and how Ferrite addresses the restrictionand successfully encodes recursive session types. , Vol. 1, No. 1, Article . Publication date: September 2020.
Consider a simple example of a counter session type, which sends an infinite stream of integervalues, incrementing each by one. To build a Ferrite program that offers such a session type, wemay attempt to define the counter session type as follows: type Counter = SendValue < u64, Counter >;
If we try to compile our program using the type definition above, we will get a compiler errorthat says "cycle detected when processing
Counter ". The problem with the above definition is thatit defines a recursive type alias that directly refers back to itself, which is not supported in Rust.Rust imposes various restrictions on the forms of recursive types that can be defined to ensurethat the space requirements of data is known at compile-time.To circumvent this restriction, we could use recursive structs. However, if Ferrite relied on re-cursive struct definitions, end users would have to explicitly wrap each recursive session type ina recursive struct. Such an approach would be not be very convenient, so we want to find a wayfor the end users to define recursive session types using type aliases. One possibility is to performrecursion at the type level . [30]Functional languages such as ML, OCaml, and Haskell provide excellent support for type-levelrecursion. We can use type-level recursion to define a recursive type
Fix , which serves as the type-level fixed point of a type function. With that, we can first define a non-recursive data structure andthen use
Fix to get the fixed point of that data structure, which becomes a recursive data structure.Such non-recursive definitions of recursive data structures have many useful applications in thedomain of recursion schemes [31]. As for Ferrite, our focus is on using
Fix to define recursivesession types in a non-recursive way.In Haskell, we can define such a
Fix data type as follows: data Fix (f :: Type -> Type) = Fix ( f ( Fix f ) )
It is parameterized by a higher-kinded type f , which has the kind Type -> Type . In the constructordefinition, a value of rolled type
Fix f can be constructed by providing a value of the unrolled type f (Fix f) . Unfortunately at the time of writing this article, higher-kinded types are not supportedin Rust. As a result, we cannot define the
Fix type in the same way as in Haskell. Fortunately,there is still a way to achieve the same outcome by using defunctionalization [28, 29] to emulatehigher-kinded polymorphism in Rust. This can be done by defining a
TypeApp trait as follows: trait TypeApp < X > { type Applied; }
The
TypeApp trait is parameterized by a type parameter X , which serves as the type argument tobe applied to. This makes it possible for a Rust type F that implements TypeApp to act as if it haskind
Type -> Type and be "applied" to X . The associated type Applied is then used as the result typeof "applying" X to F . Using TypeApp , we can now define
Fix in Rust as follows: struct Fix < F: TypeApp < Fix < F > > > { unfix : Box < F::Applied > }
The new version of
Fix is now parameterized over a type F that implements TypeApp< Fix
Fix now contains
Box
F::Applied representing the result of applying
Fix
Fix for recursive session types, we want to implement
TypeApp for all session types inFerrite. We also need to pick a type for the recursion point of the fixed point type, and we chose Z for that purpose. The implementation of TypeApp for Z is shown on the left below. It simply replacesitself with the type argument X when applied. The implementation of TypeApp for
SendValue
TypeApp implemented, we can define the earlier recursive session type
Counter as the type: type Counter = Fix < SendValue < u64, Z > >;
To make
Counter a valid session type, we must implement
Protocol for
Fix and Z . Moreover, since Fix is iso-recursive, Ferrite provides constructs for rolling and unrolling session types. Below isthe function fix_session , which rolls up an offered unrolled session type into
Fix : fn fix_session< C: Context, F: Protocol + TypeApp < Fix < F > > >( cont: PartialSession < C, F::Applied > )-> PartialSession < C, Fix < F > > The function fix_session is used for rolling an offered session type
Fix
F::Applied . The continuation offering the unrolled type
F::Applied is given to fix_session , andthe Ferrite program returned offers the rolled up session type
Fix
Counter session typethat we defined realier. fn stream_producer (count: u64) -> Session < Counter >{ fix_session ( send_value_async ( async move || {task::sleep ( Duration::from_secs(1) ).await;( count,stream_producer ( count + 1 ) ) }) ) }
We define stream_producer as a recursive function that recursively generates the Ferrite programof type
Session
SendValue
In the previous section we explored a recursive session type
Counter , which is defined non-recursivelyusing
Fix and Z . Since Counter is defined as a linear session type, it cannot be shared among multi-ple clients. Shared communication, however, is essential for implementing many practical applica-tions. For example, we may want to implement a simple counter web service using session types,to send a unique count for each request. To support such shared communication, we introduce shared session types in Ferrite, enabling safe shared communication among multiple clients.Ferrite implements shared session types as introduced in Balzer and Pfenning [12], which pro-vide language primitives for the acquiring and releasing of shared processes and stratify sessiontypes into a linear and shared layer with two modalities connecting the layers. The
SILL R typesextended with shared session types are as follows: , Vol. 1, No. 1, Article . Publication date: September 2020. S , ↑ SL AA , B , ↓ SL S | ϵ | τ ⊲ A | τ ⊳ A | A ⊸ B | A ⊗ B | A ⊕ B | A N B The type system of
SILL R is extended to have one additional layer S for shared session types .At the shared layer, there is one connective ↑ SL A , which represents a linear to shared modality forshifting a linear session type A up to the shared layer. This modality amounts to the acquiring of ashared process. A linear session type ↓ SL S is added to the linear layer, which represents a shared tolinear modality for shifting a shared session type S down to the linear layer. This modality amountsto the releasing of an acquired process.The usage of the two modalities can be demonstrated with a recursive definition of a sharedcounter example in SILL R , as shown below. The code defines a shared session type SharedCounter ,which is a shared version of the linear counter example. The linear portion of
SharedCounter in between ↑ SL (acquire) and ↓ SL (release) resembles a critical section. The definition shows that SharedCounter is a shared session type that, when being acquired , offers a linear session type
Int ⊳ ↓ SL SharedCounter in the critical section. In other words, when acquired
SharedCounter firstsends an integer value and then continues as the linear session type ↓ SL SharedCounter . Once thelinear critical section is released , the shared session type
SharedCounter becomes available to otherclients.
SharedCounter = ↑ SL Int ⊳ ↓ SL SharedCounter
Shared session types are recursive in nature, as they have tooffer the same linear critical section to all clients that acquire a shared process. As a result, we canuse the same technique that we use for defining recursive session types also for shared recursivesession types. Below we show how the shared session type
SharedCounter can be defined in Ferrite: type SharedCounter = LinearToShared < SendValue < u64, Z > >;
Compared to linear recursive session types, the main difference is that instead of using
Fix , ashared session type is defined using a new
LinearToShared construct. This corresponds to the ↑ SL in SILL R , with the inner type SendValue
The struct
LinearToShared is parameterized by a linear session type F that implements SharedTypeApp
SharedTypeApp trait instead of the
TypeApp trait to ensure that the ses-sion type is strictly equi-synchronizing [12, 14], requiring an acquired session to be released tothe same type at which it was previously acquired. Ferrite enforces this requirement by omit-ting an implementation of
SharedTypeApp for
End , ruling out invalid shared session types such as
LinearToShared< SendValue
SharedToLinear , which corresponds to ↓ SL in SILL R .The struct SharedToLinear is also parameterized by F , but without the TypeApp constraint. Since
SharedToLinear and
LinearToShared are mutually recursive, the type parameter F alone is sufficient forreconstructing the type LinearToShared
SharedToLinear
Protocol traitis implemented for linear session types in Ferrite, including
SharedToLinear . A new trait
SharedProtocol is also defined for identifying shared session types, i.e.
LinearToShared .Once a shared process is started, a shared channel is created to allow multiple clients to ac-cess the shared process through the shared channel. The code below shows the definition of the
SharedChannel struct in Ferrite. Unlike linear channels, shared channels follow structural typing , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 19 rules in the same way as functional variables, i.e., they can be weakened or contracted. This meansthat we can delegate the handling of shared channels to Rust as long as shared channels implementRust’s trait
Clone to permit contraction. struct SharedChannel < S: SharedProtocol > { ... }impl < S: SharedProtocol > Clone for SharedChannel < S > { ... }
On the client side, a
SharedChannel serves as an endpoint for interacting with a shared processrunning in parallel. To start the execution of such a shared process, a corresponding Ferrite pro-gram has to be defined and executed. Similar to
PartialSession , we define
SharedSession as shownbelow to represent such a shared Ferrite program. struct SharedSession < S: SharedProtocol > { ... }
Just as
PartialSession encodes linear Ferrite programs without executing them,
SharedSession en-codes shared Ferrite programs without executing them. Since
SharedSession does not implementthe
Clone trait, the shared Ferrite program itself is affine and cannot be shared. To enable sharing,the shared Ferrite program must first be executed using the run_shared_session defined below. Thefunction takes a shared Ferrite program of type
SharedSession and starts running it in the back-ground as a shared process. Then in parallel, the shared channel of type
SharedChannel is returnedto the caller, which can then be passed to multiple clients for accessing the shared process. fn run_shared_session < S: SharedProtocol >( session : SharedSession < S > ) -> SharedChannel < S >
Below is a high level overview of how a shared session type program is defined and used in Fer-rite. The shared client counter_client is defined as a function that accepts a shared channel counter oftype
SharedChannel
Session
SharedSession
SharedChannel
Based on the work by Balzer and Pfenning [12], Ferrite achieves safecommunication for shared session types by treating the linear portion of the process as a criticalsection and enclosing it within acquire and release. The
SharedChannel works as an alias to theshared process running in the background, and clients having a reference to the
SharedChannel can acquire an exclusive linear channel to communicate with the shared process. During the lifetime ofthe linear channel, the shared process is locked and cannot be acquired by other clients. With thestrictly equi-synchronizing constraint in place, the linear channel is eventually released throughthe
SharedToLinear session type back to the same shared session type at which it was acquired. At , Vol. 1, No. 1, Article . Publication date: September 2020. this point one of the other clients that are waiting to acquire the shared process will get the nextexclusive access, and the cycle repeats.
This section reflects on our choice of host language and the guarantees provided by Ferrite andconcludes with directions for future work.
We chose Rust as the host language for Ferrite, as we believe in Rust’s potential for writing sessiontype applications. Having the tagline "Fearless Concurrency" as one of its selling points, Rusthas put much effort in its language design to ensure that concurrent programs can be writtensafely and efficiently. Rust has many unique features such as ownership, borrowing, and lifetimes,amounting to an affine type system, which make it well suited for high performance applications.For instance, Rust allows values to be safely modified without having to do expensive copying,under specific conditions enforced by the type system. Rust also provides a broad selection ofhigh-level concurrency libraries, such as futures, channels, async/await, which Ferrite makes useof without having to implement the concurrency primitives from scratch.Particularly conducive for the purpose of implementing an embedded DSL is Rust’s support offunctional programming constructs, such as parametric polymorphism, traits (type classes), andassociated types (type families). Although Rust still lacks some more advanced language features,such as higher-kinded types and data kinds, we managed to find alternative ways, such as usingtraits as indirection to implement recursive session types in Ferrite. Rust moreover provides excel-lent support for type inference, making it possible for Ferrite programs to be written with minimaltype annotations needed.
A natural question to ask is what the guarantees are that Ferrite provides. Since Ferrite is a directembedding of
SILL R , it enjoys the properties of SILL R , assuming the absence of implementationerrors. SILL R in turn comprises the language SILL S [12], including value input and output from itsprecursor SILL [9], extended with Rust statements. Both
SILL S and SILL are proved safe, ensuringprotocol adherence in particular. As such these guarantees directly translate into
SILL R and Ferrite,given Ferrite’s judgmental embedding of SILL R . This means that as long as a programmer only usesthe Ferrite library constructs made available, the session type programs are guaranteed to complywith their defined protocol. However, since SILL R includes arbitrary Rust statements and allowsthe input and output of arbitrary Rust values, a Ferrite session type program is subject to all theerrors that a standard Rust program can suffer from. As a result, a Rust panic! may be the result ofexecuting a Ferrite session type program.The current implementation of Ferrite simply propagates panics to the main application. A fu-ture improvement would be to provide explicit handling of panics raised within a Ferrite program,so that end users are given the possibility to recover from runtime errors. For example, one optionwould be to catch panics from certain Ferrite sub-programs and convert the offered session typeto an internal choice. Alternatively, we may consider extending Ferrite’s session type language toadd explicit language constructs for safe error handling in the spirit of Fowler et al. [32].An interesting endeavor beyond the scope of this work is the proving of properties of interest ofcombined Ferrite/Rust code. For example, even though the linear fragment of SILL S is proved to bedeadlock-free, deadlocks may still come about from embedded Rust code. In order to reason aboutdeadlock-freedom of a combined Ferrite/Rust program, the semantics of SILL R must be integrated , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 21 with a semantics of Rust. The RustBelt [33] or Oxide [34] verification efforts for Rust offer valuablestarting points for such an endeavor. N-ary Choice.
Although not mentioned in this article, the current implementation of Ferritesupports binary versions of both internal and external choice . There are some fine details of howFerrite deals with the affinity constraints for branching operations and how we work around thelack of support for higher-rank types in Rust to encapsulate the continuation variants in differentbranches. The page limit unfortunately precludes a discussion thereof. Currently under develop-ment is the support of n-ary choice, of which we have a working prototype using row polymorphism and prisms . In profunctor optics, prisms are the duals of lenses and as such allow “injection of asession type into a sum” as opposed to its “projection from a product”. Viewing the linear contextas a product of session types and a choice as a sum of session types, the notions of lenses andprims provide powerful abstractions for context and choice manipulations.
Deep Embedding.
Ferrite currently uses the async-std library for spawning Ferrite processes aslightweight async tasks, and uses async channels for communication. There exist many competingconcurrency runtimes for Rust other than async-std , and it may be useful if end users could choosefrom different runtimes when running Ferrite programs. Some users may even want to opt out ofasynchronous programming and instead use native threads for Ferrite processes.Unfortunately, such flexibility is not possible with Ferrit’s current implementation because Fer-rite is a shallowly embedded
DSL. This means that Ferrite programs cannot be interpreted in alter-native ways, such as using a different concurrency library or channel implementation. We plan toexplore a deep embedding of Ferrite to allow multiple interpretations of Ferrite programs. Such anapproach has been explored by Lindley and Morris [21] in the context of Haskell. A correspondingdeep embedding in Rust, however, may face challenges due to limitations of Rust’s type systemmentioned in Section 6.1.
Nested Recursive Session Types.
Section 5.1 discusses how the type Z is used as the type-levelrecursion point for a recursive session type definition inside Fix . Generalizing this idea further, itis possible to define nested recursive session types with one
Fix<...> session type nested insideanother
Fix . The support of such nested recursive types is possible because we can implement
TypeApp for
Fix itself, essentially lifting
Fix from
Type to Type -> Type . A nested recursive sessiontype would have multiple recursion points, and requires the usage of
S
Fix . We currently have a working prototype for nested recursive session types inFerrite, with some details left for improving its usability. We plan to further investigate possibleuse cases for nested recursive session types.
Static vs. Dynamic Guarantees.
Session type programs require linear usage of channels to achievesession fidelity. Since the majority of host languages do not have a linear type system, the linearusage of channels has to be enforced in some other ways. Ferrite follows the approach by Pucellaand Tov [19], Imai et al. [20], Padovani [35], Lindley and Morris [21], Imai et al. [23], and staticallytype-checks session type programs, i.e., non-linear use of channels will raise a compile-time error.In contrast, Jespersen et al. [24] and Kokke [25] rely on Rust’s affine type system to statically pre-vent session type channels to be used more than once, but rely on dynamic detection of channelsthat are closed without being used.In addition to enforcing linear usage of channels, there are also other static guarantees of interest.In particular, the linear use of higher-order channels is enforced by Ferrite, Imai et al. [20] and Imai , Vol. 1, No. 1, Article . Publication date: September 2020. et al. [23]; while Padovani [35] relies on dynamic checking to ensure that sent channels are usedlinearly. Ferrite also ensures statically that session type programs can only be executed when fullylinked. Existing works such as Pucella and Tov [19], Imai et al. [20], and Imai et al. [23], on theother hand, require manual linking of session type programs, making it possible for a provider torun without any client to communicate with running at the same time.
Intuitionistic vs. Classical Session Types.
The works by Pucella and Tov [19], Imai et al. [20],Lindley and Morris [21], and Imai et al. [23] are all based on classical linear logic formulations ofsession types [8, 36]. As a result, users are required to dualize a session type for two session typeprograms to communicate. For example, a provider offering
ReceiveValue
SendValue
Continuation Passing Style vs. Indexed Monad.
The works by Pucella and Tov [19], Imai et al. [20]Lindley and Morris [21], and Imai et al. [23] use an indexed (parameterized) monad to track thelinear usage of session type channels. An indexed monad is used to encode partial session typeprograms, which can be composed using the bind operator. The encoding requires to keep theresult type as well as the pre- and post-conditions in the indexed monad. For example, a monadicencoding of Ferrite would be something like
PartialSession
Shared Session Types.
Ferrite is the first library that implements shared session types with acquire-release semantics [12]. This is in contrast to the copying semantics available through the linearexponential. Few of the related works address the need for shared session types. One reason beingthat for languages with structural rules such as Haskell and Ocaml, shared sessions based on acopy semantics can be trivially achieved by running different instances of the same session typeprogram. Pucella and Tov [19], Imai et al. [20], and Imai et al. [23] support some form of ad hocsharing through non-linear channels that can be listened to by multiple providers and clients. Thiskind of sharing, however, is outside of the session type language and thus loses some of the safety , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 23 guarantees provided by session types. In particular, they may be no provider listening on one sideof the shared channel, causing clients on the other side to deadlock.In contrast, Ferrite’s shared channels provide the safety guarantee of session fidelity describedby Balzer and Pfenning [12], and allow true sharing of resources in a safe manner.
Linear Context.
The approach of encoding the linear context as a type-level list is used by Pucellaand Tov [19], Imai et al. [20], Lindley and Morris [21], and Imai et al. [23]. In their early works,Pucella and Tov [19] provide operations to rearrange channels in the linear context in order toaccess a target channel as the first element. Imai et al. [20] then introduce the use of contextlenses for random access and update of channels in the linear context, and the same techniqueis later adopted by Imai et al. [23]. Imai et al. [20] also introduce the use of natural numbers toimplement context lenses through type classes. Imai et al. [23], in comparison, require a hand-written implementation of a context lens for each position in the linear context, and only providecontext lenses for the first four slots by default.Ferrite’s design of the linear context and context lenses is close to the one by Imai et al. [20],with some differences and improvements. Being based on intuitionistic linear logic session types,the linear context in Ferrite holds session type channels of client polarity. In contrast, the channelsin the linear context of Imai et al. [20] is of provider polarity, and channels in linear context of Imaiet al. [23] may be of either provider or client polarity. In the work by Imai et al. [20], new contextlenses are first generated together with an empty channel slot, and then the receive channel con-struct is used to bind the received channel to the empty channel slot. In contrast, Ferrite simplifiesboth operations into a single step done by receive_channel . Higher-Order Channels.
Ferrite is one of the few libraries that implements higher-order chan-nels, also known as session delegation . Higher-order channels support the sending and receivingof channels along other channels. Jespersen et al. [24] and Kokke [25] support higher-order chan-nels, but without enforcing the linear usage of the sent channels statically. Other than Ferrite, Imaiet al. [20] and Imai et al. [23] also support higher-order channels with static linearity enforcement.For Imai et al. [23], the sending of a channel needs to be accompanied with a polarity tag to identifywhether the channel sent is of provider or client polarity.
Managed Concurrency.
Many of the related works do not manage the concurrency aspects ofspawning multiple session type processes and linking them for communication. Jespersen et al.[24] and Kokke [25] pass around channels as function arguments, and require the users to useexternal methods such as fork to run multiple processes in parallel. Pucella and Tov [19] introducea
Rendezvous type, which acts as a non-linear channel with two endpoints with session types thatare dual to each other. The user then has to use fork to spawn a separate thread, and run eitherthe provider or client by providing the respective endpoint of the channel. Since the
Rendezvous type is non-linear, there is no guarantee that a provider must have exactly one counterpart clientfor communication. Imai et al. [23] also provide a non-linear channel similar to
Rendezvous forcommunication, with the two endpoints having the same session type but with opposite polarities.In contrast, Ferrite manages the concurrency and communication on behalf of the end user, en-suring that a provider is always paired with exactly one client and that both proceses alwaysstart executing at the same time. Ferrite provides constructs such as cut , include_session , and apply_channel for linking multiple Ferrite programs. Imai et al. [20] offer a similar way of linkingmultiple session type programs. Users need to first use new to allocate an empty slot in the linearcontext, and then use fork on a provider. The continuation then has the dual session type of theprovider in the post type of the indexed monad, so that the program can continue as the client. , Vol. 1, No. 1, Article . Publication date: September 2020., Vol. 1, No. 1, Article . Publication date: September 2020.
Rendezvous forcommunication, with the two endpoints having the same session type but with opposite polarities.In contrast, Ferrite manages the concurrency and communication on behalf of the end user, en-suring that a provider is always paired with exactly one client and that both proceses alwaysstart executing at the same time. Ferrite provides constructs such as cut , include_session , and apply_channel for linking multiple Ferrite programs. Imai et al. [20] offer a similar way of linkingmultiple session type programs. Users need to first use new to allocate an empty slot in the linearcontext, and then use fork on a provider. The continuation then has the dual session type of theprovider in the post type of the indexed monad, so that the program can continue as the client. , Vol. 1, No. 1, Article . Publication date: September 2020., Vol. 1, No. 1, Article . Publication date: September 2020. This material is based upon work supported by the National Science Foundation under GrantNo. CCF-1718267. Any opinions, findings, and conclusions or recommendations expressed in thismaterial are those of the author(s) and do not necessarily reflect the views of the National ScienceFoundation.
REFERENCES [1] Andrew Gerrand. The go blog: Share memory by communicating, 2010. URLhttps://blog.golang.org/share-memory-by-communicating.[2] Steve Klabnik and Carol Nichols. The Rust programming language, 2018. URL https://doc.rust-lang.org/stable/book/.[3] Servo. Servo source code - canvas paint thread, 2020. URL https://github.com/servo/servo/blob/c67b3d71e23f10ba2049bdd9aed822b19ed8527f/components/canvas/canvas_paint_thread.rs.[4] Kohei Honda. Types for dyadic interaction. In , pages509–523. Springer, 1993.[5] Kohei Honda, Vasco T. Vasconcelos, and Makoto Kubo. Language primitives and type discipline for structuredcommunication-based programming. In , pages 122–138. Springer,1998.[6] Kohei Honda, Nobuko Yoshida, and Marco Carbone. Multiparty asynchronous session types. In , pages 273–284. ACM, 2008.[7] Luís Caires and Frank Pfenning. Session types as intuitionistic linear propositions. In , pages 222–236. Springer, 2010.[8] Philip Wadler. Propositions as sessions. In , pages 273–286. ACM, 2012.[9] Bernardo Toninho, Luís Caires, and Frank Pfenning. Higher-order processes, functions, and sessions: a monadicintegration. In , pages 350–369. Springer, 2013. doi: https://doi.org/10.1007/978-3-642-37036-6_20.[10] Bernardo Toninho.
A Logical Foundation for Session-based Concurrent Computation . PhD thesis, Carnegie MellonUniversity and New University of Lisbon, 2015.[11] Luís Caires, Frank Pfenning, and Bernardo Toninho. Linear logic propositions as session types.
Mathematical Struc-tures in Computer Science , 26(3):367–423, 2016.[12] Stephanie Balzer and Frank Pfenning. Manifest sharing with session types.
Proceedings of the ACM on ProgrammingLanguages (PACMPL) , 1(ICFP):37:1–37:29, 2017.[13] Stephanie Balzer, Frank Pfenning, and Bernardo Toninho. A universal session type for untyped asynchronous com-munication. In , LIPIcs, pages 30:1–30:18. SchlossDagstuhl - Leibniz-Zentrum fuer Informatik, 2018.[14] Stephanie Balzer, Bernardo Toninho, and Frank Pfenning. Manifest deadlock-freedom for shared session types. In , volume 11423 of
Lecture Notes in Computer Science , pages 611–639.Springer, 2019.[15] Raymond Hu, Nobuko Yoshida, and Kohei Honda. Session-based distributed programming in Java. In , volume 5142 of
Lecture Notes in Computer Science , pages 516–541. Springer, 2008. doi: 10.1007/978-3-540-70592-5\_22. URL https://doi.org/10.1007/978-3-540-70592-5_22.[16] Raymond Hu, Dimitrios Kouzapas, Olivier Pernet, Nobuko Yoshida, and Kohei Honda. Type-safe eventful sessions inJava. In , volume 6183 of
Lecture Notes in ComputerScience , pages 329–353. Springer, 2010. doi: 10.1007/978-3-642-14107-2_16.[17] Alceste Scalas and Nobuko Yoshida. Lightweight session programming in Scala. In , volume 56 of
Leibniz International Proceedings in Informatics (LIPIcs) , pages21:1–21:28. Schloss Dagstuhl – Leibniz-Zentrum fuer Informatik, 2016.[18] Matthew Sackman and Susan Eisenbach. Session types in haskell: Updating message passing for the 21st century.Technical report, Imperial College, 2008. URL http://hdl.handle.net/10044/1/5918.[19] Riccardo Pucella and Jesse A. Tov. Haskell session types with (almost) no class. In , pages 25–36. ACM, 2008. doi: 10.1145/1411286.1411290.[20] Keigo Imai, Shoji Yuen, and Kiyoshi Agusa. Session type inference in haskell. In , volume 69 of
EPTCS , pages 74–91, 2010. doi: 10.4204/EPTCS.69.6.[21] Sam Lindley and J. Garrett Morris. Embedding session types in Haskell. In ,pages 133–145. ACM, 2016. doi: 10.1145/2976002.2976018. URL https://doi.org/10.1145/2976002.2976018., Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 25 [22] Luca Padovani. A simple library implementation of binary sessions.
Journal of Functional Programming , 27:e4, 2017.doi: 10.1017/S0956796816000289.[23] Keigo Imai, Nobuko Yoshida, and Shoji Yuen. Session-ocaml: a session-based library with polarities and lenses.
Scienceof Computer Programming , 172:135–159, 2019. doi: 10.1016/j.scico.2018.08.005.[24] Thomas Bracht Laumann Jespersen, Philip Munksgaard, and Ken Friis Larsen. Session types for Rust. In , 2015. doi: 10.1145/2808098.2808100.[25] Wen Kokke. Rusty variation: Deadlock-free sessions with failure in rust. In , pages 48–60, 2019.[26] J. Nathan Foster, Michael B. Greenwald, Jonathan T. Moore, Benjamin C. Pierce, and Alan Schmitt. Combinators forbidirectional tree transformations: A linguistic approach to the view-update problem.
ACM Trans. Program. Lang.Syst. , 29(3):17, 2007. doi: 10.1145/1232420.1232424. URL https://doi.org/10.1145/1232420.1232424.[27] Matthew Pickering, Jeremy Gibbons, and Nicolas Wu. Profunctor optics: Modular data acces-sors.
Programming Journal , 1(2):7, 2017. doi: 10.22152/programming-journal.org/2017/1/7. URLhttps://doi.org/10.22152/programming-journal.org/2017/1/7.[28] John C. Reynolds. Definitional interpreters for higher-order programming languages. In
ACM Annual Conference ,volume 2, pages 717–740. ACM, 1972. doi: 10.1145/800194.805852.[29] Jeremy Yallop and Leo White. Lightweight higher-kinded polymorphism. In
Functional and Logic Programming -12th International Symposium, FLOPS 2014, Kanazawa, Japan, June 4-6, 2014. Proceedings , pages 119–135, 2014. doi:10.1007/978-3-319-07151-0\_8. URL https://doi.org/10.1007/978-3-319-07151-0_8.[30] Karl Crary, Robert Harper, and Sidd Puri. What is a recursive module? In
ACM SIGPLAN Conference on ProgrammingLanguage Design and Implementation (PLDI) , pages 50–63, 1999.[31] Erik Meijer, Maarten M. Fokkinga, and Ross Paterson. Functional programming with bananas, lenses, en-velopes and barbed wire. In
Functional Programming Languages and Computer Architecture, 5th ACM Conference,Cambridge, MA, USA, August 26-30, 1991, Proceedings , pages 124–144, 1991. doi: 10.1007/3540543961\_7. URLhttps://doi.org/10.1007/3540543961_7.[32] Simon Fowler, Sam Lindley, J. Garrett Morris, and Sára Decova. Exceptional asynchronous session types: Session typeswithout tiers.
Proceedings of the ACM on Programming Languages , 3(POPL):28:1–28:29, 2019. doi: 10.1145/3290341.[33] Ralf Jung, Jacques-Henri Jourdan, Robbert Krebbers, and Derek Dreyer. Rustbelt: securing the foundations of therust programming language.
Proc. ACM Program. Lang. , 2(POPL):66:1–66:34, 2018. doi: 10.1145/3158154. URLhttps://doi.org/10.1145/3158154.[34] Aaron Weiss, Daniel Patterson, Nicholas D. Matsakis, and Amal Ahmed. Oxide: The essence of rust.
CoRR ,abs/1903.00982, 2019. URL http://arxiv.org/abs/1903.00982.[35] Luca Padovani. A Simple Library Implementation of Binary Sessions. working paper or preprint, October 2015. URLhttps://hal.archives-ouvertes.fr/hal-01216310.[36] Simon J. Gay and Vasco Thudichum Vasconcelos. Linear type theory for asynchronous session types.
J. Funct.Program. , 20(1):19–50, 2010. doi: 10.1017/S0956796809990268. URL https://doi.org/10.1017/S0956796809990268., Vol. 1, No. 1, Article . Publication date: September 2020.
Table 3. Overview of session terms in SILL R and Ferrite. SILL R Ferrite Term Description ϵ End terminate ; Terminate session. wait a ; K Wait for channel a to close. τ ⊲ A ReceiveValue detach _ shared _ session ; K s Detach linear session and continue asshared session K s . release _ shared _ session a ; K l Release acquired linear session.-
Fix
Roll up session type
F::Applied offered by cont . unfix_session_for(a, cont) Unroll channel a to session type F::Applied in cont . A TYPING RULESA.1 Typing Rules for
SILL R Following is a list of inference rules in
SILL R . Communication Γ ; ∆ ⊢ a :: A Γ ; ∆ , a ′ : A ⊢ b :: B Γ ; ∆ , ∆ ⊢ a ′ ← cut a ; b :: B (T-cut) Γ ; · ⊢ a :: A Γ ; ∆ , a ′ : A ⊢ b :: B Γ ; ∆ ⊢ a ′ ← include a ; b :: B (T-incl) Γ ; · ⊢ f :: A ⊸ B Γ ; · ⊢ a :: A Γ ; · ⊢ apply _ channel f a :: B (T-app) Γ ; a : A ⊢ forward a :: A (T-fwd) Termination Γ ; · ⊢ terminate ; :: ϵ (T1 R ) Γ ; ∆ ⊢ K :: A Γ ; ∆ , a : ϵ ⊢ wait a ; K :: A (T1 L ) Receive Value , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 27 Γ , x : τ ; ∆ ⊢ K :: A Γ ; ∆ ⊢ x ← receive _ value ; K :: τ ⊲ A (T ⊲ R ) Γ ; ∆ , a : A ⊢ K :: B Γ , x : τ ; ∆ , a : τ ⊲ A ⊢ send _ value _ to a x ; K :: B (T ⊲ L ) Send Value Γ ; ∆ ⊢ K :: A Γ , x : τ ; ∆ ⊢ send _ value x ; K :: τ ⊳ A (T ⊳ R ) Γ , a : τ ; ∆ , a : A ⊢ K :: A Γ ; ∆ , a : τ ⊲ A ⊢ x ← receive _ value _ from a ; K :: B (T ⊳ L ) Receive Channel Γ ; ∆ , a : A ⊢ K :: B Γ ; ∆ ⊢ a ← receive _ channel ; K :: A ⊸ B (T ⊸ R ) Γ ; ∆ , f : A ⊢ K :: B Γ ; ∆ , f : A ⊸ A , a : A ⊢ send _ channel _ to f a ; K :: B (T ⊸ L ) Send Channel Γ ; ∆ ⊢ K :: B Γ ; ∆ , a : A ⊢ send _ channel _ from a ; K :: A ⊗ B (T ⊗ R ) Γ ; ∆ , f : A , a : A ⊢ K :: B Γ ; ∆ , f : A ⊗ A ⊢ a ← receive _ channel _ from f ; K :: B (T ⊗ L ) External Choice Γ ; ∆ ⊢ K l :: A Γ ; ∆ ⊢ K r :: B Γ ; ∆ ⊢ offer _ choice K l K r :: A N B (T N R ) Γ ; ∆ , a : A ⊢ K :: B Γ ; ∆ , a : A N A ⊢ choose _ left a ; K :: B (T N L ) Γ ; ∆ , a : A ⊢ K :: B Γ ; ∆ , a : A N A ⊢ choose _ right a ; K :: B (T N L ) Internal Choice Γ ; ∆ ⊢ K :: A Γ ; ∆ ⊢ offer _ left ; K :: A ⊕ B (T ⊕ R ) Γ ; ∆ ⊢ K :: B Γ ; ∆ ⊢ offer _ right ; K :: A ⊕ B (T ⊕ R ) Γ ; ∆ , a : A ⊢ K l :: B Γ ; ∆ , a : A ⊢ K r :: B Γ ; ∆ , a : A ⊕ A ⊢ case a K l K r :: B (T N L ) Shared Session Types Γ ; · ⊢ K l :: A Γ ; · ⊢ accept _ shared _ session ; K l :: ↑ SL A (T ↑ SL R ) Γ ; · ⊢ K s :: S Γ ; · ⊢ detach _ shared _ session ; K s :: ↓ SL S (T ↓ SL R ) Γ ; ∆ , a : A ⊢ K :: B Γ , s : ↑ SL A ; ∆ ⊢ a ← acquire _ shared _ session s ; K :: B (T ↑ SL L ) Γ , s : S ; ∆ ⊢ K :: B Γ ; ∆ , a : ↓ SL S ⊢ s ← release _ shared _ session a ; K :: B (T ↓ SL L ) A.2 Typing Constructs for Ferrite
Following is a list of function signatures of the term constructors provided in Ferrite.
Forward fn forward < N, C, A > ( n : N ) -> PartialSession < C, A >where A : Protocol, C : Context, N::Target : EmptyContext, N : ContextLens < C, A, Empty >
Termination fn terminate < C: EmptyContext >() -> PartialSession < C, End > fn wait < C: Context, A: Protocol,N: ContextLens < C, End, Empty > >( n: N, cont: PartialSession < N::Target, A > )-> PartialSession < C, A >
Communication , Vol. 1, No. 1, Article . Publication date: September 2020. fn cut < C1, C2, C3, C4, A, B >( cont1 : PartialSession < C3, B >,cont2 : PartialSession < C2, A > )-> PartialSession < C4, B >where A : Protocol, B : Protocol,C1 : Context, C2 : Context,C3 : Context, C4 : Context,C1 : AppendContext < ( A, () ), Appended = C3 >,C1 : AppendContext < C2, Appended = C4 >, fn include_session < C, A, B >( session : Session < A >,cont : impl FnOnce ( C :: Length )-> PartialSession < C::Appended, B > )-> PartialSession < C, B >where A : Protocol, B : Protocol, C : Context,C : AppendContext < ( A, () ) >fn apply_channel < A, B >( f : Session < ReceiveChannel < A, B > >,a : Session < A > )-> Session < B >where A : Protocol, B : Protocol
Receive Value fn receive_value < T, C, A, Fut >( cont : impl FnOnce (T) -> Fut + Send + 'static )-> PartialSession
Send Value fn send_value < T, C, A >( val : T, cont : PartialSession < C, A > )-> PartialSession < C, SendValue < T, A > >where T: Send + 'static, A: Protocol, C: Contextfn send_value_async < T, C, A, Fut >( cont_builder: implFnOnce () -> Fut + Send + 'static )-> PartialSession < C, SendValue < T, A > >where T: Send + 'static, A: Protocol, C: Context,Fut : Future < Output =( T, PartialSession < C, A > ) > + Send fn receive_value_from < N, C, T, A, B, Fut >( n : N,cont : impl FnOnce (T) -> Fut + Send + 'static )-> PartialSession < C, B >where A : Protocol, B : Protocol, C : Context,T : Send + 'static,Fut : Future < Output =PartialSession < N :: Target, B > > + Send,N : ContextLens < C, SendValue < T, A >, A >
Receive Channel fn receive_channel < C, A, B >( cont : impl FnOnce ( C::Length ) ->PartialSession < C::Appended, B > )-> PartialSession < C, ReceiveChannel < A, B > >where A : Protocol, B : Protocol,C : AppendContext < ( A, () ) > fn send_channel_to < N1, N2, C, A1, A2, B >( n1 : N1, n1 : N2,cont : PartialSession < N1::Target, B > )-> PartialSession < C, B >where C : Context, B : Protocol,A1 : Protocol, A2 : Protocol,N1 : ContextLens < N2::Target,ReceiveChannel < A1, A2 >, A2 >,N2 : ContextLens < C, A1, Empty >
Send Channel fn send_channel_from < N, C, A, B >( n : N,cont: PartialSession < N::Target, B > )-> PartialSession < C, SendChannel < A, B > >where C : Context, A : Protocol, B : Protocol,N : ContextLens < C, A, Empty > fn receive_channel_from < C1, C2, A1, A2, B, N >( n : N,cont: impl FnOnce ( C2::Length )-> PartialSession < C2::Appended, B > )-> PartialSession < C1, B >where A1 : Protocol, A2 : Protocol,B : Protocol, C1 : Context,C2 : AppendContext < ( A1, () ) >,N : ContextLens < C1,SendChannel < A1, A2 >, A2, Target = C2 >
Recursive Session Types , Vol. 1, No. 1, Article . Publication date: September 2020. errite: A Judgmental Embedding of Session Types in Rust 29 fn fix_session < F, A, C >( cont: PartialSession < C, A > )-> PartialSession < C, Fix < F > >where C : Context, F : Protocol, A : Protocol,F : TypeApp < Fix < F >, Applied = A > fn unfix_session_for < N, C, A, B, F >( n : N,cont : PartialSession < N::Target, B > )-> PartialSession < C, B >where A : Protocol, B : Protocol, C : Context,F : Protocol + TypeApp < Fix < F >, Applied = A >,N : ContextLens < C, Fix < F >, A, >
Shared Session Types fn accept_shared_session < F >( cont : PartialSession <( Lock < F >, () ), F::Applied > )-> SharedSession < LinearToShared < F > >where F::Applied : Protocol,F : Protocol + SharedTypeApp < SharedToLinear < F > >fn detach_shared_session < F, C >( cont : SharedSession < LinearToShared < F > > )-> PartialSession <( Lock < F >, C ), SharedToLinear < F > >where C : EmptyContext, F::Applied : Protocol,F : Protocol + SharedTypeApp < SharedToLinear < F > > fn acquire_shared_session < F, C, A, Fut >( shared : SharedChannel < LinearToShared < F > >,cont : impl FnOnce ( C :: Length ) -> Fut+ Send + 'static )-> PartialSession < C, A >where C : Context, A : Protocol, F::Applied : Protocol,F : Protocol + SharedTypeApp < SharedToLinear < F > >,C : AppendContext < ( F::Applied , () ) >,Fut : Future < Output =PartialSession < C::Appended, A > > + Send,fn release_shared_session < N, C, F, A >( n : N,cont : PartialSession < N::Target, A > )-> PartialSession < C, A >where A : Protocol, C : Context,F : Protocol + SharedTypeApp < SharedToLinear < F > >,N : ContextLens < C, SharedToLinear < F >, Empty >
External Choice type InjectCont < C, A, B > =Either
Internal Choice , Vol. 1, No. 1, Article . Publication date: September 2020. struct ContSum < C1, C2, A >where C1 : Context, C2 : Context, A : Protocol{ result: Either
B EXTENDED EXAMPLESB.1 Derivation Tree for Hello World Example
We can show how that the function receive_value embeds the inference rule ( T ⊲ R ), by visualizing itas extending Rust’s type system with an additional inference rule shown below. The judgments aregiven a Rust context Ψ , with variables in subject to Rust typing rules. From the inference rule wecan see that it shares the the essence of ( T ⊲ R ), with the same linear context C in both the premiseand conclusion, and the offered session type changing from A in the premise to ReceiveValue
Similarly, we can think of the function send_value_to as introducing the inference rule shownbelow to Rust’s type system. The notation L ⇒ ContextLens<...> is used to denote that the type L implements a specific ContextLens instance in Rust. Notice that the type of l is not changed in thecontinuation, but the type L may implement a more than one instances of ContextLens to be usedin the continuation. Ψ , l: L ⊢ cont : PartialSession < L::Target, B > L ⇒ ContextLens < C,ReceiveValue
We can build the derivation tree of hello_provider as follows: Ψ , name : String ⊢ println!("Hello, {}!", name); terminate() : PartialSession < (), End > Ψ ⊢ |name| { println!("Hello, {}!", name); terminate() } : impl FnOnce(String) -> PartialSession < (), End > Ψ ⊢ receive_value(|name|{ println!("Hello, {}!", name); terminate() }) : PartialSession < (), ReceiveValue
The first part of derivation tree of hello_client , of how a received channel is bound to the linearcontext, is shown as follows: D Ψ , a: Z ⊢ send_value_to(a, ...) : PartialSession <(ReceiveValue
With the received channel and context lens in the environment, The second part of derivationtree of hello_client , D , is continued as follows: (Empty, ()) ⇒ EmptyContext Ψ ⊢ terminate() : PartialSession <(Empty,()), End > Z ⇒ ContextLens <(End,()),End, Empty,Target=(Empty,()) > Ψ , a: Z ⊢ wait(a, terminate()) : PartialSession <(End, ()), End > Z ⇒ ContextLens <(ReceiveValue
B.2 Communication
B.2.1 Include Session.
Other than the general cut rule, Ferrite also provides a more restrictedversion of cut called include : , Vol. 1, No. 1, Article . Publication date: September 2020. Γ ; · ⊢ a :: A Γ ; ∆ , x : A ⊢ b :: B Γ ; ∆ ⊢ x ← include a ; b :: B ( T -incl ) The typing rule
T-incl is nearly identical to
T-cut , with the additional restriction that the linearcontext for a must be empty. This restriction makes it much easier for the Rust compiler to inferthe type for ∆ , since there is no longer a need to split it into two parts for the two continuations.Without the linear context splitted, the existing context lenses can also be preserved, making themusable before and after an include . It is also clear that T-incl is derivable from T-cut , by simplyunifiying ∆ in T-cut with · . As a result, introducing T-incl does not affect the properties of sessiontypes.
T-incl is implemented in Ferrite as the include_session function as follow: fn include_session < A: Protocol, B: Protocol, C: Context >( a: Session < A >,cont: impl FnOnce ( C::Length )-> PartialSession < C::Appended, B >) -> PartialSession < C, B >where C : AppendContext < ( A, () ) >, include_session takes a Ferrite program of type
Session and appends the offered channel tothe linear context C . Similar to receive_channel , include_session generates a context lens of type C::Length , which is used by the continuation closure cont to access the appended channel A to C . cont returns a PartialSession < C::Appended, B > , indicating that it offers session type B using thelinear context C appended with A . Finally include_session returns PartialSession < C, B > , whichworks on the original linear context C and offers session type B .The apply_channel construct is implemented using include_session , send_channel_to , and forward asshown below. fn apply_channel < A: Protocol, B: Protocol >( f: Session < ReceiveChannel >, a: Session < A > )-> Session < B >{ include_session ( f, | chan_f | {include_session ( a, | chan_a | {send_channel_to ( chan_f, chan_a,forward ( chan_f ) ) }) }) } We can prove that the typing rule
T-app is derivable from
T-cut , as shown below. Γ ; f ′ : B ⊢ forward f ′ :: B Γ ; f ′ : A ⊸ B , a : A ⊢ send _ channel _ to f ′ a ′ ; forward f ′ :: B ... Γ ; · ⊢ a :: A Γ ; f ′ : A ⊸ B ⊢ a ′ ← cut a ; send _ channel _ to f ′ a ′ ; forward f ′ :: B ... Γ ; · ⊢ f :: A ⊸ B Γ ; · ⊢ f ′ ← cut f ; a ′ ← cut a ; send _ channel _ to f ′ a ′ ; forward f ′ ; :: B Γ ; · ⊢ apply _ channel f a :: B B.3 Recursive Session Types
B.3.1 Unrolling Session Types.
The function unfix_session_for shown below works with a contextlens N operating on a linear context C to unroll a recursive session type Fix
N::Target is the result of replacing
Fix
Counter channeloffered by stream_producer as follows: fn stream_client () -> Session < ReceiveChannel < Counter, End > > { receive_channel ( | stream | { unfix_session_for ( stream, receive_value_from ( stream, async move | count | { println!("Received value: {}", count); include_session ( stream_client (), | next | { send_channel_to ( next, stream, forward ( next ) ) }) }) ) }) } The function stream_client is a bit more involved than stream_server , so we will go through thesteps one line at a time. In the first line, we define stream_client to be a recursive function withno argument that returns a
Session< ReceiveChannel
Counter and then terminates. In the body, receive_channel is used toreceive the
Counter channel, and the continuation closure binds the stream context lens for accessingthe
Counter channel in the linear context. Inside the continuation closure (line 3), the expression unfix_session_for (...) have the type
PartialSession< (Counter, ()), End > , so it has one channel ofsession type
Counter in the linear context. Using the context lens stream , unfix_session_for(stream,...) unrolls Counter so that the continuation at line 4 have the type
PartialSession< (SendValue
SendValue
PartialSession<(Counter, ()), End > , which is the same type as in line 3. Experienced functional programmers mayrecognize that we could have done some inner recursion to get back to line 3, however this is notan option in Rust as inner recursive closure is not supported. Instead, we need to find some wayof doing recursion back to stream_client , but this would require some way of converting the Rusttype from
Session< ReceiveChannel
Session
PartialSession< (Counter, (ReceiveChannel
Counter and the context lens next boundto the second channel
ReceiveChannel < Counter, End > . Comparing the session types of stream and next , we can notice that stream can be sent to next , which can be done using send_channel_to(stream,next, ...) . Finally in the continuation in line 10, we need to produce the Rust type
PartialSession<(Empty, (End, ())), End > . This can be done by forwarding the channel next using forward ( next ) ,which then the empty linear context allows completion of the Ferrite program. , Vol. 1, No. 1, Article . Publication date: September 2020., Vol. 1, No. 1, Article . Publication date: September 2020.