sess

package module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 7, 2026 License: MIT Imports: 4 Imported by: 0

README

Go Reference Go Report Card Coverage Status

English | 简体中文 | Español | 日本語 | Français

sess

Session-typed communication protocols via algebraic effects on kont.

Overview

Session types assign a type to each step of a communication protocol. Each operation — send, receive, select, offer, close — is individually well-typed via Go generics, and protocol composition within a single endpoint is type-safe. Duality (matching operations across endpoints) is a programmer responsibility: the programmer writes dual protocols, and mismatches manifest at runtime as type assertion failures or deadlocks.

sess encodes session types as algebraic effects evaluated by the kont effect system. Each protocol step — send, receive, select, offer, close — is an effect that suspends the computation until the transport completes the operation. The transport returns iox.ErrWouldBlock at computational boundaries, allowing proactor event loops (e.g., io_uring) to multiplex execution without thread-blocking.

Two equivalent API families are available: Cont (closure-based, straightforward to compose) and Expr (frame-based, amortized zero-allocation for hot paths).

Composition Boundary

sess owns the session effect signature and the endpoint transport that interprets it. It uses iox.ErrWouldBlock as the non-blocking boundary for bounded queues, but does not own the full iox outcome algebra; takt owns proactor-style scheduling and completion correlation; cove owns contextual evidence for suspension-aware composition. Within sess, iox.ErrMore remains outside the endpoint transport domain and is treated as an unexpected dispatcher failure.

Installation

go get code.hybscloud.com/sess

Requires Go 1.26+.

Session Operations

Each operation has a dual. When one endpoint performs an operation, the other must perform its dual.

Operation Dual Suspends?
Send[T] — send a value Recv[T] — receive a value iox.ErrWouldBlock
SelectL / SelectR — choose a branch Offer — follow the peer's choice iox.ErrWouldBlock
Close — end the session Close Never

Usage

Use Run to prototype and validate protocols. Use Exec with externally managed endpoints. Use the Expr API (RunExpr/ExecExpr) when you need stepping control or want to minimize allocation overhead on hot paths.

Send and Receive

One side sends a value; the dual side receives it.

client := sess.SendThen(42, sess.CloseDone("ok"))
server := sess.RecvBind(func(n int) kont.Eff[string] {
	return sess.CloseDone(fmt.Sprintf("got %d", n))
})
a, b := sess.Run(client, server) // "ok", "got 42"

Expr equivalent: ExprSendThen, ExprRecvBind, ExprCloseDone, RunExpr.

Branching

One side selects a branch; the dual side offers both branches and follows the selection.

client := sess.SelectLThen(sess.SendThen(1, sess.CloseDone("left")))
server := sess.OfferBranch(
	func() kont.Eff[string] {
		return sess.RecvBind(func(n int) kont.Eff[string] {
			return sess.CloseDone(fmt.Sprintf("left %d", n))
		})
	},
	func() kont.Eff[string] { return sess.CloseDone("right") },
)
a, b := sess.Run(client, server)
Recursive Protocols

Protocols that repeat use Loop with Either: Left continues the loop, Right terminates.

counter := sess.Loop(0, func(i int) kont.Eff[kont.Either[int, string]] {
	if i >= 3 {
		return sess.CloseDone(kont.Right[int, string]("done"))
	}
	return sess.SendThen(i, kont.Pure(kont.Left[int, string](i+1)))
})
Delegation

Transfer an endpoint to a third party by sending it; accept delegation by receiving it.

delegator := sess.SendThen(endpoint, sess.CloseDone("delegated"))
acceptor := sess.RecvBind(func(ep *sess.Endpoint) kont.Eff[string] {
	return sess.CloseDone("accepted")
})
Stepping

For proactor event loops (e.g., io_uring), Step and Advance evaluate one effect at a time. Unlike Run and Exec — which synchronously wait for progress — the stepping API yields iox.ErrWouldBlock to the caller, letting the event loop reschedule.

ep, _ := sess.New()
protocol := sess.ExprSendThen(42, sess.ExprCloseDone[struct{}](struct{}{}))
_, susp := sess.Step[struct{}](protocol)
// In a proactor event loop (e.g., io_uring), yield on boundary:
_, nextSusp, err := sess.Advance(ep, susp)
if err != nil {
	return susp // yield to event loop, reschedule when ready
}
susp = nextSusp
Error Handling

Compose session protocols with error effects. Throw immediately aborts the paired run. The returned thrown value is the session-global uncaught throw cause; check it before interpreting a peer-side Either.

client := kont.ExprThrowError[string, string]("boom")
server := sess.ExprRecvBind(func(v string) kont.Expr[string] {
	return sess.ExprCloseDone("recv: " + v)
})

clientResult, serverResult, thrown := sess.RunErrorExpr[string](client, server)
if thrown != nil {
	// Global session abort.
	fmt.Println("session aborted:", *thrown)
	// The peer-side Either may still be locally unresolved.
	_ = clientResult
	_ = serverResult
	return
}

// No uncaught session-wide throw: both Either values are final local outcomes.
fmt.Println(clientResult, serverResult)

In brief:

  • thrown == nil: both Either values are final local outcomes.
  • thrown != nil: the paired run aborted globally; *thrown is the uncaught throw, and a peer-side Either may still be unresolved.

Execution Model

Function Description
Run / RunExpr Run both sides on one goroutine, creating an endpoint pair internally
Exec / ExecExpr Run one side on a pre-created endpoint
Step + Advance Evaluate one effect at a time for external event loops

Cont vs Expr: Cont is closure-based and straightforward to compose. Expr is frame-based with amortized zero-allocation, suited for hot paths.

Contract

sess exposes a trusted-caller transport API. Each endpoint is intended for use by one goroutine at a time, and the hot path intentionally omits concurrent-use guards and post-Close checks.

If a payload type is an interface, the value must still carry a concrete dynamic type. Nil interface values such as any(nil) or error(nil) are outside the contract; if nil is semantically meaningful, use a nil value of a concrete type or wrap it explicitly.

API

Category Cont Expr
Constructors SendThen, RecvBind, CloseDone, SelectLThen, SelectRThen, OfferBranch ExprSendThen, ExprRecvBind, ExprCloseDone, ExprSelectLThen, ExprSelectRThen, ExprOfferBranch
Recursion Loop ExprLoop
Execution Exec, Run ExecExpr, RunExpr
Error execution ExecError, RunError ExecErrorExpr, RunErrorExpr
Stepping Step, Advance, StepError, AdvanceError
Bridge Reify (Cont→Expr), Reflect (Expr→Cont)
Transport New(*Endpoint, *Endpoint)

Practical Recipes

A paired error-aware run defines dual protocols and lets RunErrorExpr create the endpoint pair internally:

// 1. Define the protocol on each side using the dual operations.
clientProg := sess.ExprSendThen(42, sess.ExprRecvBind(
	func(reply string) kont.Expr[string] {
		return sess.ExprCloseDone(reply)
	},
))
serverProg := sess.ExprRecvBind(func(n int) kont.Expr[string] {
	return sess.ExprSendThen(
		fmt.Sprintf("got %d", n),
		sess.ExprCloseDone[string]("ok"),
	)
})

// 2. Run both sides in lockstep with error handling.
type Err struct{ Reason string }
left, right, thrown := sess.RunErrorExpr[Err](clientProg, serverProg)
if thrown != nil {
	// The session aborted; left/right may carry only partial results.
	_ = thrown
}
_ = left; _ = right

For proactor integration, create endpoints with sess.New() and drive StepError / AdvanceError yourself: the suspension yields whenever the underlying transport returns iox.ErrWouldBlock, and the loop resumes it when the matching endpoint completes.

References

  • Kohei Honda. 1993. Types for Dyadic Interaction. In Proc. 4th International Conference on Concurrency Theory (CONCUR '93). LNCS 715, 509–523. https://doi.org/10.1007/3-540-57208-2_35
  • Kohei Honda, Vasco T. Vasconcelos, and Makoto Kubo. 1998. Language Primitives and Type Discipline for Structured Communication-Based Programming. In Proc. 7th European Symposium on Programming (ESOP '98). LNCS 1381, 122–138. https://doi.org/10.1007/BFb0053567
  • Philip Wadler. 2014. Propositions as Sessions. Journal of Functional Programming 24, 2-3 (2014), 384–418. https://doi.org/10.1017/S095679681400001X
  • Dominic A. Orchard and Nobuko Yoshida. 2016. Effects as Sessions, Sessions as Effects. In Proc. 43rd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL '16). 568–581. https://doi.org/10.1145/2837614.2837634
  • Sam Lindley and J. Garrett Morris. 2022. Lightweight Functional Session Types. In Behavioural Types: From Theory to Tools. 265–286. https://doi.org/10.1201/9781003337331-12
  • Simon Fowler, Sam Lindley, J. Garrett Morris, and Sára Decova. 2019. Exceptional Asynchronous Session Types: Session Types without Tiers. Proc. ACM Program. Lang. 3, POPL (Jan. 2019), 1–29. https://doi.org/10.1145/3290341

Dependencies

License

MIT — see LICENSE.

©2026 Hayabusa Cloud Co., Ltd.

Documentation

Overview

Package sess provides session-typed communication protocols via algebraic effects on code.hybscloud.com/kont.

Protocols are composed of typed operations dispatched on a session endpoint.

Architecture

  • Transport: Lock-free bounded SPSC queues via code.hybscloud.com/lfq. New creates an Endpoint pair.
  • Non-blocking: Operations return code.hybscloud.com/iox.ErrWouldBlock on backpressure. code.hybscloud.com/iox.ErrMore is outside the session transport domain and is treated as an unexpected dispatcher failure.
  • Execution: Dual-world API supporting closure-based (Cont-world) and defunctionalized (Expr-world) evaluation.
  • Error Handling: Session operations are non-blocking, while error operations short-circuit into code.hybscloud.com/kont.Either. Paired error runners also return the first uncaught session-wide thrown cause as `*E`. When that `*E` is non-nil, treat it as the authoritative global outcome and check it before interpreting a peer code.hybscloud.com/kont.Either, because the non-throwing side may still be locally unresolved.
  • Contract: Each Endpoint is intended for single-goroutine use over an SPSC transport. Protocol duality and post-Close terminality remain caller responsibilities so the hot path stays validation-free. For interface-typed payloads, the value must carry a concrete dynamic type; nil interface payloads such as any(nil) are out of contract.

sess owns the session effect signature and its endpoint transport. It does not own proactor scheduling, contextual evidence, or the general outcome algebra; those remain the roles of takt, cove, and iox respectively.

API Topologies

Integration

  • Stepping: Step and Advance (or StepError/AdvanceError) evaluate computations one effect at a time, making them easy to integrate with a proactor loop.
  • Blocking: Exec, Run (and Error/Expr variants) wait past boundaries using adaptive backoff.

Example

epA, _ := sess.New()
protocol := sess.ExprSendThen(42, sess.ExprCloseDone[struct{}](struct{}{}))
_, susp := sess.Step[struct{}](protocol)
for susp != nil {
	var err error
	if _, susp, err = sess.Advance(epA, susp); err != nil {
		continue // retry on ErrWouldBlock
	}
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Advance

func Advance[R any](ep *Endpoint, susp *kont.Suspension[R]) (R, *kont.Suspension[R], error)

Advance dispatches the suspended session operation on the endpoint. DispatchSession is non-blocking: returns iox.ErrWouldBlock when the bounded SPSC queue cannot make progress (the I/O boundary).

On success (nil error), the suspension is consumed and the protocol advances to the next effect or completion. On iox.ErrWouldBlock, the suspension is unconsumed and may be retried after the peer makes progress.

func AdvanceError

func AdvanceError[E, R any](ep *Endpoint, susp *kont.Suspension[kont.Either[E, R]]) (kont.Either[E, R], *kont.Suspension[kont.Either[E, R]], error)

AdvanceError dispatches the suspended operation on the endpoint. Session ops are non-blocking (ErrWouldBlock). Error ops are eager: Throw discards the suspension and returns Left.

func CloseDone

func CloseDone[A any](a A) kont.Eff[A]

CloseDone closes the session and returns a. Fuses Perform(Close{}) + Then + Pure.

func Exec

func Exec[R any](ep *Endpoint, protocol kont.Eff[R]) R

Exec runs a Cont-world session protocol on a pre-created endpoint. Blocks on iox.ErrWouldBlock via adaptive backoff (iox.Backoff), without spawning goroutines or creating channels.

func ExecError

func ExecError[E, R any](ep *Endpoint, protocol kont.Eff[R]) kont.Either[E, R]

ExecError runs a Cont-world session protocol with error handling on a pre-created endpoint. Returns Either[E, R] — Right on success, Left on Throw. Blocks on iox.ErrWouldBlock via adaptive backoff (iox.Backoff), without spawning goroutines or creating channels.

func ExecErrorExpr

func ExecErrorExpr[E, R any](ep *Endpoint, protocol kont.Expr[R]) kont.Either[E, R]

ExecErrorExpr runs an Expr-world session protocol with error handling on a pre-created endpoint. Returns Either[E, R] — Right on success, Left on Throw. Blocks on iox.ErrWouldBlock via adaptive backoff (iox.Backoff), without spawning goroutines or creating channels.

func ExecExpr

func ExecExpr[R any](ep *Endpoint, protocol kont.Expr[R]) R

ExecExpr runs an Expr-world session protocol on a pre-created endpoint. Blocks on iox.ErrWouldBlock via adaptive backoff (iox.Backoff), without spawning goroutines or creating channels.

func ExprCloseDone

func ExprCloseDone[A any](a A) kont.Expr[A]

ExprCloseDone closes the session and returns a. Fuses ExprPerform(Close{}) + ExprThen + ExprReturn.

func ExprLoop

func ExprLoop[S, A any](initial S, step func(S) kont.Expr[kont.Either[S, A]]) kont.Expr[A]

ExprLoop runs a recursive session protocol (Expr-world). step returns Left(nextState) to continue or Right(result) to finish. Stack-safe: pure completed steps are iterated without Go stack growth; effectful steps are trampolined through evalFrames via UnwindFrame.

func ExprOfferBranch

func ExprOfferBranch[A any](onLeft func() kont.Expr[A], onRight func() kont.Expr[A]) kont.Expr[A]

ExprOfferBranch waits for the peer's choice and calls onLeft or onRight. Fuses ExprPerform(Offer{}) + ExprBind + Either branch.

func ExprRecvBind

func ExprRecvBind[T, B any](f func(T) kont.Expr[B]) kont.Expr[B]

ExprRecvBind receives a value and passes it to f. Fuses ExprPerform(Recv[T]{}) + ExprBind.

func ExprSelectLThen

func ExprSelectLThen[B any](next kont.Expr[B]) kont.Expr[B]

ExprSelectLThen selects the left branch and continues with next. Fuses ExprPerform(SelectL{}) + ExprThen.

func ExprSelectRThen

func ExprSelectRThen[B any](next kont.Expr[B]) kont.Expr[B]

ExprSelectRThen selects the right branch and continues with next. Fuses ExprPerform(SelectR{}) + ExprThen.

func ExprSendThen

func ExprSendThen[T, B any](v T, next kont.Expr[B]) kont.Expr[B]

ExprSendThen sends a value and then continues with next. Fuses ExprPerform(Send[T]{Value: v}) + ExprThen.

func Loop

func Loop[S, A any](initial S, step func(S) kont.Eff[kont.Either[S, A]]) kont.Eff[A]

Loop runs a recursive session protocol (Cont-world). step returns Left(nextState) to continue or Right(result) to finish. Stack-safe: delegates recursion to ExprLoop's iterative trampoline via Reify/Reflect, avoiding Go stack growth on deep pure Left chains.

func New

func New() (*Endpoint, *Endpoint)

New creates a connected pair of session endpoints. Internal transport uses bounded lock-free SPSC queues: two for data (A→B, B→A), two for branch choice (A→B, B→A), and a shared atomic counter for close signaling.

Session operations are non-blocking: DispatchSession returns iox.ErrWouldBlock when the peer has not yet produced or consumed.

func OfferBranch

func OfferBranch[A any](onLeft func() kont.Eff[A], onRight func() kont.Eff[A]) kont.Eff[A]

OfferBranch waits for the peer's choice and calls onLeft or onRight. Fuses Perform(Offer{}) + Bind + Either branch.

func RecvBind

func RecvBind[T, B any](f func(T) kont.Eff[B]) kont.Eff[B]

RecvBind receives a value and passes it to f. Fuses Perform(Recv[T]{}) + Bind.

func Reflect

func Reflect[A any](m kont.Expr[A]) kont.Eff[A]

Reflect converts an Expr-world session protocol to Cont-world. The resulting Eff can be evaluated with Exec or Run.

func Reify

func Reify[A any](m kont.Eff[A]) kont.Expr[A]

Reify converts a Cont-world session protocol to Expr-world. The resulting Expr can be evaluated with ExecExpr, RunExpr, or stepped with Step and Advance.

func Run

func Run[A, B any](a kont.Eff[A], b kont.Eff[B]) (A, B)

Run creates a session pair, runs both Cont-world protocols, and returns both results. Interleaves execution of both sides on the calling goroutine using adaptive backoff (iox.Backoff) when neither side can make progress. Does not spawn goroutines or create channels.

func RunError

func RunError[E, A, B any](a kont.Eff[A], b kont.Eff[B]) (kont.Either[E, A], kont.Either[E, B], *E)

RunError creates a session pair, runs both Cont-world protocols with error handling, and returns both local results plus the first uncaught session-wide thrown cause when one occurs.

When the returned thrown is non-nil, the paired session terminated by an uncaught Throw. Callers should check thrown before interpreting a peer-side Either result, because the non-throwing side may still be locally unresolved when the session abort becomes global.

Interleaves execution of both sides on the calling goroutine using adaptive backoff (iox.Backoff). Does not spawn goroutines or create channels.

func RunErrorExpr

func RunErrorExpr[E, A, B any](a kont.Expr[A], b kont.Expr[B]) (kont.Either[E, A], kont.Either[E, B], *E)

RunErrorExpr creates a session pair, runs both Expr-world protocols with error handling, and returns both local results plus the first uncaught session-wide thrown cause when one occurs.

If thrown is nil, both returned Either values are final local outcomes. If thrown is non-nil, the paired session terminated by uncaught Throw; callers should inspect thrown first, because a peer-side Either may still reflect a locally unresolved computation rather than a peer-local throw. Internally, doneA/doneB track whether each side has actually completed, because StepError/AdvanceError leave a pending side in the zero value of Either until its suspension resolves.

Interleaves execution of both sides on the calling goroutine using adaptive backoff (iox.Backoff). Does not spawn goroutines or create channels.

func RunExpr

func RunExpr[A, B any](a kont.Expr[A], b kont.Expr[B]) (A, B)

RunExpr creates a session pair, runs both Expr-world protocols, and returns both results. Interleaves execution of both sides on the calling goroutine using adaptive backoff (iox.Backoff) when neither side can make progress. Does not spawn goroutines or create channels.

func SelectLThen

func SelectLThen[B any](next kont.Eff[B]) kont.Eff[B]

SelectLThen selects the left branch and continues with next. Fuses Perform(SelectL{}) + Then.

func SelectRThen

func SelectRThen[B any](next kont.Eff[B]) kont.Eff[B]

SelectRThen selects the right branch and continues with next. Fuses Perform(SelectR{}) + Then.

func SendThen

func SendThen[T, B any](v T, next kont.Eff[B]) kont.Eff[B]

SendThen sends a value and then continues with next. Fuses Perform(Send[T]{Value: v}) + Then.

func Step

func Step[R any](protocol kont.Expr[R]) (R, *kont.Suspension[R])

Step evaluates a session protocol until the first effect suspension. Returns (result, nil) on completion, or (zero, suspension) if pending.

func StepError

func StepError[E, R any](protocol kont.Expr[R]) (kont.Either[E, R], *kont.Suspension[kont.Either[E, R]])

StepError evaluates a session protocol with error support until the first effect suspension. Returns (Either[E, R], nil) on completion or error, or (zero, suspension) if pending.

Types

type Close

type Close struct {
	kont.Phantom[struct{}]
}

Close is the effect operation for closing the session. Perform(Close{}) signals session termination under the caller-owned protocol contract.

func (Close) DispatchSession

func (Close) DispatchSession(ctx *sessionContext) (kont.Resumed, error)

DispatchSession handles Close on the session transport. Atomically increments the shared close counter. Never blocks and does not police later protocol misuse.

type Endpoint

type Endpoint struct {
	// contains filtered or unexported fields
}

Endpoint represents one side of a session-typed channel pair. Transport is backed by bounded lock-free SPSC queues from lfq. Endpoints are intended for single-goroutine use; sess relies on caller discipline instead of hot-path concurrent-use or post-Close policing.

func (*Endpoint) Serial

func (ep *Endpoint) Serial() Serial

Serial returns the serial number assigned to this endpoint's session.

type Offer

type Offer struct {
	kont.Phantom[kont.Either[struct{}, struct{}]]
}

Offer is the effect operation for receiving a branch choice from the peer. Perform(Offer{}) receives the peer's Left or Right selection.

func (Offer) DispatchSession

func (Offer) DispatchSession(ctx *sessionContext) (kont.Resumed, error)

DispatchSession handles Offer on the session transport. Non-blocking: returns iox.ErrWouldBlock if the choice queue is empty. true → Left (peer selected left), false → Right (peer selected right).

type Recv

type Recv[T any] struct {
	kont.Phantom[T]
}

Recv is the effect operation for receiving a value of type T. Perform(Recv[T]{}) receives a typed value from the peer.

func (Recv[T]) DispatchSession

func (Recv[T]) DispatchSession(ctx *sessionContext) (kont.Resumed, error)

DispatchSession handles Recv on the session transport. Non-blocking: returns iox.ErrWouldBlock if the bounded SPSC queue is empty.

type SelectL

type SelectL struct {
	kont.Phantom[struct{}]
}

SelectL is the effect operation for choosing the left branch. Perform(SelectL{}) signals the left choice to the peer.

func (SelectL) DispatchSession

func (SelectL) DispatchSession(ctx *sessionContext) (kont.Resumed, error)

DispatchSession handles SelectL on the session transport. Non-blocking: returns iox.ErrWouldBlock if the choice queue is full.

type SelectR

type SelectR struct {
	kont.Phantom[struct{}]
}

SelectR is the effect operation for choosing the right branch. Perform(SelectR{}) signals the right choice to the peer.

func (SelectR) DispatchSession

func (SelectR) DispatchSession(ctx *sessionContext) (kont.Resumed, error)

DispatchSession handles SelectR on the session transport. Non-blocking: returns iox.ErrWouldBlock if the choice queue is full.

type Send

type Send[T any] struct {
	kont.Phantom[struct{}]
	Value T
}

Send is the effect operation for sending a value of type T. Perform(Send[T]{Value: v}) sends v to the peer endpoint. For interface-typed T, v must carry a concrete dynamic type. Nil interface payloads such as any(nil) are out of contract.

func (Send[T]) DispatchSession

func (s Send[T]) DispatchSession(ctx *sessionContext) (kont.Resumed, error)

DispatchSession handles Send on the session transport. Non-blocking: returns iox.ErrWouldBlock if the bounded SPSC queue is full.

type Serial

type Serial = uint32

Serial is a monotonically increasing session identifier. Each call to New assigns the next serial value.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL