Turmeric supports session types -- a type discipline that statically verifies
communication protocols between concurrent processes. The feature is enabled with
the -Xsessions compiler flag.
Binary session types describe a two-party communication channel. One end of the
channel runs a protocol P; the other end runs the dual protocol dual(P).
;; Allocate a two-ended channel for the protocol Send<int, Close>.
(let [[a b] (make-session (Send int Close))]
...)
The pair [a b] gives both endpoints. Endpoint a has type
Session[Send int Close]; endpoint b has the dual type
Session[Recv int Close].
send -- advance the sender side by one message:
(let [a (send a 42)] ; a advances from Send<int,Close> to Close
(close a))
recv -- advance the receiver side by one message; returns [value new-ch]:
(let [[v b] (recv b)] ; v = 42, b advances from Recv<int,Close> to Close
(close b))
close -- consume the channel after the protocol ends (type Close).
When the sender can choose between two branches use choose-left / choose-right;
the receiver uses offer and pattern-matches on Left/Right:
;; Sender side: Choose<Send int Close, Close>
(let [ch (choose-left ch)] ; picks the Left branch
(let [ch (send ch 7)]
(close ch)))
;; Receiver side: Branch<Recv int Close, Close>
(match (offer ch)
(Left ch)
(let [[n ch] (recv ch)]
(close ch))
(Right ch)
(close ch))
Rec introduces a recursive protocol variable self that unrolls on each
loop iteration:
;; Server: repeat (recv int, send int) until client closes
(defn echo-server [^linear ch :(Session (Rec self (Branch (Recv int (Send int self)) Close)))] :nil
(match (offer ch)
(Left ch)
(let [[n ch] (recv ch)]
(let [ch (send ch n)]
(echo-server ch)))
(Right ch)
(close ch)))
See stdlib/session.tur for the echo-server-loop and echo-client-call
helpers that wrap this pattern.
recv-timeout is a non-blocking receive that returns a Choice value:
(match (recv-timeout ch 500) ; 500 ms deadline
(Left [v ch]) (do (println v) (close ch))
(Right ch) (do (println "timed out") (close ch)))
The duality rule governs how the two ends of a channel relate:
| Protocol (one end) | Dual (other end) |
|---|---|
Send T P |
Recv T dual(P) |
Recv T P |
Send T dual(P) |
Choose P Q |
Branch dual(P) dual(Q) |
Branch P Q |
Choose dual(P) dual(Q) |
Close |
Close |
Rec self P |
Rec self dual(P) |
make-session accepts one protocol type and automatically produces the dual
for the second endpoint.
Session channels and algebraic effects can coexist freely. A function may perform effects while holding a linear channel, as long as the channel is consumed exactly once along every code path:
(defeffect Log [msg :cstr] :nil)
(defn logged-send [^linear ch :(Session (Send int Close)) val :int] :int
(perform (Log "before send"))
(let [ch (send ch val)]
(close ch)
0))
See tests/fixtures/session-effects/ for a complete example.
Multi-party session types generalize binary sessions to N >= 2 participants. A global protocol specifies all interactions between named roles; the compiler projects it onto each role's local view.
Declare a global protocol with defprotocol:
(defprotocol Ping [A B]
(-> A B int) ; A sends an int to B
(-> B A int)) ; B replies with an int to A
The role list [A B] names each participant. Each (-> From To type) line
is one message transfer.
After declaring a protocol, allocate role endpoints with make-protocol:
(let [[ra rb] (make-protocol Ping)]
...)
ra has type (Role Ping A) and rb has type (Role Ping B). Each role
endpoint is linear -- it must be consumed exactly once.
send-to -- send a message to a named role peer:
(let [ra (send-to ra B 42)] ; A sends 42 to B; ra advances in protocol
...)
recv-from -- receive a message from a named role peer; returns [value new-ch]:
(let [[v rb] (recv-from rb A)] ; B receives from A; v = 42
...)
close -- close the role endpoint after the protocol is complete:
(close ra)
Full two-role ping example:
(defprotocol Ping [A B]
(-> A B int)
(-> B A int))
(defn role-a [^linear ch :(Role Ping A)] :nil
(let [ch (send-to ch B 42)]
(let [[v ch] (recv-from ch B)]
(println v)
(close ch))))
(defn role-b [^linear ch :(Role Ping B)] :nil
(let [[v ch] (recv-from ch A)]
(let [ch (send-to ch A v)]
(close ch))))
(defn main [] :int
(let [[ra rb] (make-protocol Ping)]
(let [t (spawn (fn [] (role-b rb)))]
(role-a ra)
(join t)))
0)
defprotocol and make-protocol support any number of roles (N >= 2). For
a three-role pipeline:
(defprotocol Pipeline [A B C]
(-> A B int) ; A sends to B
(-> B C int)) ; B forwards to C
(defn main [] :int
(let [[ra rb rc] (make-protocol Pipeline)]
(let [ta (spawn (fn [] (role-a ra)))]
(let [tb (spawn (fn [] (role-b rb)))]
(role-c rc)
(join ta)
(join tb))))
0)
The runtime uses a shared router so all N role endpoints communicate through a single lock-based message router allocated on the heap.
At compile time the elaborator projects the global protocol onto each role's local view. Projection removes all interactions that do not involve the current role:
(-> From To T) projects to Send T rest for From, and
Recv T rest for To. For any other role R, it is transparent (role R
keeps its own remaining protocol).TUR-E0220.The projection check happens in elab_global.c when (make-protocol P) is
elaborated.
| Code | Meaning |
|---|---|
TUR-E0210 |
Session operation on a non-session type |
TUR-E0211 |
Session channel dropped (linearity violation) |
TUR-E0212 |
Session operation does not match current protocol state |
TUR-E0213 |
Protocol mismatch between session endpoints |
TUR-E0214 |
Channel used after close |
TUR-E0215 |
Channel used more than once (linearity violation) |
TUR-E0216 |
Receive-timeout used on a non-timeout protocol |
TUR-E0217 |
Offer used on a non-branch protocol |
TUR-E0218 |
Choose used on a non-choice protocol |
TUR-E0219 |
Type mismatch in session message payload |
TUR-E0220 |
Global protocol is not projectable (mergeability failure) |
TUR-E0221 |
Role not declared in the protocol |
TUR-E0222 |
Role implementation type mismatch |
TUR-E0223 |
Global protocol not well-formed (undeclared role used) |
Use tur explain to get a detailed description of any error code:
tur explain TUR-E0212
tur explain TUR-E0220
The test fixtures under tests/fixtures/session-* and tests/fixtures/errors/session-*
provide runnable examples for every feature and failure mode described in this guide.