Skip to main content

lean-best-practices

Mathlib4-derived best practices for writing and structuring Lean 4 libraries. Use when user asks to "add a Lean theorem", "create a Lean module", "review Lean code", "structure a Lean library like Mathlib", "fix a Lean build error", or works on `.lean` / `lakefile.lean` / `lean-toolchain` files. Codifies file template, naming, proof style, attribute usage, the `abbrev`/`omega` trap, and the common errors that bite first-time contributors.

Quellinformationen

Repository
solana-foundation/leanprover-solanalib
Letzte Quellaktivität
17. Juni 2026 um 01:53
Erkannte Sprache von SKILL.md
Englisch
Sterne
12
Forks
1

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
lean-best-practices
description
Mathlib4-derived best practices for writing and structuring Lean 4 libraries. Use when user asks to "add a Lean theorem", "create a Lean module", "review Lean code", "structure a Lean library like Mathlib", "fix a Lean build error", or works on `.lean` / `lakefile.lean` / `lean-toolchain` files. Codifies file template, naming, proof style, attribute usage, the `abbrev`/`omega` trap, and the common errors that bite first-time contributors.
metadata
{"source-project":"solana-foundation/leanprover-solanalib","version":"0.1.0"}
# Lean 4 best practices Conventions for writing Lean 4 libraries in the Mathlib4 style. Distilled from the official Mathlib contributor docs plus painful lessons learned bootstrapping the [Solanalib](https://github.com/solana-foundation/leanprover-solanalib) library — examples below use Solanalib's code, but the rules are general. ## When this skill applies - Adding or modifying any `.lean` file in a library that aims to follow Mathlib conventions. - Editing `lakefile.lean`, `lean-toolchain`, `lake-manifest.json`, or `.github/workflows/` for a Lean project. - Reviewing a Lean PR for style or structure. - Bootstrapping a new Lean 4 library that should match the Mathlib ecosystem's conventions. ## Default decisions Make these without asking: - **Verify locally before pushing.** `elan` installs the right toolchain from the `lean-toolchain` file; `lake build` (and `lake test`) before any push. CI cycles cost minutes; local builds cost seconds. Never push a Lean change without a local build pass. - **Track Mathlib's `lean-toolchain` pin.** When bumping Lean, fetch `https://raw.githubusercontent.com/leanprover-community/mathlib4/master/lean-toolchain`, set `lean-toolchain` to match, and update the Mathlib `require` rev in `lakefile.lean` to the same tag. `doc-gen4`'s `main` branch tracks this — drift breaks docs. - **Copyright authorship line is `<Project> Contributors`.** Never personal names, never the parent org. The `Copyright (c) YEAR <Owner>` line is the legal-owner statement and is separate. - **No AI-attribution trailers** (`Co-Authored-By: Claude …`) in commits. - **Commit `lake-manifest.json`** but not `.lake/`. Reproducible builds depend on the manifest; `.lake/` is per-machine cache. ## Operating procedure ### Adding a new module 1. Decide the file path. Organise by **domain concept**, not by **consumer or framework** (Mathlib organises by mathematical concept, not by who uses it; sibling libraries should organise the same way within their domain). 2. Write the file using the template in `## File template` below. 3. Add `import <Library>.<Concept>.<Aspect>` to the library root file (alphabetical within its block). 4. Run `lake build` locally. Fix any errors. Run `lake test`. 5. If the module introduces a load-bearing API surface, add a regression test under the test library mirror (`<Library>Test/<Concept>/<Aspect>Test.lean`) and import it from the test root. ### Adding a new theorem 1. Place it in the file whose subject it concerns, not in a global `Theorems.lean`. 2. Name it `<conclusion>_of_<hypothesis>` (Mathlib pattern: `add_pos_of_pos_of_nonneg`) or `<descriptive_name>` if self-explanatory (`Nat.sub_add_cancel`). 3. Write the statement with all binders explicit (`autoImplicit := false` is on globally in the lakefile — keep it that way). 4. Prefer term-mode proofs (`:= rfl`, `:= by simp`) over multi-line `by` blocks when feasible. Otherwise use the tactic style in `## Proof style` below. 5. Tag with `@[simp]` only if the lemma rewrites *toward* a normal form. Don't tag the headline result. ### Adding a new type **Default to `notation` over `abbrev` when the type is a synonym for `Nat`/`Int`.** See `## Type aliases: notation, not abbrev` below — this is the most-likely-to-bite gotcha. For real structures, use `structure`/`inductive` with `@[ext]`: ```lean @[ext] structure Slot where /-- The slot number on the cluster. -/ value : Nat deriving Repr, DecidableEq ``` ## File template Every `.lean` file starts with this exact shape: ```lean /- Copyright (c) <YEAR> <Legal Owner>. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: <Project> Contributors -/ import <Library>.<dep1> import <Library>.<dep2> -- alphabetical within their block /-! # <Module title — short, capitalised> <One-paragraph summary explaining what this module is for and why it exists. End at the level of "what does this file give to the rest of the library".> ## Main definitions * `<Library>.Foo` — <one-line gloss> * `<Library>.Foo.bar` — <one-line gloss> ## Main statements * `foo_property_baz` — <what it proves and why it matters> -/ namespace <Library> -- declarations end <Library> ``` **Critical ordering:** copyright `/- ... -/` comment first, then imports, then `/-! ... -/` module docstring. The module docstring is a *declaration*, not a comment, so it cannot precede `import`. Mistaking this produces `invalid 'import' command, it must be used in the beginning of the file`. **Subsection headers** (`## Main definitions`, `## Main statements`, `## Implementation notes`, `## References`) follow Mathlib's convention and are rendered as anchored sections in the doc-gen4 output. ## Naming conventions Inherited from Mathlib: | Kind | Convention | Example | |---|---|---| | Types, structures, classes, inductives | `UpperCamelCase` | `Account`, `TransferResult` | | Defs, functions, fields | `lowerCamelCase` | `credit`, `lamportsPerSol`, `source` | | Theorems, lemmas, propositions | `snake_case` | `transfer_preserves_total`, `credit_lamports` | | Files | `UpperCamelCase.lean` | `Transfer.lean` | | Folders | `UpperCamelCase` | `Account/`, `Primitives/` | Theorem-name pattern: `<conclusion>_of_<hypothesis>`. For projection-shape `@[simp]` lemmas, the form `<def>_<field>` is conventional (`credit_lamports`, `Account.mk_lamports`). ## Bounded types: bridging UInt64 to Nat for omega **`omega` in Lean 4.31.0-rc1 has very limited `UInt64` support.** Tests like `(a b : UInt64) (h : a ≤ b) : b - a + a = b := by omega` fail with `No usable constraints found`. The fix is to keep two parallel type-aliases and bridge between them with `.toNat`: ```lean notation "Lamports" => UInt64 -- strict on-chain shape notation "LamportsUnchecked" => Nat -- for omega-style reasoning ``` The pattern in practice: ```lean -- Define operations on UInt64 with explicit bounds proofs in the signature: def credit (a : Account) (amount : Lamports) (h : a.lamports.toNat + amount.toNat < UInt64.size) : Account := { a with lamports := a.lamports + amount } -- Provide a `_toNat` bridge lemma that drops the result into Nat: theorem credit_lamports_toNat (a : Account) (n : Lamports) (h : a.lamports.toNat + n.toNat < UInt64.size) : (credit a n h).lamports.toNat = a.lamports.toNat + n.toNat := by rw [credit_lamports, UInt64.toNat_add, Nat.mod_eq_of_lt h] ``` Then conservation-style proofs become: ```lean theorem transfer_preserves_total ... : (transfer ...).source.lamports.toNat + (transfer ...).destination.lamports.toNat = src.lamports.toNat + dst.lamports.toNat := by have h_under_nat := UInt64.le_iff_toNat_le.mp h_under -- bridge UInt64 ≤ → Nat ≤ simp only [transfer] rw [debit_lamports_toNat, credit_lamports_toNat] omega -- now in Nat-land ``` Key core lemmas to know (all in `Init.Data.UInt.Lemmas`): | Lemma | What it gives | |---|---| | `UInt64.le_iff_toNat_le` | `a ≤ b ↔ a.toNat ≤ b.toNat` (proved by `rfl`) | | `UInt64.toNat_add` (`@[simp]`) | `(a + b).toNat = (a.toNat + b.toNat) % UInt64.size` | | `UInt64.toNat_sub_of_le` (`@[simp]`) | `b ≤ a → (a - b).toNat = a.toNat - b.toNat` | | `UInt64.toNat_lt` (`@[simp]`) | `n.toNat < 2 ^ 64` | | `Nat.mod_eq_of_lt` | `n < m → n % m = n` (used with `toNat_add` to drop the `%`) | ### `.toNat` vs `.toBitVec` — which bridge? There are *two* possible bridges out of `UInt64`. Use the one that matches what you're proving: | Use | Bridge | Closer | |---|---|---| | Conservation / accounting / counting (Solanalib's core case) | `.toNat` | `omega` | | Wrap-aware arithmetic, ring algebra, explicit-overflow exploit proofs | `.toBitVec` | `bv_omega` | Mathlib's `Mathlib/Data/UInt` builds its `UInt64` algebra on `.toBitVec`, not `.toNat` — but it's scoped (`open scoped UInt64.CommRing`) precisely because the Mathlib authors flag that algebraic instances on `UInt64` *"interfere more with software-verification use-cases."* For Solanalib, `.toNat` is the default because **a conservation theorem stated at `.toNat` level needs fewer preconditions** than the equivalent UInt64-level statement: the natural sum doesn't need a "RHS-doesn't-overflow" side condition, since `Nat` is unbounded. Same trade as every comparable software-verification project (seL4, CompCert, Cardano Plutus). Don't pre-emptively `import Mathlib.Data.UInt` — it brings ~700 extra build jobs for the algebraic-instances chain we don't currently use. Import surgically when a specific theorem genuinely needs `bv_omega` or scoped ring tactics. ## Why not `abbrev`? The historical reason for using `notation` (over `abbrev`) was that **`omega` does not unfold `abbrev T : Type := Nat`.** Its preprocessor classifies hypotheses by their surface type and silently drops constraints over the alias, producing `omega could not prove the goal: No usable constraints found.` Wrong: ```lean abbrev Lamports : Type := Nat -- omega will refuse to see (a : Lamports) ≤ b ``` Right: ```lean namespace Solanalib.Lamports -- declarations using Nat directly (the namespace header is parsed BEFORE the -- notation is declared, so `Lamports` here is still an identifier). end Solanalib.Lamports /-- A lamport count: the smallest unit of native Solana value. -/ notation "Lamports" => Nat ``` `notation` is a parse-time substitution, so `(amount : Lamports)` is elaborated as `(amount : Nat)` from the start. omega and `simp`-set lemmas about `Nat` work without ceremony. The notation must be declared *after* any `namespace <SameName>` blocks in the same file, because the parser substitutes the token everywhere once the notation is in scope. **Escape hatch:** if/when strict typing is needed (e.g. to enforce `≤ 2^64 - 1` on a `u64`-like quantity), promote to a single-field `structure` with explicit conversion. Plan to migrate the call sites mechanically; the migration is breaking but tractable. ## Bundled structures vs typeclasses When a domain concept has *several mathematical properties* that must hold together (e.g. "this is a function that decays from peak to zero in a window"), prefer a **bundled structure** with the function and proofs embedded, not a `class`-based typeclass. ```lean -- ✓ Mathlib-aligned bundled-structure pattern (used by OrderHom, MulHom, -- LinearMap, RingHom, ...): structure WindowedDecay where tBegin : Nat tEnd : Nat peak : Nat apply : Nat → Nat bounded : ∀ t, apply t ≤ peak -- ... three more properties ... -- Concrete-shape providers expose a constructor: def LinearDecay.toWindowedDecay (tBegin tEnd peak : Nat) : WindowedDecay := { … } ``` Why bundled over typeclass: - **No instance-resolution surprises.** A `WindowedDecay` value is just a value; you pass it explicitly. No implicit-argument elaboration footguns (like the `fun p {_ _} h_begin …` workaround that `class Decay` needed). - **Mathlib does it this way.** Their `OrderHom`, `MulHom`, `RingHom`, `LinearMap`, etc. are all bundled structures, not classes. - **Composition is plain function-call syntax.** `d.complementary t`, `d.apply t` — no typeclass magic. - **Generic theorems live as methods on the structure**, not as separate generic functions over `[Decay T]`. When *to* reach for a `class`: when you have a true type-level interface (e.g. `Monoid α`, `AddCommGroup α`) where the typeclass system's instance resolution is doing useful work — finding the appropriate algebraic structure for `Nat`, `Int`, `Rat`, etc. For domain-specific finance shapes with concrete parameter values, bundled structures are usually the better tool. ## Numeric layer: `Solanalib.Numeric` A dedicated folder for numeric infrastructure that domain modules build on. Currently houses `Fraction` (Q68.60 fixed-point, common in Solana DeFi). Future additions: `Fraction128` (the bounded `u128`-backed refinement of `Fraction`), `Q.96`, etc. The convention for spec-layer numeric types is: 1. **`Nat` underneath, exposed through a `structure`** so domain code can talk about "a Fraction" without exposing the encoding. `omega` works directly on the underlying `Nat`. 2. **Operations carry their preconditions in the type** (`Fraction.sub (h : b.bits ≤ a.bits)`, like `Account.debit`). 3. **`@[ext]` plus `@[simp]` projection lemmas for every field / operation** so `ext; simp` proofs compose cleanly downstream. 4. **`LE`/`LT` instances delegate to the underlying `Nat` order**; provide `Decidable` instances explicitly — they don't come for free from the `LE` instance, and `decide` will fail mysteriously without them. ```lean instance : LE Fraction := ⟨fun a b => a.bits ≤ b.bits⟩ instance (a b : Fraction) : Decidable (a ≤ b) := Nat.decLe a.bits b.bits ``` Bounded `UIntN`-backed refinements live as separate types (e.g. `Fraction128`) with `.toNat`-style bridges to the unbounded spec version, mirroring the `Lamports` / `LamportsUnchecked` pattern. ## Attribute usage - **`@[ext]`** on every `structure`. Generates the extensionality lemma `Foo.ext : a.f₁ = b.f₁ → … → a = b`. Costs nothing now, saves writing it later. - **`@[simp]`** on lemmas that rewrite *toward* a normal form. The classic case: projection lemmas that expose a field after a constructor or update. ```lean @[simp] theorem credit_lamports (a : Account) (n : Lamports) : (credit a n).lamports = a.lamports + n := rfl ``` Don't tag the headline theorem (`transfer_preserves_total`) with `@[simp]` — it's not a rewrite rule, it's a result. - **`@[reducible]`** — almost never needed at small library scale. `notation` is the right answer for type synonyms. ## Proof style - `by` at the end of the preceding line, never alone on its own line. - Tactic block indented 2 spaces. - Subgoals marked with `·` (centered dot), not `case`. - `omega` for `Nat`/`Int` linear arithmetic. `linarith` for ordered field arithmetic. `decide` for closed decidable goals.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen