Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/leanprover/lean4/llms.txt

Use this file to discover all available pages before exploring further.

Elaboration is the process that converts parsed Syntax into type-correct kernel Expr terms. It is the richest and most powerful layer of Lean 4 metaprogramming, giving you access to types, the environment, unification, and declaration creation. This page surveys the full elaboration API from the ground up.

The Elaboration Pipeline

When Lean processes a file, each command passes through these stages:
  1. ParsingSyntax tree with source positions, no name resolution.
  2. Macro expansionmacro_rules rewrite SyntaxSyntax (in MacroM).
  3. Command elaborationCommandElabM dispatches to term or declaration elaborators.
  4. Term elaborationTermElabM converts term syntax into Expr, resolving names, inferring types, synthesizing instances.
  5. Tactic evaluation — within by blocks, TacticM reduces goals to Expr witnesses.
  6. Kernel checking — the trusted kernel independently verifies the produced Expr.
Each stage has its own monad, each extending the one below.

CoreM — The Foundation

CoreM is the base monad for all metaprogramming in Lean. It provides:
open Lean Core in
-- Read the global environment
#check (getEnv : CoreM Environment)

-- Modify the environment
#check (modifyEnv : (Environment → Environment) → CoreM Unit)

-- Get compiler options
#check (getOptions : CoreM Options)

-- Throw a structured error
#check (throwError : MessageData → CoreM α)

-- Log informational messages
#check (logInfo : MessageData → CoreM Unit)
#check (logWarning : MessageData → CoreM Unit)

Accessing the Environment

The Environment holds every declaration, theorem, and axiom in scope:
open Lean in
def lookupConst (name : Name) : CoreM (Option ConstantInfo) := do
  return (← getEnv).find? name

-- ConstantInfo variants: defnInfo, thmInfo, axiomInfo, opaqueInfo, quotInfo, inductInfo, ctorInfo, recInfo
open Lean in
def getConstType (name : Name) : CoreM Expr := do
  let env ← getEnv
  match env.find? name with
  | some ci => return ci.type
  | none    => throwError "unknown constant: {name}"

MetaM — Unification and Type Inference

MetaM extends CoreM with a metavariable context and type-checking facilities. It is the workhorse for most non-trivial metaprogramming.

Metavariables

Metavariables (MVarId) are typed holes filled in during unification:
open Lean Meta in
example : MetaM Unit := do
  -- Create a metavariable of type Nat
  let mvar ← mkFreshExprMVar (mkConst ``Nat) .synthetic .anonymous
  -- Inspect its type
  let ty ← inferType mvar
  logInfo m!"mvar type: {ty}"

Definitional Equality: isDefEq

isDefEq checks whether two expressions are definitionally equal, potentially unifying metavariables:
open Lean Meta in
def tryUnify (t s : Expr) : MetaM Bool := do
  isDefEq t s

-- isDefEq is the basis of all unification in elaboration.
-- It respects β-reduction, δ-reduction, ι-reduction, and ζ-reduction.

Weak-Head Normal Form: whnf

whnf reduces an expression to weak-head normal form — enough reduction to expose the outermost constructor or lambda:
open Lean Meta in
def reduceToHead (e : Expr) : MetaM Expr := do
  whnf e

-- Example: whnf `(Nat.succ (Nat.succ 0))` → `Nat.succ (Nat.succ 0)` (already WHNF)
-- Example: whnf `(List.length [1,2,3])` → reduces the definition

Type Inference

open Lean Meta in
def checkType (e : Expr) : MetaM Expr := do
  inferType e

Building Expressions

Expr is Lean 4’s internal representation of terms — a de Bruijn indexed λ-calculus with constants, applications, and universe levels.
open Lean Meta in
example : MetaM Expr := do
  -- mkAppN: apply a function to multiple arguments
  let f := mkConst ``Nat.add
  let a := mkNatLit 3
  let b := mkNatLit 4
  let app := mkAppN f #[a, b]  -- Nat.add 3 4

  -- mkForallFVars: abstract over free variables to form a ∀-type
  withLocalDecl `n .default (mkConst ``Nat) fun nFVar => do
    let body := mkApp2 (mkConst ``Nat.succ) nFVar nFVar
    let forallExpr ← mkForallFVars #[nFVar] body  -- ∀ (n : Nat), Nat.succ n

  -- mkLambdaFVars: abstract over free variables to form a λ
  withLocalDecl `x .default (mkConst ``Nat) fun xFVar => do
    let body := mkApp (mkConst ``Nat.succ) xFVar
    let lam ← mkLambdaFVars #[xFVar] body  -- fun (x : Nat) => Nat.succ x

  return mkNatLit 0  -- placeholder

Telescopes

Telescopes decompose Pi/Lambda chains into arrays of free variables:
open Lean Meta in
def analyzeType (ty : Expr) : MetaM Unit :=
  forallTelescope ty fun params resultType => do
    logInfo m!"Parameters: {params}"
    logInfo m!"Result type: {resultType}"

-- Also: lambdaTelescope, forallTelescopeReducing

TermElabM — Term Elaboration

TermElabM extends MetaM with the full term elaborator.

Elaborating Terms

open Lean Elab Term in
-- The main entry point: elaborate a syntax node with an optional expected type
def myElab (stx : Syntax) : TermElabM Expr := do
  elabTerm stx none  -- no expected type
  -- or: elabTerm stx (some expectedType)

Introducing Local Declarations

open Lean Meta in
example : MetaM Expr :=
  -- withLocalDecl: introduce a hypothesis into the local context
  withLocalDecl `h .default (mkConst ``Nat) fun hFVar => do
    -- hFVar is a free variable of type Nat in scope here
    return hFVar

-- withLocalDeclD: same but with "default" binder info (explicit)
-- withLetDecl: introduce a let-binding

Registering Term Elaborators

open Lean Elab Term in
-- Register with elab_rules
elab_rules : term
  | `(myTerm $n:num) => do
      return mkNatLit n.getNat

-- Or with the low-level @[term_elab] attribute
@[term_elab myTermSyntaxKind]
def myTermElabFn : TermElab := fun stx expectedType? => do
  -- stx is the matched syntax, expectedType? is the expected type if known
  elabTerm stx[1] expectedType?

CommandElabM — Command Elaboration

Commands are top-level declarations, #check, def, theorem, etc. The CommandElabM monad runs them.
open Lean Elab Command in
-- Lift a MetaM action into CommandElabM
def myCommand : CommandElabM Unit := do
  liftTermElabM do
    let ty ← inferType (mkConst ``Nat.add)
    logInfo m!"Nat.add has type {ty}"

Adding Declarations

open Lean in
-- addDecl: add a declaration to the environment (kernel-checked)
-- addAndCompile: add and also compile to bytecode/native

def addSimpleDef (name : Name) (value type : Expr) : CoreM Unit := do
  let decl := Declaration.defnDecl {
    name        := name
    levelParams := []
    type        := type
    value       := value
    hints       := .regular (getReducibilityHintsNumHeartbeats (← getOptions))
    safety      := .safe
  }
  addDecl decl
addAndCompile additionally compiles the declaration to bytecode, making it available for #eval and native_decide:
open Lean in
-- addAndCompile (decl : Declaration) : CoreM Unit
-- Same as addDecl but also runs the compiler

A Complete Custom Command

import Lean
open Lean Elab Command Meta

syntax (name := showType) "#showType" ident : command

@[command_elab showType]
def elabShowType : CommandElab
  | `(#showType $id:ident) => do
      let name ← resolveGlobalConstNoOverload id
      let ty ← liftTermElabM <| do
        let ci ← getConstInfo name
        return ci.type
      logInfo m!"{name} : {ty}"
  | _ => throwUnsupportedSyntax

#showType Nat.add
-- Nat.add : Nat → Nat → Nat

Logging and Error Reporting

All monads above CoreM inherit these logging functions:
open Lean in
example : CoreM Unit := do
  -- Informational message (shown in VS Code info widget)
  logInfo m!"Found {42} things"

  -- Warning (shown as yellow squiggle)
  logWarning m!"This looks suspicious"

  -- Error that aborts the current elaboration
  throwError "Expected a Nat literal, got something else"

  -- Error attached to a specific syntax node
  throwErrorAt stx "This token is invalid here"
The m! interpolation syntax is MessageData, which supports pretty-printing of Expr, Name, Level, and Format values:
open Lean in
example (e : Expr) (n : Name) : CoreM Unit := do
  logInfo m!"Expression: {e}"   -- pretty-printed
  logInfo m!"Name: {n}"         -- dotted name
  logInfo m!"Indented: {indentExpr e}"

Build docs developers (and LLMs) love