One binary, focused verbs
Verbs that operate on a project take an optional PATH — either a
project directory (containing cohdl.toml and src/)
or a single .cohdl file — and default to the current directory.
login, lsp and self-update take no path.
search takes a query instead and works without a project.
| Verb | Purpose |
|---|---|
cohdl check |
Parse, resolve, type-check and run residual DRC. Writes no build artifacts. |
cohdl build |
Everything check does, then assign designators, bind
parts and emit the netlist, BOM, footprints and layout artifacts.
|
cohdl fmt |
Rewrite .cohdl source into canonical form (--check to gate in CI). |
cohdl lsp |
Start the Language Server Protocol server on stdio. |
cohdl self-update |
Replace the installed binary with the newest released one (--check to report only). |
cohdl search |
Search registry packages and the most-recently-published API docs for public parts. |
cohdl update |
Re-resolve dependencies to the greatest published semantic version; rewrite the manifest and lock file. |
cohdl add |
Add a dependency, fetch it, and pin it in cohdl.lock in one step. |
cohdl remove |
Remove a dependency and its lock row — the inverse of add. |
cohdl install |
Resolve every pinned dependency, fetching anything missing from the registry. |
cohdl login |
Store a registry token for publishing. |
cohdl publish |
Package the current project and publish it to registry.cohdl.org. |
cohdl docs |
Emit package API documentation or upload it to an already-published version. |
cohdl check
cohdl check [PATH] [--design NAME] [--std DIR | --no-std] [--json]
check runs the whole verdict ladder: the source parses, every
name resolves, every unit and trait obligation type-checks, every required
pin is connected, and the four residual DRC rules (voltage-exceed,
polarity-mismatch, single-driver, multi-driver) pass. Before any
.cohdl file is opened, the manifest's
[dependencies] pins are verified against
cohdl.lock — including each dependency's content hash — so a
verdict is never computed against library content the lock file did not
record. check writes nothing under out/ and never
touches design.lock; the one file it may write is
cohdl.lock, when the manifest's dependency set has changed —
a row the first time a dependency resolves, a rewrite when a pin was
edited, a removal when a dependency left the manifest.
-
--design NAMEselects whichdesigndeclaration to assemble when the project contains more than one; the default comes from[design] topin the manifest. -
--std DIRis a development override that replaces the pinnedstdpackage (theCOHDL_STDenvironment variable does the same). Using either emits the unsuppressable E1105 warning: the build no longer uses a lockedstdand must not be treated as reproducible.--no-stdopts out ofstdentirely.
Structured diagnostics with --json
check --json (and build --json) emits exactly one
JSON document to stdout instead of human-readable text. It is the same
diagnostic pipeline, restructured — the JSON and plain-text outputs always
report the identical diagnostic set, field for field, and that equivalence is
a tested property of the compiler. The document carries:
-
schema_version— an integer, bumped only on a breaking change to the document's own shape. Consumers must check it before parsing further; new diagnostic codes are ordinary content, not schema changes. -
verdict—"pass"or"fail", computed exactly as the exit code is: any error-severity diagnostic means"fail". -
diagnostics— a flat, ordered list. Each entry has a stablecode, aseverity, amessage, aprimaryspan (project-relative file, 1-based line and column range, and its own label), zero or moresecondaryspans, and ahelplist, verbatim from the text renderer. -
On
build --json, abuildobject naming the emitted artifact paths — present only when the verdict is"pass".
Here is a real capture. In a copy of the Pico 2 reference design, the 3.3 V
rail's net annotation was changed from [3.3V] to
[100nF] — a capacitance where a voltage belongs:
net V3V3 [100nF]: reg.VOUT, reg.FB, mcu.IOVDD, mcu.QSPI_IOVDD, mcu.USB_OTP_VDD, mcu.VREG_VIN,
flash.VCC, hdr.V3V3, q_vsys.Gate, r_vreg_avdd.A, r_vref.A
The compiler's answer, verbatim (exit code 1):
$ cohdl check . --json
{
"schema_version": 1,
"verdict": "fail",
"diagnostics": [
{
"code": "E110",
"severity": "error",
"message": "net voltage annotation has the wrong unit type: expected `Voltage`, found `Capacitance`",
"primary": {
"file": "src/main.cohdl",
"start_line": 67,
"start_col": 15,
"end_line": 67,
"end_col": 20,
"message": "`100nF` is a `Capacitance`"
},
"secondary": [],
"help": [
"annotate with a voltage (e.g. `[3.3V]`), or `[gnd]` for ground"
]
}
]
}
Note what the diagnostic contains: a stable code from the error-code registry, the exact span of the offending literal, and both the expected and the actual unit by name. That precision is a language rule, not a courtesy — it is what lets a tool (or a model) repair a design from diagnostics alone.
cohdl build
cohdl build [PATH] [--design NAME] [--std DIR | --no-std] [--out-dir DIR]
[--emit ipc2581] [--emit kicad_pcb] [--json]
build runs the full check, then assigns designators
(recorded in design.lock, so they stay stable across rebuilds),
binds part numbers, and emits the fabrication artifacts. Output goes to
out/ under the project directory, or to --out-dir DIR.
| Artifact | Path | When |
|---|---|---|
| KiCad netlist | out/<name>.net |
Always — importable by KiCad's pcbnew. |
| Bill of materials | out/<name>-bom.csv |
Always. |
| Designator lock | design.lock (project root) |
Always — commit it; it is the record of designator stability. |
| Footprints | out/footprints/*.kicad_mod |
One per pad-bearing footprint the design uses. |
| Layout constraints | out/<name>-layout.json |
Only when the design declares layout metadata. |
| Physics hints | CSV set in out/ |
Only when the design carries physics attributes — the constraint files the Quilter autorouter reads. |
| IPC-2581 handoff | out/<name>.xml |
Only with --emit ipc2581. |
| KiCad board | out/<name>.kicad_pcb |
Only with --emit kicad_pcb — a native KiCad 10
board file with placements and net-bound footprints, written by
the compiler itself (no KiCad installation). |
The output directory is managed, not just written to. Each build records the
set of files it wrote; on the next build, a file the compiler wrote before
but no longer produces — say, a layout document after the design's layout
metadata was removed — is deleted as stale, and a file the compiler did
not write is never overwritten. A transcript from the Pico 2
reference design, with the footprint list trimmed here (the real build
writes seventeen .kicad_mod files):
$ cohdl build examples/rpi-pico2
Built design `Pico2`: 52 instances, 67 nets
wrote examples/rpi-pico2/out/rpi-pico2.net
wrote examples/rpi-pico2/out/rpi-pico2-bom.csv
wrote examples/rpi-pico2/out/rpi-pico2-layout.json
wrote examples/rpi-pico2/out/footprints/passive-CHIP_0402.kicad_mod
wrote examples/rpi-pico2/out/footprints/qfn-QFN60N40P700X700_1EP340X340.kicad_mod
wrote examples/rpi-pico2/out/footprints/usb-connectors-type_c-FP_USB_C_Receptacle_HRO_TYPE_C_31_M_12.kicad_mod
wrote examples/rpi-pico2/design.lock
Every artifact is byte-deterministic: rebuilding the same source against the same locked dependencies produces identical files, so generated output can be committed and diffed like any other source.
cohdl fmt
cohdl fmt [PATH] [--check]
fmt rewrites every .cohdl file at
PATH into the language's canonical form — one authoritative
layout, so diffs show design changes and never formatting churn. In a project
directory it also canonicalizes the manifest's [dependencies]
section (entries sorted by name); it never touches cohdl.lock,
which is machine-generated.
fmt only formats valid source. A file that does not parse is
reported with the ordinary parse diagnostics and left untouched —
fmt is not a repair tool. With --check nothing is
rewritten: each drifted file is reported as
would reformat <file> and the command exits non-zero,
which is the shape a CI gate wants. When everything is already canonical it
prints All files are in canonical form. and exits 0.
cohdl lsp
cohdl lsp
Starts the Language Server Protocol server on stdio. It takes no flags or
arguments — the editor owns the transport. The server exits 0 after an
orderly shutdown/exit sequence, 1 on
exit without shutdown (as the protocol specifies),
and 2 on a transport failure. Editor wiring — including the VS Code
extension that packages this server — is covered in
Editor support.
cohdl self-update
Replaces the running binary with the newest released one, consuming the
same artifact contract as the one-line installer: it finds the newest
vX.Y.Z release on GitHub, verifies the download against the
release's published sha256sums.txt, and swaps it in place; a
release that publishes no build for this platform is reported as an error.
--check reports whether a newer release exists and installs
nothing. It takes no other arguments.
cohdl search
cohdl search QUERY [--json]
Searches both registry packages and package-local public part
declarations. It is read-only, needs neither a project nor a registry login,
and never changes a manifest, lock file or package cache. The query is
trimmed, must contain at least three Unicode scalar values, may occupy at
most 128 UTF-8 bytes, and may not contain a control character.
cohdl search TPS59650
cohdl search TPS59650 --json
cohdl search -- -12V
Human output has separate Packages and
Parts sections. Package hits include an actionable
name@version; part hits include the owning
package@version, importable fully-qualified name, trust tier and
matching manufacturer/MPN metadata. The JSON form carries the identical
bounded rows as one document: top-level query,
packages and parts, with a results
array and has_more boolean in each result family. There is no
total count; has_more is the explicit truncation signal.
Part search is projected from the most-recently-published version's
cohdl docs API sidecar. It indexes 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.
“Latest” here therefore means most recently published, and
every row names that exact version; it is deliberately different from the
greatest-semantic-version selection used by an unversioned
cohdl add or cohdl update. Each existing package's
most-recently-published version becomes searchable after its owner re-runs
cohdl docs --publish once to backfill the index.
A valid query with no hits prints
No packages or parts matched `QUERY`. and exits 0. Invalid input
is an invocation error (exit 2, prose on stderr, nothing on stdout). A
registry or response-protocol failure is E1204 and exits 1: the human form
writes the diagnostic to stderr, while --json writes the existing
diagnostic JSON document to stdout.
Registry commands
Dependencies in CoHDL are pinned to exact X.Y.Z versions —
range syntax is rejected at parse time, permanently — and
cohdl.lock records each dependency's version and sha256 content
hash, re-verified on every check and build. The
dependency-management commands below are the sanctioned ways those pins
change; search and docs leave them untouched. This is a summary;
Packages & registry covers the model.
-
cohdl search QUERY [--json]— discover packages and the most-recently-published version's public parts, without a project or login. -
cohdl add NAME[@X.Y.Z] [PATH]— resolve the greatest published semantic version (or the exact one given), fetch it into the local cache, and write the[dependencies]entry plus its lock row in one step. The three-tier namespace is validated before any network call: a bare name is CoHDL official,@brand/nameis a verified manufacturer, and@contrib/nameis community. -
cohdl remove NAME [PATH]— delete the dependency entry and its lock row; the symmetric inverse ofadd. -
cohdl install [PATH]— resolve every pinned dependency percohdl.lock, fetching anything missing from the registry. A content-hash mismatch is a hard error, never a silent re-fetch. -
cohdl update [NAME] [PATH] [--dep NAME]— the only sanctioned pin change: re-resolve one dependency (named positionally or with--dep) or all of them to the greatest published semantic version, registry first with local packages as the fallback, rewriting[dependencies]andcohdl.locktogether. It also migrates a manifest that predates dependency pinning by writing its first[dependencies]section. -
cohdl login— opens the registry account page for you to create a token; paste it at the prompt and it is verified and stored. -
cohdl publish [PATH]— package the project and publish it. The namespace tier,[package] versionand[package] licenseare all checked locally before anything is uploaded. The server independently recomputes the content hash, and that server-side hash is what consumers' lock files verify against. -
cohdl docs [PATH] [--out FILE] [--publish]— emit the package's API-document JSON;--publishuploads it to an already-published version and is the idempotent backfill path for registry part search.
Two environment variables override registry defaults:
COHDL_REGISTRY replaces
registry.cohdl.org as the registry
endpoint, and COHDL_HOME replaces ~/.cohdl as the
location of the package cache and stored credentials.
Exit codes
Exit codes separate "the design has problems" from "the invocation has problems", and the two never blur:
| Code | Meaning |
|---|---|
0 |
Clean. Warnings are allowed on a clean exit, and a valid
search with no matches is still successful.
|
1 |
The accepted command failed. Compiler errors render as source
diagnostics; fmt --check uses 1 when a file is not
canonical; and search uses E1204 for an unreachable
registry or invalid response protocol. Human diagnostics render to
stderr; with --json, stdout carries exactly one JSON
document instead.
|
2 |
Invocation-level failure — the E000 class: bad flags, a flag invalid for the command, an invalid search query, a missing project, a design-selection failure, or nothing to build. Prose on stderr, never a JSON document. Source diagnostics collected before the failure still render to stderr first — they are never discarded. |
The stream split is deliberate: stdout belongs to machine-readable output
(a successful search --json result document or a diagnostic
document) and stderr to human-readable text, so a tool never has to parse
around progress messages.