lean4-htt/tests/elab/sym_intro_issue.lean
Leonardo de Moura 2964193af8
fix: avoid assigning mvar when Sym.intros produces no binders (#13451)
This PR fixes a bug in `Sym.introCore.finalize` where the original
metavariable was unconditionally assigned via a delayed assignment, even
when no binders were introduced. As a result, `Sym.intros` would return
`.failed` while the goal metavariable had already been silently
assigned, confusing downstream code that relies on `isAssigned` (e.g. VC
filters in `mvcgen'`).

The test and fix were suggested by Sebastian Graf (@sgraf812).

Co-authored-by: Sebastian Graf <sgraf1337@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:47:47 +00:00

38 lines
1.7 KiB
Text

/-
MWE: `Sym.introCore` assigns the original mvar even when no binders are introduced.
`Sym.intros` returns `.failed` but the mvar is secretly assigned.
The bug is in `introCore.finalize`: it unconditionally creates an `auxMVar` with a
delayed assignment and assigns the original mvar to it, even when `fvars` is empty
(no binders were introduced). Then `intros` checks `fvars.isEmpty` and returns `.failed`,
but the mvar is already assigned. Downstream code that checks `isAssigned` (e.g. VC filters
in mvcgen') will wrongly think the goal is solved.
Fix: guard `finalize` with `if fvars.isEmpty then return (#[], mvarId)`.
-/
import Lean
-- Demonstrate the bug by calling `Sym.intros` on a non-forall goal.
-- With the fix in `introCore.finalize`, this prints "GOOD".
-- Without the fix, it prints "BUG".
/--
info: before intros: isAssigned=false
intros returned .failed (as expected for non-forall target)
after intros: isAssigned=false, isDelayedAssigned=false
GOOD: mvar is still unassigned
-/
#guard_msgs in
open Lean Meta Sym in
#eval show MetaM Unit from do
let goal ← mkFreshExprMVar (mkConst ``False) .syntheticOpaque
let goalId := goal.mvarId!
IO.println s!"before intros: isAssigned={← goalId.isAssigned}"
let result ← Sym.SymM.run (Sym.intros goalId)
match result with
| .failed => IO.println "intros returned .failed (as expected for non-forall target)"
| .goal _ _ => IO.println "intros returned .goal (unexpected)"
IO.println s!"after intros: isAssigned={← goalId.isAssigned}, isDelayedAssigned={← goalId.isDelayedAssigned}"
if (← goalId.isAssigned) then
IO.println "BUG: mvar was assigned despite intros returning .failed"
else
IO.println "GOOD: mvar is still unassigned"