Skip to content

Index alias declarations and resolve them as Lean does - #308

Merged
Deicyde merged 3 commits into
mainfrom
feat/index-alias-declarations
Oct 14, 2026
Merged

Deicyde merged 3 commits into
mainfrom
feat/index-alias-declarations

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Closes #266. I took this on 2026-10-14 when the user asked for new work; the board lists #266 under this session.

The declaration index skipped Batteries' alias. So an article whose lean: named an alias got lean-target-not-found from autoform audit --lean-root, and render built no link for it. ATLAS v1 has six such targets, the last ones the index cannot resolve.

Indexing (autoform_cli/lean.py)

  • alias b := a records b, and alias ⟨mp, mpr⟩ := a_iff records each of its names that is not _. Neither is recorded as ⟨mp.
  • Attributes and modifiers are read as for any declaration.
  • The := must be on the alias's line, which keeps lines where alias is an ordinary identifier out of the index. The target may be on that line or the next nonblank one.
  • Every declaration now records whether it is protected, written on its line or on the lines above that hold only attribute lists and modifiers.
  • A name written _root_.X is recorded as X, whatever namespace it is declared in. This applies to every declaration, not only aliases: theorem _root_.X inside a namespace was indexed under that namespace before.
  • SourceIndex.alias_target resolves one alias, and SourceIndex.reached follows a chain of them.

How the audit treats an alias (autoform_cli/audit.py)

An alias has its target's kind, size and elaboration limits when the index can be sure what the target is.

Resolving the target.

  • The audit tries the names Lean tries: the alias's namespace, each enclosing one, then the root. It passes over a declaration below the alias in its file, which Lean has not seen yet.
  • The first other name must be an earlier declaration in the same file. Otherwise the target is unknown:
    • a name the index lacks may come from Mathlib;
    • one in another file may not be imported;
    • a protected one for a one-component target is skipped by Lean unless the module system has also made it private.
  • An alias whose target is unknown matches any intent, and is measured by its own span, like any other declaration.
  • On ATLAS, the review counted 69 aliases the first version placed; 68 of them have their target in their own file.

Kind.

  • lean-target-kind-mismatch names an alias's kind as alias of def.
  • Each name of an iff alias, and any alias of one, is a theorem whatever its target is, since Batteries emits a thmDecl.
  • An alias of a non-theorem is a def in Batteries. The audit still checks it against its target's kind, because it asks what object an article describes, and an alias names its target's.

Other checks.

  • lean-target-wanted reports an alias of a wanted command.
  • An indexed alias ends the deprecated-attribute search and the node-too-large span of the declaration above it. Its own @[deprecated] makes it a deprecated target.

autoform unused-hypotheses

It now refuses an alias as an unread target ("an alias, whose own proof only names its target"), and the run exits 1. The probe would otherwise report every hypothesis as used.

Not covered

  • An alias whose target is in another file is not followed.
  • A target reachable only through open stays unresolved.
  • An iff alias with its ⟨…⟩ list split across lines, or with its := on the next line, is not indexed. Mathlib has six.

Tests

  • test_aliases_are_indexed_with_where_their_target_can_be:

    • both alias forms;
    • _ slots, modifiers and attributes;
    • guillemets, including a «x.y» target that counts as one component;
    • _root_ in names and targets;
    • a target on the very next line and after a blank line;
    • lines where alias is an ordinary identifier.
  • test_root_qualified_names_leave_their_namespace_out.

  • test_an_alias_is_followed_only_to_an_earlier_declaration_of_its_file:

    • a chain;
    • a same-file private target;
    • a dotted target that reaches a protected declaration;
    • an alias named like its target, which passes over itself.

    These come out unknown:

    • a cross-file alias, even when its own file holds an outer candidate;
    • a protected one-component target, with protected on the same line or on the line above;
    • a chain whose end the index cannot place.
  • test_audit_resolves_an_alias_s_target_as_lean_does: every resolved alias shows as a finding, covering:

    • same namespace;
    • a chain;
    • a private target;
    • a declaration below the alias;
    • an iff alias of a Mathlib lemma and of a local axiom, and an alias of an iff alias;
    • innermost namespace first.

    Five aliases that cannot be placed must give no finding:

    • a protected target;
    • an enclosing-namespace target;
    • a target in another file;
    • the Mathlib-shadowed Nat case;
    • a Mathlib target.
  • test_audit_reads_an_alias_s_limits_and_wanted_command_from_its_target:

    • a plain alias and an iff alias each report their target's limit;
    • an alias the index cannot place keeps its own limits;
    • an alias of a wanted command is reported as one.
  • test_audit_measures_an_alias_by_its_target_s_span: the target named beside its alias counts once.

  • test_an_alias_is_refused_without_running_lean, in test_unused_hypotheses.py: a cross-file alias and one the index could follow are both refused.

  • test_an_alias_ends_the_source_span_of_the_declaration_above_it, plus an old-alias case in the deprecated test.

Twenty-four mutants of the change were each caught by these tests:

  • the candidate order reversed, or only the first candidate tried;
  • no chain following;
  • the iff rule applied after resolving;
  • other files allowed;
  • continuing past a candidate the index lacks;
  • no declared-later rule;
  • protected always visible, or always skipped;
  • the alias's keyword used in the wanted check;
  • the alias's own limits and size;
  • no dedup of a target named beside its alias;
  • no unused-hypotheses refusal;
  • _root_ kept in names;
  • the next nonblank line skipped;
  • atomicity read from a .;
  • a cross-file candidate skipped instead of ending the search;
  • the line check run before the file check;
  • > for >= in the line check, which loops on an alias named like its target;
  • reached stopping at an iff alias;
  • an unplaced alias contributing no limits;
  • unused-hypotheses refusing only unplaced aliases;
  • a partly placed chain returning its last placed alias;
  • protected read only from the keyword's line.

The unit suite on Python 3.10 passed on the second commit: https://github.com/facebookresearch/autoform-bot/actions/runs/38540151992. CI on this PR covers the third.

Review history

An adversarial review of the first commit found that it treated every project file as visible. That could reach a declaration Lean cannot see: a downstream or unrelated file, Batteries' implicitly private wanted placeholders, or module-system private names. It also found that an iff alias could take its local target's kind. The second commit narrows resolution to the alias's own file, makes iff aliases theorems, and adds the tests above.

A review of the second commit found that protected written on its own line above the keyword, as Mathlib does 29 times, was not recorded. It also found that several rules that end resolution were unpinned. The third commit reads modifiers from those lines and adds the tests for them.

Overlap

Trial merges are clean against #307, #296, #300 and #301. The rest resolve mechanically.

#90, in lean.py and audit.py:

The #212-#219 and #284 stack, in lean.py, the README and tests/test_audit.py:

  • the stack's _scan returns found, len(lines), so keep that return, with this PR's Declaration(...) line;
  • the unused-hypotheses exit sentence becomes "it is not found, several source files declare it, or it is an alias, whose proof only names its target";
  • keep both imports in tests/test_audit.py.

This machine is over the agreed load limit for local pytest runs. The tests were called directly in Python, and CI on this PR is the full run.

The declaration index skipped Batteries' `alias`, so an article whose
`lean:` named an alias got `lean-target-not-found` from `autoform audit
--lean-root`, and `render` built no link for it. ATLAS v1 has six such
targets, the last ones the index cannot resolve.

`alias b := a` now records `b`, and `alias ⟨mp, mpr⟩ := a_iff` records
each of its names that is not `_`. The `:=` must be on the alias's line;
the target may be on that line or the next. A declaration also records
whether it is `private` or `protected`, and a name written `_root_.X` is
recorded as `X`, for every declaration.

The audit gives an alias its target's kind, size and elaboration limits.
It resolves the target as Lean resolves a name: in the alias's namespace,
then each enclosing one, then the root, passing over a declaration below
the alias in its file, another file's `private` one, and, for a
one-component target, a `protected` one. It stops at the first name the
index does not hold, since Mathlib or another library may declare it;
such an alias matches any intent and is measured by its own line. Each
name of an iff alias is a theorem whatever its target is. An alias of a
wanted command is reported like one.

`autoform unused-hypotheses` refuses an alias: its own proof only names
its target, so the probe would find every hypothesis used.

Closes #266.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 14, 2026
Review of the first version found that resolution treated every other
project file as visible. Lean sees only what a file imports, so a file
that imports the alias's file, or an unrelated one, could supply the
target: a wrong kind, and another declaration's limits and span charged
to the article. Batteries' wanted placeholders and module-system
declarations are private without saying so, which the index could not
see either.

An alias is now followed only to an earlier declaration of its own file.
A candidate in another file, one the index lacks, or a `protected` one
for a one-component target, which the module system may have made
private too, leaves the target unknown: any intent matches, and the
alias is measured by its own span. Every step goes to an earlier line,
so a chain always ends. Declarations record only `protected` now.

Each name of an iff alias, and an alias of one, is a theorem even when
the index reaches the iff lemma. A target written `«a.b»` is one
component. The unused-hypotheses refusal no longer suggests naming the
target, which may be outside the index. The tests now pin a target on
the very next line, a same-file private target, a dotted target that
reaches a protected declaration, a local iff target, a cross-file alias,
and a target named beside its alias.
A second review found that `protected` written on its own line above the
keyword, as Mathlib does 29 times, was not recorded, so an alias was
followed to a declaration Lean skips. The scanner now reads modifiers
from the lines above the keyword that hold only attribute lists and
modifiers.

The tests now pin the rules that end resolution: a candidate in another
file ends it even when the alias's own file holds an outer one, a
declaration on the alias's own line is passed over (an alias named like
its target), and a chain whose end the index cannot place leads nowhere.
They also check an iff alias's limits, the own limits of an alias the
index cannot place, and that unused-hypotheses refuses an alias it could
follow. The README says an explicitly private protected name is found
too, and that an unplaced alias keeps its own limits.
@Deicyde

Deicyde commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Verdict: ready to merge once it is out of draft. This review covers head f3f8be2. The PR went through three review rounds; each round's should-fix findings are fixed in the commit that followed it, and nothing above a nit remains.

Review rounds

  1. First commit:

    • Resolution treated every other project file as visible. Lean sees only what a file imports, so a downstream or unrelated file could supply an alias's target, and the audit would report a wrong kind and charge another declaration's limits and span to the article. Batteries' wanted placeholders and module-system declarations are private without saying so, so they hit the same problem.
    • An iff alias took a local target's kind.
    • Six mutants survived the tests.

    The second commit follows an alias only to an earlier declaration of its own file, makes iff aliases theorems, and adds tests.

  2. Second commit:

    • protected written on its own line above the keyword was not recorded. Mathlib does this 29 times.
    • The rules that end resolution were unpinned. One of them, the >= in the line check, is the only thing that stops an alias named like its target from looping.

    The third commit reads modifiers from those lines and adds the tests.

  3. Third commit: checked here.

What was checked

  • CI: all 11 checks pass on f3f8be2, real Lean included.
  • Resolution against Lean: alias_target was compared with resolveGlobalName, resolveQualifiedName and resolvePrivateName (Lean/ResolveName.lean, the same from v4.27.0 to v4.35.0).
    • It returns None at a candidate the index lacks or one in another file.
    • It passes over a candidate below the alias in its file.
    • It returns None for a protected candidate with a one-component target. Lean skips such a candidate unless it is also private, written or under the module system.
    • Each step goes to an earlier line of one file, so chains end.
  • Batteries: both names of an iff alias are theorems (addSide emits a thmDecl), and so is a plain alias of one (wasOriginallyTheorem).
  • Tests: 24 mutants of the change were each caught by the tests. They are listed in the description.
  • Scale: on ATLAS, 68 of the 74 aliases have their target earlier in their own file and are followed. The other six stay unknown and match any intent: five Mathlib targets reached through open ... in, and one in another file. On Mathlib, a survey of 5,766 aliases found every resolved target inside its file's import closure.

Known limits. None of these blocks merge.

  1. Only same-file targets are followed. A cross-file alias matches any intent and is measured by its own span and limits. Following imports would recover the one cross-file ATLAS alias.

  2. Shared target spans. An article naming an alias and another article naming its target are each charged the target's span, so a large target can give both a node-too-large. Splitting the target clears both.

  3. Inherited scanner limits. Some limits of the lexical scanner now apply to aliases too:

    • namespace A.B … end B loses track of the namespace;
    • export, and import all;
    • declaration-like lines inside multi-line strings;
    • a quoted namespace component holding a dot, which gives an impossible candidate.

    These predate the PR, and each fails toward "unknown" or an existing gap rather than a new false finding.

  4. Unindexed iff aliases. An iff alias whose ⟨…⟩ list is split across lines, or whose := is on the next line, is not indexed. Mathlib has six.

Overlap. #90 and the #212-#219/#284 stack each conflict mechanically. The description gives the resolution for whichever lands second.

Posted by PR swarm: PR Swarm Lead

@Deicyde
Deicyde marked this pull request as ready for review October 14, 2026 16:56
@Deicyde
Deicyde merged commit 1b4c04d into main Oct 14, 2026
11 checks passed
@Deicyde
Deicyde deleted the feat/index-alias-declarations branch October 14, 2026 16:57
Deicyde added a commit that referenced this pull request Oct 14, 2026
Resolves the conflicts with #308, which indexes alias declarations and
resolves them as Lean does. #288 touches only the REPL server and merges
as is.
- lean: keep the universe-binder stripping in _scan and build the name
  with #308's _qualified, recording whether the declaration is
  protected.
- lean: let the name prefilter read an alias line as _aliases does. It
  took `alias ⟨mp, mpr⟩ := t` to declare "⟨mp", so a file whose only
  requested name came from an iff alias was skipped: audit reported
  lean-target-not-found, check --lean-root reported the declaration as
  not found, and render built no link.
- audit: take #308's lean-target-wanted check, which reads each target's
  kind through aliases, and keep it before the early exit for catalogs,
  so a catalog that records proof: formalized is checked through its
  aliases too. kinds is now computed before that check. bodies stays
  after the exit, next to the limit loop and the size line that use it,
  since catalogs use neither; the size line keeps the catalog guard and
  sums the bodies' spans.
- tests: a targeted index finds both names of an iff alias, and the
  proved-catalog wanted test also names an alias of its theorem_wanted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Index alias declarations so articles can name them

1 participant