Skip to content

chore(project): shrink public module interfaces - #22

Draft
BoltonBailey wants to merge 3 commits into
SamuelSchlesinger:devfrom
BoltonBailey:chore/minimize-module-interfaces
Draft

chore(project): shrink public module interfaces#22
BoltonBailey wants to merge 3 commits into
SamuelSchlesinger:devfrom
BoltonBailey:chore/minimize-module-interfaces

Conversation

@BoltonBailey

@BoltonBailey BoltonBailey commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

This PR removes expose and public, to try to make the import interface more minimal and make the library in general faster to compile.

🤖 Generated with Claude Code

BoltonBailey and others added 3 commits August 1, 2026 19:09
Under the module system a module's interface is what every downstream
module must rebuild against. Two things inflate it: `public import X`
re-exports X to everything downstream, and `@[expose]` puts a
definition's body in the interface rather than just its signature.

This narrows both, mechanically and conservatively:

- 525 of 920 blanket `@[expose] public section` become `public section`,
  so definition bodies stay private to their defining module;
- 113 `public import X` become `import X`, as reported by `lake shake`.

Only removals were applied. Shake also suggests compensating *additions*
to restore a public closure it has narrowed; those are deliberately
ignored, so this can only shrink an interface, never widen one. Every
individual change was verified by a full build of all five CI targets
and reverted if it broke anything, so no proof was weakened or adjusted
to accommodate a narrower interface.

Measured effect on the built artifacts: 891 KB moved out of a 68.75 MB
public interface, with the private side growing by the same amount. The
reach of `@[expose]` is limited here because a public theorem's
statement stays in the interface regardless, and this library is mostly
theorems.

The remaining 395 exposes and the rest of shake's import report are
untouched; they are the candidates whose removal did not build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second batch of the mechanical interface narrowing begun in the previous
commit, produced and verified the same way: only removals are applied,
each one is gated on a full build of all five CI targets, and anything
that breaks is reverted rather than accommodated.

- 48 further `@[expose] public section` become `public section`;
- 19 further `public import X` become `import X`.

The expose pass is now complete: 573 of 920 blankets removed, 347 left
because their definition bodies are genuinely needed downstream. The
import pass ran a fresh `lake shake` afterwards and is roughly 9% done.

Two findings worth recording, both arguing against expecting much more
from this direction:

Re-scanning after 573 de-exposures unlocked only about 37 newly-legal
import demotions (~1011 candidates expected, 1048 found). De-exposing
does make demotions legal that were not before, but the effect is small
here.

Acceptance falls off sharply with a module's downstream reach. The
expose pass ran cone-size ascending and went from ~86% accepted among
near-leaves to ~44% among the widely-imported modules, where almost any
downstream proof that unfolds a definition blocks the change. The public
surface of this library is largely load-bearing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`scripts/shake_minimize.py` drives the mechanical narrowing performed in
the two preceding commits, and can resume it.

Under the module system a module's interface is what downstream modules
rebuild against. `public import X` re-exports X to everything
downstream, and `@[expose]` puts a definition's body in the interface
rather than just its signature. The two constrain each other: a public
declaration's signature may only mention names from public imports, and
an exposed body must too, so de-exposing is what makes some import
demotions legal. The script runs either shrink as a resumable queue.

The safety property is that it only ever applies *removals*. `lake
shake` also suggests compensating *additions* to restore a public
closure it has narrowed; those are deliberately ignored, so a change
that would need one simply fails its build and is rolled back. Every
candidate is verified by a full build of all five CI gate targets
individually, and no proof is ever adjusted to accommodate a narrower
interface.

Implementation notes:

- imports may wrap across two lines to satisfy the 100-column limit, so
  edits work on whole import statements and re-wrap only when needed;
- `meta import` lines are never touched, and shake's `-- shake: keep`
  and `keep-all` annotations are honoured;
- expose candidates are ordered by transitive dependent count ascending,
  which front-loads the cheap, near-certain wins — a module can only
  break things downstream of it, and that cone is also the rebuild cost;
- state is rewritten after every verified step and SIGTERM is trapped,
  so an interrupted run leaves the tree at the last green state.

CONTRIBUTING.md documents the expose-before-imports workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant