Blog › ICP guides
Lean 4 developer on retainer: tactic system, simp, induction, dependent types, mathlib4, and Lean 4 theorem proving on monthly retainer
December 4, 2026 · ~15 min read
A Lean 4 proof development for a verified list processing library needed to prove that appending an empty list on the right is a no-op: theorem append_nil (list : List α) : list ++ [] = list. The developer opened the proof with simp only [List.append_nil], expecting the mathlib4 simp lemma to close the goal in one step. Lean 4 rejected the tactic: List.append_nil has type forall α (list : List α), list ++ [] = list — but the developer was trying to use it to prove exactly that same statement, creating a circular dependency. The theorem cannot prove itself via simp. The proof required induction. The developer opened induction list with which generated two goals: the base case for [] (the nil constructor) and the inductive case for a :: tail with inductive hypothesis ih : tail ++ [] = tail. For the base case, the goal was [] ++ [] = [], which reduces by List.append’s definition to [] = [] — simp (without the circular lemma restriction) closed this immediately. For the inductive case, the goal was (a :: tail) ++ [] = a :: tail. The developer applied rw [List.cons_append] to unfold one step of List.append’s recursive case, which rewrote (a :: tail) ++ [] to a :: (tail ++ []), producing the goal a :: (tail ++ []) = a :: tail. Applying rw [ih] substituted tail ++ [] with tail using the inductive hypothesis, closing the goal. Two tactic failures per proof attempt. The Lean 4 developer on retainer diagnosed the circular simp lemma issue: simp only [List.append_nil] as the opening tactic was circular; the correct structure required induction list with first, then simp for the base case and rw [List.cons_append, ih] for the inductive step. Tactic failures: 2 per attempt → 0.
The work log entry read “fixed append_nil proof, 6h.” It names the result and duration. It cannot explain why simp only [List.append_nil] creates a circular dependency — the simp lemma database stores List.append_nil as a rewrite rule of the form ?list ++ [] → ?list, and when simp applies this rule to the goal list ++ [] = list, it is attempting to use the theorem being proved as a rewrite rule to prove itself, which Lean 4’s kernel rejects as a definitional equality issue. It cannot explain why plain simp works for the base case while simp only [List.append_nil] fails — the base case goal [] ++ [] = [] reduces definitionally because List.append is defined by pattern matching on the first argument: the [] branch returns the second argument directly, so [] ++ [] = [] holds by computation without needing any lemma. It cannot explain why rw [List.cons_append] was needed instead of simp in the inductive case — List.cons_append states (a :: l) ++ l’ = a :: (l ++ l’) and applies a single targeted unfolding step that exposes the inductive hypothesis position, while a bare simp might loop by applying List.append_nil circularly in more complex goal contexts. The 6 hours of induction strategy selection, simp lemma set analysis, and rw sequencing are invisible in the diff.
Lean 4 tactic system: simp, simp only, rw, induction, and the simp lemma database
Lean 4 proofs are constructed through a tactic-based proof mode where tactics transform the current proof state — a collection of goals, each with a local context of hypotheses and a goal type. The most frequently used tactic is simp, which applies a database of rewrite rules repeatedly until no more apply. simp draws from lemmas tagged with the @[simp] attribute, which in a standard mathlib4 installation includes hundreds of List, Nat, Bool, and algebraic structure lemmas. The unrestricted simp call often closes simple goals automatically but can loop on complex ones or apply lemmas in unexpected orders. simp only [lemma1, lemma2] restricts the simp set to exactly the named lemmas, giving precise control over which rewrites are applied. The tension between simp and simp only is a central concern in Lean 4 retainer work: simp is faster to write but brittle when the simp lemma database changes, while simp only requires explicitly curating the lemma set and fails silently when the required lemmas are not listed.
rw (rewrite) applies a single equational rewrite to the current goal. rw [h] where h : a = b replaces occurrences of a with b in the goal. rw [←h] rewrites in the reverse direction, replacing b with a. Unlike simp only, rw applies exactly one rewrite step and then stops — it does not saturate. This makes rw precise but verbose: a proof that simp closes in one line may require a sequence of rw calls with carefully ordered lemmas. The choice between rw and simp only depends on whether the goal requires a single targeted substitution (use rw) or a confluence of multiple reductions (use simp only). rw [h1, h2, h3] applies three rewrites in sequence, with each subsequent rewrite applied to the goal produced by the previous one — the order matters because later rewrites may need the result of earlier ones.
induction x with performs structural induction on a term x of an inductive type. For List α, it generates two goals: the nil case ([]) and the cons case (a :: tail with an inductive hypothesis about tail). The with clause introduces names for the constructor arguments and the inductive hypothesis, e.g., induction list with | nil => ... | cons a tail ih => .... Choosing the right induction variable is critical: inducting on the wrong argument produces goals where the inductive hypothesis does not match the recursive subterm, making the proof impossible. For List.append theorems, induction on the first list argument aligns with the function’s recursive definition on its first argument — inducting on the second argument would produce an inductive hypothesis that cannot be applied because List.append’s recursion never peels off the second argument. Senior Lean 4 retainers spend significant time on induction variable selection, generalization (using revert before induction to strengthen the inductive hypothesis), and identifying when mutual induction or well-founded induction is needed.
Lean 4 type system: Prop, Type u, Sort u, universe polymorphism, and dependent types
Lean 4’s type system is built around a predicative universe hierarchy. Prop is the universe of propositions — types whose terms are proofs. Type 0 (also written Type) is the universe of ordinary data types; Type 1 is the universe of types that contain Type 0, and so on. Sort 0 = Prop and Sort (n+1) = Type n, so Sort u is the most general universe expression. Universe polymorphism allows definitions to quantify over universe levels: def id.{u} (α : Sort u) (a : α) : α := a works for any universe including Prop, Type 0, Type 1, and all higher levels. Universe polymorphism errors are among the most confusing in Lean 4: a function defined in Type 0 cannot be applied to a Prop-valued argument, producing universe level mismatch errors that require explicit .{u} universe annotations. The retainer work involves diagnosing these mismatches — identifying which definition has an implicit universe annotation that is too restrictive — and adding the explicit .{u} suffix to make the definition universe-polymorphic.
Lean 4 supports dependent types through dependent pi types written (x : α) → B x or using the forall binder ∀ x : α, B x. Dependent types enable type-level invariants: Fin n is the type of natural numbers less than n, carrying its bound in the type; Vector α n is a list of exactly n elements of type α. Working with dependent types requires managing how terms flow into types: when a term is substituted for a variable in a dependent type, the resulting type must typecheck with the substitution applied. The Fin.isLt proof field carries evidence that the Fin value is less than its bound, and functions that index into Vector require proof that the index is in bounds. Lean 4’s elaborator handles much of this automatically through unification, but complex dependent type computations require explicit proof terms or the conv tactic for navigating inside type expressions. Lean 4 was developed by Leonardo de Moura and Sebastian Ullrich at Microsoft Research and then at the Lean FRO; it is a ground-up redesign of Lean 3 with a new compiler, elaborator, macro system, and tactic framework, making it retainer-compatible with projects ranging from undergraduate mathematics formalization to industrial-scale verified software development. Its closest retainer-ecosystem neighbors are Coq (Ltac tactic language, Gallina term language, CompCert verified compiler) and Agda (cubical type theory option, proof-relevant mathematics), but Lean 4’s mathlib4 library, programmable tactic framework, and Lean-as-a-general-purpose-language positioning make the retainer work distinct in simp lemma set engineering, metaprogramming, and proof automation design.
Lean 4 structures and type classes unify the object-oriented and algebraic approaches to abstraction. A structure declaration defines a record type with named fields; a class declaration defines a type class whose instances are synthesized by Lean 4’s instance resolution algorithm. [inst : MyClass α] (square bracket notation) marks an instance-implicit argument that Lean 4 infers automatically from the local context and the global instance database. Type class hierarchies in mathlib4 encode algebraic structures: Semigroup, Monoid, Group, Ring, Field, each extending the previous with additional operations and axioms. Lean 4 retainer work frequently involves diagnosing instance synthesis failures: Lean 4 cannot find a required instance, often because the instance is defined in a module that was not imported, because the instance requires a typeclass hypothesis that was not brought into scope, or because the instance has a more restrictive universe than the use site.
How HourTab tracks Lean 4 developer retainer hours
Lean 4 retainer work carries the invisible-hours problem common to all proof-assistant retainers, amplified by the distance between the apparent simplicity of individual tactics and the combinatorial complexity of proof strategy selection. Teams using Lean 4 for verified mathematics formalization, type-safe API design, or certified algorithm implementation frequently encounter the circular simp pattern described above: a developer reaches for a standard library lemma to close a goal, only to find that the lemma is exactly what they are trying to prove, and the fix requires restructuring the proof as an induction with targeted rewrites in each case. The two tactic failures per proof attempt described above is one instance of a broader pattern; the retainer work is the simp lemma analysis that identifies the circularity, the induction variable selection that aligns with the function’s recursive definition, the rw sequencing that exposes the inductive hypothesis, and the simp lemma set curation that makes the proof robust against future mathlib4 updates. Lean 4 retainers produce visible outcomes — tactic failures: 2 per attempt → 0; compilation errors: N → 0 — but the hours spent on simp circularity diagnosis, induction strategy (which variable? revert first?), rw ordering (which lemma unblocks the inductive hypothesis?), and mathlib4 theorem search (does this already exist? what is the canonical name?) appear in work logs as “fixed proof” without explaining the List.append recursive definition mechanics.
HourTab gives Lean 4 developers a public retainer-hours URL they send to clients — typically academic research groups formalizing mathematics in Lean 4 and mathlib4, companies building verified software with dependent type invariants, and projects migrating from Lean 3 to Lean 4 where proof scripts require translation and tactic API changes. For Lean 4 retainers, each work log entry should name the mechanism (circular simp lemma repair: simp only [theorem_being_proved] restructured to induction with simp and rw [ih]; induction variable selection: induction on first List argument aligned with List.append recursion; rw sequencing: rw [List.cons_append] to expose inductive hypothesis position; simp only lemma set curation; mathlib4 theorem search: exact? / apply? for automation; universe polymorphism annotation for Sort u), the specific theorem, tactic sequence, and before/after failure count, and the proof strategy rationale. Lean 4 retainers are often compared to Coq developer retainers for the shared dependently typed proof assistant positioning, but Lean 4’s simp lemma database, rw vs simp distinction, mathlib4 instance hierarchy, and Lean-as-a-programming-language architecture make the retainer work distinct in tactic automation design, metaprogramming, and simp lemma set engineering. HourTab’s work log makes the circular simp diagnosis, induction variable selection, and rw sequencing visible to clients who would otherwise see only the symptom — proof compilation failure — and not understand why the fix required understanding that List.append’s recursion is on its first argument, and why simp only [List.append_nil] before induction was the difference between a proof that typechecks and one that fails with a circular lemma application.
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 proof engineering 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 Lean 4 tactic engineering (simp, simp only, rw, exact, intro, induction, cases, apply, constructor, omega, decide, norm_num), Lean 4 type system (Prop, Type u, Sort u; universe polymorphism; structure and class; dependent pi types; instance-implicit [] arguments), List and Nat theorem engineering (List.append_nil, List.cons_append, List.append_assoc, List.length_append, Nat.add_comm, Nat.succ_inj), and mathlib4 theorem reuse and simp lemma set curation.
What Lean 4 work is most commonly underlogged in a retainer?
Circular simp lemma diagnosis (simp only [theorem_being_proved] creates circular dependency; restructured to induction with simp and rw [ih]; tactic failures: 2 per attempt → 0; 5–9 hrs invisible); induction base case repair (simp closed [] ++ [] = [] by definitional reduction; simp only [List.append_nil] failed because that is the theorem being proved; 3 failures per proof → 0; 4–8 hrs invisible); universe polymorphism annotation (universe mismatch between Prop and Type sites; explicit Sort u annotation needed; 2 errors per polymorphic definition → 0; 6–10 hrs invisible).
What are typical Lean 4 developer retainer rates?
Entry-level Lean 4 developers (1–2 years, Lean 4 basics, standard tactics, core List theorems) bill at $80–$145/hr. Mid-level Lean 4 proof engineers (2–4 years, induction strategy, simp only curation, dependent types, mathlib4 reuse) bill at $140–$230/hr. Senior Lean 4 architects (4–8 years, large-scale verified software, Lean 4 metaprogramming, macro/elab systems, mathlib4-scale proof development) bill at $195–$335/hr. Monthly retainer ranges: $2,100–$5,200/mo advisory (15–25 hrs), $7,500–$20,000/mo for full Lean 4 proof engineering engagements.
What should a Lean 4 developer retainer agreement include?
A Lean 4 developer retainer agreement should specify: tactic scope (basic: simp, simp only, rw, exact, intro, induction, cases, apply; automation: omega, decide, norm_num; conv for focused rewriting); type system scope (Prop, Type u, Sort u; universe polymorphism; structure/class; instance-implicit; dependent pi types); mathlib4 scope (Data.List.Basic, Algebra.Group.Basic, simp lemma sets; exact? and apply? for automation); metaprogramming scope (macro, elab, Syntax, Expr, MetaM); and hour logging format (advisory category, before/after tactic failure count, whether fix required induction restructuring, simp lemma curation, universe annotation, or mathlib4 theorem search).
How should Lean 4 developer retainer hours be logged?
Log each Lean 4 retainer session with: advisory category (circular simp repair: simp only [theorem_being_proved] restructured to induction with simp and rw [ih]; induction base case: simp not simp only for base case goals that reduce definitionally; universe annotation: explicit Sort u for polymorphic terms; simp lemma set curation: simp only [lemma1, lemma2] vs simp; mathlib4 theorem search: exact? and apply? for finding automation); the specific theorem, tactic sequence, and before/after failure count (theorem: list ++ [] = list; induction list with; nil: simp; cons a tail ih: rw [List.cons_append, ih]; tactic failures: 2 per attempt → 0); and the before/after metric. Include whether fix required induction restructuring, simp lemma curation, universe polymorphism annotation, or mathlib4 theorem search.