Skip to content

refactor(cabal-install): generalise GenericInstallPlan to arbitrary node keys - #12092

Open
andreabedini wants to merge 1 commit into
haskell:masterfrom
andreabedini:andrea/wip/prep/p06-installplan-nodekey
Open

refactor(cabal-install): generalise GenericInstallPlan to arbitrary node keys#12092
andreabedini wants to merge 1 commit into
haskell:masterfrom
andreabedini:andrea/wip/prep/p06-installplan-nodekey

Conversation

@andreabedini

Copy link
Copy Markdown
Collaborator

Summary

Distribution.Client.InstallPlan was already polymorphic in its two node types
(GenericPlanPackage ipkg srcpkg), yet it hardcoded the key of those nodes to
UnitId throughout: the IsUnit synonym pinned Key a ~ UnitId, Processing
held Set UnitId, and BuildOutcomes was a Map UnitId.

This replaces the UnitId-specific IsUnit constraint with

type IsGraph ipkg srcpkg = (IsNode ipkg, IsNode srcpkg, Key ipkg ~ Key srcpkg)

and threads the shared key type (Key ipkg) through the operations that
previously mentioned UnitId directly, so Processing and BuildOutcomes
become parameterised over the key. Functions that print or report on keys gain
Pretty/Show (Key ipkg) constraints accordingly, and helpers become more
honest about what they need — e.g. depends :: IsNode a => a -> [Key a] instead
of depends :: IsUnit a => a -> [UnitId].

Why

Nothing in the plan logic actually depends on the key being a UnitId; it only
needs the two node types to share a common IsNode key. Removing that
over-specialisation is a small tightening of the abstraction on its own.

The concrete motivation is cross-compilation (#11179), where install plans are
keyed by a stage-qualified key rather than a bare UnitId. This is a
preparatory refactor extracted from that PR — landing it separately shrinks
the eventual core change and keeps it reviewable.

Behaviour

Behaviour-preserving. The sole instantiation on master
type PlanPackage = GenericPlanPackage InstalledPackageInfo (ConfiguredPackage UnresolvedPkgLoc)
— remains UnitId-keyed, so there is no functional change. No changelog entry
(internal refactor, not user-facing).

QA notes

  • cabal build all is green.
  • cabal-install /InstallPlan/ unit tests pass.
  • -Werror validate build (cabal.validate.project) is clean; hlint reports no hints.

Copilot AI review requested due to automatic review settings July 6, 2026 05:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors cabal-install’s install-plan graph abstractions to stop hardcoding UnitId as the node key, instead threading a shared Key type through GenericInstallPlan operations via a new IsGraph constraint. This is a behaviour-preserving abstraction tightening that keeps the current UnitId-keyed instantiation working while enabling future work (e.g. stage-qualified keys for cross compilation).

Changes:

  • Replace the IsUnit synonym with IsGraph and generalise key-dependent APIs (depends, Processing, BuildOutcomes, lookup, closures, execution).
  • Update ProjectBuilding code to use nodeKey/generic keys instead of installedUnitId/UnitId-specific types.
  • Adjust unit tests to use IsGraph and add key-type constraints where required for pretty-printing/debug output.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
cabal-install/src/Distribution/Client/InstallPlan.hs Introduces IsGraph and generalises plan operations/state from UnitId to an arbitrary shared node key.
cabal-install/src/Distribution/Client/ProjectBuilding.hs Updates dependency-fold and build-status lookup to use generic node keys (nodeKey) and key-parameterised maps.
cabal-install/tests/UnitTests/Distribution/Client/InstallPlan.hs Updates generators/tests to work with IsGraph and key pretty-printing/show constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cabal-install/src/Distribution/Client/ProjectBuilding.hs Outdated
Comment thread cabal-install/src/Distribution/Client/InstallPlan.hs
Comment thread cabal-install/src/Distribution/Client/InstallPlan.hs Outdated
Comment thread cabal-install/src/Distribution/Client/InstallPlan.hs
Comment thread cabal-install/src/Distribution/Client/InstallPlan.hs
Comment thread cabal-install/src/Distribution/Client/InstallPlan.hs
Comment thread cabal-install/src/Distribution/Client/InstallPlan.hs
@andreabedini
andreabedini force-pushed the andrea/wip/prep/p06-installplan-nodekey branch from 31a8412 to 07c2b95 Compare July 6, 2026 07:00
@andreabedini
andreabedini requested review from Mikolaj and fgaz July 6, 2026 09:15
@Mikolaj

Mikolaj commented Jul 16, 2026

Copy link
Copy Markdown
Member

Great to see PRs split and refactors done in separate PRs. Could you add a PR template and a changelog? Is there UndecideableInstance unavoidable?

Comment thread cabal-install/tests/UnitTests/Distribution/Client/InstallPlan.hs Outdated
@fgaz

fgaz commented Jul 25, 2026

Copy link
Copy Markdown
Member

Is it possible that this PR was authored at least partly by an LLM? It reads somewhat like claude to me

depends = nodeNeighbors
-- | @ipkg@ and @srcpkg@ are the two node types of a single graph: both are
-- 'IsNode's sharing a common key type. The shared key is spelled @'Key' ipkg@.
type IsGraph ipkg srcpkg = (IsNode ipkg, IsNode srcpkg, Key ipkg ~ Key srcpkg)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key ipkg ~ Key srcpkg spells trouble for typechecking driven development, because in case of a type error GHC is unlikely to generate a helpful type checking message of suggestion other than "you know, this equation should hold but it doesn't".

Do we really need to generalise? Could we come up with a more monomorphic / type-restricted approach?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied below

…ode keys

The install-plan machinery in Distribution.Client.InstallPlan was written to
be polymorphic in its two node types (`GenericPlanPackage ipkg srcpkg`), yet
it hardcoded the *key* of those nodes to `UnitId` everywhere: the `IsUnit`
synonym pinned `Key a ~ UnitId`, `Processing` held `Set UnitId`, and
`BuildOutcomes` was a `Map UnitId`. That is an over-specialisation — nothing
in the plan logic actually depends on the key being a `UnitId`; it only needs
the two node types to share a common key that is an `IsNode` key.

This replaces the `UnitId`-specific `IsUnit` constraint with

    type IsGraph ipkg srcpkg = (IsNode ipkg, IsNode srcpkg, Key ipkg ~ Key srcpkg)

and threads the shared key type (`Key ipkg`) through the operations that
previously mentioned `UnitId` directly, so `Processing` and `BuildOutcomes`
become parameterised over the key. The few functions that print or report on
keys gain `Pretty`/`Show (Key ipkg)` constraints accordingly. Helpers become
more honest about what they need — e.g. `depends :: IsNode a => a -> [Key a]`
rather than `depends :: IsUnit a => a -> [UnitId]`.

This is a behaviour-preserving refactor. The sole instantiation on master,
`type PlanPackage = GenericPlanPackage InstalledPackageInfo (ConfiguredPackage
UnresolvedPkgLoc)`, remains `UnitId`-keyed, so there is no functional change.
The motivation is cross-compilation (haskell#11179), where install plans are keyed by
a stage-qualified key rather than a bare `UnitId`; extracting this
generalisation on its own removes the `UnitId` assumption ahead of that work
and keeps the eventual core change smaller.
@andreabedini

Copy link
Copy Markdown
Collaborator Author

@Mikolaj

Could you add a PR template and a changelog?

Will do.

Is that UndecideableInstance unavoidable?

Good question. In this PR, yes it is. It's because Binary (GenericInstalPlan ...) has in its context Show (Key ipkg) which is a type family application. But why would a Binary instance require Show (Key ipkg)? It turns out it comes from Binary (Graph a) in Cabal-syntax. That instance's get rebuilds the graph via `, which shows a node key to format its "duplicate key" error message.

I had lived with it because as a general rule by cross-compilation work didn't need to change Cabal or Cabal-syntax but now I am convinced that Binary (Graph key) requiring Show key just to throw an error message is a silly idea.

I have opened #12169 to change the involved Binary instances to not re-validate the serialised data. The serialised files are never written by hand and always a round-trip of our put. The Structured constraint already gives a strong guarantee.

With #12169:

  • Binary (Graph a) stops demanding Show (Key a)
  • Binary (GenericInstallPlan ...) stops needing Show (Key ipkg)
  • Pretty is also gone because we don't use mkInstallPlan
  • So won't have any Key-applied constraints in any of the instances and we won't need UndecidableInstances.

@Mikolaj let me know which direction you want me to take.

@fgaz

Is it possible that this PR was authored at least partly by an LLM? It reads somewhat like claude to me

This has been extracted using Claude from my cross compilation PR #11179 which I had written myself. AI or not, I have put a sizeable amount of effort in it.

@Bodigrim

Key ipkg ~ Key srcpkg spells trouble for typechecking driven development, because in case of a type error GHC is unlikely to generate a helpful type checking message of suggestion other than "you know, this equation should hold but it doesn't".
Do we really need to generalise?

Fair point, and let's be clear, this generalisation is useless on master :D
The reason for this PR is that, in the cross-compilation work, the nodes of the build graph need to be tagged with the build stage but the current code makes it impossible:

type Key (GenericPlanPackage ipkg srcpkg) = UnitId

Changing it to type Key (GenericPlanPackage ipkg srcpkg) = Key ipkg allows me to instantiate InstallPlan (WithStage InstalledPackageInfo) (WithStage ElaboratedConfiguredPackage) whose Key is WithStage UnitId.

Notice that the polymorphism is not pervasive but only used twice

-- in Distribution.Client.InstallPlan
type InstallPlan           = GenericInstallPlan InstalledPackageInfo ConfiguredPackage
-- in Distribution.Client.ProjectPlanning.Types
type ElaboratedInstallPlan = GenericInstallPlan ElaboratedInstalledPackageInfo ElaboratedConfiguredPackage

Could we come up with a more monomorphic / type-restricted approach?

This is what I came up with, if you have other suggestions I am keen to hear.

@andreabedini
andreabedini force-pushed the andrea/wip/prep/p06-installplan-nodekey branch from 07c2b95 to 2d4d422 Compare July 28, 2026 06:08
@Mikolaj

Mikolaj commented Jul 28, 2026

Copy link
Copy Markdown
Member

@Mikolaj let me know which direction you want me to take.

Yes, #12169 sounds good to me, thank you.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants