Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hale Documentation

Hale ports Haskell’s web ecosystem to Lean 4 with maximalist typing – encoding correctness, invariants, and protocol guarantees directly in the type system.

Mantra: Extensive typing/proving with no compromise on performance.

Architecture Overview

+-----------------------------------------------------------+
|                    User Application                        |
|              (Application : WAI type)                      |
+-----------------------------------------------------------+
|                    Middleware Stack                         |
|    AddHeaders . ForceSSL . Gzip . RequestLogger . ...      |
+-----------------------------------------------------------+
|                    WAI Interface                            |
|  Request -> (Response -> IO Received) -> AppM .pending .sent|
+----------+----------+-----------+-------------------------+
|   Warp   | WarpTLS  | WarpQUIC  |  Static File Server     |
| HTTP/1.x |  HTTPS   |  HTTP/3   |  (wai-app-static)       |
+----------+----------+-----------+-------------------------+
|              Transport Layer                                |
|  Socket(.listening) -> Socket(.connected) -> Connection    |
|     TCP (plain)    |   TLS (OpenSSL)    |  QUIC            |
+-----------------------------------------------------------+
|              Operating System (via FFI)                     |
|         POSIX sockets | OpenSSL | kqueue/epoll             |
+-----------------------------------------------------------+

Leveraging Lean 4’s Dependent Type System

Lean 4 is not Haskell with nicer syntax – it is a full dependently-typed proof assistant that also happens to compile to efficient native code. Hale exploits this to turn protocol specifications, resource lifecycles, and algebraic contracts into compile-time obligations that are verified by the kernel and then erased at runtime (zero cost). Below are the four core techniques.

1. Phantom Type Parameters – Zero-Cost State Machines

A phantom parameter encodes the resource’s lifecycle state in the type. Functions that require a particular state accept only that constructor; transitions return the new state. The parameter is fully erased at runtime (same machine code as an untyped handle), yet the compiler rejects every protocol violation.

structure Socket (state : SocketState) where    -- phantom parameter, erased at runtime
  raw : RawSocket

def bind   (s : Socket .fresh)     : IO (Socket .bound)     -- fresh  --> bound
def listen (s : Socket .bound)     : IO (Socket .listening)  -- bound  --> listening
def accept (s : Socket .listening) : IO (Socket .connected)  -- listen --> connected
def send   (s : Socket .connected) : IO Nat                  -- only connected
def close  (s : Socket state)                                -- any non-closed state
           (h : state ≠ .closed := by decide)                -- PROOF: not already closed
           : IO (Socket .closed)                             -- returns closed token

-- Compile-time errors (no runtime check needed):
-- send freshSocket       -- type error: .fresh ≠ .connected
-- accept boundSocket     -- type error: .bound ≠ .listening
-- close closedSocket     -- type error: cannot prove .closed ≠ .closed

2. Proof-Carrying Structures – Invariants by Construction

Lean 4 erases proof terms at compile time, so proof fields in structures are literally free – same sizeof, same codegen. The invariant is guaranteed the moment the value exists; no runtime validation is ever needed.

structure Ratio where
  num : Int
  den : Nat
  den_pos : den > 0          -- erased: denominator always positive
  coprime : Nat.Coprime num.natAbs den  -- erased: always in lowest terms

structure Settings where
  settingsTimeout : Nat := 30
  settingsTimeoutPos : settingsTimeout > 0 := by omega  -- erased: never zero

3. Indexed Monads – Exactly-Once Protocol Enforcement

Where Haskell’s WAI relies on a gentleman’s agreement (“call respond exactly once”), Lean 4 enforces it at the type level via an indexed monad whose pre/post state parameters track the response lifecycle. The constructor is private, so the only way to produce AppM .pending .sent ResponseReceived is through the provided combinators – fabrication is a type error.

structure AppM (pre post : ResponseState) (α : Type) where
  private mk ::          -- can't fabricate: constructor is private
  run : IO α

def AppM.respond         -- the ONLY transition from .pending to .sent
  (callback : Response → IO ResponseReceived)
  (resp : Response)
  : AppM .pending .sent ResponseReceived

-- Double-respond is a compile-time error:
-- After the first respond, state is .sent.
-- A second respond would need AppM .sent .sent, which does not exist.

4. Inductive Types – Protocol Semantics as Data

Sum types encode every valid protocol state or message shape. Pattern matching is exhaustive, so the compiler verifies that every case is handled.

inductive Response where
  | responseFile    ...   -- sendfile(2)
  | responseBuilder ...   -- in-memory ByteArray
  | responseStream  ...   -- chunked streaming
  | responseRaw     ...   -- raw socket (WebSocket upgrade)

inductive RequestBodyLength where
  | chunkedBody           -- Transfer-Encoding: chunked
  | knownLength (n : Nat) -- Content-Length: n

Design Principle: Proofs on Objects, Not Wrapper Types

Invariants belong inside the original type, not in a separate wrapper. Proof fields are zero-cost (erased), so there is never a reason to create ValidSettings alongside Settings – just put the proof in Settings.

Package Index

Links marked API point to the auto-generated doc-gen4 API reference. Links marked Guide point to hand-written documentation.

Core Infrastructure

PackageGuideAPITheoremsDescription
BaseGuideAPI88Foundational types, functors, monads
ByteStringGuideAPI7Byte array operations
NetworkAPI7POSIX sockets with phantom state
HttpTypesAPI42HTTP methods, status, headers, URI

Web Application Interface

PackageGuideAPITheoremsDescription
WAIGuideAPI17Request/Response/Application/Middleware
WarpGuideAPI11HTTP/1.x server (156k QPS)
WarpTLSGuideAPIHTTPS via OpenSSL FFI
WarpQUICAPIHTTP/3 over QUIC
WaiExtraGuideAPI1136 middleware modules
WaiAppStaticGuideAPI4Static file serving
WebSocketsGuideAPI6RFC 6455 protocol

Protocol Implementations

PackageGuideAPITheoremsDescription
Http2API10HTTP/2 framing (RFC 9113)
Http3API24HTTP/3 framing + QPACK
QUICAPIQUIC transport
TLSGuideAPIOpenSSL FFI wrapper

Utilities

PackageGuideAPITheoremsDescription
MimeTypesAPIMIME type lookup
CookieAPIHTTP cookie parsing
Base64APIRFC 4648 codec
ResourceTGuideAPI1Resource management monad
FastLoggerAPIBuffered thread-safe logging
AutoUpdateAPIPeriodic cached values
TimeManagerAPIConnection timeout management

Proven Properties (257 theorems)

See Proofs.md for a complete catalog of all theorems.

Hale.Base — Haskell base for Lean 4

API Reference: Hale.Base

Re-exports all Base sub-modules.

Design Philosophy (Concurrency)

All concurrency primitives are promise-based, not OS-thread-based. Blocking operations return BaseIO (Task a) rather than suspending an OS thread. This enables scaling to millions of concurrent “threads” on Lean’s task-pool scheduler.

Cancellation is cooperative: killThread sets a CancellationToken rather than throwing an asynchronous exception. CPU-bound threads that never check the token will not be interrupted.

Foundational

Core Abstractions

Data Structures

Traversals

Numeric Types

Advanced Abstractions

  • ArrowControl.Arrow

Concurrency

  • ConcurrentControl.Concurrent
  • MVarControl.Concurrent.MVar
  • ChanControl.Concurrent.Chan
  • QSemControl.Concurrent.QSem
  • QSemNControl.Concurrent.QSemN

Void

Lean: Hale.Base.Void | Haskell: Data.Void

Overview

Uninhabited type (alias for Empty). Provides absurd for ex falso reasoning.

API Mapping

LeanHaskellKind
VoidVoidType
Void.absurdabsurdFunction

Instances

  • BEq Void
  • Ord Void
  • ToString Void
  • Repr Void
  • Hashable Void
  • Inhabited (Void -> a) — functions from Void are always inhabited

Proofs & Guarantees

  • eq_absurd — uniqueness of void functions: any two functions from Void are equal

Example

-- A function from Void to any type is unique
def fromVoid : Void -> Nat := Void.absurd

Function

Lean: Hale.Base.Function | Haskell: Data.Function

Overview

Standard function combinators: on, applyTo (&), const (K combinator), flip (C combinator).

API Mapping

LeanHaskellKind
Function.ononCombinator
Function.applyTo(&)Combinator
Function.constconstCombinator
Function.flipflipCombinator

Instances

None (standalone functions).

Proofs & Guarantees

  • on_apply(f.on g) x y = f (g x) (g y)
  • applyTo_applyx.applyTo f = f x
  • const_applyFunction.const a b = a
  • flip_flipflip (flip f) = f (involution)
  • flip_applyFunction.flip f x y = f y x

Example

-- Compare strings by length
Function.on (· + ·) String.length "hi" "hello"
-- => 7

Newtype

Lean: Hale.Base.Newtype | Haskell: Data.Monoid / Data.Semigroup

Overview

Monoid/semigroup wrappers: Dual, Endo, First, Last, Sum, Product, All, Any. Each wraps a value and provides an Append instance with specific combining semantics.

API Mapping

LeanHaskellKind
DualDualWrapper
EndoEndoWrapper
FirstFirstWrapper
LastLastWrapper
Sum / Sum.getSumSum / getSumWrapper
Product / Product.getProductProduct / getProductWrapper
AllAllWrapper
AnyAnyWrapper

Instances

All wrappers provide Append.

WrapperBEqOrdReprHashableToString
Dualyesyesyesyesyes
Sumyesyesyesyesyes
Productyesyesyesyesyes
Allyesyesyesyesyes
Anyyesyesyesyesyes
Firstyesyesyes
Lastyesyesyes
Endo

Proofs & Guarantees

  • append_assoc — associativity of Append for each wrapper

Example

-- Sum combines via addition
Sum.mk 3 ++ Sum.mk 4
-- => Sum 7

Bifunctor

Lean: Hale.Base.Bifunctor | Haskell: Data.Bifunctor

Overview

Typeclass for types with two covariant type parameters. Provides bimap, mapFst, mapSnd.

API Mapping

LeanHaskellKind
Bifunctor classBifunctorTypeclass
bimapbimapMethod
mapFstfirstMethod
mapSndsecondMethod
LawfulBifunctor(lawful)Typeclass

Instances

  • Bifunctor Prod
  • Bifunctor Sum
  • Bifunctor Except
  • LawfulBifunctor Prod
  • LawfulBifunctor Sum
  • LawfulBifunctor Except

Proofs & Guarantees

  • bimap_idbimap id id = id (via LawfulBifunctor)
  • bimap_compbimap (f1 . f2) (g1 . g2) = bimap f1 g1 . bimap f2 g2 (via LawfulBifunctor)

Example

-- Map over both components of a pair
Bifunctor.bimap (· * 10) (· ++ "!") (1, "hello")
-- => (10, "hello!")

Contravariant

Lean: Hale.Base.Contravariant | Haskell: Data.Functor.Contravariant

Overview

Contravariant functors — types that consume values rather than produce them. Where a covariant functor maps over outputs, a contravariant functor maps over inputs.

API Mapping

LeanHaskellKind
Contravariant classContravariantTypeclass
contramapcontramapMethod
PredicatePredicateType
EquivalenceEquivalenceType
LawfulContravariant class(lawful)Typeclass

Instances

  • Contravariant Predicate
  • Contravariant Equivalence
  • LawfulContravariant Predicate
  • LawfulContravariant Equivalence

Proofs & Guarantees

  • contramap_idcontramap id = id (via LawfulContravariant)
  • contramap_compcontramap (f . g) = contramap g . contramap f (via LawfulContravariant)

Example

-- Adapt a predicate on Nat to work on String via length
Contravariant.contramap String.length (Predicate.mk (· > 3))
-- Now accepts strings with length > 3

Const

Lean: Hale.Base.Const | Haskell: Data.Functor.Const

Overview

Constant functor — ignores its second type parameter. Useful for phantom types and accumulating effects during traversals.

API Mapping

LeanHaskellKind
ConstConstType
getConstgetConstAccessor
Functor ConstFunctor (Const a)Instance
Pure ConstApplicative (Const a)Instance

Instances

  • BEq (Const a b) (requires BEq a)
  • Ord (Const a b) (requires Ord a)
  • Repr (Const a b) (requires Repr a)
  • ToString (Const a b) (requires ToString a)
  • Functor (Const a)
  • Pure (Const a) (requires Append a and Inhabited a)

Proofs & Guarantees

  • map_val — mapping over Const preserves the wrapped value
  • map_idFunctor.map id = id
  • map_compFunctor.map (f . g) = Functor.map f . Functor.map g

Example

-- Mapping over Const does nothing to the stored value
(Functor.map f (Const.mk 42) : Const Nat String).getConst == 42
-- => true

Identity

Lean: Hale.Base.Identity | Haskell: Data.Functor.Identity

Overview

Identity functor/monad — trivial wrapper. Useful as a base case for monad transformer stacks and as a witness that a computation is pure.

API Mapping

LeanHaskellKind
IdentityIdentityType
runIdentityrunIdentityAccessor

Instances

  • Functor Identity
  • Pure Identity
  • Bind Identity
  • Seq Identity
  • Applicative Identity
  • Monad Identity
  • BEq (Identity a) (derived)
  • Ord (Identity a) (derived)
  • Repr (Identity a) (derived)
  • Hashable (Identity a) (derived)
  • Inhabited (Identity a) (derived)
  • ToString (Identity a)

Proofs & Guarantees

  • map_idFunctor.map id = id
  • map_compFunctor.map (f . g) = Functor.map f . Functor.map g
  • pure_bind — left identity: pure a >>= f = f a
  • bind_pure — right identity: m >>= pure = m
  • bind_assoc — associativity: (m >>= f) >>= g = m >>= (fun x => f x >>= g)

Example

-- Identity monad is just a trivial wrapper
(Identity.mk 42 >>= fun n => Identity.mk (n + 1)).runIdentity
-- => 43

Compose

Lean: Hale.Base.Compose | Haskell: Data.Functor.Compose

Overview

Composition of functors/applicatives. Compose F G a wraps F (G a), allowing two functors to be composed into a single functor (or two applicatives into a single applicative).

API Mapping

LeanHaskellKind
ComposeComposeType
getComposegetComposeAccessor

Instances

  • Functor (Compose F G) (requires Functor F, Functor G)
  • Pure (Compose F G) (requires Applicative F, Applicative G)
  • Seq (Compose F G) (requires Applicative F, Applicative G)
  • Applicative (Compose F G) (requires Applicative F, Applicative G)

Proofs & Guarantees

  • map_idFunctor.map id = id (with lawful functors)
  • map_compFunctor.map (f . g) = Functor.map f . Functor.map g (with lawful functors)

Example

-- Compose List and Option into a single functor
Compose.mk [some 1, none, some 3]
-- : Compose List Option Nat

Category

Lean: Hale.Base.Category | Haskell: Control.Category

Overview

Abstract category with identity and composition. Uses diagrammatic order (f >>> g = g . f), which reads left-to-right as a pipeline.

API Mapping

LeanHaskellKind
Category classCategoryTypeclass
Category.ididMethod
Category.comp / >>>>>>Method
Fun(->)Type
LawfulCategory class(lawful)Typeclass

Instances

  • Category Fun — the function arrow as a category
  • LawfulCategory Fun — all three laws hold definitionally

Proofs & Guarantees

  • id_compid >>> f = f (via LawfulCategory)
  • comp_idf >>> id = f (via LawfulCategory)
  • comp_assoc(f >>> g) >>> h = f >>> (g >>> h) (via LawfulCategory)

Example

-- Compose functions in diagrammatic (left-to-right) order
(Fun.mk (· + 1) >>> Fun.mk (· * 2)).apply 3
-- => 8  (first add 1, then multiply by 2)

NonEmpty

Lean: Hale.Base.NonEmpty | Haskell: Data.List.NonEmpty

Overview

Non-empty list with a guaranteed minimum of one element. The length function returns a subtype {n : Nat // n >= 1}, encoding the non-emptiness invariant at the type level.

API Mapping

LeanHaskellKind
NonEmptyNonEmptyType
headheadAccessor
lastlastAccessor
lengthlengthFunction
toListtoListFunction
singletonsingletonConstructor
consconsConstructor
appendappendFunction
mapmapFunction
reversereverseFunction
foldrfoldrFunction
foldr1foldr1Function
foldl1foldl1Function
fromList?nonEmptyFunction
fromListfromListFunction

Instances

  • Append (NonEmpty a)
  • Functor NonEmpty
  • Pure NonEmpty
  • Bind NonEmpty
  • Monad NonEmpty
  • ToString (NonEmpty a) (requires ToString a)
  • BEq (NonEmpty a) (derived)
  • Ord (NonEmpty a) (derived)
  • Repr (NonEmpty a) (derived)
  • Hashable (NonEmpty a) (derived)

Proofs & Guarantees

  • toList_ne_niltoList ne != [] (the list is never empty)
  • reverse_length — reversing preserves length
  • toList_lengthtoList length agrees with NonEmpty.length
  • map_length — mapping preserves length

Example

-- Construct a non-empty list
let ne := NonEmpty.mk 1 [2, 3]
ne.head    -- => 1
ne.last    -- => 3
ne.length  -- => ⟨3, by omega⟩

Either

Lean: Hale.Base.Either | Haskell: Data.Either

Overview

Sum type with Left and Right constructors. Right-biased for Functor/Monad instances. Includes partitioning of lists of Either values.

API Mapping

LeanHaskellKind
EitherEitherType
left / rightLeft / RightConstructors
isLeft / isRightisLeft / isRightPredicate
fromLeft / fromRightfromLeft / fromRightAccessor
eithereitherEliminator
mapLeft / mapRightfirst / secondFunction
swapN/AFunction
partitionEitherspartitionEithersFunction

Instances

  • Functor (Either a)
  • Pure (Either a)
  • Bind (Either a)
  • Seq (Either a)
  • Applicative (Either a)
  • Monad (Either a)
  • Bifunctor Either
  • ToString (Either a b) (requires ToString a, ToString b)

Proofs & Guarantees

  • swap_swapswap (swap e) = e (involution)
  • isLeft_not_isRightisLeft e = !isRight e
  • partitionEithers_length — total elements preserved after partitioning

Example

-- Partition a list of Either values
Either.partitionEithers [.left "a", .right 1, .left "b", .right 2]
-- => (["a", "b"], [1, 2])

Ord

Lean: Hale.Base.Ord | Haskell: Data.Ord

Overview

Ordering utilities. Down reverses the comparison order. comparing lifts comparisons through projections. clamp returns a proof-carrying bounded value.

API Mapping

LeanHaskellKind
DownDownType
comparingcomparingFunction
clampclampFunction (returns {y // lo <= y /\ y <= hi})

Instances

  • BEq (Down a) (requires BEq a)
  • Ord (Down a) (requires Ord a, reversed)
  • ToString (Down a) (requires ToString a)

Proofs & Guarantees

  • get_mk(Down.mk x).get = x
  • compare_reversecompare (Down.mk a) (Down.mk b) = compare b a

Example

-- Down reverses comparison order
compare (Down.mk 3) (Down.mk 7)
-- => Ordering.gt  (because 3 < 7, but Down reverses it)

Tuple

Lean: Hale.Base.Tuple | Haskell: Data.Tuple + Prelude

Overview

Pair operations: swap, map components, curry/uncurry. Provides bidirectional conversions between curried and uncurried function forms.

API Mapping

LeanHaskellKind
Tuple.swapswapFunction
Tuple.mapFstfirst (Control.Arrow)Function
Tuple.mapSndsecondFunction
Tuple.bimapbimapFunction
Tuple.currycurryFunction
Tuple.uncurryuncurryFunction

Instances

None (standalone functions on Prod).

Proofs & Guarantees

  • swap_swapswap (swap p) = p (involution)
  • curry_uncurrycurry (uncurry f) = f
  • uncurry_curryuncurry (curry f) = f
  • bimap_idbimap id id = id
  • bimap_compbimap (f1 . f2) (g1 . g2) = bimap f1 g1 . bimap f2 g2
  • mapFst_eq_bimapmapFst f = bimap f id
  • mapSnd_eq_bimapmapSnd f = bimap id f

Example

-- Curry converts a function on pairs to a curried function
Tuple.curry (fun p => p.1 + p.2) 2 3
-- => 5

Foldable

Lean: Hale.Base.Foldable | Haskell: Data.Foldable

Overview

Typeclass for structures that can be folded to a summary value. Provides a rich API with many derived operations built on top of foldr.

API Mapping

LeanHaskellKind
Foldable classFoldableTypeclass
foldrfoldrMethod
foldlfoldl'Method
toListtoListMethod
foldMapfoldMapFunction
nullnullFunction
lengthlengthFunction
anyanyFunction
allallFunction
find?findFunction
elemelemFunction
minimum?minimumFunction
maximum?maximumFunction
sumsumFunction
productproductFunction

Instances

  • Foldable List
  • Foldable Option
  • Foldable List.NonEmpty
  • Foldable (Either a)

Proofs & Guarantees

None listed (correctness follows from typeclass laws).

Example

-- Sum all elements in a list
Foldable.sum [1, 2, 3]
-- => 6

Traversable

Lean: Hale.Base.Traversable | Haskell: Data.Traversable

Overview

Typeclass for structures that can be traversed with effects. Extends Functor by allowing each element to produce an applicative effect, then collecting those effects.

API Mapping

LeanHaskellKind
Traversable classTraversableTypeclass
traversetraverseMethod
sequencesequenceAMethod
LawfulTraversable(lawful)Typeclass

Instances

  • Traversable List
  • Traversable Option
  • Traversable NonEmpty

Note: Either α cannot have a Traversable instance due to universe constraints in Lean 4.

Proofs & Guarantees

  • traverse_identity — traversing with the identity applicative is just map (via LawfulTraversable)

Example

-- Sequence a list of Options: all must be Some to get Some
Traversable.sequence [some 1, some 2, some 3]
-- => some [1, 2, 3]

Traversable.sequence [some 1, none, some 3]
-- => none

Ratio

Lean: Hale.Base.Ratio | Haskell: Data.Ratio

Overview

Exact rational arithmetic. Invariants are enforced in the type: the denominator is always positive and the numerator and denominator are always coprime.

API Mapping

LeanHaskellKind
RatioRatio / RationalType
mk'%Constructor
fromIntfromIntegralConstructor
fromNatfromIntegralConstructor
negnegateFunction
absabsFunction
add(+)Function
sub(-)Function
mul(*)Function
invrecipFunction
div(/)Function
floorfloorFunction
ceilceilingFunction
roundroundFunction
zero0Constant
one1Constant

Instances

  • OfNat Ratio 0 / OfNat Ratio 1
  • Inhabited Ratio
  • LE Ratio / LT Ratio
  • BEq Ratio
  • Ord Ratio
  • Add Ratio / Sub Ratio / Mul Ratio / Neg Ratio
  • ToString Ratio

Proofs & Guarantees

Invariants maintained by construction via the den_pos and coprime fields in the Ratio structure:

  • Denominator is always positive
  • Numerator and denominator are always coprime

Example

-- Exact rational arithmetic: 1/2 + 1/3 = 5/6
Ratio.mk' 1 2 (by omega) + Ratio.mk' 1 3 (by omega)
-- => 5/6

Complex

Lean: Hale.Base.Complex | Haskell: Data.Complex

Overview

Complex numbers parameterized by coefficient type. Supports arithmetic operations and conjugation.

API Mapping

LeanHaskellKind
ComplexComplexType
re / imrealPart / imagPartAccessor
ofReal(:+ 0)Constructor
i0 :+ 1Constant
conjugateconjugateFunction
magnitudeSquaredmagnitude ^2Function

Instances

  • Inhabited (Complex a) (requires Inhabited a)
  • ToString (Complex a) (requires ToString a)
  • Add (Complex a) (requires Add a)
  • Neg (Complex a) (requires Neg a)
  • Sub (Complex a) (requires Sub a)
  • Mul (Complex a) (requires Add a, Sub a, Mul a)
  • BEq (Complex a) (derived)
  • Ord (Complex a) (derived)
  • Repr (Complex a) (derived)
  • Hashable (Complex a) (derived)

Proofs & Guarantees

  • conjugate_conjugateconjugate (conjugate z) = z (involution)
  • add_comm'z1 + z2 = z2 + z1

Example

-- Compute magnitude squared of 3 + 4i
Complex.magnitudeSquared ⟨3, 4⟩
-- => 25  (since 3^2 + 4^2 = 25)

Fixed

Lean: Hale.Base.Fixed | Haskell: Data.Fixed

Overview

Fixed-point decimal arithmetic. Fixed p stores integers scaled by 10^p. Addition and subtraction are exact (no rounding).

API Mapping

LeanHaskellKind
FixedFixedType
rawMkFixedAccessor
scaleresolutionFunction
fromIntfromIntegralConstructor
toRatiotoRationalFunction

Instances

  • Add (Fixed p) / Sub (Fixed p) / Neg (Fixed p) / Mul (Fixed p)
  • OfNat (Fixed p) 0 / OfNat (Fixed p) 1
  • Inhabited (Fixed p)
  • ToString (Fixed p)
  • BEq (Fixed p) (derived)
  • Ord (Fixed p) (derived)
  • Repr (Fixed p) (derived)
  • Hashable (Fixed p) (derived)

Proofs & Guarantees

  • scale_pos — the scale factor 10^p is always positive
  • add_exact — addition of fixed-point values is exact (no rounding)
  • sub_exact — subtraction of fixed-point values is exact (no rounding)

Example

-- Fixed-point with 2 decimal places: 3.00 + 1.57 = 4.57
Fixed.fromInt (p := 2) 3 + ⟨157⟩
-- => 4.57  (internally stored as raw 457)

Arrow

Lean: Hale.Base.Arrow | Haskell: Control.Arrow

Overview

Arrow abstraction for generalized function-like computations. Extends Category with lifting and product/sum operations.

API Mapping

LeanHaskellKind
Arrow classArrowTypeclass
arrarrMethod
firstfirstMethod
secondsecondMethod
split(***)Method
ArrowChoice classArrowChoiceTypeclass
leftleftMethod
rightrightMethod
fanin`(

Instances

  • Arrow Fun
  • ArrowChoice Fun

Proofs & Guarantees

None listed (laws follow from LawfulCategory and the arrow laws).

Example

-- Lift a pure function into the Arrow
(Arrow.arr (Cat := Fun) (· * 2)).apply 3
-- => 6

Concurrent

Lean: Hale.Control.Concurrent | Haskell: Control.Concurrent

Overview

Thread management primitives: forking lightweight threads, cooperative cancellation, delays, and yielding. Threads run on Lean’s task pool via IO.asTask, not as 1:1 OS threads.

API Mapping

LeanHaskellKind
ThreadIdThreadIdType
forkIOforkIOFunction
forkFinallyforkFinallyFunction
killThreadkillThreadFunction
threadDelaythreadDelayFunction
yieldyieldFunction
waitThread(no direct equivalent)Function
PosNat(no direct equivalent)Type ({ n : Nat // n > 0 })

Differences from GHC

AspectGHChale
Thread modelGreen threads with RTS schedulerIO.asTask on Lean’s thread pool
CancellationAsynchronous exceptions (throwTo)Cooperative CancellationToken
threadDelay unitMicrosecondsMicroseconds (converted internally to ms, rounded up)
Thread ID uniquenessRuntime-assigned, may be reusedMonotonic PosNat counter (≥ 1), never reused

Instances

  • BEq ThreadId — equality by id field
  • Hashable ThreadId — hash of id field
  • ToString ThreadId"ThreadId(n)"
  • Repr ThreadId"ThreadId(n)"

ThreadId Structure

structure ThreadId where
  private mk ::
  id : PosNat                           -- unique, monotonic, >= 1
  private task : Task (Except IO.Error Unit)
  private cancelToken : Std.CancellationToken

Fields task and cancelToken are private; the only public field is id.

Proofs & Guarantees

  • Unique IDs by construction: freshThreadId reads and increments a global monotonic counter. Within a single process, no two ThreadId values share the same id.
  • forkFinally finaliser guarantee: The finally_ callback runs in both the success and error branches of a try/catch, so it executes regardless of outcome.
  • Cooperative cancellation only: killThread sets a CancellationToken; it does not forcibly terminate the thread. A thread that never checks the token will continue running.

Example

import Hale.Control.Concurrent

open Hale

-- Fork a thread that prints a message
let tid <- forkIO do
  IO.println "hello from thread"

-- Fork with a finaliser
let tid2 <- forkFinally (do IO.println "working..."; pure 42) fun
  | .ok val   => IO.println s!"done: {val}"
  | .error e  => IO.println s!"failed: {e}"

-- Cooperative cancellation
killThread tid

-- Delay for 1 second (1_000_000 microseconds)
threadDelay 1000000

-- Yield to other threads
yield

-- Wait for a thread to complete
waitThread tid2

Performance Notes

  • forkIO is lightweight: it schedules a task on the thread pool, not an OS thread.
  • threadDelay has millisecond granularity due to IO.sleep. A request for 1 microsecond will sleep for 1 millisecond.
  • yield is equivalent to IO.sleep 0, giving other tasks a chance to run.

MVar

Lean: Hale.Control.Concurrent.MVar | Haskell: Control.Concurrent.MVar

Overview

A synchronisation variable that is either empty or holds a value of type a. All blocking is promise-based: waiting tasks are dormant IO.Promise values, not blocked OS threads. This allows scaling to millions of concurrent tasks.

MVar is the fundamental building block for all other concurrency primitives in hale (Chan, QSem, QSemN).

Concurrent Type Alias

abbrev Concurrent (a : Type) := BaseIO (Task a)

Any function returning Concurrent a is non-blocking by construction. Compose with BaseIO.bindTask:

let task <- mv.take        -- Concurrent a = BaseIO (Task a)
BaseIO.bindTask task fun val => ...

API Mapping

LeanHaskellKindSignature
MVar.newnewMVarConstructora -> BaseIO (MVar a)
MVar.newEmptynewEmptyMVarConstructorBaseIO (MVar a)
MVar.taketakeMVarAsync opMVar a -> BaseIO (Task a)
MVar.putputMVarAsync opMVar a -> a -> BaseIO (Task Unit)
MVar.readreadMVarAsync opMVar a -> BaseIO (Task a)
MVar.swapswapMVarAsync opMVar a -> a -> BaseIO (Task a)
MVar.withMVarwithMVarCombinatorMVar a -> (a -> BaseIO (a x b)) -> BaseIO (Task b)
MVar.modifymodifyMVarCombinatorMVar a -> (a -> BaseIO (a x b)) -> BaseIO (Task b)
MVar.modify_modifyMVar_CombinatorMVar a -> (a -> BaseIO a) -> BaseIO (Task Unit)
MVar.tryTaketryTakeMVarNon-blockingMVar a -> BaseIO (Option a)
MVar.tryPuttryPutMVarNon-blockingMVar a -> a -> BaseIO Bool
MVar.tryReadtryReadMVarNon-blockingMVar a -> BaseIO (Option a)
MVar.isEmptyisEmptyMVarQueryMVar a -> BaseIO Bool
MVar.takeSync(N/A)Sync wrapperMVar a -> BaseIO a
MVar.putSync(N/A)Sync wrapperMVar a -> a -> BaseIO Unit
MVar.readSync(N/A)Sync wrapperMVar a -> BaseIO a

Constraint: [Nonempty a]

Blocking operations (take, read, swap, withMVar, modify, modify_) require [Nonempty a] for IO.Promise construction. Non-blocking try* operations do not require this constraint.

Proofs & Guarantees

Structural Invariant

Maintained by all operations:

  • If value.isSome, then takers is empty (no one waits when a value exists).
  • If value.isNone, then putters is empty (no one waits to put when empty).

FIFO Fairness

Waiters are queued in Std.Queue and dequeued in insertion order. Both takers and putters are served FIFO.

No Lost Wakeups

  • Every put on an empty MVar with a taker wakes exactly one taker.
  • Every take on a full MVar with a putter wakes exactly one putter.
  • Proof by case analysis on the match branches in take/put.

Axiom-Dependent Properties (documented, not machine-checked)

  • Linearizability – depends on Std.Mutex providing mutual exclusion.
  • Starvation-freedom – depends on Lean’s task scheduler being eventually fair and on complementary operations eventually occurring.
  • Progress – follows from no-lost-wakeups + mutex correctness.

Example

import Hale.Control.Concurrent.MVar

open Hale

-- Create an MVar with an initial value
let mv <- MVar.new 42

-- Non-blocking try operations
let val <- mv.tryRead    -- some 42
let ok  <- mv.tryPut 99  -- false (MVar is full)

-- Async take (returns a Task)
let task <- mv.take
let val  <- IO.wait task  -- 42 (MVar is now empty)

-- Async put
let task <- mv.put 100
IO.wait task              -- MVar now holds 100

-- Modify with result
let task <- mv.modify fun x => pure (x + 1, x)
let old  <- IO.wait task  -- 100 (MVar now holds 101)

-- Sync wrappers (block OS thread -- prefer async versions)
let v <- mv.takeSync      -- 101
mv.putSync 200

Performance Notes

  • Promise-based blocking: Waiters are dormant promises, not spinning or sleeping OS threads. A million waiting tasks consume memory for the promise objects but no OS thread resources.
  • Mutex contention: All operations go through Std.Mutex.atomically. Under extreme contention, this is the bottleneck.
  • Sync wrappers (takeSync, putSync, readSync) block an OS thread via IO.wait. Use them only at top-level or in non-scalable contexts; prefer the async versions for concurrent code.
  • isEmpty is a snapshot: The result may be stale by the time you act on it. Do not use it for synchronisation.

Chan

Lean: Hale.Control.Concurrent.Chan | Haskell: Control.Concurrent.Chan

Overview

An unbounded FIFO channel with subscriber-based duplication. Internally uses a Std.Queue buffer protected by a Std.Mutex, with a queue of reader promises for when the buffer is empty. A shared write-side mutex holds the subscriber list, enabling dup.

All blocking uses promises: read returns BaseIO (Task a), never blocking an OS thread.

API Mapping

LeanHaskellKindSignature
Chan.newnewChanConstructorBaseIO (Chan a)
Chan.writewriteChanFunctionChan a -> a -> BaseIO Unit
Chan.readreadChanAsync opChan a -> BaseIO (Task a)
Chan.dupdupChanFunctionChan a -> BaseIO (Chan a)
Chan.tryRead(no direct equivalent)Non-blockingChan a -> BaseIO (Option a)

Constraint: [Nonempty a]

Chan.read requires [Nonempty a] for IO.Promise construction. Chan.tryRead and Chan.write do not require this constraint.

Subscriber-Based Dup Design

Unlike Haskell’s linked-list-of-MVars implementation, hale uses a subscriber array:

  • Each Chan has a private readState (per-reader buffer + waiters) and a shared writeState (subscriber list).
  • write delivers the value to all current subscribers.
  • dup creates a new readState and registers it in the shared subscriber list. The new channel shares the write side but reads independently.
  • Values written before dup that have not been read are not visible on the new channel (matching Haskell’s dupChan semantics).

Proofs & Guarantees

Structural Invariant

Maintained by write/read:

buffer.isEmpty = false  =>  waiters.isEmpty = true

If there are buffered values, no reader should be waiting. Enforced by read consuming from the buffer first, and write resolving a waiter before buffering.

FIFO Ordering

Values are read in the order they were written, guaranteed by Std.Queue.

Write Fan-Out

A single write delivers to all current subscribers atomically (the subscriber list is read under the write-side mutex).

Example

import Hale.Control.Concurrent.Chan

open Hale

-- Create a channel
let ch <- Chan.new Nat

-- Write values
ch.write 1
ch.write 2
ch.write 3

-- Read (async, returns Task)
let task <- ch.read
let val  <- IO.wait task  -- 1

-- Non-blocking read
let maybe <- ch.tryRead   -- some 2

-- Duplicate: new reader sees future writes only
let ch2 <- ch.dup
ch.write 10
let t1 <- ch.read
let t2 <- ch2.read
let v1 <- IO.wait t1  -- 3 (buffered before dup, still in ch's buffer)
                       -- actually 3 was already buffered, 10 goes after
let v2 <- IO.wait t2  -- 10 (ch2 only sees writes after dup)

Performance Notes

  • Unbounded buffer: There is no backpressure. A fast writer with a slow reader will accumulate values in the Std.Queue buffer without bound.
  • Write cost is O(subscribers): Each write iterates over all subscribers. For a single-reader channel this is O(1); for many dup’d readers it scales linearly.
  • Reader contention: Each reader has its own Std.Mutex-protected state, so multiple readers do not contend with each other. Writers contend briefly on the shared write-side mutex to read the subscriber list.
  • Promise-based blocking: A read on an empty channel creates a dormant promise – no OS thread is consumed while waiting.

QSem

Lean: Hale.Control.Concurrent.QSem | Haskell: Control.Concurrent.QSem

Overview

A simple quantity semaphore: at most n resources can be acquired concurrently. All blocking is promise-based. Waiters are served in FIFO order.

API Mapping

LeanHaskellKindSignature
QSem.newnewQSemConstructorNat -> BaseIO QSem
QSem.waitwaitQSemAsync opQSem -> BaseIO (Task Unit)
QSem.signalsignalQSemFunctionQSem -> BaseIO Unit
QSem.withSem(bracket pattern)CombinatorQSem -> IO a -> IO a

Proofs & Guarantees

Non-Negative Count

The count is Nat by construction – it cannot underflow below zero.

Structural Invariant

count > 0  =>  waiters = empty

If resources are available, no one should be waiting. Maintained by signal: it wakes a waiter before incrementing the count.

FIFO Fairness

Waiters are queued in Std.Queue and dequeued in insertion order.

No Lost Wakeups

signal either wakes exactly one waiter or increments the count. Both branches are mutually exclusive (case split on waiters.dequeue?).

Exception Safety

withSem uses try/finally to ensure signal is called even if the action throws.

Example

import Hale.Control.Concurrent.QSem

open Hale

-- Create a semaphore allowing 3 concurrent accesses
let sem <- QSem.new 3

-- Acquire one unit (async, non-blocking by type)
let task <- sem.wait
IO.wait task

-- Release one unit
sem.signal

-- Bracket pattern: acquire, run, release
let result <- sem.withSem do
  -- at most 3 threads run this section concurrently
  pure 42

Performance Notes

  • Promise-based blocking: A wait on an exhausted semaphore creates a dormant promise. No OS thread is consumed while waiting.
  • withSem blocks an OS thread via IO.wait for the initial acquire. For fully non-blocking usage, call wait directly and chain with BaseIO.bindTask.
  • Mutex contention: Both wait and signal go through Std.Mutex.atomically. Under high contention this is the bottleneck.
  • Single-unit granularity: For multi-unit acquire/release, use QSemN instead.

QSemN

Lean: Hale.Control.Concurrent.QSemN | Haskell: Control.Concurrent.QSemN

Overview

A generalised quantity semaphore that allows acquiring and releasing arbitrary amounts of a resource. Like QSem, but wait and signal take a Nat parameter for the number of units.

API Mapping

LeanHaskellKindSignature
QSemN.newnewQSemNConstructorNat -> BaseIO QSemN
QSemN.waitwaitQSemNAsync opQSemN -> Nat -> BaseIO (Task Unit)
QSemN.signalsignalQSemNFunctionQSemN -> Nat -> BaseIO Unit
QSemN.withSemN(bracket pattern)CombinatorQSemN -> Nat -> IO a -> IO a

Proofs & Guarantees

Non-Negative Count

The count is Nat by construction – it cannot underflow below zero.

Structural Invariant

count >= n  =>  no waiter requesting <= n is queued

When signal adds resources, it greedily wakes FIFO-ordered waiters whose requested amount can be satisfied.

Greedy FIFO Wakeup

signal calls wakeWaiters, which iterates from the front of the queue and wakes each waiter whose needed amount is less than or equal to the current count. It stops at the first waiter that cannot be satisfied (head-of-line blocking, matching Haskell semantics).

No Lost Wakeups

signal always either wakes one or more waiters or leaves the increased count for future wait calls. The wakeWaiters loop is exhaustive up to the first unsatisfiable request.

Exception Safety

withSemN uses try/finally to ensure signal n is called even if the action throws.

Example

import Hale.Control.Concurrent.QSemN

open Hale

-- Create a semaphore with 100 units (e.g., bytes of bandwidth)
let sem <- QSemN.new 100

-- Acquire 30 units
let task <- sem.wait 30
IO.wait task

-- Release 30 units
sem.signal 30

-- Bracket pattern: acquire n, run, release n
let result <- sem.withSemN 50 do
  -- up to 100 units can be held concurrently
  pure "done"

Greedy Wakeup Detail

When signal n is called, the internal wakeWaiters function runs:

  1. Peek at the front waiter (needed, promise).
  2. If count >= needed, subtract needed from count, resolve promise, and repeat from step 1.
  3. If count < needed, stop. The waiter remains queued (head-of-line blocking).
  4. If the queue is empty, stop.

This means a large request at the front of the queue can block smaller requests behind it, even if enough resources are available for the smaller ones. This matches Haskell’s QSemN semantics.

Performance Notes

  • Promise-based blocking: A wait on an exhausted semaphore creates a dormant promise. No OS thread is consumed while waiting.
  • withSemN blocks an OS thread via IO.wait for the initial acquire. For fully non-blocking usage, call wait directly and chain with BaseIO.bindTask.
  • Wakeup cost: signal may wake multiple waiters in a single call (greedy loop), all within a single Std.Mutex.atomically block.
  • Head-of-line blocking: A large request at the front of the queue prevents smaller requests behind it from being satisfied. Consider whether this is acceptable for your workload.

ByteString

Lean: Hale.ByteString | Haskell: bytestring (https://hackage.haskell.org/package/bytestring)

API Reference: Hale.ByteString

Overview

Port of Haskell’s bytestring library to Lean 4 with maximalist typing. Provides:

  • Strict ByteString (Data.ByteString.ByteString): Slice-based representation with O(1) take/drop/splitAt, bounds proofs in the type
  • Short ByteString (Data.ByteString.ShortByteString): Thin newtype over ByteArray for API compatibility
  • Lazy ByteString (Data.ByteString.Lazy.LazyByteString): Chunked, Thunk-based lazy evaluation with non-empty chunk invariant
  • Builder (Data.ByteString.Builder): Continuation-based O(1) concatenation with proven Monoid laws
  • Char8 (Data.ByteString.Char8): Latin-1 character-oriented wrappers

Lean Stdlib Reuse

Uses ByteArray, UInt8, IO.FS.readBinFile/writeBinFile from Lean’s standard library. The port adds the slice representation, lazy chunking, and typed invariants that Lean lacks.

Module Map

Lean ModuleHaskell Module
Hale.ByteString.Data.ByteString.InternalData.ByteString.Internal
Hale.ByteString.Data.ByteStringData.ByteString
Hale.ByteString.Data.ByteString.Char8Data.ByteString.Char8
Hale.ByteString.Data.ByteString.ShortData.ByteString.Short
Hale.ByteString.Data.ByteString.Lazy.InternalData.ByteString.Lazy.Internal
Hale.ByteString.Data.ByteString.LazyData.ByteString.Lazy
Hale.ByteString.Data.ByteString.Lazy.Char8Data.ByteString.Lazy.Char8
Hale.ByteString.Data.ByteString.BuilderData.ByteString.Builder

Data.ByteString

Lean: Hale.ByteString.Data.ByteString | Haskell: Data.ByteString

Overview

Strict byte strings with O(1) slicing. The core type is a slice into a ByteArray:

structure ByteString where
  data : ByteArray
  off : Nat
  len : Nat
  valid : off + len ≤ data.size

Key API Mapping

LeanHaskellNotes
ByteString.emptyemptyO(1)
ByteString.singletonsingleton
ByteString.packpackList UInt8 → ByteString
ByteString.unpackunpackByteString → List UInt8
ByteString.taketakeO(1) slice
ByteString.dropdropO(1) slice
ByteString.splitAtsplitAtO(1) slice
ByteString.head hheadTotal with proof h : len > 0
ByteString.tail htailO(1) slice with proof
ByteString.last hlastTotal with proof
ByteString.init hinitO(1) slice with proof
ByteString.index i hindexBounds-checked with proof h : i < len
ByteString.foldlfoldl'Strict left fold
ByteString.foldrfoldrRight fold
ByteString.mapmapO(n)
ByteString.reversereverseO(n)
ByteString.filterfilterO(n)
ByteString.isPrefixOfisPrefixOf
ByteString.isSuffixOfisSuffixOf
ByteString.isInfixOfisInfixOf
ByteString.readFilereadFileWraps IO.FS.readBinFile
ByteString.writeFilewriteFileWraps IO.FS.writeBinFile

Instances

  • BEq ByteString – byte-by-byte comparison
  • Ord ByteString – lexicographic ordering
  • Append ByteString – concatenation (O(m+n))
  • ToString ByteString[w1, w2, ...] format
  • Hashable ByteString
  • Inhabited ByteString – empty

Proofs

  • take_valid / drop_valid – slice operations preserve bounds
  • null_iff_length_zeronull <-> length = 0

Performance

OperationComplexityNotes
take, drop, splitAtO(1)Zero-copy slice
head, last, indexO(1)Direct array access
tail, initO(1)Slice adjustment
cons, snoc, appendO(n)Copies data
map, reverse, filterO(n)
copyO(n)Materialises fresh array

Data.ByteString.Char8

Lean: Hale.ByteString.Data.ByteString.Char8 | Haskell: Data.ByteString.Char8

Overview

Character-oriented operations on strict ByteString using the Latin-1 (ISO 8859-1) encoding. Each Char is truncated to its low 8 bits via Char.toUInt8.

This module re-exports the core ByteString type and provides Char-based wrappers around the byte-level API.

Key API Mapping

LeanHaskellNotes
Char8.packpackString → ByteString (Latin-1)
Char8.unpackunpackByteString → String
Char8.singletonsingletonChar → ByteString
Char8.headheadReturns Char with proof
Char8.lastlastReturns Char with proof
Char8.mapmap(Char → Char) → ByteString → ByteString
Char8.filterfilter(Char → Bool) → ByteString → ByteString
Char8.lineslinesSplit on '\n' (byte 10)
Char8.wordswordsSplit on ASCII whitespace
Char8.unlinesunlinesJoin with '\n'
Char8.unwordsunwordsJoin with ' '
Char8.isSpace(helper)ASCII whitespace check

Design Notes

  • Latin-1 only: Characters above U+00FF are truncated. This matches Haskell’s Data.ByteString.Char8 semantics exactly.
  • No Unicode support: For UTF-8 encoded byte strings, use a dedicated text library instead.
  • Shared representation: Char8 functions operate on the same ByteString type – there is no separate Char8ByteString.

Instances

No additional instances beyond those on ByteString. The Char8 module provides functions, not a new type.

Data.ByteString.Short

Lean: Hale.ByteString.Data.ByteString.Short | Haskell: Data.ByteString.Short

Overview

Short byte strings: a thin newtype over ByteArray providing a ByteString-compatible API without the slice indirection. Useful when you need compact storage and do not benefit from O(1) slicing.

Core Type

structure ShortByteString where
  data : ByteArray

Unlike ByteString, there is no offset/length – the entire ByteArray is the content.

Key API Mapping

LeanHaskellNotes
ShortByteString.emptyempty
ShortByteString.packpackList UInt8 → ShortByteString
ShortByteString.unpackunpackShortByteString → List UInt8
ShortByteString.lengthlengthO(1), direct ByteArray.size
ShortByteString.indexindexBounds-checked with proof
ShortByteString.toByteStringfromShortWraps as full-offset slice
ShortByteString.fromByteStringtoShortCopies slice to fresh array

Instances

  • BEq ShortByteString
  • Ord ShortByteString
  • Inhabited ShortByteString – empty
  • ToString ShortByteString

When to Use

  • Use ShortByteString for small, long-lived keys (e.g., hash map keys) where GC overhead of the slice representation matters.
  • Use ByteString for I/O buffers and large data where O(1) slicing is valuable.

Performance

OperationComplexityNotes
lengthO(1)Direct array size
indexO(1)Direct array access
toByteStringO(1)Wraps with off=0
fromByteStringO(n)Copies slice data
pack, unpackO(n)

Data.ByteString.Lazy

Lean: Hale.ByteString.Data.ByteString.Lazy | Haskell: Data.ByteString.Lazy

Overview

Lazy byte strings built from a spine of strict ByteString chunks connected via Thunk. This emulates Haskell’s lazy evaluation in Lean’s strict runtime.

Core Type

inductive LazyByteString where
  | empty : LazyByteString
  | chunk : (c : ByteString) → (h : c.len > 0) → Thunk LazyByteString → LazyByteString

The h : c.len > 0 invariant guarantees that every chunk in the spine is non-empty. This rules out degenerate representations and ensures that operations like null and length behave correctly.

Key API Mapping

LeanHaskellNotes
LazyByteString.emptyempty
LazyByteString.fromStrictfromStrictSingle-chunk lazy
LazyByteString.toStricttoStrictMaterialises all chunks
LazyByteString.toChunkstoChunksList ByteString
LazyByteString.fromChunksfromChunksFilters empty chunks
LazyByteString.lengthlengthTraverses spine
LazyByteString.taketakeLazy, may split a chunk
LazyByteString.dropdropLazy
LazyByteString.appendappendO(1) thunk link
LazyByteString.mapmapChunk-wise
LazyByteString.foldlChunksfoldlChunksFold over chunks

Instances

  • BEq LazyByteString
  • Append LazyByteString – O(1) via thunk linking
  • Inhabited LazyByteString – empty

Design Notes

  • Non-empty chunk invariant: The proof h : c.len > 0 on every chunk constructor prevents empty chunks from appearing in the spine. This simplifies reasoning about null, head, and uncons.
  • Thunk-based laziness: Each tail is wrapped in Thunk so chunks are only forced on demand, matching Haskell’s lazy list spine.
  • Chunk size: fromChunks silently drops empty byte strings. Consumers should use defaultChunkSize (typically 32 KiB) when building lazy byte strings incrementally.

Performance

OperationComplexityNotes
appendO(1)Thunk link, no copying
consO(1)Prepends single-byte chunk
lengthO(chunks)Must traverse spine
toStrictO(n)Copies all data into one array
take, dropO(chunks affected)May split one chunk

Data.ByteString.Builder

Lean: Hale.ByteString.Data.ByteString.Builder | Haskell: Data.ByteString.Builder

Overview

Continuation-based builder for efficient incremental construction of byte strings. The builder type wraps a difference-list style function, achieving O(1) concatenation.

Core Type

structure Builder where
  run : LazyByteString → LazyByteString

A Builder is a function that prepends its content onto a continuation LazyByteString. Two builders compose by function composition, giving O(1) append.

Key API Mapping

LeanHaskellNotes
Builder.emptymemptyIdentity builder
Builder.append<> / mappendO(1) composition
Builder.singletonword8Single byte
Builder.byteStringbyteStringFrom strict ByteString
Builder.lazyByteStringlazyByteStringFrom lazy ByteString
Builder.toLazyByteStringtoLazyByteStringExecute the builder
Builder.word16BEword16BEBig-endian 16-bit
Builder.word16LEword16LELittle-endian 16-bit
Builder.word32BEword32BEBig-endian 32-bit
Builder.word32LEword32LELittle-endian 32-bit
Builder.word64BEword64BEBig-endian 64-bit
Builder.word64LEword64LELittle-endian 64-bit
Builder.intDecintDecDecimal integer encoding

Monoid Law Proofs

The module proves that Builder forms a lawful monoid:

  • empty_append: Builder.empty ++ b = b
  • append_empty: b ++ Builder.empty = b
  • append_assoc: (a ++ b) ++ c = a ++ (b ++ c)

These follow directly from function composition laws.

Usage Pattern

let b := Builder.byteString header
     ++ Builder.word32BE payloadLen
     ++ Builder.byteString payload
let result := Builder.toLazyByteString b

Performance

OperationComplexityNotes
appendO(1)Function composition
singleton, byteStringO(1)Wraps input
toLazyByteStringO(n)Executes all builders

Word8 – UInt8 Classification

Lean: Hale.Word8 | Haskell: word8

UInt8 classification predicates and byte constants. All predicates are @[inline] for zero overhead. Proofs via exhaustive native_decide over all 256 values.

API

FunctionDescription
isUpperByte in [65, 90]
isLowerByte in [97, 122]
isAlphaASCII letter
isDigitASCII digit
isSpaceWhitespace byte

Files

  • Hale/Word8/Data/Word8.lean – Classification predicates and constants

Time – Clock and Time Types

Lean: Hale.Time | Haskell: time

UTC time and durations. Wraps Lean’s IO.monoNanosNow for high-resolution monotonic timing.

Key Types

structure NominalDiffTime where
  nanoseconds : Int

Files

  • Hale/Time/Data/Time/Clock.lean – NominalDiffTime, UTCTime

STM – Software Transactional Memory

Lean: Hale.STM | Haskell: stm

Pessimistic STM with global mutex serialization. Provides TVar, TMVar, and TQueue for concurrent programming.

Key Types

TypeDescription
STM αTransactional computation
TVar αMutable variable (IO.Ref α)
TMVar αSynchronization variable (empty or full)
TQueue αUnbounded FIFO queue (two-list amortized)

API

FunctionDescription
atomicallyExecute STM transaction
retryBlock and retry transaction
orElseAlternative transaction
newTVarIO / readTVar / writeTVarTVar operations
newTMVar / takeTMVar / putTMVarTMVar operations
newTQueue / readTQueue / writeTQueueTQueue operations

Files

  • Hale/STM/Control/Monad/STM.lean – STM monad, atomically, retry, orElse
  • Hale/STM/Control/Concurrent/STM/TVar.lean – Transactional variables
  • Hale/STM/Control/Concurrent/STM/TMVar.lean – Transactional MVars
  • Hale/STM/Control/Concurrent/STM/TQueue.lean – Transactional queues

DataDefault – Default Values

Lean: Hale.DataDefault | Haskell: data-default

Default typeclass providing sensible starting values, distinct from Inhabited.

Key Types

class Default (α : Type) where
  default : α

Instances for: Bool, Nat, Int, String, List, Array, Option, Unit, products.

Files

  • Hale/DataDefault/Data/Default.lean – Default class and instances

ResourceT – Resource Management Monad

Lean: Hale.ResourceT | Haskell: resourcet

API Reference: Hale.ResourceT | Resource

Ensures cleanup of resources (file handles, connections) even on exceptions.

Lifecycle

  runResourceT ---+
                  |
    allocate(acquire, release)
       | -> (ReleaseKey, resource)
       |
    ... use resource ...
       |
    release(key)  <-- optional early release
       |
  +---- finally: all remaining cleanups run (LIFO order)

Detailed Flow

  +---------------------------------------------+
  | runResourceT                                 |
  |                                              |
  |   ref <- IO.mkRef #[]   (cleanup registry)  |
  |                                              |
  |   try                                        |
  |     allocate(open, close)                    |
  |       |  push (key0, close) to registry      |
  |       v                                      |
  |     allocate(connect, disconnect)            |
  |       |  push (key1, disconnect) to registry |
  |       v                                      |
  |     ... user code ...                        |
  |       |                                      |
  |     release(key0)  -- early release          |
  |       |  remove key0 from registry           |
  |       |  run close action                    |
  |       v                                      |
  |     ... more user code ...                   |
  |                                              |
  |   finally                                    |
  |     for (_, cleanup) in registry.reverse:    |
  |       try cleanup catch _ => ()              |
  |       -- key1 (disconnect) runs here         |
  |       -- key0 already released, not in list  |
  +---------------------------------------------+

Core Types

ResourceT (Monad Transformer)

def ResourceT (m : Type -> Type) (a : Type) := IO.Ref CleanupMap -> m a

ReleaseKey (Opaque, Single-Use)

structure ReleaseKey where
  private mk ::
    id : Nat

The constructor is private – only allocate can create keys.

Guarantees

  • LIFO cleanup: Last allocated = first released
  • Exception-safe: try/finally ensures all cleanups run
  • Single-use keys: Releasing twice is a no-op (idempotent)
  • Cleanup isolation: If one cleanup throws, remaining cleanups still run

Proven Properties (1 theorem)

TheoremStatement
releaseKey_eqa = b <-> a.id = b.id

Axiom-Dependent Properties (documented, not machine-checked)

  • Exception safety depends on IO.finally semantics
  • LIFO ordering depends on Array preserving insertion order (which it does)

Instances

  • Monad (ResourceT m) (for any Monad m)
  • MonadLift IO (ResourceT m) (for any MonadLift IO m)

Files

  • Hale/ResourceT/Control/Monad/Trans/Resource.lean – Full implementation + 1 proof

UnliftIO – MonadUnliftIO

Lean: Hale.UnliftIO | Haskell: unliftio-core

Allows running monadic actions in IO by “unlifting” them. CPS form avoids universe issues.

Key Types

class MonadUnliftIO (m : Type → Type) where
  withRunInIO : ((∀ α, m α → IO α) → IO β) → m β

Laws

  • withRunInIO (fun run => run m) = m (identity)
  • withRunInIO (fun run => run (liftIO io)) = liftIO io (lift-run roundtrip)

Files

  • Hale/UnliftIO/Control/Monad/IO/Unlift.lean – MonadUnliftIO class

Conduit – Stream Processing

Lean: Hale.Conduit | Haskell: conduit

Composable streaming data pipelines. ConduitT i o m r is a CPS wrapper over Pipe with O(1) monadic bind.

Key Types

TypeDescription
ConduitT i o m rStream processor (input i, output o, monad m, result r)
Source m oProducer (ConduitT () o m ())
Sink i m rConsumer (ConduitT i Void m r)

API

FunctionDescription
awaitRequest next input
yieldProduce output
leftoverCPush back unconsumed input
awaitForeverProcess all inputs
pipe / .|Fuse two conduits
runConduitExecute a pipeline
bracketPResource-safe bracket

Files

  • Hale/Conduit/Data/Conduit.lean – Re-exports
  • Hale/Conduit/Data/Conduit/Internal/Pipe.lean – Pipe type
  • Hale/Conduit/Data/Conduit/Internal/Conduit.lean – ConduitT, fusion
  • Hale/Conduit/Data/Conduit/Combinators.lean – Standard combinators

Network – POSIX Sockets with Phantom State

Lean: Hale.Network | Haskell: network

POSIX socket API with phantom type parameters encoding lifecycle states. A Socket (state : SocketState) tracks state transitions (fresh → bound → listening → connected → closed) at compile time, making it a type error to send on an unconnected socket or close an already-closed one.

Socket State Machine

  Fresh ──bind──► Bound ──listen──► Listening ──accept──► Connected
    │                                                         │
    └──────────────────── close ◄─────────────────────────────┘

Key Types

TypeDescription
Socket (state : SocketState)Phantom-typed socket
SocketState.fresh | .bound | .listening | .connected | .closed
SockAddrSocket address
AcceptOutcome / RecvOutcome / SendOutcomeNon-blocking I/O outcomes
RecvBufferBuffered reader with CRLF scanning (C FFI)
EventDispatcherGreen thread suspension bridge (kqueue/epoll)

API

FunctionState TransitionSignature
socket.freshIO (Socket .fresh)
bind.fresh.boundSocket .fresh → SockAddr → IO (Socket .bound)
listen.bound.listeningSocket .bound → Nat → IO (Socket .listening)
accept.listening.connectedSocket .listening → IO (Socket .connected × SockAddr)
connect.fresh.connectedSocket .fresh → SockAddr → IO (Socket .connected)
sendrequires .connectedSocket .connected → ByteArray → IO Nat
recvrequires .connectedSocket .connected → Nat → IO ByteArray
closeany → .closedSocket s → IO Unit

Files

  • Hale/Network/Network/Socket/Types.lean – SocketState, SockAddr, phantom socket type
  • Hale/Network/Network/Socket/FFI.lean – C FFI declarations
  • Hale/Network/Network/Socket.lean – High-level API with state transitions
  • Hale/Network/Network/Socket/ByteString.lean – ByteArray send/recv
  • Hale/Network/Network/Socket/Blocking.lean – Blocking wrappers
  • Hale/Network/Network/Socket/EventDispatcher.lean – kqueue/epoll dispatcher

IpRoute – IP Address Types

Lean: Hale.IpRoute | Haskell: iproute

IP addresses (IPv4, IPv6) and CIDR ranges with decidable membership predicate.

Key Types

TypeDescription
IPv432-bit address
IPv6Pair of UInt64
AddrRangeCIDR range with prefix length

API

FunctionSignature
ofOctets(UInt8, UInt8, UInt8, UInt8) → IPv4
toOctetsIPv4 → (UInt8, UInt8, UInt8, UInt8)
isMatchedToDecidable CIDR membership

Files

  • Hale/IpRoute/Data/IP.lean – IPv4, IPv6, CIDR ranges

Recv – Socket Receive

Lean: Hale.Recv | Haskell: recv

Thin wrapper for socket recv returning ByteArray. Empty result signals EOF.

API

FunctionDescription
recvReceive up to N bytes, returns empty ByteArray on EOF
recvStringUTF-8 decoded variant

Files

  • Hale/Recv/Network/Socket/Recv.lean – recv, recvString

StreamingCommons – Streaming Network Utilities

Lean: Hale.StreamingCommons | Haskell: streaming-commons

Streaming network utilities: bind/connect helpers and application data abstraction.

Key Types

TypeDescription
AppDataConnection data with read/write/close/address fields

API

FunctionDescription
bindPortTCPBind server socket to port
getSocketTCPConnect to remote TCP server
acceptSafeAccept with transient error retry

Files

  • Hale/StreamingCommons/Data/Streaming/Network.lean – AppData, bind/connect helpers

SimpleSendfile – Zero-Copy File Sending

Lean: Hale.SimpleSendfile | Haskell: simple-sendfile

Efficient file sending using sendfile(2) syscall for zero-copy transfer, with read+send fallback.

Key Types

structure FilePart where
  offset : Nat
  count  : Nat

API

FunctionDescription
sendFileSend file (or portion) over connected socket

Files

  • Hale/SimpleSendfile/Network/Sendfile.lean – sendFile, FilePart

TLS – OpenSSL FFI Wrapper

Lean: Hale.TLS | Haskell: tls (concept) / OpenSSL bindings

API Reference: Hale.TLS | Types | Context | WarpTLS

HTTPS support via OpenSSL/LibreSSL C FFI.

TLS Handshake Flow

  Client                          Server
    |                               |
    |     TCP connect               |
    |------------------------------>|
    |                               |
    |     ClientHello               |
    |------------------------------>|
    |                               | SSL_accept()
    |     ServerHello + Cert        | (OpenSSL FFI)
    |<------------------------------|
    |                               |
    |     Key Exchange + Finished   |
    |------------------------------>|
    |                               |
    |     === TLS Established ===   |
    |                               |
    |     HTTP/1.1 (encrypted)      |
    |------------------------------>|
    |     or HTTP/2 (via ALPN)      |
    |                               |

Opaque Handles (FFI Pattern)

opaque TLSContextHandle : NonemptyType    -- SSL_CTX*
opaque TLSSessionHandle : NonemptyType    -- SSL*
def TLSContext := TLSContextHandle.type
def TLSSession := TLSSessionHandle.type

Both handles use the lean_alloc_external / lean_register_external_class pattern (same as Socket). The GC finalizer calls SSL_free / SSL_CTX_free automatically when the Lean object is collected.

API

LeanC (OpenSSL)Signature
createContextSSL_CTX_new + SSL_CTX_use_certificate_fileString -> String -> IO TLSContext
setAlpnSSL_CTX_set_alpn_select_cbTLSContext -> IO Unit
acceptSocketSSL_new + SSL_set_fd + SSL_acceptTLSContext -> RawSocket -> IO TLSSession
readSSL_readTLSSession -> USize -> IO ByteArray
writeSSL_writeTLSSession -> ByteArray -> IO Unit
closeSSL_shutdown + SSL_freeTLSSession -> IO Unit
getVersionSSL_get_versionTLSSession -> IO String
getAlpnSSL_get0_alpn_selectedTLSSession -> IO (Option String)

TLS Version Types

inductive TLSVersion where
  | tls10 | tls11 | tls12 | tls13

Security

  • Minimum TLS 1.2 (enforced by SSL_CTX_set_min_proto_version)
  • ALPN for HTTP/2 negotiation (“h2” / “http/1.1”)
  • Certificate + key validated at context creation time (OpenSSL rejects invalid files)
  • Hardware AES-NI acceleration (via OpenSSL)
  • Session finalizer ensures SSL_shutdown + SSL_free on GC

Integration with WarpTLS

WarpTLS.runTLS creates a TLSContext once at startup, then for each accepted connection:

1. Socket.accept  -> Socket .connected
2. TLS.acceptSocket ctx sock -> TLSSession
3. parseRequest + app + sendResponse (via TLS read/write)
4. TLS.close session
5. Socket.close sock

Both TLS session and socket are cleaned up in try/finally blocks.

Files

  • Hale/TLS/Network/TLS/Types.lean – TLSVersion, CipherID
  • Hale/TLS/Network/TLS/Context.lean – Opaque handles + FFI declarations

HttpTypes – Core HTTP Types

Lean: Hale.HttpTypes | Haskell: http-types

Core HTTP/1.1 types: methods, status codes, headers, versions, and URI query parsing. Status codes carry a bounded proof (100 ≤ code ≤ 999).

Key Types

TypeDescription
StdMethodInductive of standard HTTP methods (GET, POST, HEAD, PUT, DELETE, etc.)
MethodStdMethod or custom String
StatusHTTP status with bounded 3-digit code (proof: 100 ≤ code ≤ 999)
HeaderNameCase-insensitive header name
HeaderHeaderName × ByteArray
HttpVersionMajor/minor pair with ordering
Query / QueryItemParsed query strings

API

FunctionSignature
parseQueryString → Query
renderQueryBool → Query → String
urlEncodeBool → ByteArray → ByteArray
urlDecodeBool → ByteArray → Option ByteArray

Files

  • Hale/HttpTypes/Network/HTTP/Types/Method.lean – StdMethod, Method
  • Hale/HttpTypes/Network/HTTP/Types/Status.lean – Status with bounded proof
  • Hale/HttpTypes/Network/HTTP/Types/Header.lean – Header types
  • Hale/HttpTypes/Network/HTTP/Types/Version.lean – HttpVersion
  • Hale/HttpTypes/Network/HTTP/Types/URI.lean – Query parsing and URL encoding

HttpDate – HTTP Date Parsing

Lean: Hale.HttpDate | Haskell: http-date

HTTP date parsing and formatting per RFC 7231. Supports IMF-fixdate, RFC 850, and asctime formats. Fields carry bounded proofs (month 1-12, day 1-31, hour 0-23, etc.).

Key Types

structure HTTPDate where
  year   : Nat
  month  : Nat  -- 1-12
  day    : Nat  -- 1-31
  hour   : Nat  -- 0-23
  minute : Nat  -- 0-59
  second : Nat  -- 0-60 (leap second)

Files

  • Hale/HttpDate/Network/HTTP/Date.lean – HTTPDate type, parser, formatter

Http2 – HTTP/2 Protocol

Lean: Hale.Http2 | Haskell: http2

HTTP/2 framing, HPACK header compression, flow control, and server implementation per RFC 9113. Stream IDs carry a 31-bit bound proof. Settings carry RFC 9113 value constraint proofs.

Key Types

TypeDescription
StreamId31-bit bounded stream identifier
FrameTypeInductive of all RFC 9113 frame types
ErrorCodeConnection/stream error codes
SettingsRFC 9113 settings with proof-carrying value constraints
ConnectionErrorError code + message for GOAWAY
StreamErrorStream ID + error code + message for RST_STREAM

Modules

ModuleDescription
Frame.TypesFrame type definitions and flags
Frame.EncodeBinary frame serialization
Frame.DecodeBinary frame deserialization
HPACK.TableDynamic/static header table
HPACK.HuffmanHuffman coding
HPACK.EncodeHPACK header compression
HPACK.DecodeHPACK header decompression
FlowControlWindow-based flow control
StreamStream lifecycle management
ServerHTTP/2 server implementation

Files

  • Hale/Http2/Network/HTTP2/Types.lean – Core types
  • Hale/Http2/Network/HTTP2/Frame/Types.lean – Frame definitions
  • Hale/Http2/Network/HTTP2/Frame/Encode.lean – Frame encoding
  • Hale/Http2/Network/HTTP2/Frame/Decode.lean – Frame decoding
  • Hale/Http2/Network/HTTP2/HPACK/Table.lean – HPACK tables
  • Hale/Http2/Network/HTTP2/HPACK/Huffman.lean – Huffman coding
  • Hale/Http2/Network/HTTP2/HPACK/Encode.lean – HPACK encoding
  • Hale/Http2/Network/HTTP2/HPACK/Decode.lean – HPACK decoding
  • Hale/Http2/Network/HTTP2/FlowControl.lean – Flow control
  • Hale/Http2/Network/HTTP2/Stream.lean – Stream management
  • Hale/Http2/Network/HTTP2/Server.lean – Server

Http3 – HTTP/3 Protocol

Lean: Hale.Http3 | Haskell: http3

HTTP/3 framing and QPACK header compression per RFC 9114. Uses QUIC variable-length integer encoding (RFC 9000 Section 16) with proven roundtrip properties.

Key Types

TypeDescription
FrameTypeInductive of HTTP/3 frame types (DATA, HEADERS, SETTINGS, GOAWAY, etc.)

Proven Properties

  • decodeVarInt (encodeVarInt n) = some n (roundtrip)

Modules

ModuleDescription
FrameFrame types + varint encoding/decoding
ErrorHTTP/3 error codes
QPACK.TableQPACK static/dynamic tables
QPACK.EncodeQPACK header compression
QPACK.DecodeQPACK header decompression
ServerHTTP/3 server

Files

  • Hale/Http3/Network/HTTP3/Frame.lean – Frame types, varint codec
  • Hale/Http3/Network/HTTP3/Error.lean – Error codes
  • Hale/Http3/Network/HTTP3/QPACK/Table.lean – QPACK tables
  • Hale/Http3/Network/HTTP3/QPACK/Encode.lean – QPACK encoding
  • Hale/Http3/Network/HTTP3/QPACK/Decode.lean – QPACK decoding
  • Hale/Http3/Network/HTTP3/Server.lean – Server

QUIC – QUIC Transport Protocol

Lean: Hale.QUIC | Haskell: quic

QUIC transport protocol types per RFC 9000. Connection IDs carry a bounded-length proof (≤ 20 bytes). Stream IDs encode directionality (client/server, bidi/uni) in the type.

Key Types

TypeDescription
ConnectionIdVariable-length up to 20 bytes (bounded proof)
StreamIdUInt64 with 2-bit type encoding
TransportParamsRFC 9000 Section 18 defaults

Modules

ModuleDescription
TypesCore protocol types
ConfigConfiguration
ConnectionConnection management
StreamStream management
ServerQUIC server
ClientQUIC client

Files

  • Hale/QUIC/Network/QUIC/Types.lean – ConnectionId, StreamId, TransportParams
  • Hale/QUIC/Network/QUIC/Config.lean – Configuration
  • Hale/QUIC/Network/QUIC/Connection.lean – Connection management
  • Hale/QUIC/Network/QUIC/Stream.lean – Stream management
  • Hale/QUIC/Network/QUIC/Server.lean – Server
  • Hale/QUIC/Network/QUIC/Client.lean – Client

BsbHttpChunked – Chunked Transfer Encoding

Lean: Hale.BsbHttpChunked | Haskell: bsb-http-chunked

HTTP/1.1 chunked transfer encoding framing. Each chunk: <hex-length>\r\n<data>\r\n. Terminator: 0\r\n\r\n.

API

FunctionDescription
hexCharEncode a hex digit
natToHexEncode Nat as hex bytes

Files

  • Hale/BsbHttpChunked/Network/HTTP/Chunked.lean – Chunked encoding helpers

HttpClient – HTTP Client

Lean: Hale.HttpClient | Haskell: http-client

HTTP client with pluggable transport. Connection abstracts plain TCP and TLS uniformly.

Key Types

TypeDescription
ConnectionRecord with read/write/close callbacks
RequestSerializable HTTP request
ResponseParsed status, headers, body

API

FunctionDescription
connectionFromSocketTCP connection builder
connectionFromTLSTLS connection builder

Modules

  • Types – Core types
  • Connection – Transport abstraction
  • Request – Request building and serialization
  • Response – Response parsing
  • Redirect – Redirect following

Files

  • Hale/HttpClient/Network/HTTP/Client/Types.lean – Connection, Request, Response
  • Hale/HttpClient/Network/HTTP/Client/Connection.lean – Transport
  • Hale/HttpClient/Network/HTTP/Client/Request.lean – Request
  • Hale/HttpClient/Network/HTTP/Client/Response.lean – Response
  • Hale/HttpClient/Network/HTTP/Client/Redirect.lean – Redirects

HttpConduit – HTTP + Conduit Integration

Lean: Hale.HttpConduit | Haskell: http-conduit

Conduit integration for HTTP client and simple high-level HTTP API.

API

FunctionDescription
httpSourceStream response body as conduit source
parseUrlParse URL string (http:// and https://)

Files

  • Hale/HttpConduit/Network/HTTP/Client/Conduit.lean – Conduit bridge
  • Hale/HttpConduit/Network/HTTP/Simple.lean – Simple one-shot API

Req – Type-Safe HTTP Client

Lean: Hale.Req | Haskell: req

Type-safe HTTP client with compile-time guarantees: method/body compatibility, HTTPS-only auth, non-empty hostnames, proven option monoid laws.

Compile-Time Guarantees

  • HttpBodyAllowed prevents GET with body
  • basicAuth returns Option .Https for type safety
  • Hostname non-emptiness proven at construction

Files

  • Hale/Req/Network/HTTP/Req.lean – Type-safe request builders

WAI – Web Application Interface

Lean: Hale.WAI | Haskell: wai

API Reference: Hale.WAI | Network.Wai.Internal | Network.Wai

The core abstraction for Hale’s HTTP ecosystem. Every web application, middleware, and server speaks WAI.

Request Lifecycle

   Client HTTP Request
         |
         v
  +--------------+     +------------------+
  | Socket.accept |---->| parseRequest     |
  |  (.listening) |     | (headers, body)  |
  +--------------+     +--------+---------+
                                |
                     WAI Request struct
                                |
         +----------------------+----------------------+
         |                      |                      |
         v                      v                      v
  +--------------+     +--------------+     +--------------+
  |  Middleware   |---->|  Middleware   |---->|  Application |
  |  (ForceSSL)  |     |  (Logger)    |     |  (user code) |
  +--------------+     +--------------+     +------+-------+
                                                    |
                                             Response (inductive)
                                                    |
                       +----------------------------+----------+
                       |              |              |          |
                       v              v              v          v
                  responseFile   responseBuilder  responseStream  responseRaw
                  (sendfile)    (in-memory)      (chunked)       (WebSocket)
                       |              |              |          |
                       +--------------+--------------+----------+
                                       |
                                sendResponse
                                       |
                                       v
                              Socket.connected

Core Types

Application (CPS + Indexed Monad)

abbrev Application :=
  Request -> (Response -> IO ResponseReceived) -> AppM .pending .sent ResponseReceived

The return type AppM .pending .sent ResponseReceived is an indexed monad that leverages Lean 4’s dependent types to enforce exactly-once response at compile time:

  • .pending → .sent: The application must transition from “no response” to “response sent”. AppM.respond is the only combinator that performs this transition.
  • No double-respond: After respond, the state is .sent. A second respond would need AppM .sent .sent, which does not exist – type error.
  • No skip-respond: The return type demands .sent as post-state. Returning without calling respond would leave the state at .pendingtype error.
  • No fabrication: AppM.mk is private. Application code cannot construct the value without going through the real combinators.

This is strictly stronger than Haskell’s WAI, where the contract is a gentleman’s agreement. Here the Lean 4 kernel verifies it.

Middleware (Monoid under Composition)

abbrev Middleware := Application -> Application

Proven algebraic properties:

TheoremStatement
idMiddleware_comp_leftid . m = m
idMiddleware_comp_rightm . id = m
modifyRequest_idmodifyRequest id = id
modifyResponse_idmodifyResponse id = id
ifRequest_falseifRequest (fun _ => false) m = id

Response (4 Delivery Modes)

inductive Response where
  | responseFile    (status headers path part)   -- sendfile(2)
  | responseBuilder (status headers body)        -- in-memory ByteArray
  | responseStream  (status headers body)        -- chunked streaming
  | responseRaw     (rawAction fallback)         -- raw socket (WebSocket)

Proven accessor laws (12 theorems):

  • status_responseBuilder s h b = s (and for File, Stream)
  • headers_responseBuilder s h b = h (and for File)
  • mapResponseHeaders id r = r (per constructor: builder, file, stream)
  • mapResponseStatus id r = r (per constructor: builder, file, stream)

RequestBodyLength (Dependent Encoding)

inductive RequestBodyLength where
  | chunkedBody                    -- Transfer-Encoding: chunked
  | knownLength (bytes : Nat)      -- Content-Length: N

Full Theorem List (17)

Response Accessor Laws (11, in Internal.lean)

TheoremSource
status_responseBuilderWai.Internal
status_responseFileWai.Internal
status_responseStreamWai.Internal
headers_responseBuilderWai.Internal
headers_responseFileWai.Internal
mapResponseHeaders_id_responseBuilderWai.Internal
mapResponseHeaders_id_responseFileWai.Internal
mapResponseHeaders_id_responseStreamWai.Internal
mapResponseStatus_id_responseBuilderWai.Internal
mapResponseStatus_id_responseFileWai.Internal
mapResponseStatus_id_responseStreamWai.Internal

Middleware Algebra (5, in Wai.lean)

TheoremSource
idMiddleware_comp_leftWai
idMiddleware_comp_rightWai
modifyRequest_idWai
modifyResponse_idWai
ifRequest_falseWai

Response Linearity (Compile-Time Guarantee)

  • Exactly-once response: The indexed AppM monad enforces at the type level that respond is invoked exactly once. AppM .pending .sent ResponseReceived can only be produced via AppM.respond (which transitions .pending → .sent). Double-respond is a compile-time error: no combinator transitions .sent → .sent. The private mk on AppM prevents circumventing the guarantee.

Files

  • Hale/WAI/Network/Wai/Internal.lean – Core types + 11 accessor theorems
  • Hale/WAI/Network/Wai.lean – Public API + 5 middleware algebra theorems

Warp – High-Performance HTTP Server

Lean: Hale.Warp | Haskell: warp

API Reference: Hale.Warp | Types | Settings | Request | Response | Run

A fast HTTP/1.x server achieving 156,000+ requests/sec with keep-alive.

Connection Lifecycle

  runSettings(port, app)
         |
         v
  +--------------------+
  | listenTCP(port)    |
  | Socket(.listening) |
  +--------+-----------+
           |
     acceptLoop <------------------------------------+
           |                                          |
           v                                          |
  +--------------------+                              |
  | Socket.accept      |                              |
  | -> (.connected)    |                              |
  +--------+-----------+                              |
           |                                          |
     forkIO (green thread)                            |
           |                                          |
           v                                          |
  +------------------------+                          |
  | runConnection          |                          |
  |  +------------------+  |                          |
  |  | parseRequest     |  |                          |
  |  | -> WAI.Request   |  |                          |
  |  +--------+---------+  |                          |
  |           |             |                          |
  |  +--------v---------+  |                          |
  |  | app(req, respond) |  |                          |
  |  | -> Response       |  |                          |
  |  +--------+---------+  |                          |
  |           |             |                          |
  |  +--------v---------+  |                          |
  |  | sendResponse     |  |                          |
  |  +--------+---------+  |                          |
  |           |             |                          |
  |  +--------v---------+  |                          |
  |  | connAction?      |  |                          |
  |  | keepAlive/close  |  |                          |
  |  +--------+---------+  |                          |
  |           |             |                          |
  |     +-----+-----+      |                          |
  |     | keepAlive  |      |                          |
  |     | -> loop ---+------+-- (back to parseRequest) |
  |     |            |      |                          |
  |     | close      |      |                          |
  |     | -> cleanup |      |                          |
  |     +------------+      |                          |
  +-------------------------+                          |
                                                       |
           <-------------------------------------------+

Transport Abstraction

inductive Transport where
  | tcp                                         -- plain channel
  | tls (major minor : Nat) (alpn : Option String) (cipher : UInt16)
  | quic (alpn : Option String) (cipher : UInt16)

Security proofs:

TheoremStatement
tcp_not_secureTransport.isSecure .tcp = false
tls_is_secureTransport.isSecure (.tls ..) = true
quic_is_secureTransport.isSecure (.quic ..) = true

Keep-Alive State Machine

  +---------------+    Connection: close    +-----------+
  |   HTTP/1.1    | ----------------------> |   CLOSE   |
  | (keep-alive   |                         +-----------+
  |  by default)  |    no Connection hdr         ^
  |               | ----------------------> keepAlive
  +---------------+                              |
                                                 |
  +---------------+    Connection: keep-alive    |
  |   HTTP/1.0    | ----------------------> keepAlive
  | (close by     |
  |  default)     |    no Connection hdr    +-----------+
  |               | ----------------------> |   CLOSE   |
  +---------------+                         +-----------+

Proven semantics:

  • connAction_http10_default: HTTP/1.0 without Connection header -> close
  • connAction_http11_default: HTTP/1.1 without Connection header -> keep-alive

How Lean 4’s Dependent Types Secure the Warp Server

Warp is where every dependent-type technique in Hale converges: phantom state machines, proof-carrying structures, and indexed monads all cooperate to make entire classes of server bugs unrepresentable.

Socket State Machine (phantom type parameter)

structure Socket (state : SocketState) where   -- phantom parameter, erased at runtime
  raw : RawSocket

-- Every function declares its pre/post state:
listenTCP  : ... -> IO (Socket .listening)
accept     : Socket .listening -> IO (Socket .connected × SockAddr)
send       : Socket .connected -> ByteArray -> IO Nat
close      : Socket state -> (state ≠ .closed := by decide) -> IO (Socket .closed)

-- All of these are compile-time errors (not runtime, not asserts):
-- accept freshSocket       -- .fresh ≠ .listening
-- send listeningSocket     -- .listening ≠ .connected
-- close closedSocket       -- cannot prove .closed ≠ .closed

Exactly-Once Response (indexed monad)

The server provides a respond callback and calls (app req respond).run. The AppM .pending .sent return type guarantees that by the time .run executes, the application has called respond exactly once:

-- In runConnection (the trust boundary):
let _received ← (app req fun resp => sendResponse sock settings req resp).run

Settings with Proof Fields (zero-cost invariants)

structure Settings where
  settingsPort : UInt16
  settingsTimeout : Nat := 30
  settingsBacklog : Nat := 128
  settingsTimeoutPos : settingsTimeout > 0 := by omega   -- erased at runtime
  settingsBacklogPos : settingsBacklog > 0 := by omega   -- erased at runtime

The proof fields are erased at compile time (zero cost, same sizeof). It is impossible to construct a Settings with a zero timeout or zero backlog – the by omega obligation cannot be discharged for 0 > 0.

InvalidRequest (Exhaustive Error Model)

inductive InvalidRequest where
  | notEnoughLines | badFirstLine | nonHttp | incompleteHeaders
  | connectionClosedByPeer | overLargeHeader | badProxyHeader
  | payloadTooLarge | requestHeaderFieldsTooLarge

Full Theorem List (11)

Transport Security (3, in Types.lean)

TheoremStatement
tcp_not_secureTCP is not encrypted
tls_is_secureTLS is always encrypted
quic_is_secureQUIC is always encrypted

Keep-Alive Semantics (2, in Run.lean)

TheoremStatement
connAction_http10_defaultHTTP/1.0 defaults to close
connAction_http11_defaultHTTP/1.1 defaults to keep-alive

HTTP Version Parsing (5, in Request.lean)

TheoremStatement
parseHttpVersion_http11Parsing “HTTP/1.1” yields http11
parseHttpVersion_http10Parsing “HTTP/1.0” yields http10
parseHttpVersion_http09Parsing “HTTP/0.9” yields http09
parseHttpVersion_http20Parsing “HTTP/2.0” yields http20
parseRequestLine_emptyEmpty string yields none

Settings Validity (1, in Settings.lean)

TheoremStatement
defaultSettings_validDefault timeout > 0 and backlog > 0

Performance

  • 156k QPS (wrk, 4 threads, 50 connections, keep-alive)
  • 30k QPS (ab, 100 connections, no keep-alive)
  • Green threads via forkIO (scheduler-managed, not OS threads)
  • RecvBuffer in C (CRLF scanning entirely in native code)

Files (17 modules)

ModulePurpose
TypesConnection, Transport, Source, InvalidRequest
SettingsServer configuration with proof fields
RequestHTTP request parsing
ResponseHTTP response rendering
RunAccept loop + keep-alive state machine
DateCached HTTP date header (AutoUpdate)
HeaderO(1) indexed header lookup (13 headers)
CounterAtomic connection counter
ReadInt / PackIntFast integer parsing/rendering
IO, HashMap, Conduit, SendFileInternal utilities
WithApplicationTest helper (ephemeral port)
InternalRe-exports for downstream (warp-tls, warp-quic)

WarpTLS – HTTPS for Warp

Lean: Hale.WarpTLS | Haskell: warp-tls

HTTPS support for Warp using OpenSSL FFI. TLS 1.2+ enforced. ALPN negotiation for HTTP/2. Uses EventDispatcher and Green monad for non-blocking I/O.

Key Types

TypeDescription
OnInsecuredenyInsecure | allowInsecure – handling for non-TLS connections
CertSettingsCertificate source (certFile with paths)

Files

  • Hale/WarpTLS/Network/Wai/Handler/WarpTLS.lean – runTLS, TLS settings
  • Hale/WarpTLS/Network/Wai/Handler/WarpTLS/Internal.lean – Internal types

WarpQUIC – HTTP/3 over QUIC

Lean: Hale.WarpQUIC | Haskell: warp-quic

WAI handler over HTTP/3/QUIC transport. TLS 1.3 mandatory (QUIC requirement).

Key Types

structure Settings where
  port     : Nat
  certFile : String
  keyFile  : String

Files

  • Hale/WarpQUIC/Network/Wai/Handler/WarpQUIC.lean – Settings, QUIC server entry point

WaiExtra – Middleware Collection

Lean: Hale.WaiExtra | Haskell: wai-extra

API Reference: Hale.WaiExtra | AddHeaders | ForceSSL | Gzip | HttpAuth | Test

36 middleware modules for request/response transformation.

Middleware Composition

         +----------------------------------------------+
         |         Middleware Composition                 |
         |                                               |
         |   m1 . m2 . m3 : Middleware                   |
         |                                               |
         |   Algebraic Laws:                             |
         |     id . m = m        (left identity)         |
         |     m . id = m        (right identity)        |
         |     (f.g).h = f.(g.h) (associativity)         |
         +----------------------------------------------+

  Request --> m1 --> m2 --> m3 --> App --> Response
              |       |       |              |
              | modify| check |              | modify
              | headers auth  |              | headers
              v       v       v              v

Available Middleware

Request Modification

MiddlewareProven PropertiesDescription
methodOverrideOverride method from _method query param
methodOverridePostOverride method from POST body
acceptOverrideOverride Accept from _accept param
realIpExtract client IP from X-Forwarded-For
rewriteURL path rewriting rules

Response Modification

MiddlewareProven PropertiesDescription
addHeadersaddHeaders [] = idAdd headers to responses
stripHeadersstripHeaders [] = idRemove headers from responses
combineHeadersMerge duplicate headers
gzipGzip compression (framework)
streamFileConvert file->stream responses

Routing and Filtering

MiddlewareProven PropertiesDescription
selectselect (fun _ => none) = idConditional middleware
routedrouted (fun _ => true) m = m, routed (fun _ => false) = idPath-based routing
vhostVirtual host routing
urlMapURL prefix routing

Security

MiddlewareProven PropertiesDescription
forceSSLSecure requests pass throughRedirect HTTP->HTTPS
forceDomainRedirect to canonical domain
httpAuthHTTP Basic Authentication
localOnlyRestrict to localhost
requestSizeLimitReject oversized bodies (413)
validateHeadersReject invalid header chars (500)

Monitoring

MiddlewareDescription
requestLoggerApache/dev format logging
requestLogger.jsonStructured JSON logging
healthCheckHealth check endpoint (200 OK)
timeoutRequest timeout (503)

Protocol

MiddlewareDescription
autoheadHEAD->GET + strip body
cleanPathNormalize URL paths (301 redirect)
approotApplication root detection
eventSourceServer-Sent Events
jsonpJSONP callback wrapping

Proven Properties (11 theorems)

All proofs are in the source files, verified at compile time (no sorry):

AddHeaders Identity (3, in AddHeaders.lean)

TheoremStatement
addHeaders_nil_builderEmpty headers on builder = identity
addHeaders_nil_fileEmpty headers on file = identity
addHeaders_nil_streamEmpty headers on stream = identity

StripHeaders Identity (3, in StripHeaders.lean)

TheoremStatement
stripHeaders_nil_builderEmpty strip list on builder = identity
stripHeaders_nil_fileEmpty strip list on file = identity
stripHeaders_nil_streamEmpty strip list on stream = identity

Select (1, in Select.lean)

TheoremStatement
select_noneAlways-none selector = identity middleware

Routed (2, in Routed.lean)

TheoremStatement
routed_trueAlways-true predicate = apply middleware
routed_falseAlways-false predicate = identity middleware

ForceSSL (1, in ForceSSL.lean)

TheoremStatement
forceSSL_secureSecure requests pass through unchanged

HealthCheck (1, in HealthCheckEndpoint.lean)

TheoremStatement
healthCheck_passthroughNon-matching paths pass through to inner app

Files (36 modules)

FilePurpose
Middleware/AddHeaders.leanAdd headers + 3 identity proofs
Middleware/StripHeaders.leanRemove headers + 3 identity proofs
Middleware/Select.leanConditional middleware + 1 proof
Middleware/Routed.leanPath-based routing + 2 proofs
Middleware/ForceSSL.leanHTTP->HTTPS redirect + 1 proof
Middleware/HealthCheckEndpoint.leanHealth check + 1 proof
Middleware/Autohead.leanHEAD method handling
Middleware/AcceptOverride.leanAccept header override
Middleware/MethodOverride.leanMethod override (query param)
Middleware/MethodOverridePost.leanMethod override (POST body)
Middleware/Vhost.leanVirtual host routing
Middleware/Timeout.leanRequest timeout
Middleware/CombineHeaders.leanHeader deduplication
Middleware/StreamFile.leanFile->stream conversion
Middleware/Rewrite.leanURL rewriting
Middleware/CleanPath.leanPath normalization
Middleware/ForceDomain.leanDomain redirect
Middleware/Local.leanLocalhost restriction
Middleware/RealIp.leanClient IP extraction
Middleware/HttpAuth.leanBasic authentication
Middleware/RequestSizeLimit.leanBody size limit
Middleware/ValidateHeaders.leanHeader validation
Middleware/RequestLogger.leanRequest logging
Middleware/RequestLogger/JSON.leanJSON request logging
Middleware/Gzip.leanGzip compression
Middleware/Approot.leanApplication root
Middleware/Jsonp.leanJSONP support
UrlMap.leanURL prefix routing
Header.leanWAI header utilities
Request.leanRequest utilities
Parse.leanMultipart/form parsing
EventSource.leanServer-Sent Events
EventSource/EventStream.leanSSE stream types
Test.leanWAI test utilities
Test/Internal.leanTest internals
Middleware/RequestSizeLimit/Internal.leanSize limit internals

WaiAppStatic – Static File Serving

Lean: Hale.WaiAppStatic | Haskell: wai-app-static

API Reference: Hale.WaiAppStatic | Types | Filesystem | Static

Serves static files from the filesystem with type-safe path validation.

Request Flow

  GET /css/style.css
         |
         v
  +---------------------+
  | toPieces(pathInfo)   |  Validate each path segment
  | "css"       -> Piece |  No dots, no slashes
  | "style.css" -> Piece |
  +--------+------------+
           |
  +--------v------------+
  | ssLookupFile         |  Filesystem stat + MIME lookup
  | -> LookupResult      |
  +--------+------------+
           |
     +-----+------+------------+------------+
     |            |            |            |
     v            v            v            v
  lrFile      lrFolder     lrNotFound   lrRedirect
  200 OK      try index    404          301
  + Cache     files
  + MIME

Dependent Type: Piece (Path Traversal Prevention)

structure Piece where
  val : String
  no_dot   : startsDot val = false        -- no dotfiles
  no_slash : containsSlash val = false     -- no path traversal

The no_dot and no_slash fields are proofs, erased at runtime. A Piece is just a String at runtime, but the type system guarantees it cannot contain a leading dot or an embedded slash.

Smart Constructor

def toPiece (t : String) : Option Piece  -- None if invalid!

Proven properties (4 theorems, in Types.lean):

TheoremStatement
empty_piece_validtoPiece "" = some _
toPiece_rejects_dottoPiece ".hidden" = none
toPiece_rejects_slashtoPiece "a/b" = none
toPiece_accepts_simpletoPiece "index.html" = some _

Security guarantee: It is impossible to construct a Piece with a leading dot or embedded slash. The staticApp function only accepts Piece values, so dotfile serving and path traversal are prevented by the type system, not by runtime checks.

Other Types

LookupResult (File Lookup Outcome)

inductive LookupResult where
  | lrFile (file : File)       -- found a file
  | lrFolder                   -- found a directory
  | lrNotFound                 -- path does not exist
  | lrRedirect (pieces : Pieces)  -- client should be redirected

MaxAge (Cache Control)

inductive MaxAge where
  | noMaxAge                   -- no cache-control header
  | maxAgeSeconds (seconds : Nat)  -- max-age=N
  | maxAgeForever              -- ~1 year (31536000s)
  | noStore                    -- cache-control: no-store
  | noCache                    -- cache-control: no-cache

StaticSettings (Server Configuration)

structure StaticSettings where
  ssLookupFile    : Pieces -> IO LookupResult
  ssGetMimeType   : Piece -> String
  ssMaxAge        : MaxAge := .maxAgeSeconds 3600
  ssRedirectToIndex : Bool := true
  ssIndices       : List Piece := [unsafeToPiece "index.html"]
  ssListing       : Option (Pieces -> IO Response) := none

Files

  • WaiAppStatic/Types.lean – Piece, LookupResult, StaticSettings + 4 proofs
  • WaiAppStatic/Storage/Filesystem.lean – Filesystem backend
  • Network/Wai/Application/Static.lean – WAI Application wrapper

WaiHttp2Extra – HTTP/2 Server Push

Lean: Hale.WaiHttp2Extra | Haskell: wai-http2-extra

HTTP/2 server push via referer prediction. Learns resource associations from Referer headers and proactively pushes on subsequent requests. LRU eviction for bounded memory.

Key Types

TypeDescription
PushSettingsMiddleware configuration

API

FunctionDescription
pushOnRefererCreate push middleware

Modules

  • Types – PushSettings, configuration
  • LRU – LRU eviction cache
  • Manager – Thread-safe push table
  • ParseURL – URL parsing for same-origin checks
  • Referer – Main middleware entry point

Files

  • Hale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer.lean – Main middleware
  • Hale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/Types.lean – Configuration
  • Hale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/LRU.lean – LRU cache
  • Hale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/Manager.lean – Push manager
  • Hale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/ParseURL.lean – URL parsing

WaiWebSockets – WebSocket WAI Handler

Lean: Hale.WaiWebSockets | Haskell: wai-websockets

Upgrade WAI requests to WebSocket connections.

API

FunctionDescription
isWebSocketsReqCheck if request is a WebSocket upgrade
websocketsAppUpgrade WAI request to WebSocket connection

Files

  • Hale/WaiWebSockets/Network/Wai/Handler/WebSockets.lean – WebSocket upgrade handler

WebSockets – RFC 6455 Protocol

Lean: Hale.WebSockets | Haskell: websockets

API Reference: Hale.WebSockets | Types | Frame | Handshake | Connection | WAI Bridge

Native WebSocket framing implementation with typed state machine.

WebSocket Upgrade Flow

  HTTP GET /chat
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZQ==
  Sec-WebSocket-Version: 13
         |
         v
  +----------------------------+
  | isValidHandshake(headers)? |
  |   Check Upgrade header     |
  +------+-------+-------------+
         |yes    |no
         v       v
  +----------+  +--------------+
  | Handshake|  | Normal HTTP  |
  | SHA-1+B64|  | Application  |
  +----+-----+  +--------------+
       |
       v HTTP 101 Switching Protocols
  +----------------------------+
  |  PendingConnection         |
  |  state = .pending          |
  +------------+---------------+
               | acceptIO
               v
  +----------------------------+
  |  Connection                |
  |  state = .open_            |
  |                            |
  |  sendText / sendBinary     |
  |  receiveData / receiveText |
  |  sendPing (auto-pong)      |
  +------------+---------------+
               | sendClose
               v
  +----------------------------+
  |  state = .closing          |
  |  (awaiting close frame)    |
  +------------+---------------+
               | receive close
               v
  +----------------------------+
  |  state = .closed           |
  +----------------------------+

Frame Format (RFC 6455 section 5.2)

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |   Extended payload length     |
 |I|S|S|S|  (4)  |A|     (7)     |          (16/64)              |
 |N|V|V|V|       |S|             |  (if payload len==126/127)    |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+-------------------------------+
 | Masking-key (if MASK=1)       |         Payload Data          |
 +-------------------------------+-------------------------------+

Dependent Types

Opcode (Bounded to 4 bits)

inductive Opcode where
  | continuation | text | binary | close | ping | pong
  | reserved (val : Fin 16)   -- Fin 16 bounds to [0, 15]

The reserved constructor uses Fin 16 to statically guarantee all opcode values fit in 4 bits, matching the wire format.

Connection State Machine

inductive ConnectionState where
  | pending   -- handshake not completed
  | open_     -- data transfer active
  | closing   -- close frame sent
  | closed    -- terminated

CloseCode (Typed Status Codes)

structure CloseCode where
  code : UInt16

-- Named constants:
CloseCode.normal         -- 1000
CloseCode.goingAway      -- 1001
CloseCode.protocolError   -- 1002
CloseCode.unsupportedData -- 1003

ServerApp (Application Type)

abbrev ServerApp := PendingConnection -> IO Unit

Proven Properties (6 theorems)

All opcode encoding/decoding roundtrips, in Types.lean:

TheoremStatement
opcode_roundtrip_textfromUInt8 (toUInt8 .text) = .text
opcode_roundtrip_binaryfromUInt8 (toUInt8 .binary) = .binary
opcode_roundtrip_closefromUInt8 (toUInt8 .close) = .close
opcode_roundtrip_pingfromUInt8 (toUInt8 .ping) = .ping
opcode_roundtrip_pongfromUInt8 (toUInt8 .pong) = .pong
opcode_roundtrip_continuationfromUInt8 (toUInt8 .continuation) = .continuation

Implementation Notes

  • XOR masking: applyMask applies the 4-byte masking key via XOR. XOR is its own inverse, so the same function masks and unmasks.
  • Auto-pong: When a ping control frame is received during receiveData, the connection automatically replies with a pong frame (RFC 6455 section 5.5.3).
  • SHA-1 placeholder: The handshake module contains a placeholder SHA-1 implementation. TODO: replace with full FIPS 180-4 implementation for production.

Files

  • Types.lean – Core types (Opcode, ConnectionState, Connection, PendingConnection) + 6 roundtrip proofs
  • Frame.lean – Frame encode/decode + XOR masking
  • Handshake.lean – WebSocket upgrade (SHA-1 + Base64, magic GUID)
  • Connection.lean – High-level send/receive API with auto-pong

CaseInsensitive – Case-Insensitive Comparison

Lean: Hale.CaseInsensitive | Haskell: case-insensitive

Wrapper type CI α that compares values case-insensitively while preserving the original. Stores both original and pre-computed foldedCase. Equality, ordering, and hashing use only foldedCase.

Key Types

TypeDescription
FoldCase αTypeclass with foldCase : α → α
CI αCase-insensitive wrapper

Files

  • Hale/CaseInsensitive/Data/CaseInsensitive.lean – FoldCase, CI

Vault – Type-Safe Heterogeneous Map

Lean: Hale.Vault | Haskell: vault

Type-safe heterogeneous container keyed by Key α tokens. Backed by Std.HashMap mapping unique Nat IDs to type-erased values.

Key Types

TypeDescription
Key αTyped key (globally unique)
VaultHeterogeneous map

Axiom-Dependent Properties

  • Type safety of lookup depends on axiom that unsafeCast is safe when the original value was of type α

Files

  • Hale/Vault/Data/Vault.lean – Key, Vault, insert, lookup, delete

AutoUpdate – Periodic Cached Values

Lean: Hale.AutoUpdate | Haskell: auto-update

Periodically updated cached values. Runs an IO action in a background task and caches the result for consumers. Uses IO.asTask with .dedicated priority and Std.CancellationToken for lifecycle management.

Key Types

structure UpdateSettings (α : Type) where
  interval : Nat      -- update interval in microseconds
  action   : IO α     -- action to run periodically

Files

  • Hale/AutoUpdate/Control/AutoUpdate.lean – UpdateSettings, mkAutoUpdate

TimeManager – Connection Timeout Management

Lean: Hale.TimeManager | Haskell: time-manager

Periodically sweeps registered handles and fires timeout callbacks for expired connections. Uses IO.asTask with .dedicated priority.

Guarantees

  • tickle resets timeout – O(1)
  • cancel prevents future firing
  • Thread-safe via IO.Ref atomicity

Files

  • Hale/TimeManager/System/TimeManager.lean – TimeManager, Handle, tickle, cancel

Cookie – HTTP Cookie Parsing

Lean: Hale.Cookie | Haskell: cookie

Parse Cookie and Set-Cookie headers per RFC 6265.

Key Types

TypeDescription
CookiePairString × String (name, value)

API

FunctionSignature
parseCookiesString → List CookiePair

Files

  • Hale/Cookie/Web/Cookie.lean – CookiePair, parseCookies

MimeTypes – MIME Type Lookup

Lean: Hale.MimeTypes | Haskell: mime-types

Map file extensions to MIME types. Default fallback: application/octet-stream. Handles multi-part extensions (e.g., tar.gz).

Key Types

TypeDescription
MimeTypeString
MimeMapList (Extension × MimeType)

API

FunctionDescription
defaultMimeLookupLook up MIME type by filename
defaultMimeMapBuilt-in extension → MIME mapping

Files

  • Hale/MimeTypes/Network/Mime.lean – MimeType, MimeMap, lookup

Base64 – RFC 4648 Codec

Lean: Hale.Base64 | Haskell: base64-bytestring

Base64 encoding and decoding per RFC 4648.

Guarantees

  • decode (encode bs) = some bs (roundtrip)
  • Output of encode contains only [A-Za-z0-9+/=]

API

FunctionSignature
encodeByteArray → String
decodeString → Option ByteArray

Files

  • Hale/Base64/Data/ByteString/Base64.lean – encode, decode

FastLogger – Buffered Thread-Safe Logging

Lean: Hale.FastLogger | Haskell: fast-logger

High-performance buffered logger with mutex synchronization and periodic flushing via AutoUpdate. Supports stdout, stderr, file, and callback destinations.

Key Types

TypeDescription
LogTypestdout | stderr | file | callback
LogStrString (log message)

Files

  • Hale/FastLogger/System/Log/FastLogger.lean – LogType, LogStr, logger lifecycle

WaiLogger – WAI Request Logging

Lean: Hale.WaiLogger | Haskell: wai-logger

Formats HTTP request/response data in Apache Combined Log Format: host - - [date] "method path version" status size.

API

FunctionSignature
apacheFormatRequest → Status → Option Nat → String

Files

  • Hale/WaiLogger/Network/Wai/Logger.lean – apacheFormat

UnixCompat – POSIX Compatibility

Lean: Hale.UnixCompat | Haskell: unix-compat

Subset of POSIX operations needed by Warp: file descriptor operations and close-on-exec.

Key Types

TypeDescription
FdFile descriptor (UInt32)
FileStatusFile size and type

API

FunctionDescription
closeFdClose a file descriptor

Files

  • Hale/UnixCompat/System/Posix/Compat.lean – Fd, FileStatus, closeFd

AnsiTerminal – Terminal ANSI Codes

Lean: Hale.AnsiTerminal | Haskell: ansi-terminal

ANSI terminal escape codes for colored output.

Key Types

TypeDescription
Colorblack | red | green | yellow | blue | magenta | cyan | white
Intensitybold | normal

API

FunctionDescription
resetReset all attributes
setFgSet foreground color
setBgSet background color

Files

  • Hale/AnsiTerminal/System/Console/ANSI.lean – Color, Intensity, escape code helpers

PSQueues – Priority Search Queues

Lean: Hale.PSQueues | Haskell: psqueues

Priority search queues (IntPSQ) for efficient priority-indexed operations.

Files

  • Hale/PSQueues/Data/IntPSQ.lean – IntPSQ implementation

DataFrame – Tabular Data

Lean: Hale.DataFrame | Haskell: dataframe (adapted)

Tabular data structure with typed columns, supporting subset, sort, join, aggregation, statistics, and CSV I/O.

Modules

ModuleDescription
Internal.TypesCore DataFrame/Column types
Internal.ColumnColumn operations
Operations.SubsetRow/column selection
Operations.SortSorting
Operations.AggregationGroup-by and aggregation
Operations.JoinInner/outer joins
Operations.StatisticsMean, std, quantiles
Operations.TransformMap, filter, apply
IO.CSVCSV read/write
DisplayPretty-printing

Files

  • Hale/DataFrame/DataFrame.lean – Re-exports
  • Hale/DataFrame/DataFrame/Internal/Types.lean – Core types
  • Hale/DataFrame/DataFrame/Internal/Column.lean – Column operations
  • Hale/DataFrame/DataFrame/Operations/*.lean – Operations
  • Hale/DataFrame/DataFrame/IO/CSV.lean – CSV I/O
  • Hale/DataFrame/DataFrame/Display.lean – Display

API Reference

The API reference is auto-generated by doc-gen4 from Lean docstrings. Every public definition, structure, class, and theorem is documented.

Browse the full API reference

Core Infrastructure

Web Application Interface

Protocol Implementations

Proven Properties Catalog

All theorems link to their source modules in the API Reference.

Total: 257 compile-time verified theorems across 52 files

All theorems are checked by the Lean 4 kernel at compile time. The codebase is completely sorry-free — every proof obligation is fully discharged.

Algebraic Laws

Monoid/Semigroup Associativity (8, in Base/Data/Newtype.lean)

TheoremModule
Dual.append_assocBase/Data/Newtype
Endo.append_assocBase/Data/Newtype
First.append_assocBase/Data/Newtype
Last.append_assocBase/Data/Newtype
Sum.append_assocBase/Data/Newtype
Product.append_assocBase/Data/Newtype
All.append_assocBase/Data/Newtype
Any.append_assocBase/Data/Newtype

Functor Laws (14)

TheoremModule
Identity.map_idBase/Data/Functor/Identity
Identity.map_compBase/Data/Functor/Identity
Const.map_idBase/Data/Functor/Const
Const.map_compBase/Data/Functor/Const
Const.map_valBase/Data/Functor/Const
Proxy.map_idBase/Data/Proxy
Proxy.map_compBase/Data/Proxy
Either.map_idBase/Data/Either
Either.map_compBase/Data/Either
Compose.map_idBase/Data/Functor/Compose
Compose.map_compBase/Data/Functor/Compose
Product.map_idBase/Data/Functor/Product
Product.map_compBase/Data/Functor/Product
Sum.map_idBase/Data/Functor/Sum
Sum.map_compBase/Data/Functor/Sum

Monad Laws (9)

TheoremModule
Identity.pure_bindBase/Data/Functor/Identity
Identity.bind_pureBase/Data/Functor/Identity
Identity.bind_assocBase/Data/Functor/Identity
Proxy.pure_bindBase/Data/Proxy
Proxy.bind_pureBase/Data/Proxy
Proxy.bind_assocBase/Data/Proxy
Either.pure_bindBase/Data/Either
Either.bind_pureBase/Data/Either
Either.bind_assocBase/Data/Either

Monad Combinators (1)

TheoremModule
join_pureBase/Control/Monad

Data Structure Properties

Either (3)

TheoremModule
swap_swapBase/Data/Either
isLeft_not_isRightBase/Data/Either
partitionEithers_lengthBase/Data/Either

Maybe/Option (8)

TheoremModule
maybe_noneBase/Data/Maybe
maybe_someBase/Data/Maybe
fromMaybe_noneBase/Data/Maybe
fromMaybe_someBase/Data/Maybe
catMaybes_nilBase/Data/Maybe
mapMaybe_nilBase/Data/Maybe
maybeToList_listToMaybeBase/Data/Maybe
catMaybes_eq_mapMaybe_idBase/Data/Maybe

Tuple (7)

TheoremModule
swap_swapBase/Data/Tuple
curry_uncurryBase/Data/Tuple
uncurry_curryBase/Data/Tuple
bimap_idBase/Data/Tuple
bimap_compBase/Data/Tuple
mapFst_eq_bimapBase/Data/Tuple
mapSnd_eq_bimapBase/Data/Tuple

List (4)

TheoremModule
tails_lengthBase/Data/List
inits_lengthBase/Data/List
tails_nilBase/Data/List
inits_nilBase/Data/List

NonEmpty (4)

TheoremModule
toList_ne_nilBase/Data/List/NonEmpty
reverse_lengthBase/Data/List/NonEmpty
toList_lengthBase/Data/List/NonEmpty
map_lengthBase/Data/List/NonEmpty

Ord/Down (2)

TheoremModule
get_mkBase/Data/Ord
compare_reverseBase/Data/Ord

Bool (5)

TheoremModule
bool_falseBase/Data/Bool
bool_trueBase/Data/Bool
guard'_trueBase/Data/Bool
guard'_falseBase/Data/Bool
bool_iteBase/Data/Bool

Void (1)

TheoremModule
eq_absurdBase/Data/Void

Function (5)

TheoremModule
on_applyBase/Data/Function
applyTo_applyBase/Data/Function
const_applyBase/Data/Function
flip_flipBase/Data/Function
flip_applyBase/Data/Function

Foldable (2)

TheoremModule
foldr_nilBase/Data/Foldable
foldl_nilBase/Data/Foldable

String (1)

TheoremModule
unwords_nilBase/Data/String

Char (3)

TheoremModule
isAlphaNum_iffBase/Data/Char
isSpace_eq_isWhitespaceBase/Data/Char
ord_eq_toNatBase/Data/Char

Ix (1)

TheoremModule
Ix.inRange_iff_index_isSome_natBase/Data/Ix

ExitCode (2)

TheoremModule
success_toUInt32Base/System/Exit
isSuccess_iffBase/System/Exit

Numeric Type Properties

Complex (2)

TheoremModule
conjugate_conjugateBase/Data/Complex
add_comm'Base/Data/Complex

Fixed-Point (5)

TheoremModule
scale_posBase/Data/Fixed
add_exactBase/Data/Fixed
sub_exactBase/Data/Fixed
neg_negBase/Data/Fixed
fromInt_zeroBase/Data/Fixed

Time (2)

TheoremModule
fromSeconds_toSecondsTime/Data/Time/Clock
diffUTCTime_selfTime/Data/Time/Clock

Protocol Correctness

HTTP Method Properties (21, in HttpTypes/Network/HTTP/Types/Method.lean)

TheoremWhat it proves
parseMethod_GETParsing “GET” yields .standard .GET
parseMethod_POSTParsing “POST” yields .standard .POST
parseMethod_HEADParsing “HEAD” yields .standard .HEAD
parseMethod_PUTParsing “PUT” yields .standard .PUT
parseMethod_DELETEParsing “DELETE” yields .standard .DELETE
parseMethod_TRACEParsing “TRACE” yields .standard .TRACE
parseMethod_CONNECTParsing “CONNECT” yields .standard .CONNECT
parseMethod_OPTIONSParsing “OPTIONS” yields .standard .OPTIONS
parseMethod_PATCHParsing “PATCH” yields .standard .PATCH
parseMethod_customNon-standard strings yield .custom
Method.get_is_safeGET is a safe method
Method.head_is_safeHEAD is a safe method
Method.options_is_safeOPTIONS is a safe method
Method.trace_is_safeTRACE is a safe method
Method.post_not_safePOST is not safe
Method.patch_not_safePATCH is not safe
Method.put_is_idempotentPUT is idempotent
Method.delete_is_idempotentDELETE is idempotent
Method.post_not_idempotentPOST is not idempotent
Method.patch_not_idempotentPATCH is not idempotent
Method.safe_implies_idempotentAll safe methods are idempotent

HTTP Status Properties (13, in HttpTypes/Network/HTTP/Types/Status.lean)

TheoremWhat it proves
status200_valid200 is in valid range [100, 599]
status404_valid404 is in valid range
status500_valid500 is in valid range
status100_valid100 is in valid range
status301_valid301 is in valid range
status100_no_body100 must not have body
status101_no_body101 must not have body
status204_no_body204 must not have body
status304_no_body304 must not have body
status200_may_have_body200 may have body
status201_may_have_body201 may have body
status404_may_have_body404 may have body
status500_may_have_body500 may have body

HTTP Version Properties (4, in HttpTypes/Network/HTTP/Types/Version.lean)

TheoremWhat it proves
http09_validHTTP/0.9 has major=0, minor=9
http10_validHTTP/1.0 has major=1, minor=0
http11_validHTTP/1.1 has major=1, minor=1
http20_validHTTP/2.0 has major=2, minor=0

Transport Security (3, in Warp/Types.lean)

TheoremWhat it proves
tcp_not_secureTCP is not encrypted
tls_is_secureTLS is always encrypted
quic_is_secureQUIC is always encrypted

Keep-Alive Semantics (2, in Warp/Run.lean)

TheoremWhat it proves
connAction_http10_defaultHTTP/1.0 defaults to close
connAction_http11_defaultHTTP/1.1 defaults to keep-alive

HTTP Version Parsing (5, in Warp/Request.lean)

TheoremWhat it proves
parseHttpVersion_http11“HTTP/1.1” -> http11
parseHttpVersion_http10“HTTP/1.0” -> http10
parseHttpVersion_http09“HTTP/0.9” -> http09
parseHttpVersion_http20“HTTP/2.0” -> http20
parseRequestLine_empty“” -> none

Warp Settings (1, in Warp/Settings.lean)

TheoremWhat it proves
defaultSettings_validDefault timeout > 0 and backlog > 0

Encoding/Decoding Roundtrips

HTTP/2 Frame Types (10, in Http2/Network/HTTP2/Frame/Types.lean)

TheoremFrame Type
fromUInt8_toUInt8_dataDATA
fromUInt8_toUInt8_headersHEADERS
fromUInt8_toUInt8_priorityPRIORITY
fromUInt8_toUInt8_rstStreamRST_STREAM
fromUInt8_toUInt8_settingsSETTINGS
fromUInt8_toUInt8_pushPromisePUSH_PROMISE
fromUInt8_toUInt8_pingPING
fromUInt8_toUInt8_goawayGOAWAY
fromUInt8_toUInt8_windowUpdateWINDOW_UPDATE
fromUInt8_toUInt8_continuationCONTINUATION

HTTP/3 Frame Types (7, in Http3/Network/HTTP3/Frame.lean)

TheoremFrame Type
FrameType.roundtrip_dataDATA
FrameType.roundtrip_headersHEADERS
FrameType.roundtrip_cancelPushCANCEL_PUSH
FrameType.roundtrip_settingsSETTINGS
FrameType.roundtrip_pushPromisePUSH_PROMISE
FrameType.roundtrip_goawayGOAWAY
FrameType.roundtrip_maxPushIdMAX_PUSH_ID

HTTP/3 Error Codes (17, in Http3/Network/HTTP3/Error.lean)

TheoremError Code
H3Error.roundtrip_noErrorH3_NO_ERROR
H3Error.roundtrip_generalProtocolErrorH3_GENERAL_PROTOCOL_ERROR
H3Error.roundtrip_internalErrorH3_INTERNAL_ERROR
H3Error.roundtrip_streamCreationErrorH3_STREAM_CREATION_ERROR
H3Error.roundtrip_closedCriticalStreamH3_CLOSED_CRITICAL_STREAM
H3Error.roundtrip_frameUnexpectedH3_FRAME_UNEXPECTED
H3Error.roundtrip_frameErrorH3_FRAME_ERROR
H3Error.roundtrip_excessiveLoadH3_EXCESSIVE_LOAD
H3Error.roundtrip_idErrorH3_ID_ERROR
H3Error.roundtrip_settingsErrorH3_SETTINGS_ERROR
H3Error.roundtrip_missingSettingsH3_MISSING_SETTINGS
H3Error.roundtrip_requestRejectedH3_REQUEST_REJECTED
H3Error.roundtrip_requestCancelledH3_REQUEST_CANCELLED
H3Error.roundtrip_requestIncompleteH3_REQUEST_INCOMPLETE
H3Error.roundtrip_messageErrorH3_MESSAGE_ERROR
H3Error.roundtrip_connectErrorH3_CONNECT_ERROR
H3Error.roundtrip_versionFallbackH3_VERSION_FALLBACK

WebSocket Opcodes (6, in WebSockets/Types.lean)

TheoremOpcode
opcode_roundtrip_textTEXT
opcode_roundtrip_binaryBINARY
opcode_roundtrip_closeCLOSE
opcode_roundtrip_pingPING
opcode_roundtrip_pongPONG
opcode_roundtrip_continuationCONTINUATION

ByteString Properties

ByteString Invariants (3, in ByteString/Internal.lean)

TheoremWhat it proves
take_validtake result has valid offset/length
drop_validdrop result has valid offset/length
null_iff_length_zeronull bs <-> length bs = 0

Builder Monoid (3, in ByteString/Builder.lean)

TheoremWhat it proves
empty_appendempty ++ b = b (left identity)
append_emptyb ++ empty = b (right identity)
append_assoc(a ++ b) ++ c = a ++ (b ++ c) (associativity)

Short ByteString (1, in ByteString/Short.lean)

TheoremWhat it proves
length_toShorttoShort preserves length

Word8 Properties (4, in Word8/Data/Word8.lean)

TheoremWhat it proves
toLower_idempotenttoLower . toLower = toLower
toUpper_idempotenttoUpper . toUpper = toUpper
isUpper_toLowerUpper -> toLower -> isLower
isLower_toUpperLower -> toUpper -> isUpper

CaseInsensitive (1, in CaseInsensitive/Data/CaseInsensitive.lean)

TheoremWhat it proves
ci_eq_iffCI equality is by foldedCase

Socket State (11, in Network/Socket/Types.lean)

TheoremWhat it proves
SocketState.fresh_ne_boundfresh /= bound
SocketState.fresh_ne_listeningfresh /= listening
SocketState.fresh_ne_connectedfresh /= connected
SocketState.fresh_ne_closedfresh /= closed
SocketState.bound_ne_listeningbound /= listening
SocketState.bound_ne_connectedbound /= connected
SocketState.bound_ne_closedbound /= closed
SocketState.listening_ne_connectedlistening /= connected
SocketState.listening_ne_closedlistening /= closed
SocketState.connected_ne_closedconnected /= closed
SocketState.beq_refls == s = true

Response Lifecycle (1, in WAI/Network/Wai/Internal.lean)

TheoremWhat it proves
ResponseState.pending_ne_sentpending /= sent

Path Safety (4, in WaiAppStatic/Types.lean)

TheoremWhat it proves
empty_piece_validEmpty string is a valid piece
toPiece_rejects_dotDotfiles rejected at construction
toPiece_rejects_slashPath traversal rejected at construction
toPiece_accepts_simpleNormal filenames accepted

WAI Response Laws (11, in WAI/Internal.lean)

TheoremWhat it proves
status_responseBuilderStatus accessor on builder
status_responseFileStatus accessor on file
status_responseStreamStatus accessor on stream
headers_responseBuilderHeaders accessor on builder
headers_responseFileHeaders accessor on file
mapResponseHeaders_id_responseBuildermapHeaders id on builder = id
mapResponseHeaders_id_responseFilemapHeaders id on file = id
mapResponseHeaders_id_responseStreammapHeaders id on stream = id
mapResponseStatus_id_responseBuildermapStatus id on builder = id
mapResponseStatus_id_responseFilemapStatus id on file = id
mapResponseStatus_id_responseStreammapStatus id on stream = id

WAI Middleware Algebra (5, in WAI/Wai.lean)

TheoremWhat it proves
idMiddleware_comp_leftid . m = m
idMiddleware_comp_rightm . id = m
modifyRequest_idmodifyRequest id = id
modifyResponse_idmodifyResponse id = id
ifRequest_falseifRequest (always false) m = id

Middleware Properties (11)

AddHeaders (3, in WaiExtra/AddHeaders.lean)

TheoremWhat it proves
addHeaders_nil_builderEmpty headers on builder = identity
addHeaders_nil_fileEmpty headers on file = identity
addHeaders_nil_streamEmpty headers on stream = identity

StripHeaders (3, in WaiExtra/StripHeaders.lean)

TheoremWhat it proves
stripHeaders_nil_builderEmpty strip list on builder = identity
stripHeaders_nil_fileEmpty strip list on file = identity
stripHeaders_nil_streamEmpty strip list on stream = identity

Select (1, in WaiExtra/Select.lean)

TheoremWhat it proves
select_noneAlways-none = identity

Routed (2, in WaiExtra/Routed.lean)

TheoremWhat it proves
routed_trueAlways-true = apply middleware
routed_falseAlways-false = identity

ForceSSL (1, in WaiExtra/ForceSSL.lean)

TheoremWhat it proves
forceSSL_secureSecure requests pass through

HealthCheck (1, in WaiExtra/HealthCheckEndpoint.lean)

TheoremWhat it proves
healthCheck_passthroughNon-matching paths pass through

ResourceT (1, in ResourceT/Resource.lean)

TheoremWhat it proves
releaseKey_eqa = b <-> a.id = b.id

Involution/Self-Inverse Properties (Summary)

TheoremModule
swap_swap (Tuple)Base/Data/Tuple
swap_swap (Either)Base/Data/Either
conjugate_conjugateBase/Data/Complex
flip_flipBase/Data/Function
curry_uncurryBase/Data/Tuple
neg_neg (Fixed)Base/Data/Fixed
toLower_idempotentWord8/Data/Word8
toUpper_idempotentWord8/Data/Word8

Proof-Carrying Structures (Invariants by Construction)

These are not standalone theorems but proof fields embedded in structures, enforced at construction time and erased at runtime:

StructureProof FieldInvariant
Ratioden_posDenominator > 0
RatiocoprimeNum and den are coprime
Pieceno_dotNo leading dot
Pieceno_slashNo embedded slash
SettingssettingsTimeoutPosTimeout > 0
SettingssettingsBacklogPosBacklog > 0
Socketstate (phantom)Socket state machine

Roadmap

Completed

  • Base library (40+ modules)
  • ByteString (strict, lazy, builder, short)
  • Networking (POSIX sockets, TLS, QUIC)
  • HTTP stack (HTTP/1.x, HTTP/2, HTTP/3)
  • WAI ecosystem (Warp, WarpTLS, WarpQUIC, middleware, static files, WebSockets)
  • glibc wrappers (sockets, sendfile, POSIX compat)

Not yet ported

Essential libraries:

  • containers (Data.Map, Data.Set, Data.IntMap, Data.Sequence)
  • text (Data.Text)
  • transformers (Control.Monad.Trans)
  • vector (Data.Vector)
  • binary (Data.Binary)
  • directory (System.Directory)
  • filepath (System.FilePath)
  • unordered-containers (Data.HashMap, Data.HashSet)

Web development:

  • aeson (JSON)
  • servant (type-level web API)

Other:

  • lens
  • attoparsec
  • criterion
  • deepseq
  • async

Zero-Cost POSIX Compliance: Encoding the Socket State Machine in Lean 4’s Type System

The best runtime check is the one that never runs.

The problem

The POSIX socket API is a state machine. A socket must be created, then bound, then set to listen, before it can accept connections. Calling operations in the wrong order — send on an unconnected socket, accept before listen, close twice — returns an error code that nothing forces you to check, and double-close can silently destroy another thread’s file descriptor.

Every production socket library deals with this in one of three ways:

  1. Runtime checks — assert the state at every call, throw on violation (Python, Java, Go).
  2. Documentation — trust the programmer to read the man page (C, Rust).
  3. Ignore it — let the OS return EBADF and hope someone checks the return code.

All three push the bug to runtime. Lean 4 offers a fourth option: make the bug unrepresentable at the type level, then erase the proof at compile time so the generated code is identical to raw C.

The state machine

Socket state machine: Fresh → Bound → Listening → Connected, with close from any state to Closed (proof: state ≠ .closed)

Five states, seven transitions, and one proof obligation. That is the entire POSIX socket protocol. Let us encode it.

Step 1: States as an inductive type

inductive SocketState where
  | fresh      -- socket() returned, nothing else happened
  | bound      -- bind() succeeded
  | listening  -- listen() succeeded
  | connected  -- connect() or accept() produced this socket
  | closed     -- close() succeeded — terminal state
deriving DecidableEq

DecidableEq gives us by decide for free — the compiler can prove any two concrete states are distinct without any user effort.

Step 2: The socket carries its state as a phantom parameter

structure Socket (state : SocketState) where
  protected mk ::
  raw : RawSocket    -- opaque FFI handle (lean_alloc_external)

The state parameter exists only at the type level. It is erased at runtime: a Socket .fresh and a Socket .connected have the exact same memory layout (a single pointer to the OS file descriptor). Zero overhead.

The constructor is protected to prevent casual state fabrication.

Step 3: Each function declares its pre- and post-state

-- Creation: produces .fresh
def socket (fam : Family) (typ : SocketType) : IO (Socket .fresh)

-- Binding: requires .fresh, produces .bound
def bind (s : Socket .fresh) (addr : SockAddr) : IO (Socket .bound)

-- Listening: requires .bound, produces .listening
def listen (s : Socket .bound) (backlog : Nat) : IO (Socket .listening)

-- Accepting: requires .listening, produces .connected
def accept (s : Socket .listening) : IO (Socket .connected × SockAddr)

-- Connecting: requires .fresh, produces .connected
def connect (s : Socket .fresh) (addr : SockAddr) : IO (Socket .connected)

-- Sending/receiving: requires .connected
def send (s : Socket .connected) (data : ByteArray) : IO Nat
def recv (s : Socket .connected) (maxlen : Nat)     : IO ByteArray

The Lean 4 kernel threads these constraints through the program. If you write send freshSocket data, the kernel sees Socket .fresh where it expects Socket .connected, and reports a type error. No runtime check. No assertion. No exception. No branch in the generated code.

Step 4: Double-close prevention via proof obligation

def close (s : Socket state)
          (_h : state ≠ .closed := by decide)
          : IO (Socket .closed)

This is where dependent types shine brightest. The second parameter is a proof that the socket is not already closed. Let us trace what happens for each concrete state:

CallProof obligationby decideResult
close (s : Socket .fresh).fresh ≠ .closedtrivially truecompiles
close (s : Socket .bound).bound ≠ .closedtrivially truecompiles
close (s : Socket .listening).listening ≠ .closedtrivially truecompiles
close (s : Socket .connected).connected ≠ .closedtrivially truecompiles
close (s : Socket .closed).closed ≠ .closedimpossibletype error

For the first four, the default tactic by decide discharges the proof automatically — the caller writes nothing. For the fifth, the proposition .closed ≠ .closed is logically false: no proof can exist, so the program is rejected at compile time.

The proof is erased during compilation. The generated C code is:

lean_object* close(lean_object* socket) {
    close_fd(lean_get_external_data(socket));
    return lean_io_result_mk_ok(lean_box(0));
}

No branch. No flag. No state field. The proof did its job during type-checking and vanished.

Step 5: Distinctness theorems

We also prove that all five states are pairwise distinct:

theorem SocketState.fresh_ne_bound     : .fresh ≠ .bound     := by decide
theorem SocketState.fresh_ne_listening : .fresh ≠ .listening := by decide
theorem SocketState.fresh_ne_connected : .fresh ≠ .connected := by decide
theorem SocketState.fresh_ne_closed    : .fresh ≠ .closed    := by decide
theorem SocketState.bound_ne_listening : .bound ≠ .listening := by decide
-- ... (11 theorems total)

These are trivially proved by decide (the kernel evaluates the BEq instance). They exist so that downstream code can use them as lemmas without re-proving distinctness.

What the compiler actually rejects

-- ❌ send on a fresh socket
let s ← socket .inet .stream
send s "hello".toUTF8
-- Error: type mismatch — expected Socket .connected, got Socket .fresh

-- ❌ accept before listen
let s ← socket .inet .stream
let s ← bind s addr
accept s
-- Error: type mismatch — expected Socket .listening, got Socket .bound

-- ❌ double close
let s ← socket .inet .stream
let s ← close s
close s
-- Error: state ≠ .closed — proposition .closed ≠ .closed is false

-- ✅ correct sequence
let s ← socket .inet .stream
let s ← bind s ⟨"0.0.0.0", 8080⟩
let s ← listen s 128
let (conn, addr) ← accept s
let _ ← send conn "HTTP/1.1 200 OK\r\n\r\n".toUTF8
let _ ← close conn
let _ ← close s

Try it yourself

Open this example in the Lean 4 Playground — try uncommenting the failing examples to see the type errors live.

Uncomment bad_double_close or bad_send_fresh and the kernel rejects the program immediately — the error messages tell you exactly which state transition is invalid.

What this does not capture

This state machine models the programmatic protocol — what operations you have called — not the OS-level state of the socket. The real world is messier:

  • Non-blocking connect returns EINPROGRESS while the TCP handshake is in flight. The socket is neither .fresh nor .connected — it is connecting. A real non-blocking API would need a .connecting state and a resolution step (pollConnect).
  • Peer disconnect can happen at any time. A Socket .connected may be broken underneath; you only discover this when send/recv returns an error. The type guarantees the call is legal to attempt, not that it will succeed — that is what IO encodes.
  • Half-close via shutdown(SHUT_WR) leaves a socket readable but not writable. The five-state model has no way to express this.

In short, the type-level state machine is sound for blocking sockets on the happy path. For a production non-blocking server, richer states and an IO-based resolution protocol would be needed — a topic for a future post.

The punchline

ApproachLines of state-checking codeRuntime costCatches at
C (man page)00never (silent error)
Python (runtime)~50branch per callruntime
Rust (typestate)~300compile-time
Lean 4 (dependent types)00compile-time

Lean 4 is unique: the proof obligation state ≠ .closed is a real logical proposition that the kernel verifies. It is not a lint, not a static analysis heuristic, not a convention. It is a mathematical proof of protocol compliance, checked by the same kernel that verifies Mathlib’s theorems — and then thrown away so the generated code runs at the speed of C.


This post is part of the Hale documentation — a port of Haskell’s web ecosystem to Lean 4 with maximalist typing.

The HTTP Standard, Enforced by Types

RFC 9110 has 200 pages. We turned the important ones into theorems.

What if the spec could check itself?

HTTP is specified across several RFCs: 9110 (semantics), 9113 (HTTP/2), 9114 (HTTP/3), 9000 (QUIC), 6455 (WebSocket). Each defines methods, status codes, frame types, error codes, state machines, and properties like “safe methods do not modify server state” or “1xx responses MUST NOT contain a body.”

In every mainstream HTTP library, these rules are comments in the source code. In Hale, they are theorems checked by the Lean 4 kernel.


1. Method semantics (RFC 9110 §9.2)

RFC 9110 classifies methods as safe (read-only) or idempotent (repeatable). It also states that every safe method is idempotent. We encode the definitions and prove the implication:

-- The definitions
def Method.isSafe : Method → Bool
  | .standard .GET     => true
  | .standard .HEAD    => true
  | .standard .OPTIONS => true
  | .standard .TRACE   => true
  | _                  => false

def Method.isIdempotent : Method → Bool
  | .standard .PUT    => true
  | .standard .DELETE => true
  | m                 => m.isSafe

-- Per-method proofs (21 theorems)
theorem Method.get_is_safe     : (.standard .GET).isSafe = true      := by rfl
theorem Method.post_not_safe   : (.standard .POST).isSafe = false    := by rfl
theorem Method.put_is_idempotent  : (.standard .PUT).isIdempotent = true  := by rfl
theorem Method.post_not_idempotent : (.standard .POST).isIdempotent = false := by rfl
-- ... (17 more)

-- The universal property: safe ⟹ idempotent (RFC 9110 §9.2.2)
theorem Method.safe_implies_idempotent (m : Method) (h : m.isSafe = true) :
    m.isIdempotent = true := by
  match m with
  | .standard .GET     => rfl
  | .standard .HEAD    => rfl
  | .standard .OPTIONS => rfl
  | .standard .TRACE   => rfl
  | .standard .PUT     => simp [Method.isSafe] at h
  | .standard .POST    => simp [Method.isSafe] at h
  -- ... (exhaustive case analysis, contradictions resolved by simp)

This is not a test that runs on four inputs. It is a proof for all methods — including custom ones. The | .custom _ => case must also be handled, and it is: isSafe returns false for custom methods, so the hypothesis h is contradictory and simp discharges it.


2. Status code bodies (RFC 9110 §6.4.1)

The RFC says: 1xx, 204, and 304 responses MUST NOT contain a body. We encode this as a predicate and prove it for every relevant status code:

def Status.mustNotHaveBody (s : Status) : Bool :=
  s.statusCode / 100 == 1 || s.statusCode == 204 || s.statusCode == 304

-- Proven for each status code:
theorem status100_no_body : status100.mustNotHaveBody = true  := by native_decide
theorem status101_no_body : status101.mustNotHaveBody = true  := by native_decide
theorem status204_no_body : status204.mustNotHaveBody = true  := by native_decide
theorem status304_no_body : status304.mustNotHaveBody = true  := by native_decide
theorem status200_may_have_body : status200.mustNotHaveBody = false := by native_decide
theorem status404_may_have_body : status404.mustNotHaveBody = false := by native_decide

If someone adds a new status code to the library, they can immediately check whether it may carry a body — the kernel evaluates the predicate at compile time.


3. Exactly-once response (the indexed monad)

In Haskell’s WAI, the rule “call respond exactly once per request” is a comment. In Hale, it is an indexed monad — a type-level state machine that the Lean 4 kernel verifies:

inductive ResponseState where
  | pending    -- no response sent yet
  | sent       -- response has been sent

structure AppM (pre post : ResponseState) (α : Type) where
  private mk ::      -- private: cannot fabricate
  run : IO α

-- The ONLY way to go from .pending to .sent:
def AppM.respond (callback : Response → IO ResponseReceived) (resp : Response)
    : AppM .pending .sent ResponseReceived := ⟨callback resp⟩

-- Every Application must produce AppM .pending .sent:
abbrev Application :=
  Request → (Response → IO ResponseReceived) → AppM .pending .sent ResponseReceived

Why double-respond is a type error:

-- After the first respond, the state is .sent.
-- AppM.ibind chains:  AppM s₁ s₂ α → (α → AppM s₂ s₃ β) → AppM s₁ s₃ β
-- A second respond needs pre-state .pending, but we are in .sent.

def doubleRespond (respond : Response → IO ResponseReceived)
    : AppM .pending .sent ResponseReceived :=
  AppM.ibind (AppM.respond respond resp1) fun _ =>
    AppM.respond respond resp2
    -- ❌ Type error: expected AppM .sent _ _, got AppM .pending .sent _

Why skip-respond is a type error:

def skipRespond : AppM .pending .sent ResponseReceived :=
  AppM.ipure ResponseReceived.done
  -- ❌ Type error: AppM.ipure has type AppM s s α, so this is
  --    AppM .pending .pending ResponseReceived — not .pending .sent

The private mk seals the guarantee: you cannot construct AppM .pending .sent without actually calling respond.


4. Transport security (proven at the type level)

Warp supports TCP, TLS, and QUIC. The type system encodes which transports are secure:

inductive Transport where
  | tcp
  | tls (major minor : Nat) (alpn : Option String) (cipher : UInt16)
  | quic (alpn : Option String) (cipher : UInt16)

def Transport.isSecure : Transport → Bool
  | .tcp      => false
  | .tls ..   => true
  | .quic ..  => true

-- Proven:
theorem tcp_not_secure : Transport.isSecure .tcp = false := rfl
theorem tls_is_secure (v1 v2 p c) : Transport.isSecure (.tls v1 v2 p c) = true := rfl
theorem quic_is_secure (p c)      : Transport.isSecure (.quic p c) = true := rfl

These are trivial — rfl suffices because the kernel evaluates the function on the concrete constructor. But having them as named theorems means downstream code can prove, say, “if we’re on TLS, the connection is secure” without re-deriving the fact.


5. Keep-alive semantics (RFC 9112)

HTTP/1.0 defaults to close; HTTP/1.1 defaults to keep-alive. This is specified in RFC 9112 §9.3 and proven in Hale:

theorem connAction_http10_default (req : Request)
    (hVer : (req.httpVersion == http11) = false)
    (hNoConn : req.requestHeaders.find? (...) = none) :
    connAction req = .close := by
  unfold connAction; simp [hVer, hNoConn]

theorem connAction_http11_default (req : Request)
    (hVer : (req.httpVersion == http11) = true)
    (hNoConn : req.requestHeaders.find? (...) = none) :
    connAction req = .keepAlive := by
  unfold connAction; simp [hVer, hNoConn]

6. Wire-format bijectivity (roundtrip theorems)

When encoding frame types, error codes, or opcodes to bytes and back, information must not be lost. We prove this for every codec:

HTTP/2 frame types (RFC 9113 §6)

-- 10 roundtrip theorems:
theorem FrameType.roundtrip_data    : fromUInt8 (toUInt8 .data) = .data       := by rfl
theorem FrameType.roundtrip_headers : fromUInt8 (toUInt8 .headers) = .headers := by rfl
-- ... (8 more: priority, rstStream, settings, pushPromise, ping, goaway, windowUpdate, continuation)

HTTP/3 error codes (RFC 9114)

-- 17 roundtrip theorems:
theorem roundtrip_noError : fromUInt64 (toUInt64 .noError) = .noError := by rfl
theorem roundtrip_generalProtocolError : fromUInt64 (toUInt64 .generalProtocolError) = ... := by rfl
-- ... (15 more)

WebSocket opcodes (RFC 6455 §5.2)

-- 6 roundtrip theorems:
theorem roundtrip_text  : Opcode.fromUInt8 (Opcode.toUInt8 .text) = .text   := by rfl
theorem roundtrip_close : Opcode.fromUInt8 (Opcode.toUInt8 .close) = .close := by rfl
-- ...

These ensure that every frame/error/opcode survives a serialize → deserialize cycle unchanged. The proof is by rfl — the kernel evaluates both sides and confirms they are definitionally equal.


7. HTTP/2 stream lifecycle (RFC 9113 §5.1)

The HTTP/2 stream state machine is encoded as an inductive type:

inductive StreamState where
  | idle            -- not yet opened
  | open            -- HEADERS sent/received
  | halfClosedLocal -- local END_STREAM sent
  | halfClosedRemote -- remote END_STREAM received
  | resetLocal      -- RST_STREAM sent
  | resetRemote     -- RST_STREAM received
  | closed          -- terminal
  | reservedLocal   -- PUSH_PROMISE received
  | reservedRemote  -- PUSH_PROMISE sent

Nine states, directly from RFC 9113 §5.1 Figure 2. The exhaustive pattern matching ensures every state transition is handled in every function that operates on streams.


The total count

CategoryTheoremsRFC/Standard
Method semantics21RFC 9110 §9.2
Status code properties13RFC 9110 §6.4.1, §15
Status code ranges5RFC 9110 §15
HTTP version parsing4RFC 9110
Method parsing roundtrips10RFC 9110
Transport security3TLS 1.2+/QUIC
Keep-alive semantics2RFC 9112 §9.3
Response exactly-once(type-level)WAI contract
H2 frame roundtrips10RFC 9113 §6
H3 frame roundtrips7RFC 9114
H3 error roundtrips17RFC 9114
WS opcode roundtrips6RFC 6455 §5.2
Middleware algebra5WAI composition
Response accessor laws11WAI internal
Total~114

Every one of these is verified by the Lean 4 kernel at compile time. They cannot be wrong (modulo axioms, which are documented). They cannot go stale — if someone changes a definition, the proof breaks and the build fails.


This post is part of the Hale documentation — a port of Haskell’s web ecosystem to Lean 4 with maximalist typing.

Proofs as Architecture: A Catalog of Dependent-Type Patterns in Hale

In Lean 4, a proof field is free. It costs zero bytes at runtime, zero branches in the generated code, and infinite confidence at review time.

Hale ports Haskell’s web ecosystem to Lean 4. Where Haskell uses conventions, Hale uses proofs. Where Haskell uses runtime checks, Hale uses type-level constraints. This post catalogs every technique we use, with concrete examples from the codebase.


Pattern 1: Proof-carrying structures

Idea: Embed a proof directly as a field in a structure. Lean 4 erases proofs at compile time, so the struct has zero additional overhead — same size, same layout, same codegen.

Rational numbers: always normalized

structure Ratio where
  num : Int
  den : Nat
  den_pos : den > 0                        -- erased: denominator always positive
  coprime : Nat.Coprime num.natAbs den     -- erased: always in lowest terms

Every Ratio value that exists is by construction in lowest terms with a positive denominator. There is no normalize function, no “please remember to reduce” comment, no runtime gcd call on read. The proof happens once, at construction time, and is then thrown away.

What this buys: Structural equality on Ratio is correct. Two ratios are equal if and only if they represent the same number — no need to normalize before comparing.

QUIC Connection ID: RFC 9000 §17.2 length bound

structure ConnectionId where
  bytes : ByteArray
  hLen : bytes.size ≤ 20 := by omega     -- erased: RFC max length

RFC 9000 Section 17.2 says connection IDs must not exceed 20 bytes. The proof field hLen encodes this as a compile-time obligation. Constructing a ConnectionId with 21 bytes is a type error — omega cannot prove 21 ≤ 20.

Warp Settings: positive timeout and backlog

structure Settings where
  settingsTimeout : Nat := 30
  settingsTimeoutPos : settingsTimeout > 0 := by omega    -- erased
  settingsBacklog : Nat := 128
  settingsBacklogPos : settingsBacklog > 0 := by omega    -- erased

-- Proven: the default is valid
theorem defaultSettings_valid :
    defaultSettings.settingsTimeout > 0 ∧ defaultSettings.settingsBacklog > 0 := ...

A zero timeout would immediately close every connection. A zero backlog would reject all pending connections. Both are impossible to construct.

Static file paths: no traversal attacks

structure Piece where
  val : String
  no_dot : ¬ val.startsWith "."     -- rejects dotfiles
  no_slash : ¬ val.contains '/'     -- rejects embedded slashes

Path traversal (../../etc/passwd) is a compile-time error. The toPiece constructor validates strings and returns Option Piece — invalid paths produce none before they ever reach the filesystem.


Pattern 2: Phantom type parameters (zero-cost state machines)

Idea: Add a type parameter that exists only at the type level and is erased at runtime. Functions constrain which values of the parameter they accept, so the compiler enforces the state machine.

Socket lifecycle (POSIX)

(Covered in detail in blog post 1)

structure Socket (state : SocketState) where
  raw : RawSocket

def send (s : Socket .connected) (data : ByteArray) : IO Nat  -- only .connected
def close (s : Socket state) (h : state ≠ .closed := by decide) : IO (Socket .closed)

Five states, eleven distinctness theorems, one proof obligation on close. All erased. Same codegen as C.


Pattern 3: Indexed monads (exactly-once protocol enforcement)

Idea: Parameterise a monad by a pre-state and a post-state. The bind operation chains states: AppM s₁ s₂ → (→ AppM s₂ s₃) → AppM s₁ s₃. Make the constructor private so the only way to produce a value is through the provided combinators.

WAI Application: exactly-once response

(Covered in detail in blog post 2)

structure AppM (pre post : ResponseState) (α : Type) where
  private mk ::
  run : IO α

def AppM.respond (cb : Response → IO ResponseReceived) (r : Response)
    : AppM .pending .sent ResponseReceived

Double-respond, skip-respond, and token fabrication are all type errors.


Pattern 4: Inductive types as protocol specifications

Idea: Use exhaustive sum types to represent every valid message, frame, or state in a protocol. Pattern matching is total — the compiler rejects incomplete handlers.

HTTP/2 stream states (RFC 9113 §5.1)

inductive StreamState where
  | idle | open | halfClosedLocal | halfClosedRemote
  | resetLocal | resetRemote | closed
  | reservedLocal | reservedRemote

Nine states, straight from the RFC. Any function that dispatches on a stream must handle all nine. Miss one and the build fails.

WebSocket connection lifecycle (RFC 6455)

inductive ConnectionState where
  | pending   -- handshake not completed
  | open_     -- data transfer
  | closing   -- close frame sent
  | closed    -- terminated

QUIC connection lifecycle (RFC 9000)

inductive ConnectionState where
  | handshaking | established | closing | closed

HTTP/2 frame types (RFC 9113 §6)

inductive FrameType where
  | data | headers | priority | rstStream | settings
  | pushPromise | ping | goaway | windowUpdate | continuation
  | unknown (id : UInt8)

The unknown variant is a catch-all for future frame types — the pattern match is still exhaustive.

HTTP request errors (Warp)

inductive InvalidRequest where
  | notEnoughLines | badFirstLine | nonHttp | incompleteHeaders
  | connectionClosedByPeer | overLargeHeader | badProxyHeader
  | payloadTooLarge | requestHeaderFieldsTooLarge

Every protocol-level error has its own constructor. No stringly-typed error messages. No error_code: 42 lookups.


Pattern 5: Roundtrip theorems (wire-format bijectivity)

Idea: Prove that serialization followed by deserialization is the identity function. This ensures no information is lost on the wire.

-- HTTP/2 (10 theorems)
theorem FrameType.roundtrip_data : fromUInt8 (toUInt8 .data) = .data := by rfl

-- HTTP/3 error codes (17 theorems)
theorem roundtrip_noError : fromUInt64 (toUInt64 .noError) = .noError := by rfl

-- WebSocket opcodes (6 theorems)
theorem roundtrip_text : Opcode.fromUInt8 (Opcode.toUInt8 .text) = .text := by rfl

-- HTTP methods (9 theorems)
theorem parseMethod_GET : parseMethod "GET" = .standard .GET := by rfl

-- HTTP versions (4 theorems)
theorem parseHttpVersion_http11 : parseHttpVersion "HTTP/1.1" = some http11 := by rfl

All proved by rfl — the kernel evaluates both sides and confirms they are definitionally equal. If someone changes the encoding (e.g., swaps two frame type constants), the proof breaks immediately.


Pattern 6: Algebraic laws (composition contracts)

Idea: Prove that combinators satisfy algebraic laws. This ensures that refactoring (reordering middleware, simplifying chains) preserves behavior.

Middleware composition

theorem idMiddleware_comp_left  (m : Middleware) : id ∘ m = m         := rfl
theorem idMiddleware_comp_right (m : Middleware) : m ∘ id = m         := rfl
theorem modifyRequest_id  : modifyRequest id = idMiddleware           := rfl
theorem modifyResponse_id : modifyResponse id = idMiddleware          := rfl
theorem ifRequest_false (m : Middleware) : ifRequest (· => false) m = id := rfl

Builder monoid

theorem empty_append (b : Builder) : ∅ ++ b = b       := ...
theorem append_empty (b : Builder) : b ++ ∅ = b       := ...
theorem append_assoc (a b c)       : (a ++ b) ++ c = a ++ (b ++ c) := ...

Response functor identity

theorem mapResponseHeaders_id_builder (s h b) :
    (responseBuilder s h b).mapResponseHeaders id = responseBuilder s h b := rfl

theorem mapResponseStatus_id_builder (s h b) :
    (responseBuilder s h b).mapResponseStatus id = responseBuilder s h b := rfl
-- (6 more for file and stream variants)

Case-insensitive header equality

theorem addHeaders_nil_builder (s h b) :
    (responseBuilder s h b).mapResponseHeaders (· ++ []) = responseBuilder s h b := ...

Pattern 7: Opaque FFI handles with automatic cleanup

Idea: Use lean_alloc_external with a GC finalizer. The Lean type is opaque (no structure visible to user code), so the only way to operate on it is through the provided FFI functions.

opaque SocketHandle  : NonemptyType   -- POSIX socket fd
opaque EventLoopHandle : NonemptyType -- kqueue/epoll fd
opaque RecvBufferHandle : NonemptyType -- buffered reader
opaque TLSContextHandle : NonemptyType -- OpenSSL SSL_CTX
opaque TLSSessionHandle : NonemptyType -- OpenSSL SSL session

The C-side finalizer calls close(fd) or SSL_CTX_free(ctx) when the Lean GC collects the object. The opaque keyword ensures user code cannot peek inside the handle or forge one from an integer.


Pattern 8: Explicit axioms for FFI trust boundaries

When a property depends on FFI behavior that the Lean kernel cannot verify, we declare it as an axiom with clear documentation:

-- Warp: header count is bounded by the loop guard in recvHeaders
axiom recvHeaders_bounded (buf : RecvBuffer) :
    ∀ rl hdrs, recvHeaders buf = pure (rl, hdrs) → hdrs.length ≤ maxHeaders

-- Green scheduler: bind terminates if operands terminate
axiom GreenBase.bind_terminates {...} : True

-- Green scheduler: await resumes when task completes
axiom GreenBase.await_resumes {...} : True

-- Green scheduler: non-blocking scheduler prevents starvation
axiom GreenBase.no_pool_starvation {...} : True

Axioms are the honest answer to “I cannot prove this in Lean, but I believe it to be true based on the C/OS semantics.” They document the trust boundary explicitly — unlike undocumented unsafePerformIO calls in Haskell, axioms are visible in the dependency graph and can be audited.


The big picture

TechniqueWhat it encodesRuntime costExample
Proof fieldsStructural invariants0 (erased)Ratio.den_pos, ConnectionId.hLen
Phantom paramsState machines0 (erased)Socket (state : SocketState)
Indexed monadsExactly-once protocols0 (erased)AppM .pending .sent
Inductive typesMessage/frame/state spaces0 (tag only)StreamState, FrameType
Roundtrip thmsWire-format correctness0 (erased)46 bijectivity proofs
Algebraic lawsComposition safety0 (erased)id ∘ m = m, monoid laws
Opaque FFIResource encapsulation0 (pointer)SocketHandle, TLSContextHandle
AxiomsFFI trust boundary0 (erased)recvHeaders_bounded

Total theorem count: 230+ across the codebase, covering:

  • POSIX socket protocol
  • HTTP/1.1 and HTTP/2 semantics (RFC 9110, 9112, 9113)
  • HTTP/3 framing and errors (RFC 9114)
  • QUIC transport parameters (RFC 9000)
  • WebSocket protocol (RFC 6455)
  • WAI application contract
  • Algebraic composition laws
  • Rational arithmetic invariants
  • Wire-format bijectivity

Every theorem is checked by the Lean 4 kernel. They cannot be wrong (modulo axioms). They cannot go stale. They cannot be disabled by a -Wno-whatever flag. They are as much a part of the architecture as the functions they describe.


This post is part of the Hale documentation — a port of Haskell’s web ecosystem to Lean 4 with maximalist typing.