The boundary
CoHDL emits everything downstream of the schematic, and stops there. It does not route, and it never invents a placement — the compiler carries the physical facts an author declares (land geometry, locked positions, routing constraints) into formats a layout tool consumes, and the layout tool does the placing and routing. In practice that tool has been Quilter for autorouting and KiCad for review and fabrication output.
A build writes, under out/:
-
the KiCad netlist (
<package>.net) and BOM (<package>-bom.csv) — the schematic-side outputs, covered on the CLI page; -
footprints/*.kicad_mod— one KiCad footprint file per land pattern the design uses, projected from thepadandfootprintdeclarations below; -
<package>-layout.json— the layout-constraint artifact: board outline, locked placements, net classes, differential pairs, placement hints; - eight physics-constraint CSV files, emitted when the design declares physics facts;
-
<package>.xml— an IPC-2581 handoff document, withbuild --emit ipc2581; -
<package>.kicad_pcb— a native KiCad 10 board file (placements, net-bound footprints, board outline), withbuild --emit kicad_pcb— written by the compiler itself, no KiCad installation required.
All of it is deterministic: same source and same locked dependencies produce the same bytes, so every one of these artifacts can sit in version control and diff meaningfully.
Pads
A pad is a standalone, reusable declaration — defined once, referenced by any
number of footprints. This is the Cadence-style split: fixing a land
dimension in one pad fixes it in every footprint that references
it. The vocabulary is closed. shape is one of rect,
circle, oval or annulus;
size is shape-dependent — (w, h) for rect and oval,
(d) for circle, (outer, inner) for annulus.
layer is top_copper, bottom_copper or
through_all, and plating is smd or
plated_through_hole.
// KiCad R_0402_1005Metric pad (0.54mm x 0.64mm).
pub pad P_Chip_0402 {
shape: rect
size: (0.54mm, 0.64mm)
layer: top_copper
plating: smd
}
Every dimension is a Length, written with the mm
unit. The language's zero-coercion rule applies to geometry exactly as it
does to volts: a bare number where a dimension is expected is a compile
error naming the expected unit. A through-hole pad must declare its
drill — and declaring one on an SMD pad is an error the other
way (code E805 covers the whole family of malformed pads):
pub pad P_MicroFit3_PTH {
shape: circle
size: (1.5mm)
layer: through_all
plating: plated_through_hole
drill: 1.02mm
}
A small set of bounded fabrication controls — a corner chamfer or corner radius on rectangular SMD pads, solder-mask expansion, reduced or segmented paste apertures — exists as provisional syntax for footprints that need them; omitting them preserves the plain geometry above.
Footprints
A footprint composes pad references into a land pattern. Each
pad N: Symbol at (x, y) line places one pad instance relative to
the footprint's origin; courtyard declares the keep-out
rectangle and silkscreen_ref positions the reference
designator's silkscreen text:
pub footprint FP_LED_0603_1608Metric {
pad 1: P_Chip_LED0603 at (-0.75mm, 0mm)
pad 2: P_Chip_LED0603 at (0.75mm, 0mm)
silkscreen {
polarity_marker cathode_pin 1 shape band
}
courtyard { shape: rect, at: (0mm, 0mm), size: (2.8mm, 1.4mm) }
silkscreen_ref { at: (0mm, -1.2mm) }
}
Pad numbers are checked against the bound device's declared pin numbers at
cohdl build — a footprint with a missing or extra number is an
error naming exactly which numbers disagree (E807). The same number may
appear in several placement lines: that models one electrical terminal
implemented by several physical features, such as an exposed pad's thermal
vias. An empty footprint body is also legal — a stage-one
placeholder that lets a part bind a named footprint before anyone has drawn
its geometry; the pad-consistency check applies once the body has content.
A pad placement may carry rotate ANGLE. Quad packages are the
motivating case — the same lead pad is placed on all four sides, rotated
rather than redefined:
pub footprint QFN56N40P700X700_1EP400X400 {
pad 1: P_QFN56N40P700X700_LEAD_CORNER at (-3.425mm, -2.6mm)
pad 2: P_QFN56N40P700X700_LEAD at (-3.425mm, -2.2mm)
// ...
pad 15: P_QFN56N40P700X700_LEAD at (-2.6mm, 3.425mm) rotate 90
pad 16: P_QFN56N40P700X700_LEAD at (-2.2mm, 3.425mm) rotate 90
// ...
}
The rotation is preserved as a fact: the emitted KiCad pad carries the declared size plus a real rotation angle, rather than the common library trick of silently swapping width and height. Pad rotation accepts any whole degree from 0 to 359 — the same range as component placement below (E811).
That QFN's name is not decoration. For the common package families — QFP,
QFN, SOIC/SOP, SOT, BGA, and CHIP/MELF passives — a footprint's identifier
must be its IPC-7351 land-pattern designator (with - mapped to
_), and for geometrically regular layouts the compiler
cross-checks it: the pin count and pitch the name encodes must agree with
what the pad placements actually draw, and a mismatch is a compile error
naming the disagreement. Footprints outside
those families — connectors, modules, the LED above — use ordinary
identifiers.
Silkscreen
The silkscreen { } block draws legend graphics from four closed
primitives — line, circle, arc and
polygon, all with Length-typed geometry — plus two
semantic markers. pin_1_marker near pad N shape dot|triangle
and polarity_marker cathode_pin N shape band|arrow say what the
mark means; the compiler expands each into ordinary checked
primitives, and both emitters consume that one expansion, so the KiCad and
IPC-2581 outputs cannot drift apart. The marker's standoff is measured from
the pad's edge, not its centre — otherwise the mark would sit on the copper
it is supposed to point at.
silkscreen {
pin_1_marker near pad 1 shape dot
line from (-9mm, -12.75mm) to (9mm, -12.75mm) width 0.15mm
line from (-9mm, -12.75mm) to (-9mm, -6.75mm) width 0.15mm
line from (9mm, -12.75mm) to (9mm, -6.75mm) width 0.15mm
line from (-9mm, -6.75mm) to (9mm, -6.75mm) width 0.15mm
}
A marker must name a pad the footprint actually declares, and a polarity
marker needs a second distinct terminal to orient toward — both checked at
declaration (E812). There is deliberately no auto-inference: the compiler
never guesses pin 1 or a cathode from pin roles; every mark is an explicit
statement. On the KiCad side each primitive becomes its native counterpart
(fp_line, fp_circle, fp_arc,
fp_poly) on the silk layer; the IPC-2581 emitter carries the
same graphics as contour polygons.
Mount holes
Some footprints need holes with no electrical function — a connector shell's
alignment pegs, a switch's mounting legs. mount_hole is the
construct for exactly that, with its own numbering that is fully disjoint
from pad numbering: a footprint can have pad 1..16 and
mount_hole 1..2 without collision, and mount-hole numbers are
never checked against the device's pins.
pub footprint FP_Molex_43045_0212 {
pad 1: P_MicroFit3_PTH at (0mm, 0mm)
pad 2: P_MicroFit3_PTH at (0mm, 3mm)
mount_hole 1: non_plated at (-3mm, 3.94mm) diameter 1.02mm
mount_hole 2: non_plated at (3mm, 3.94mm) diameter 1.02mm
// ...
}
Plating is non_plated (the common case) or plated —
there is no SMD value, because a mount hole is definitionally a hole, and no
layer field, because it always spans the board. The default shape is a
circle taking diameter D; rectangular and oval holes — a
keyswitch's locating legs, for instance — are written
mount_hole 1: non_plated shape: rect size: (2.0mm, 1.5mm) at (-6.75mm, 0mm).
Writing diameter with a rect shape, or size: with a
circle, is a compile error naming the mismatch (E810). In the KiCad output a
non-plated mount hole becomes KiCad's own np_thru_hole pad
type; a plated one becomes an ordinary through-hole pad with no net.
Placement
Placement lives in a design's layout { } block, alongside the
routing constraints of the next section. place locks one
top-level instance at a coordinate:
place NAME at (x, y) [rotate ANGLE] [side top|bottom]. Anything
you do not place stays the layout tool's problem — that split is the point.
Board-edge and mechanically-fixed parts get locked; parts whose best
position depends on routing are left to the autorouter. From the compiler's
Raspberry Pi Pico 2 recreation example:
layout {
net_class HighSpeed { USBC_DP, USBC_DM, USB_DP, USB_DM, USB_DPX, USB_DMX }
diff_pair(USBC_DP,
USBC_DM) [differential_impedance: 100ohm, single_ended_impedance: 50ohm, frequency: 1GHz]
diff_pair(USB_DPX,
USB_DMX) [differential_impedance: 100ohm, single_ended_impedance: 50ohm, frequency: 1GHz]
length_match(USB_DPX, USB_DMX) [tolerance: 0.15mm]
net_class Clock { XIN, XR }
board_outline: "mechanical/pico2-outline.dxf"
// Interface ports are pre-positioned (locked) at their board-edge
// locations, oriented; the layout partner places the rest around them.
place hdr at (0mm, 0mm)
place usb at (-23.64mm, 0mm) rotate 270
place esd at (-13mm, 0mm)
place swd at (22mm, 0mm) rotate 90
place mcu at (0mm, -2mm)
place xtal at (0mm, 5mm)
place c_xin at (-4mm, 5mm) rotate 90
place c_xout at (4mm, 5mm) rotate 270
}
board_outline references a mechanical DXF and extracts exactly
one closed polyline from its Edge.Cuts layer — straight
segments and arcs — at build time. The outline is a mechanical engineering
artifact, so CoHDL reads it rather than re-authoring it; a missing or
non-closed outline is a compile error naming the problem (E1006). CoHDL is
not a DXF viewer: nothing else in the file is read.
rotate accepts any whole degree from 0 to 359. A value of 360
or above is an error rather than being reduced — a full turn is 0, and
rotate 450 is more likely a slip than a deliberate 90. Because
emitted coordinates are byte-stability-critical, rotated geometry is
computed with a checked-in fixed-point sine table rather than the platform's
floating-point math library, whose last bit differs across platforms — a
rotated pad lands on the same bytes on every machine, and the table is exact
at the four cardinal angles.
side bottom puts a component on the back of the board — the
OpenMicroKbd macropad places its entire MCU that way, as
place mcu at (-40mm, 24mm) rotate 270 side bottom. The
footprint is authored exactly once, for its natural top-side orientation;
the emitters apply the mirroring (following KiCad's own flip-then-orient
convention), so the part lands correctly when the board is flipped and no
hand-maintained mirrored footprint ever exists. side defaults
to top and composes freely with rotate (E1008).
One honest limitation: place reaches only a design's own
top-level instances — a part instantiated inside a sub-circuit
fn cannot currently be locked, a disclosed gap rather than a
hidden one.
layout.json
Everything the layout { } block and the placement-adjacent
attributes declare is emitted as one JSON artifact,
<package>-layout.json — written only when the design
actually carries layout metadata, and removed by a rebuild that no longer
does. Its notable fields, from the Pico 2 example's real output:
{
"schema_version": 1,
"net_classes": [
{ "name": "HighSpeed", "nets": ["USBC_DP", "USBC_DM", "USB_DP", "USB_DM", "USB_DPX", "USB_DMX"] },
{ "name": "Clock", "nets": ["XIN", "XR"] }
],
"diff_pairs": [
{ "p": "USBC_DP", "n": "USBC_DM" },
{ "p": "USB_DPX", "n": "USB_DMX" }
],
"length_matches": [
{ "nets": ["USB_DPX", "USB_DMX"], "tolerance": "0.15mm" }
],
...
}
Beyond the constraint arrays it carries board_outline (the DXF
source path plus the extracted segment geometry), placements
(instance path, coordinates, rotation, and a "side" key present
only for back-side parts) and placement_hints. A hint is the
soft counterpart to place: the attribute
#[placement_hint("board edge, USB-C cutout")] on an instance
passes an opaque suggestion through to the layout tool without locking
anything. Net names in this artifact are the final netlist names —
identical to the .net file, so the two always agree. CoHDL
never verifies these constraints are physically met; it has no geometry to
check them against, and says so.
Physics hints for the autorouter
An autorouter can infer some electrical intent from a netlist, but the design source simply knows more. Seven structured attributes attach physics facts directly to the declaration each fact is about — three on nets:
#[ground(primary|secondary)]— ground nets; at most one primary per design;#[high_current(500mA)]— power nets and their current;#[impedance(50ohm, frequency: 1GHz)]— single-ended controlled impedance;
and four on instances:
#[bypass(mcu.VDD, 100nF)]— on the bypass capacitor itself, naming the pin it serves;#[crystal_oscillator(mcu, XIN, XOUT)]— on the crystal, naming the oscillator's pins;#[switching_converter(inductor: l1, input_capacitor: c_in, output_capacitor: c_out)]— on the converter, naming its power components (only the inductor is required);#[bga_fanout]— on a BGA that needs fanout generation.
#[high_current(500mA)]
net VBUS [5V]: usb.VBUS, esd.VBUS, d_vbus.Anode, r_vbus_top.A, hdr.VBUS
#[ground(primary)]
net GND [gnd]: mcu.GND, mcu.VREG_PGND, reg.GND, flash.GND, xtal.GND, usb.GND, usb.SHIELD,
esd.GND, rcc1.B, rcc2.B, swd.P2, boot.B, led.Cathode, hdr.GND, hdr.AGND,
// ...
The arguments are structurally checked, not opaque prose: pin and instance
references must resolve, and the numeric values are unit-typed, so
#[high_current(100nF)] is a compile error naming the expected
unit (structural mistakes are E1009; unit mismatches are E110). The
attributes also work inside reusable sub-circuits — a
#[bypass] written once inside a decouple function
produces one independently-resolved fact per call site. A differential
pair's physics ride the diff_pair(...) [...] bracket shown in
the placement excerpt above, since that fact belongs to a pair of nets
rather than one declaration.
When a design carries any of these facts, cohdl build emits an
eight-file CSV set matching Quilter's
physics-constraint templates exactly:
bga_components.csv, bypass_capacitors.csv,
crystal_oscillators.csv, differential_pairs.csv,
ground_nets.csv, high_current_nets.csv,
single_ended_impedance_signals.csv and
switching_converters.csv — designators and pad-number pins,
values in the templates' own scales:
capacitor,bypassed_component,bypassed_pin,capacitance
C9,U2,1,100
C9,U2,11,100
Nothing is auto-inferred: every constraint in these files is an attribute an author wrote, and omitting an attribute simply leaves that decision to the autorouter's own detection. The OpenMicroKbd use case shows the whole loop on a shipped board — attributes in source, CSVs out, Quilter placing and routing from them.
IPC-2581
cohdl build --emit ipc2581 writes
<package>.xml beside the netlist — an IPC-2581 revision
B1 document, the vendor-neutral handoff format, validated against the
consortium's published schema in the compiler's test suite. It carries the
logical netlist, every instance's designator, MPN and unit-typed spec
values, the layout constraints, and the real physical geometry a layout tool
needs: per-pin pad stacks with copper, mask and paste, the board profile
extracted from the DXF, and the silkscreen graphics. It is also honest
about what it is not: the document marks itself
logical-complete,placement-staged,unrouted. Instances you
placed are fixed inside the outline, where a layout tool treats them as
locked; everything else is staged in a deterministic grid just outside it —
the standard "please place me" signal — and no routing is claimed, because
none exists yet. Where the schematic ends, the layout tool begins.