Zulip Chat Archive

Stream: mathlib4

Topic: Rewriting lint-style.py in Lean


Michael Rothgang (May 25 2024 at 17:18):

I have "just" rewritten most of lint-style.py in Lean: more specifically:

  • #12928 rewrites the set_option linter in syntax, as an honest syntax linter
    (This also means better coverage.)

  • #13199 rewrites most remaining linters as text-based Lean linters. For simplicity, that PR depends on this one - but feel free to review it already. (The file Tactic/Linter/TextBased.lean defines the linter; the other changes are from the previous PR or PRed separately.)

I am looking particularly for feedback on

  • any performance aspects I have overlooked? I've gone through at least one round of macro-optimising all checkers; now they're not abyssimally slow any more: but I'm sure they could be faster!
  • thoughts on integrating this into mathlib
  • taking a look at the spin-off PRs, particularly #12928 and #13212 (but #13203 is also welcome)

Michael Rothgang (May 25 2024 at 17:18):

Help rewriting the remaining linters is also most welcome: these are

  • the "missing module docstring" linter -> can or should this become part of the docBlame linter?
  • "the second line is not correctly indented" -> long-term, this should become part of a formatter...
  • "space after "←", in tactic lists (but not when this refers to monads) -> this should become a syntax linter
  • rewriting the non-terminal simp linter: I think the best way is to land #11821, after ironing out whatever kinks remain.

Thomas Murrills (May 25 2024 at 17:24):

For writing the remaining linters, note that #11520 (which isn’t quite ready, but does work) might be relevant/convenient: it allows us to lint syntax by matching on it in macro_rules style, e.g.

linting_rules : style
| `(foo_stx) => return msg

Thomas Murrills (May 25 2024 at 17:31):

(“String-based” linters like “space after ” in this context could involve inspecting the source info, and the syntax match means we know we’re looking “at the right thing”. Though the motivation for #11520 is really the ability to deprecate syntax.)

Damiano Testa (May 26 2024 at 07:59):

This is great, thanks! I left two "bureaucratic" comments on the PR.

Michael Rothgang (May 27 2024 at 08:52):

Thanks for the feedback. A status update:

  • the performance issue has been resolved (compiling instead of running the interpreter really helps)
  • I have hooked up the linter to the build workflow and verified that new linter's errors are caught in CI
  • In short: there are no conceptual questions remaining with the rewrite, mostly reviewing and landing code! Reviews are welcome on
    • #13240, rewriting the copyright header check (and basic infrastructure)
    • #13245, rewriting update_style_exceptions.py in Lean: an important pre-requisite for moving the later linters
    • #12928, rewriting the set_option linter: this is a syntax linter, hence this PR is independent of the others

This is one of my first programming projects in Lean: I'm sure there's the occasional language feature making my life easier. Even a look from the "code style" perspective is useful!

Richard Osborn (Jun 01 2024 at 04:08):

In #12928, I was looking at setOptionLinter and was thinking of ways of flattening the code. Ultimately, the code is essentially "if b then return" repeated, so it seems like there should be a way of writing this without the repetition. My first thought is there must be a way of wrapping CommandElabM Unit with an Alternative instance, so that it's failure is just return. The following is a partial flattening, but I feel like it could be made to look much nicer.

def setOptionLinter : Linter where run := withSetOptionIn fun stx => do
    unless getLinterHash ( getOptions) do
      return
    if ( MonadState.get).messages.hasErrors then
      return
    if let some (head, name) := do
      let head  stx.find? is_set_option
      let name := (toString ( parse_set_option head)).drop 1
      guard <| name.startsWith "pp." || name.startsWith "profiler." || name.startsWith "trace."
      return (head, name)
    then
      Linter.logLint linter.setOption head m!"Forbidden set_option `{name}`; please remove"

I'd love to know if what I'm imagining is possible or if I'm just sleep deprived :sweat_smile:

Richard Osborn (Jun 02 2024 at 03:24):

Ok, I managed to get the following working.

def setOptionLinter : Linter where run := withSetOptionIn fun stx => return <* OptionT.run do
  guard <| getLinterHash ( getOptions)
  guard ¬ ( MonadState.get).messages.hasErrors
  let head  stx.find? is_set_option
  -- Drop a leading backtick.
  let name := (toString ( parse_set_option head)).drop 1
  guard <| name.startsWith "pp." || name.startsWith "profiler." || name.startsWith "trace."
  Linter.logLint linter.setOption head m!"Forbidden set_option `{name}`; please remove"

Richard Osborn (Jun 03 2024 at 02:22):

@Michael Rothgang Does the above code seems like an improvement? Feel free to use it or if you want I can submit a patch to the PR. You will need to import Lake.Util.Lift for the Monad lifting instances.

Michael Rothgang (Jun 08 2024 at 19:55):

@Richard Osborn Thanks for thinking about this! To me, this code is denser, but not necessarily clearer... you're aiming at removing the initial "if b then return" pattern?

Jon Eugster (Jun 09 2024 at 11:22):

My reasoning is not really rational, but somehow I find the following the easiest to parse:

def setOptionLinter : Linter where run := withSetOptionIn fun stx => return <* OptionT.run do
  guard <| getLinterHash ( getOptions)
  guard ¬ ( MonadState.get).messages.hasErrors
  let head  stx.find? is_set_option
  let name := (toString ( parse_set_option head)).drop 1
  if name.startsWith "pp." || name.startsWith "profiler." || name.startsWith "trace." then
    Linter.logLint linter.setOption head m!"Forbidden set_option `{name}`; please remove"

Richard Osborn (Jun 09 2024 at 12:51):

I like Jon's suggestion as well. It'd also look nicer if guard wasn't a function, but notation. I realize the "monadic style" is still not as common as the normal imperative if-then, but I mainly find the nested if let some ... fairly clunky looking. As lean is the only language I know of that does this imperative style do blocks, I'm not entirely sure what the right balance is.

Patrick Massot (Jun 09 2024 at 18:39):

Those string manipulations are not the right way to handle Lean names. You should use the Name api.

Michael Rothgang (Jun 10 2024 at 06:27):

Fair point, I'll read up on it and use it. (Might be in two or three weeks only.)

Michael Rothgang (Jun 10 2024 at 06:27):

Thanks for the pointer!


Last updated: May 02 2025 at 03:31 UTC