A package is a manifest and a source tree
Every package — a board project, a component library, even the standard
library — has the same shape: a cohdl.toml manifest and its
sources under src/. The manifest's [package]
section declares the package's identity: name and
version, plus optional license,
description and repository. A package that is a
buildable board also carries a [design] section naming its top
design; a pure library has none. This is the manifest of the Raspberry Pi
Pico 2 example that ships with the compiler:
[package]
name = "rpi-pico2"
version = "0.1.0"
license = "MIT"
[design]
top = "Pico2"
[dependencies]
"@raspberrypi/mcu" = "0.1.1"
"@richtek/dcdc" = "0.1.1"
flash = "0.1.1"
osc = "0.1.1"
passive = "0.2.1"
qfn = "0.1.1"
soic = "0.1.0"
std = "0.3.0"
usb = "0.1.1"
Module paths follow the file tree — there is no mod
declaration. A file's path under src/ becomes its module path,
rooted at the package name: in the @richtek/dcdc package, the
file src/buck_boost/rt6150b.cohdl is the module
richtek_dcdc::buck_boost::rt6150b. Scoped names are quoted in
the manifest and normalize to underscore namespaces in source. The Pico 2
design references its parts by exactly these paths:
inst mcu: raspberrypi_mcu::RP2350A_QFN60
inst reg: richtek_dcdc::buck_boost::rt6150b::BUCKBOOST_RT6150B
inst flash: flash::FLASH_W25Q32
A fully-qualified path is always valid. Or import once —
use osc::XTAL_12MHZ; — and write
inst xtal: XTAL_12MHZ unqualified thereafter. Each
use imports exactly one name; there are no glob imports.
Within a single package, every top-level name in every file is visible to
every other file. Across package boundaries, pub is enforced:
referencing a non-pub item from another package is a compile
error naming the item and its actual visibility.
Exact versions only
Every [dependencies] entry is name = "X.Y.Z" — an
exact version triple. Range operators (^, ~,
>=, <, *) are not a feature
CoHDL lacks yet; they are rejected at manifest parse, permanently, as a
language rule.
The reason is what the language is for. Software lives with semver ranges because a bad patch can be patched again; a fabricated board cannot. A component library's "safe patch" can move real copper — a pad grows, a courtyard shifts — and CoHDL guarantees that the same source and the same dependency set produce the same netlist bytes. That guarantee belongs to the language, not to a resolver's configuration, so there is no syntax for a floating version at all. Every bump is a deliberate, visible act:
$ cohdl check .
error[E1101]: dependency `passive`: `^0.2` is a version range — CoHDL requires exact versions (a hardware library's "patch" can move real copper; every bump is an explicit `cohdl update`)
--> cohdl.toml:19
= help: did you mean `passive = "0.2.0"`?
The lock file
cohdl.lock is generated beside the manifest: one
[[package]] entry per dependency, recording the exact version
that resolved and a sha256 hash over the dependency's full package
content — its sources, its reference documents, its footprints. The version
is a human label; the hash is the identity.
# cohdl.lock — generated by cohdl. Do not hand-edit; run `cohdl update` to change a pin.
[[package]]
name = "@raspberrypi/mcu"
version = "0.1.1"
hash = "sha256:afe3112afd773be46a8bb66007f936f0c0c01b103c1abb6d5f787e01f9fd874a"
[[package]]
name = "passive"
version = "0.2.1"
hash = "sha256:75d19bb814aee93091c2f49d7be59f85b55d9ccb2c2654924814d44737d6023e"
On every cohdl check and cohdl build, the
compiler re-hashes each resolved dependency and compares it against the
locked hash — before a single .cohdl file is parsed.
A mismatch is a hard error naming the package and both hashes, never a
warning and never a silent proceed:
error[E1103]: locked package `passive 0.2.1` has changed on disk: locked sha256:0000000000000000000000000000000000000000000000000000000000000000, found sha256:75d19bb814aee93091c2f49d7be59f85b55d9ccb2c2654924814d44737d6023e
--> cohdl.lock
= help: the content of a locked version must never change; if this bump is intentional, publish it as a new version and run `cohdl update`
An ordinary build never rewrites the lock entry for an unchanged pin — it
only verifies it. The lock file changes only when the manifest does: a row
is recorded when a dependency first resolves or when its manifest pin
changes, and rows are dropped for dependencies removed from the manifest.
Re-pinning to newer content remains cohdl update's job. Commit
cohdl.lock to version control, the same convention
design.lock already establishes for designators. It is the
durable, diffable record of exactly which library content a board was built
against — answerable months or years after fabrication, without any
registry lookup.
The registry
registry.cohdl.org is where published packages live. Its namespace has three closed tiers, and the tier is structural in the name itself — no badge, no metadata flag, no side channel to check:
| Tier | Name shape | Who publishes |
|---|---|---|
| Official | std, passive |
Bare names are reserved for the CoHDL project's own account — never first-come-first-served. |
| Manufacturer | @raspberrypi/mcu |
@brand must be a human-verified manufacturer account;
verification is a gated registry process, not self-service.
|
| Community | @contrib/imu |
Any authenticated account, first-come-first-served — within this one shared prefix only. |
Because the tier lives in the name, @raspberrypi/mcu and a
hypothetical @contrib/mcu are entirely distinct,
non-colliding, non-confusable names. The namespace grammar is validated
twice: client-side before any network call, so a bad name fails fast with
the same message the server would give, and server-side, which is
authoritative. Fetched content is cached under
~/.cohdl/registry, so a design that has resolved once keeps
building offline.
cohdl search QUERY [--json] is the read-only discovery path. It
needs no project or login and searches package metadata plus package-local
public parts projected from cohdl docs API sidecars. Part search
includes the owning package, fully-qualified and short names, device,
intent, arguments, structural variant, and primary and alternate AVL fields
within fixed resource-safety projection budgets, including ordinary
manufacturer and MPN values. The query is
trimmed, must contain at least three Unicode scalar values, may occupy no
more than 128 UTF-8 bytes and may contain no control character.
Search uses the most-recently-published version and reports its exact
version on every result; that is distinct from the greatest-semantic-version
rule used by an unversioned add or update. Package
and part families are bounded independently and expose has_more
when truncated, never a total count. Each existing package's most-recent
sidecar needs one idempotent cohdl docs --publish re-upload after
the search-index rollout.
The verbs
Discovery and the package lifecycle:
| Command | What it does |
|---|---|
cohdl search TPS59650 |
Searches packages and the most-recently-published API sidecars for
public parts. It is project-free, login-free and read-only;
--json returns the same bounded rows.
|
cohdl add @raspberrypi/mcu |
Resolves the greatest published semantic version (or the one given as
name@X.Y.Z), writes the
[dependencies] entry and the lock row in one step.
|
cohdl install |
Resolves every dependency against cohdl.lock, fetching
anything missing. A hash mismatch is a hard error.
|
cohdl update passive |
Re-resolves one dependency (or all of them, with no name) to its
greatest published semantic version, rewriting manifest and lock
together. This is the only sanctioned way a pin changes — never a
side effect of install or build.
|
cohdl remove passive |
Deletes the [dependencies] entry and its lock row —
the symmetric inverse of add.
|
cohdl login |
Opens the registry's account page and stores a token locally, for publishing. |
cohdl publish |
Validates the package name and license locally, packs the project into a deterministic archive, and publishes it. |
cohdl docs --publish |
Re-generates and uploads API documentation for an already-published version; this also idempotently refreshes its public-part search rows when that version is the most recently published. |
One detail closes the trust loop: on publish, the server independently
recomputes the archive's content hash, and that server-computed hash — not
the publisher's local one — is what cohdl.lock verifies on
every later install. A version number alone says nothing about content; the
hash chain from the registry's own computation to your lock file does.
Publishing
A license is required to publish. The CLI refuses in pre-flight, before
packing, and the server refuses independently — a version that declares no
[package] license is never accepted. The value itself is not
checked against any license list: proprietary terms are fine; only silence
is refused. description and repository are
optional and render on the package's registry page. None of these keys
affect a verdict or an emitted byte.
[package]
name = "passive"
version = "0.2.1"
license = "MIT"
description = "Chip resistors, MLCC capacitors, and inductors with verified land patterns."
[dependencies]
std = "0.3.0"
A package can also carry reference documents. One or more
#[doc("relative/path")] attributes on a declaration name files
relative to the package root; they ship inside the published archive and
render on the package's registry page through a Markdown subset that allows
no raw HTML. The compiler never opens them — like
#[intent(...)], they have zero effect on compilation:
#[doc("docs/README.md")]
pub device TestPoint {
pins {
required SIGNAL: 1 [passive]
}
}
Separately, cohdl docs derives a structured API document from
the compiler's checked view of a package. cohdl publish uploads
it best-effort as a replaceable sidecar after the immutable package archive
succeeds; cohdl docs --publish regenerates or backfills that
sidecar later without changing the package tar or content hash. The registry
API explorer reads the sidecar, and its package-local public
part items feed the most-recently-published search index. Malformed,
private, non-part and foreign dependency entries are skipped by indexing.
What ships today
The standard library is deliberately small: std owns the core
traits — TwoTerminal, Capacitor,
Resistor, Polarized, Diode,
IC, Connector — and nothing else. Because
std is every package's implicit prelude, anything it
accumulates becomes global vocabulary forever, so devices, purchasable
parts, pads and footprints all live in focused packages instead. Nothing
about std is privileged: it is an ordinary versioned package
that resolves by exactly the rule passive does.
The official families, all resolvable like any other dependency:
| Package | Owns |
|---|---|
std |
Universal component contracts only — the core traits. |
passive |
Generated chip resistors, MLCCs and chip inductors with their land patterns, plus passive helper circuits. |
qfn, soic |
Audited QFN/DFN/SON and SOIC/TSSOP land patterns. |
connectors |
General-purpose board connectors and headers. |
usb |
USB connectors — Type-C, Micro-B — and controllers. |
esd |
ESD protection devices. |
diode |
Discrete diodes. |
flash |
Nonvolatile NOR flash memories. |
ldo |
Low-dropout regulators. |
led |
Discrete and addressable LEDs and their traits. |
mic |
Microphones and the Microphone trait. |
mosfet |
Discrete MOSFETs. |
osc |
Crystals and oscillators. |
dcdc |
DC/DC step-down converters. |
logic |
Logic gates and buffers. |
audio-amp |
Audio power amplifiers. |
antenna |
RF antennas with layout guidance. |
cellular, esim |
Cellular modules and eUICC (eSIM) devices. |
load-switch |
Power-distribution load switches. |
misc |
Fabrication primitives with explicit electrical semantics — test points, mounting holes. |
@espressif/esp32, @raspberrypi/mcu,
@st/stm32
|
Espressif SoCs and modules, Raspberry Pi microcontrollers, and STMicroelectronics STM32 MCUs. |
@richtek/dcdc, @ti/dcdc,
@ti/logic, @ti/power-switch
|
Richtek DC/DC converters; TI DC/DC controllers, level translators, and protected power paths. |
The passive family sets the evidentiary bar: which resistors
and capacitors exist is datasheet data, not a guess — Yageo primary
throughout — and every emitted Yageo part number is checked against
Yageo's own specsheet endpoint. A part that does not resolve is omitted,
never asserted. Beyond the official and manufacturer tiers, a growing set
of @contrib community packages covers IMUs, chargers,
environmental sensors, radios and more, each stating plainly whether its
land pattern is the manufacturer's own recommendation or an independent
derivation.
Dependency loading is intentionally direct-only: if a part you instantiate
resolves its footprint from qfn, your design lists
qfn in its own manifest — which is why the Pico 2 manifest
above pins the land-pattern packages alongside the component families. What
your board depends on is exactly what its manifest says, nothing implied.