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
| Package | Guide | API | Theorems | Description |
|---|---|---|---|---|
| Base | Guide | API | 88 | Foundational types, functors, monads |
| ByteString | Guide | API | 7 | Byte array operations |
| Network | — | API | 7 | POSIX sockets with phantom state |
| HttpTypes | — | API | 42 | HTTP methods, status, headers, URI |
Web Application Interface
| Package | Guide | API | Theorems | Description |
|---|---|---|---|---|
| WAI | Guide | API | 17 | Request/Response/Application/Middleware |
| Warp | Guide | API | 11 | HTTP/1.x server (156k QPS) |
| WarpTLS | Guide | API | — | HTTPS via OpenSSL FFI |
| WarpQUIC | — | API | — | HTTP/3 over QUIC |
| WaiExtra | Guide | API | 11 | 36 middleware modules |
| WaiAppStatic | Guide | API | 4 | Static file serving |
| WebSockets | Guide | API | 6 | RFC 6455 protocol |
Protocol Implementations
| Package | Guide | API | Theorems | Description |
|---|---|---|---|---|
| Http2 | — | API | 10 | HTTP/2 framing (RFC 9113) |
| Http3 | — | API | 24 | HTTP/3 framing + QPACK |
| QUIC | — | API | — | QUIC transport |
| TLS | Guide | API | — | OpenSSL FFI wrapper |
Utilities
| Package | Guide | API | Theorems | Description |
|---|---|---|---|---|
| MimeTypes | — | API | — | MIME type lookup |
| Cookie | — | API | — | HTTP cookie parsing |
| Base64 | — | API | — | RFC 4648 codec |
| ResourceT | Guide | API | 1 | Resource management monad |
| FastLogger | — | API | — | Buffered thread-safe logging |
| AutoUpdate | — | API | — | Periodic cached values |
| TimeManager | — | API | — | Connection 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
- Bifunctor —
Data.Bifunctor - Contravariant —
Data.Functor.Contravariant - Const —
Data.Functor.Const - Identity —
Data.Functor.Identity - Compose —
Data.Functor.Compose - Category —
Control.Category
Data Structures
Traversals
- Foldable —
Data.Foldable - Traversable —
Data.Traversable
Numeric Types
Advanced Abstractions
- Arrow —
Control.Arrow
Concurrency
- Concurrent —
Control.Concurrent - MVar —
Control.Concurrent.MVar - Chan —
Control.Concurrent.Chan - QSem —
Control.Concurrent.QSem - QSemN —
Control.Concurrent.QSemN
Void
Lean: Hale.Base.Void | Haskell: Data.Void
Overview
Uninhabited type (alias for Empty). Provides absurd for ex falso reasoning.
API Mapping
| Lean | Haskell | Kind |
|---|---|---|
Void | Void | Type |
Void.absurd | absurd | Function |
Instances
BEq VoidOrd VoidToString VoidRepr VoidHashable VoidInhabited (Void -> a)— functions fromVoidare always inhabited
Proofs & Guarantees
eq_absurd— uniqueness of void functions: any two functions fromVoidare 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
| Lean | Haskell | Kind |
|---|---|---|
Function.on | on | Combinator |
Function.applyTo | (&) | Combinator |
Function.const | const | Combinator |
Function.flip | flip | Combinator |
Instances
None (standalone functions).
Proofs & Guarantees
on_apply—(f.on g) x y = f (g x) (g y)applyTo_apply—x.applyTo f = f xconst_apply—Function.const a b = aflip_flip—flip (flip f) = f(involution)flip_apply—Function.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
| Lean | Haskell | Kind |
|---|---|---|
Dual | Dual | Wrapper |
Endo | Endo | Wrapper |
First | First | Wrapper |
Last | Last | Wrapper |
Sum / Sum.getSum | Sum / getSum | Wrapper |
Product / Product.getProduct | Product / getProduct | Wrapper |
All | All | Wrapper |
Any | Any | Wrapper |
Instances
All wrappers provide Append.
| Wrapper | BEq | Ord | Repr | Hashable | ToString |
|---|---|---|---|---|---|
Dual | yes | yes | yes | yes | yes |
Sum | yes | yes | yes | yes | yes |
Product | yes | yes | yes | yes | yes |
All | yes | yes | yes | yes | yes |
Any | yes | yes | yes | yes | yes |
First | yes | — | yes | — | yes |
Last | yes | — | yes | — | yes |
Endo | — | — | — | — | — |
Proofs & Guarantees
append_assoc— associativity ofAppendfor 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
| Lean | Haskell | Kind |
|---|---|---|
Bifunctor class | Bifunctor | Typeclass |
bimap | bimap | Method |
mapFst | first | Method |
mapSnd | second | Method |
LawfulBifunctor | (lawful) | Typeclass |
Instances
Bifunctor ProdBifunctor SumBifunctor ExceptLawfulBifunctor ProdLawfulBifunctor SumLawfulBifunctor Except
Proofs & Guarantees
bimap_id—bimap id id = id(viaLawfulBifunctor)bimap_comp—bimap (f1 . f2) (g1 . g2) = bimap f1 g1 . bimap f2 g2(viaLawfulBifunctor)
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
| Lean | Haskell | Kind |
|---|---|---|
Contravariant class | Contravariant | Typeclass |
contramap | contramap | Method |
Predicate | Predicate | Type |
Equivalence | Equivalence | Type |
LawfulContravariant class | (lawful) | Typeclass |
Instances
Contravariant PredicateContravariant EquivalenceLawfulContravariant PredicateLawfulContravariant Equivalence
Proofs & Guarantees
contramap_id—contramap id = id(viaLawfulContravariant)contramap_comp—contramap (f . g) = contramap g . contramap f(viaLawfulContravariant)
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
| Lean | Haskell | Kind |
|---|---|---|
Const | Const | Type |
getConst | getConst | Accessor |
Functor Const | Functor (Const a) | Instance |
Pure Const | Applicative (Const a) | Instance |
Instances
BEq (Const a b)(requiresBEq a)Ord (Const a b)(requiresOrd a)Repr (Const a b)(requiresRepr a)ToString (Const a b)(requiresToString a)Functor (Const a)Pure (Const a)(requiresAppend aandInhabited a)
Proofs & Guarantees
map_val— mapping overConstpreserves the wrapped valuemap_id—Functor.map id = idmap_comp—Functor.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
| Lean | Haskell | Kind |
|---|---|---|
Identity | Identity | Type |
runIdentity | runIdentity | Accessor |
Instances
Functor IdentityPure IdentityBind IdentitySeq IdentityApplicative IdentityMonad IdentityBEq (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_id—Functor.map id = idmap_comp—Functor.map (f . g) = Functor.map f . Functor.map gpure_bind— left identity:pure a >>= f = f abind_pure— right identity:m >>= pure = mbind_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
| Lean | Haskell | Kind |
|---|---|---|
Compose | Compose | Type |
getCompose | getCompose | Accessor |
Instances
Functor (Compose F G)(requiresFunctor F,Functor G)Pure (Compose F G)(requiresApplicative F,Applicative G)Seq (Compose F G)(requiresApplicative F,Applicative G)Applicative (Compose F G)(requiresApplicative F,Applicative G)
Proofs & Guarantees
map_id—Functor.map id = id(with lawful functors)map_comp—Functor.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
| Lean | Haskell | Kind |
|---|---|---|
Category class | Category | Typeclass |
Category.id | id | Method |
Category.comp / >>> | >>> | Method |
Fun | (->) | Type |
LawfulCategory class | (lawful) | Typeclass |
Instances
Category Fun— the function arrow as a categoryLawfulCategory Fun— all three laws hold definitionally
Proofs & Guarantees
id_comp—id >>> f = f(viaLawfulCategory)comp_id—f >>> id = f(viaLawfulCategory)comp_assoc—(f >>> g) >>> h = f >>> (g >>> h)(viaLawfulCategory)
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
| Lean | Haskell | Kind |
|---|---|---|
NonEmpty | NonEmpty | Type |
head | head | Accessor |
last | last | Accessor |
length | length | Function |
toList | toList | Function |
singleton | singleton | Constructor |
cons | cons | Constructor |
append | append | Function |
map | map | Function |
reverse | reverse | Function |
foldr | foldr | Function |
foldr1 | foldr1 | Function |
foldl1 | foldl1 | Function |
fromList? | nonEmpty | Function |
fromList | fromList | Function |
Instances
Append (NonEmpty a)Functor NonEmptyPure NonEmptyBind NonEmptyMonad NonEmptyToString (NonEmpty a)(requiresToString a)BEq (NonEmpty a)(derived)Ord (NonEmpty a)(derived)Repr (NonEmpty a)(derived)Hashable (NonEmpty a)(derived)
Proofs & Guarantees
toList_ne_nil—toList ne != [](the list is never empty)reverse_length— reversing preserves lengthtoList_length—toListlength agrees withNonEmpty.lengthmap_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
| Lean | Haskell | Kind |
|---|---|---|
Either | Either | Type |
left / right | Left / Right | Constructors |
isLeft / isRight | isLeft / isRight | Predicate |
fromLeft / fromRight | fromLeft / fromRight | Accessor |
either | either | Eliminator |
mapLeft / mapRight | first / second | Function |
swap | N/A | Function |
partitionEithers | partitionEithers | Function |
Instances
Functor (Either a)Pure (Either a)Bind (Either a)Seq (Either a)Applicative (Either a)Monad (Either a)Bifunctor EitherToString (Either a b)(requiresToString a,ToString b)
Proofs & Guarantees
swap_swap—swap (swap e) = e(involution)isLeft_not_isRight—isLeft e = !isRight epartitionEithers_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
| Lean | Haskell | Kind |
|---|---|---|
Down | Down | Type |
comparing | comparing | Function |
clamp | clamp | Function (returns {y // lo <= y /\ y <= hi}) |
Instances
BEq (Down a)(requiresBEq a)Ord (Down a)(requiresOrd a, reversed)ToString (Down a)(requiresToString a)
Proofs & Guarantees
get_mk—(Down.mk x).get = xcompare_reverse—compare (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
| Lean | Haskell | Kind |
|---|---|---|
Tuple.swap | swap | Function |
Tuple.mapFst | first (Control.Arrow) | Function |
Tuple.mapSnd | second | Function |
Tuple.bimap | bimap | Function |
Tuple.curry | curry | Function |
Tuple.uncurry | uncurry | Function |
Instances
None (standalone functions on Prod).
Proofs & Guarantees
swap_swap—swap (swap p) = p(involution)curry_uncurry—curry (uncurry f) = funcurry_curry—uncurry (curry f) = fbimap_id—bimap id id = idbimap_comp—bimap (f1 . f2) (g1 . g2) = bimap f1 g1 . bimap f2 g2mapFst_eq_bimap—mapFst f = bimap f idmapSnd_eq_bimap—mapSnd 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
| Lean | Haskell | Kind |
|---|---|---|
Foldable class | Foldable | Typeclass |
foldr | foldr | Method |
foldl | foldl' | Method |
toList | toList | Method |
foldMap | foldMap | Function |
null | null | Function |
length | length | Function |
any | any | Function |
all | all | Function |
find? | find | Function |
elem | elem | Function |
minimum? | minimum | Function |
maximum? | maximum | Function |
sum | sum | Function |
product | product | Function |
Instances
Foldable ListFoldable OptionFoldable List.NonEmptyFoldable (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
| Lean | Haskell | Kind |
|---|---|---|
Traversable class | Traversable | Typeclass |
traverse | traverse | Method |
sequence | sequenceA | Method |
LawfulTraversable | (lawful) | Typeclass |
Instances
Traversable ListTraversable OptionTraversable 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 justmap(viaLawfulTraversable)
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
| Lean | Haskell | Kind |
|---|---|---|
Ratio | Ratio / Rational | Type |
mk' | % | Constructor |
fromInt | fromIntegral | Constructor |
fromNat | fromIntegral | Constructor |
neg | negate | Function |
abs | abs | Function |
add | (+) | Function |
sub | (-) | Function |
mul | (*) | Function |
inv | recip | Function |
div | (/) | Function |
floor | floor | Function |
ceil | ceiling | Function |
round | round | Function |
zero | 0 | Constant |
one | 1 | Constant |
Instances
OfNat Ratio 0/OfNat Ratio 1Inhabited RatioLE Ratio/LT RatioBEq RatioOrd RatioAdd Ratio/Sub Ratio/Mul Ratio/Neg RatioToString 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
| Lean | Haskell | Kind |
|---|---|---|
Complex | Complex | Type |
re / im | realPart / imagPart | Accessor |
ofReal | (:+ 0) | Constructor |
i | 0 :+ 1 | Constant |
conjugate | conjugate | Function |
magnitudeSquared | magnitude ^2 | Function |
Instances
Inhabited (Complex a)(requiresInhabited a)ToString (Complex a)(requiresToString a)Add (Complex a)(requiresAdd a)Neg (Complex a)(requiresNeg a)Sub (Complex a)(requiresSub a)Mul (Complex a)(requiresAdd a,Sub a,Mul a)BEq (Complex a)(derived)Ord (Complex a)(derived)Repr (Complex a)(derived)Hashable (Complex a)(derived)
Proofs & Guarantees
conjugate_conjugate—conjugate (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
| Lean | Haskell | Kind |
|---|---|---|
Fixed | Fixed | Type |
raw | MkFixed | Accessor |
scale | resolution | Function |
fromInt | fromIntegral | Constructor |
toRatio | toRational | Function |
Instances
Add (Fixed p)/Sub (Fixed p)/Neg (Fixed p)/Mul (Fixed p)OfNat (Fixed p) 0/OfNat (Fixed p) 1Inhabited (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 factor10^pis always positiveadd_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
| Lean | Haskell | Kind |
|---|---|---|
Arrow class | Arrow | Typeclass |
arr | arr | Method |
first | first | Method |
second | second | Method |
split | (***) | Method |
ArrowChoice class | ArrowChoice | Typeclass |
left | left | Method |
right | right | Method |
fanin | `( |
Instances
Arrow FunArrowChoice 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
| Lean | Haskell | Kind |
|---|---|---|
ThreadId | ThreadId | Type |
forkIO | forkIO | Function |
forkFinally | forkFinally | Function |
killThread | killThread | Function |
threadDelay | threadDelay | Function |
yield | yield | Function |
waitThread | (no direct equivalent) | Function |
PosNat | (no direct equivalent) | Type ({ n : Nat // n > 0 }) |
Differences from GHC
| Aspect | GHC | hale |
|---|---|---|
| Thread model | Green threads with RTS scheduler | IO.asTask on Lean’s thread pool |
| Cancellation | Asynchronous exceptions (throwTo) | Cooperative CancellationToken |
threadDelay unit | Microseconds | Microseconds (converted internally to ms, rounded up) |
| Thread ID uniqueness | Runtime-assigned, may be reused | Monotonic PosNat counter (≥ 1), never reused |
Instances
BEq ThreadId— equality byidfieldHashable ThreadId— hash ofidfieldToString 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:
freshThreadIdreads and increments a global monotonic counter. Within a single process, no twoThreadIdvalues share the sameid. forkFinallyfinaliser guarantee: Thefinally_callback runs in both the success and error branches of a try/catch, so it executes regardless of outcome.- Cooperative cancellation only:
killThreadsets aCancellationToken; 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
forkIOis lightweight: it schedules a task on the thread pool, not an OS thread.threadDelayhas millisecond granularity due toIO.sleep. A request for 1 microsecond will sleep for 1 millisecond.yieldis equivalent toIO.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
| Lean | Haskell | Kind | Signature |
|---|---|---|---|
MVar.new | newMVar | Constructor | a -> BaseIO (MVar a) |
MVar.newEmpty | newEmptyMVar | Constructor | BaseIO (MVar a) |
MVar.take | takeMVar | Async op | MVar a -> BaseIO (Task a) |
MVar.put | putMVar | Async op | MVar a -> a -> BaseIO (Task Unit) |
MVar.read | readMVar | Async op | MVar a -> BaseIO (Task a) |
MVar.swap | swapMVar | Async op | MVar a -> a -> BaseIO (Task a) |
MVar.withMVar | withMVar | Combinator | MVar a -> (a -> BaseIO (a x b)) -> BaseIO (Task b) |
MVar.modify | modifyMVar | Combinator | MVar a -> (a -> BaseIO (a x b)) -> BaseIO (Task b) |
MVar.modify_ | modifyMVar_ | Combinator | MVar a -> (a -> BaseIO a) -> BaseIO (Task Unit) |
MVar.tryTake | tryTakeMVar | Non-blocking | MVar a -> BaseIO (Option a) |
MVar.tryPut | tryPutMVar | Non-blocking | MVar a -> a -> BaseIO Bool |
MVar.tryRead | tryReadMVar | Non-blocking | MVar a -> BaseIO (Option a) |
MVar.isEmpty | isEmptyMVar | Query | MVar a -> BaseIO Bool |
MVar.takeSync | (N/A) | Sync wrapper | MVar a -> BaseIO a |
MVar.putSync | (N/A) | Sync wrapper | MVar a -> a -> BaseIO Unit |
MVar.readSync | (N/A) | Sync wrapper | MVar 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, thentakersis empty (no one waits when a value exists). - If
value.isNone, thenputtersis 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
puton an empty MVar with a taker wakes exactly one taker. - Every
takeon 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.Mutexproviding 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 viaIO.wait. Use them only at top-level or in non-scalable contexts; prefer the async versions for concurrent code. isEmptyis 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
| Lean | Haskell | Kind | Signature |
|---|---|---|---|
Chan.new | newChan | Constructor | BaseIO (Chan a) |
Chan.write | writeChan | Function | Chan a -> a -> BaseIO Unit |
Chan.read | readChan | Async op | Chan a -> BaseIO (Task a) |
Chan.dup | dupChan | Function | Chan a -> BaseIO (Chan a) |
Chan.tryRead | (no direct equivalent) | Non-blocking | Chan 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
Chanhas a privatereadState(per-reader buffer + waiters) and a sharedwriteState(subscriber list). writedelivers the value to all current subscribers.dupcreates a newreadStateand registers it in the shared subscriber list. The new channel shares the write side but reads independently.- Values written before
dupthat have not been read are not visible on the new channel (matching Haskell’sdupChansemantics).
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.Queuebuffer without bound. - Write cost is O(subscribers): Each
writeiterates over all subscribers. For a single-reader channel this is O(1); for manydup’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
readon 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
| Lean | Haskell | Kind | Signature |
|---|---|---|---|
QSem.new | newQSem | Constructor | Nat -> BaseIO QSem |
QSem.wait | waitQSem | Async op | QSem -> BaseIO (Task Unit) |
QSem.signal | signalQSem | Function | QSem -> BaseIO Unit |
QSem.withSem | (bracket pattern) | Combinator | QSem -> 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
waiton an exhausted semaphore creates a dormant promise. No OS thread is consumed while waiting. withSemblocks an OS thread viaIO.waitfor the initial acquire. For fully non-blocking usage, callwaitdirectly and chain withBaseIO.bindTask.- Mutex contention: Both
waitandsignalgo throughStd.Mutex.atomically. Under high contention this is the bottleneck. - Single-unit granularity: For multi-unit acquire/release, use
QSemNinstead.
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
| Lean | Haskell | Kind | Signature |
|---|---|---|---|
QSemN.new | newQSemN | Constructor | Nat -> BaseIO QSemN |
QSemN.wait | waitQSemN | Async op | QSemN -> Nat -> BaseIO (Task Unit) |
QSemN.signal | signalQSemN | Function | QSemN -> Nat -> BaseIO Unit |
QSemN.withSemN | (bracket pattern) | Combinator | QSemN -> 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:
- Peek at the front waiter
(needed, promise). - If
count >= needed, subtractneededfromcount, resolvepromise, and repeat from step 1. - If
count < needed, stop. The waiter remains queued (head-of-line blocking). - 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
waiton an exhausted semaphore creates a dormant promise. No OS thread is consumed while waiting. withSemNblocks an OS thread viaIO.waitfor the initial acquire. For fully non-blocking usage, callwaitdirectly and chain withBaseIO.bindTask.- Wakeup cost:
signalmay wake multiple waiters in a single call (greedy loop), all within a singleStd.Mutex.atomicallyblock. - 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 overByteArrayfor 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 Module | Haskell Module |
|---|---|
Hale.ByteString.Data.ByteString.Internal | Data.ByteString.Internal |
Hale.ByteString.Data.ByteString | Data.ByteString |
Hale.ByteString.Data.ByteString.Char8 | Data.ByteString.Char8 |
Hale.ByteString.Data.ByteString.Short | Data.ByteString.Short |
Hale.ByteString.Data.ByteString.Lazy.Internal | Data.ByteString.Lazy.Internal |
Hale.ByteString.Data.ByteString.Lazy | Data.ByteString.Lazy |
Hale.ByteString.Data.ByteString.Lazy.Char8 | Data.ByteString.Lazy.Char8 |
Hale.ByteString.Data.ByteString.Builder | Data.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
| Lean | Haskell | Notes |
|---|---|---|
ByteString.empty | empty | O(1) |
ByteString.singleton | singleton | |
ByteString.pack | pack | List UInt8 → ByteString |
ByteString.unpack | unpack | ByteString → List UInt8 |
ByteString.take | take | O(1) slice |
ByteString.drop | drop | O(1) slice |
ByteString.splitAt | splitAt | O(1) slice |
ByteString.head h | head | Total with proof h : len > 0 |
ByteString.tail h | tail | O(1) slice with proof |
ByteString.last h | last | Total with proof |
ByteString.init h | init | O(1) slice with proof |
ByteString.index i h | index | Bounds-checked with proof h : i < len |
ByteString.foldl | foldl' | Strict left fold |
ByteString.foldr | foldr | Right fold |
ByteString.map | map | O(n) |
ByteString.reverse | reverse | O(n) |
ByteString.filter | filter | O(n) |
ByteString.isPrefixOf | isPrefixOf | |
ByteString.isSuffixOf | isSuffixOf | |
ByteString.isInfixOf | isInfixOf | |
ByteString.readFile | readFile | Wraps IO.FS.readBinFile |
ByteString.writeFile | writeFile | Wraps IO.FS.writeBinFile |
Instances
BEq ByteString– byte-by-byte comparisonOrd ByteString– lexicographic orderingAppend ByteString– concatenation (O(m+n))ToString ByteString–[w1, w2, ...]formatHashable ByteStringInhabited ByteString– empty
Proofs
take_valid/drop_valid– slice operations preserve boundsnull_iff_length_zero–null <-> length = 0
Performance
| Operation | Complexity | Notes |
|---|---|---|
take, drop, splitAt | O(1) | Zero-copy slice |
head, last, index | O(1) | Direct array access |
tail, init | O(1) | Slice adjustment |
cons, snoc, append | O(n) | Copies data |
map, reverse, filter | O(n) | |
copy | O(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
| Lean | Haskell | Notes |
|---|---|---|
Char8.pack | pack | String → ByteString (Latin-1) |
Char8.unpack | unpack | ByteString → String |
Char8.singleton | singleton | Char → ByteString |
Char8.head | head | Returns Char with proof |
Char8.last | last | Returns Char with proof |
Char8.map | map | (Char → Char) → ByteString → ByteString |
Char8.filter | filter | (Char → Bool) → ByteString → ByteString |
Char8.lines | lines | Split on '\n' (byte 10) |
Char8.words | words | Split on ASCII whitespace |
Char8.unlines | unlines | Join with '\n' |
Char8.unwords | unwords | Join with ' ' |
Char8.isSpace | (helper) | ASCII whitespace check |
Design Notes
- Latin-1 only: Characters above U+00FF are truncated. This matches Haskell’s
Data.ByteString.Char8semantics exactly. - No Unicode support: For UTF-8 encoded byte strings, use a dedicated text library instead.
- Shared representation:
Char8functions operate on the sameByteStringtype – there is no separateChar8ByteString.
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
| Lean | Haskell | Notes |
|---|---|---|
ShortByteString.empty | empty | |
ShortByteString.pack | pack | List UInt8 → ShortByteString |
ShortByteString.unpack | unpack | ShortByteString → List UInt8 |
ShortByteString.length | length | O(1), direct ByteArray.size |
ShortByteString.index | index | Bounds-checked with proof |
ShortByteString.toByteString | fromShort | Wraps as full-offset slice |
ShortByteString.fromByteString | toShort | Copies slice to fresh array |
Instances
BEq ShortByteStringOrd ShortByteStringInhabited ShortByteString– emptyToString ShortByteString
When to Use
- Use
ShortByteStringfor small, long-lived keys (e.g., hash map keys) where GC overhead of the slice representation matters. - Use
ByteStringfor I/O buffers and large data where O(1) slicing is valuable.
Performance
| Operation | Complexity | Notes |
|---|---|---|
length | O(1) | Direct array size |
index | O(1) | Direct array access |
toByteString | O(1) | Wraps with off=0 |
fromByteString | O(n) | Copies slice data |
pack, unpack | O(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
| Lean | Haskell | Notes |
|---|---|---|
LazyByteString.empty | empty | |
LazyByteString.fromStrict | fromStrict | Single-chunk lazy |
LazyByteString.toStrict | toStrict | Materialises all chunks |
LazyByteString.toChunks | toChunks | List ByteString |
LazyByteString.fromChunks | fromChunks | Filters empty chunks |
LazyByteString.length | length | Traverses spine |
LazyByteString.take | take | Lazy, may split a chunk |
LazyByteString.drop | drop | Lazy |
LazyByteString.append | append | O(1) thunk link |
LazyByteString.map | map | Chunk-wise |
LazyByteString.foldlChunks | foldlChunks | Fold over chunks |
Instances
BEq LazyByteStringAppend LazyByteString– O(1) via thunk linkingInhabited LazyByteString– empty
Design Notes
- Non-empty chunk invariant: The proof
h : c.len > 0on everychunkconstructor prevents empty chunks from appearing in the spine. This simplifies reasoning aboutnull,head, anduncons. - Thunk-based laziness: Each tail is wrapped in
Thunkso chunks are only forced on demand, matching Haskell’s lazy list spine. - Chunk size:
fromChunkssilently drops empty byte strings. Consumers should usedefaultChunkSize(typically 32 KiB) when building lazy byte strings incrementally.
Performance
| Operation | Complexity | Notes |
|---|---|---|
append | O(1) | Thunk link, no copying |
cons | O(1) | Prepends single-byte chunk |
length | O(chunks) | Must traverse spine |
toStrict | O(n) | Copies all data into one array |
take, drop | O(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
| Lean | Haskell | Notes |
|---|---|---|
Builder.empty | mempty | Identity builder |
Builder.append | <> / mappend | O(1) composition |
Builder.singleton | word8 | Single byte |
Builder.byteString | byteString | From strict ByteString |
Builder.lazyByteString | lazyByteString | From lazy ByteString |
Builder.toLazyByteString | toLazyByteString | Execute the builder |
Builder.word16BE | word16BE | Big-endian 16-bit |
Builder.word16LE | word16LE | Little-endian 16-bit |
Builder.word32BE | word32BE | Big-endian 32-bit |
Builder.word32LE | word32LE | Little-endian 32-bit |
Builder.word64BE | word64BE | Big-endian 64-bit |
Builder.word64LE | word64LE | Little-endian 64-bit |
Builder.intDec | intDec | Decimal integer encoding |
Monoid Law Proofs
The module proves that Builder forms a lawful monoid:
empty_append:Builder.empty ++ b = bappend_empty:b ++ Builder.empty = bappend_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
| Operation | Complexity | Notes |
|---|---|---|
append | O(1) | Function composition |
singleton, byteString | O(1) | Wraps input |
toLazyByteString | O(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
| Function | Description |
|---|---|
isUpper | Byte in [65, 90] |
isLower | Byte in [97, 122] |
isAlpha | ASCII letter |
isDigit | ASCII digit |
isSpace | Whitespace 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
| Type | Description |
|---|---|
STM α | Transactional computation |
TVar α | Mutable variable (IO.Ref α) |
TMVar α | Synchronization variable (empty or full) |
TQueue α | Unbounded FIFO queue (two-list amortized) |
API
| Function | Description |
|---|---|
atomically | Execute STM transaction |
retry | Block and retry transaction |
orElse | Alternative transaction |
newTVarIO / readTVar / writeTVar | TVar operations |
newTMVar / takeTMVar / putTMVar | TMVar operations |
newTQueue / readTQueue / writeTQueue | TQueue operations |
Files
Hale/STM/Control/Monad/STM.lean– STM monad, atomically, retry, orElseHale/STM/Control/Concurrent/STM/TVar.lean– Transactional variablesHale/STM/Control/Concurrent/STM/TMVar.lean– Transactional MVarsHale/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/finallyensures 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)
| Theorem | Statement |
|---|---|
releaseKey_eq | a = b <-> a.id = b.id |
Axiom-Dependent Properties (documented, not machine-checked)
- Exception safety depends on
IO.finallysemantics - LIFO ordering depends on
Arraypreserving insertion order (which it does)
Instances
Monad (ResourceT m)(for anyMonad m)MonadLift IO (ResourceT m)(for anyMonadLift 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
| Type | Description |
|---|---|
ConduitT i o m r | Stream processor (input i, output o, monad m, result r) |
Source m o | Producer (ConduitT () o m ()) |
Sink i m r | Consumer (ConduitT i Void m r) |
API
| Function | Description |
|---|---|
await | Request next input |
yield | Produce output |
leftoverC | Push back unconsumed input |
awaitForever | Process all inputs |
pipe / .| | Fuse two conduits |
runConduit | Execute a pipeline |
bracketP | Resource-safe bracket |
Files
Hale/Conduit/Data/Conduit.lean– Re-exportsHale/Conduit/Data/Conduit/Internal/Pipe.lean– Pipe typeHale/Conduit/Data/Conduit/Internal/Conduit.lean– ConduitT, fusionHale/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
| Type | Description |
|---|---|
Socket (state : SocketState) | Phantom-typed socket |
SocketState | .fresh | .bound | .listening | .connected | .closed |
SockAddr | Socket address |
AcceptOutcome / RecvOutcome / SendOutcome | Non-blocking I/O outcomes |
RecvBuffer | Buffered reader with CRLF scanning (C FFI) |
EventDispatcher | Green thread suspension bridge (kqueue/epoll) |
API
| Function | State Transition | Signature |
|---|---|---|
socket | → .fresh | IO (Socket .fresh) |
bind | .fresh → .bound | Socket .fresh → SockAddr → IO (Socket .bound) |
listen | .bound → .listening | Socket .bound → Nat → IO (Socket .listening) |
accept | .listening → .connected | Socket .listening → IO (Socket .connected × SockAddr) |
connect | .fresh → .connected | Socket .fresh → SockAddr → IO (Socket .connected) |
send | requires .connected | Socket .connected → ByteArray → IO Nat |
recv | requires .connected | Socket .connected → Nat → IO ByteArray |
close | any → .closed | Socket s → IO Unit |
Files
Hale/Network/Network/Socket/Types.lean– SocketState, SockAddr, phantom socket typeHale/Network/Network/Socket/FFI.lean– C FFI declarationsHale/Network/Network/Socket.lean– High-level API with state transitionsHale/Network/Network/Socket/ByteString.lean– ByteArray send/recvHale/Network/Network/Socket/Blocking.lean– Blocking wrappersHale/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
| Type | Description |
|---|---|
IPv4 | 32-bit address |
IPv6 | Pair of UInt64 |
AddrRange | CIDR range with prefix length |
API
| Function | Signature |
|---|---|
ofOctets | (UInt8, UInt8, UInt8, UInt8) → IPv4 |
toOctets | IPv4 → (UInt8, UInt8, UInt8, UInt8) |
isMatchedTo | Decidable 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
| Function | Description |
|---|---|
recv | Receive up to N bytes, returns empty ByteArray on EOF |
recvString | UTF-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
| Type | Description |
|---|---|
AppData | Connection data with read/write/close/address fields |
API
| Function | Description |
|---|---|
bindPortTCP | Bind server socket to port |
getSocketTCP | Connect to remote TCP server |
acceptSafe | Accept 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
| Function | Description |
|---|---|
sendFile | Send 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
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
| Lean | C (OpenSSL) | Signature |
|---|---|---|
createContext | SSL_CTX_new + SSL_CTX_use_certificate_file | String -> String -> IO TLSContext |
setAlpn | SSL_CTX_set_alpn_select_cb | TLSContext -> IO Unit |
acceptSocket | SSL_new + SSL_set_fd + SSL_accept | TLSContext -> RawSocket -> IO TLSSession |
read | SSL_read | TLSSession -> USize -> IO ByteArray |
write | SSL_write | TLSSession -> ByteArray -> IO Unit |
close | SSL_shutdown + SSL_free | TLSSession -> IO Unit |
getVersion | SSL_get_version | TLSSession -> IO String |
getAlpn | SSL_get0_alpn_selected | TLSSession -> 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_freeon 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, CipherIDHale/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
| Type | Description |
|---|---|
StdMethod | Inductive of standard HTTP methods (GET, POST, HEAD, PUT, DELETE, etc.) |
Method | StdMethod or custom String |
Status | HTTP status with bounded 3-digit code (proof: 100 ≤ code ≤ 999) |
HeaderName | Case-insensitive header name |
Header | HeaderName × ByteArray |
HttpVersion | Major/minor pair with ordering |
Query / QueryItem | Parsed query strings |
API
| Function | Signature |
|---|---|
parseQuery | String → Query |
renderQuery | Bool → Query → String |
urlEncode | Bool → ByteArray → ByteArray |
urlDecode | Bool → ByteArray → Option ByteArray |
Files
Hale/HttpTypes/Network/HTTP/Types/Method.lean– StdMethod, MethodHale/HttpTypes/Network/HTTP/Types/Status.lean– Status with bounded proofHale/HttpTypes/Network/HTTP/Types/Header.lean– Header typesHale/HttpTypes/Network/HTTP/Types/Version.lean– HttpVersionHale/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
| Type | Description |
|---|---|
StreamId | 31-bit bounded stream identifier |
FrameType | Inductive of all RFC 9113 frame types |
ErrorCode | Connection/stream error codes |
Settings | RFC 9113 settings with proof-carrying value constraints |
ConnectionError | Error code + message for GOAWAY |
StreamError | Stream ID + error code + message for RST_STREAM |
Modules
| Module | Description |
|---|---|
Frame.Types | Frame type definitions and flags |
Frame.Encode | Binary frame serialization |
Frame.Decode | Binary frame deserialization |
HPACK.Table | Dynamic/static header table |
HPACK.Huffman | Huffman coding |
HPACK.Encode | HPACK header compression |
HPACK.Decode | HPACK header decompression |
FlowControl | Window-based flow control |
Stream | Stream lifecycle management |
Server | HTTP/2 server implementation |
Files
Hale/Http2/Network/HTTP2/Types.lean– Core typesHale/Http2/Network/HTTP2/Frame/Types.lean– Frame definitionsHale/Http2/Network/HTTP2/Frame/Encode.lean– Frame encodingHale/Http2/Network/HTTP2/Frame/Decode.lean– Frame decodingHale/Http2/Network/HTTP2/HPACK/Table.lean– HPACK tablesHale/Http2/Network/HTTP2/HPACK/Huffman.lean– Huffman codingHale/Http2/Network/HTTP2/HPACK/Encode.lean– HPACK encodingHale/Http2/Network/HTTP2/HPACK/Decode.lean– HPACK decodingHale/Http2/Network/HTTP2/FlowControl.lean– Flow controlHale/Http2/Network/HTTP2/Stream.lean– Stream managementHale/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
| Type | Description |
|---|---|
FrameType | Inductive of HTTP/3 frame types (DATA, HEADERS, SETTINGS, GOAWAY, etc.) |
Proven Properties
decodeVarInt (encodeVarInt n) = some n(roundtrip)
Modules
| Module | Description |
|---|---|
Frame | Frame types + varint encoding/decoding |
Error | HTTP/3 error codes |
QPACK.Table | QPACK static/dynamic tables |
QPACK.Encode | QPACK header compression |
QPACK.Decode | QPACK header decompression |
Server | HTTP/3 server |
Files
Hale/Http3/Network/HTTP3/Frame.lean– Frame types, varint codecHale/Http3/Network/HTTP3/Error.lean– Error codesHale/Http3/Network/HTTP3/QPACK/Table.lean– QPACK tablesHale/Http3/Network/HTTP3/QPACK/Encode.lean– QPACK encodingHale/Http3/Network/HTTP3/QPACK/Decode.lean– QPACK decodingHale/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
| Type | Description |
|---|---|
ConnectionId | Variable-length up to 20 bytes (bounded proof) |
StreamId | UInt64 with 2-bit type encoding |
TransportParams | RFC 9000 Section 18 defaults |
Modules
| Module | Description |
|---|---|
Types | Core protocol types |
Config | Configuration |
Connection | Connection management |
Stream | Stream management |
Server | QUIC server |
Client | QUIC client |
Files
Hale/QUIC/Network/QUIC/Types.lean– ConnectionId, StreamId, TransportParamsHale/QUIC/Network/QUIC/Config.lean– ConfigurationHale/QUIC/Network/QUIC/Connection.lean– Connection managementHale/QUIC/Network/QUIC/Stream.lean– Stream managementHale/QUIC/Network/QUIC/Server.lean– ServerHale/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
| Function | Description |
|---|---|
hexChar | Encode a hex digit |
natToHex | Encode 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
| Type | Description |
|---|---|
Connection | Record with read/write/close callbacks |
Request | Serializable HTTP request |
Response | Parsed status, headers, body |
API
| Function | Description |
|---|---|
connectionFromSocket | TCP connection builder |
connectionFromTLS | TLS connection builder |
Modules
Types– Core typesConnection– Transport abstractionRequest– Request building and serializationResponse– Response parsingRedirect– Redirect following
Files
Hale/HttpClient/Network/HTTP/Client/Types.lean– Connection, Request, ResponseHale/HttpClient/Network/HTTP/Client/Connection.lean– TransportHale/HttpClient/Network/HTTP/Client/Request.lean– RequestHale/HttpClient/Network/HTTP/Client/Response.lean– ResponseHale/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
| Function | Description |
|---|---|
httpSource | Stream response body as conduit source |
parseUrl | Parse URL string (http:// and https://) |
Files
Hale/HttpConduit/Network/HTTP/Client/Conduit.lean– Conduit bridgeHale/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
HttpBodyAllowedprevents GET with bodybasicAuthreturnsOption .Httpsfor 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.respondis the only combinator that performs this transition.- No double-respond: After
respond, the state is.sent. A secondrespondwould needAppM .sent .sent, which does not exist – type error. - No skip-respond: The return type demands
.sentas post-state. Returning without callingrespondwould leave the state at.pending– type error. - No fabrication:
AppM.mkisprivate. 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:
| Theorem | Statement |
|---|---|
idMiddleware_comp_left | id . m = m |
idMiddleware_comp_right | m . id = m |
modifyRequest_id | modifyRequest id = id |
modifyResponse_id | modifyResponse id = id |
ifRequest_false | ifRequest (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)
| Theorem | Source |
|---|---|
status_responseBuilder | Wai.Internal |
status_responseFile | Wai.Internal |
status_responseStream | Wai.Internal |
headers_responseBuilder | Wai.Internal |
headers_responseFile | Wai.Internal |
mapResponseHeaders_id_responseBuilder | Wai.Internal |
mapResponseHeaders_id_responseFile | Wai.Internal |
mapResponseHeaders_id_responseStream | Wai.Internal |
mapResponseStatus_id_responseBuilder | Wai.Internal |
mapResponseStatus_id_responseFile | Wai.Internal |
mapResponseStatus_id_responseStream | Wai.Internal |
Middleware Algebra (5, in Wai.lean)
| Theorem | Source |
|---|---|
idMiddleware_comp_left | Wai |
idMiddleware_comp_right | Wai |
modifyRequest_id | Wai |
modifyResponse_id | Wai |
ifRequest_false | Wai |
Response Linearity (Compile-Time Guarantee)
- Exactly-once response: The indexed
AppMmonad enforces at the type level thatrespondis invoked exactly once.AppM .pending .sent ResponseReceivedcan only be produced viaAppM.respond(which transitions.pending → .sent). Double-respond is a compile-time error: no combinator transitions.sent → .sent. Theprivate mkonAppMprevents circumventing the guarantee.
Files
Hale/WAI/Network/Wai/Internal.lean– Core types + 11 accessor theoremsHale/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:
| Theorem | Statement |
|---|---|
tcp_not_secure | Transport.isSecure .tcp = false |
tls_is_secure | Transport.isSecure (.tls ..) = true |
quic_is_secure | Transport.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 -> closeconnAction_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)
| Theorem | Statement |
|---|---|
tcp_not_secure | TCP is not encrypted |
tls_is_secure | TLS is always encrypted |
quic_is_secure | QUIC is always encrypted |
Keep-Alive Semantics (2, in Run.lean)
| Theorem | Statement |
|---|---|
connAction_http10_default | HTTP/1.0 defaults to close |
connAction_http11_default | HTTP/1.1 defaults to keep-alive |
HTTP Version Parsing (5, in Request.lean)
| Theorem | Statement |
|---|---|
parseHttpVersion_http11 | Parsing “HTTP/1.1” yields http11 |
parseHttpVersion_http10 | Parsing “HTTP/1.0” yields http10 |
parseHttpVersion_http09 | Parsing “HTTP/0.9” yields http09 |
parseHttpVersion_http20 | Parsing “HTTP/2.0” yields http20 |
parseRequestLine_empty | Empty string yields none |
Settings Validity (1, in Settings.lean)
| Theorem | Statement |
|---|---|
defaultSettings_valid | Default 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)
| Module | Purpose |
|---|---|
Types | Connection, Transport, Source, InvalidRequest |
Settings | Server configuration with proof fields |
Request | HTTP request parsing |
Response | HTTP response rendering |
Run | Accept loop + keep-alive state machine |
Date | Cached HTTP date header (AutoUpdate) |
Header | O(1) indexed header lookup (13 headers) |
Counter | Atomic connection counter |
ReadInt / PackInt | Fast integer parsing/rendering |
IO, HashMap, Conduit, SendFile | Internal utilities |
WithApplication | Test helper (ephemeral port) |
Internal | Re-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
| Type | Description |
|---|---|
OnInsecure | denyInsecure | allowInsecure – handling for non-TLS connections |
CertSettings | Certificate source (certFile with paths) |
Files
Hale/WarpTLS/Network/Wai/Handler/WarpTLS.lean– runTLS, TLS settingsHale/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
| Middleware | Proven Properties | Description |
|---|---|---|
methodOverride | – | Override method from _method query param |
methodOverridePost | – | Override method from POST body |
acceptOverride | – | Override Accept from _accept param |
realIp | – | Extract client IP from X-Forwarded-For |
rewrite | – | URL path rewriting rules |
Response Modification
| Middleware | Proven Properties | Description |
|---|---|---|
addHeaders | addHeaders [] = id | Add headers to responses |
stripHeaders | stripHeaders [] = id | Remove headers from responses |
combineHeaders | – | Merge duplicate headers |
gzip | – | Gzip compression (framework) |
streamFile | – | Convert file->stream responses |
Routing and Filtering
| Middleware | Proven Properties | Description |
|---|---|---|
select | select (fun _ => none) = id | Conditional middleware |
routed | routed (fun _ => true) m = m, routed (fun _ => false) = id | Path-based routing |
vhost | – | Virtual host routing |
urlMap | – | URL prefix routing |
Security
| Middleware | Proven Properties | Description |
|---|---|---|
forceSSL | Secure requests pass through | Redirect HTTP->HTTPS |
forceDomain | – | Redirect to canonical domain |
httpAuth | – | HTTP Basic Authentication |
localOnly | – | Restrict to localhost |
requestSizeLimit | – | Reject oversized bodies (413) |
validateHeaders | – | Reject invalid header chars (500) |
Monitoring
| Middleware | Description |
|---|---|
requestLogger | Apache/dev format logging |
requestLogger.json | Structured JSON logging |
healthCheck | Health check endpoint (200 OK) |
timeout | Request timeout (503) |
Protocol
| Middleware | Description |
|---|---|
autohead | HEAD->GET + strip body |
cleanPath | Normalize URL paths (301 redirect) |
approot | Application root detection |
eventSource | Server-Sent Events |
jsonp | JSONP 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)
| Theorem | Statement |
|---|---|
addHeaders_nil_builder | Empty headers on builder = identity |
addHeaders_nil_file | Empty headers on file = identity |
addHeaders_nil_stream | Empty headers on stream = identity |
StripHeaders Identity (3, in StripHeaders.lean)
| Theorem | Statement |
|---|---|
stripHeaders_nil_builder | Empty strip list on builder = identity |
stripHeaders_nil_file | Empty strip list on file = identity |
stripHeaders_nil_stream | Empty strip list on stream = identity |
Select (1, in Select.lean)
| Theorem | Statement |
|---|---|
select_none | Always-none selector = identity middleware |
Routed (2, in Routed.lean)
| Theorem | Statement |
|---|---|
routed_true | Always-true predicate = apply middleware |
routed_false | Always-false predicate = identity middleware |
ForceSSL (1, in ForceSSL.lean)
| Theorem | Statement |
|---|---|
forceSSL_secure | Secure requests pass through unchanged |
HealthCheck (1, in HealthCheckEndpoint.lean)
| Theorem | Statement |
|---|---|
healthCheck_passthrough | Non-matching paths pass through to inner app |
Files (36 modules)
| File | Purpose |
|---|---|
Middleware/AddHeaders.lean | Add headers + 3 identity proofs |
Middleware/StripHeaders.lean | Remove headers + 3 identity proofs |
Middleware/Select.lean | Conditional middleware + 1 proof |
Middleware/Routed.lean | Path-based routing + 2 proofs |
Middleware/ForceSSL.lean | HTTP->HTTPS redirect + 1 proof |
Middleware/HealthCheckEndpoint.lean | Health check + 1 proof |
Middleware/Autohead.lean | HEAD method handling |
Middleware/AcceptOverride.lean | Accept header override |
Middleware/MethodOverride.lean | Method override (query param) |
Middleware/MethodOverridePost.lean | Method override (POST body) |
Middleware/Vhost.lean | Virtual host routing |
Middleware/Timeout.lean | Request timeout |
Middleware/CombineHeaders.lean | Header deduplication |
Middleware/StreamFile.lean | File->stream conversion |
Middleware/Rewrite.lean | URL rewriting |
Middleware/CleanPath.lean | Path normalization |
Middleware/ForceDomain.lean | Domain redirect |
Middleware/Local.lean | Localhost restriction |
Middleware/RealIp.lean | Client IP extraction |
Middleware/HttpAuth.lean | Basic authentication |
Middleware/RequestSizeLimit.lean | Body size limit |
Middleware/ValidateHeaders.lean | Header validation |
Middleware/RequestLogger.lean | Request logging |
Middleware/RequestLogger/JSON.lean | JSON request logging |
Middleware/Gzip.lean | Gzip compression |
Middleware/Approot.lean | Application root |
Middleware/Jsonp.lean | JSONP support |
UrlMap.lean | URL prefix routing |
Header.lean | WAI header utilities |
Request.lean | Request utilities |
Parse.lean | Multipart/form parsing |
EventSource.lean | Server-Sent Events |
EventSource/EventStream.lean | SSE stream types |
Test.lean | WAI test utilities |
Test/Internal.lean | Test internals |
Middleware/RequestSizeLimit/Internal.lean | Size 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):
| Theorem | Statement |
|---|---|
empty_piece_valid | toPiece "" = some _ |
toPiece_rejects_dot | toPiece ".hidden" = none |
toPiece_rejects_slash | toPiece "a/b" = none |
toPiece_accepts_simple | toPiece "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 proofsWaiAppStatic/Storage/Filesystem.lean– Filesystem backendNetwork/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
| Type | Description |
|---|---|
PushSettings | Middleware configuration |
API
| Function | Description |
|---|---|
pushOnReferer | Create push middleware |
Modules
Types– PushSettings, configurationLRU– LRU eviction cacheManager– Thread-safe push tableParseURL– URL parsing for same-origin checksReferer– Main middleware entry point
Files
Hale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer.lean– Main middlewareHale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/Types.lean– ConfigurationHale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/LRU.lean– LRU cacheHale/WaiHttp2Extra/Network/Wai/Middleware/Push/Referer/Manager.lean– Push managerHale/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
| Function | Description |
|---|---|
isWebSocketsReq | Check if request is a WebSocket upgrade |
websocketsApp | Upgrade 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:
| Theorem | Statement |
|---|---|
opcode_roundtrip_text | fromUInt8 (toUInt8 .text) = .text |
opcode_roundtrip_binary | fromUInt8 (toUInt8 .binary) = .binary |
opcode_roundtrip_close | fromUInt8 (toUInt8 .close) = .close |
opcode_roundtrip_ping | fromUInt8 (toUInt8 .ping) = .ping |
opcode_roundtrip_pong | fromUInt8 (toUInt8 .pong) = .pong |
opcode_roundtrip_continuation | fromUInt8 (toUInt8 .continuation) = .continuation |
Implementation Notes
- XOR masking:
applyMaskapplies the 4-byte masking key via XOR. XOR is its own inverse, so the same function masks and unmasks. - Auto-pong: When a
pingcontrol frame is received duringreceiveData, the connection automatically replies with apongframe (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 proofsFrame.lean– Frame encode/decode + XOR maskingHandshake.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
| Type | Description |
|---|---|
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
| Type | Description |
|---|---|
Key α | Typed key (globally unique) |
Vault | Heterogeneous map |
Axiom-Dependent Properties
- Type safety of
lookupdepends on axiom thatunsafeCastis 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
tickleresets timeout – O(1)cancelprevents future firing- Thread-safe via
IO.Refatomicity
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
| Type | Description |
|---|---|
CookiePair | String × String (name, value) |
API
| Function | Signature |
|---|---|
parseCookies | String → 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
| Type | Description |
|---|---|
MimeType | String |
MimeMap | List (Extension × MimeType) |
API
| Function | Description |
|---|---|
defaultMimeLookup | Look up MIME type by filename |
defaultMimeMap | Built-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
encodecontains only[A-Za-z0-9+/=]
API
| Function | Signature |
|---|---|
encode | ByteArray → String |
decode | String → 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
| Type | Description |
|---|---|
LogType | stdout | stderr | file | callback |
LogStr | String (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
| Function | Signature |
|---|---|
apacheFormat | Request → 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
| Type | Description |
|---|---|
Fd | File descriptor (UInt32) |
FileStatus | File size and type |
API
| Function | Description |
|---|---|
closeFd | Close 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
| Type | Description |
|---|---|
Color | black | red | green | yellow | blue | magenta | cyan | white |
Intensity | bold | normal |
API
| Function | Description |
|---|---|
reset | Reset all attributes |
setFg | Set foreground color |
setBg | Set 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
| Module | Description |
|---|---|
Internal.Types | Core DataFrame/Column types |
Internal.Column | Column operations |
Operations.Subset | Row/column selection |
Operations.Sort | Sorting |
Operations.Aggregation | Group-by and aggregation |
Operations.Join | Inner/outer joins |
Operations.Statistics | Mean, std, quantiles |
Operations.Transform | Map, filter, apply |
IO.CSV | CSV read/write |
Display | Pretty-printing |
Files
Hale/DataFrame/DataFrame.lean– Re-exportsHale/DataFrame/DataFrame/Internal/Types.lean– Core typesHale/DataFrame/DataFrame/Internal/Column.lean– Column operationsHale/DataFrame/DataFrame/Operations/*.lean– OperationsHale/DataFrame/DataFrame/IO/CSV.lean– CSV I/OHale/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.
Quick Links
Core Infrastructure
- Hale.Base — Foundational types, functors, monads
- Hale.ByteString — Byte array operations
- Hale.Network — POSIX sockets with phantom state
- Hale.HttpTypes — HTTP methods, status, headers, URI
Web Application Interface
- Hale.WAI — Request/Response/Application/Middleware
- Hale.Warp — HTTP/1.x server
- Hale.WarpTLS — HTTPS via OpenSSL
- Hale.WaiExtra — 36 middleware modules
- Hale.WaiAppStatic — Static file serving
- Hale.WebSockets — RFC 6455 WebSocket protocol
Protocol Implementations
- Hale.Http2 — HTTP/2 framing (RFC 9113)
- Hale.Http3 — HTTP/3 framing + QPACK
- Hale.QUIC — QUIC transport
- Hale.TLS — OpenSSL FFI wrapper
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)
| Theorem | Module |
|---|---|
Dual.append_assoc | Base/Data/Newtype |
Endo.append_assoc | Base/Data/Newtype |
First.append_assoc | Base/Data/Newtype |
Last.append_assoc | Base/Data/Newtype |
Sum.append_assoc | Base/Data/Newtype |
Product.append_assoc | Base/Data/Newtype |
All.append_assoc | Base/Data/Newtype |
Any.append_assoc | Base/Data/Newtype |
Functor Laws (14)
| Theorem | Module |
|---|---|
Identity.map_id | Base/Data/Functor/Identity |
Identity.map_comp | Base/Data/Functor/Identity |
Const.map_id | Base/Data/Functor/Const |
Const.map_comp | Base/Data/Functor/Const |
Const.map_val | Base/Data/Functor/Const |
Proxy.map_id | Base/Data/Proxy |
Proxy.map_comp | Base/Data/Proxy |
Either.map_id | Base/Data/Either |
Either.map_comp | Base/Data/Either |
Compose.map_id | Base/Data/Functor/Compose |
Compose.map_comp | Base/Data/Functor/Compose |
Product.map_id | Base/Data/Functor/Product |
Product.map_comp | Base/Data/Functor/Product |
Sum.map_id | Base/Data/Functor/Sum |
Sum.map_comp | Base/Data/Functor/Sum |
Monad Laws (9)
| Theorem | Module |
|---|---|
Identity.pure_bind | Base/Data/Functor/Identity |
Identity.bind_pure | Base/Data/Functor/Identity |
Identity.bind_assoc | Base/Data/Functor/Identity |
Proxy.pure_bind | Base/Data/Proxy |
Proxy.bind_pure | Base/Data/Proxy |
Proxy.bind_assoc | Base/Data/Proxy |
Either.pure_bind | Base/Data/Either |
Either.bind_pure | Base/Data/Either |
Either.bind_assoc | Base/Data/Either |
Monad Combinators (1)
| Theorem | Module |
|---|---|
join_pure | Base/Control/Monad |
Data Structure Properties
Either (3)
| Theorem | Module |
|---|---|
swap_swap | Base/Data/Either |
isLeft_not_isRight | Base/Data/Either |
partitionEithers_length | Base/Data/Either |
Maybe/Option (8)
| Theorem | Module |
|---|---|
maybe_none | Base/Data/Maybe |
maybe_some | Base/Data/Maybe |
fromMaybe_none | Base/Data/Maybe |
fromMaybe_some | Base/Data/Maybe |
catMaybes_nil | Base/Data/Maybe |
mapMaybe_nil | Base/Data/Maybe |
maybeToList_listToMaybe | Base/Data/Maybe |
catMaybes_eq_mapMaybe_id | Base/Data/Maybe |
Tuple (7)
| Theorem | Module |
|---|---|
swap_swap | Base/Data/Tuple |
curry_uncurry | Base/Data/Tuple |
uncurry_curry | Base/Data/Tuple |
bimap_id | Base/Data/Tuple |
bimap_comp | Base/Data/Tuple |
mapFst_eq_bimap | Base/Data/Tuple |
mapSnd_eq_bimap | Base/Data/Tuple |
List (4)
| Theorem | Module |
|---|---|
tails_length | Base/Data/List |
inits_length | Base/Data/List |
tails_nil | Base/Data/List |
inits_nil | Base/Data/List |
NonEmpty (4)
| Theorem | Module |
|---|---|
toList_ne_nil | Base/Data/List/NonEmpty |
reverse_length | Base/Data/List/NonEmpty |
toList_length | Base/Data/List/NonEmpty |
map_length | Base/Data/List/NonEmpty |
Ord/Down (2)
| Theorem | Module |
|---|---|
get_mk | Base/Data/Ord |
compare_reverse | Base/Data/Ord |
Bool (5)
| Theorem | Module |
|---|---|
bool_false | Base/Data/Bool |
bool_true | Base/Data/Bool |
guard'_true | Base/Data/Bool |
guard'_false | Base/Data/Bool |
bool_ite | Base/Data/Bool |
Void (1)
| Theorem | Module |
|---|---|
eq_absurd | Base/Data/Void |
Function (5)
| Theorem | Module |
|---|---|
on_apply | Base/Data/Function |
applyTo_apply | Base/Data/Function |
const_apply | Base/Data/Function |
flip_flip | Base/Data/Function |
flip_apply | Base/Data/Function |
Foldable (2)
| Theorem | Module |
|---|---|
foldr_nil | Base/Data/Foldable |
foldl_nil | Base/Data/Foldable |
String (1)
| Theorem | Module |
|---|---|
unwords_nil | Base/Data/String |
Char (3)
| Theorem | Module |
|---|---|
isAlphaNum_iff | Base/Data/Char |
isSpace_eq_isWhitespace | Base/Data/Char |
ord_eq_toNat | Base/Data/Char |
Ix (1)
| Theorem | Module |
|---|---|
Ix.inRange_iff_index_isSome_nat | Base/Data/Ix |
ExitCode (2)
| Theorem | Module |
|---|---|
success_toUInt32 | Base/System/Exit |
isSuccess_iff | Base/System/Exit |
Numeric Type Properties
Complex (2)
| Theorem | Module |
|---|---|
conjugate_conjugate | Base/Data/Complex |
add_comm' | Base/Data/Complex |
Fixed-Point (5)
| Theorem | Module |
|---|---|
scale_pos | Base/Data/Fixed |
add_exact | Base/Data/Fixed |
sub_exact | Base/Data/Fixed |
neg_neg | Base/Data/Fixed |
fromInt_zero | Base/Data/Fixed |
Time (2)
| Theorem | Module |
|---|---|
fromSeconds_toSeconds | Time/Data/Time/Clock |
diffUTCTime_self | Time/Data/Time/Clock |
Protocol Correctness
HTTP Method Properties (21, in HttpTypes/Network/HTTP/Types/Method.lean)
| Theorem | What it proves |
|---|---|
parseMethod_GET | Parsing “GET” yields .standard .GET |
parseMethod_POST | Parsing “POST” yields .standard .POST |
parseMethod_HEAD | Parsing “HEAD” yields .standard .HEAD |
parseMethod_PUT | Parsing “PUT” yields .standard .PUT |
parseMethod_DELETE | Parsing “DELETE” yields .standard .DELETE |
parseMethod_TRACE | Parsing “TRACE” yields .standard .TRACE |
parseMethod_CONNECT | Parsing “CONNECT” yields .standard .CONNECT |
parseMethod_OPTIONS | Parsing “OPTIONS” yields .standard .OPTIONS |
parseMethod_PATCH | Parsing “PATCH” yields .standard .PATCH |
parseMethod_custom | Non-standard strings yield .custom |
Method.get_is_safe | GET is a safe method |
Method.head_is_safe | HEAD is a safe method |
Method.options_is_safe | OPTIONS is a safe method |
Method.trace_is_safe | TRACE is a safe method |
Method.post_not_safe | POST is not safe |
Method.patch_not_safe | PATCH is not safe |
Method.put_is_idempotent | PUT is idempotent |
Method.delete_is_idempotent | DELETE is idempotent |
Method.post_not_idempotent | POST is not idempotent |
Method.patch_not_idempotent | PATCH is not idempotent |
Method.safe_implies_idempotent | All safe methods are idempotent |
HTTP Status Properties (13, in HttpTypes/Network/HTTP/Types/Status.lean)
| Theorem | What it proves |
|---|---|
status200_valid | 200 is in valid range [100, 599] |
status404_valid | 404 is in valid range |
status500_valid | 500 is in valid range |
status100_valid | 100 is in valid range |
status301_valid | 301 is in valid range |
status100_no_body | 100 must not have body |
status101_no_body | 101 must not have body |
status204_no_body | 204 must not have body |
status304_no_body | 304 must not have body |
status200_may_have_body | 200 may have body |
status201_may_have_body | 201 may have body |
status404_may_have_body | 404 may have body |
status500_may_have_body | 500 may have body |
HTTP Version Properties (4, in HttpTypes/Network/HTTP/Types/Version.lean)
| Theorem | What it proves |
|---|---|
http09_valid | HTTP/0.9 has major=0, minor=9 |
http10_valid | HTTP/1.0 has major=1, minor=0 |
http11_valid | HTTP/1.1 has major=1, minor=1 |
http20_valid | HTTP/2.0 has major=2, minor=0 |
Transport Security (3, in Warp/Types.lean)
| Theorem | What it proves |
|---|---|
tcp_not_secure | TCP is not encrypted |
tls_is_secure | TLS is always encrypted |
quic_is_secure | QUIC is always encrypted |
Keep-Alive Semantics (2, in Warp/Run.lean)
| Theorem | What it proves |
|---|---|
connAction_http10_default | HTTP/1.0 defaults to close |
connAction_http11_default | HTTP/1.1 defaults to keep-alive |
HTTP Version Parsing (5, in Warp/Request.lean)
| Theorem | What 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)
| Theorem | What it proves |
|---|---|
defaultSettings_valid | Default timeout > 0 and backlog > 0 |
Encoding/Decoding Roundtrips
HTTP/2 Frame Types (10, in Http2/Network/HTTP2/Frame/Types.lean)
| Theorem | Frame Type |
|---|---|
fromUInt8_toUInt8_data | DATA |
fromUInt8_toUInt8_headers | HEADERS |
fromUInt8_toUInt8_priority | PRIORITY |
fromUInt8_toUInt8_rstStream | RST_STREAM |
fromUInt8_toUInt8_settings | SETTINGS |
fromUInt8_toUInt8_pushPromise | PUSH_PROMISE |
fromUInt8_toUInt8_ping | PING |
fromUInt8_toUInt8_goaway | GOAWAY |
fromUInt8_toUInt8_windowUpdate | WINDOW_UPDATE |
fromUInt8_toUInt8_continuation | CONTINUATION |
HTTP/3 Frame Types (7, in Http3/Network/HTTP3/Frame.lean)
| Theorem | Frame Type |
|---|---|
FrameType.roundtrip_data | DATA |
FrameType.roundtrip_headers | HEADERS |
FrameType.roundtrip_cancelPush | CANCEL_PUSH |
FrameType.roundtrip_settings | SETTINGS |
FrameType.roundtrip_pushPromise | PUSH_PROMISE |
FrameType.roundtrip_goaway | GOAWAY |
FrameType.roundtrip_maxPushId | MAX_PUSH_ID |
HTTP/3 Error Codes (17, in Http3/Network/HTTP3/Error.lean)
| Theorem | Error Code |
|---|---|
H3Error.roundtrip_noError | H3_NO_ERROR |
H3Error.roundtrip_generalProtocolError | H3_GENERAL_PROTOCOL_ERROR |
H3Error.roundtrip_internalError | H3_INTERNAL_ERROR |
H3Error.roundtrip_streamCreationError | H3_STREAM_CREATION_ERROR |
H3Error.roundtrip_closedCriticalStream | H3_CLOSED_CRITICAL_STREAM |
H3Error.roundtrip_frameUnexpected | H3_FRAME_UNEXPECTED |
H3Error.roundtrip_frameError | H3_FRAME_ERROR |
H3Error.roundtrip_excessiveLoad | H3_EXCESSIVE_LOAD |
H3Error.roundtrip_idError | H3_ID_ERROR |
H3Error.roundtrip_settingsError | H3_SETTINGS_ERROR |
H3Error.roundtrip_missingSettings | H3_MISSING_SETTINGS |
H3Error.roundtrip_requestRejected | H3_REQUEST_REJECTED |
H3Error.roundtrip_requestCancelled | H3_REQUEST_CANCELLED |
H3Error.roundtrip_requestIncomplete | H3_REQUEST_INCOMPLETE |
H3Error.roundtrip_messageError | H3_MESSAGE_ERROR |
H3Error.roundtrip_connectError | H3_CONNECT_ERROR |
H3Error.roundtrip_versionFallback | H3_VERSION_FALLBACK |
WebSocket Opcodes (6, in WebSockets/Types.lean)
| Theorem | Opcode |
|---|---|
opcode_roundtrip_text | TEXT |
opcode_roundtrip_binary | BINARY |
opcode_roundtrip_close | CLOSE |
opcode_roundtrip_ping | PING |
opcode_roundtrip_pong | PONG |
opcode_roundtrip_continuation | CONTINUATION |
ByteString Properties
ByteString Invariants (3, in ByteString/Internal.lean)
| Theorem | What it proves |
|---|---|
take_valid | take result has valid offset/length |
drop_valid | drop result has valid offset/length |
null_iff_length_zero | null bs <-> length bs = 0 |
Builder Monoid (3, in ByteString/Builder.lean)
| Theorem | What it proves |
|---|---|
empty_append | empty ++ b = b (left identity) |
append_empty | b ++ empty = b (right identity) |
append_assoc | (a ++ b) ++ c = a ++ (b ++ c) (associativity) |
Short ByteString (1, in ByteString/Short.lean)
| Theorem | What it proves |
|---|---|
length_toShort | toShort preserves length |
Word8 Properties (4, in Word8/Data/Word8.lean)
| Theorem | What it proves |
|---|---|
toLower_idempotent | toLower . toLower = toLower |
toUpper_idempotent | toUpper . toUpper = toUpper |
isUpper_toLower | Upper -> toLower -> isLower |
isLower_toUpper | Lower -> toUpper -> isUpper |
CaseInsensitive (1, in CaseInsensitive/Data/CaseInsensitive.lean)
| Theorem | What it proves |
|---|---|
ci_eq_iff | CI equality is by foldedCase |
Socket State (11, in Network/Socket/Types.lean)
| Theorem | What it proves |
|---|---|
SocketState.fresh_ne_bound | fresh /= bound |
SocketState.fresh_ne_listening | fresh /= listening |
SocketState.fresh_ne_connected | fresh /= connected |
SocketState.fresh_ne_closed | fresh /= closed |
SocketState.bound_ne_listening | bound /= listening |
SocketState.bound_ne_connected | bound /= connected |
SocketState.bound_ne_closed | bound /= closed |
SocketState.listening_ne_connected | listening /= connected |
SocketState.listening_ne_closed | listening /= closed |
SocketState.connected_ne_closed | connected /= closed |
SocketState.beq_refl | s == s = true |
Response Lifecycle (1, in WAI/Network/Wai/Internal.lean)
| Theorem | What it proves |
|---|---|
ResponseState.pending_ne_sent | pending /= sent |
Path Safety (4, in WaiAppStatic/Types.lean)
| Theorem | What it proves |
|---|---|
empty_piece_valid | Empty string is a valid piece |
toPiece_rejects_dot | Dotfiles rejected at construction |
toPiece_rejects_slash | Path traversal rejected at construction |
toPiece_accepts_simple | Normal filenames accepted |
WAI Response Laws (11, in WAI/Internal.lean)
| Theorem | What it proves |
|---|---|
status_responseBuilder | Status accessor on builder |
status_responseFile | Status accessor on file |
status_responseStream | Status accessor on stream |
headers_responseBuilder | Headers accessor on builder |
headers_responseFile | Headers accessor on file |
mapResponseHeaders_id_responseBuilder | mapHeaders id on builder = id |
mapResponseHeaders_id_responseFile | mapHeaders id on file = id |
mapResponseHeaders_id_responseStream | mapHeaders id on stream = id |
mapResponseStatus_id_responseBuilder | mapStatus id on builder = id |
mapResponseStatus_id_responseFile | mapStatus id on file = id |
mapResponseStatus_id_responseStream | mapStatus id on stream = id |
WAI Middleware Algebra (5, in WAI/Wai.lean)
| Theorem | What it proves |
|---|---|
idMiddleware_comp_left | id . m = m |
idMiddleware_comp_right | m . id = m |
modifyRequest_id | modifyRequest id = id |
modifyResponse_id | modifyResponse id = id |
ifRequest_false | ifRequest (always false) m = id |
Middleware Properties (11)
AddHeaders (3, in WaiExtra/AddHeaders.lean)
| Theorem | What it proves |
|---|---|
addHeaders_nil_builder | Empty headers on builder = identity |
addHeaders_nil_file | Empty headers on file = identity |
addHeaders_nil_stream | Empty headers on stream = identity |
StripHeaders (3, in WaiExtra/StripHeaders.lean)
| Theorem | What it proves |
|---|---|
stripHeaders_nil_builder | Empty strip list on builder = identity |
stripHeaders_nil_file | Empty strip list on file = identity |
stripHeaders_nil_stream | Empty strip list on stream = identity |
Select (1, in WaiExtra/Select.lean)
| Theorem | What it proves |
|---|---|
select_none | Always-none = identity |
Routed (2, in WaiExtra/Routed.lean)
| Theorem | What it proves |
|---|---|
routed_true | Always-true = apply middleware |
routed_false | Always-false = identity |
ForceSSL (1, in WaiExtra/ForceSSL.lean)
| Theorem | What it proves |
|---|---|
forceSSL_secure | Secure requests pass through |
HealthCheck (1, in WaiExtra/HealthCheckEndpoint.lean)
| Theorem | What it proves |
|---|---|
healthCheck_passthrough | Non-matching paths pass through |
ResourceT (1, in ResourceT/Resource.lean)
| Theorem | What it proves |
|---|---|
releaseKey_eq | a = b <-> a.id = b.id |
Involution/Self-Inverse Properties (Summary)
| Theorem | Module |
|---|---|
swap_swap (Tuple) | Base/Data/Tuple |
swap_swap (Either) | Base/Data/Either |
conjugate_conjugate | Base/Data/Complex |
flip_flip | Base/Data/Function |
curry_uncurry | Base/Data/Tuple |
neg_neg (Fixed) | Base/Data/Fixed |
toLower_idempotent | Word8/Data/Word8 |
toUpper_idempotent | Word8/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:
| Structure | Proof Field | Invariant |
|---|---|---|
Ratio | den_pos | Denominator > 0 |
Ratio | coprime | Num and den are coprime |
Piece | no_dot | No leading dot |
Piece | no_slash | No embedded slash |
Settings | settingsTimeoutPos | Timeout > 0 |
Settings | settingsBacklogPos | Backlog > 0 |
Socket | state (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:
- Runtime checks — assert the state at every call, throw on violation (Python, Java, Go).
- Documentation — trust the programmer to read the man page (C, Rust).
- Ignore it — let the OS return
EBADFand 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
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:
| Call | Proof obligation | by decide | Result |
|---|---|---|---|
close (s : Socket .fresh) | .fresh ≠ .closed | trivially true | compiles |
close (s : Socket .bound) | .bound ≠ .closed | trivially true | compiles |
close (s : Socket .listening) | .listening ≠ .closed | trivially true | compiles |
close (s : Socket .connected) | .connected ≠ .closed | trivially true | compiles |
close (s : Socket .closed) | .closed ≠ .closed | impossible | type 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
connectreturnsEINPROGRESSwhile the TCP handshake is in flight. The socket is neither.freshnor.connected— it is connecting. A real non-blocking API would need a.connectingstate and a resolution step (pollConnect). - Peer disconnect can happen at any time. A
Socket .connectedmay be broken underneath; you only discover this whensend/recvreturns an error. The type guarantees the call is legal to attempt, not that it will succeed — that is whatIOencodes. - 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
| Approach | Lines of state-checking code | Runtime cost | Catches at |
|---|---|---|---|
| C (man page) | 0 | 0 | never (silent error) |
| Python (runtime) | ~50 | branch per call | runtime |
| Rust (typestate) | ~30 | 0 | compile-time |
| Lean 4 (dependent types) | 0 | 0 | compile-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
| Category | Theorems | RFC/Standard |
|---|---|---|
| Method semantics | 21 | RFC 9110 §9.2 |
| Status code properties | 13 | RFC 9110 §6.4.1, §15 |
| Status code ranges | 5 | RFC 9110 §15 |
| HTTP version parsing | 4 | RFC 9110 |
| Method parsing roundtrips | 10 | RFC 9110 |
| Transport security | 3 | TLS 1.2+/QUIC |
| Keep-alive semantics | 2 | RFC 9112 §9.3 |
| Response exactly-once | (type-level) | WAI contract |
| H2 frame roundtrips | 10 | RFC 9113 §6 |
| H3 frame roundtrips | 7 | RFC 9114 |
| H3 error roundtrips | 17 | RFC 9114 |
| WS opcode roundtrips | 6 | RFC 6455 §5.2 |
| Middleware algebra | 5 | WAI composition |
| Response accessor laws | 11 | WAI 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
| Technique | What it encodes | Runtime cost | Example |
|---|---|---|---|
| Proof fields | Structural invariants | 0 (erased) | Ratio.den_pos, ConnectionId.hLen |
| Phantom params | State machines | 0 (erased) | Socket (state : SocketState) |
| Indexed monads | Exactly-once protocols | 0 (erased) | AppM .pending .sent |
| Inductive types | Message/frame/state spaces | 0 (tag only) | StreamState, FrameType |
| Roundtrip thms | Wire-format correctness | 0 (erased) | 46 bijectivity proofs |
| Algebraic laws | Composition safety | 0 (erased) | id ∘ m = m, monoid laws |
| Opaque FFI | Resource encapsulation | 0 (pointer) | SocketHandle, TLSContextHandle |
| Axioms | FFI trust boundary | 0 (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.