Zulip Chat Archive
Stream: metaprogramming / tactics
Topic: Auxiliary information for delaboration
Sergei Stepanenko (Jun 01 2025 at 16:00):
Hi! I'm working with an embedding of a programming language with binders. The variables in this language are abstract. However, for readability purposes, I want to store concrete names somewhere to use them during delaboration.
What are the options for that? My current solution is wrong, because it breaks the abstraction:
MWE
Is it possible to make Lean.Name to be irrelevant, but still use it for delaboration? I guess, one possible solution is to construct terms manually, and insert mdata for lambdas and variables, but probably there is something better.
Sergei Stepanenko (Jun 04 2025 at 15:54):
Probably not the best solution, but I ended up just attaching mdata to places where I need to save variable names and writing delaborators for certain keys.
import Lean
import Qq
inductive Expr : Type u → Type (u + 1)
| unit : {X : Type u} → Expr X
| var : {X : Type u} → X → Expr X
| lam : {X : Type u} → Expr (Option X) → Expr X
| app : {X : Type u} → Expr X → Expr X → Expr X
open Expr
declare_syntax_cat term_lang
syntax "⊤" : term_lang
syntax "(" term_lang ")" : term_lang
syntax ident : term_lang
syntax "λ " ident " . " term_lang : term_lang
syntax term_lang term_lang : term_lang
section Elab
open Lean Elab Meta PrettyPrinter Delaborator SubExpr Qq
abbrev ElabCtx := Vector Name
def getIdx? (ctx : ElabCtx n) (name : Name) : Option (Fin n) :=
Vector.finIdxOf? (Vector.reverse ctx) name
abbrev TmElabM n := ReaderT (ElabCtx n) TermElabM
def TmElabM.run {n} (Γ : ElabCtx n)
(x : TmElabM n α) : TermElabM α :=
ReaderT.run x Γ
def getTmCtx (n : Nat) : TmElabM n (ElabCtx n) :=
read
def constructDom (T : Type u) (n : Nat) : Type u :=
match n with
| 0 => T
| .succ n => Option (constructDom T n)
def constructVar (n : Nat) (m : Fin n) (T : Type u) : constructDom T n :=
match n with
| 0 => Fin.elim0 m
| .succ n => Fin.induction none (fun p _ => some (constructVar n p T)) m
def quoteName (name : Name) : Q(Name) := q($name)
partial def elabTM (l : Level) (n : Nat) (Γ : Q(Type l))
: TSyntax `term_lang →
TmElabM n Q(Expr $Γ)
| `(term_lang| ($t:term_lang)) =>
elabTM l n Γ t
| `(term_lang| ⊤ ) => do
return q(Expr.unit)
| `(term_lang| $id:ident) => do
let id := id.getId
let Ψ ← getTmCtx n
let R := q(PEmpty.{l + 1})
if let some m := getIdx? Ψ id then
let Γ' := q(constructDom $R $n)
let ⟨_⟩ ← assertDefEqQ (u := l) Γ Γ'
let v := q(constructVar $n $m $R)
return Expr.mdata (MData.empty.insert `Var (DataValue.ofName id)) q(Expr.var $v)
else
throwError "Unbound variable used: {id}"
| `(term_lang| λ $id:ident . $t:term_lang) => do
let id := id.getId
let m ← getTmCtx n
let e ← (elabTM l (n + 1) q(Option $Γ) t).run (m.push id)
return Expr.mdata (MData.empty.insert `Binder (DataValue.ofName id)) q(Expr.lam $e)
| `(term_lang| $t:term_lang $e:term_lang) => do
let a ← elabTM l n Γ t
let b ← elabTM l n Γ e
return q(Expr.app $a $b)
| _ => throwUnsupportedSyntax
elab "⟪" t:term_lang "⟫" : term => do
let u ← mkFreshLevelMVar
let R ← Qq.mkFreshExprMVarQ (q(Type u))
let T ← elabTM u 0 R t |>.run (Vector.ofFn (fun i => Fin.elim0 i))
pure T
@[delab app.Expr.unit]
def delabUnit : Delab := do
`(⟪ ⊤ ⟫)
@[delab app.Expr.app]
def delabApp : Delab := do
let e ← getExpr
guard $ e.isAppOfArity' `Expr.app 3
match ← delab (e.getArg! 1), ← delab (e.getArg! 2) with
| `(⟪$x:term_lang⟫), `(⟪$y:term_lang⟫) => `(⟪$x $y⟫)
| _, _ => failure
@[delab mdata.Binder]
def delabMDataLam : Delab := do
let .mdata d e ← SubExpr.getExpr | failure
let .some (.ofName x) := d.find `Binder | failure
guard $ e.isAppOfArity' `Expr.lam 2
let P := mkIdent x
match ← delab (e.getArg! 2) with
| `(⟪$x:term_lang⟫) => `(⟪ λ $P . $x:term_lang ⟫)
| _ => failure
@[delab mdata.Var]
def delabMDataVar : Delab := do
let .mdata d e ← SubExpr.getExpr | failure
let .some (.ofName x) := d.find `Var | failure
guard $ e.isAppOfArity' `Expr.var 2
let P := mkIdent x
`(⟪ $P:ident ⟫)
end Elab
def test1 := ⟪ λ y. y ⟫
def test2 := ⟪ λ x. x ⟫
example : test1 = test2 := by
unfold test1 test2
rfl
def test3 := ⟪ λ y. λ z. z ⟫
def test4 := ⟪ λ x. λ w. w ⟫
example : test3 = test4 := by
unfold test3 test4
rfl
Robin Arnez (Jun 04 2025 at 17:44):
What about using
inductive Expr : Type u → Type (u + 1)
| unit : {X : Type u} → Expr X
| var' : {X : Type u} → X → Expr X
| lam' : {X : Type u} → Expr (Option X) → Expr X
| app : {X : Type u} → Expr X → Expr X → Expr X
def Expr.var (_nm : Lean.Name) : X → Expr X := Expr.var'
def Expr.lam (_nm : Lean.Name) : Expr (Option X) → Expr X := Expr.lam'
and
elab "⟪" t:term_lang "⟫" : term => do
let u ← mkFreshLevelMVar
let R ← Qq.mkFreshExprMVarQ (q(Type u))
elabTM u 0 R t |>.run #v[]
with the old version?
Sergei Stepanenko (Jun 05 2025 at 11:25):
Robin Arnez said:
What about using
inductive Expr : Type u → Type (u + 1) | unit : {X : Type u} → Expr X | var' : {X : Type u} → X → Expr X | lam' : {X : Type u} → Expr (Option X) → Expr X | app : {X : Type u} → Expr X → Expr X → Expr X def Expr.var (_nm : Lean.Name) : X → Expr X := Expr.var' def Expr.lam (_nm : Lean.Name) : Expr (Option X) → Expr X := Expr.lam'and
elab "⟪" t:term_lang "⟫" : term => do let u ← mkFreshLevelMVar let R ← Qq.mkFreshExprMVarQ (q(Type u)) elabTM u 0 R t |>.run #v[]with the old version?
Thanks!
I think that is actually better!
I just started to learn about metaprogramming in Lean, so I might be wrong, but I didn't find any guarantees about mdata being preserved by tactics. On the other hand, here it is possible to provide some api, and seal Expr.var and Expr.lam, so there're no hidden conversions.
Jannis Limperg (Jun 05 2025 at 21:02):
Yes, mdata is completely unstable and even core tactics make no real effort to preserve it.
Last updated: Dec 20 2025 at 21:32 UTC