feat: turn a term elaborator into a syntax object with elabToSyntax (#11222)

This PR implements `elabToSyntax` for creating scoped syntax `s :
Syntax` for an arbitrary elaborator `el : Option Expr -> TermElabM Expr`
such that `elabTerm s = el`.

Roundtripping example implementing an elaborator imitating `let`:

```lean
elab "lett " decl:letDecl ";" e:term : term <= ty? => do
  let elabE (ty? : Option Expr) : TermElabM Expr := do elabTerm e ty?
  elabToSyntax elabE fun body => do
    elabTerm (← `(let $decl:letDecl; $body)) ty?

#guard lett x := 42; (x + 1) = 43
```
This commit is contained in:
Sebastian Graf 2025-11-18 08:10:31 +01:00 committed by GitHub
parent 5a4226f2bd
commit 59d2d00132
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 6 deletions

View file

@ -26,15 +26,31 @@ namespace Lean.Elab
namespace Term
-- Same pattern as for `Methods`/`MethodsRef` in `SimpM`. See `FixedTermElabRef`.
private opaque FixedTermElabRefPointed : NonemptyType.{0}
/--
This type is an abbreviation for `Option Expr → TermElabM Expr`, but avoids a circular dependency
with `TermElabM`.
We store `FixedTermElabRef` in the `Context` of the `TermElabM` monad, inducing the circular
dependency. This mechanism allows us to register semantic term elaborators in
`el : Option Expr → TermElabM Expr` as scoped `s : Syntax`, such that `elabTerm s = el`.
-/
def FixedTermElabRef : Type := FixedTermElabRefPointed.type
instance : Nonempty FixedTermElabRef :=
by exact FixedTermElabRefPointed.property
/-- Saved context for postponed terms and tactics to be executed. -/
structure SavedContext where
declName? : Option Name
options : Options
openDecls : List OpenDecl
macroStack : MacroStack
errToSorry : Bool
levelNames : List Name
declName? : Option Name
options : Options
openDecls : List OpenDecl
macroStack : MacroStack
errToSorry : Bool
levelNames : List Name
fixedTermElabs : Array FixedTermElabRef
/-- The kind of a tactic metavariable, used for additional error reporting. -/
inductive TacticMVarKind
@ -309,10 +325,28 @@ structure Context where
If `checkDeprecated := true`, then `Linter.checkDeprecated` when creating constants.
-/
checkDeprecated : Bool := true
/--
Fixed term elaborators for supporting `elabToSyntax`.
-/
fixedTermElabs : Array FixedTermElabRef := #[]
abbrev TermElabM := ReaderT Context $ StateRefT State MetaM
abbrev TermElab := Syntax → Option Expr → TermElabM Expr
abbrev FixedTermElab := Option Expr → TermElabM Expr
unsafe def FixedTermElab.toFixedTermElabRefImpl (m : FixedTermElab) : FixedTermElabRef :=
unsafeCast m
@[implemented_by FixedTermElab.toFixedTermElabRefImpl]
opaque FixedTermElab.toFixedTermElabRef (m : FixedTermElab) : FixedTermElabRef
unsafe def FixedTermElabRef.toFixedTermElabImpl (m : FixedTermElabRef) : FixedTermElab :=
unsafeCast m
@[implemented_by FixedTermElabRef.toFixedTermElabImpl]
opaque FixedTermElabRef.toFixedTermElab (m : FixedTermElabRef) : FixedTermElab
/-
Make the compiler generate specialized `pure`/`bind` so we do not have to optimize through the
whole monad stack at every use site. May eventually be covered by `deriving`.
@ -736,6 +770,26 @@ def withMacroExpansion [Monad n] [MonadControlT TermElabM n] (beforeStx afterStx
withMacroExpansionInfo beforeStx afterStx do
withPushMacroExpansionStack beforeStx afterStx <| runInBase x
/--
Reference a fixed term elaborator by index.
This syntax should never be constructed directly; rather it is an implementation detail of
`Lean.Elab.Term.elabToSyntax`.
-/
protected def _root_.Lean.Parser.Term.fixedTermElab := leading_parser
Lean.Parser.Term.num
/-- Refer to the given term elaborator by a scoped `Syntax` object. -/
def elabToSyntax (fixedTermElab : FixedTermElab) (k : Term → TermElabM α) : TermElabM α := do
let ctx ← read
withReader (fun ctx => { ctx with fixedTermElabs := ctx.fixedTermElabs.push fixedTermElab.toFixedTermElabRef }) do
k ⟨mkNode ``Lean.Parser.Term.fixedTermElab #[Syntax.mkNatLit ctx.fixedTermElabs.size]⟩
@[builtin_term_elab Lean.Parser.Term.fixedTermElab] def elabFixedTermElab : TermElab := fun stx expectedType? => do
let some idx := stx[0].isNatLit? | throwUnsupportedSyntax
let some fixedTermElab := (← read).fixedTermElabs[idx]?
| throwError "Fixed term elaborator {idx} not found. There were only {(← read).fixedTermElabs.size} fixed term elaborators registered."
fixedTermElab.toFixedTermElab expectedType?
/--
Add the given metavariable to the list of pending synthetic metavariables.
The method `synthesizeSyntheticMVars` is used to process the metavariables on this list. -/
@ -1226,6 +1280,7 @@ def saveContext : TermElabM SavedContext :=
openDecls := (← getOpenDecls)
errToSorry := (← read).errToSorry
levelNames := (← get).levelNames
fixedTermElabs := (← read).fixedTermElabs
}
/--

View file

@ -0,0 +1,10 @@
import Lean.Elab.Term
open Lean Expr Elab Term
elab "lett " decl:letDecl ";" e:term : term <= ty? => do
let elabE (ty? : Option Expr) : TermElabM Expr := do elabTerm e ty?
elabToSyntax elabE fun body => do
elabTerm (← `(let $decl:letDecl; $body)) ty?
#guard lett x := 42; (x + 1) = 43