fix: correctly collect let-rec fvars through delayed-assigned mvar (#7304)

This PR fixes an issue where nested `let rec` declarations within
`match` expressions or tactic blocks failed to compile if they were
nested within, and recursively called, a `let rec` that referenced a
variable bound by a containing declaration.

Closes #6927

---------

Co-authored-by: Joachim Breitner <mail@joachim-breitner.de>
This commit is contained in:
jrr6 2025-03-10 14:13:48 -04:00 committed by GitHub
parent 9d39942189
commit acfc11ae42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 138 additions and 5 deletions

View file

@ -614,16 +614,26 @@ private def mkInitialUsedFVarsMap [Monad m] [MonadMCtx m] (sectionVars : Array E
for mainFVarId in mainFVarIds do
usedFVarMap := usedFVarMap.insert mainFVarId sectionVarSet
for toLift in letRecsToLift do
let state := Lean.collectFVars {} toLift.val
let state := Lean.collectFVars state toLift.type
let mut set := state.fvarSet
let mut state := Lean.collectFVars {} toLift.val
state := Lean.collectFVars state toLift.type
let mut set := {}
/- toLift.val may contain metavariables that are placeholders for nested let-recs. We should collect the fvarId
for the associated let-rec because we need this information to compute the fixpoint later. -/
let mvarIds := (toLift.val.collectMVars {}).result
for mvarId in mvarIds do
match (← letRecsToLift.findSomeM? fun (toLift : LetRecToLift) => return if toLift.mvarId == (← getDelayedMVarRoot mvarId) then some toLift.fvarId else none) with
let root ← getDelayedMVarRoot mvarId
match letRecsToLift.findSome? fun (toLift : LetRecToLift) => if toLift.mvarId == root then some toLift.fvarId else none with
| some fvarId => set := set.insert fvarId
| none => pure ()
| none =>
/- If the metavariable is not a nested let-rec, it may contribute additional free-variable
dependencies not caught in the fixed-point routine. In particular, delayed assignments
due to `match` expressions or tactic blocks induce fvar dependencies that we need to
account for (see #6927) but cannot ascertain through instantiation if those expressions
contain still-unassigned metavariable placeholders for other let-recs. See Note
[Delayed-Assigned Metavariables in Free Variable Collection] for more information. -/
let some rootAssignment ← getExprMVarAssignment? root | continue
state := Lean.collectFVars state rootAssignment
set := state.fvarSet.union set
usedFVarMap := usedFVarMap.insert toLift.fvarId set
return usedFVarMap
@ -1170,3 +1180,33 @@ builtin_initialize
end Command
end Lean.Elab
/-!
# Note [Delayed-Assigned Metavariables in Free Variable Collection]
Nested declarations using `let rec` should compile correctly even when nested within expressions
that are elaborated using delayed metavariable assignments, such as `match` expressions and tactic
blocks. Previously, declaring a `let rec` within such an expression in the following fashion
```lean
def f x :=
let rec g :=
match ... with
| pat =>
let rec h := ... g ...
... x ...
```
where `g` depends on some free variable bound by `f` (like `x` above) would cause `MutualClosure` to
fail to detect that transitive fvar dependency of `h` (which must pass it as an argument to `g`),
leading to an unbound fvar in the body of `h` that was ultimately fed to the kernel. This occurred
because `MutualClosure` processes let-recs from most to least nested. Initially, the body of `g` is
an application of the delayed-assigned metavariable generated by `match` elaboration; the root
metavariable of that delayed assignment is, in turn, assigned to an expression that refers to the
mvar that will eventually be assigned to `g` once we process that declaration. Therefore, when we
initially process the most-nested declaration `h` (before `g`), we cannot instantiate the
`match`-expression mvar's delayed assignment (since the metavariable that will eventually be
assigned to the yet-unprocessed `g` remains unassigned). Thus, we do not detect any of the fvar
dependencies of `g` in the `match` body -- namely, that corresponding to `x`, which `h` should
therefore also take as a parameter. This also caused a knock-on effect in certain situations,
wherein `h` would compile as an `axiom` rather than as `opaque`, rendering `f` erroneously
noncomputable.
-/

93
tests/lean/run/6927.lean Normal file
View file

@ -0,0 +1,93 @@
import Lean
/-
# Nested `let rec` declarations in the presence of delayed metavariable assignments
https://github.com/leanprover/lean4/issues/6927
Nested declarations using `let rec` should compile correctly even when nested within expressions
that are elaborated using delayed metavariable assignments, such as `match` expressions and tactic
blocks. See the Note [Delayed-Assigned Metavariables in Free Variable Collection] in
`MutualDef.lean` for additional details.
-/
inductive Tree : Type where
| leaf
| node (p : Nat) (brs : List Tree)
def Tree.substNats (M : Tree) (σ : Nat → Nat) : Tree :=
let rec go M : Tree := match M with
| .leaf => leaf
| .node p brs =>
let rec goList : List Tree → List Tree
| [] => []
| M :: Ms => go M :: goList Ms
.node (σ p) (goList brs)
go M
/-
Previously, free variable dependencies in `inner` were not correctly detected, leading that
declaration to contain free variables when sent to the kernel.
-/
namespace Ex1
def outer (f : Nat → Nat) :=
let rec middle n := match n with
| Nat.zero => 0 | .succ _ =>
let rec inner := middle n
f 0
()
end Ex1
/-
`by` blocks induced behavior similar to the above, since they also produce metavariables during
elaboration.
-/
def tac (f : Nat → Nat) :=
let rec middle n := by
let rec inner := middle (Nat.succ n)
exact f 0
()
/- Previously, `inner` was incorrectly compiled as an `axiom`, rendering `outer` noncomputable. -/
namespace Ex2
/--
error: fail to show termination for
Ex2.outer.middle.inner
Ex2.outer.middle
with errors
failed to infer structural recursion:
Not considering parameter f of Ex2.outer.middle.inner:
it is unchanged in the recursive calls
Not considering parameter f of Ex2.outer.middle:
it is unchanged in the recursive calls
Cannot use parameters n✝ of Ex2.outer.middle.inner and n of Ex2.outer.middle:
failed to eliminate recursive application
outer.middle f✝ (n + 1)
Cannot use parameters n of Ex2.outer.middle.inner and n of Ex2.outer.middle:
failed to eliminate recursive application
outer.middle f✝ (n + 1)
Could not find a decreasing measure.
The basic measures relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
Call from Ex2.outer.middle.inner to Ex2.outer.middle at 89:6-20:
n✝ n
n ? ?
Call from Ex2.outer.middle to Ex2.outer.middle.inner at 91:4-11:
n
n✝ _
n _
Please use `termination_by` to specify a decreasing measure.
-/
#guard_msgs in
def outer (f : Nat → Nat) (n : Nat) : Nat :=
let rec middle n : Nat := match n with
| 0 => 1
| n + 1 =>
let rec inner (n : Nat) : Nat :=
middle (n + 1)
let _ := f
inner n
middle n
end Ex2