From 561d42c85438e6a2742fe159e04db86a92636665 Mon Sep 17 00:00:00 2001 From: Julia Markus Himmel <2065352+TwoFX@users.noreply.github.com> Date: Mon, 25 May 2026 10:15:46 +0000 Subject: [PATCH 01/16] experiment: always check types for instance metavariables at instances transparency --- src/Lean/Meta/ExprDefEq.lean | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Lean/Meta/ExprDefEq.lean b/src/Lean/Meta/ExprDefEq.lean index 6a8b7ce3e962..b37aa59346de 100644 --- a/src/Lean/Meta/ExprDefEq.lean +++ b/src/Lean/Meta/ExprDefEq.lean @@ -674,7 +674,20 @@ private def checkTypesAndAssign (mvar : Expr) (v : Expr) : MetaM Bool := let mvarType ← inferType mvar let vType ← inferType v if (← respectTransparencyAtTypes) then - withImplicitConfig do + -- For instance metavariables — those created for an instance-implicit (`[..]`) parameter, + -- identified by `.synthetic` kind together with a class type — cap the transparency at + -- exactly `.instances` so an ambient `.default`/`.all` does not let semireducible + -- definitions be unfolded while checking the type of an instance assignment. This + -- intentionally does not apply to ordinary implicit (`{..}`) metavariables that happen + -- to have a class type, which are created with `.natural` kind. + let isInstance ← + if (← mvar.mvarId!.getKind) matches .synthetic then + pure (← isClass? mvarType).isSome + else + pure false + let capInstance (x : MetaM Bool) : MetaM Bool := + if isInstance then withTransparency .instances x else x + capInstance <| withImplicitConfig do if (← Meta.isExprDefEqAux mvarType vType) then mvar.mvarId!.assign v return true From 020bb06e0444910b7249a1fcaef70e32f93ce8eb Mon Sep 17 00:00:00 2001 From: Julia Markus Himmel <2065352+TwoFX@users.noreply.github.com> Date: Tue, 26 May 2026 10:28:31 +0000 Subject: [PATCH 02/16] fixes --- src/Init/Prelude.lean | 14 ++++++++++++++ .../LRAT/Internal/Formula/RatAddSound.lean | 4 +++- src/Std/Time/Format/Basic.lean | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Init/Prelude.lean b/src/Init/Prelude.lean index 497df8c35249..7a793406e367 100644 --- a/src/Init/Prelude.lean +++ b/src/Init/Prelude.lean @@ -1023,11 +1023,25 @@ theorem of_decide_eq_true [inst : Decidable p] : Eq (decide p) true → p := fun | isTrue h₁ => h₁ | isFalse h₁ => absurd h (Bool.ne_true_of_eq_false (decide_eq_false h₁)) +/-- +Variant of `of_decide_eq_true` that takes `Decidable` as an implicit argument, intended for +forward reasoning. +-/ +theorem of_decide_eq_true_forward {inst : Decidable p} : Eq (decide p) true → p := + of_decide_eq_true + theorem of_decide_eq_false [inst : Decidable p] : Eq (decide p) false → Not p := fun h => match (generalizing := false) inst with | isTrue h₁ => absurd h (Bool.ne_false_of_eq_true (decide_eq_true h₁)) | isFalse h₁ => h₁ +/-- +Variant of `of_decide_eq_false` that takes `Decidable` as an implicit argument, intended for +forward reasoning. +-/ +theorem of_decide_eq_false_forward {inst : Decidable p} : Eq (decide p) false → Not p := + of_decide_eq_false + theorem of_decide_eq_self_eq_true [inst : DecidableEq α] (a : α) : Eq (decide (Eq a a)) true := match (generalizing := false) inst a a with | isTrue _ => rfl diff --git a/src/Std/Tactic/BVDecide/LRAT/Internal/Formula/RatAddSound.lean b/src/Std/Tactic/BVDecide/LRAT/Internal/Formula/RatAddSound.lean index 38b719eb9cd2..85a826d5cbc1 100644 --- a/src/Std/Tactic/BVDecide/LRAT/Internal/Formula/RatAddSound.lean +++ b/src/Std/Tactic/BVDecide/LRAT/Internal/Formula/RatAddSound.lean @@ -412,11 +412,13 @@ theorem existsRatHint_of_ratHintsExhaustive {n : Nat} (f : DefaultFormula n) List.mem_filterMap, id_eq, exists_eq_right] at c'_in_f rw [List.mem_iff_getElem] at c'_in_f rcases c'_in_f with ⟨i, hi, c'_in_f⟩ + -- The following `simp only` leaves behind a term containing a `decide` where the embedded + -- `Decidable` instances does not typecheck at `instances` transparency. simp only [ratHintsExhaustive, getRatClauseIndices] at ratHintsExhaustive_eq_true have i_in_bounds : i < Array.size (Array.range (Array.size f.clauses)) := by grind have i_lt_f_clauses_size : i < f.clauses.size := by grind have h : i ∈ (ratHints.map (fun x => x.1)).toList := by - rw [← of_decide_eq_true ratHintsExhaustive_eq_true] + simp only [← of_decide_eq_true_forward ratHintsExhaustive_eq_true] have i_eq_range_i : i = (Array.range f.clauses.size)[i]'i_in_bounds := by grind rw [i_eq_range_i] rw [Array.mem_toList_iff] diff --git a/src/Std/Time/Format/Basic.lean b/src/Std/Time/Format/Basic.lean index 10bdf6dfd934..312a918fdd2e 100644 --- a/src/Std/Time/Format/Basic.lean +++ b/src/Std/Time/Format/Basic.lean @@ -740,13 +740,13 @@ private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (Typ | .y format => match format with | .any => Int.ofNat <$> parseAtLeastNum 1 - | .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2 + | .twoDigit => (fun x => (2000 + x : Int)) <$> Int.ofNat <$> parseNum 2 | .fourDigit => Int.ofNat <$> parseNum 4 | .extended n => Int.ofNat <$> parseNum n | .u format => match format with | .any => parseSigned <| parseAtLeastNum 1 - | .twoDigit => (2000 + ·) <$> Int.ofNat <$> parseNum 2 + | .twoDigit => (fun x => (2000 + x : Int)) <$> Int.ofNat <$> parseNum 2 | .fourDigit => parseSigned <| parseNum 4 | .extended n => parseSigned <| parseNum n | .Y format => From e1bde313638d3696a559f6ab111b3aca22ea64a8 Mon Sep 17 00:00:00 2001 From: Julia Markus Himmel <2065352+TwoFX@users.noreply.github.com> Date: Tue, 26 May 2026 12:54:47 +0000 Subject: [PATCH 03/16] Introduct option --- src/Lean/Meta/ExprDefEq.lean | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Lean/Meta/ExprDefEq.lean b/src/Lean/Meta/ExprDefEq.lean index b37aa59346de..f92b14615d8d 100644 --- a/src/Lean/Meta/ExprDefEq.lean +++ b/src/Lean/Meta/ExprDefEq.lean @@ -60,6 +60,19 @@ register_builtin_option backward.isDefEq.respectTransparency.types : Bool := { when checking whether the type of a metavariable matches the type of the term being assigned to it." } +/-- +Controls whether, when checking the type of an assignment to an instance-implicit (`[..]`) +metavariable, the transparency is capped at `.instances` so an ambient `.default`/`.all` +does not let semireducible definitions be unfolded. + +This option only has an effect when `backward.isDefEq.respectTransparency.types` is `true`. +-/ +register_builtin_option backward.isDefEq.respectTransparency.instances : Bool := { + defValue := true + descr := "if true (the default), cap transparency at `.instances` when checking the type \ + of an assignment to an instance-implicit metavariable" +} + /-- Controls whether non-instance implicit arguments get their transparency bumped to `TransparencyMode.implicit` during `isDefEq`. @@ -681,7 +694,8 @@ private def checkTypesAndAssign (mvar : Expr) (v : Expr) : MetaM Bool := -- intentionally does not apply to ordinary implicit (`{..}`) metavariables that happen -- to have a class type, which are created with `.natural` kind. let isInstance ← - if (← mvar.mvarId!.getKind) matches .synthetic then + if backward.isDefEq.respectTransparency.instances.get (← getOptions) && + (← mvar.mvarId!.getKind) matches .synthetic then pure (← isClass? mvarType).isSome else pure false From 11206ad146504fd90b658d885204a8abeb3f1596 Mon Sep 17 00:00:00 2001 From: Julia Markus Himmel <2065352+TwoFX@users.noreply.github.com> Date: Wed, 27 May 2026 08:41:02 +0000 Subject: [PATCH 04/16] experiment: do not bump transparency when assigning `outParam`s --- src/Lean/Meta/SynthInstance.lean | 30 +++++++++++++++++++++++++++++- tests/elab/irredoutparam.lean | 15 +++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/elab/irredoutparam.lean diff --git a/src/Lean/Meta/SynthInstance.lean b/src/Lean/Meta/SynthInstance.lean index 6a03f9943177..abffcbf2dc7e 100644 --- a/src/Lean/Meta/SynthInstance.lean +++ b/src/Lean/Meta/SynthInstance.lean @@ -32,6 +32,28 @@ register_builtin_option backward.synthInstance.canonInstances : Bool := { descr := "use optimization that relies on 'morally canonical' instances during type class resolution" } +/-- +Controls the transparency used by the post-synthesis check that unifies the original goal type +with the synthesized result's type when the class has `outParam`s. + +When `true` (the default), the check uses `.instances` transparency, matching the transparency +used by the rest of typeclass resolution. This means a non-reducible `def Bar := Nat` is *not* +unfolded, so e.g. `#synth Foo Nat Bar` cannot be satisfied by `instance : Foo Nat Nat`. + +When `false`, the check bumps transparency to `.default`, which unfolds semireducible +definitions. This was the historical behavior, retained because patterns like Mathlib's +`def OrderDual (α : Type) : Type := α` rely on `OrderDual α` and `α` being defEq at `.default` +during the outParam check. + +See `assignOutParams` for the implementation. +-/ +register_builtin_option backward.isDefEq.respectTransparency.outParams : Bool := { + defValue := true + descr := "if true (the default), do not bump transparency to `.default` \ + when checking that a synthesized instance's type matches the original goal type \ + for classes with `outParam`s" +} + namespace SynthInstance def getMaxHeartbeats (opts : Options) : Nat := @@ -842,8 +864,14 @@ private def assignOutParams (type : Expr) (result : Expr) : MetaM Bool := do ``` Mathlib developers are currently trying to refactor the `OrderDual` declaration, but it will take time. We will try to remove the `withDefault` again after the refactoring. + + The `backward.isDefEq.respectTransparency.outParams` option (defaulting to `true`) keeps the + check at `.instances` transparency. Setting it to `false` restores the historical `withDefault` + behavior. -/ - let defEq ← withDefault <| withAssignableSyntheticOpaque <| isDefEq type resultType + let bumpTransparency := !backward.isDefEq.respectTransparency.outParams.get (← getOptions) + let defEq ← (if bumpTransparency then withDefault else id) <| + withAssignableSyntheticOpaque <| isDefEq type resultType unless defEq do trace[Meta.synthInstance] "{crossEmoji} result type{indentExpr resultType}\nis not definitionally equal to{indentExpr type}" return defEq diff --git a/tests/elab/irredoutparam.lean b/tests/elab/irredoutparam.lean new file mode 100644 index 000000000000..a82e11569a6e --- /dev/null +++ b/tests/elab/irredoutparam.lean @@ -0,0 +1,15 @@ +class Foo (α : Type) (β : outParam Type) where + +def Bar := + Nat + +instance : Foo Nat Nat where + +/-- +error: failed to synthesize + Foo Nat Bar + +Hint: Additional diagnostic information may be available using the `set_option diagnostics true` command. +-/ +#guard_msgs in +#synth Foo Nat Bar -- instFooNat From 4200b4baf1127e0947c39d7c05a4e9a01ea6039b Mon Sep 17 00:00:00 2001 From: Julia Markus Himmel <2065352+TwoFX@users.noreply.github.com> Date: Wed, 27 May 2026 12:03:12 +0000 Subject: [PATCH 05/16] experiment: fix `inferInstanceAs` elaborator --- src/Lean/Elab/BuiltinTerm.lean | 38 ++++++++++++++++++++++++++++++- tests/elab/inferInstanceAs2.lean | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/elab/inferInstanceAs2.lean diff --git a/src/Lean/Elab/BuiltinTerm.lean b/src/Lean/Elab/BuiltinTerm.lean index 32906b9bc869..3a16c592efdc 100644 --- a/src/Lean/Elab/BuiltinTerm.lean +++ b/src/Lean/Elab/BuiltinTerm.lean @@ -349,6 +349,42 @@ private def resynthInstImplicitArgs (type : Expr) : TermElabM Expr := do let args := mvars ++ args.drop mvars.size instantiateMVars (mkAppN fn args) +/-- +Best-effort unification of the user-supplied `type` against the `expectedType` to resolve +user-placed `_` placeholders. We decompose both sides as applications of a common head and +unify arguments position-by-position, skipping instance-implicit positions. + +Instance-implicit arguments of `type` are fresh synthetic class metavariables introduced by +`elabType` that will be discarded by `resynthInstImplicitArgs`. Routing them through `isDefEq` +serves no purpose and can spuriously fail: assigning a synthetic class metavariable triggers +a transparency cap at `.instances` in the type-equality check (see +`backward.isDefEq.respectTransparency.instances`), which prevents non-`[reducible]` definitions +from unfolding — so, e.g., `Neg (Nat ⧸ n) =?= Neg (Zmod n)` fails even though `Zmod` reduces +to `Nat ⧸ n` at `.default` transparency. The outer `isDefEq` then fails and rolls back the +useful assignments made to user `_` placeholders. + +When the two sides do not share a common-shape head, we fall back to a plain `isDefEq`. +Per-argument `isDefEq` calls are best-effort: a failure at one position does not roll back +successes at others. +-/ +private def unifyTypeForInferInstanceAs (type expectedType : Expr) : TermElabM Unit := do + let typeFn := type.getAppFn + let expectedFn := expectedType.getAppFn + let typeArgs := type.getAppArgs + let expectedArgs := expectedType.getAppArgs + unless typeFn.isConst && expectedFn.isConst + && typeFn.constName! == expectedFn.constName! + && typeArgs.size == expectedArgs.size do + discard <| isDefEq type expectedType + return + unless (← isDefEq typeFn expectedFn) do + return + let (_, bis, _) ← forallMetaTelescope (← inferType typeFn) + for i in [:typeArgs.size] do + if i < bis.size && bis[i]!.isInstImplicit then + continue + discard <| isDefEq typeArgs[i]! expectedArgs[i]! + @[builtin_term_elab Lean.Parser.Term.inferInstanceAs] def elabInferInstanceAs : TermElab := fun stx expectedType? => do -- The type argument is the last child (works for both `inferInstanceAs T` and `inferInstanceAs <| T`) let typeStx := stx[stx.getNumArgs - 1]! @@ -363,7 +399,7 @@ private def resynthInstImplicitArgs (type : Expr) : TermElabM Expr := do let type ← withSynthesize do let type ← elabType typeStx -- Unify with expected type to resolve metavariables (e.g., `_` placeholders) - discard <| isDefEq type expectedType + unifyTypeForInferInstanceAs type expectedType return type -- Re-infer instance-implicit args, so that synthesis is not influenced by the expected type's -- instance choices. diff --git a/tests/elab/inferInstanceAs2.lean b/tests/elab/inferInstanceAs2.lean new file mode 100644 index 000000000000..2b417f1bdb0d --- /dev/null +++ b/tests/elab/inferInstanceAs2.lean @@ -0,0 +1,39 @@ +/-! +Tests for `inferInstanceAs` against expected types whose explicit arguments wrap a +non-reducible defined type (`Zmod n` here, which unfolds to `Nat ⧸ n`). User-placed +`_` placeholders in the `inferInstanceAs` type argument must be resolved by matching +against the expected type without getting blocked by the `.instances` transparency +cap on synthetic instance metavariables. +-/ + +class HasQuotient (A : outParam <| Type u) (B : Type v) where + Quotient (A) : B → Type max u v + +notation:35 G " ⧸ " H:34 => HasQuotient.Quotient G H + +class Foo (α : Type) [Neg α] where + +instance {n : Nat} : Foo (Fin n) where + +instance : HasQuotient Nat Nat where + Quotient n := Fin n + +instance {n : Nat} : Neg (Nat ⧸ n) := + inferInstanceAs <| Neg (Fin n) + +instance {n : Nat} : Foo (Nat ⧸ n) := + inferInstanceAs <| Foo (Fin n) + +def Zmod (n : Nat) := + Nat ⧸ n +deriving Neg + +instance {n : Nat} : Foo (Zmod n) := + inferInstanceAs <| Foo (_ ⧸ _) + +instance {n : Nat} : Foo (Zmod n) := + inferInstanceAs <| Foo (Nat ⧸ n) + +set_option backward.isDefEq.respectTransparency.instances false in +instance {n : Nat} : Foo (Zmod n) := + inferInstanceAs <| Foo (_ ⧸ _) From 8e04897322c04a3cec3bec3d81595f858c7cc4ae Mon Sep 17 00:00:00 2001 From: Paul Reichert <6992158+datokrat@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:33:45 +0000 Subject: [PATCH 06/16] fixes for stage2 --- src/Lean/Environment.lean | 2 ++ src/Std/Time/Format/Basic.lean | 1 + src/Std/Time/Zoned/Database/PosixTz.lean | 1 + 3 files changed, 4 insertions(+) diff --git a/src/Lean/Environment.lean b/src/Lean/Environment.lean index 7acba898530b..446900430e48 100644 --- a/src/Lean/Environment.lean +++ b/src/Lean/Environment.lean @@ -2301,6 +2301,8 @@ where p := body p.isProp +set_option allowUnsafeReducibility true in +attribute [local instance_reducible] Id in /-- Constructs environment from `importModulesCore` results. diff --git a/src/Std/Time/Format/Basic.lean b/src/Std/Time/Format/Basic.lean index 312a918fdd2e..3988d5bccdc0 100644 --- a/src/Std/Time/Format/Basic.lean +++ b/src/Std/Time/Format/Basic.lean @@ -731,6 +731,7 @@ private def parseOffset (withMinutes : Reason) (withSeconds : Reason) (withColon return Offset.ofSeconds ⟨hours.val * sign⟩ +set_option backward.isDefEq.respectTransparency false in private def parseWith (config : FormatConfig) : (mod : Modifier) → Parser (TypeFormat mod) | .G format => match format with diff --git a/src/Std/Time/Zoned/Database/PosixTz.lean b/src/Std/Time/Zoned/Database/PosixTz.lean index 1fe9328ca6cd..b568a35bec46 100644 --- a/src/Std/Time/Zoned/Database/PosixTz.lean +++ b/src/Std/Time/Zoned/Database/PosixTz.lean @@ -36,6 +36,7 @@ private def posixParseSign : Parser Int := <|> attempt (pchar '+' *> pure 1) <|> pure 1 +set_option backward.isDefEq.respectTransparency false in --