Skip to content

Commit 6e8621f

Browse files
committed
feat: add maxSynthPendingDepth
This commit also adds supports for tracking `synthPending` failures when using `set_option diagnostics true`. It also increases limit 2. closes #3313 closes #3927
1 parent 941d2fb commit 6e8621f

File tree

7 files changed

+101
-19
lines changed

7 files changed

+101
-19
lines changed

src/Lean/CoreM.lean

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ register_builtin_option maxHeartbeats : Nat := {
3030
descr := "maximum amount of heartbeats per command. A heartbeat is number of (small) memory allocations (in thousands), 0 means no limit"
3131
}
3232

33+
def useDiagnosticMsg := s!"use `set_option {diagnostics.name} true` to get diagnostic information"
34+
3335
namespace Core
3436

3537
builtin_initialize registerTraceClass `Kernel
@@ -251,7 +253,7 @@ protected def withIncRecDepth [Monad m] [MonadControlT CoreM m] (x : m α) : m
251253
throw <| Exception.error .missing "elaboration interrupted"
252254

253255
def throwMaxHeartbeat (moduleName : Name) (optionName : Name) (max : Nat) : CoreM Unit := do
254-
let msg := s!"(deterministic) timeout at `{moduleName}`, maximum number of heartbeats ({max/1000}) has been reached\nuse `set_option {optionName} <num>` to set the limit\nuse `set_option {diagnostics.name} true` to get diagnostic information"
256+
let msg := s!"(deterministic) timeout at `{moduleName}`, maximum number of heartbeats ({max/1000}) has been reached\nuse `set_option {optionName} <num>` to set the limit\n{useDiagnosticMsg}"
255257
throw <| Exception.error (← getRef) (MessageData.ofFormat (Std.Format.text msg))
256258

257259
def checkMaxHeartbeatsCore (moduleName : String) (optionName : Name) (max : Nat) : CoreM Unit := do

src/Lean/Elab/Calc.lean

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def mkCalcTrans (result resultType step stepType : Expr) : MetaM (Expr × Expr)
4242
unless (← getCalcRelation? resultType).isSome do
4343
throwError "invalid 'calc' step, step result is not a relation{indentExpr resultType}"
4444
return (result, resultType)
45-
| _ => throwError "invalid 'calc' step, failed to synthesize `Trans` instance{indentExpr selfType}"
45+
| _ => throwError "invalid 'calc' step, failed to synthesize `Trans` instance{indentExpr selfType}\n{useDiagnosticMsg}"
4646

4747
/--
4848
Adds a type annotation to a hole that occurs immediately at the beginning of the term.

src/Lean/Elab/Term.lean

+1-1
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ def synthesizeInstMVarCore (instMVar : MVarId) (maxResultSize? : Option Nat := n
784784
if (← read).ignoreTCFailures then
785785
return false
786786
else
787-
throwError "failed to synthesize instance{indentExpr type}"
787+
throwError "failed to synthesize{indentExpr type}\n{useDiagnosticMsg}"
788788

789789
def mkCoe (expectedType : Expr) (e : Expr) (f? : Option Expr := none) (errorMsgHeader? : Option String := none) : TermElabM Expr := do
790790
withTraceNode `Elab.coe (fun _ => return m!"adding coercion for {e} : {← inferType e} =?= {expectedType}") do

src/Lean/Meta/Basic.lean

+24-8
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ structure Diagnostics where
283283
heuristicCounter : PHashMap Name Nat := {}
284284
/-- Number of times a TC instance is used. -/
285285
instanceCounter : PHashMap Name Nat := {}
286+
/-- Pending instances that were not synthesized because `maxSynthPendingDepth` has been reached. -/
287+
synthPendingFailures : PHashMap Expr MessageData := {}
286288
deriving Inhabited
287289

288290
/--
@@ -306,6 +308,11 @@ structure SavedState where
306308
meta : State
307309
deriving Nonempty
308310

311+
register_builtin_option maxSynthPendingDepth : Nat := {
312+
defValue := 2
313+
descr := "maximum number of nested `synthPending` invocations. When resolving unification constraints, pending type class problems may need to be synthesized. These type class problems may create new unification constraints that again require solving new type class problems. This option puts a threshold on how many nested problems are created."
314+
}
315+
309316
/--
310317
Contextual information for the `MetaM` monad.
311318
-/
@@ -321,8 +328,8 @@ structure Context where
321328
Track the number of nested `synthPending` invocations. Nested invocations can happen
322329
when the type class resolution invokes `synthPending`.
323330
324-
Remark: in the current implementation, `synthPending` fails if `synthPendingDepth > 0`.
325-
We will add a configuration option if necessary. -/
331+
Remark: `synthPending` fails if `synthPendingDepth > maxSynthPendingDepth`.
332+
-/
326333
synthPendingDepth : Nat := 0
327334
/--
328335
A predicate to control whether a constant can be unfolded or not at `whnf`.
@@ -480,21 +487,30 @@ variable [MonadControlT MetaM n] [Monad n]
480487

481488
/-- If diagnostics are enabled, record that `declName` has been unfolded. -/
482489
def recordUnfold (declName : Name) : MetaM Unit := do
483-
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter } =>
490+
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter, synthPendingFailures } =>
484491
let newC := if let some c := unfoldCounter.find? declName then c + 1 else 1
485-
{ unfoldCounter := unfoldCounter.insert declName newC, heuristicCounter, instanceCounter }
492+
{ unfoldCounter := unfoldCounter.insert declName newC, heuristicCounter, instanceCounter, synthPendingFailures }
486493

487494
/-- If diagnostics are enabled, record that heuristic for solving `f a =?= f b` has been used. -/
488495
def recordDefEqHeuristic (declName : Name) : MetaM Unit := do
489-
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter } =>
496+
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter, synthPendingFailures } =>
490497
let newC := if let some c := heuristicCounter.find? declName then c + 1 else 1
491-
{ unfoldCounter, heuristicCounter := heuristicCounter.insert declName newC, instanceCounter }
498+
{ unfoldCounter, heuristicCounter := heuristicCounter.insert declName newC, instanceCounter, synthPendingFailures }
492499

493500
/-- If diagnostics are enabled, record that instance `declName` was used during TC resolution. -/
494501
def recordInstance (declName : Name) : MetaM Unit := do
495-
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter } =>
502+
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter, synthPendingFailures } =>
496503
let newC := if let some c := instanceCounter.find? declName then c + 1 else 1
497-
{ unfoldCounter, heuristicCounter, instanceCounter := instanceCounter.insert declName newC }
504+
{ unfoldCounter, heuristicCounter, instanceCounter := instanceCounter.insert declName newC, synthPendingFailures }
505+
506+
/-- If diagnostics are enabled, record that synth pending failures. -/
507+
def recordSynthPendingFailure (type : Expr) : MetaM Unit := do
508+
if (← isDiagnosticsEnabled) then
509+
unless (← get).diag.synthPendingFailures.contains type do
510+
-- We need to save the full context since type class resolution uses multiple metavar contexts and different local contexts
511+
let msg ← addMessageContextFull m!"{type}"
512+
modifyDiag fun { unfoldCounter, heuristicCounter, instanceCounter, synthPendingFailures } =>
513+
{ unfoldCounter, heuristicCounter, instanceCounter, synthPendingFailures := synthPendingFailures.insert type msg }
498514

499515
def getLocalInstances : MetaM LocalInstances :=
500516
return (← read).localInstances

src/Lean/Meta/Diagnostics.lean

+16-3
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,20 @@ def mkDiagSummaryForUnfoldedReducible (counters : PHashMap Name Nat) : MetaM Dia
6060
def mkDiagSummaryForUsedInstances : MetaM DiagSummary := do
6161
mkDiagSummary (← get).diag.instanceCounter
6262

63-
def appendSection (m : MessageData) (cls : Name) (header : String) (s : DiagSummary) : MessageData :=
63+
def mkDiagSynthPendingFailure (failures : PHashMap Expr MessageData) : MetaM DiagSummary := do
64+
if failures.isEmpty then
65+
return {}
66+
else
67+
let mut data := #[]
68+
for (_, msg) in failures do
69+
data := data.push m!"{if data.isEmpty then " " else "\n"}{msg}"
70+
return { data }
71+
72+
def appendSection (m : MessageData) (cls : Name) (header : String) (s : DiagSummary) (resultSummary := true) : MessageData :=
6473
if s.isEmpty then
6574
m
6675
else
67-
let header := s!"{header} (max: {s.max}, num: {s.data.size}):"
76+
let header := if resultSummary then s!"{header} (max: {s.max}, num: {s.data.size}):" else header
6877
m ++ .trace { cls } header s.data
6978

7079
def reportDiag : MetaM Unit := do
@@ -75,13 +84,17 @@ def reportDiag : MetaM Unit := do
7584
let unfoldReducible ← mkDiagSummaryForUnfoldedReducible unfoldCounter
7685
let heu ← mkDiagSummary (← get).diag.heuristicCounter
7786
let inst ← mkDiagSummaryForUsedInstances
87+
let synthPending ← mkDiagSynthPendingFailure (← get).diag.synthPendingFailures
7888
let unfoldKernel ← mkDiagSummary (Kernel.getDiagnostics (← getEnv)).unfoldCounter
79-
unless unfoldDefault.isEmpty && unfoldInstance.isEmpty && unfoldReducible.isEmpty && heu.isEmpty && inst.isEmpty do
89+
unless unfoldDefault.isEmpty && unfoldInstance.isEmpty && unfoldReducible.isEmpty && heu.isEmpty && inst.isEmpty && synthPending.isEmpty do
8090
let m := MessageData.nil
8191
let m := appendSection m `reduction "unfolded declarations" unfoldDefault
8292
let m := appendSection m `reduction "unfolded instances" unfoldInstance
8393
let m := appendSection m `reduction "unfolded reducible declarations" unfoldReducible
8494
let m := appendSection m `type_class "used instances" inst
95+
let m := appendSection m `type_class
96+
s!"max synth pending failures (maxSynthPendingDepth: {maxSynthPendingDepth.get (← getOptions)}), use `set_option maxSynthPendingDepth <limit>`"
97+
synthPending (resultSummary := false)
8598
let m := appendSection m `def_eq "heuristic for solving `f a =?= f b`" heu
8699
let m := appendSection m `kernel "unfolded declarations" unfoldKernel
87100
let m := m ++ "use `set_option diagnostics.threshold <num>` to control threshold for reporting counters"

src/Lean/Meta/SynthInstance.lean

+9-5
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ def main (type : Expr) (maxResultSize : Nat) : MetaM (Option AbstractMVarsResult
640640
action.run { maxResultSize := maxResultSize, maxHeartbeats := getMaxHeartbeats (← getOptions) } |>.run' {}
641641
catch ex =>
642642
if ex.isRuntime then
643-
throwError "failed to synthesize{indentExpr type}\n{ex.toMessageData}"
643+
throwError "failed to synthesize{indentExpr type}\n{ex.toMessageData}\n{useDiagnosticMsg}"
644644
else
645645
throw ex
646646

@@ -794,14 +794,17 @@ def trySynthInstance (type : Expr) (maxResultSize? : Option Nat := none) : MetaM
794794
(toLOptionM <| synthInstance? type maxResultSize?)
795795
(fun _ => pure LOption.undef)
796796

797+
def throwFailedToSynthesize (type : Expr) : MetaM Expr :=
798+
throwError "failed to synthesize{indentExpr type}\n{useDiagnosticMsg}"
799+
797800
def synthInstance (type : Expr) (maxResultSize? : Option Nat := none) : MetaM Expr :=
798801
catchInternalId isDefEqStuckExceptionId
799802
(do
800803
let result? ← synthInstance? type maxResultSize?
801804
match result? with
802805
| some result => pure result
803-
| none => throwError "failed to synthesize{indentExpr type}")
804-
(fun _ => throwError "failed to synthesize{indentExpr type}")
806+
| none => throwFailedToSynthesize type)
807+
(fun _ => throwFailedToSynthesize type)
805808

806809
@[export lean_synth_pending]
807810
private def synthPendingImp (mvarId : MVarId) : MetaM Bool := withIncRecDepth <| mvarId.withContext do
@@ -815,9 +818,10 @@ private def synthPendingImp (mvarId : MVarId) : MetaM Bool := withIncRecDepth <|
815818
| none =>
816819
return false
817820
| some _ =>
818-
/- TODO: use a configuration option instead of the hard-coded limit `1`. -/
819-
if (← read).synthPendingDepth > 1 then
821+
let max := maxSynthPendingDepth.get (← getOptions)
822+
if (← read).synthPendingDepth > max then
820823
trace[Meta.synthPending] "too many nested synthPending invocations"
824+
recordSynthPendingFailure mvarDecl.type
821825
return false
822826
else
823827
withReader (fun ctx => { ctx with synthPendingDepth := ctx.synthPendingDepth + 1 }) do

tests/lean/run/3313.lean

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
class Zero (α : Type) where
2+
zero : α
3+
4+
class AddCommGroup (α : Type) extends Zero α where
5+
6+
class Ring (α : Type) extends Zero α, AddCommGroup α
7+
8+
class Module (R : Type) (M : Type) [Zero R] [Zero M] where
9+
10+
instance (R : Type) [Zero R] : Module R R := ⟨⟩
11+
12+
structure Submodule (R : Type) (M : Type) [Zero R] [Zero M] [Module R M] : Type where
13+
14+
class HasQuotient (A : outParam <| Type) (B : Type) where
15+
quotient' : B → Type
16+
17+
instance Submodule.hasQuotient {R : Type} {M : Type} [Ring R] [AddCommGroup M]
18+
[Module R M]: HasQuotient M (Submodule R M) := ⟨fun _ => M⟩
19+
20+
def Synonym (M : Type) [Zero M] := M
21+
22+
instance Synonym.zero {G : Type} [Zero G] : Zero (Synonym G) := ⟨(Zero.zero : G)⟩
23+
24+
instance Synonym.addCommGroup {G : Type} [AddCommGroup G] : AddCommGroup (Synonym G) :=
25+
{ Synonym.zero with }
26+
27+
instance Synonym.module (M : Type) {R : Type} [Zero R] [Zero M] [Module R M] :
28+
Module R (Synonym M) := { }
29+
30+
variable (R : Type) [Ring R]
31+
32+
#synth HasQuotient (Synonym (Synonym R)) (Submodule R (Synonym (Synonym R))) -- works
33+
34+
/--
35+
info: [type_class] max synth pending failures (maxSynthPendingDepth: 1), use `set_option maxSynthPendingDepth <limit>`
36+
AddCommGroup Ruse `set_option diagnostics.threshold <num>` to control threshold for reporting counters
37+
---
38+
error: failed to synthesize
39+
HasQuotient (Synonym (Synonym R)) (Submodule R (Synonym (Synonym R)))
40+
use `set_option diagnostics true` to get diagnostic information
41+
-/
42+
#guard_msgs in
43+
set_option maxSynthPendingDepth 1 in
44+
set_option diagnostics true in
45+
#synth HasQuotient (Synonym (Synonym R)) (Submodule R (Synonym (Synonym R))) -- fails
46+
47+
#synth HasQuotient (Synonym (Synonym R)) (Submodule R (Synonym (Synonym R))) -- works

0 commit comments

Comments
 (0)