Lean 4 (4.31)
This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].
1 Assessment
This page assesses Lean 4 in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.
Core coupling 81/100 · Ecosystem coupling 65/100 · Engineering readiness 64/100
1.1 Core coupling
1.1.1 Type
- ✅ Dependent types let a type name a value:
Vector α nandFin ncarry the length in the type, so a length mismatch and an out-of-range index are rejected at elaboration - ✅ Propositions are types, so a proof is a term: The Curry–Howard correspondence puts a theorem and a program in one system, and the same kernel checks both
- ✅ A structure type names its fields and their types:
structurewith typeclass fields gives a record whose construction is checked, andwheresupplies the values by name - ✅ Typeclass resolution is checked instance search: An
instanceis found by the declared class at compile time, so a missing instance is an elaboration error rather than a run-time lookup - ✅
Subtypecouples a value to its proof:{x // p x}is a pair of a value and a proof that the predicate holds, so an invariant is carried by the term itself - ✅
unsafemarks the one escape from the logic: A declaration that bypasses the kernel must be namedunsafe, so the boundary is visible in the source
1.1.2 Construction
- ✅ Construction runs through the kernel, so an invariant cannot be skipped: A value of an inductive type is built by its constructors, and a term that does not fit the type fails elaboration
- ✅
whereputs the field values beside the type: An instance of a structure is written with named fields, so a reader does not count an argument list - ✅
macroandsyntaxextend the surface at the definition site: A notation for the domain is declared once and then used like a built-in - ✅
derivinggenerates the mechanical instances:Repr,BEq,Hashable, andInhabitedare produced by the elaborator, so a type does not repeat boilerplate - ✅
donotation builds the effectful value: Construction inIO,Option, orExceptis written as a sequence of binds - ⚠️ Termination is an obligation on every definition: A recursive
defmust be shown to terminate structurally or by a decreasing measure, so a natural formulation of an algorithm can need a proof before it compiles
1.1.3 Memory
- ✅ The compiler inserts the reference counting: Borrow inference decides which arguments are borrowed and which are owned, and emits the retain and release operations, so a pure definition never writes one
- ✅ There is no tracing collector: A value is reclaimed when its last reference goes away, so there is no collection pause to plan around
- ✅ A value of an inductive type is immutable: No term updates a field in place, so sharing is safe by construction
- ⚠️ A reference cycle is not collected: A mutable reference cell (
IO.Ref) whose value points back to the cell keeps its storage alive, because reference counting has no cycle detector - ⚠️ Allocation is not controllable from the source: The allocator is not a parameter and there is no placement control, so a program cannot choose where a value lives
- ⚠️ A deeply shared structure still pays a count per reference: A persistent structure that shares a subtree is counted at every edge, and the compiler removes many but not all of those operations
1.1.4 Error
- ✅
Option αandExcept ε αare the error types: A partial function returns a value that states the failure, anddonotation sequences the recovery path - ✅ A total function cannot fail by definition: In the pure fragment the type states that the function returns, and
partialis required to opt out of that guarantee - ✅
Decidableturns a proposition into a branch that is total:if h : p then ... else ...computes from a proof of decidability, so the two branches cover the cases by construction - ✅ There are no exceptions in the logic: An error is a value in the return type, so a computation that can fail carries the failure in its signature rather than raising out of it
- ⚠️
panic!and anIOfailure leave the kernel’s guarantee: An abort at run time is not a term in the logic, and the type of the function does not mention it - ⚠️ Error-freedom is a theorem, not an automatic result: To state that a function never fails, you prove it; the compiler does not derive the property
1.1.5 Concurrency
- ✅ The pure fragment has no shared mutable state: A mathematical term is a value with no location, so two proofs cannot race by construction
- ✅
Taskgives structured, joinable parallelism inIO: A task is a value that can be awaited, so a spawned computation is joined through its result - ⚠️ There is no concurrency construct in the language: Parallelism lives in the
IOmonad throughTaskand the promise cell, so a concurrent program is anIOprogram like any other - ⚠️ A promise carries a value between tasks and the program orders its use: Nothing in the type states when the value becomes available
- ✅ The runtime owns the thread pool: The scheduler and its thread count are runtime settings, so the language does not expose a thread object
- ⚠️ The kernel says nothing about a task’s behaviour: A theorem about a function does not constrain the parallel schedule that runs it
1.1.6 Platform
- ❌ Lean 4 is the only implementation: The language is defined by its own elaborator and kernel, so there is no second independent compiler to test the specification against (P1)
- ✅ The trusted kernel is small and portable: The elaborator is written in Lean and the kernel is a compact checker, so the language does not rest on a large run-time
- ✅
@[extern]and@[export]make the foreign boundary a declaration: A call out to C is written and recorded, so the escape from the language is visible in the source - ✅ The output is C, so the target set is the C compiler’s: The compiler emits C and hands it to a C back end, so a Lean program runs where a C program runs
- ⚠️ A native build needs a C toolchain and the Lean runtime: The build environment is part of the platform, and a deployed binary carries the runtime
- ⚠️ The toolchain moves in nightly steps: A project pins its version in
lean-toolchain, and a dependency may require a different one
1.1.7 Trade-offs (not coupling)
- Proof work is a separate effort from programming: A
defis not obliged to carry a proof, and requiring one is the author’s decision - The kernel is deliberately small and the tactics are not trusted: A tactic produces a term that the kernel rechecks, which is a decision about where trust sits
partialandunsafeare the intended escape hatches: A non-terminating or pointer-touching definition is expressible, at the cost of its guarantee- Typeclass search is a proof search with a cost: An instance can be ambiguous or expensive to find, and that is an elaboration property rather than a run-time coupling
- The library is one large shared body:
mathlib’s definitions belong to the community, so a development inherits its choices instead of buying a library - The audience is mathematics and verification: The adoption of the language follows from that, and it is a market fact rather than a mechanism of the language
1.2 Ecosystem coupling
1.2.1 Performance
- ✅ The compiler emits C, and a native optimiser then compiles it: The generated C passes through a mature C back end, so the run-time cost benefits from that pipeline
- ✅ Borrow inference removes most reference-count traffic: The compiler’s borrow analysis turns many retain and release pairs into nothing, so a pure functional definition stays close to imperative cost
- ✅
@[inline]and@[specialize]remove an abstraction at the seam: A monomorphic specialisation of a polymorphic definition is generated on request - ✅ Compile-time evaluation removes run-time work: A proof discharged by
decideornative_decideis computed while the project is built - ⚠️ Elaboration dominates the build time: Typeclass search, tactic execution, and proof checking are spent at compile time, so a large development builds slowly even when its binary is small
- ⚠️ A count is not free on a shared structure: A persistent structure counted at each edge still pays for the sharing the compiler cannot prove away
1.2.2 Ecosystem
- ❌ The package universe is small: Dependencies are resolved from git by
lake, and the practical library set ismathlib, the core library, and a few support packages (Ec3) - ✅
lakeis a standard build tool and dependency resolver:lakefile.leanorlakefile.tomldeclares a package,lake buildbuilds it, andlake-manifest.jsonrecords the resolved revisions - ✅ The editor integration is first class: The Lean 4 server speaks LSP and the infoview shows the goal, the hypotheses, and the diagnostics beside the source
- ✅
mathlibis one of the largest libraries of formalised mathematics: It is developed in the open and its continuous integration checks every proof - ⚠️ The community is academic and small: The centre of gravity is
mathlib, the Zulip chat, and the university groups, and industrial use is rare - ⚠️ A project’s revision set is tight against the toolchain:
mathlibtracks recent Lean releases, so a project that pins an older toolchain cannot use the currentmathlib
1.2.3 Evolution
- ✅ A project pins its toolchain and its dependency revisions:
lean-toolchainnames the version andlake-manifest.jsonnames the commits, so a build is reproducible without a separate lock convention - ✅ Lean 4 is the stable line and its minor releases are backward compatible: A project on a pinned version is not moved by a release it did not take
- ✅ The language and its libraries are open and publicly discussed: The request-for-comments archive and the community chat record why a feature changed
- ⚠️ Lean 4 replaced Lean 3 with a different language: A Lean 3 development is a porting project, and the mathematical libraries moved with the new language
- ⚠️ The ecosystem moves in nightly steps: A feature can land in a nightly build and reach a stable release later, so a dependency may require a version the project has not taken
1.2.4 Domain
- ✅ Proof engineering and formalised mathematics are the native domain:
theorem,by,simp, andmathlibare built for that work - ✅ Verified systems programming is supported: A verified algorithm compiles to C and ships as an ordinary binary, which is the growing use of the language
- ✅ It is a general-purpose functional language beside the proof assistant: A
defwith no proof obligation is ordinary functional code - ❌ The domain lock-in is the point of the tool: The language expects a proof-carrying artifact, and a program that states no theorem gets little from the system (D1)
- ⚠️
mathlibis a shared global body rather than a set of packages: A mathematical development is expected to build on its definitions, so the notation and the lemma names belong to the community - ⚠️ A formalisation is a research and maintenance commitment: A proof tracks the definitions it is about, so a change to one of them can invalidate a development
1.2.5 Learning
- ❌ The curve is very steep: Dependent types, the elimination rules, and the elaborator’s inference model are new to a working programmer (L1)
- ❌ Proof writing is a second skill on top of programming: Tactic scripts, the
simpsets, and the explicit proof term are a subject of their own - ❌ The elaboration is not visible in the source: Why a
by simpsucceeded, or why an instance was not found, must be read from the goal state and the options (L3) - ✅ The infoview shows the goal and the hypotheses at the cursor: The state is displayed rather than inferred by the reader
- ✅ The libraries supply the lemmas: An algebra or order fact is used without being reproved
- ⚠️ The documentation trail is uneven: The reference manual is complete for the language, but much elaboration behaviour is documented in the source, and the community’s books are a growth area
1.2.6 Syntax
- ✅ Term syntax reads like mathematics:
∀ x, P x → Q x,∃,fun x => ..., and infix notation for the relations the library defines - ✅ Notation is declared and hygienic:
notation,infix, andmacroadd a form where it is defined, so the surface can match the domain - ✅
donotation sequences effects as statements:let,if, andforinsidedoread like an imperative body over a monad - ✅ Pattern matching is checked for exhaustiveness: A missing constructor is a compile-time error, so a case is not forgotten
- ⚠️ Layout is significant:
do,by, andwhereblocks are delimited by indentation, so a re-indentation changes the parse - ⚠️ A macro can change what a line means:
macroandelabrewrite the surface, so the reader must know the notation the library installed
1.2.7 Testing
- ✅
#evalruns a term in the file: A definition can be evaluated and printed beside its proof, and#checkreports its type - ✅
exampleandby decidestate a checked claim inline: A decidable proposition is proved by evaluation, and anexampleblock is a test the elaborator runs - ✅
lake testruns the project’s test executable: A test driver is declared in the package and run by the build tool - ✅
#print axiomsreports the assumptions behind a theorem: A proof that usedsorryor an extra axiom shows the dependency, so an unchecked step is discoverable - ⚠️
native_decideextends the trusted base: It closes a goal by evaluating compiled code, so the compiler and the runtime, not only the kernel, are relied on - ⚠️ An
IOprogram is outside the logic: The behaviour of a program that reads a file or a socket is tested as an ordinary program rather than proved
1.2.8 Security
- ✅ The kernel rechecks every proof term: A theorem that elaborates cannot be invalid, so a claimed property is a checked one
- ✅
sorryis visible and reportable: The placeholder closes any goal with a warning, and#print axiomsshows that it was used, so an unfinished proof is discoverable rather than hidden - ✅
unsafenames the escape from the logic: A declaration that bypasses the kernel is marked, so the boundary can be found in the source - ✅ No pointer type exists in the language surface: A term written without
unsafecannot express an address, so the memory-safety class of bugs is outside the pure fragment - ⚠️ The trusted base is wider than the kernel in a native binary: The runtime, the C compiler, and the runtime’s primitives are all in the path of a compiled program
- ⚠️ There is no capability model:
IOgives a program the authority of the operating system, and the sandbox is the operating system’s
1.3 Engineering readiness
1.3.1 Reproducibility
- ❌ Builds are not byte-reproducible by default.
1.3.2 Supply chain
- ⚠️ Published packages carry no signed provenance by default.
- ⚠️ The package registry is a flat name space.
1.3.3 Observability
- ⚠️ No stable tracing and metrics standard.
- ⚠️ The standard library has no structured logging.
1.3.4 Feedback
- ✅ No charged mechanism in this dimension.
1.3.5 Interop
- ⚠️ No maintained portable target.
1.3.6 Governance
- ✅ No charged mechanism in this dimension.
1.4 Profile
Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.
1.4.1 Energy and cost
What this axis asks: what does a running program cost in energy and time?
1.4.2 AI and agent readiness
What this axis asks: how well does a coding model and an agent work in this language?
- ❌ Sparse training data: models write it unreliably [8].
1.4.3 Safety certification
What this axis asks: is there a certified or standards-qualified path to a safety-critical system?
- ⚠️ No certified or standards-qualified path for safety-critical systems.
1.5 Scoring detail
1.5.1 Core coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Type | T1 |
dynamic no static check | — | 0 |
T2 |
weak static | — | 0 | |
T3 |
dynamic/duck dispatch | — | 0 | |
T4 |
no sum types | — | 0 | |
T5 |
no generics | — | 0 | |
T6 |
no namespaces | — | 0 | |
T7 |
type erasure/reflection | — | 0 | |
T8 |
operator overloading | — | 0 | |
T9 |
inheritance required for polymorphism | — | 0 | |
T10 |
no null-safety | — | 0 | |
T11 |
no module boundary | — | 0 | |
T12 |
no effect tracking | — | 0 | |
T13 |
async/sync colouring | — | 0 | |
T14 |
unchecked foreign boundary | ✅ | −4 | |
| Type subtotal | −4 | |||
| Construction | C1 |
global mutable state by default | — | 0 |
C2 |
no immutability by default | — | 0 | |
C3 |
constructors coupled to allocation | — | 0 | |
C4 |
no constructor/validation | — | 0 | |
| Construction subtotal | 0 | |||
| Memory | M1 |
manual deallocation | — | 0 |
M2 |
GC non-deterministic pauses | — | 0 | |
M3 |
no RAII | — | 0 | |
M4 |
no ownership model | — | 0 | |
| Memory subtotal | 0 | |||
| Error | Er1 |
no structured path | — | 0 |
Er2 |
unchecked exceptions only | — | 0 | |
Er3 |
errors ignored/non-exhaustive | — | 0 | |
Er4 |
no composable result type | — | 0 | |
| Error subtotal | 0 | |||
| Concurrency | K1 |
GIL/no parallel | — | 0 |
K2 |
shared mutable state default | — | 0 | |
K3 |
manual sync no race detection | — | 0 | |
K4 |
no structured concurrency | — | 0 | |
K5 |
no primitives | — | 0 | |
| Concurrency subtotal | 0 | |||
| Platform | P1 |
cannot function outside one vendor/domain | ✅ | −15 |
P2 |
single-vendor but other uses | — | 0 | |
P3 |
legacy/discontinued platform | — | 0 | |
| Platform subtotal | −15 |
Core Dimensions score: 81/100 — total cost = −19 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.2 Ecosystem coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Performance | R1 |
interpreted no AOT/JIT | — | 0 |
R2 |
GC pauses | — | 0 | |
R3 |
JIT warmup | — | 0 | |
| Performance subtotal | 0 | |||
| Ecosystem | Ec1 |
no package manager | — | 0 |
Ec2 |
fragmented tooling | — | 0 | |
Ec3 |
small/niche/discontinued | ✅ | −8 | |
Ec4 |
proprietary | — | 0 | |
Ec5 |
platform-locked tooling | — | 0 | |
Ec6 |
monoculture | — | 0 | |
Ec7 |
legacy burden | — | 0 | |
| Ecosystem subtotal | −8 | |||
| Evolution | V1 |
breaking changes | — | 0 |
V2 |
frozen | — | 0 | |
V3 |
discontinued | — | 0 | |
V4 |
glacial standards | — | 0 | |
| Evolution subtotal | 0 | |||
| Domain | D1 |
domain/vendor lock-in | ✅ | −15 |
D2 |
poor domain fit | — | 0 | |
D3 |
framework forces architecture | — | 0 | |
| Domain subtotal | −15 | |||
| Learning | L1 |
steep | ✅ | −8 |
L2 |
large surface/multi-paradigm | — | 0 | |
L3 |
sparse docs | ✅ | −4 | |
| Learning subtotal | −12 | |||
| Syntax | S1 |
verbose | — | 0 |
S2 |
inconsistent | — | 0 | |
S3 |
punctuation-heavy | — | 0 | |
| Syntax subtotal | 0 | |||
| Testing | G1 |
weak tooling | — | 0 |
G2 |
poor debugging | — | 0 | |
G3 |
runtime-only errors | — | 0 | |
| Testing subtotal | 0 | |||
| Security | Q1 |
memory-unsafe | — | 0 |
Q2 |
dynamic/weak typing risk | — | 0 | |
Q3 |
known vulnerability/injection | — | 0 | |
| Security subtotal | 0 |
Meta Dimensions score: 65/100 — total cost = −35 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.3 Engineering readiness Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Reproducibility | Y1 |
build not reproducible | ✅ | −8 |
Y2 |
dependencies not pinned | — | 0 | |
| Reproducibility subtotal | −8 | |||
| Supply chain | Y3 |
no signed provenance | ✅ | −8 |
Y4 |
registry without namespacing | ✅ | −4 | |
| Supply chain subtotal | −12 | |||
| Observability | Y5 |
no stable tracing standard | ✅ | −8 |
Y6 |
no structured logging | ✅ | −4 | |
| Observability subtotal | −12 | |||
| Feedback | Y7 |
slow build | — | 0 |
Y8 |
no fast in-editor analysis | — | 0 | |
| Feedback subtotal | 0 | |||
| Interop | Y9 |
no foreign-function interface | — | 0 |
Y10 |
no portable target | ✅ | −4 | |
| Interop subtotal | −4 | |||
| Governance | Y11 |
single-vendor governance | — | 0 |
Y12 |
closed specification | — | 0 | |
| Governance subtotal | 0 |
Readiness Dimensions score: 64/100 — total cost = −36 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
Verdict: Lean 4’s design produces the highest core score here, because its constructs are separating by construction. Dependent types let a type state a property of a value, propositions-as-types puts the specification and the program in one term, and the kernel rechecks every proof, so an invalid claim does not elaborate. Reference counting with borrow inference keeps the memory story deterministic and out of the source. Main strengths: an exact fit to mathematics and verification, a small trusted kernel, and a first-class editor and library. Main weaknesses: the language has a single implementation, the library universe beyond mathlib is small, the toolchain moves in nightly steps, and the curve is among the steepest.
The formal development. The definitions, theorems, and proofs behind this page are published in:
- On the Nature of Coupling: All Coupling as Shared Resource Access — Zenodo, 2026 — doi:10.5281/zenodo.21925122
- Coupling as a Consequence of Cohesion: Decomposition, the Coupling Floor, and Domain-Imposed Coupling — Zenodo, 2026 — doi:10.5281/zenodo.21925201
- On the Nature of Cohesion: Cohesion as Correctness Relative to a Partitioning Rule — Zenodo, 2026 — doi:10.5281/zenodo.21703193