lean4-htt/tests/pkg/homo/Homo/Init.lean
Leonardo de Moura 897e556d90
feat: add E-matching diagnostics to grind (#13558)
This PR adds the option `grind.ematch.diagnostics`, which tracks how
E-matching theorem instances depend on each other. When enabled, `grind`
records, for every new theorem instance, the set of previous instances
whose generated terms participated in the match. This produces a
hyper-graph `{thm_1, ..., thm_n} => thm` describing the provenance of
each instantiation.

The hyper-graph is stored in `Grind.Result` so downstream tooling can
inspect it. The trace class `trace.grind.ematch.diagnostics.compact`
prints a compact textual view of the hyper-graph, restricted to
constant-name origins. Example output:

```
  [grind.ematch.diagnostics.compact] ️ instances
    [inst] [] => th1
    [inst] [th1] => th3
    [inst] [th1] => th2
    [inst] [th2, th3] => th4
    [inst] [th4] => th5
```

The implementation stores an `ematchDiagSource` field on each `ENode`
and threads a `withEmatchDiagSource` reader through fact assertion so
that newly internalized terms inherit the origin of the instance that
produced them. During E-matching, `Choice` collects the sources of every
matched argument, and the resulting set becomes the predecessor set of
the new instance.
2026-04-29 12:17:55 +00:00

159 lines
5.7 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module
import Lean.Meta.Tactic.Grind.Types
import Lean.Meta.Sym.Simp.Attr
import Lean.Meta.Sym.Simp.Simproc
import Lean.Meta.Sym.Simp.Rewrite
import Lean.Meta.AppBuilder
namespace Homomorphism
open Lean Meta Grind Sym Simp
initialize registerTraceClass `homo
initialize registerTraceClass `homo.pred
initialize registerTraceClass `homo.visit
initialize homoPredExt : SimplePersistentEnvExtension (Name × Name) (NameMap Name) ←
let add := fun s (f, thm) => s.insert f thm
registerSimplePersistentEnvExtension {
addEntryFn := add
addImportedFn := fun es => mkStateFromImportedEntries add {} es
}
def getPredMap : CoreM (NameMap Name) :=
return homoPredExt.getState (← getEnv)
def addPredicate (thmName : Name) : MetaM Unit := do
let info ← getConstInfo thmName
unless (← isProp info.type) do
throwError "invalid homomorphism predicate, `{thmName}` is not a proposition"
let vs := info.levelParams.map mkLevelParam
forallTelescope info.type fun xs type => do
let found? := type.find? fun e => Id.run do
unless e.getAppNumArgs == xs.size do return false
let .const _ us := e.getAppFn | return false
return e.getAppArgs == xs && us == vs
let some found := found? |
throwError "invalid homomorphism predicate, `{thmName}` does not contain application that covers all parameters"
let .const declName _ := found.getAppFn | unreachable!
if (← getPredMap).contains declName then
throwError "invalid homomorphism predicate, `{declName}` already contains a theorem associated with it."
modifyEnv fun env => homoPredExt.addEntry env (declName, thmName)
initialize registerBuiltinAttribute {
name := `grind_homo_pred
descr := "add a theorem to be applied to atoms"
add := fun declName _ _ =>
discard <| addPredicate declName |>.run {} {}
}
/--
Declares attribute `[grind_mono]` for marking theorems implementing the homomorphism.
-/
initialize homoSimpExtension : SymSimpExtension ←
registerSymSimpAttr `grind_homo "`grind` homomorphism attribute"
/--
Returns theorems marked with `[grind_mono]`
-/
def getTheorems : CoreM Theorems :=
homoSimpExtension.getTheorems
/--
Creates a simproc that applies the theorems marked with `[grind_mono]`.
This simproc is meant to be applied as a `pre` method.
Recall that `grind` internalizes terms bottom-up. By the time a
simplification set runs on a term `e`, all subterms of `e` are already
in the E-graph and have been processed by the pipeline.
**Stop condition.** When simp encounters a term `t` during traversal:
- If a rule matches `t`: apply it, continue (result is a new term).
- If no rule matches `t` AND `t` is already in the E-graph:
stop, don't descend. Otherwise: descend normally.
-/
def mkRewriter : GoalM Sym.Simp.Simproc := do
let s ← get
-- Remark: We are not using any discharger. So, our rewriting rules are all context
-- independent.
let rw := (← getTheorems).rewrite
return fun e => do
trace[homo.visit] "{e}"
let r ← rw e
if !r.isRfl then return r
-- If `e` is already in the E-graph, we don't revisit its children
let done := s.enodeMap.contains { expr := e }
return .rfl (done := done)
structure State where
cache : Sym.Simp.Cache := {}
processed : PHashSet ExprPtr := {}
initialize homoExt : SolverExtension State ←
registerSolverExtension (return {})
def get' : GoalM State := do
homoExt.getState
abbrev modify' (f : State → State) : GoalM Unit := do
homoExt.modifyState f
/-- Apply the homomorphism theorems. -/
def applyHomo (e : Expr) : GoalM Sym.Simp.Result := do
let methods := { pre := (← mkRewriter) }
-- Reuse cache.
let persistentCache := (← homoExt.getState).cache
homoExt.modifyState fun s => { s with cache := {} } -- Improve uniqueness. This is a minor optimization
let (r, simpState) ← Sym.Simp.SimpM.run (Sym.Simp.simp e) (methods := methods) (s := { persistentCache })
homoExt.modifyState fun s => { s with cache := simpState.persistentCache }
return r
/--
Returns `true` if some theorem marked with `[grind_homo]` is applicable to `e`.
Motivation: we don't want to start the simplifier and fail immediately.
-/
def isTarget (e : Expr) : CoreM Bool := do
let thms ← getTheorems
return !(thms.getMatch e).isEmpty
/--
Internalization procedure for this module. See `homoExt.setMethods`
-/
def internalize (e : Expr) (_ : Option Expr) : GoalM Unit := do
let f := e.getAppFn
if let .const declName us := f then
let s ← get'
unless s.processed.contains { expr := e } do
modify' fun s => { s with processed := s.processed.insert { expr := e } }
if let some thmName := (← getPredMap).find? declName then
let thm := mkAppN (mkConst thmName us) e.getAppArgs
let pred ← Meta.inferType thm
trace[homo.pred] "{pred}"
addNewRawFact thm (← Meta.inferType thm) (← getGeneration e) .input .other
return ()
unless (← isTarget e) do return ()
if !(← alreadyInternalized e) then
/-
The `grind` core has an optimization: it does not internalize top-level equalities
since they can be merged immediately. A satellite solver may implement the `newEq` handler,
but this is too inconvenient. It is easier to force `e` to be internalized.
-/
let_expr Eq _ lhs rhs := e | return ()
let gen := max (← getGeneration lhs) (← getGeneration rhs)
Grind.internalize e gen
return ()
let .step e₁ h₁ _ ← applyHomo e | return ()
let r ← preprocess e₁
let h ← mkEqTrans h₁ (← r.getProof)
let gen ← getGeneration e
Grind.internalize r.expr gen
trace[homo] "{e}\n====>\n{r.expr}"
pushEq e r.expr h
initialize
homoExt.setMethods
(internalize := internalize)
end Homomorphism