Zulip Chat Archive

Stream: lean4

Topic: linters


Yury G. Kudryashov (Jan 25 2024 at 03:06):

Hi, where can I read about (a) builtin linters; (b) std4 linters? What is the difference between two approaches? I want to forward-port some missing linters from mathlib3 and add new ones, and I need docs and/or examples to do that.

Yury G. Kudryashov (Jan 26 2024 at 01:36):

@Mario Carneiro :up:?

Mario Carneiro (Jan 27 2024 at 04:24):

@Yury G. Kudryashov

The mathlib linters from lean 3 were ported to what are now the std linters. These run at the end of processing a file or the whole library, using #lint or in a separate stage in CI. They see all declarations in the environment, so they are good for things where even autogenerated definitions need to be linted, but they don't have access to the original syntax so they can't detect things like using the wrong tactics to do something, or bad formatting. These linters are disabled using @[nolint foo] or the nolints.json file.

The lean core linters are new in lean 4, and they trigger immediately after parsing each command. As a result they are better for giving "live feedback", but they are only given the syntax of the command, which means it can be difficult to even determine what things in the environment correspond to it. The elaboration info tree is available however, so these are well suited to linters that want to remark on syntactic things like use of deprecated definitions, unreachable tactics, or unused variables, and the results appear immediately in the editor. These lints are disabled using set_option linter.foo false before the command.

All unported linters are of the std linter kind, because that was the only kind we had in lean 3. But it's possible that they can be reimagined as lean core linters. (We also need better names for both kinds of linters, because neither one really subsumes the other and it's confusing to call them the same.)

Yury G. Kudryashov (Jan 28 2024 at 01:16):

Is there any documentation for people who want to write these linters (of either kind), or should I look at the source of existing linters and learn from there?

Mario Carneiro (Jan 28 2024 at 01:27):

there is nothing I am aware of. The linter APIs themselves are fairly simple, but you will want to copy other linters anyway to see the techniques for doing useful things from them

Mario Carneiro (Jan 28 2024 at 01:28):

example of a std linter: src#Std.Tactic.Lint.defLemma

Yury G. Kudryashov (Jan 28 2024 at 01:38):

Thank you! I'll start porting the fintype/decidable/encodable/... linter and ask more questions here tomorrow.

Emilie (Shad Amethyst) (Jan 28 2024 at 16:46):

I would love to see some of the knowledge one can get from analyzing the existing linters to be put down into text somewhere, so it can become less of an arcane art

Yury G. Kudryashov (Feb 03 2024 at 22:11):

How do I run a specific linter on a specific declaration?

Yury G. Kudryashov (Feb 04 2024 at 00:35):

Another question: given a docs#Lean.Expr of the form .forallE name type body info, how do I check if body uses this variable?

Mario Carneiro (Feb 04 2024 at 01:52):

If you want to run a linter on a declaration using the metaprogramming API it looks like this:

import Std
def foo : Nat := 1
#eval Option.isSome <$> Std.Tactic.Lint.docBlame.test ``foo

Mario Carneiro (Feb 04 2024 at 01:53):

If your expression has no loose bvars (which is generally an invariant for expressions in MetaM) then you can use !body.hasLooseBVars to check if the forall is dependent (or call isArrow which does both this and the forallE match)

Yury G. Kudryashov (Feb 04 2024 at 01:55):

And what's the right way to descend inside an expr to maintain the "no loose bvars" invariant?

Mario Carneiro (Feb 04 2024 at 01:56):

either instantiate body with an expression of type type if you have one, or instantiate it with a fresh mvar of that type, or use withLocalDecl to create a fvar of the type and instantiate body with that

Mario Carneiro (Feb 04 2024 at 01:56):

using instantiate1

Yury G. Kudryashov (Feb 04 2024 at 01:57):

#xy: I need Lean 4 version of docs3#get_pi_binders_nondep for the linter.

Yury G. Kudryashov (Feb 04 2024 at 01:57):

I'll try.

Mario Carneiro (Feb 04 2024 at 01:58):

I would suggest modeling that function after src#Lean.Meta.forallTelescope

Mario Carneiro (Feb 04 2024 at 01:59):

in particular, it needs to pass its return variables in continuation passing style because the local variables have to exist in an extended context

Mario Carneiro (Feb 04 2024 at 02:01):

in fact, you might even just do this by calling forallTelescope and postprocessing the returned Array Expr into the desired form

Mario Carneiro (Feb 04 2024 at 02:02):

and then you don't have to worry about the instantiating and local variable creation

Yury G. Kudryashov (Feb 04 2024 at 03:05):

AFAICS, forallTelescope won't allow me to use isArrow

Kyle Miller (Feb 04 2024 at 03:15):

Maybe you could take a look at docs#Lean.Meta.getFunInfo, which processes dependencies between arguments

Yury G. Kudryashov (Feb 04 2024 at 03:18):

I'll have a look

Yury G. Kudryashov (Feb 04 2024 at 03:18):

Thanks!

James Gallicchio (Feb 04 2024 at 03:42):

Maybe would be a good chapter for the metaprogramming book

James Gallicchio (Feb 04 2024 at 03:42):

(linters, as a topic)

Yury G. Kudryashov (Feb 04 2024 at 03:43):

You may want to do too many things in a linter...

Yury G. Kudryashov (Feb 04 2024 at 03:54):

BTW, where can I read about for loops in Lean 4?

Adam Topaz (Feb 04 2024 at 04:06):

https://lean-lang.org/papers/do.pdf ?

Adam Topaz (Feb 04 2024 at 04:07):

or are you looking for the docs#ForIn class?

Yury G. Kudryashov (Feb 04 2024 at 04:08):

Thank you for both links! It looks like I need neither of them right now but I'll read them tomorrow or on Monday.

Yury G. Kudryashov (Feb 04 2024 at 04:32):

How do I check if a given Expr is of the form ∀ _ _ _, Decidable _?

Kyle Miller (Feb 04 2024 at 04:37):

Use forallTelescopeReducing (reducing is for handling DecidableRel and such, do forallTelescope if you don't want that), and then for the expr e that it passes to the continuation, do e.isAppOfArity ``Decidable 2

Yury G. Kudryashov (Feb 04 2024 at 04:37):

I'm using t.getForallBody.isAppOf

Kyle Miller (Feb 04 2024 at 04:39):

Ok, that's fine -- equivalent to using forallTelescope, but faster since it doesn't instantiate anything, which you don't need here if you're just checking for Decidable.

Yury G. Kudryashov (Feb 04 2024 at 04:46):

First draft: branch#YK-dec-lint

Yury G. Kudryashov (Feb 04 2024 at 04:46):

Heavily based on docs#Std.Tactic.Lint.impossibleInstance

Yury G. Kudryashov (Feb 04 2024 at 04:47):

I know that it doesn't auto ignore decls in Decidable. yet.

Kyle Miller (Feb 04 2024 at 05:05):

Three comments:

  • Rather than (← inferType (← mkConstWithLevelParams declName)) you can do (← getConstInfo declName).type like before
  • Rather than if !... then you can use unless ... do
  • I'm not sure they ever appear in the wild, but to deal with optParams and such you can do t.cleanupAnnotations.getForallBody.isAppOf

Yury G. Kudryashov (Feb 04 2024 at 05:09):

Thank you!

Yury G. Kudryashov (Feb 04 2024 at 05:10):

I'll wait for the CI, then fix the issues.

Yury G. Kudryashov (Feb 04 2024 at 08:20):

While I'm fixing issues found by this linter, I would appreciate hints for writing a more advanced linter that also catches definitions that assume [Decidable _], then use it for proofs only.

Mario Carneiro (Feb 04 2024 at 08:22):

I think that would just be asserting that the [Decidable _] binder is nondependent in the theorem's type

Yury G. Kudryashov (Feb 04 2024 at 08:25):

I meant telling

def ex1 [DecidableEq α] (x y : α) : Nat : if x = y then 0 else 1

from

def ex1 [DecidableEq α] (x y : α) (p : α  α  Prop) (hp : x = y  p x y) (hp' : x  y  p x y) : {n : Nat // p x y} :=
  1, if h : x = y then hp h else hp' h

Yury G. Kudryashov (Feb 04 2024 at 08:26):

(of course, IRL p will depend on n)

Yury G. Kudryashov (Feb 04 2024 at 08:29):

The current implementation does what mathlib3 version did (minus ignoring Decidable.* and Encodable.* for now) but while looking at lint errors, I found some definitions that use decidability for proofs only.

Mario Carneiro (Feb 04 2024 at 08:30):

hm, I'll take a look at that tomorrow

Yury G. Kudryashov (Feb 04 2024 at 08:36):

BTW, how do I ask if the definition is computable?

Kyle Miller (Feb 04 2024 at 08:36):

I haven't tested it, but something you could do is take the body and erase all the proofs with something like

def eraseProofs (e : Expr) : MetaM Expr :=
  Meta.transform e
    (pre := fun e => do
      if ( Meta.isProof e) then
        return .done ( mkSyntheticSorry ( inferType e))
      else
        return .continue)

and then check whether the fvars for the Decidable instances are present.

For interfacing the forallTelescope with the declaration value to get the body, you can use docs#Lean.Expr.beta rather than mkAppN

Expr.beta is to make sure the fvars actually get sunk into the expression. This erasure idea though doesn't detect whether there's some have/let outside the proof that's only used within the proof. I guess for this the transform could also do zeta reduction if that's something you want to handle?

Yury G. Kudryashov (Feb 04 2024 at 08:37):

IMHO, noncomputable definitions should be treated like theorems: there is no reason to assume [Decidable] if you're not using it in the type and you aren't going to create a computable definition.

Yury G. Kudryashov (Feb 04 2024 at 08:38):

@Kyle Miller Thanks, I'll test it tomorrow.

Mario Carneiro (Feb 04 2024 at 08:39):

I'm not sure I agree with that; you can still compute definitions using Decidable using rfl / by decide even if they are noncomputable

Yury G. Kudryashov (Feb 04 2024 at 08:41):

Let's talk about some specific definition tomorrow. I'm going to bed soon.

Eric Wieser (Feb 04 2024 at 14:44):

Sometimes you have a structure with multiple fields, and even though one field is non-computable, you want the other one to be defeq to some other computable construction

Eric Wieser (Feb 04 2024 at 14:45):

The one I feel strongly about is Finsupp.single, which really ought to be defeq to Pi.single (but due to the classical decidable stuff in finsupp it currently isn't)

Kevin Buzzard (Feb 04 2024 at 16:55):

This could also be solved with nonconstructive finsupps, right?

Yury G. Kudryashov (Feb 04 2024 at 20:36):

I suggest the following: once I fix all theorems, I run the linter on noncomputable defs, get a list of matches, then we'll discuss whether it's better to enable it on noncomputable defs and add nolints or disable it.

Kyle Miller (Feb 04 2024 at 20:44):

Another linter idea: find cases of letI or haveI that occur inside proofs, since they could be replaced by let or have, and they're almost certainly accidentally from the port, or from people thinking that the I still stands for "instance" (like from Lean 3) rather than "inline" (which is now what it is in Lean 4).

The condition can't just be "in a proof". Maybe if the expected type at the letI or haveI is a Prop?

This would have to be done in consultation with the InfoTrees, since letI and haveI are not present in the elaborated expression.

Mario Carneiro (Feb 04 2024 at 23:45):

Kyle Miller said:

Another linter idea: find cases of letI or haveI that occur inside proofs, since they could be replaced by let or have, and they're almost certainly accidentally from the port, or from people thinking that the I still stands for "instance" (like from Lean 3) rather than "inline" (which is now what it is in Lean 4).

Note that it was a conscious decision in mathport to align letI to letI even though the semantics are different, because these are normally used for instances, and it turns out that we generally want them to be inlined too

Yury G. Kudryashov (Feb 05 2024 at 04:05):

@Kyle Miller I updated the linter so that it should work for defs now (tested on the examples from this thread, awaiting CI).


Last updated: May 02 2025 at 03:31 UTC