Skip to content

Packaging

Joshua Shinavier edited this page Jun 10, 2026 · 4 revisions

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.

The model at a glance

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.

Definitions

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.

Modules and namespaces

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.

Module dependencies

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.

Packages

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.

Package dependencies and versions

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.

Self-bootstrapping and forward-compatibility

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.

Entity metadata

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.chars module 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).

Lifecycle and versioning

A LifecycleInfo records version milestones for an entity. Each milestone is independently optional:

  • availableSince — the Version in which the entity was introduced.
  • deprecatedSince — the Version in 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.

Cross-references

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.

Why metadata is bundled

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.

See also

Clone this wiki locally