Skip to content

Select the public Lean declaration among duplicates and report ambiguous names - #348

Merged
Deicyde merged 11 commits into
mainfrom
fix/lean-duplicate-declarations
Oct 17, 2026
Merged

Deicyde merged 11 commits into
mainfrom
fix/lean-duplicate-declarations

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The lexical source index kept the first occurrence of each declaration name in
path order. An unbuilt or private draft that sorted before the public
declaration Lean binds became the answer for render links, work and search
locations, audit's kind, limit and size checks, and impact lines, while
skeleton refused every cross-file repeat, the private-beside-public case
included. Closed draft #140 cleared the ambiguity
set in that case but still returned the private occurrence.

The scanner now records written private, and _indexed_source_snapshot
keeps every occurrence of each name and selects one:

  1. a name declared once has that declaration, even a private one;
  2. otherwise the one occurrence that is neither written private nor a wanted
    command (Batteries makes those private placeholders), wherever it is in the
    order;
  3. otherwise (two or more public occurrences, or only private and wanted
    ones) no declaration, and every consumer reports the name as defined more
    than once, listing each occurrence as path:line in path then line order.

So a private draft no longer wins. A public copy in a file no library builds
is reported as defined more than once, because the index cannot tell which
files are built: mark it private or move it into a folder the walk skips,
such as .trash.

SourceIndex.elsewhere and the first-wins bookkeeping are removed. Everything
that reads the index (check, render, audit, doctor, skeleton, work
list/context, search, impact) goes through
_indexed_source_snapshot, so they all see the same selection.

Commits:

  • Select the public declaration among duplicate Lean source names: data
    model, selection rule, and the consumers that only read the selection
    (skeleton, impact, audit spans, alias resolution).
  • Report Lean declarations defined more than once in check, render and audit:
    the user-facing diagnostics.
  • Say why a private and a public declaration of one name can coexist:
    rewords text from the first two commits.
  • Review follow-ups, each rewording, shortening or retesting code added
    earlier in this branch: Correct the text that describes duplicate Lean declarations (docstrings, --require-declarations help, README),
    Return the ambiguity of a Lean name as one string (source golf), Fold the duplicate-declaration selection tests into one table and Assert the selected declaration first in the selection table (test golf).
  • Merge main 22ae9093 into duplicate Lean declarations: Remove autoform unused-hypotheses in favor of Batteries' unusedArguments linter #352 removed
    autoform unused-hypotheses, so the merge drops this branch's edits to
    that command's test and README text.
  • Post-merge review follow-ups: Describe the duplicate-declaration rule exactly and shorten a lookup (README, docstrings, one audit line) and
    Pin a private alias among duplicates and trim the duplicate-name tests.
  • Merge main 30e28b11 into duplicate Lean declarations: Fix multiline Lean declaration indexing #363 reads a
    declaration's name from the next line when the keyword stands alone, and
    passes that text to _aliases as body; the merge passes this branch's
    private flag as well. private is still detected when the name
    continues on the next line, for theorems and aliases alike.

This check covers the source index only. It does not detect article-level
primary ownership (#143, and #193 for audit's duplicate-lean-target):
lean-target-ambiguous is about one name with several source declarations,
and neither check reads the other's data.

Behavior changes

  • Index API (autoform_cli.lean): Declaration.private added after
    protected (default False), so alias is now the seventh positional
    field. SourceIndex.elsewhere is removed. Added are
    SourceIndex.duplicates (every occurrence of each repeated name, in path
    and line order), occurrences(name) and ambiguity(name) (the
    path:line list of a name that selects no declaration, else "").
    find(name) returns the selected declaration, or None for an absent or
    ambiguous name. _WANTED_KEYWORDS moved from audit.py to lean.py.
  • Repeats in the same file now count. Two public def x in one file are
    ambiguous, where before the first one won. Lean rejects such a file anyway.
  • alias_target: an occurrence above the alias in the alias's own file is its
    target, whatever other files declare, because importing one of them would
    clash. A candidate found only in other files still answers None.
  • check --lean-root: a name defined more than once fails with
    error: <node>: declaration defined more than once in <root>: <name> (<path:line>, ...)
    and exit 1. The not-found line is unchanged.
  • render: an ambiguous name shows
    <code>name</code> <span class="bp-missing">defined more than once in the Lean sources</span>
    and the CLI warns
    warning: declaration defined more than once in the Lean sources: <node>: <name> (<path:line>, ...);
    --require-declarations exits 1, and its help text names the case.
    Python API change: RenderReport.unresolved
    entries are now complete messages
    (declaration not found in the Lean sources: <node>: <name> or the
    ambiguous message), and the CLI prints them as given. The CLI output for a
    name that is not found is unchanged byte for byte.
  • audit --lean-root: new finding code lean-target-ambiguous, reason
    Lean declaration target is defined more than once: <name> (<path:line>, ...),
    and no other Lean check runs on that name. Kind, deprecation, limits and
    size of a name with a private twin now come from the public declaration.
    Source spans are measured per declaration over every occurrence, so a
    private twin or a later duplicate now ends the span of the declaration
    above it (it used to be absorbed into that span, which could raise a false
    node-too-large).
  • doctor --lean-root: the lean targets check fails with
    N finding(s): lean-target-ambiguous through audit. No code change.
  • skeleton: the reason for an ambiguous name changes
    from several source files declare it: <files> to
    declaration defined more than once in the Lean sources: <path:line>, .... A name with
    one public declaration beside private ones is now probed in the public
    declaration's module instead of being refused. That reason string also
    appears in --output reports and --json.
  • work list / work context (source_file) and search (source_file,
    line): the public declaration's location for a name with a private twin,
    and null for an ambiguous name.
  • work impact: a compiled constant is located among its name's occurrences
    in its own module's file, so private helpers that share a source name with
    other declarations keep their lines. The selected declaration is used only
    when the module has no source path.
  • README: the skeleton, check, work-context, search and source-index
    paragraphs describe the rule and its limits. The rule sees only indexed
    declarations. A public declaration behind open X in, or written «main»
    where the article writes main, leaves a lone private occurrence selected.
    In a module file, a declaration outside a public section and without
    public counts as public, so such a name may be reported as defined more
    than once where Lean accepts it, or selected where Lean binds it nowhere
    outside its file.

Tests

Mapped to the issue's acceptance list. Each assertion checks the selected
declaration's path and line, and the full ordered path:line list of
occurrences and ambiguity, not just whether a set is empty. The index
cases are rows of one table,
test_a_name_selects_its_one_public_declaration_or_is_ambiguous, named
below by row id.

  • Earlier private, later public:
    • Index: rows earlier-private and private-above (private on the
      keyword's line and on the line above). Expects B.lean:2; occurrences
      A.lean:1|3, B.lean:2.
    • Consumers:
      • render: test_a_declaration_beside_an_earlier_private_twin_is_located_at_the_public_one
      • audit: test_audit_checks_the_public_declaration_beside_an_earlier_private_twin
      • skeleton: test_a_root_beside_a_private_twin_is_probed_in_its_public_module
      • runtime: test_lean_locations_skip_a_private_twin_and_leave_an_ambiguous_name_unlocated
      • search: test_a_lean_target_skips_a_private_twin_and_an_ambiguous_one_is_unlocated
      • impact: test_helpers_sharing_a_source_name_are_located_in_their_own_module_s_file
      • check: the private case of test_check_resolves_a_name_beside_a_private_twin_and_rejects_two_public_ones pins behavior main already has (check only tests that the name exists); its two-public case is the one that fails on main.
  • Later private, earlier public: row later-private (A.lean:2). This
    pins behavior main already has.
  • Two public definitions:
    • Index: row two-public (two public plus one private, all three
      listed).
    • check: two-public case of
      test_check_resolves_a_name_beside_a_private_twin_and_rejects_two_public_ones
      (exact error line, exit 1).
    • render and CLI: test_a_declaration_defined_twice_is_reported_not_located.
    • audit: test_audit_reports_a_target_defined_more_than_once_and_checks_nothing_else_of_it.
    • skeleton: test_a_root_several_files_declare_fails_closed_naming_every_file.
    • runtime and search: the ambiguous cases of the tests above.
  • Two private-only spellings: rows two-private and private-wanted.
  • Same short name in distinct namespaces: test_one_short_name_in_distinct_namespaces_is_no_duplicate
    (A.x, root-written B.x and private C.x, each at its own path and line,
    no duplicates). This pins behavior main already has.
  • Also covered:
    • a wanted command beside a theorem selects the theorem (row wanted);
    • a private alias counts as private (row private-alias; without it, a
      mutant that drops the alias's flag passed every test);
    • a lone private declaration stays selected (row lone-private, a pin);
    • an alias of a file-local private beside a public twin
      (test_an_alias_of_a_private_declaration_names_its_own_file_one);
    • a private twin ends the audit span above it
      (test_a_private_twin_ends_the_source_span_of_the_declaration_above_it,
      on _source_spans directly; it fails if the duplicates are left out of
      the measured declarations).
  • Adjusted existing tests:
    • test_unresolved_declarations_are_reported_not_linked: full message in
      RenderReport.unresolved.
    • test_an_alias_ends_the_source_span_of_the_declaration_above_it: spans
      keyed by declaration.
    • test_lean_file_edited_while_it_is_read_is_named: the edit runs once in
      _changed_path and once in the snapshot's first attempt, leaving two
      def edited in one file, now reported as two occurrences.

Fail-before evidence: the branch's test files were run against main
22ae909 in a scratch tree, selecting the 17 new and changed test functions
(28 cases). 27 failed and 1 passed (check's private-twin case, which main
already accepts). The discriminating failures are:

  • main selects A.lean for the earlier-private (both spellings), wanted,
    two-public, two-private and private-and-wanted rows;
  • render, runtime and search locate A.lean;
  • audit reports a kind mismatch;
  • check exits 0 on two public definitions;
  • skeleton refuses the private-twin case and gives the old reason for two
    public declarations;
  • impact loses the second private helper's line;
  • the alias target is None.

The later-private and lone-private rows and the distinct-namespace test
fail on main only because occurrences or duplicates does not exist
there; they pin behavior main already has. The private-alias row also
selects A.lean on main, by path order; it pins that an alias's private
counts. The new _source_spans test
fails on main because its span map is keyed differently; a mutant that
drops duplicates from the measured declarations shows it catches the span
bug. test_unresolved_declarations_are_reported_not_linked fails on main
on the old RenderReport.unresolved entry form, the intended Python API
change.

Runs:

  • At 02b5174 (main 30e28b1 merged), the 8 touched test files
    (test_lean_sources, test_audit, test_skeleton, test_render, test_graph,
    test_impact, test_runtime, test_search) plus test_doctor, test_cli and
    test_skill_examples pass on Python 3.13 (1178 passed, 41 skipped, 1
    xfailed). test_graph and test_audit also pass on a merge with main
    e4b5506, whose Let declaring containers and hidden source notes pass check with a warning #361 changes check. CI runs Python 3.10 and 3.13.
  • At 45d5bef, before the review follow-ups, the full non-Lean suite
    passed: 3124 passed, 137 skipped. The 48 failures, all in
    tests/test_library_generate.py, are environmental ("Autoform is not
    installed", because the venv uses --no-install-project). The same 48
    fail on main 7e34a3a. The follow-ups were checked with the touched
    files only.
  • ruff check autoform_cli servers tests is clean.
  • Real-Lean tests were not run. Every consumer is covered with the fake
    probe.

Follow-ups, not in this PR:

  • Refresh the bundled example's immutable pin once this is on main, so the
    generated-project workflow runs the landed check.
  • Compare names by component so Ns.«foo» and Ns.foo are one key
    (impact's _name_key already does).

Overlap with open PRs

The branch contains main 30e28b1 and merges cleanly with main e4b5506.
Checked with a per-PR merge-tree overlap of each open PR against this
branch merged with e4b5506, and against main; PRs not listed conflict
with this branch exactly as they do with main.

Refs #170. The issue's last acceptance item, a generated-project workflow running the landed check, needs the bundled example's pin refreshed after this merges; the issue can close then.

The lexical source index kept the first occurrence of each name by path
order. When an unbuilt or private draft sorted before the public
declaration Lean binds, render linked to the draft, runtime and search
reported its file, audit checked its kind and size, and impact lost the
line; skeleton and unused-hypotheses refused every cross-file repeat,
the private-beside-public case included (#170; #140 cleared ambiguity
but still returned the private occurrence).

The scanner now records written `private` (on the keyword's line or the
modifier lines above it). `_indexed_source_snapshot` keeps every
occurrence of each name and selects one: the only occurrence, else the
only one that is neither private nor a wanted command (Batteries makes
those private placeholders). Two public occurrences, or only private and
wanted ones, select nothing. `SourceIndex.duplicates` lists every
occurrence of a repeated name by path and line, `occurrences` and
`ambiguous` read it, and `format_locations` gives the `path:line` list
every diagnostic uses. `elsewhere` and its first-wins bookkeeping are
removed.

Consumers follow the selection: skeleton and unused-hypotheses probe the
public module and refuse an ambiguous name with each `path:line`; impact
locates a compiled constant among the name's occurrences in its own
module file, so private helpers keep their lines; an alias of a private
declaration above it in its own file resolves to it; audit measures spans
per declaration over every occurrence, so a private twin no longer
stretches the declaration above it. `_WANTED_KEYWORDS` moves to lean.py.

Design critique findings F1 (wanted commands), F2 (impact private
records) and F3 (alias of a file-local private) are folded in.
…audit

After the index stops selecting an ambiguous name, check, render and
audit still described it as not found, which sends the user looking for
a missing declaration instead of an extra one (#170).

`check --lean-root` now fails with "declaration defined more than once
in <root>: <name> (<path:line>, ...)". Render shows "defined more than
once in the Lean sources" in place of the location and warns with the
same list, so `--require-declarations` fails; `RenderReport.unresolved`
entries become complete messages so the CLI prints them as given. Audit
adds `lean-target-ambiguous` with the list and runs no other Lean check
on that name; doctor's lean targets check reports it through audit.

Design critique F7: the `RenderReport.unresolved` change is an API change
of a Python export, not of a CLI or JSON contract.
The _select docstring and the README said a private declaration and a public one of the same name coexist only in modules that do not import each other. Lean also accepts the public one in a module that imports the private one's file, because a private declaration is local to its file. Say that instead.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 15, 2026
@Deicyde

Deicyde commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Review: changes requested (reviewed at 45d5bef)

Reviewed for style, golfing, architecture and hygiene (source, README, tests, commits, PR body). The design holds: one _select gives every consumer the same answer, elsewhere is gone, and the impact and alias changes are needed. Targeted tests pass on the head. The blockers are stale or contradictory text and history.

Required:

  1. History. 45d5bef only rewords text added earlier in this PR, and its body is one 304-character line. Two commit messages cite a review document outside the repository: "Design critique F1..." in af445e0 and "Design critique F7" in 7552ab4. Keep F7's point as "RenderReport.unresolved entries are now complete messages; this changes a Python export, not a CLI or JSON contract." Since main keeps merge commits, cleaning this needs either a squash merge or an approved force-push that folds 45d5bef into af445e0 and drops the trailers. In the PR body, "Two commits" then becomes true; replace the ~/bin/pr_conflicts.sh invocation with a plain description.
  2. autoform_cli/lean.py:130: stale SourceIndex docstring. Use """The scanner's declarations: the one each name selects in ``declarations``, and every occurrence of a repeated name in ``duplicates``."""
  3. autoform_cli/lean.py:709-712: "a wanted command declares nothing" contradicts lean.py:59-60 and Batteries, which makes a private placeholder. Use "A private declaration, including the placeholder a wanted command makes, is local to its file, so beside them every other module sees the one public declaration." Align the comment at tests/test_skeleton.py:1926-1927 with it: "A private declaration is local to its file, so every other module binds the public one."
  4. autoform_cli/__main__.py:440-441: the --require-declarations help omits the new failure. Use "fail when a 'lean:' declaration is not found in the Lean sources, is defined more than once in them, or cannot be looked up because the sources cannot be read".
  5. autoform_cli/README.md:1077-1080: the module-system limit cuts both ways. Replace from "In a file that starts with" with: "In a file that starts with module, a declaration outside a public section and without public is private to Lean but public to the index, so such a name may be reported as defined more than once where Lean accepts it, or selected where Lean binds it nowhere outside its file." Delete "which can only report an ambiguity Lean would not" from the PR body.
  6. autoform_cli/README.md:984-985: reads as if checkdecls rejects duplicates. Use "fails on a lean: name that is absent from the sources (as leanblueprint checkdecls does for LaTeX blueprints) or defined more than once in them (see below)."

Optional (behavior-preserving; _select fold checked exhaustively):

  • Replace ambiguous() and the exported format_locations with one SourceIndex.ambiguity(name) -> str; all four call sites pair them (about 10 lines, one fewer export).
  • tests/test_lean_sources.py:3061-3134: merge the seven selection tests into one parametrized table (about 39 lines). Drop tests/test_unused_hypotheses.py:224-243, same locate_declaration path as the skeleton test (21). Keep one of the runtime and search tests (about 20). Pin the span fix at tests/test_audit.py:881-902 directly on _source_spans (about 8).
  • lean.py:121-125: private after protected, so _scan fits one line (2-3).
  • lean.py:717-719: public = [...] or same (2). lean.py:145-151 occurrences (2). __main__.py:1459-1462 single warning loop (2). lean.py:190-193 alias_target (2).
  • README.md:1095-1097: drop the Audit contract repeat (3). skeleton.py:2293: begin the reason with "declaration", as its sibling does.

Several sentences in this branch no longer matched the code:

- The SourceIndex docstring still said `declarations` holds every
  declaration the scanner found. It holds the one each name selects;
  every occurrence of a repeated name is in `duplicates`.
- The `_select` docstring said a wanted command declares nothing, while
  the `_WANTED_KEYWORDS` comment and the README say Batteries makes it a
  private placeholder. The placeholder is a private declaration, so say
  that, and align the skeleton test comment.
- `render --require-declarations` help named only the not-found case,
  though the flag also fails on a name defined more than once.
- The README limit for `module` files named one direction only. A
  declaration outside a `public section` and without `public` is private
  to Lean but public to the index, so a name can be reported as defined
  more than once where Lean accepts it, or selected where Lean binds it
  nowhere outside its file.
- The check paragraph read as if `leanblueprint checkdecls` also rejects
  duplicates.

The Audit contract repeated the source-index paragraph's account of
`lean-target-ambiguous`; drop the repeat, as the contract does not list
`lean-target-not-found` either.
All four diagnostics that report a name defined more than once (check,
render, audit and skeleton) passed `index.ambiguous(name)` straight to
`format_locations`. Replace the pair with `SourceIndex.ambiguity(name)`,
which returns the `path:line` list, or "" when the name selects a
declaration. `format_locations` is no longer exported.

The skeleton and unused-hypotheses reason for such a name now begins
with "declaration", as its not-found sibling and the render and check
messages do.

Smaller cuts with no change in behavior:

- `Declaration.private` follows `protected`, so both constructor calls
  pass the fields by position.
- `_select` folds the single-declaration case into the public filter:
  with no public declaration it falls back to all of them, which selects
  the only one or none.
- `occurrences` and `alias_target` lose an early return each.
- `_render` prints unresolved names and warnings in one loop, since both
  lists now hold complete messages.
The seven index tests for selection repeated one shape: write files,
look the name up, compare locations. One parametrized table now has a
row per case: earlier private (on the keyword's line and on the line
above), later private, a wanted command, a lone private, two public
plus a private, two private, and private beside `proof_wanted`. Each
row pins every occurrence by path and line, the selected one, and the
ambiguity string. The private flags are pinned by the selection itself:
without them the private rows would be ambiguous.

The audit test for a private twin ending the span above it built six
articles and a 420-line file to check that no finding appears, which
any change that suppresses findings would also pass. It now checks
`_source_spans` directly, as the alias span test beside it does.
Leaving the duplicates out of the measured declarations makes `big`
span four lines and drops `Big.lean:3`, so the regression still fails.

The unused-hypotheses private-twin test is removed: unused-hypotheses
looks names up through skeleton's `locate_declaration`, and
test_a_root_beside_a_private_twin_is_probed_in_its_public_module pins
the same probe imports through it.
On main, where `SourceIndex.occurrences` does not exist, every row of
the table failed on that attribute, hiding the selection main gets
wrong. Asserting `find` first makes the earlier-private, wanted,
two-public, two-private and private-and-wanted rows fail on main with
the wrong location; only the rows whose selection main already gets
right still fail on the missing attribute.
@Deicyde

Deicyde commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Update: requested changes made (fa831c6)

  • Items 2 to 6 are fixed:
    • the SourceIndex docstring;
    • the _select wording on wanted commands, with the matching test comment;
    • the --require-declarations help;
    • the README module-system limit, which now covers both directions;
    • the README check sentence, where checkdecls covers only the absent case.
  • The behavior-preserving golfing is applied; the PR is +463/-71, down from +520/-66:
    • SourceIndex.ambiguity(name) replaces ambiguous() and the exported format_locations, with messages byte-identical;
    • private sits after protected;
    • _select, occurrences and alias_target are shorter, and _render uses one warning loop;
    • the README Audit-contract repeat is dropped;
    • the seven selection tests are one table that asserts the selected path and line;
    • the span fix is pinned directly on _source_spans, and a mutant check confirms it catches the bug;
    • the unused-hypotheses private-twin test is dropped, since the skeleton test covers the same locate_declaration path.
  • Kept: the runtime and search tests, which exercise different code (runtime.py:342 and search.py:458).
  • The only user-visible text change is the skeleton reason, which now begins with "declaration" like its sibling.
  • New conflict: draft Label statement approvals by the repository's code-owner review policy #219 gains one __main__.py hunk at the merged warning loop. Keep Label statement approvals by the repository's code-owner review policy #219's line, then the single loop.
  • Verified independently: touched test files on Python 3.10 and 3.13, and ruff. CI is green on every job: tests 3.10/3.13, real Lean, the real-Lean skeleton suite, the real REPL, Windows and CLA. It merges cleanly with main.

Still open: item 1, the commit history. It needs a squash merge or an approved force-push.

It is a draft, so it needs to be marked ready before merging.

#352 removed autoform unused-hypotheses. Drop this branch's edit to its
deleted test file, the README sentence saying it looks names up like
skeleton, and its mention among the commands that leave an ambiguous
name unresolved; keep this branch's skeleton description.
- README: work context and search leave `source_file` (and search's
  `line`) null when the scan finds several declarations and chooses none;
  the rule counts two or more public declarations, including one in a
  file no library builds.
- alias_target: a candidate above the alias wins because importing a
  public twin would clash and an imported private one is tried only after
  this file's own; written `private` is not consulted for a protected
  candidate.
- _select: "when the index can be sure which", "two or more", and its
  parameter is `occurrences`.
- impact `_locator`: the compiled module's file comes first and the index
  gives the line.
- audit looks a name up only after the ambiguity check.
- Add a private-alias row to the selection table: a mutant that drops the
  alias's private flag passed every test.
- Correct the edited-while-read comment: the edit is appended twice
  across three captures.
- `_index` is the one-file case of `_index_files`.
- Drop the doctor phase (doctor forwards audit's codes, which test_audit
  pins) and the check test's proof_wanted row (the same check path as the
  private row; the selection table pins wanted commands).
- Fold two pairs of assertions.
#363 reads a declaration's name from the next nonblank line when the
keyword stands alone, and passes that text to `_aliases` as `body`. Keep
it, and pass this branch's `private` flag as well.
@Deicyde

Deicyde commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Review: ready to merge (at 02b5174; this replaces the "changes requested" review above)

Re-reviewed after merging main, for new bugs, merge interactions, style, golfing and the design. Each finding was checked by an independent verifier.

  • Earlier items: items 2 to 6 were fixed in fa831c6. Item 1, the commit history, is settled by a squash merge (see below).

  • Merges:

  • Design: checked against Lean's own source (v4.34.1 and v4.35) and the Batteries the projects use:

    • Private names are mangled per module.
    • A module that imports a public declaration cannot declare a private one of the same name.
    • Importing two modules that declare the same public name fails, except for identical theorems.
    • Every wanted command, including a def_wanted with a body, elaborates to a private def.

    So the unique public declaration is the one every other module binds, and every other repeat fails closed. Preferring files a library target builds was weighed and rejected: module_of counts every module under a root, and it would tie check, render and audit to lake translate-config. On real projects nothing breaks today. None of the 783 names the hatcher-at and hartshorne blueprints reference is ambiguous: their ambiguous names, 34 and 35, are each declared only privately in several files.

  • Scanner: private is detected on the keyword line, on lines above (including across blank lines, comments and docstrings), after attribute lists, and on aliases. Nothing persists a SourceIndex, so no cache version needs bumping.

  • Tests: 43 mutants of the new lines, 40 caught. The one real gap was a private alias, now a table row. The other two survivors are equivalent on any source Lean accepts. On main, 27 of the 28 new and changed cases fail; the one that passes is a private-twin check main already accepts.

  • Fixed in 260c369 and 75749ed:

    • the work-context and search contracts now say source_file/line are also null for an ambiguous name;
    • "two or more public declarations", and a public copy in an unbuilt file counts;
    • three stale docstrings;
    • one audit lookup moved below the ambiguity check;
    • a test comment that miscounted captures;
    • a redundant doctor phase and check-test row dropped;
    • a duplicated test helper merged.
  • Known limit, documented, follow-up: in a module file, a declaration outside a public section is private to Lean but public to the index. In formal-math, 1900 of 1928 files are module files and 325 declarations are private by that default. None collides with a public name today, and a collision fails closed.

CI is green on every job at 02b5174, and the branch merges cleanly with main e4b5506. To merge: mark the PR ready, squash it, and trim the squash message to the PR title and summary. GitHub's default squash message includes every commit message, including the "Design critique F1/F7" lines.

@Deicyde
Deicyde marked this pull request as ready for review October 17, 2026 18:50
@Deicyde
Deicyde merged commit c2041a5 into main Oct 17, 2026
7 checks passed
@Deicyde
Deicyde deleted the fix/lean-duplicate-declarations branch October 17, 2026 18:53
Deicyde added a commit that referenced this pull request Oct 17, 2026
Main brings #333 (folder links by their committed spelling) and #348
(a Lean name selects its one public declaration, or is reported as
ambiguous). README.md, __main__.py, audit.py, lean.py, render.py and
tests/test_lean_sources.py conflicted. The resolution keeps main's
selection and its duplicates map, which replace this branch's elsewhere
map and per-file declaration lines; source spans take main's keying by
declaration and stay bounded by each file's own declarations. check and
audit report main's ambiguous names first and keep the ledger hint for a
catalog name that is not found. The alias prepass and the name filter
keep scanning every file that may declare a requested name, so a filtered
index selects as the unfiltered one does. Repository links take main's
folder spellings and keep the quoted ref.
Deicyde added a commit that referenced this pull request Oct 17, 2026
#348 made a Lean name select its one public declaration, keyed source
spans by declaration, and resolved an alias to a declaration above it in
its own file before looking elsewhere. Three tests this branch added
still assumed the index kept the first file's declaration: the span test
looked X.bar up by name, and the two alias tests expected an alias of its
own file's private declaration to have no target.

The span test now looks up X.bar's declaration. The alias tests move
B.lean's private N.aux below the alias, so the target is unknown exactly
while A.lean's N.aux is read too. With the alias prepass disabled both
alias tests fail again, and with duplicates left out of the span bounds
the span test fails.
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.

1 participant