fix: unfoldReducible' optimization regression in grind (#9682)

This PR fixes a regression introduced by an optimization in the
`unfoldReducible` step used by the `grind` normalizer. It also ensures
that projection functions are not reduced, as they are folded in a later
step.
This commit is contained in:
Leonardo de Moura 2025-08-02 14:57:25 +02:00 committed by GitHub
parent 08c3f3c236
commit 3056848819
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 5 deletions

View file

@ -41,15 +41,19 @@ def dsimpCore (e : Expr) : GrindM Expr := do profileitM Exception "grind dsimp"
/--
Unfolds all `reducible` declarations occurring in `e`.
Similar to `unfoldReducible`, but uses `inShareCommon` as an extra filter
Similar to `unfoldReducible`, but uses `alreadyInternalized` as an extra filter
-/
def unfoldReducible' (e : Expr) : GrindM Expr := do
def unfoldReducible' (e : Expr) : GoalM Expr := do
if !(← isUnfoldReducibleTarget e) then return e
let pre (e : Expr) : GrindM TransformStep := do
if (← inShareCommon e) then return .done e
let pre (e : Expr) : GoalM TransformStep := do
-- We used to use `inShareCommon` here, but it was to correct.
-- `inShareCommon` is used in terms that have not been preprocessed (e.g., proofs).
if (← alreadyInternalized e) then return .done e
let .const declName _ := e.getAppFn | return .continue
unless (← isReducible declName) do return .continue
if isGrindGadget declName then return .continue
-- See comment at isUnfoldReducibleTarget.
if (← getEnv).isProjectionFn declName then return .continue
let some v ← unfoldDefinition? e | return .continue
return .visit v
Core.transform e (pre := pre)

View file

@ -59,7 +59,9 @@ def isUnfoldReducibleTarget (e : Expr) : CoreM Bool := do
return Option.isSome <| e.find? fun e => Id.run do
let .const declName _ := e | return false
if getReducibilityStatusCore env declName matches .reducible then
return !isGrindGadget declName
-- Remark: it is wasteful to unfold projection functions since
-- kernel projections are folded again in the `foldProjs` preprocessing step.
return !isGrindGadget declName && !env.isProjectionFn declName
else
return false
@ -72,6 +74,8 @@ def unfoldReducible (e : Expr) : MetaM Expr := do
let .const declName _ := e.getAppFn | return .continue
unless (← isReducible declName) do return .continue
if isGrindGadget declName then return .continue
-- See comment at isUnfoldReducibleTarget.
if (← getEnv).isProjectionFn declName then return .continue
let some v ← unfoldDefinition? e | return .continue
return .visit v
Core.transform e (pre := pre)

View file

@ -0,0 +1,68 @@
import Std
open Std
/--
An extensional tree map with a default value.
To preserve extensionality, we require that the default value is not present in the tree.
**Implementation note**: we use `Ord α` rather than a `cmp : αα → Ordering` argument,
because `grind` can not instantiate `ReflCmp` and `TransCmp` theorems because there is no constant to key on.
-/
structure TreeMapD (α : Type u) [Ord α] [TransOrd α] (β : Type v) (d : β) where
tree : ExtTreeMap α β compare
no_default : ∀ a : α, tree[a]? ≠ some d := by grind
namespace TreeMapD
variable {α : Type u} [Ord α] [TransOrd α] {β : Type v} {d : β}
instance : GetElem (TreeMapD α β d) α β (fun _ _ => True) where
getElem := fun m a _ => m.tree[a]?.getD d
@[local grind] private theorem getElem_mk
(tree : ExtTreeMap α β compare) (no_default : ∀ a : α, tree[a]? ≠ some d) (a : α) :
(TreeMapD.mk tree no_default)[a] = tree[a]?.getD d := rfl
@[local grind] private theorem getElem?_tree [DecidableEq β] (m : TreeMapD α β d) (a : α) :
m.tree[a]? = if m[a] = d then none else some m[a] := by
grind [cases TreeMapD]
@[local grind] private theorem mem_tree (m : TreeMapD α β d) (a : α) :
a ∈ m.tree ↔ m[a] ≠ d := by
grind [cases TreeMapD]
@[ext, grind ext]
theorem ext [LawfulEqOrd α] {m₁ m₂ : TreeMapD α β d} (h : ∀ a : α, m₁[a] = m₂[a]) : m₁ = m₂ := by
rcases m₁ with ⟨tree₁, no_default₁⟩
rcases m₂ with ⟨tree₂, no_default₂⟩
congr
ext a b
specialize h a
grind
def empty : TreeMapD α β d where
tree := ∅
instance : EmptyCollection (TreeMapD α β d) :=
⟨empty⟩
@[grind =] theorem empty_eq_emptyc : (empty : TreeMapD α β d) = ∅ := rfl
instance : Inhabited (TreeMapD α β d) :=
⟨empty⟩
@[grind =] theorem getElem_empty (a : α) : (∅ : TreeMapD α β d)[a] = d := rfl
variable [DecidableEq β]
def insert (m : TreeMapD α β d) (a : α) (b : β) : TreeMapD α β d where
tree := if b = d then m.tree.erase a else m.tree.insert a b
no_default := by
-- `grind` can't do this split because of the dependent typing in the `xs[i]?` notation.
split <;> grind
@[grind =] theorem getElem_insert [DecidableEq α] [LawfulEqOrd α] (m : TreeMapD α β d) (a : α) (b : β) :
(m.insert a b)[k] = if k = a then b else m[k] := by
grind [insert]