Skip to content

perf(api): measure and shrink the public declaration surface #584

Closed

Description

@SamuelSchlesinger

The module template's @[expose] public section makes nearly every declaration public. main has 18,113 public theorems against 2,115 private ones. Every public declaration is API that users must navigate and that carries deprecation obligations. Under the module system it is also part of the interface: adding one, or changing its statement, can invalidate downstream build artifacts (#569). Helpers that no other module uses don't need to be public.

1. Measure. Use the .ilean reference data that Lake already produces, or an equivalent Lean environment traversal. For every public declaration in Complexity/, count references from other library modules, from ComplexityTests/ only, and from abstracts/. Report by directory:

  • never referenced outside their module;
  • referenced only by tests;
  • used by other modules.

Also count @[expose] definition bodies that no other module unfolds. Spot-check a sample against grep. Make the script reproducible and keep it under scripts/.

2. Pilot. In one or two high fan-out areas (for example Foundations/Data/List or a crypto directory), make the never-used-elsewhere helpers private, and narrow @[expose] where bodies aren't needed. Keep every public statement users rely on, and record the before/after interface size and the downstream invalidation effect of a typical helper edit. Clean-build time is expected to be roughly unchanged; the gain is in incremental rebuilds and API size. Say so honestly.

3. Policy. Propose a short AGENTS.md rule: helpers private by default, public only for intended API. Include the upstream implications (cslib conventions) and a compatibility rule for previously public names: a deprecation alias where outside users plausibly depend on them.

Done when: the measurement is published, the pilot has landed with measured effects, and the policy is proposed. Broad privatization across the library is follow-up work, done area by area, coordinated so it doesn't collide with in-flight PRs.

Endorsed by Samuel (2026-10-23).

Activity

  1. SamuelSchlesinger commented on Oct 23, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    Claimed by worker worker-claude-584-c4b0c164 (Claude Code, Opus 5.5), delegated by coordinator coordinator-claude-c4b0c164-20261023. Scope: Phase 1 only — the measurement script under scripts/ and the published counts. Pilot privatization and the policy proposal wait for the coordinator's choice of area. Branch: perf/public-surface-584. State: active.

  2. SamuelSchlesinger commented on Oct 23, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    worker-claude-584-c4b0c164: active. Scope widened by the coordinator to the shared #587 report: scripts/quality_report.py (with a small .olean reader, scripts/QualityReport.lean) now computes this issue's reference classification plus the #587 per-area signals. Validating; baseline tables and the PR follow on branch perf/public-surface-584.

  3. SamuelSchlesinger commented on Oct 23, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    Measurement (Phase 1) at main 2549de63, produced by scripts/quality_report.py (#604). The same run produces the #587 per-area baseline.

    Of 26,376 public declarations written in Complexity/, 11,815 (45%) are used by other library modules, 3,869 (15%) only by tests or abstracts (3,834 by tests), and 10,692 (41%) never outside their own module. 7,966 of those 10,692 are used inside their module (helpers) and the rest nowhere (terminal results or dead code). After setting aside declarations that may be used without being named (2,008: simp, fun_prop, aesop, instances, notation), that are named in module docs or READMEs (2,105), that are deprecated (314), and that a staying-public statement or exposed body mentions (1,039), 5,226 privatization candidates remain: 4,929 theorems and 297 definitions.

    Directory Public Used by other library modules Tests or abstracts only Never referenced outside module … used in own module … implicit-use attribute … named in docs … deprecated … in a public statement Privatization candidates (thm / def)
    Algorithms 1,372 632 159 581 (42%) 488 24 81 19 148 309 (280 / 29)
    Computability 18,122 7,601 2,721 7,800 (43%) 5,902 1,410 1,458 216 758 3,958 (3,711 / 247)
    Crypto 1,840 864 319 657 (36%) 429 231 108 32 28 258 (253 / 5)
    Foundations 2,699 1,532 316 851 (32%) 571 169 303 33 24 322 (319 / 3)
    Languages 224 169 22 33 (15%) 24 6 8 0 0 19 (19 / 0)
    Logics 1,565 776 195 594 (38%) 439 141 91 7 79 276 (266 / 10)
    MachineLearning 68 17 27 24 (35%) 18 2 6 4 2 10 (10 / 0)
    Probability 462 221 110 131 (28%) 89 13 46 3 0 69 (69 / 0)
    Tactic 24 3 0 21 (88%) 6 12 4 0 0 5 (2 / 3)
    Total 26,376 11,815 3,869 10,692 (41%) 7,966 2,008 2,105 314 1,039 5,226 (4,929 / 297)

    Most candidates are theorems used only in the proofs of their own module, so privatizing them needs no statement changes. Definitions are mostly pinned by public statements, so a definition becomes private only together with the statements that mention it.

    Exposure. 5,991 definitions are exposed through @[expose] (instances and abbreviations, which are exposed by default, are excluded). No other library module references 2,283 of them, and 2,023 more are referenced but never unfolded by name (unfold, delta, an element of a simp/rw list, an equation lemma). That's evidence, not proof, that the body isn't needed, since rfl and decide unfold silently. Tests and abstracts are non-module files that load private data, so they see every body whether or not it's exposed.

    Modules with the most declarations never referenced outside them
    Module Never referenced outside Candidates Public Transitive importers Rebuild fan-out
    Computability.GenericRAM.Simulation.Run 87 48 107 1 1
    Computability.Machines.Turing.MultiTape.Halting.Partrec 63 39 74 2 2
    Computability.Machines.Turing.SingleTape.Partrec 56 33 67 1 1
    Computability.Complexity.Classes.ValiantVazirani.Reduction 54 37 67 8 8
    Computability.Circuit.Boolean.Iteration.PrimeCycles 51 24 62 0 0
    Computability.Circuit.Rational.LaurentReduction 49 40 68 21 21
    Computability.Complexity.Classes.MIP.NEXP 49 20 55 1 1
    Computability.TreeEval.Reader.Cert.Step 47 46 61 3 3
    Computability.GenericRAM.Simulation.Turing 46 5 76 2 2
    Computability.Circuit.Rational.Affine 44 25 59 23 23
    Computability.Circuit.Boolean.CarrySave 44 24 58 6 6
    Computability.Complexity.Classes.Space.GeneralizedGeography.Reduction 44 17 47 1 1
    Computability.Complexity.Parameterized.VertexCover.BussKernel 43 26 60 0 0
    Computability.Complexity.Classes.Alternation.GuessVerify 42 29 46 5 3
    Computability.Complexity.Classes.ArthurMerlin.GraphNonIsomorphism 42 19 43 1 1
    Computability.Complexity.AffineDisperser.PolyTime 42 15 50 3 3
    Computability.Circuit.Monotone.BipartiteMatching 42 5 54 0 0
    Computability.Circuit.Boolean.WeightedThreshold.Potential 41 28 67 0 0
    Computability.Complexity.Problems.SubsetSum 41 23 56 16 16
    Computability.Machines.Turing.MultiTape.Universal.Compose 38 36 44 122 97

    Method. References come from the .ilean files that Lake writes beside each .olean. Lean's frontend fills them from the info trees of every command, including asynchronously elaborated proofs (Lean.Elab.Frontend, Lean.Server.References.findModuleRefs). Each reference records a full constant name with its defining module, the source range and the enclosing declaration. Only identifiers written in the source are recorded, never macro output. A small reader, scripts/QualityReport.lean, reads the three .olean parts without importing them. It provides the declaration list, kinds, export status, docstrings, declaration ranges and the same-module constants that each exported statement or exposed body mentions. References to generated constants (.eq_1, .induct, .mk, .rec, .injEq, simps and to_additive lemmas, …) count for their owner, and fields and constructors count for their structure.

    Reliability.

    • 20 sampled declarations across the categories were checked with grep, and every classification held. One apparent cross-module use of ExtOp.majorityDeMorganCircuit turned out to be the unrelated Cslib.Circuits.Boolean.majorityDeMorganCircuit.
    • Systematic text check: of the 10,692 declarations never referenced outside their module, 8,738 (82%) have no occurrence of their final name component anywhere else in the code of Complexity/, ComplexityTests/ or abstracts/. In a random sample of 25 from the remaining 1,954, every match was a name collision (mono, refl, run, …) or a doc mention, never a missed reference.
    • In the other direction, the 18 referenced declarations without textual matches are to_additive lemmas and identifiers inside ⟪…⟫ notation, where the .ilean data is right.

    Caveats.

    • Generated declarations are attributed to their owner and not counted as public declarations. That covers 5,461 public constants Lean generates, plus match_, _proof_ and eq_N names.
    • Attribute-driven use leaves no reference: simp sets, fun_prop, Aesop rules, instance resolution, ext/refl/trans, program_certificate, fo_realize. These declarations are flagged and kept out of the candidates. Explicit uses such as simp [foo], attribute [simp] foo and double-backtick names in tactic code are recorded.
    • Notation and macros: a use of u ≼ v records nothing for BigOLe. Declarations named on the right-hand side of a notation, macro or syntax command are therefore flagged.
    • Deprecated aliases (314) are counted separately and never as candidates; Quality program: a uniform, library-wide overhaul of interfaces, structure and documentation #587 deletes them.
    • Namespaces don't matter, because references are full constant names after open, export and dot-notation resolution. Text-based checks have to allow for them, which is why the cross-check above uses final name components.

    What the module system treats as interface (from the toolchain source and a scratch-module experiment):

    • Importers rebuild when the exported .olean bytes change. Lake's trace for a plain import uses only that file's hash, with early cutoff; import all and non-module files (our tests and abstracts) also depend on the private parts.

    • The exported .olean holds public statements (theorems become axioms), exposed bodies, inductive types, attribute entries for public declarations and compiler signature data. It also records the axioms each exported declaration's proof uses (exportedAxiomsExt).

    • Docstrings and declaration ranges live in .olean.server and are not part of that trace.

    • Measured on a scratch module, the exported .olean stayed byte-identical after:

      • a proof edit,
      • adding a private helper or changing its statement,
      • a docstring edit or shifting every line,
      • changing a non-exposed definition's body.

      It changed after adding a public theorem, changing an exposed body, and, surprisingly, after a proof edit that changed the axiom set (simp → decide: [propext, Quot.sound] → []). Proof edits are interface-neutral only while the axioms they use stay the same, which in practice they almost always do.

    • Rebuild fan-out is often much smaller than the set of transitive importers, because only public import chains propagate. Foundations.Data.Fin.Tuple has 693 transitive importers, but an interface change rebuilds only 146 modules.

    • Private @[simp] lemmas, @[aesop] rules and instances work inside their module and are not exported. A private @[fun_prop] theorem is different: Mathlib's fun_prop extension exports the entry anyway, and fun_prop in an importing module then fails with Unknown constant `_private…` . A private fun_prop helper needs attribute [local fun_prop], which works and exports nothing. A non-module test can't name a private declaration either, so privatizing a declaration that tests use means changing the tests.

    Pilot candidates (for the coordinator and Samuel). The largest invalidation payoff, measured as candidates times rebuild fan-out, is in the Machines and Circuit areas (Universal.Compose, Encodings.Decoder, Complexity.BigO, Circuit.Depth). Both areas are Codex's.

    active: Phase 1 is done; the PR is #604 (awaiting review). Waiting for the pilot choice before any source edits.

  4. SamuelSchlesinger commented on Oct 23, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    worker-claude-584-c4b0c164: the coordinator moved privatization to the area leads (#587), so there is no separate pilot. The policy is in #609 (a CONTRIBUTING subsection on the public surface under the module system, built on #604). State: awaiting review for both #604 and #609.

  5. SamuelSchlesinger commented on Oct 23, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    worker-claude-584-c4b0c164: I addressed the #604 review in e8977be8: custom build directories are now checked, areas follow #587's assignments, and documentation in any module protects a declaration. Re-review is requested. #609 (policy) is awaiting review. Once #604 lands, I'll rerun the #587 baseline with the corrected area map.

  6. SamuelSchlesinger commented on Oct 23, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    worker-claude-584-c4b0c164: both PRs are approved at their heads by coordinator-codex-01a16ab8. #604 (scripts/quality_report.py) is approved at 5ce04ec8 and #609 (CONTRIBUTING policy) at 52104e32; required CI is running on both. Once #604 lands, the #587 baseline needs a rerun with the corrected area map. Since 2549de63, every path except Init has an owner.

  7. SamuelSchlesinger commented on Oct 24, 2026

    @SamuelSchlesinger
    CollaboratorAuthor

    Done. #604 delivered the measurement (scripts/quality_report.py) and #609 the policy, both merged. Ongoing shrinking now runs under #587, which reruns the report after each area PR. — coordinator-claude-c4b0c164-20261023, after an issue audit by reviewer-claude-c4b0c164-01 against main at about 17:00 UTC.

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions