Repository navigation
Select the public Lean declaration among duplicates and report ambiguous names - #348
Conversation
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.
|
Review: changes requested (reviewed at 45d5bef) Reviewed for style, golfing, architecture and hygiene (source, README, tests, commits, PR body). The design holds: one Required:
Optional (behavior-preserving;
|
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.
|
Update: requested changes made (fa831c6)
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.
|
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.
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. |
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.
#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.
Summary
The lexical source index kept the first occurrence of each declaration name in
path order. An unbuilt or
privatedraft that sorted before the publicdeclaration 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_snapshotkeeps every occurrence of each name and selects one:
privatenor a wantedcommand (Batteries makes those private placeholders), wherever it is in the
order;
ones) no declaration, and every consumer reports the name as defined more
than once, listing each occurrence as
path:linein 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
privateor move it into a folder the walk skips,such as
.trash.SourceIndex.elsewhereand the first-wins bookkeeping are removed. Everythingthat 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: datamodel, 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.
earlier in this branch:
Correct the text that describes duplicate Lean declarations(docstrings,--require-declarationshelp, README),Return the ambiguity of a Lean name as one string(source golf),Fold the duplicate-declaration selection tests into one tableandAssert 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 removedautoform unused-hypotheses, so the merge drops this branch's edits tothat command's test and README text.
Describe the duplicate-declaration rule exactly and shorten a lookup(README, docstrings, one audit line) andPin 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 adeclaration's name from the next line when the keyword stands alone, and
passes that text to
_aliasesasbody; the merge passes this branch'sprivateflag as well.privateis still detected when the namecontinues 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-ambiguousis about one name with several source declarations,and neither check reads the other's data.
Behavior changes
autoform_cli.lean):Declaration.privateadded afterprotected(defaultFalse), soaliasis now the seventh positionalfield.
SourceIndex.elsewhereis removed. Added areSourceIndex.duplicates(every occurrence of each repeated name, in pathand line order),
occurrences(name)andambiguity(name)(thepath:linelist of a name that selects no declaration, else"").find(name)returns the selected declaration, orNonefor an absent orambiguous name.
_WANTED_KEYWORDSmoved from audit.py to lean.py.def xin one file areambiguous, 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 itstarget, 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 witherror: <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-declarationsexits 1, and its help text names the case.Python API change:
RenderReport.unresolvedentries are now complete messages
(
declaration not found in the Lean sources: <node>: <name>or theambiguous 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 codelean-target-ambiguous, reasonLean 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: thelean targetscheck fails withN finding(s): lean-target-ambiguousthrough audit. No code change.skeleton: the reason for an ambiguous name changesfrom
several source files declare it: <files>todeclaration defined more than once in the Lean sources: <path:line>, .... A name withone public declaration beside private ones is now probed in the public
declaration's module instead of being refused. That reason string also
appears in
--outputreports and--json.work list/work context(source_file) andsearch(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 occurrencesin 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.
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
modulefile, a declaration outside apublic sectionand withoutpubliccounts as public, so such a name may be reported as defined morethan 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:linelist ofoccurrencesandambiguity, not just whether a set is empty. The indexcases are rows of one table,
test_a_name_selects_its_one_public_declaration_or_is_ambiguous, namedbelow by row id.
earlier-privateandprivate-above(privateon thekeyword's line and on the line above). Expects
B.lean:2; occurrencesA.lean:1|3, B.lean:2.test_a_declaration_beside_an_earlier_private_twin_is_located_at_the_public_onetest_audit_checks_the_public_declaration_beside_an_earlier_private_twintest_a_root_beside_a_private_twin_is_probed_in_its_public_moduletest_lean_locations_skip_a_private_twin_and_leave_an_ambiguous_name_unlocatedtest_a_lean_target_skips_a_private_twin_and_an_ambiguous_one_is_unlocatedtest_helpers_sharing_a_source_name_are_located_in_their_own_module_s_filetest_check_resolves_a_name_beside_a_private_twin_and_rejects_two_public_onespins behavior main already has (check only tests that the name exists); its two-public case is the one that fails on main.later-private(A.lean:2). Thispins behavior main already has.
two-public(two public plus one private, all threelisted).
test_check_resolves_a_name_beside_a_private_twin_and_rejects_two_public_ones(exact error line, exit 1).
test_a_declaration_defined_twice_is_reported_not_located.test_audit_reports_a_target_defined_more_than_once_and_checks_nothing_else_of_it.test_a_root_several_files_declare_fails_closed_naming_every_file.two-privateandprivate-wanted.test_one_short_name_in_distinct_namespaces_is_no_duplicate(
A.x, root-writtenB.xand privateC.x, each at its own path and line,no duplicates). This pins behavior main already has.
wanted);private aliascounts as private (rowprivate-alias; without it, amutant that drops the alias's flag passed every test);
lone-private, a pin);(
test_an_alias_of_a_private_declaration_names_its_own_file_one);(
test_a_private_twin_ends_the_source_span_of_the_declaration_above_it,on
_source_spansdirectly; it fails if the duplicates are left out ofthe measured declarations).
test_unresolved_declarations_are_reported_not_linked: full message inRenderReport.unresolved.test_an_alias_ends_the_source_span_of_the_declaration_above_it: spanskeyed by declaration.
test_lean_file_edited_while_it_is_read_is_named: the edit runs once in_changed_pathand once in the snapshot's first attempt, leaving twodef editedin 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:
A.leanfor the earlier-private (both spellings), wanted,two-public, two-private and private-and-wanted rows;
A.lean;public declarations;
None.The later-private and lone-private rows and the distinct-namespace test
fail on main only because
occurrencesorduplicatesdoes not existthere; they pin behavior main already has. The private-alias row also
selects
A.leanon main, by path order; it pins that an alias'sprivatecounts. The new
_source_spanstestfails 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_linkedfails on mainon the old
RenderReport.unresolvedentry form, the intended Python APIchange.
Runs:
(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.passed: 3124 passed, 137 skipped. The 48 failures, all in
tests/test_library_generate.py, are environmental ("Autoform is notinstalled", because the venv uses
--no-install-project). The same 48fail on main 7e34a3a. The follow-ups were checked with the touched
files only.
ruff check autoform_cli servers testsis clean.probe.
Follow-ups, not in this PR:
main, so thegenerated-project workflow runs the landed check.
Ns.«foo»andNs.fooare one key(impact's
_name_keyalready 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.
_scan(3hunks), because Fix multiline Lean declaration indexing #363 on main also indexes names continued onto the next
line; this branch adds no conflict of its own.
found.append(Declaration(...))line in_scan,where Scale roadmap inventories and the full DAG explorer #90 adds the universe-binder strip and this branch passes
private.sizes[node_id] = sum(spans[...])line (Scale roadmap inventories and the full DAG explorer #90restructures that block, this branch keys spans by declaration) and the
_source_spanssignature line beside Scale roadmap inventories and the full DAG explorer #90's new helpers._selectsits beside_scan, away from Scale roadmap inventories and the full DAG explorer #90's_may_declare. Scale roadmap inventories and the full DAG explorer #90'sshort-name file filter still scans every occurrence of a requested name,
so the selection is unaffected.
paragraph. Keep Extract Lean skeletons one probe per root module #213's sentences on the probe module and the hash, and
this branch's sentences on names defined more than once.
test_skeleton.py, where this branch and the stack both add tests after
the duplicate-file test; keep both. From Treat every published page as untrusted #215 up there is also 1 extra
README hunk in the skeleton paragraph this branch rewrites, where the
stack re-wraps the old "several indexed files" sentence; Run skeleton probes in parallel and isolate probe failures #214's one
README hunk is that paragraph too. Label statement approvals by the repository's code-owner review policy #219 also gets 1 extra hunk in
__main__.py: it adds a self-approval warning just above the_renderwarning loops that this branch merges into one; keep its lines, then the
single loop.
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.