lean4-htt/src/Lean/Elab/Binders.lean
Leonardo de Moura 79609938a8 feat: allow universe constraints to be postponed longer
Before this commit, each `isDefEq u v` invocation would fail if there
were pending universe level constraints. This commit, moves the
postponed universe constraints back to the `MetaM` state.
It also adds the combinator
```lean
withoutPostponingUniverseConstraints x
```
which executes `x` and throws an error if there are pending universe
constraints. We use the combinator at `elabApp` and `elabBinders`.
Without this commit, we would fail to elaborate simple terms such as
```lean
  Functor.map Prod.fst (x s)
```
because after elaborating `Prod.fst` and trying to ensure its type
match the expected one, we would be stuck at the universe constraint:
```
  u =?= max u ?v
```

Another benefit of the new approach is better error messages. Instead
of getting a mysterious type mismatch constraint, we get a list of
universe contraints the system is stuck at.

cc @Kha
2020-10-26 15:50:05 -07:00

535 lines
22 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.Elab.Term
import Lean.Elab.Quotation
namespace Lean.Elab.Term
open Meta
/--
Given syntax of the forms
a) (`:` term)?
b) `:` term
return `term` if it is present, or a hole if not. -/
private def expandBinderType (ref : Syntax) (stx : Syntax) : Syntax :=
if stx.getNumArgs == 0 then
mkHole ref
else
stx[1]
/-- Given syntax of the form `ident <|> hole`, return `ident`. If `hole`, then we create a new anonymous name. -/
private def expandBinderIdent (stx : Syntax) : TermElabM Syntax :=
match_syntax stx with
| `(_) => mkFreshIdent stx
| _ => pure stx
/-- Given syntax of the form `(ident >> " : ")?`, return `ident`, or a new instance name. -/
private def expandOptIdent (stx : Syntax) : TermElabM Syntax := do
if stx.getNumArgs == 0 then
pure $ mkIdentFrom stx (← mkFreshInstanceName)
else
pure stx[0]
structure BinderView :=
(id : Syntax)
(type : Syntax)
(bi : BinderInfo)
partial def quoteAutoTactic : Syntax → TermElabM Syntax
| stx@(Syntax.ident _ _ _ _) => throwErrorAt stx "invalic auto tactic, identifier is not allowed"
| stx@(Syntax.node k args) => do
if stx.isAntiquot then
throwErrorAt stx "invalic auto tactic, antiquotation is not allowed"
else
let quotedArgs ← `(Array.empty)
for arg in args do
if k == nullKind && Quotation.isAntiquotSplice arg then
throwErrorAt arg "invalic auto tactic, antiquotation is not allowed"
else
let quotedArg ← quoteAutoTactic arg
quotedArgs ← `(Array.push $quotedArgs $quotedArg)
`(Syntax.node $(quote k) $quotedArgs)
| Syntax.atom info val => `(Syntax.atom {} $(quote val))
| Syntax.missing => unreachable!
def declareTacticSyntax (tactic : Syntax) : TermElabM Name :=
withFreshMacroScope do
let name ← MonadQuotation.addMacroScope `_auto
let type := Lean.mkConst `Lean.Syntax
let tactic ← quoteAutoTactic tactic
let val ← elabTerm tactic type
let val ← instantiateMVars val
trace[Elab.autoParam]! val
let decl := Declaration.defnDecl { name := name, lparams := [], type := type, value := val, hints := ReducibilityHints.opaque, isUnsafe := false }
addDecl decl
compileDecl decl
pure name
/-
Expand `optional (binderTactic <|> binderDefault)`
def binderTactic := parser! " := " >> " by " >> tacticParser
def binderDefault := parser! " := " >> termParser
-/
private def expandBinderModifier (type : Syntax) (optBinderModifier : Syntax) : TermElabM Syntax := do
if optBinderModifier.isNone then
pure type
else
let modifier := optBinderModifier[0]
let kind := modifier.getKind
if kind == `Lean.Parser.Term.binderDefault then
let defaultVal := modifier[1]
`(optParam $type $defaultVal)
else if kind == `Lean.Parser.Term.binderTactic then
let tac := modifier[2]
let name ← declareTacticSyntax tac
`(autoParam $type $(mkIdentFrom tac name))
else
throwUnsupportedSyntax
private def getBinderIds (ids : Syntax) : TermElabM (Array Syntax) :=
ids.getArgs.mapM fun id =>
let k := id.getKind
if k == identKind || k == `Lean.Parser.Term.hole then
pure id
else
throwErrorAt id "identifier or `_` expected"
private def matchBinder (stx : Syntax) : TermElabM (Array BinderView) :=
match stx with
| Syntax.node k args => do
if k == `Lean.Parser.Term.simpleBinder then
-- binderIdent+
let ids ← getBinderIds args[0]
let type := mkHole stx
ids.mapM fun id => do pure { id := (← expandBinderIdent id), type := type, bi := BinderInfo.default }
else if k == `Lean.Parser.Term.explicitBinder then
-- `(` binderIdent+ binderType (binderDefault <|> binderTactic)? `)`
let ids ← getBinderIds args[1]
let type := expandBinderType stx args[2]
let optModifier := args[3]
let type ← expandBinderModifier type optModifier
ids.mapM fun id => do pure { id := (← expandBinderIdent id), type := type, bi := BinderInfo.default }
else if k == `Lean.Parser.Term.implicitBinder then
-- `{` binderIdent+ binderType `}`
let ids ← getBinderIds args[1]
let type := expandBinderType stx args[2]
ids.mapM fun id => do pure { id := (← expandBinderIdent id), type := type, bi := BinderInfo.implicit }
else if k == `Lean.Parser.Term.instBinder then
-- `[` optIdent type `]`
let id ← expandOptIdent args[1]
let type := args[2]
pure #[ { id := id, type := type, bi := BinderInfo.instImplicit } ]
else
throwUnsupportedSyntax
| _ => throwUnsupportedSyntax
private def registerFailedToInferBinderTypeInfo (type : Expr) (ref : Syntax) : TermElabM Unit :=
registerCustomErrorIfMVar type ref "failed to infer binder type"
private partial def elabBinderViews (binderViews : Array BinderView)
(i : Nat) (fvars : Array Expr) (lctx : LocalContext) (localInsts : LocalInstances) : TermElabM (Array Expr × LocalContext × LocalInstances) :=
if h : i < binderViews.size then
let binderView := binderViews.get ⟨i, h⟩
withRef binderView.type $ withLCtx lctx localInsts do
let type ← elabType binderView.type
registerFailedToInferBinderTypeInfo type binderView.type
let fvarId ← mkFreshFVarId
let fvar := mkFVar fvarId
let fvars := fvars.push fvar
let lctx := lctx.mkLocalDecl fvarId binderView.id.getId type binderView.bi
match (← isClass? type) with
| none => elabBinderViews binderViews (i+1) fvars lctx localInsts
| some className =>
resettingSynthInstanceCache do
let localInsts := localInsts.push { className := className, fvar := mkFVar fvarId }
elabBinderViews binderViews (i+1) fvars lctx localInsts
else
pure (fvars, lctx, localInsts)
private partial def elabBindersAux (binders : Array Syntax)
(i : Nat) (fvars : Array Expr) (lctx : LocalContext) (localInsts : LocalInstances) : TermElabM (Array Expr × LocalContext × LocalInstances) := do
if h : i < binders.size then
let binderViews ← matchBinder (binders.get ⟨i, h⟩)
let (fvars, lctx, localInsts) ← elabBinderViews binderViews 0 fvars lctx localInsts
elabBindersAux binders (i+1) fvars lctx localInsts
else
pure (fvars, lctx, localInsts)
/--
Elaborate the given binders (i.e., `Syntax` objects for `simpleBinder <|> bracketedBinder`),
update the local context, set of local instances, reset instance chache (if needed), and then
execute `x` with the updated context. -/
def elabBinders {α} (binders : Array Syntax) (x : Array Expr → TermElabM α) : TermElabM α :=
withoutPostponingUniverseConstraints do
if binders.isEmpty then
x #[]
else
let lctx ← getLCtx
let localInsts ← getLocalInstances
let (fvars, lctx, newLocalInsts) ← elabBindersAux binders 0 #[] lctx localInsts
resettingSynthInstanceCacheWhen (newLocalInsts.size > localInsts.size) $ withLCtx lctx newLocalInsts $
x fvars
@[inline] def elabBinder {α} (binder : Syntax) (x : Expr → TermElabM α) : TermElabM α :=
elabBinders #[binder] (fun fvars => x (fvars.get! 0))
@[builtinTermElab «forall»] def elabForall : TermElab := fun stx _ =>
match_syntax stx with
| `(forall $binders*, $term) =>
elabBinders binders fun xs => do
let e ← elabType term
mkForallFVars xs e
| _ => throwUnsupportedSyntax
@[builtinTermElab arrow] def elabArrow : TermElab :=
adaptExpander fun stx => match_syntax stx with
| `($dom:term -> $rng) => `(forall (a : $dom), $rng)
| _ => throwUnsupportedSyntax
@[builtinTermElab depArrow] def elabDepArrow : TermElab := fun stx _ =>
-- bracketedBinder `->` term
let binder := stx[0]
let term := stx[2]
elabBinders #[binder] fun xs => do
mkForallFVars xs (← elabType term)
/--
Auxiliary functions for converting `Term.app ... (Term.app id_1 id_2) ... id_n` into `#[id_1, ..., id_m]`
It is used at `expandFunBinders`. -/
private partial def getFunBinderIds? (stx : Syntax) : TermElabM (Option (Array Syntax)) :=
let rec loop (idOnly : Bool) (stx : Syntax) (acc : Array Syntax) :=
match_syntax stx with
| `($f $a) => do
if idOnly then
pure none
else
let (some acc) ← loop false f acc | pure none
loop true a acc
| `(_) => do let ident ← mkFreshIdent stx; pure (some (acc.push ident))
| `($id:ident) => pure (some (acc.push id))
| _ => pure none
loop false stx #[]
/--
Auxiliary function for expanding `fun` notation binders. Recall that `fun` parser is defined as
```
def funBinder : Parser := implicitBinder <|> instBinder <|> termParser maxPrec
parser! unicodeSymbol "λ" "fun" >> many1 funBinder >> "=>" >> termParser
```
to allow notation such as `fun (a, b) => a + b`, where `(a, b)` should be treated as a pattern.
The result is a pair `(explicitBinders, newBody)`, where `explicitBinders` is syntax of the form
```
`(` ident `:` term `)`
```
which can be elaborated using `elabBinders`, and `newBody` is the updated `body` syntax.
We update the `body` syntax when expanding the pattern notation.
Example: `fun (a, b) => a + b` expands into `fun _a_1 => match _a_1 with | (a, b) => a + b`.
See local function `processAsPattern` at `expandFunBindersAux`.
The resulting `Bool` is true if a pattern was found. We use it "mark" a macro expansion. -/
partial def expandFunBinders (binders : Array Syntax) (body : Syntax) : TermElabM (Array Syntax × Syntax × Bool) :=
let rec loop (body : Syntax) (i : Nat) (newBinders : Array Syntax) := do
if h : i < binders.size then
let binder := binders.get ⟨i, h⟩
let processAsPattern : Unit → TermElabM (Array Syntax × Syntax × Bool) := fun _ => do
let pattern := binder
let major ← mkFreshIdent binder
let (binders, newBody, _) ← loop body (i+1) (newBinders.push $ mkExplicitBinder major (mkHole binder))
let newBody ← `(match $major:ident with | $pattern => $newBody)
pure (binders, newBody, true)
match binder with
| Syntax.node `Lean.Parser.Term.implicitBinder _ => loop body (i+1) (newBinders.push binder)
| Syntax.node `Lean.Parser.Term.instBinder _ => loop body (i+1) (newBinders.push binder)
| Syntax.node `Lean.Parser.Term.explicitBinder _ => loop body (i+1) (newBinders.push binder)
| Syntax.node `Lean.Parser.Term.hole _ =>
let ident ← mkFreshIdent binder
let type := binder
loop body (i+1) (newBinders.push $ mkExplicitBinder ident type)
| Syntax.node `Lean.Parser.Term.paren args =>
-- `(` (termParser >> parenSpecial)? `)`
-- parenSpecial := (tupleTail <|> typeAscription)?
let binderBody := binder[1]
if binderBody.isNone then processAsPattern ()
else
let idents := binderBody[0]
let special := binderBody[1]
if special.isNone then processAsPattern ()
else if special[0].getKind != `Lean.Parser.Term.typeAscription then
processAsPattern ()
else
-- typeAscription := `:` term
let type := special[0][1]
match (← getFunBinderIds? idents) with
| some idents => loop body (i+1) (newBinders ++ idents.map (fun ident => mkExplicitBinder ident type))
| none => processAsPattern ()
| Syntax.ident _ _ _ _ =>
let type := mkHole binder
loop body (i+1) (newBinders.push $ mkExplicitBinder binder type)
| _ => processAsPattern ()
else
pure (newBinders, body, false)
loop body 0 #[]
namespace FunBinders
structure State :=
(fvars : Array Expr := #[])
(lctx : LocalContext)
(localInsts : LocalInstances)
(expectedType? : Option Expr := none)
private def propagateExpectedType (fvar : Expr) (fvarType : Expr) (s : State) : TermElabM State := do
match s.expectedType? with
| none => pure s
| some expectedType =>
let expectedType ← whnfForall expectedType
match expectedType with
| Expr.forallE _ d b _ =>
isDefEq fvarType d
let b := b.instantiate1 fvar
pure { s with expectedType? := some b }
| _ => pure { s with expectedType? := none }
private partial def elabFunBinderViews (binderViews : Array BinderView) (i : Nat) (s : State) : TermElabM State :=
if h : i < binderViews.size then
let binderView := binderViews.get ⟨i, h⟩
withRef binderView.type $ withLCtx s.lctx s.localInsts do
let type ← elabType binderView.type
registerFailedToInferBinderTypeInfo type binderView.type
let fvarId ← mkFreshFVarId
let fvar := mkFVar fvarId
let s := { s with fvars := s.fvars.push fvar }
-- dbgTrace (toString binderView.id.getId ++ " : " ++ toString type)
/-
We do **not** want to support default and auto arguments in lambda abstractions.
Example: `fun (x : Nat := 10) => x+1`.
We do not believe this is an useful feature, and it would complicate the logic here.
-/
let lctx := s.lctx.mkLocalDecl fvarId binderView.id.getId type binderView.bi
let s ← withRef binderView.id $ propagateExpectedType fvar type s
let s := { s with lctx := lctx }
match (← isClass? type) with
| none => elabFunBinderViews binderViews (i+1) s
| some className =>
resettingSynthInstanceCache do
let localInsts := s.localInsts.push { className := className, fvar := mkFVar fvarId }
elabFunBinderViews binderViews (i+1) { s with localInsts := localInsts }
else
pure s
partial def elabFunBindersAux (binders : Array Syntax) (i : Nat) (s : State) : TermElabM State := do
if h : i < binders.size then
let binderViews ← matchBinder (binders.get ⟨i, h⟩)
let s ← elabFunBinderViews binderViews 0 s
elabFunBindersAux binders (i+1) s
else
pure s
end FunBinders
def elabFunBinders {α} (binders : Array Syntax) (expectedType? : Option Expr) (x : Array Expr → Option Expr → TermElabM α) : TermElabM α :=
if binders.isEmpty then
x #[] expectedType?
else do
let lctx ← getLCtx
let localInsts ← getLocalInstances
let s ← FunBinders.elabFunBindersAux binders 0 { lctx := lctx, localInsts := localInsts, expectedType? := expectedType? }
resettingSynthInstanceCacheWhen (s.localInsts.size > localInsts.size) $ withLCtx s.lctx s.localInsts $
x s.fvars s.expectedType?
/-
Recall that
```
def typeSpec := parser! " : " >> termParser
def optType : Parser := optional typeSpec
``` -/
def expandOptType (ref : Syntax) (optType : Syntax) : Syntax :=
if optType.isNone then
mkHole ref
else
optType[0][1]
/- Helper function for `expandEqnsIntoMatch` -/
private def getMatchAltNumPatterns (matchAlts : Syntax) : Nat :=
let alt0 := matchAlts[1][0]
let pats := alt0[0].getSepArgs
pats.size
/- Helper function for `expandMatchAltsIntoMatch` -/
private def expandMatchAltsIntoMatchAux (ref : Syntax) (matchAlts : Syntax) (matchTactic : Bool) : Nat → Array Syntax → MacroM Syntax
| 0, discrs =>
pure $ Syntax.node (if matchTactic then `Lean.Parser.Tactic.match else `Lean.Parser.Term.match)
#[mkAtomFrom ref "match ", mkNullNode discrs, mkNullNode, mkAtomFrom ref " with ", matchAlts]
| n+1, discrs => withFreshMacroScope do
let x ← `(x)
let discrs := if discrs.isEmpty then discrs else discrs.push $ mkAtomFrom ref ", "
let discrs := discrs.push $ Syntax.node `Lean.Parser.Term.matchDiscr #[mkNullNode, x]
let body ← expandMatchAltsIntoMatchAux ref matchAlts matchTactic n discrs
if matchTactic then
`(tactic| intro $x:term; $body:tactic)
else
`(@fun $x => $body)
/--
Expand `matchAlts` syntax into a full `match`-expression.
Example
```
| 0, true => alt_1
| i, _ => alt_2
```
expands intro (for tactic == false)
```
fun x_1 x_2 =>
match x_1, x_2 with
| 0, true => alt_1
| i, _ => alt_2
```
and (for tactic == true)
```
intro x_1; intro x_2;
match x_1, x_2 with
| 0, true => alt_1
| i, _ => alt_2
```
-/
def expandMatchAltsIntoMatch (ref : Syntax) (matchAlts : Syntax) (tactic := false) : MacroM Syntax :=
expandMatchAltsIntoMatchAux ref matchAlts tactic (getMatchAltNumPatterns matchAlts) #[]
def expandMatchAltsIntoMatchTactic (ref : Syntax) (matchAlts : Syntax) : MacroM Syntax :=
expandMatchAltsIntoMatchAux ref matchAlts true (getMatchAltNumPatterns matchAlts) #[]
@[builtinTermElab «fun»] def elabFun : TermElab := fun stx expectedType? => do
-- "fun " >> ((many1 funBinder >> darrow >> termParser) <|> matchAlts)
if stx[1].isOfKind `Lean.Parser.Term.matchAlts then
let stxNew ← liftMacroM $ expandMatchAltsIntoMatch stx stx[1]
withMacroExpansion stx stxNew $ elabTerm stxNew expectedType?
else
let binders := stx[1].getArgs
let body := stx[3]
let (binders, body, expandedPattern) ← expandFunBinders binders body
if expandedPattern then
let newStx ← `(fun $binders* => $body)
withMacroExpansion stx newStx $ elabTerm newStx expectedType?
else
elabFunBinders binders expectedType? fun xs expectedType? => do
/- We ensure the expectedType here since it will force coercions to be applied if needed.
If we just use `elabTerm`, then we will need to a coercion `Coe (α → β) (α → δ)` whenever there is a coercion `Coe β δ`,
and another instance for the dependent version. -/
let e ← elabTermEnsuringType body expectedType?
mkLambdaFVars xs e
/- If `useLetExpr` is true, then a kernel let-expression `let x : type := val; body` is created.
Otherwise, we create a term of the form `(fun (x : type) => body) val`
The default elaboration order is `binders`, `typeStx`, `valStx`, and `body`.
If `elabBodyFirst == true`, then we use the order `binders`, `typeStx`, `body`, and `valStx`. -/
def elabLetDeclAux (n : Name) (binders : Array Syntax) (typeStx : Syntax) (valStx : Syntax) (body : Syntax)
(expectedType? : Option Expr) (useLetExpr : Bool) (elabBodyFirst : Bool) : TermElabM Expr := do
let (type, val, arity) ← elabBinders binders fun xs => do
let type ← elabType typeStx
registerCustomErrorIfMVar type typeStx "failed to infer 'let' declaration type"
if elabBodyFirst then
let type ← mkForallFVars xs type
let val ← mkFreshExprMVar type
pure (type, val, xs.size)
else
let val ← elabTermEnsuringType valStx type
let type ← mkForallFVars xs type
let val ← mkLambdaFVars xs val
pure (type, val, xs.size)
trace[Elab.let.decl]! "{n} : {type} := {val}"
let result ←
if useLetExpr then
withLetDecl n type val fun x => do
let body ← elabTerm body expectedType?
let body ← instantiateMVars body
mkLetFVars #[x] body
else
let f ← withLocalDecl n BinderInfo.default type fun x => do
let body ← elabTerm body expectedType?
let body ← instantiateMVars body
mkLambdaFVars #[x] body
pure $ mkApp f val
if elabBodyFirst then
forallBoundedTelescope type arity fun xs type => do
let valResult ← elabTermEnsuringType valStx type
let valResult ← mkLambdaFVars xs valResult
unless (← isDefEq val valResult) do
throwError "unexpected error when elaborating 'let'"
pure result
structure LetIdDeclView :=
(id : Name)
(binders : Array Syntax)
(type : Syntax)
(value : Syntax)
def mkLetIdDeclView (letIdDecl : Syntax) : LetIdDeclView :=
-- `letIdDecl` is of the form `ident >> many bracketedBinder >> optType >> " := " >> termParser
let id := letIdDecl[0].getId
let binders := letIdDecl[1].getArgs
let optType := letIdDecl[2]
let type := expandOptType letIdDecl optType
let value := letIdDecl[4]
{ id := id, binders := binders, type := type, value := value }
private def expandLetEqnsDeclVal (ref : Syntax) (alts : Syntax) : Nat → Array Syntax → MacroM Syntax
| 0, discrs =>
pure $ Syntax.node `Lean.Parser.Term.match
#[mkAtomFrom ref "match ", mkNullNode discrs, mkNullNode, mkAtomFrom ref " with ", alts]
| n+1, discrs => withFreshMacroScope do
let x ← `(x)
let discrs := if discrs.isEmpty then discrs else discrs.push $ mkAtomFrom ref ", "
let discrs := discrs.push $ Syntax.node `Lean.Parser.Term.matchDiscr #[mkNullNode, x]
let body ← expandLetEqnsDeclVal ref alts n discrs
`(fun $x => $body)
def expandLetEqnsDecl (letDecl : Syntax) : MacroM Syntax := do
let ref := letDecl
let matchAlts := letDecl[3]
let val ← expandMatchAltsIntoMatch ref matchAlts
pure $ Syntax.node `Lean.Parser.Term.letIdDecl #[letDecl[0], letDecl[1], letDecl[2], mkAtomFrom ref " := ", val]
def elabLetDeclCore (stx : Syntax) (expectedType? : Option Expr) (useLetExpr : Bool) (elabBodyFirst : Bool) : TermElabM Expr := do
let ref := stx
let letDecl := stx[1][0]
let body := stx[3]
if letDecl.getKind == `Lean.Parser.Term.letIdDecl then
let { id := id, binders := binders, type := type, value := val } := mkLetIdDeclView letDecl
elabLetDeclAux id binders type val body expectedType? useLetExpr elabBodyFirst
else if letDecl.getKind == `Lean.Parser.Term.letPatDecl then
-- node `Lean.Parser.Term.letPatDecl $ try (termParser >> pushNone >> optType >> " := ") >> termParser
let pat := letDecl[0]
let optType := letDecl[2]
let type := expandOptType stx optType
let val := letDecl[4]
let stxNew ← `(let x : $type := $val; match x with | $pat => $body)
let stxNew := match useLetExpr, elabBodyFirst with
| true, false => stxNew
| true, true => stxNew.updateKind `Lean.Parser.Term.«let*»
| false, true => stxNew.updateKind `Lean.Parser.Term.«let!»
| false, false => unreachable!
withMacroExpansion stx stxNew $ elabTerm stxNew expectedType?
else if letDecl.getKind == `Lean.Parser.Term.letEqnsDecl then
let letDeclIdNew ← liftMacroM $ expandLetEqnsDecl letDecl
let declNew := stx[1].setArg 0 letDeclIdNew
let stxNew := stx.setArg 1 declNew
withMacroExpansion stx stxNew $ elabTerm stxNew expectedType?
else
throwUnsupportedSyntax
@[builtinTermElab «let»] def elabLetDecl : TermElab :=
fun stx expectedType? => elabLetDeclCore stx expectedType? true false
@[builtinTermElab «let!»] def elabLetBangDecl : TermElab :=
fun stx expectedType? => elabLetDeclCore stx expectedType? false false
@[builtinTermElab «let*»] def elabLetStarDecl : TermElab :=
fun stx expectedType? => elabLetDeclCore stx expectedType? true true
builtin_initialize registerTraceClass `Elab.let
end Lean.Elab.Term