The "flexible" linter #
The "flexible" linter makes sure that a "rigid" tactic (such as rw
) does not act on the
output of a "flexible" tactic (such as simp
).
For example, this ensures that, if you want to use simp [...]
in the middle of a proof,
then you should replace simp [...]
by one of
- a
suffices \"expr after simp\" by simpa
line; - the output of
simp? [...]
, so that the final code containssimp only [...]
; - something else that does not involve
simp
!
Otherwise, the linter will complain.
Simplifying and appealing to a geometric intuition, you can imagine a (tactic) proof like a directed graph, where
- each node is a local hypothesis or a goal in some metavariable and
- two hypotheses/goals are connected by an arrow if there is a tactic that modifies the source
of the arrow into the target (this does not apply well to all tactics, but it does apply to
a large number of them).
With this in mind, a tactic like
rw [lemma]
takes a very specific input and return a very predictable output. Such a tactic is "rigid". Any tactic is rigid, unless it is inflexible
orstoppers
. Conversely, a tactic likesimp
acts on a wide variety of inputs and returns an output that is possibly unpredictable: if later modifications adds asimp
-lemma or some internals ofsimp
changes, the output ofsimp
may change as well. Such a tactic isflexible
. Other examples aresplit
,abel
,norm_cast
,... Let's go back to the graph picture above. - ✅️ [
rigid
-->flexible
] A sequencerw [lemma]; simp
is unlikely to break, sincerw [lemma]
produces the same output unless some really major change happens! - ❌️ [
flexible
-->rigid
] A sequencesimp; rw [lemma]
is instead more likely to break, since the goal aftersimp
is subject to change by even a small, likely, modification of thesimp
set. - ✅️ [
flexible
-->flexible
] A sequencesimp; linarith
is also quite stable, since iflinarith
was able to close the goal with a "weaker"simp
, it will likely still be able to close the goal with asimp
that takes one further step. - ✅️ [
flexible
-->stopper
] Finally, a sequencesimp; ring_nf
is stable and, moreover, the output ofring_nf
is a "normal form", which means that it is likely to produce an unchanged result, even if the initial input is different from the proof in its initial form. A stopper can be followed by a rigid tactic, "stopping" the spread of the flexible reach.
What the linter does is keeping track of nodes that are connected by flexible
tactics and
makes sure that only flexible
or stoppers
follow them.
Such nodes are Stained
.
Whenever it reaches a stopper
edge, the target node is no longer Stained
and it is
available again to rigid
tactics.
Currently, the only tactics that "start" the bookkeeping are most forms of non-only
simp
s.
These are encoded by the flexible?
predicate.
Future modifications of the linter may increase the scope of the flexible?
predicate and
forbid a wider range of combinations.
TODO #
The example
example (h : 0 = 0) : True := by
simp at h
assumption
should trigger the linter, since assumption
uses h
that has been "stained" by simp at h
.
However, assumption
contains no syntax information for the location h
, so the linter in its
current form does not catch this.
Implementation notes #
A large part of the code is devoted to tracking FVar
s and MVar
s between tactics.
For the FVar
s, this follows the following heuristic:
- if the unique name of the
FVar
is preserved, then we use that; - otherwise, if the
userName
of theFVar
is preserved, then we use that; - if neither is preserved, we drop the ball and stop tracking the
FVarId
.
For the MVar
s, we use the information of Lean.Elab.TacticInfo.goalsBefore
and
Lean.Elab.TacticInfo.goalsAfter
.
By looking at the mvar
s that are either only "before" or only "after", we focus on the
"active" goals.
We then propagate all the FVarId
s that were present in the "before" goals to the "after" goals,
while leaving untouched the ones in the "inert" goals.
The flexible linter makes sure that "rigid" tactics do not follow "flexible" tactics.
flexible? stx
is true
if stx
is syntax for a tactic that takes a "wide" variety of
inputs and modifies them in possibly unpredictable ways.
The prototypical flexible tactic is simp
.
The prototypical non-flexible tactic rw
.
simp only
is also non-flexible.
Instances For
Heuristics for determining goals goals that a tactic modifies what they become #
The two definitions goalsTargetedBy
, goalsCreatedBy
extract a list of
MVarId
s attempting to determine on which goals the tactic t
is acting and what are the
resulting modified goals.
This is mostly based on the heuristic that the tactic will "change" an MVarId
.
goalsTargetedBy t
are the MVarId
s before the TacticInfo
t
that "disappear" after it.
They should correspond to the goals in which the tactic t
performs some action.
Equations
- t.goalsTargetedBy = List.filter (fun (x : Lean.MVarId) => decide ¬x.name ∈ List.map (fun (x : Lean.MVarId) => x.name) t.goalsAfter) t.goalsBefore
Instances For
goalsCreatedBy t
are the MVarId
s after the TacticInfo
t
that were not present before.
They should correspond to the goals created or changed by the tactic t
.
Equations
- t.goalsCreatedBy = List.filter (fun (x : Lean.MVarId) => decide ¬x.name ∈ List.map (fun (x : Lean.MVarId) => x.name) t.goalsBefore) t.goalsAfter
Instances For
extractCtxAndGoals take? tree
takes as input a function take? : Syntax → Bool
and
an InfoTree
and returns the array of pairs (stx, mvars)
,
where stx
is a syntax node such that take? stx
is true
and
mvars
indicates the goal state:
- the context before
stx
- the context after
stx
- a list of metavariables closed by
stx
- a list of metavariables created by
stx
A typical usage is to find the goals following a simp
application.
Stained
is the type of the stained locations: it can be
- a
Name
(typically of associated to theFVarId
of a local declaration); - the goal (
⊢
); - the "wildcard" -- all the declaration in context (
*
).
- name: Lean.Name → Mathlib.Linter.Flexible.Stained
- goal: Mathlib.Linter.Flexible.Stained
- wildcard: Mathlib.Linter.Flexible.Stained
Instances For
Equations
Equations
- Mathlib.Linter.Flexible.instInhabitedStained = { default := Mathlib.Linter.Flexible.Stained.name default }
toStained stx
scans the input Syntax
stx
extracting identifiers and atoms, making an effort
to convert them to Stained
.
The function is used to extract "location" information about stx
: either explicit locations as in
rw [] at locations
or implicit ones as rw [h]
.
Whether or not what this function extracts really is a location will be determined by the linter
using data embedded in the InfoTree
s.
getStained stx
expects stx
to be an argument of a node of SyntaxNodeKind
Lean.Parser.Tactic.location
.
Typically, we apply getStained
to the output of getLocs
.
See getStained!
for a similar function.
getStained! stx
expects stx
to be an argument of a node of SyntaxNodeKind
Lean.Parser.Tactic.location
.
Typically, we apply getStained!
to the output of getLocs
.
It returns the HashSet
of Stained
determined by the locations in stx
.
The only difference with getStained stx
, is that getStained!
never returns {}
:
if getStained stx = {}
, then getStained' stx = {.goal}
.
This means that tactics that do not have an explicit "at
" in their syntax will be treated as
acting on the main goal.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Stained.toFMVarId mv lctx st
takes a metavariable mv
, a local context lctx
and
a Stained
st
and returns the array of pairs (FVarId, mv)
s that lctx
assigns to st
(the second component is always mv
):
- if
st
"is" aName
, returns the singleton of theFVarId
with the name carried byst
; - if
st
is.goal
, returns the singleton#[default]
; - if
st
is.wildcard
, returns the array of all theFVarId
s inlctx
with alsodefault
(to keep track of thegoal
).
Equations
- Mathlib.Linter.Flexible.Stained.toFMVarId mv lctx (Mathlib.Linter.Flexible.Stained.name a) = match lctx.findFromUserName? a with | none => #[] | some decl => #[(decl.fvarId, mv)]
- Mathlib.Linter.Flexible.Stained.toFMVarId mv lctx Mathlib.Linter.Flexible.Stained.goal = #[(default, mv)]
- Mathlib.Linter.Flexible.Stained.toFMVarId mv lctx Mathlib.Linter.Flexible.Stained.wildcard = Array.map (fun (x : Lean.FVarId) => (x, mv)) (lctx.getFVarIds.push default)
Instances For
SyntaxNodeKind
s that are mostly "formatting": mostly they are ignored
because we do not want the linter to spend time on them.
The nodes that they contain will be visited by the linter anyway.
The nodes that follow them, though, will not be visited by the linter.
Equations
- One or more equations did not get rendered due to their size.
Instances For
By default, if a SyntaxNodeKind
is not special-cased here, then the linter assumes that
the tactic will use the goal as well: this heuristic works well with exact
, refine
, apply
.
For tactics such as cases
this is not true: for these tactics, usesGoal?
yields `false.
Instances For
getFVarIdCandidates fv name lctx
takes an input an FVarId
, a Name
and a LocalContext
.
It returns an array of guesses for a "best fit" FVarId
in the given LocalContext
.
The first entry of the array is the input FVarId
fv
, if it is present.
The next entry of the array is the FVarId
with the given Name
, if present.
Usually, the first entry of the returned array is "the best approximation" to (fv, name)
.
Equations
- Mathlib.Linter.Flexible.getFVarIdCandidates fv name lctx = Array.map (fun (x : Lean.LocalDecl) => x.fvarId) #[lctx.find? fv, lctx.findFromUserName? name].reduceOption
Instances For
Tactics often change the name of the current MVarId
, as well as the names of the FVarId
s
appearing in their local contexts.
The function reallyPersist
makes an attempt at "tracking" pairs (fvar, mvar)
across a
simultaneous change represented by an "old" list of MVarId
s and the corresponding
MetavarContext
and a new one.
This arises in the context of the information encoded in the InfoTree
s when processing a
tactic proof.
persistFVars
is one step in persisting: track a single FVarId
between two LocalContext
s.
If an FVarId
with the same unique name exists in the new context, use it.
Otherwise, if an FVarId
with the same userName
exists in the new context, use it.
If both of these fail, return default
(i.e. "fail").
Equations
- Mathlib.Linter.Flexible.persistFVars fv before after = (Mathlib.Linter.Flexible.getFVarIdCandidates fv ((before.find? fv).getD default).userName after).getD 0 default
Instances For
reallyPersist
converts an array of pairs (fvar, mvar)
to another array of the same type.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The main implementation of the flexible linter.
Equations
- One or more equations did not get rendered due to their size.