Skip to content

refactor(Tactic/Linter/Header): make the header linter stateful - #42201

Draft
marcelolynch wants to merge 9 commits into
leanprover-community:masterfrom
marcelolynch:2026/07/HeaderStateful
Draft

refactor(Tactic/Linter/Header): make the header linter stateful#42201
marcelolynch wants to merge 9 commits into
leanprover-community:masterfrom
marcelolynch:2026/07/HeaderStateful

Conversation

@marcelolynch

@marcelolynch marcelolynch commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

This PR rewrites the header linter as a stateful linter (leanprover/lean4#14357). Linter behavior on well-formed files does not change. All four header linter test files pass without changes.

The linter state replaces two pieces of ad-hoc machinery:

  • The state caches the library-root check. This removes the Std.Mutex that guarded the cache against concurrent async linter runs.
  • The state records a verdict for the module: the header checks ran, or the module is exempt. The linter checks the header of each module exactly once, and every later command takes a fast path.

One behavior improves on malformed files: a file without a module doc-string gets one set of warnings at its first command. The previous implementation repeated the full header parse and its warnings on every command of such a file. The repeated parse was the largest per-command cost of the linter, for example during edits of a new file that does not have its module doc-string yet.

Other performance effects are small. After the verdict, each command reads one state field, and the end-of-file checks of the consumer linters replace a header parse with a state read.

The state also stores the parsed module header, and the header linter becomes a producer. The minImports and longLine linters read the parsed header from the state for their end-of-file checks, instead of a re-parse of the file. When the header checks did not run, both linters fall back to the previous re-parse. Two new tests pin the payload path and the fallback path of the minImports report.

🤖 Generated with Claude Code

marcelolynch and others added 4 commits July 28, 2026 10:41
Rewrite the minImports linter with the stateful linter API
(leanprover/lean4#14357). The cumulative import data moves from a global
IO.Ref into the linter state, which the elaborator threads through the
commands of the file. The IO.Ref was unsafe under parallel elaboration,
so `#import_bumps` had to set `Elab.async false`; the stateful state is
safe and the option stays on.

Linter behavior is unchanged: MathlibTest/MinImports.lean passes as is
(validated on a nightly-2026-07-27 toolchain).

NOTE: CI stays red until the toolchain bump to v4.34.0-rc1, which first
provides `registerStatefulLinter`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new section runs with Elab.async explicitly on: the linter state
accumulates correctly across commands, and #import_bumps keeps parallel
elaboration enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ionIn'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ort_bumps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the t-linter Linter label Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR summary a314edf962

Import changes for modified files

No significant changes to the import graph

Import changes for all files
Files Import difference

Declarations diff (regex)

+ HeaderState
+ async_uses_norm_num
+ headerChecks
+ headerPost
+ longLinePost
+ minImportsPost
- headerLinter
- longLineLinter
- minImportsLinter

You can run this locally as follows
## from your `mathlib4` directory:
git clone https://github.com/leanprover-community/mathlib-ci.git ../mathlib-ci

## summary with just the declaration names:
../mathlib-ci/scripts/pr_summary/declarations_diff.sh <optional_commit>

## more verbose report:
../mathlib-ci/scripts/pr_summary/declarations_diff.sh long <optional_commit>

The doc-module for scripts/pr_summary/declarations_diff.sh in the mathlib-ci repository contains some details about this script.

Declarations diff (Lean -- pending)

Computed after the build finishes.


No changes to strong technical debt.

No changes to weak technical debt.

Current commit a314edf962
Reference commit 3b6f23172a

This script lives in the mathlib-ci repository. To run it locally, from your mathlib4 directory:

git clone https://github.com/leanprover-community/mathlib-ci.git ../mathlib-ci
../mathlib-ci/scripts/reporting/technical-debt-metrics.sh pr_summary
  • The relative value is the weighted sum of the differences with weight given by the inverse of the current value of the statistic.
  • The absolute value is the relative value divided by the total sum of the inverses of the current values (i.e. the weighted average of the differences).

marcelolynch and others added 5 commits July 28, 2026 17:28
Rewrite the header linter with the stateful linter API
(leanprover/lean4#14357). The linter state caches the library-root
check, which replaces the Std.Mutex that guarded the cache against
concurrent async linter runs, and records that the checks ran, so the
linter checks the header of each module exactly once. A file without a
module doc-string gets one set of warnings at its first command; the
IO.Ref-era behavior repeated the full header parse and its warnings on
every command of such a file.

Linter behavior on well-formed files is unchanged; the header linter
tests pass as is (validated on a nightly-2026-07-27 toolchain).

NOTE: CI stays red until the toolchain bump to v4.34.0-rc1, which first
provides `registerStatefulLinter`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header linter state gains two things. The `headerSyntax` field
stores the parsed module header, so other linters can read it instead
of a re-parse of the file. The `settled` flag replaces `ran` and also
covers exempt modules, so every command of an exempt file takes the
same fast path as a checked file.

The `withSetOptionIn'` helper moves to the `Mathlib.Linter` namespace
and becomes public: the consumer linters share the single copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eader linter state

The end-of-file report positions its warnings with the parsed header
from the state of the header linter. When the header checks did not
run, the report falls back to the previous behavior: it reads the file
and parses the header again.

Two new tests pin both paths. MathlibTest/Linter/Header/MinImportsPayload.lean
exercises the payload path, and a new section in MathlibTest/MinImports.lean
exercises the fallback. A temporary throwError probe in the fallback
verified that each test takes its intended path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… linter

The longLine linter becomes a stateful linter with trivial state, so
its end-of-file check can read the parsed header from the state of the
header linter. When the header checks did not run, the check parses
the header itself, as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@marcelolynch
marcelolynch force-pushed the 2026/07/HeaderStateful branch from 263f312 to a314edf Compare July 28, 2026 20:51
@mathlib-dependent-issues mathlib-dependent-issues Bot added the blocked-by-other-PR This PR depends on another PR (this label is automatically managed by a bot) label Jul 28, 2026
@mathlib-dependent-issues

Copy link
Copy Markdown

This PR/issue depends on:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked-by-other-PR This PR depends on another PR (this label is automatically managed by a bot) t-linter Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant