Blog › ICP guides
Lean 4 developer on retainer: theorem proving, omega/ring tactics, mathlib4, and formal verification on monthly retainer
October 8, 2026 · ~20 min read
An interval arithmetic library written in Lean 4 for a financial risk calculation system was producing division-by-zero panics approximately once per thousand batch runs in production. The crash was intermittent — the upstream pipeline was supposed to guarantee non-zero denominators, but this guarantee was documented only in a comment: -- denominator is always non-zero here. The library function divide (n : Int) (d : Int) : Int had no mechanism to enforce this guarantee statically. The Lean 4 developer on retainer changed the function signature to divide (n : Int) (d : Int) (h : d ≠ 0) : Int — the h : d ≠ 0 parameter is a proof obligation. A value of type d ≠ 0 (which desugars to d = 0 → False) cannot be provided unless the caller can construct a proof that d is non-zero; concrete non-zero literals discharge it via by decide, arithmetic reasoning discharges it via omega, and propagated upstream guarantees discharge it by threading the proof through the call chain. The Lean 4 type checker statically rejects any call site that cannot produce the proof. Eighteen call sites were restructured; seven required the non-zero proof to be propagated from their own callers, extending the guarantee up the pipeline. Division-by-zero panics in production: weekly → 0 over a 60-day monitoring period.
The work log entry read “added h : d ≠ 0 parameter to divide, restructured 18 call sites, 9h.” It names the function and the duration, leaving the client unable to explain why adding one parameter to one function eliminated intermittent production crashes. The diagnosis required understanding that Lean 4’s type system treats propositions as types: d ≠ 0 is a type, a proof of d ≠ 0 is a value of that type, and the function that requires such a proof cannot be called without one. The 18 call sites each required analysis: which had concrete non-zero denominators dischargeable by by decide, which had denominators derived from expressions the omega tactic could reason about arithmetically, and which had denominators derived from user input requiring the non-zero guarantee to be added to their own callers’ signatures. The chain of guarantee propagation extended seven levels up the call graph. None of that analysis has an artifact in the committed diff beyond the changed function signature, the 18 restructured call sites, and the new h parameters in seven upstream functions. The type checker enforces the non-zero denominator invariant permanently.
Lean 4 fundamentals: Prop vs Type, theorem proving, and tactic blocks
Lean 4 is a proof assistant and general-purpose programming language built on the Calculus of Constructions with inductive types. The universe hierarchy distinguishes Prop (the type of logical propositions, proof-irrelevant — all proofs of a proposition are definitionally equal), Type 0 (the type of ordinary computational types), Type 1 (the type of Type 0), and so on; Sort u generalizes over all universes. The key design decision: Prop is impredicative and proof-irrelevant — two proofs of the same proposition p are definitionally equal, which means propositions can be freely used as types without computational content at runtime. d ≠ 0 is d = 0 → False: a function type from the proof d = 0 to False (the empty type, with no constructors). A proof of d ≠ 0 is a function that, given a proof that d = 0, produces a term of type False — which is impossible unless d = 0 is actually false. The decide tactic synthesizes proofs of decidable propositions by computation: by decide proves (3 : Int) ≠ 0 because DecidableEq Int is an instance, and the machine can compute that 3 = 0 is false. The omega tactic proves linear integer and natural number arithmetic: by omega proves n + 1 ≠ 0 given n : Nat because the Lean 4 omega implementation knows that natural number successors are never zero.
theorem in Lean 4 declares a term whose type lives in Prop (or more generally, a type the user intends to use as a proof). def declares a computational definition. Entering by switches from term-mode to tactic-mode; tactics transform the proof goal incrementally. Core tactics: intro h introduces a hypothesis, moving a → antecedent from the goal type into the local context; apply f applies a lemma to the current goal, creating subgoals for each unfilled premise; exact t closes the goal by providing a term t of the goal type directly; rfl closes goals of the form a = a; simp applies a library of rewriting lemmas automatically; ring proves equalities in commutative (semi)rings by normalization, handling distributivity and commutativity; omega proves linear integer and natural number arithmetic goals; linarith proves linear arithmetic hypotheses over ordered fields; norm_num proves numeric computation goals; constructor splits an And goal into two subgoals or introduces an existential witness; cases h destructs a hypothesis on its constructors; induction n inducts on a natural number or inductive type; contradiction closes a goal when the context contains contradictory hypotheses; assumption closes a goal by finding a matching hypothesis in the local context. Compound tactics: have h : P := by ... introduces an intermediate lemma with proof; let x := ... introduces a local definition; show P changes the goal display; obtain 〈a, b〉 := h destructs a conjunction or exists hypothesis.
def in Lean 4 uses structural recursion automatically when the function recurses on a syntactically smaller argument. def length : List α → Nat | [] => 0 | _ :: t => 1 + length t is structurally recursive because t is a structural subterm of the input list; Lean verifies this automatically. When the decreasing argument is not syntactic, termination_by provides a measure: termination_by a.length + b.length tells Lean that the sum of the two list lengths decreases on every recursive call, and Lean generates proof obligations that it discharges automatically for well-formed measures. partial def suppresses termination checking entirely — marking a function as intentionally non-total — analogous to %partial in Idris. A retainer engagement converting partial def functions to verified-total definitions involves first characterizing the termination argument: is the recursion structural on a data type, or does it require a numeric measure, or does it require a more complex well-founded relation? Each case requires different proof infrastructure. The investigation commonly runs 8–18 hours per function cluster; the resulting termination_by annotation and the well-foundedness proof are a 2–5 line diff that permanently eliminates a potential divergence.
The do notation in Lean 4 builds monadic computation for IO, Option, Except, and any Monad instance. def main : IO Unit := do let line ← IO.getLine; IO.println s!"Got: {line}" uses ← for monadic bind and sequences effects through the IO monad. String interpolation: s!"value is {x}" uses the s! macro prefix; f!"value is {x}" uses a Format-based variant. IO.FS.readFile reads files; IO.Process.run runs subprocesses. The EIO ε α type generalizes IO with a user-defined error type; ExceptT and StateT monad transformers stack effects. Lean 4’s Decidable type class provides a principled mechanism for decidable propositions: if h : p then ... else ... uses if-with-proof, binding h : p in the then branch and h : ¬p in the else branch, giving the programmer access to the proof in both branches. A retainer engagement designing an IO pipeline for batch financial processing commonly involves 8–15 hours of effect stack design — choosing which effects need type-level tracking, ordering the transformer stack, and threading effects through helper functions. The design decisions are visible in the transformer stack type signatures but not in the rationale for scoping and error recovery choices.
Lean 4 mathlib4, macro system, lake build, and type class design
Mathlib4 is the Lean 4 community mathematics library containing over 100,000 theorems across algebra, analysis, topology, category theory, and combinatorics. For retainer work on numerical and algorithmic software, the most commonly used components are: Mathlib.Tactic.Ring (the ring tactic for ring equation proofs — commutative rings, semirings, including polynomial identities); Mathlib.Tactic.Linarith (linarith for linear arithmetic over ordered fields and rings); Mathlib.Tactic.Omega (omega for Presburger arithmetic over integers and naturals); Mathlib.Tactic.Norm_num (norm_num for numeric computation proofs and arithmetic inequalities); Mathlib.Data.List.Basic (lemmas about List.length, List.map, List.append, List.filter, List.foldl); Mathlib.Data.Nat.Basic (natural number arithmetic properties, divisibility, GCD); Mathlib.Data.Int.Basic (integer arithmetic, absolute value, bounded intervals). The import Mathlib umbrella import brings all of Mathlib into scope at the cost of a multi-minute compile; production Lean 4 projects import only specific modules (import Mathlib.Tactic.Ring) to keep build times manageable. A retainer engagement integrating Mathlib into a numerical library for the first time commonly involves 4–8 hours of build system configuration before the first lemma can be used: lakefile.lean dependency addition, package manifest pinning, Lake cache setup to avoid rebuilding 100k theorems from source each session, and identification of which Mathlib import path contains the needed lemma.
The Lean 4 macro system extends the language with new syntax at compile time. macro_rules defines pattern-based syntax transformations: macro "unless " c:term " do " b:doSeq : doElem => `(doElem| if !$c then $b) defines an unless keyword. The syntax command declares a new syntax category: syntax "assert_eq " term " , " term : tactic declares a new tactic syntax. macro combines syntax declaration and elaboration in one form. The Lean.Elab.Tactic monad provides access to the proof state for custom tactic authorship: elab "discharge_positive" : tactic => do let goal ← getMainGoal; ... creates a new tactic that inspects the goal type and dispatches to appropriate sub-tactics programmatically. Lean 4 quotation syntax: `(term| e) constructs a syntax object for expression e; `(tactic| omega) constructs a tactic syntax object for use in macro expansions. Anti-quotation: `(term| $x + 1) splices the syntax object x into the quoted expression. Lean 4’s macro hygiene renames macro-introduced identifiers automatically to avoid capture. A retainer engagement writing a domain-specific tactic — for example, a discharge_bounds tactic that automatically proves 0 ≤ x and x ≤ 100 goals for domain-constrained sensor readings — typically invests 12–25 hours in goal inspection, dispatch logic, and test coverage across the tactic’s intended domain before the tactic is reliable enough for production use.
The Lake build system manages Lean 4 projects. lakefile.lean declares the package, its dependencies, and its build targets: require mathlib from git "https://github.com/leanprover-community/mathlib4" @ "v4.x.0" adds Mathlib as a versioned dependency. lean_lib MyLib { roots := #[`MyLib] } defines a library target; lean_exe myexe { root := `Main } defines an executable target. lake build compiles the project; lake update updates the package lock file (lake-manifest.json). The Lean 4 language server (LSP) provides real-time type checking and tactic goal display in VS Code via the Lean4 extension: hovering over a term shows its type; placing the cursor inside a by block shows the current proof state — remaining goals, hypotheses in context, expected goal type. Interactive proof development is fundamental to Lean 4 retainer work: the developer at an omega goal sees the exact arithmetic constraints; at a simp goal sees the rewriting steps applied and which lemmas fired. A retainer engagement migrating a Lean 4 project to a new Lean or Mathlib version commonly involves 6–14 hours of lakefile.lean updates, API change resolution, deprecated tactic replacement, and proof script repair that is invisible in the resulting version bump commit.
Lean 4 structure and class define record types and type class instances. structure Point where x : Float; y : Float creates a record with { x := 1.0, y := 2.0 } literal syntax and p.x/p.y dot projection. class Hashable (α : Type) where hash : α → UInt64 declares a type class; instance : Hashable Point where hash p := ... provides an instance. @[simp] attributes register lemmas for the simp tactic’s rewriting database: a lemma marked @[simp] will be applied automatically when simp is invoked, enabling downstream proofs to close goals that involve the lemma without explicit invocation. Building a well-designed @[simp] lemma library for a domain — a set of rewriting rules that orient equations in the canonical direction, that do not loop, and that cover the algebraic laws the domain uses — is a 15–40 hour design investment whose product is not visible in any single proof but enables hundreds of downstream proof closures. deriving Repr, DecidableEq, BEq, Hashable auto-generates standard instances; @[reducible] and @[inline] control elaboration and code generation behavior for performance-sensitive definitions. The namespace system scopes definitions: namespace MyLib ... end MyLib prefixes all def and theorem declarations; open MyLib brings the namespace into scope for a file or section.
How HourTab tracks Lean 4 developer retainer hours
Lean 4 retainer work shares the fundamental invisible-work problem with all proof assistant retainers: the highest-value changes produce the smallest diffs. Adding h : d ≠ 0 to a function signature is one token in the diff and eliminates indefinite production panics. Replacing Admitted with a 23-line tactic proof is a 23-line diff that makes a previously-silenced verification obligation permanently verified. Adding a termination_by measure is a 2-line diff that closes a potential divergence the test suite never reached. The call-site restructuring, the proof obligation propagation through seven upstream functions, the termination argument analysis — none of these have artifacts proportional to their complexity. A client reading the diff sees parameter additions; the client cannot see the type-theoretic analysis that determined which call sites needed upstream changes versus which could close the obligation locally.
HourTab gives Lean 4 developers a public retainer-hours URL they send to clients — typically formal verification teams, safety-critical embedded systems groups, cryptographic protocol organizations, or financial systems companies requiring mathematical correctness guarantees — at the start of an engagement. For Lean 4 retainers, each work log entry should name the mechanism (proof parameter addition; decide/omega/linarith/ring/norm_num/simp tactic application; Admitted proof replacement; termination_by measure addition; @[simp] lemma library authorship; macro_rules/syntax macro system design; structure/class/instance type class architecture; mathlib4 integration; Lake dependency configuration; custom tactic elab design), the specific theorem or function name and type signature before and after, the tactic error or proof goal state, the change and why, and the before/after observable metric. Lean 4 retainers are often compared to Coq developer retainers and Agda developer retainers for proof assistant work, and to Idris developer retainers for dependent types engagements. The distinction from Coq and Agda is that Lean 4 is designed as both a production programming language and a proof assistant: retainers often combine pure verification work (theorem proving, Admitted replacement) with general-purpose application development (Lake build pipelines, IO services, performance optimization) in the same engagement. HourTab’s work log bridges the gap: the entry names the theorem, the tactic used, the proof goal state change, and the before/after production metric, so the client understands what the retainer accomplished without needing to know type theory.
Track Lean 4 developer retainer hours without the status emails
HourTab gives Lean 4 developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Lean 4 developer retainers
What does a Lean 4 developer on retainer typically do?
A Lean 4 developer on monthly retainer covers proof obligation design (encoding runtime invariants as type-level proof parameters — d ≠ 0 / NonEmpty / in-bounds, discharged via decide/omega/linarith/ring at call sites or propagated from upstream guarantees), Admitted proof elimination (placeholder audit, proof completion with tactic combinators, have/obtain goal decomposition), termination engineering (termination_by measure design, partial def → total def conversion, sized type and fuel parameter design for external recursion), and mathlib4 integration (lakefile.lean dependency configuration, Mathlib lemma reuse, @[simp] lemma library authorship, type class hierarchy design unlocking tactic automation).
What Lean 4 work is most underlogged in a retainer?
Proof parameter addition and call-site restructuring (added h : d ≠ 0 to divide; 18 call sites restructured, 7 requiring upstream guarantee propagation; division-by-zero panics: weekly → 0; 9–22 hrs invisible in one function signature and 18 call-site restructurings), Admitted proof replacement (3 placeholders representing non-zero determinant, arithmetic overflow bound, and list index in-bounds assumptions; replaced with omega/ring/linarith proofs; 12–28 hrs invisible in proof term construction), and termination_by measure addition (4 functions rejected by termination checker — recursion on intermediate structure rather than original argument; added termination_by measures; potential divergence: 4 → 0; 8–20 hrs invisible in termination argument analysis).
What are typical Lean 4 developer retainer rates?
Entry-level Lean 4 developers (1–2 years, theorem/def/example, basic tactics intro/apply/exact/rfl/simp/omega, #check/#eval, do notation for IO) bill at $90–$155/hr. Mid-level Lean 4 engineers (2–4 years, omega/ring/linarith/norm_num/simp proof construction, have/obtain/calc structured proofs, termination_by measure design, mathlib4 integration, @[simp] lemma authorship, macro_rules syntax transformations, type class instance design) bill at $145–$260/hr. Senior Lean 4 architects (4–8 years, full proof obligation architecture for safety-critical systems, custom tactic elab via Lean.Elab.Tactic monad, universe polymorphism management, performance-critical Lean 4 application development, multi-file Lake project architecture, mathlib4 contribution-level theorem proving) bill at $200–$360/hr. Monthly retainer ranges: $2,800–$6,500/mo for advisory retainers (15–25 hrs), $9,000–$24,000/mo for full formal verification engagements.
What should a Lean 4 developer retainer agreement include?
A Lean 4 developer retainer agreement should specify: proof obligation design scope (invariant-as-type encoding for runtime crash categories; d ≠ 0 / NonEmpty / in-bounds proof parameter addition; decide/omega/linarith/ring proof synthesis at call sites; proof parameter propagation through call chains), Admitted proof scope (placeholder audit; proof completion with tactic combinators; documentation of deferral reason), termination engineering scope (termination_by measure design; partial def → total def conversion; sized type and fuel parameter design), mathlib4 scope (lakefile.lean dependency configuration; Mathlib lemma reuse vs custom proof; @[simp] lemma library authorship; type class hierarchy design), macro system scope (macro_rules/syntax transformations; custom tactic elab design), and hour logging format (theorem name; type before and after; tactic error or goal state; change and why; before/after metric; Lean 4 version and mathlib4 commit).
How should Lean 4 developer retainer hours be logged?
Log each Lean 4 retainer session with: advisory category (proof parameter addition; decide/omega/linarith/ring/norm_num/simp tactic application; Admitted proof replacement; termination_by measure addition; @[simp] lemma library authorship; macro_rules/syntax macro design; structure/class/instance type class architecture; mathlib4 integration; Lake dependency configuration; custom tactic elab design), specific theorem or function name and type before and after, tactic error or proof goal state if applicable, change applied and why (added h : d ≠ 0 proof parameter because the non-zero guarantee was documented only in a comment — the proof parameter forces all call sites to either prove non-zero locally via decide or omega, or propagate the guarantee from upstream, making the invariant statically enforced and permanently checked at compile time), and before/after metric (division-by-zero panics: weekly → 0; Admitted proofs: 3 → 0; potential divergence cases: 4 → 0). Include Lean 4 version, mathlib4 commit hash, and Lake manifest version. For omega proofs: note the arithmetic bound. For termination_by: note the decreasing measure and the argument it is applied to.