fix: use getDecLevel/isLevelDefEq for for loop mut var universe constraints (#13332)

This PR fixes universe unification for `for` loops with `mut` variables
whose types span multiple implicit universes. The old approach used
`ensureHasType (mkSort mi.u.succ)` per variable, which generated
constraints like `max (?u+1) (?v+1) =?= ?u+1` that the universe solver
cannot decompose. The new approach uses `getDecLevel`/`isLevelDefEq` on
the decremented level, producing `max ?u ?v =?= ?u` which `solveSelfMax`
handles directly.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Graf 2026-04-08 18:34:51 +02:00 committed by GitHub
parent a3cc301de5
commit f9b2f6b597
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 2 deletions

View file

@ -111,8 +111,14 @@ open Lean.Meta
for x in loopMutVars do
let defn ← getLocalDeclFromUserName x.getId
Term.addTermInfo' x defn.toExpr
-- ForIn forces all mut vars into the same universe: that of the do block result type.
discard <| Term.ensureHasType (mkSort (mi.u.succ)) defn.type
-- 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)

View file

@ -0,0 +1,45 @@
import Std.Data.HashSet
/-!
Test that `for` loops with `mut` variables whose type spans multiple implicit universes
do not get stuck on `max (?u+1) (?v+1) =?= ?u+1`. This is solved by instead solving the decremented
constraint `max ?u ?v =?= ?u` eagerly, which solves by `solveSelfMax`.
-/
-- Works with explicit universe variables
example {α : Type (max u v)} {β : Type v} [Inhabited α] [Inhabited β] (as : Array α) : Array α := Id.run do
let mut m : α × β := default
for a in as do
m := (a, m.2)
return #[m.1]
-- Works with legacy do elaborator
set_option backward.do.legacy true in
example [Inhabited α] [Inhabited β] (as : Array α) : Array α := Id.run do
let mut m : α × β := default
for a in as do
m := (a, m.2)
return #[m.1]
-- Also works with implicit universe variables (the actual regression case)
example [Inhabited α] [Inhabited β] (as : Array α) : Array α := Id.run do
let mut m : α × β := default
for a in as do
m := (a, m.2)
return #[m.1]
-- Closer to the real world reproducer from aesop's UnionFind data structure:
def cluster [BEq α] [Hashable α] [BEq β] [Hashable β] (f : α → Array β)
(as : Array α) : Array (Array α) := Id.run do
let mut clusters := Std.HashSet.ofArray as
let mut aOccs : Std.HashMap β (Array α) := {}
for a in as do
for b in f a do
match aOccs[b]? with
| some as' =>
for a' in as' do
clusters := clusters.insert a'
aOccs := aOccs.insert b (as'.push a)
| none =>
aOccs := aOccs.insert b #[a]
return #[clusters.toArray]