State invariants
A type can say which fields state contains. An invariant can say which
combinations are valid. This example proves 0 <= reserved <= capacity
for every state its GenServer can construct through Gale.
A quota state whose reserved amount is always between zero and capacity.
Every constructor and transition is checked by F*/Z3 during compilation. Outside this module, the opaque State can only be obtained from these verified functions. A GenServer can therefore store State without exposing its fields.
module invariants.quota_state
@moduledoc """A quota state whose reserved amount is always between zero and capacity.
Every constructor and transition is checked by F*/Z3 during compilation.Outside this module, the opaque State can only be obtained from these verifiedfunctions. A GenServer can therefore store State without exposing its fields."""
pub opaque type State = { reserved: Integer, capacity: Integer }invariant state { state.reserved >= 0 and state.reserved <= state.capacity}
pub fn new(capacity: Integer) -> Result<State, :invalid> { if capacity >= 0 { Ok({reserved: 0, capacity: capacity}) } else { Error(:invalid) }}
fn add_nonnegative(a: Integer, b: Integer) -> Integerrequires { b >= 0 }ensures result { result == a + b } { a + b}
pub fn reserve(state: State, amount: Integer) -> Result<State, :invalid> { if amount >= 0 and amount <= state.capacity - state.reserved { Ok(state {reserved: add_nonnegative(state.reserved, amount)}) } else { Error(:invalid) }}
pub fn release(state: State, amount: Integer) -> Result<State, :invalid> { if amount >= 0 and amount <= state.reserved { Ok(state {reserved: state.reserved - amount}) } else { Error(:invalid) }}
pub fn available(state: State) -> Integerensures result { result >= 0 } { state.capacity - state.reserved}A GenServer whose state carries a compile-time quota invariant.
Callbacks handle messages and replies; quota_state owns the pure transitions. Rejected reservations return an error and keep the previous valid state. This proves a property of the state, not delivery, liveness, or crash freedom.
module invariants.quota
@moduledoc """A GenServer whose state carries a compile-time quota invariant.
Callbacks handle messages and replies; quota_state owns the pure transitions.Rejected reservations return an error and keep the previous valid state.This proves a property of the state, not delivery, liveness, or crash freedom."""
implements gale_std.gen_server.Server< Integer, invariants.quota_state.State, Request, Never, Never, Never> as service
pub type Request = | Available(reply_to: gale_std.gen_server.ReplyTo<Integer>) | Reserve(amount: Integer, reply_to: gale_std.gen_server.ReplyTo<Result<Integer, :invalid>>) | Release(amount: Integer, reply_to: gale_std.gen_server.ReplyTo<Result<Integer, :invalid>>)
pub type Service = servicepub type Target = gale_std.gen_server.ServerTarget<Service>
pub fn start_link(capacity: Integer) -> gale_std.gen_server.StartResult<Service> { gale_std.gen_server.start_link(invariants.quota, capacity, [])}
pub fn available(server: Target) -> Integer { gale_std.gen_server.call(server, fn(reply_to) => Available(reply_to))}
pub fn reserve(server: Target, amount: Integer) -> Result<Integer, :invalid> { gale_std.gen_server.call(server, fn(reply_to) => Reserve(amount, reply_to))}
pub fn release(server: Target, amount: Integer) -> Result<Integer, :invalid> { gale_std.gen_server.call(server, fn(reply_to) => Release(amount, reply_to))}
pub fn init(capacity: Integer) -> gale_std.otp.Init<invariants.quota_state.State, Never> { match invariants.quota_state.new(capacity) { Ok(state) -> (:ok, state) Error(reason) -> (:stop, reason) }}
fn finish( old: invariants.quota_state.State, next: Result<invariants.quota_state.State, :invalid>, reply_to: gale_std.gen_server.ReplyTo<Result<Integer, :invalid>>) -> gale_std.otp.Next<invariants.quota_state.State, Never> { match next { Ok(state) -> { let _ = gale_std.gen_server.reply(reply_to, Ok(invariants.quota_state.available(state))) let result: gale_std.otp.Next<invariants.quota_state.State, Never> = (:noreply, state) result } Error(reason) -> { let _ = gale_std.gen_server.reply(reply_to, Error(reason)) let result: gale_std.otp.Next<invariants.quota_state.State, Never> = (:noreply, old) result } }}
pub fn handle_call<R>( build: fn(gale_std.gen_server.ReplyTo<R>) -> Request, from: gale_std.gen_server.ReplyTo<R>, state: invariants.quota_state.State) -> gale_std.otp.Next<invariants.quota_state.State, Never> { match build(from) { Available(reply_to) -> { let _ = gale_std.gen_server.reply(reply_to, invariants.quota_state.available(state)) let result: gale_std.otp.Next<invariants.quota_state.State, Never> = (:noreply, state) result } Reserve(amount, reply_to) -> finish(state, invariants.quota_state.reserve(state, amount), reply_to) Release(amount, reply_to) -> finish(state, invariants.quota_state.release(state, amount), reply_to) }}Remove the capacity check in reserve and compilation fails. Add another
ordinary state factory and its result is checked automatically. Construct a raw record in the
callback and return it as quota_state.State, and ordinary typechecking fails.
The state guarantee therefore survives the boundary between verified pure code
and an effectful OTP callback.
Calls may still time out, processes may crash, and foreign code must respect Galeβs typed interface. The proof does not establish liveness, message delivery, or the correctness of handwritten Elixir that bypasses the opaque API.
Proofs cover ordinary functions and effects, including supported recursive helpers. See the proof reference for the supported subset and trust boundary.
Building on dependency contracts
Section titled βBuilding on dependency contractsβThe same project contains a bounded queue. Its invariant observes native list
length with builtin.length. Enqueue calls gale_std append using its declared
length contract, and reverse uses its length-preservation contract. The application
build checks the state transitions against those library interfaces; the native
implementations remain trusted externs.
A bounded queue verified against standard-library contracts.
module invariants.queue_state
@moduledoc """A bounded queue verified against standard-library contracts."""
pub opaque type State = { items: List<Integer>, capacity: Integer }invariant state { builtin.length(state.items) <= state.capacity}
pub fn new(capacity: Integer) -> Result<State, :invalid> { if capacity >= 0 { Ok({items: [], capacity: capacity}) } else { Error(:invalid) }}
pub fn enqueue(state: State, value: Integer) -> Result<State, :full> { if builtin.length(state.items) < state.capacity { Ok(state {items: gale_std.list.append(state.items, [value])}) } else { Error(:full) }}
pub fn reverse(state: State) -> State { state {items: gale_std.list.reverse(state.items)}}
pub fn size(state: State) -> Integerensures result { result >= 0 } { builtin.length(state.items)}
pub fn first(state: State) -> Option<Integer> { gale_std.list.first(state.items)}
pub fn enqueue_and_notify( state: State, value: Integer, observer: gale_std.process.Pid<Integer>) -> Result<State, :full> { match enqueue(state, value) { Ok(next) -> { let _ = gale_std.process.send(observer, size(next)) Ok(next) } Error(reason) -> Error(reason) }}
pub fn enqueue_and_reply( state: State, value: Integer, reply_to: gale_std.gen_server.ReplyTo<Result<Integer, :full>>) -> Stateensures result { result.capacity == state.capacity } { match enqueue(state, value) { Ok(next) -> { let _ = gale_std.gen_server.reply(reply_to, Ok(size(next))) next } Error(reason) -> { let _ = gale_std.gen_server.reply(reply_to, Error(reason)) state } }}enqueue_and_notify and enqueue_and_reply perform real messaging in the same
module as the invariant. Their returned states are verified without a purity
modifier. The compiler models send attempts and normal return values; it does
not infer delivery or eventual response from those attempts.
Changing < state.capacity to <= state.capacity in enqueue fails verification:
the additional element could exceed capacity. This also works when the helper
library is a separate Gale dependency with its own transitive dependencies.
Bounded job state
Section titled βBounded job stateβThe transition model can also track queued work, an active attempt, and terminal accounting. These ordinary functions are shared by a verified receive loop and a GenServer. The hosts import the same opaque state type and use the same transitions.
A bounded job lifecycle with attempt IDs and verified accounting.
Stale completions, cancellations, and failure reports preserve the entire state. Each current attempt reaches at most one terminal accounting transition. Attempt IDs belong to one initialized state lifetime; restarting begins again.
module invariants.job_state
@moduledoc """A bounded job lifecycle with attempt IDs and verified accounting.
Stale completions, cancellations, and failure reports preserve the entire state.Each current attempt reaches at most one terminal accounting transition.Attempt IDs belong to one initialized state lifetime; restarting begins again."""
pub type Phase<A> = Idle | Running(id: Integer, payload: A) | Closedpub type Worker = gale_std.process.Pid<(Integer, Integer)>
pub type Event = Completed(Integer) | Cancelled(Integer) | Failed(Integer)
fn active(phase: Phase<Integer>) -> Integerensures result { result >= 0 and result <= 1 } { match phase { Running(payload: _) -> 1 Idle -> 0 Closed -> 0 }}
pub opaque type State = { phase: Phase<Integer>, pending: List<Integer>, capacity: Integer, next_id: Integer, completed: Integer, cancelled: Integer, failed: Integer, worker: Worker}invariant state { state.capacity > 0 and builtin.length(state.pending) + active(state.phase) <= state.capacity and state.completed >= 0 and state.cancelled >= 0 and state.failed >= 0 and state.next_id == state.completed + state.cancelled + state.failed + active(state.phase) and match state.phase { Running(id, _) -> id >= 0 and id == state.next_id - 1 Idle -> true Closed -> state.pending == [] }}
pub type Snapshot = { load: Integer, completed: Integer, cancelled: Integer, failed: Integer, current: Option<Integer>, closed: Boolean}
pub fn new(capacity: Integer, worker: Worker) -> Result<State, :invalid>ensures result { match result { Ok(state) -> state.capacity == capacity and state.worker == worker and state.phase == Idle and state.pending == [] and state.next_id == 0 and state.completed == 0 and state.cancelled == 0 and state.failed == 0 Error(_) -> capacity <= 0 }} { if capacity > 0 { Ok({phase: Idle, pending: [], capacity: capacity, next_id: 0, completed: 0, cancelled: 0, failed: 0, worker: worker}) } else { Error(:invalid) }}
pub fn is_closed(state: State) -> Boolean { match state.phase { Closed -> true _ -> false }}
pub fn load(state: State) -> Integerensures result { result >= 0 and result <= state.capacity } { builtin.length(state.pending) + active(state.phase)}
fn current(state: State, id: Integer) -> Boolean { match state.phase { Running(id: attempt) -> attempt == id _ -> false }}
pub fn snapshot(state: State) -> Snapshot { {load: load(state), completed: state.completed, cancelled: state.cancelled, failed: state.failed, current: match state.phase { Running(payload: _, id: id) -> Some(id) _ -> None }, closed: is_closed(state)}}
pub fn submit(state: State, payload: Integer) -> Result<State, :rejected>ensures result { match result { Ok(next) -> load(next) == load(state) + 1 and next.phase == state.phase and next.completed == state.completed and next.cancelled == state.cancelled and next.failed == state.failed and next.next_id == state.next_id and next.capacity == state.capacity and next.worker == state.worker Error(_) -> is_closed(state) or load(state) >= state.capacity }} { if is_closed(state) or load(state) >= state.capacity { Error(:rejected) } else { Ok(state {pending: gale_std.list.append(state.pending, [payload])}) }}
pub fn start_next(state: State) -> Stateensures result { load(result) == load(state) and result.completed == state.completed and result.cancelled == state.cancelled and result.failed == state.failed and result.capacity == state.capacity and result.worker == state.worker and match state.phase { Idle -> match state.pending { [] -> result == state [_ | _] -> current(result, state.next_id) and result.next_id == state.next_id + 1 } _ -> result == state }} { match state.phase { Idle -> match state.pending { [] -> state [payload | rest] -> { let next: State = state { phase: Running(state.next_id, payload), pending: rest, next_id: state.next_id + 1 } let _ = gale_std.process.send(state.worker, (state.next_id, payload)) next } } _ -> state }}
pub fn complete(state: State, id: Integer) -> Stateensures result { if current(state, id) { result.completed == state.completed + 1 and result.phase == Idle and result.pending == state.pending and result.next_id == state.next_id and result.cancelled == state.cancelled and result.failed == state.failed and result.capacity == state.capacity and result.worker == state.worker } else { result == state }} { if current(state, id) { state {phase: Idle, completed: state.completed + 1} } else { state }}
pub fn cancel(state: State, id: Integer) -> Stateensures result { if current(state, id) { result.cancelled == state.cancelled + 1 and result.phase == Idle and result.pending == state.pending and result.next_id == state.next_id and result.completed == state.completed and result.failed == state.failed and result.capacity == state.capacity and result.worker == state.worker } else { result == state }} { if current(state, id) { state {phase: Idle, cancelled: state.cancelled + 1} } else { state }}
pub fn fail(state: State, id: Integer) -> Stateensures result { if current(state, id) { result.failed == state.failed + 1 and result.phase == Idle and result.pending == state.pending and result.next_id == state.next_id and result.completed == state.completed and result.cancelled == state.cancelled and result.capacity == state.capacity and result.worker == state.worker } else { result == state }} { if current(state, id) { state {phase: Idle, failed: state.failed + 1} } else { state }}
pub fn close(state: State) -> Stateensures result { is_closed(result) and load(result) == 0 and result.completed == state.completed and result.failed == state.failed and result.cancelled == state.cancelled + active(state.phase) and result.next_id == state.next_id and result.capacity == state.capacity and result.worker == state.worker} { state {phase: Closed, pending: [], cancelled: state.cancelled + active(state.phase)}}
pub fn relevant(state: State, event: Event) -> Boolean { match event { Completed(id) -> current(state, id) Cancelled(id) -> current(state, id) Failed(id) -> current(state, id) }}
@doc """Handles a report and dispatches the next queued job only for a current attempt.Reports are ordinary typed messages; they do not establish worker provenance."""pub fn handle_event(state: State, event: Event) -> Stateensures result { if relevant(state, event) { load(result) == load(state) - 1 and result.next_id >= state.next_id and result.next_id <= state.next_id + 1 and result.capacity == state.capacity and result.worker == state.worker and match event { Completed(_) -> result.completed == state.completed + 1 and result.cancelled == state.cancelled and result.failed == state.failed Cancelled(_) -> result.cancelled == state.cancelled + 1 and result.completed == state.completed and result.failed == state.failed Failed(_) -> result.failed == state.failed + 1 and result.completed == state.completed and result.cancelled == state.cancelled } } else { result == state }} { if relevant(state, event) { let next = match event { Completed(id) -> complete(state, id) Cancelled(id) -> cancel(state, id) Failed(id) -> fail(state, id) } start_next(next) } else { state }}Two hosts, the same state
Section titled βTwo hosts, the same stateβThe raw process uses a typed message union and passes valid state to its next
recursive call. The GenServer schedules dispatch with an OTP continuation and
returns the same state type from its callbacks. Both hosts are selected for
verification automatically, including when the state module comes from a separate
Gale dependency. The copied code_change default also preserves valid state.
The runtime tests run the same job lifecycle through both hosts, including stale and duplicate reports, rejected submissions, closing, and a GenServer system code change. The contracts check state validity and exact accounting. Reports remain claims supplied by messages; no sender authentication, job execution, delivery, or across-restart guarantee follows from these proofs.
A raw receive loop hosting the same verified job state as invariants.job_server. Replies use typed process IDs; reports are untrusted claims about attempt IDs.
Raw receive loop
module invariants.job_process
@moduledoc """A raw receive loop hosting the same verified job state as invariants.job_server.Replies use typed process IDs; reports are untrusted claims about attempt IDs."""
pub type Message = | Submit(payload: Integer, reply_to: gale_std.process.Pid<Result<:ok, :rejected>>) | Report(invariants.job_state.Event) | Inspect(gale_std.process.Pid<invariants.job_state.Snapshot>) | Close(gale_std.process.Pid<invariants.job_state.Snapshot>) | Stop
pub fn start(capacity: Integer, worker: invariants.job_state.Worker) -> Result<gale_std.process.Pid<Message>, :invalid> { match invariants.job_state.new(capacity, worker) { Ok(state) -> Ok(gale_std.process.spawn(fn() receives Message => loop(state))) Error(reason) -> Error(reason) }}
fn loop(state: invariants.job_state.State) receives Message -> :ok { receive { Submit(payload, reply_to) -> { match invariants.job_state.submit(state, payload) { Ok(queued) -> { let next = invariants.job_state.start_next(queued) let _ = gale_std.process.send(reply_to, Ok(:ok)) loop(next) } Error(reason) -> { let _ = gale_std.process.send(reply_to, Error(reason)) loop(state) } } } Report(event) -> loop(invariants.job_state.handle_event(state, event)) Inspect(reply_to) -> { let _ = gale_std.process.send(reply_to, invariants.job_state.snapshot(state)) loop(state) } Close(reply_to) -> { let next = invariants.job_state.close(state) let _ = gale_std.process.send(reply_to, invariants.job_state.snapshot(next)) loop(next) } Stop -> :ok }}A GenServer hosting job_state with a continuation for initial dispatch. The ordinary process and this server share every state transition.
GenServer callbacks
module invariants.job_server
@moduledoc """A GenServer hosting job_state with a continuation for initial dispatch.The ordinary process and this server share every state transition."""
implements gale_std.gen_server.Server<Args, invariants.job_state.State, Request, Never, invariants.job_state.Event, :dispatch> as service
pub type Args = (Integer, invariants.job_state.Worker)pub type Request = | Submit(payload: Integer, reply_to: gale_std.gen_server.ReplyTo<Result<:ok, :rejected>>) | Inspect(gale_std.gen_server.ReplyTo<invariants.job_state.Snapshot>) | Close(gale_std.gen_server.ReplyTo<invariants.job_state.Snapshot>)pub type Service = servicepub type Target = gale_std.gen_server.ServerTarget<Service>
pub fn start_link(args: Args) -> gale_std.gen_server.StartResult<Service> { gale_std.gen_server.start_link(invariants.job_server, args, [])}
pub fn init(args: Args) -> gale_std.otp.Init<invariants.job_state.State, :dispatch> { let (capacity, worker) = args match invariants.job_state.new(capacity, worker) { Ok(state) -> (:ok, state, (:continue, :dispatch)) Error(reason) -> (:stop, reason) }}
pub fn submit(server: Target, payload: Integer) -> Result<:ok, :rejected> { gale_std.gen_server.call(server, fn(reply_to) => Submit(payload, reply_to))}
pub fn snapshot(server: Target) -> invariants.job_state.Snapshot { gale_std.gen_server.call(server, fn(reply_to) => Inspect(reply_to))}
pub fn close(server: Target) -> invariants.job_state.Snapshot { gale_std.gen_server.call(server, fn(reply_to) => Close(reply_to))}
pub fn handle_call<R>( build: fn(gale_std.gen_server.ReplyTo<R>) -> Request, from: gale_std.gen_server.ReplyTo<R>, state: invariants.job_state.State) -> gale_std.otp.Next<invariants.job_state.State, :dispatch> { match build(from) { Submit(payload, reply_to) -> { match invariants.job_state.submit(state, payload) { Ok(queued) -> { let _ = gale_std.gen_server.reply(reply_to, Ok(:ok)) let next: gale_std.otp.Next<invariants.job_state.State, :dispatch> = (:noreply, queued, (:continue, :dispatch)) next } Error(reason) -> { let _ = gale_std.gen_server.reply(reply_to, Error(reason)) let next: gale_std.otp.Next<invariants.job_state.State, :dispatch> = (:noreply, state) next } } } Inspect(reply_to) -> { let _ = gale_std.gen_server.reply(reply_to, invariants.job_state.snapshot(state)) let next: gale_std.otp.Next<invariants.job_state.State, :dispatch> = (:noreply, state) next } Close(reply_to) -> { let closed = invariants.job_state.close(state) let _ = gale_std.gen_server.reply(reply_to, invariants.job_state.snapshot(closed)) let next: gale_std.otp.Next<invariants.job_state.State, :dispatch> = (:noreply, closed) next } }}
pub fn handle_info(event: invariants.job_state.Event, state: invariants.job_state.State) -> gale_std.otp.Next<invariants.job_state.State, :dispatch> { (:noreply, invariants.job_state.handle_event(state, event))}
pub fn handle_continue(value: :dispatch, state: invariants.job_state.State) -> gale_std.otp.Next<invariants.job_state.State, :dispatch> { (:noreply, invariants.job_state.start_next(state))}