This PR fixes #12846, where the new do elaborator produced confusing errors when a do element's continuation had a mismatched monadic result type. The errors were misleading both in location (e.g., pointing at the value of `let x ← value` rather than the `let` keyword) and in content (e.g., mentioning `PUnit.unit` which the user never wrote). The fix introduces `DoElemCont.ensureUnitAt`/`ensureHasTypeAt`, which check the continuation result type early and report mismatches with a clear message ("The `do` element has monadic result type ... but the rest of the `do` block has monadic result type ..."). Each do-element elaborator (`let`, `have`, `let rec`, `for`, `unless`, `dbg_trace`, `assert!`, `idbg`, etc.) now captures its keyword token via `%$tk` and passes it to `ensureUnitAt` so that the error points at the do element rather than at an internal elaboration artifact. The old ad-hoc type check in `for` and the confusing `ensureHasType` call in `continueWithUnit` are replaced by this uniform mechanism. Additionally, `extractMonadInfo` now calls `instantiateMVars` on the expected type, and `While.lean`/`If.lean` macros propagate token info through their expansions. Closes #12846 --------- Co-authored-by: Rob23oba <robin.arnez@web.de>
197 lines
8.5 KiB
Text
197 lines
8.5 KiB
Text
/-
|
||
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
Authors: Sebastian Graf
|
||
-/
|
||
module
|
||
|
||
prelude
|
||
public import Lean.Elab.BuiltinDo.Basic
|
||
meta import Lean.Parser.Do
|
||
import Init.Control.Do
|
||
import Lean.Meta.ProdN
|
||
|
||
public section
|
||
|
||
namespace Lean.Elab.Do
|
||
|
||
open Lean.Parser.Term
|
||
open Lean.Meta
|
||
|
||
@[builtin_macro Lean.Parser.Term.doFor] def expandDoFor : Macro := fun stx => do
|
||
match stx with
|
||
| `(doFor| for $[$_ : ]? $_:ident in $_ do $_) =>
|
||
-- This is the target form of the expander, handled by `elabDoFor` below.
|
||
Macro.throwUnsupported
|
||
| `(doFor| for%$tk $decls:doForDecl,* do $body) =>
|
||
let decls := decls.getElems
|
||
let `(doForDecl| $[$h? : ]? $pattern in $xs) := decls[0]! | Macro.throwUnsupported
|
||
let mut doElems := #[]
|
||
let mut body := body
|
||
-- Expand `pattern` into an `Ident` `x`:
|
||
let x ←
|
||
if pattern.raw.isIdent then
|
||
pure ⟨pattern⟩
|
||
else if pattern.raw.isOfKind ``Lean.Parser.Term.hole then
|
||
Term.mkFreshIdent pattern
|
||
else
|
||
-- This case is a last resort, because it introduces a `match` and that will cause eager
|
||
-- defaulting. In practice this means that `mut` vars default to `Nat` too often.
|
||
-- Hence we try to only generate a `match` if we absolutely must.
|
||
let x ← Term.mkFreshIdent pattern
|
||
body ← `(doSeq| match $x:term with | $pattern => $body)
|
||
pure x
|
||
-- Expand the remaining `doForDecl`s:
|
||
for doForDecl in decls[1...*] do
|
||
/-
|
||
Expand
|
||
```
|
||
for x in xs, y in ys do
|
||
body
|
||
```
|
||
into
|
||
```
|
||
let mut s := Std.toStream ys
|
||
for x in xs do
|
||
match Std.Stream.next? s with
|
||
| none => break
|
||
| some (y, s') =>
|
||
s := s'
|
||
body
|
||
```
|
||
-/
|
||
let `(doForDecl| $[$h? : ]? $y in $ys) := doForDecl | Macro.throwUnsupported
|
||
if let some h := h? then
|
||
Macro.throwErrorAt h "The proof annotation here has not been implemented yet."
|
||
/- Recall that `@` (explicit) disables `coeAtOutParam`.
|
||
We used `@` at `Stream` functions to make sure `resultIsOutParamSupport` is not used. -/
|
||
let toStreamApp ← withRef ys `(@Std.toStream _ _ _ $ys)
|
||
let s := mkIdentFrom ys (← withFreshMacroScope <| MonadQuotation.addMacroScope `__s)
|
||
doElems := doElems.push (← `(doSeqItem| let mut $s := $toStreamApp:term))
|
||
body ← `(doSeq|
|
||
match @Std.Stream.next? _ _ _ $s with
|
||
| none => break
|
||
| some ($y, s') =>
|
||
$s:ident := s'
|
||
do $body)
|
||
doElems := doElems.push (← `(doSeqItem| for%$tk $[$h? : ]? $x:ident in $xs do $body))
|
||
`(doElem| do $doElems*)
|
||
| _ => Macro.throwUnsupported
|
||
|
||
@[builtin_doElem_elab Lean.Parser.Term.doFor] def elabDoFor : DoElab := fun stx dec => do
|
||
let `(doFor| for%$tk $[$h? : ]? $x:ident in $xs do $body) := stx | throwUnsupportedSyntax
|
||
let dec ← dec.ensureUnitAt tk
|
||
checkMutVarsForShadowing #[x]
|
||
let uα ← mkFreshLevelMVar
|
||
let uρ ← mkFreshLevelMVar
|
||
let α ← mkFreshExprMVar (mkSort (uα.succ)) (userName := `α) -- assigned by outParam
|
||
let ρ ← mkFreshExprMVar (mkSort (uρ.succ)) (userName := `ρ) -- assigned in the next line
|
||
let xs ← Term.elabTermEnsuringType xs ρ
|
||
let mi := (← read).monadInfo
|
||
let mutVars := (← read).mutVars
|
||
|
||
let info ← inferControlInfoSeq body
|
||
let oldReturnCont ← getReturnCont
|
||
let returnVarName ← mkFreshUserName `__r
|
||
let loopMutVars := mutVars.filter fun x => info.reassigns.contains x.getId
|
||
let loopMutVarNames :=
|
||
if info.returnsEarly then
|
||
returnVarName :: (loopMutVars.map (·.getId)).toList
|
||
else
|
||
(loopMutVars.map (·.getId)).toList
|
||
let useLoopMutVars (e : Option Expr) : TermElabM (Array Expr) := do
|
||
let mut defs := #[]
|
||
unless e.isNone || info.returnsEarly do
|
||
throwError "Early returning {e} but the info said there is no early return"
|
||
if info.returnsEarly then
|
||
let returnVar ←
|
||
match e with
|
||
| none => mkNone oldReturnCont.resultType
|
||
| some e => mkSome oldReturnCont.resultType e
|
||
defs := defs.push returnVar
|
||
for x in loopMutVars do
|
||
let defn ← getLocalDeclFromUserName x.getId
|
||
Term.addTermInfo' x defn.toExpr
|
||
-- ForIn forces the mut tuple into the universe mi.u: that of the do block result type.
|
||
-- If we don't do this, then we are stuck on solving constraints such as
|
||
-- `max ?u.46 ?u.47 =?= max (max ?u.22 ?u.46) ?u.47`
|
||
-- It's important we do this as a separate isLevelDefEq check on the decremented level because
|
||
-- otherwise (`ensureHasType (mkSort mi.u.succ)`) we are stuck on constraints like
|
||
-- `max (?u+1) (?v+1) =?= ?u+1`
|
||
let u ← getDecLevel defn.type
|
||
discard <| isLevelDefEq u mi.u
|
||
defs := defs.push defn.toExpr
|
||
if info.returnsEarly && loopMutVars.isEmpty then
|
||
defs := defs.push (mkConst ``Unit.unit)
|
||
return defs
|
||
|
||
let (preS, σ) ← mkProdMkN (← useLoopMutVars none) mi.u
|
||
|
||
let (app, p?) ← match h? with
|
||
| none =>
|
||
let instForIn ← Term.mkInstMVar <| mkApp3 (mkConst ``ForIn [uρ, uα, mi.u, mi.v]) mi.m ρ α
|
||
let app := mkConst ``ForIn.forIn [uρ, uα, mi.u, mi.v]
|
||
-- ForIn.forIn : {m ρ α : _} → [ForIn m ρ α] → {β : _} → ρ → β → (α → β → m (ForInStep β)) → m β
|
||
let app := mkApp7 app mi.m ρ α instForIn σ xs preS -- 1 arg remaining: loop body
|
||
pure (app, none)
|
||
| some _ =>
|
||
let d ← mkFreshExprMVar (mkApp2 (mkConst ``Membership [uα, uρ]) α ρ) (userName := `d) -- outParam
|
||
let instForIn ← Term.mkInstMVar <| mkApp4 (mkConst ``ForIn' [uρ, uα, mi.u, mi.v]) mi.m ρ α d
|
||
let app := mkConst ``ForIn'.forIn' [uρ, uα, mi.u, mi.v]
|
||
-- ForIn'.forIn' : {m ρ α : _} → [Membership α ρ] → [ForIn' m ρ α d] → {β : _} → ρ → β → ((a : α) → a ∈ x → β → m (ForInStep β)) → m β
|
||
let app := mkApp8 app mi.m ρ α d instForIn σ xs preS -- 1 arg remaining: loop body
|
||
pure (app, some d)
|
||
let s ← mkFreshUserName `__s
|
||
let xh : Array (Name × (Array Expr → DoElabM Expr)) := match h?, p? with
|
||
| some h, some d =>
|
||
#[(x.getId, fun _ => pure α),
|
||
(h.getId, fun x => pure (mkApp5 (mkConst ``Membership.mem [uα, uρ]) α ρ d xs x[0]!))]
|
||
| _, _ =>
|
||
#[(x.getId, fun _ => pure α)]
|
||
|
||
let body ←
|
||
withLocalDeclsD xh fun xh => do
|
||
Term.addLocalVarInfo x xh[0]!
|
||
if let some h := h? then
|
||
Term.addLocalVarInfo h xh[1]!
|
||
withLocalDecl s .default σ (kind := .implDetail) fun loopS => do
|
||
mkLambdaFVars (xh.push loopS) <| ← do
|
||
bindMutVarsFromTuple loopMutVarNames loopS.fvarId! do
|
||
let newDoBlockResultType := mkApp (mkConst ``ForInStep [mi.u]) σ
|
||
withDoBlockResultType newDoBlockResultType do
|
||
let continueCont := do
|
||
let (tuple, _tupleTy) ← mkProdMkN (← useLoopMutVars none) mi.u
|
||
let yield := mkApp2 (mkConst ``ForInStep.yield [mi.u]) σ tuple
|
||
mkPureApp newDoBlockResultType yield
|
||
let breakCont := do
|
||
let (tuple, _tupleTy) ← mkProdMkN (← useLoopMutVars none) mi.u
|
||
let done := mkApp2 (mkConst ``ForInStep.done [mi.u]) σ tuple
|
||
mkPureApp newDoBlockResultType done
|
||
let returnCont := { oldReturnCont with k := fun e => do
|
||
let (tuple, _tupleTy) ← mkProdMkN (← useLoopMutVars (some e)) mi.u
|
||
let done := mkApp2 (mkConst ``ForInStep.done [mi.u]) σ tuple
|
||
mkPureApp newDoBlockResultType done
|
||
}
|
||
enterLoopBody breakCont continueCont returnCont do
|
||
-- Elaborate the loop body, which must have result type `PUnit`, just like the whole `for` loop.
|
||
elabDoSeq body { dec with k := continueCont, kind := .duplicable }
|
||
|
||
let forIn := mkApp app body
|
||
|
||
let γ := (← read).doBlockResultType
|
||
let rest ←
|
||
withLocalDeclD s σ fun postS => do mkLambdaFVars #[postS] <| ← do
|
||
bindMutVarsFromTuple loopMutVarNames postS.fvarId! do
|
||
if info.returnsEarly then
|
||
let ret ← getFVarFromUserName returnVarName
|
||
let ret ← if loopMutVars.isEmpty then mkAppM ``Prod.fst #[ret] else pure ret
|
||
let motive := mkLambda `_ .default (← inferType ret) (← mkMonadicType γ)
|
||
let app := mkApp3 (mkConst ``Break.runK.match_1 [mi.u, mi.v.succ]) oldReturnCont.resultType motive ret
|
||
let none := mkSimpleThunk (← dec.continueWithUnit)
|
||
let some ← withLocalDeclD (← mkFreshUserName `r) oldReturnCont.resultType fun r => do
|
||
mkLambdaFVars #[r] (← oldReturnCont.k r)
|
||
return mkApp2 app some none
|
||
else
|
||
dec.continueWithUnit
|
||
|
||
mkBindApp σ γ forIn rest
|