CoHDL
  1. Docs
  2. Getting started

Getting started

A CoHDL project is a directory with a manifest and some source files. This guide walks the whole loop once: write a small design, let the compiler check it, break it twice on purpose to see what a diagnostic looks like, and build the files a fab flow actually consumes.

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.

cohdl.toml complete file
[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.

src/main.cohdl complete 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:

src/main.cohdl the mistake
    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. R1 stays R1 across rebuilds and design changes, so revision-to-revision diffs of the outputs stay small and honest.
out/blinky-bom.csv complete file
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.