lean4-htt/src/Lean/Meta/Offset.lean
Leonardo de Moura 6871abaa44
refactor: replace grind canonicalizer with type-directed normalizer (#13166)
This PR replaces the `grind` canonicalizer with a new type-directed
normalizer (`Sym.canon`) that goes inside binders and applies targeted
reductions in type positions, eliminating the O(n^2) `isDefEq`-based
approach.

The old canonicalizer maintained a map from `(function,
argument_position)` to previously seen arguments, iterating the list and
calling `isDefEq` for each new argument. This produced performance
problems in some goal. For example, for a goal containing `n` numeric
literals, it would produce O(n^2) `isDefEq` comparisons.

The new canonicalizer normalizes types directly:
- **Instances**: re-synthesized via `synthInstance` with the type
normalized first, so `OfNat (Fin (2+1)) 0` and `OfNat (Fin 3) 0` produce
the same instance.
- **Types**: normalized with targeted reductions — eta, projection,
match/ite/cond, and Nat arithmetic (`n.succ + 1` → `n + 2`, `2 + 1` →
`3`).
- **Values**: traversed but not reduced, preserving lambdas for grind's
beta module.

The canonicalizer enters binders (the old one did not), using separate
caches for type-level and value-level contexts. Propositions are not
normalized to avoid interfering with grind's proposition handling.

Move `SynthInstance` from `Grind` to `Sym` since the canonicalizer now
lives in `Sym` and needs instance synthesis. The `Grind` namespace
re-exports the key functions.

Add `no_index` annotations to `val_addNat` and `val_castAdd` patterns in
`Fin/Lemmas.lean` — arithmetic in type positions is now normalized, so
patterns must not rely on the un-normalized form for e-matching
indexing.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 02:43:22 +00:00

163 lines
5.9 KiB
Text
Raw 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.

/-
Copyright (c) 2019 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
module
prelude
public import Lean.Data.LBool
public import Lean.Meta.Basic
import Lean.Meta.NatInstTesters
import Lean.Util.SafeExponentiation
public section
namespace Lean.Meta
private abbrev withInstantiatedMVars (e : Expr) (k : Expr → OptionT MetaM α) : OptionT MetaM α := do
let eNew ← instantiateMVars e
if eNew.getAppFn.isMVar then
failure
else
k eNew
open Structural in -- TODO FIX
/--
Evaluate simple `Nat` expressions.
Remark: this method assumes the given expression has type `Nat`. -/
partial def evalNat (e : Expr) : OptionT MetaM Nat := do
match e with
| .lit (.natVal n) => return n
| .mdata _ e => evalNat e
| .const ``Nat.zero .. => return 0
| .app .. => visit e
| .mvar .. => visit e
| _ => failure
where
evalPow (b n : Expr) : OptionT MetaM Nat := do
let n ← evalNat n
guard (← checkExponent n)
return (← evalNat b) ^ n
visit e := do
match_expr e with
| OfNat.ofNat _ n i => guard (← isInstOfNatNat i); evalNat n
| Nat.succ a => return (← evalNat a) + 1
| Nat.add a b => return (← evalNat a) + (← evalNat b)
| Add.add _ i a b => guard (← isInstAddNat i); return (← evalNat a) + (← evalNat b)
| HAdd.hAdd _ _ _ i a b => guard (← isInstHAddNat i); return (← evalNat a) + (← evalNat b)
| Nat.sub a b => return (← evalNat a) - (← evalNat b)
| Sub.sub _ i a b => guard (← isInstSubNat i); return (← evalNat a) - (← evalNat b)
| HSub.hSub _ _ _ i a b => guard (← isInstHSubNat i); return (← evalNat a) - (← evalNat b)
| Nat.mul a b => return (← evalNat a) * (← evalNat b)
| Mul.mul _ i a b => guard (← isInstMulNat i); return (← evalNat a) * (← evalNat b)
| HMul.hMul _ _ _ i a b => guard (← isInstHMulNat i); return (← evalNat a) * (← evalNat b)
| Nat.div a b => return (← evalNat a) / (← evalNat b)
| Div.div _ i a b => guard (← isInstDivNat i); return (← evalNat a) / (← evalNat b)
| HDiv.hDiv _ _ _ i a b => guard (← isInstHDivNat i); return (← evalNat a) / (← evalNat b)
| Nat.mod a b => return (← evalNat a) % (← evalNat b)
| Mod.mod _ i a b => guard (← isInstModNat i); return (← evalNat a) % (← evalNat b)
| HMod.hMod _ _ _ i a b => guard (← isInstHModNat i); return (← evalNat a) % (← evalNat b)
| Nat.pow a b => evalPow a b
| NatPow.pow _ i a b => guard (← isInstNatPowNat i); evalPow a b
| Pow.pow _ _ i a b => guard (← isInstPowNat i); evalPow a b
| HPow.hPow _ _ _ i a b => guard (← isInstHPowNat i); evalPow a b
| _ => failure
/--
Checks that expression `e` is definitional equal to `inst`.
Uses `instances` transparency so that reducible terms and instances extended
other instances are unfolded.
-/
def matchesInstance (e inst : Expr) : MetaM Bool :=
-- Note. We use withNewMCtxDepth to avoid assigning meta-variables in isDefEq checks
withNewMCtxDepth (withTransparency .instances (isDefEq e inst))
mutual
/--
Quick function for converting `e` into `s + k` s.t. `e` is definitionally equal to `Nat.add s k`.
This function always succeeds in finding such `s` and `k`
(as a last resort it returns `e` and `0`).
-/
private partial def getOffset (e : Expr) : MetaM (Expr × Nat) :=
return (← isOffset? e).getD (e, 0)
/--
Similar to `getOffset` but returns `none` if the expression is not an offset.
-/
partial def isOffset? (e : Expr) : OptionT MetaM (Expr × Nat) := do
let add (a b : Expr) := do
let v ← evalNat b
let (s, k) ← getOffset a
return (s, k+v)
match_expr e with
| Nat.succ a =>
let (s, k) ← getOffset a
return (s, k+1)
| Nat.add a b => add a b
| Add.add _ i a b => guard (← matchesInstance i Nat.mkInstAdd); add a b
| HAdd.hAdd _ _ _ i a b => guard (← matchesInstance i Nat.mkInstHAdd); add a b
| _ => failure
end
private def isNatZero (e : Expr) : MetaM Bool := do
match (← evalNat e) with
| some v => return v == 0
| _ => return false
def mkOffset (e : Expr) (offset : Nat) : MetaM Expr := do
if offset == 0 then
return e
else if (← isNatZero e) then
return mkNatLit offset
else
return mkNatAdd e (mkNatLit offset)
def isDefEqOffset (s t : Expr) : MetaM LBool := do
let ifNatExpr (x : MetaM LBool) : MetaM LBool := do
let type ← inferType s
-- Remark: we use `withNewMCtxDepth` to make sure we don't assign metavariables when performing the `isDefEq` test
if (← withNewMCtxDepth <| Meta.isExprDefEqAux type (mkConst ``Nat)) then
x
else
return LBool.undef
let isDefEq (s t) : MetaM LBool :=
ifNatExpr <| toLBoolM <| Meta.isExprDefEqAux s t
if !(← getConfig).offsetCnstrs then
return LBool.undef
else
match (← isOffset? s) with
| some (s, k₁) =>
match (← isOffset? t) with
| some (t, k₂) => -- s+k₁ =?= t+k₂
if k₁ == k₂ then
isDefEq s t
else if k₁ < k₂ then
isDefEq s (← mkOffset t (k₂ - k₁))
else
isDefEq (← mkOffset s (k₁ - k₂)) t
| none =>
match (← evalNat t) with
| some v₂ => -- s+k₁ =?= v₂
if v₂ ≥ k₁ then
isDefEq s (mkNatLit <| v₂ - k₁)
else
ifNatExpr <| return LBool.false
| none =>
return LBool.undef
| none =>
match (← evalNat s) with
| some v₁ =>
match (← isOffset? t) with
| some (t, k₂) => -- v₁ =?= t+k₂
if v₁ ≥ k₂ then
isDefEq (mkNatLit <| v₁ - k₂) t
else
ifNatExpr <| return LBool.false
| none =>
match (← evalNat t) with
| some v₂ => ifNatExpr <| return (v₁ == v₂).toLBool -- v₁ =?= v₂
| none => return LBool.undef
| none => return LBool.undef
end Lean.Meta