Skip to content

Commit 86a00bd

Browse files
committed
feat(TacticAnalysis): check if we are in a try/anyGoals context (#29964)
This PR adds a check if the tactic is being run in a `try` or `anyGoals` call, so that we do not add a warning for the original tactic failing in this context. This was the source of a few "original tactic failed" in the `grind` regression reports. Co-authored-by: Anne C.A. Baanen <vierkantor@vierkantor.com>
1 parent 8a851dd commit 86a00bd

File tree

3 files changed

+101
-37
lines changed

3 files changed

+101
-37
lines changed

Mathlib/Tactic/TacticAnalysis.lean

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ register_option linter.tacticAnalysis : Bool := {
4646

4747
namespace Mathlib.TacticAnalysis
4848

49+
/-- Information about a tactic in a sequence, parsed from infotrees and passed to a tactic
50+
analysis pass. -/
51+
structure TacticNode where
52+
/-- `ContextInfo` at the infotree node. -/
53+
ctxI : ContextInfo
54+
/-- `TacticInfo` at the infotree node. -/
55+
tacI : TacticInfo
56+
/-- This tactic is allowed to fail because it is in a `try`/`anyGoals`/etc block. -/
57+
mayFail : Bool
58+
59+
/-- Run tactic code, given by a piece of syntax, in the context of a tactic info node.
60+
61+
Convenience abbreviation for `ContextInfo.runTacticCode`. -/
62+
abbrev TacticNode.runTacticCode (i : TacticNode) :
63+
MVarId → Syntax → CommandElabM (List MVarId) :=
64+
i.ctxI.runTacticCode i.tacI
65+
4966
/-- Stores the configuration for a tactic analysis pass.
5067
5168
This provides the low-level interface into the tactic analysis framework.
@@ -55,7 +72,7 @@ structure Config where
5572
to a sequence of tactics from the source file. Should do all reporting itself,
5673
for example by `Lean.Linter.logLint`.
5774
-/
58-
run : Array (ContextInfo × TacticInfo) → CommandElabM Unit
75+
run : Array TacticNode → CommandElabM Unit
5976

6077
/-- The internal representation of a tactic analysis pass,
6178
extending `Config` with some declaration meta-information.
@@ -145,8 +162,7 @@ would result in three sequences:
145162
Similarly, a declaration with multiple `by` blocks results in each of the blocks getting its
146163
own sequence.
147164
-/
148-
def findTacticSeqs (tree : InfoTree) :
149-
CommandElabM (Array (Array (ContextInfo × TacticInfo))) := do
165+
def findTacticSeqs (tree : InfoTree) : CommandElabM (Array (Array TacticNode)) := do
150166
-- Turn the CommandElabM into a surrounding context for traversing the tree.
151167
let ctx ← read
152168
let state ← get
@@ -177,7 +193,13 @@ def findTacticSeqs (tree : InfoTree) :
177193
-- We discard `childTactics` here, because those are either already picked up by a
178194
-- sequencing operator, or come from macros.
179195
if let .ofTacticInfo i := i then
180-
return ((ctx, i), childSequences)
196+
let childSequences :=
197+
-- This tactic accepts the failure of its children.
198+
if stx.getKind ∈ [``Lean.Parser.Tactic.tacticTry_, ``Lean.Parser.Tactic.anyGoals] then
199+
childSequences.map (· |>.map fun i => { i with mayFail := true })
200+
else
201+
childSequences
202+
return (some ⟨ctx, i, false⟩, childSequences)
181203
return (none, childSequences)
182204
else
183205
return (none, childSequences))
@@ -274,35 +296,36 @@ structure ComplexConfig where
274296
/-- Test the `config` against a sequence of tactics, using the context info and tactic info
275297
from the start of the sequence. -/
276298
def testTacticSeq (config : ComplexConfig) (tacticSeq : Array (TSyntax `tactic))
277-
(ctxI : ContextInfo) (i : TacticInfo) (ctx : config.ctx) :
299+
(i : TacticNode) (ctx : config.ctx) :
278300
CommandElabM Unit := do
279301
/- Syntax quotations use the current ref's position info even for nodes which do not usually
280302
carry position info. We set the ref here to ensure we log messages on the correct range. -/
281303
withRef (mkNullNode tacticSeq) do
282304
let stx ← `(tactic| $tacticSeq;*)
283305
-- TODO: support more than 1 goal. Probably by requiring all tests to succeed in a row
284-
if let [goal] := i.goalsBefore then
306+
if let [goal] := i.tacI.goalsBefore then
285307
let (oldGoals, oldHeartbeats) ← withHeartbeats <|
286308
try
287-
ctxI.runTacticCode i goal stx
309+
i.runTacticCode goal stx
288310
catch e =>
289-
logWarning m!"original tactic '{stx}' failed: {e.toMessageData}"
311+
if !i.mayFail then
312+
logWarning m!"original tactic '{stx}' failed: {e.toMessageData}"
290313
return [goal]
291-
let (new, newHeartbeats) ← withHeartbeats <| ctxI.runTactic i goal <| config.test ctx
314+
let (new, newHeartbeats) ← withHeartbeats <| i.ctxI.runTactic i.tacI goal <| config.test ctx
292315
if let some msg ← config.tell stx oldGoals oldHeartbeats new newHeartbeats then
293316
logWarning msg
294317

295318
/-- Run the `config` against a sequence of tactics, using the `trigger` to determine which
296319
subsequences should be `test`ed. -/
297-
def runPass (config : ComplexConfig) (seq : Array (ContextInfo × TacticInfo)) :
320+
def runPass (config : ComplexConfig) (seq : Array TacticNode) :
298321
CommandElabM Unit := do
299322
let mut acc := none
300323
let mut firstInfo := none
301324
let mut tacticSeq := #[]
302-
for (ctxI, i) in seq do
325+
for i in seq do
303326
if firstInfo.isNone then
304-
firstInfo := some (ctxI, i)
305-
let stx : TSyntax `tactic := ⟨i.stx⟩
327+
firstInfo := some i
328+
let stx : TSyntax `tactic := ⟨i.tacI.stx⟩
306329
tacticSeq := tacticSeq.push stx
307330
match config.trigger acc stx with
308331
| .continue ctx =>
@@ -312,16 +335,16 @@ def runPass (config : ComplexConfig) (seq : Array (ContextInfo × TacticInfo)) :
312335
tacticSeq := #[]
313336
firstInfo := none
314337
| .accept ctx =>
315-
if let some (ctxI, i) := firstInfo then
316-
testTacticSeq config tacticSeq ctxI i ctx
338+
if let some i := firstInfo then
339+
testTacticSeq config tacticSeq i ctx
317340
else
318341
logWarningAt stx m!"internal error in tactic analysis: accepted an empty sequence."
319342
acc := none
320343
-- Insert a `done` at the end so we can handle a final `.continue` at the end.
321344
match config.trigger acc (← `(tactic| done)) with
322345
| .accept ctx =>
323-
if let some (ctxI, i) := firstInfo then
324-
testTacticSeq config tacticSeq ctxI i ctx
346+
if let some i := firstInfo then
347+
testTacticSeq config tacticSeq i ctx
325348
| _ => pure ()
326349

327350
/-- Constructor for a `Config` which breaks the pass up into multiple pieces. -/

Mathlib/Tactic/TacticAnalysis/Declarations.lean

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ def terminalReplacement (oldTacticName newTacticName : String) (oldTacticKind :
6565
let ((sig, _, modules), _) ← (Mathlib.Tactic.ExtractGoal.goalSignature name goal).run
6666
let imports := modules.toList.map (s!"import {·}")
6767
return .error tac m!"{"\n".intercalate imports}\n\ntheorem {sig} := by\n fail_if_success {tac}\n {stx}"
68-
tell stx _ oldHeartbeats new newHeartbeats :=
69-
match new with
68+
tell stx old oldHeartbeats new newHeartbeats :=
69+
-- If the original tactic failed, then we do not need to check the replacement.
70+
if !old.isEmpty then
71+
return none
72+
else match new with
7073
| .error _ msg =>
7174
if reportFailure then
7275
let msg :=
@@ -195,15 +198,16 @@ private abbrev mergeWithGrindAllowed : Std.HashSet Name := { `«tactic#adaptatio
195198
inherit_doc linter.tacticAnalysis.mergeWithGrind]
196199
def Mathlib.TacticAnalysis.mergeWithGrind : TacticAnalysis.Config where
197200
run seq := do
198-
if let #[(preCtx, preI), (_postCtx, postI)] := seq[seq.size - 2:].toArray then
199-
if postI.stx.getKind == ``Lean.Parser.Tactic.grind && preI.stx.getKind ∉ mergeWithGrindAllowed then
200-
if let [goal] := preI.goalsBefore then
201+
if let #[preI, postI] := seq[seq.size - 2:].toArray then
202+
if postI.tacI.stx.getKind == ``Lean.Parser.Tactic.grind &&
203+
preI.tacI.stx.getKind ∉ mergeWithGrindAllowed then
204+
if let [goal] := preI.tacI.goalsBefore then
201205
let goals ← try
202-
preCtx.runTacticCode preI goal postI.stx
206+
preI.runTacticCode goal postI.tacI.stx
203207
catch _e =>
204208
pure [goal]
205209
if goals.isEmpty then
206-
logWarningAt preI.stx m!"'{preI.stx}; grind' can be replaced with 'grind'"
210+
logWarningAt preI.tacI.stx m!"'{preI.tacI.stx}; grind' can be replaced with 'grind'"
207211

208212
/-- Suggest replacing a sequence of tactics with `grind` if that also solves the goal. -/
209213
register_option linter.tacticAnalysis.terminalToGrind : Bool := {
@@ -225,28 +229,28 @@ def Mathlib.TacticAnalysis.terminalToGrind : TacticAnalysis.Config where
225229
-- already solved by `grind` and if so pushing that tactic onto `replaced`.
226230
-- By repeating this until `grind` fails for the first time, we get a terminal sequence
227231
-- of replaceable tactics.
228-
for (ctx, i) in seq.reverse do
229-
if replaced.length >= threshold - 1 && i.stx.getKind != ``Lean.Parser.Tactic.grind then
230-
if let [goal] := i.goalsBefore then
232+
for i in seq.reverse do
233+
if replaced.length >= threshold - 1 && i.tacI.stx.getKind != ``Lean.Parser.Tactic.grind then
234+
if let [goal] := i.tacI.goalsBefore then
231235
-- Count the heartbeats of the original tactic sequence, verifying that this indeed
232236
-- closes the goal like it does in userspace.
233-
let suffix := ⟨i.stx⟩ :: replaced
237+
let suffix := ⟨i.tacI.stx⟩ :: replaced
234238
let seq ← `(tactic| $suffix.toArray;*)
235239
let (oldGoals, heartbeats) ← withHeartbeats <|
236240
try
237-
ctx.runTacticCode i goal seq
241+
i.runTacticCode goal seq
238242
catch _e =>
239243
pure [goal]
240244
if !oldGoals.isEmpty then
241-
logWarningAt i.stx m!"Original tactics failed to solve the goal: {seq}"
245+
logWarningAt i.tacI.stx m!"Original tactics failed to solve the goal: {seq}"
242246
oldHeartbeats := heartbeats
243247

244248
-- To check if `grind` can close the goal, run `grind` on the current goal
245249
-- and verify that no goals remain afterwards.
246250
let tac ← `(tactic| grind)
247251
let (newGoals, heartbeats) ← withHeartbeats <|
248252
try
249-
ctx.runTacticCode i goal tac
253+
i.runTacticCode goal tac
250254
catch _e =>
251255
pure [goal]
252256
newHeartbeats := heartbeats
@@ -256,7 +260,7 @@ def Mathlib.TacticAnalysis.terminalToGrind : TacticAnalysis.Config where
256260
break
257261
else
258262
break
259-
replaced := ⟨i.stx⟩ :: replaced
263+
replaced := ⟨i.tacI.stx⟩ :: replaced
260264

261265
if h : replaced.length >= threshold ∧ success then
262266
let stx := replaced[0]
@@ -281,16 +285,16 @@ register_option linter.tacticAnalysis.tryAtEachStep.fraction : Nat := {
281285
def Mathlib.TacticAnalysis.tryAtEachStep (tac : Syntax → MVarId → CommandElabM (TSyntax `tactic)) : TacticAnalysis.Config where
282286
run seq := do
283287
let fraction := linter.tacticAnalysis.tryAtEachStep.fraction.get (← getOptions)
284-
for (ctx, i) in seq do
285-
if let [goal] := i.goalsBefore then
288+
for i in seq do
289+
if let [goal] := i.tacI.goalsBefore then
286290
if (hash goal) % fraction = 0 then
287-
let tac ← tac i.stx goal
291+
let tac ← tac i.tacI.stx goal
288292
let goalsAfter ← try
289-
ctx.runTacticCode i goal tac
293+
i.runTacticCode goal tac
290294
catch _e =>
291295
pure [goal]
292296
if goalsAfter.isEmpty then
293-
logInfoAt i.stx m!"`{i.stx}` can be replaced with `{tac}`"
297+
logInfoAt i.tacI.stx m!"`{i.tacI.stx}` can be replaced with `{tac}`"
294298

295299
/-- Run `grind` at every step in proofs, reporting where it succeeds. -/
296300
register_option linter.tacticAnalysis.tryAtEachStepGrind : Bool := {

MathlibTest/TacticAnalysis.lean

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,40 @@ example : P 37 := by
216216
end
217217

218218
end tryAtEachStep
219+
220+
section grindReplacement
221+
222+
set_option linter.tacticAnalysis.regressions.omegaToCutsat true
223+
224+
-- We should not complain about `omega` (and others) failing in a `try` context.
225+
example : x = y := by
226+
try omega
227+
rfl
228+
229+
-- Example with more than one tactic step:
230+
example : x = y := by
231+
try
232+
symm
233+
symm
234+
omega
235+
rfl
236+
237+
set_option linter.unusedVariables false in
238+
theorem create_a_few_goals (h1 : 1 + 1 = 2) (h2 : y = z) : x = y := rfl
239+
240+
-- We should not complain about `omega` (and others) failing in an `any_goals` context.
241+
example : x = y := by
242+
apply create_a_few_goals
243+
any_goals omega
244+
rfl
245+
246+
-- Example with more than one tactic step:
247+
example : x = y := by
248+
apply create_a_few_goals
249+
any_goals
250+
symm
251+
symm
252+
omega
253+
rfl
254+
255+
end grindReplacement

0 commit comments

Comments
 (0)