Install the compiler
CoHDL is open source
(github.com/conol-ai/cohdl,
MIT), and every compiler release from v0.3.0 on ships prebuilt binaries
for macOS (Apple Silicon and Intel) and Linux (static builds for x86-64
and AArch64). One line fetches the newest release, verifies its sha256
against the release's published checksums, and installs to
~/.cohdl/bin:
curl -fsSL https://raw.githubusercontent.com/conol-ai/cohdl/main/install.sh | sh
Set COHDL_VERSION to pin an exact version — v0.3.0 or later;
the older v0.1.0/v0.2.0 tags belong to the previous implementation and use
a different artifact scheme — and COHDL_INSTALL_DIR to install
elsewhere. An installed binary
updates itself with cohdl self-update
(--check reports without installing). On Windows, download
cohdl-vX.Y.Z-x86_64-pc-windows-msvc.tar.gz from the
releases page by
hand. Building from source is plain cargo build. Everything
below — every command, every diagnostic, every artifact — is genuine
output from this toolchain.
A project is a directory
A project needs exactly two things: a cohdl.toml manifest and
CoHDL source under src/. The manifest names the package, picks
which design is the top-level board, and pins every dependency to an exact
version — CoHDL has no version ranges, deliberately, because a range is a
way for two builds of the same source to disagree.
[package]
name = "blinky"
version = "0.1.0"
license = "MIT"
[design]
top = "Blinky"
[dependencies]
connectors = "0.1.1"
led = "0.1.1"
passive = "0.2.1"
std = "0.3.0"
Dependencies are library packages: passive owns chip resistors
and capacitors, led owns LEDs, connectors owns
headers and sockets, and std carries only the universal traits
every component package builds on. The official packages are published on
the registry, and one command
fetches everything the manifest pins (paths below shortened to
~):
$ cohdl install .
fetched connectors 0.1.1 -> ~/.cohdl/registry/connectors/0.1.1
fetched led 0.1.1 -> ~/.cohdl/registry/led/0.1.1
fetched passive 0.2.1 -> ~/.cohdl/registry/passive/0.2.1
fetched std 0.3.0 -> ~/.cohdl/registry/std/0.3.0
installed 4 dependencies (4 fetched from the registry)
Forget this step and nothing guesses on your behalf: a check with unfetched
dependencies fails with E1102, naming the manifest line it
cannot resolve and telling you to run cohdl install. When a
dependency first resolves, the compiler writes cohdl.lock next
to the manifest, recording each dependency's exact version and a content
hash; from then on a dependency that changes out from under you is a hard
error, not a silent difference.
Packages & registry has the full story.
A first design
The smallest board worth compiling: an indicator LED and its series resistor, brought out on a six-pin socket. Three instances, three nets, one file.
// Blinky: a green LED and its series resistor on a 2x3 pin socket.
design Blinky {
inst hdr: connectors::headers::smd_254::SOCKET_2X3_254_SMD
inst led: led::LED_GREEN
inst r_led: passive::R_470R_F_0402
net V3V3 [3.3V]: hdr.P1, r_led.A
net LED_A: r_led.B, led.Anode
net GND: led.Cathode, hdr.P2
nc: hdr.P3, hdr.P4, hdr.P5, hdr.P6
}
Each inst declares an instance of a catalog part — a real,
orderable component that carries its manufacturer, part number and
footprint. Each net names a connection and lists the pins on
it as instance.PIN. The [3.3V] annotation types
the supply net: it is not a comment, and a part whose pins cannot take
3.3 V does not connect to it. Finally, nc declares the
socket pins this board deliberately leaves unconnected — in CoHDL an
unconnected pin is a decision you write down, never an accident.
This design checks clean:
$ cohdl check .
No errors found.
What the compiler refuses
The interesting part of a type system is what it will not accept. Two mistakes, introduced on purpose.
A bare number where a unit belongs
R_470R_F_0402 is a catalog part, but the generic device behind
it can be instantiated directly — ChipResistor takes its
resistance and tolerance as type parameters, and the
[R0402] bracket picks the case-size variant. Suppose you do
that and write the resistance the way every netlist tool lets you, as a
number:
inst r_led: passive::ChipResistor<470, 1%>[R0402]
error[E113]: a bare number is never valid for `R: Resistance` — write the unit (e.g. `470ohm`)
--> src/main.cohdl:6:39
|
6 | inst r_led: passive::ChipResistor<470, 1%>[R0402]
| ^^^
= help: RFC-001: no bare numbers, no default units, no coercion
1 error emitted
470 is not a resistance; 470ohm is. Units are
types, there are no default units, and nothing coerces — writing the unit
makes the line legal. The language covers
units and generics properly.
A required pin left floating
Every pin on every device carries a connection obligation. Delete
hdr.P6 from the nc line — the kind of edit that
survives every visual schematic review — and the design no longer compiles:
error[E701]: required pin `Blinky::hdr.P6` is unresolved: add it to a `net` or explicitly mark it `nc`
--> src/main.cohdl:4:5
|
4 | inst hdr: connectors::headers::smd_254::SOCKET_2X3_254_SMD
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:: connectors/headers/smd_254.cohdl:11:9
|
11 | required P6: 6 [passive]
| ------------------------ `P6` is declared `required` on device `Socket_2x3` here
1 error emitted
Both diagnostics have the same shape, and the shape is the point: a stable
code (E113, E701) from the
formal error-code registry, an exact span into
your source — plus, for E701, a second span into the library
declaration that created the obligation — and the construct named
precisely — the instance, the pin, the
type parameter. Never a bare "type mismatch". Tools get the same
information as one JSON document via --json
(CLI reference), and the exit code is honest:
0 for a clean check, 1 when diagnostics were
reported, 2 for an invocation-level failure such as a bad flag.
Building
cohdl build runs the full check and then emits everything
downstream of the schematic:
$ cohdl build .
Built design `Blinky`: 3 instances, 3 nets
wrote ./out/blinky.net
wrote ./out/blinky-bom.csv
wrote ./out/footprints/connectors-headers-smd_254-FP_Socket_2x3_254_SMD.kicad_mod
wrote ./out/footprints/led-FP_LED_0603_1608Metric_WURTH.kicad_mod
wrote ./out/footprints/passive-CHIP_0402.kicad_mod
wrote ./design.lock
-
out/blinky.net— the netlist, in the S-expression format KiCad's board editor imports directly. -
out/blinky-bom.csv— the bill of materials. Every line is an orderable part, because every catalog part is one. -
out/footprints/*.kicad_mod— one footprint per pad-bearing land pattern, emitted from the pad geometry declared in the library source, not copied from a footprint zoo. -
design.lock, in the project root — the designator assignments.R1staysR1across rebuilds and design changes, so revision-to-revision diffs of the outputs stay small and honest.
Manufacturer,Comment,Designator,Footprint
"Wurth Elektronik","150060GS75000","D1","FP_LED_0603_1608Metric_WURTH"
"Yageo","RC0402FR-07470RL","R1","CHIP_0402"
"Samtec","SSW-103-22-SM-D-VS","J1","FP_Socket_2x3_254_SMD"
When a design declares layout metadata — placements, rotations, physics
hints — the build also writes the layout-constraint document
blinky-layout.json, --emit ipc2581 adds an
IPC-2581 handoff file for tools that speak it, and
--emit kicad_pcb a ready-to-open KiCad board — no KiCad
installation involved.
Layout & fabrication covers both.
All of it is deterministic by construction: the same source and the same locked dependencies produce the same verdict, the same designators, and byte-identical output files. A build is a fact about the source, not an event that happened on someone's machine.
Keeping it canonical
The language has one canonical form, defined by the language rather than by
per-project configuration. cohdl fmt . rewrites source into
it, and cohdl fmt . --check changes nothing but fails —
printing would reformat ./src/main.cohdl and exiting
1 — when a rewrite would change bytes, which makes it a cheap
CI gate. Since generated outputs are byte-stable and source has one
spelling, every diff in a CoHDL repository is a real change.