-
Notifications
You must be signed in to change notification settings - Fork 24
Packaging
This page describes Hydra's packaging model: the types that organize Hydra programs into definitions, modules, and packages, and the metadata attached to each. It explains the model as a concept; for where this code physically lives, see Code organization, and for the kernel type breakdown see Implementation.
The packaging types are defined in
Hydra/Sources/Kernel/Types/Packaging.hs,
in the hydra.packaging namespace.
Hydra organizes code as a three-level containment hierarchy:
- A package is a named, independently versioned collection of modules.
- A module is a named collection of definitions sharing a namespace.
- A definition binds a name to a term, a type, or a primitive.
Package
├─ metadata, dependencies (on other packages)
└─ Module*
├─ metadata, dependencies (on other modules)
└─ Definition* (term | type | primitive)
└─ metadata
Every level — package, module, and definition — can carry the same optional
EntityMetadata (documentation and lifecycle information).
This uniformity is deliberate: see Why metadata is bundled.
A Definition is the smallest named unit. It is a union of three cases:
- A term definition (
TermDefinition) binds a name to a term, with an optional type signature. When the signature is absent, it is inferred. - A type definition (
TypeDefinition) binds a name to a type scheme. - A primitive definition (
PrimitiveDefinition) declares a built-in function or constant: its name, an always-explicit signature, purity and totality flags, and an optional cross-compilable default implementation expressed as a Hydra term.
Each definition carries its own optional EntityMetadata.
From the packaging point of view, a primitive is simply one kind of definition, declared independently of any host language; for how primitives are added and implemented, see Adding a primitive.
A Module is a logical collection of definitions in a single namespace.
It has:
- a name (a
ModuleName, e.g.hydra.core), which is the common prefix for every definition name in the module; - optional metadata;
- a list of dependencies on other modules;
- the definitions themselves.
A module name is both an identity and a namespace prefix.
A QualifiedName pairs an optional module name with a mandatory local name, which is how a
definition within a module is referred to from elsewhere.
A ModuleDependency names a depended-on module and, optionally, the package that provides it.
When the package is omitted, the resolver searches all packages in scope.
A module name that is ambiguous across packages is a resolution error, which can be disambiguated
by naming the intended package explicitly.
A Package is a named collection of modules — the unit of distribution and versioning.
It has a name (a PackageName, e.g. hydra-kernel), optional metadata,
a list of package-level dependencies, and its modules.
A PackageDependency names another package and constrains it with a VersionSpecifier.
A Version is a version string such as "0.15" or "1.0.0".
The version specifier is intentionally minimal today: the only variant is any (any version
satisfies the dependency).
Further variants such as exact, caret, and range can be added later without breaking
consumers of the any form — an instance of the forward-compatibility principle that runs
throughout the packaging model.
Hydra is self-hosting: its kernel is described in DSLs and code-generated into each host language, and those hosts are then used to build the distribution. A key consequence is that Hydra can bootstrap itself using an older, already-published version of a host — the build does not always need to rebuild a host from the current sources before it can use one.
This rests entirely on the forward-compatibility of the data model.
A previously-published host can process the current build's data only if the Module type and its
dependencies — the core of hydra.core and hydra.packaging — have not changed in a backward-incompatible
way since that host was released.
In other words, data produced by the newer build must remain readable by the older host.
When that holds, an older published host produces output equivalent to a freshly-built current host, and
the build can safely consume the published artifact (from Maven Central, PyPI, or Hackage) instead of
rebuilding locally.
The stability of hydra.core and hydra.packaging is therefore not incidental — it is the foundation
that makes versioned, self-bootstrapping builds possible.
In practice, the build consumes published hosts by default — the Java coder from Maven Central, the
Python coder from PyPI, and the Haskell kernel runtime (hydra-kernel, hydra-haskell) from Hackage,
each pinned to a version in the project's hostVersion. When a change is backward-incompatible, an
older host cannot be trusted, and the build falls back to building a host locally as a one-time migration
shim (the --local-host mode) until a new compatible version is published. A single host can also be
pinned to an earlier good version, or forced local, without affecting the others. The contributor-facing
mechanics are in Migration shims and the
build system docs.
Any packaging entity — a package, a module, or a definition — may carry an optional
EntityMetadata record. It bundles four independently optional kinds of information:
- description — an optional, concise one-line human-readable summary of the entity.
-
comments — zero or more long-form prose notes: cross-cutting semantic conventions,
caveats, and references that would otherwise be repeated across the entity's constituents.
For example, a comment on the
hydra.lib.charsmodule can state once that all of its primitives interpret their argument as a Unicode code point, rather than repeating that on every predicate. - seeAlso — typed cross-references to related entities, for navigation and documentation.
- lifecycle — optional version-lifecycle milestones (see below).
A LifecycleInfo records version milestones for an entity. Each milestone is independently optional:
-
availableSince — the
Versionin which the entity was introduced. -
deprecatedSince — the
Versionin which the entity was deprecated, if applicable.
Further milestones (for example stableSince or removedSince) can be added later without
changing the types that carry the lifecycle.
The seeAlso field holds a list of EntityReference values.
An EntityReference is a typed pointer to a packaging entity: a package (by PackageName),
a module (by ModuleName), or a definition.
A reference to a definition is a DefinitionReference, which names a type, a term, or a
primitive by its Name.
Because these references are typed rather than free-form strings, tools can resolve and
validate them — for navigation, documentation, or impact analysis.
Earlier versions of the model attached documentation directly to each entity — for instance,
a module carried a description field of its own.
Adding any new kind of metadata then meant adding a parallel field to every entity type that
should carry it, changing the shape of Module, Package, and each definition type in lockstep.
Bundling all such information behind a single optional metadata field of type EntityMetadata
decouples what metadata exists from which entities carry it.
New metadata — another comment category, a new lifecycle milestone, a new kind of cross-reference —
is added inside EntityMetadata once, and every entity gains it without any change to its own
field shape.
This is a backward-compatibility measure: it lets the packaging model grow without breaking the
encoded shape of existing packages, modules, and definitions.
The same principle motivates the open-ended VersionSpecifier and LifecycleInfo.
-
Code organization — where packaging code lives (
packages/,heads/,dist/). - Concepts — the broader LambdaGraph data model and type system.
- Adding a primitive — primitive definitions in practice.
- Implementation — the kernel's type-module breakdown.