Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pcb.new/llms.txt

Use this file to discover all available pages before exploring further.

Specification

Overview

Zener is a domain-specific language built on top of Starlark for describing PCB schematics. It provides primitives for defining components, symbols, nets, interfaces, and modules in a type-safe, composable manner. This specification describes the language extensions and primitives added on top of Starlark. For the base Starlark language features, please refer to the Starlark specification and the starlark-rust types extension.

Table of Contents

  1. Modules and Imports
  2. Nets and Interfaces
  3. Components and Symbols
  4. Modules
  5. Utilities
  6. Schematic Position Comments

Modules and Imports

Each .zen file is a Starlark module that can be used in two ways:
  1. Symbol imports with load() bring functions and types into scope:
    load("./utils.zen", "helper")
    load("@stdlib/units.zen", "Voltage", "Resistance")
    
  2. Schematic modules with Module() create instantiable subcircuits:
    Resistor = Module("@stdlib/generics/Resistor.zen")
    Resistor(name="R1", value="10k", P1=vcc, P2=gnd)
    

Import Paths

Import paths support local files, stdlib, and remote packages:
# Local file (relative to current file)
load("./utils.zen", "helper")

# Stdlib (version controlled by toolchain)
load("@stdlib/units.zen", "Voltage", "Resistance")
load("@stdlib/interfaces.zen", "Spi")

# Remote packages (version declared in pcb.toml)
Resistor = Module("@stdlib/generics/Resistor.zen")
TPS54331 = Module("github.com/diodeinc/registry/reference/TPS54331/TPS54331.zen")
The @stdlib alias is special and virtual—its content is controlled by the toolchain. You don’t need to declare it in [dependencies]. Remote package URLs don’t include version information. Versions are declared separately in pcb.toml, so import statements remain stable across upgrades:
[dependencies]
"github.com/diodeinc/registry/reference/TPS54331" = "1.0"

Dependency Resolution

Dependencies are automatically resolved when you import remote packages. The toolchain discovers dependencies from import paths, resolves versions, downloads packages, and updates pcb.toml. @stdlib is toolchain-managed and implicit. KiCad symbol/footprint/model linkage is configured by workspace-level [[workspace.kicad_library]] entries. Dependencies are still declared in [dependencies], and auto-deps can add them from imports. If [[workspace.kicad_library]] is omitted, the toolchain defaults the workspace to two entries:
  • KiCad 9: version = "9.0.3" and models.KICAD9_3DMODEL_DIR = "gitlab.com/kicad/libraries/kicad-packages3D"
  • KiCad 10: version = "10.0.0" and models.KICAD10_3DMODEL_DIR = "gitlab.com/kicad/libraries/kicad-packages3D"
Both default entries use:
  • symbols = "gitlab.com/kicad/libraries/kicad-symbols"
  • footprints = "gitlab.com/kicad/libraries/kicad-footprints"
  • parts = "https://kicad-mirror.api.diode.computer/kicad-parts-{version}.toml"
  • http_mirror = "https://kicad-mirror.api.diode.computer/{repo_name}-{version}.tar.zst"
parts and http_mirror are optional and support template placeholders:
  • {repo}
  • {repo_name}
  • {version}
  • {major}
Version resolution uses Minimal Version Selection (MVS), which selects the minimum version satisfying all constraints rather than the newest. This ensures deterministic builds—the same code always resolves to the same versions regardless of what exists upstream. The lockfile (pcb.sum) records exact versions and cryptographic hashes. Commit it to version control for reproducible builds across machines. See Packages for complete details on version resolution and dependency commands.

Project Structure

A Zener project is a git repository containing a workspace with boards, modules, and components. Create a new project with pcb new workspace:
my-project/
├── pcb.toml              # Workspace manifest
├── pcb.sum               # Dependency lock file
├── boards/
│   └── MainBoard/
│       ├── pcb.toml      # Board manifest
│       └── MainBoard.zen
├── modules/
│   └── PowerSupply/
│       ├── pcb.toml      # Package manifest
│       └── PowerSupply.zen
└── components/
    └── TPS54331/
        ├── pcb.toml
        └── TPS54331.zen
Use pcb new board <name> to add boards and pcb new package <path> to add modules or components. Workspace manifest (root pcb.toml):
[workspace]
repository = "github.com/myorg/my-project"
pcb-version = "0.3"
endpoint = "diode.computer"
members = ["boards/*", "modules/*", "components/*"]
  • repository: Git remote URL (used to derive package URLs for publishing)
  • pcb-version: Minimum compatible pcb toolchain release series (e.g., "0.3"). Required for workspaces.
  • endpoint: Optional Diode host suffix used for workspace-scoped app/API URLs. "diode.computer" resolves to app.diode.computer and api.diode.computer.
  • members: Glob patterns matching subdirectories that contain packages
V1 workspaces are no longer supported. If you have an older project, run pcb migrate to upgrade manifests and import paths. Only the workspace root pcb.toml should contain a [workspace] section. Board manifest (e.g., boards/MainBoard/pcb.toml):
[board]
name = "WV0001"
path = "MainBoard.zen"

[dependencies]
"github.com/diodeinc/registry/reference/TPS54331" = "1.0"
  • [board]: Defines a buildable board with name and entry path
  • [dependencies]: Version constraints for this board’s dependencies
Package manifest (e.g., modules/PowerSupply/pcb.toml):
[dependencies]
"github.com/diodeinc/registry/reference/TPS54331" = "1.0"

parts = [
  { mpn = "TPS54331DR", symbol = "TPS54331.kicad_sym", symbol_name = "TPS54331", manufacturer = "Texas Instruments", qualifications = ["Preferred"] },
]
Packages (modules, components) don’t have a [board] section—they’re libraries meant to be instantiated by boards or other modules.
  • [dependencies]: Version constraints for packages imported by this package
  • parts: Optional default sourcing metadata keyed by symbol file. Each entry provides mpn, manufacturer, optional qualifications, optional datasheet, a package-relative .kicad_sym symbol path, and optional symbol_name to target a specific symbol within a multi-symbol library file. If symbol_name is omitted, the referenced .kicad_sym file must contain exactly one symbol; otherwise resolution fails and symbol_name is required. Component() sourcing precedence is described below.
See Packages for the complete manifest reference.

Prelude

These stdlib symbols are available in every user .zen file without load():
  • io, input, output — from @stdlib/io.zen
  • Net, Power, Ground, NotConnected — from @stdlib/interfaces.zen
  • Board — from @stdlib/board_config.zen
  • Layout, Part — from @stdlib/properties.zen
Local definitions shadow prelude symbols. The prelude does not apply to stdlib modules themselves.

Nets and Interfaces

Nets

A Net represents an electrical connection between component pins. Net is the base net type; specialized types like Power, Ground, and NotConnected add metadata (schematic symbols, voltage) while remaining fundamentally nets.
load("@stdlib/units.zen", "Impedance")

# Basic nets
CLK = Net()
DATA = Net(impedance=Impedance(50))  # controlled impedance
VREF = Net()  # inferred from assignment

# Power and ground (prelude — no load needed)
VCC = Power("VCC_3V3", voltage="3.3V")
GND = Ground()  # inferred from assignment; voltage defaults to 0V

# Intentionally unconnected
nc = NotConnected()
Net(name, voltage=None, impedance=None) — all parameters optional. Power and Ground additionally accept a voltage parameter. Additional net types (Analog, Pwm, Gpio) are available from @stdlib/interfaces.zen. If a net constructor omits name, the assigned variable name is used when available:
CLK = Net()       # equivalent to Net("CLK")
VDD = Power()     # equivalent to Power("VDD")
If you do spell out the same name that assignment inference would choose, Zener raises a style advice because the explicit name is redundant. Explicit constructor names still win, and unassigned constructor calls keep the existing generated-name behavior:
alias = Net("CLK")  # name is "CLK", not "alias"
Net()               # uses an auto-generated name
Typed net fields validate against their declared field type and use the same string coercions as module inputs, so Power("VCC", voltage="3.3V") is equivalent to Power("VCC", voltage=Voltage("3.3V")). Net types follow automatic conversion rules across io() boundaries: NotConnected promotes to any type (universal donor), any specialized type demotes to Net, but Net cannot automatically promote to specialized types. For explicit casting, call the target constructor with an existing net: Power(net, voltage=Voltage("3.3V")) or Net(power_net).

Interfaces

Interfaces define reusable connection patterns — groups of related nets. Define custom interfaces with interface():
MyBus = interface(
    clk = Net("CLK"),
    data = Net("DATA"),
    enable = field(bool, True),
)

bus = MyBus("BUS1", enable=False)
debug_bus = MyBus()
Interface fields can be net instances, interface instances (for hierarchical composition), or field() specs. When instantiated, the first positional argument is an optional name; named arguments override defaults. If an interface instance omits its explicit name, the assigned variable name becomes the root used for generated child nets:
PowerIf = interface(
    vcc = Net(),
    gnd = Net("GND"),
)

power = PowerIf()
# generated child nets become power_vcc and power_GND
Only values generated by the interface definition are renamed this way. Caller-provided nets or interface instances keep their existing names:
ext = Net("EXT")
power = PowerIf(vcc = ext)
# ext stays "EXT"
The standard library provides common interfaces (Spi, I2c, Uart, Usb2, DiffPair, Pcie, Jtag, Swd, etc.) in @stdlib/interfaces.zen. Helper functions UartPair(a, b) and UsartPair(a, b) create cross-connected pairs for point-to-point links.

Components and Symbols

Component

Components represent physical electronic parts with pins, a schematic symbol, and a PCB footprint.
Component(
    name = "U1",
    symbol = my_symbol,
    pins = {
        "VCC": vcc,
        "GND": gnd,
        "OUT": output_net,
    },
    prefix = "U",
    part = Part(mpn="LM358", manufacturer="TI"),
)
Constructor: Component(**kwargs)
ParameterRequiredDescription
nameyesInstance name
symbolyesSymbol object defining the schematic representation
pinsyesDict mapping pin names to nets; omit KiCad no_connect pins
partnoPart object specifying manufacturer sourcing (preferred)
prefixnoReference designator prefix (default: "U")
manufacturernoManufacturer name (legacy — prefer part)
mpnnoManufacturer part number (legacy — prefer part)
footprintnoPCB footprint path (default: inferred from symbol Footprint property)
typenoComponent type string
propertiesnoAdditional properties dict
spice_modelnoExplicit SpiceModel; default: inferred from symbol Sim.* properties when present
dnpnoDo Not Populate flag
skip_bomnoExclude from BOM (default: inverse of symbol in_bom flag)
datasheetnoDatasheet URL or path (default: part.datasheet, then this component value, then symbol Datasheet property; local component paths resolved relative to the .zen file, symbol-local paths resolved relative to the .kicad_sym file)
When KiCad symbol pin metadata is available:
  • omitted no_connect pins are auto-wired to NotConnected()
  • explicit no_connect entries warn
  • power_in and power_out pins warn if connected to plain Net instead of Power or Ground
  • if spice_model is omitted and the symbol provides Sim.Library, Sim.Name, Sim.Device=SUBCKT, Sim.Pins, and optional Sim.Params, Component() derives the SPICE model from those symbol properties

Part

Part specifies manufacturer sourcing for a component. It is a prelude symbol — available in all .zen files without load(). Constructor: Part(mpn, manufacturer, qualifications=[], datasheet=None)
ParameterRequiredDescription
mpnyesManufacturer part number (non-empty string)
manufactureryesManufacturer name (non-empty string)
qualificationsnoList of qualification strings (e.g. ["AEC-Q200"])
datasheetnoDatasheet URL or path for this manufacturer part
Attributes: .mpn, .manufacturer, .qualifications, .datasheet Use Part with the part parameter on Component() for primary sourcing, and in properties["alternatives"] for alternate parts:
Component(
    name = "R1",
    symbol = Symbol(library="@kicad-symbols/Device.kicad_sym", name="R"),
    pins = {"P1": vcc, "P2": gnd},
    part = Part(
        mpn = "RC0603FR-0710KL",
        manufacturer = "Yageo",
        qualifications = ["AEC-Q200"],
        datasheet = "https://www.yageo.com/upload/media/product/products/datasheet/rchip/RC_L_51_RoHS_L_6.pdf",
    ),
    properties = {
        "alternatives": [
            Part(mpn="ERJ-3EKF1001V", manufacturer="Panasonic"),
        ],
    },
)
During pcb build, reference designators are automatically allocated per-prefix (e.g. R1, R2, C1).

Symbol

A Symbol represents a schematic symbol loaded from a KiCad symbol library.
ic_symbol = Symbol(library="TCA9554DBR.kicad_sym")
connector = Symbol(library="@kicad-symbols/Connector_Generic.kicad_sym", name="Conn_01x14")
Constructor: Symbol(library, name=None)
  • library: Path to a .kicad_sym file. Supports local paths, @kicad-symbols/ alias, and package paths.
  • name: Symbol name within the library. Required for multi-symbol libraries; omit for single-symbol files.

Physical Quantities

Physical quantities represent electrical values with a nominal value, optional min/max bounds, and a unit. Unit-specific constructors are provided by @stdlib/units.zen (e.g. Voltage, Current, Resistance, Capacitance, Inductance, Impedance, Frequency, Temperature):
load("@stdlib/units.zen", "Voltage", "Current", "Resistance", "Capacitance")

# Point values
supply = Voltage("3.3V")
resistor = Resistance("4k7")     # 4.7kΩ using resistor notation
cap = Capacitance("100nF")

# Ranges
input_range = Voltage("1.1–3.6V")
explicit_nominal = Voltage("11–26V (12V)")

# Keyword bounds
operating = Voltage(min=11, max=26)

# Arithmetic with automatic unit tracking
power = Voltage("3.3V") * Current("0.5A")   # → 1.65W
r = Voltage("5V") / Current("100mA")        # → 50Ω
See @stdlib/units.zen for the complete list of unit constructors. Properties: .value (alias for .nominal), .nominal, .min, .max, .tolerance, .unit Methods: .with_tolerance(t), .with_value(v), .with_unit(u), .abs(), .diff(other), .within(other), .matches(other) Operators: +, -, *, / (with unit tracking), <, >, <=, >=, == (strict equality against another PhysicalValue), unary - Use .matches(other) when you want coercive comparisons against strings or scalars such as Voltage("5V").matches("5V"). String formatting: Point values → "3.3V". Symmetric tolerances → "10k 5%". Ranges → "11–26V (16V nom.)".

Generic Components

Prefer generic components over raw Component() where possible. Generics come with standard symbols, footprints, and automatic BOM matching to house parts.
Resistor = Module("@stdlib/generics/Resistor.zen")
Capacitor = Module("@stdlib/generics/Capacitor.zen")

Resistor(name="R1", value="10k", package="0603", P1=vcc, P2=gnd)
Capacitor(name="C1", value="100nF", package="0402", voltage="16V", P1=vcc, P2=gnd)
See @stdlib/generics/ for the full list of available generics and their accepted parameters.

Modules

Modules are reusable subcircuits — .zen files that declare their electrical interface and configuration, then build a circuit from them. They are the primary mechanism for hierarchical design.

Module()

Module() loads a .zen file and returns a callable that instantiates it as a subcircuit:
PowerSupply = Module("./modules/PowerSupply.zen")
TPS54331 = Module("github.com/diodeinc/registry/reference/TPS54331/TPS54331.zen")
Instantiation takes a required name and passes remaining arguments as inputs to the module’s io() and config() declarations:
PowerSupply(name="PSU1", VIN=vin, VOUT=vout, GND=gnd, output_voltage="3.3V")
Additional instantiation parameters:
  • properties: Dict of property overrides for the module instance.
  • dnp: Bool — mark as Do Not Populate.
  • schematic: "collapse" or "embed" — controls schematic rendering of the subcircuit.

io()

Declare a net or interface input for a module. This defines the module’s electrical interface — the nets that a parent must (or may) connect when instantiating it. io, input, and output are prelude symbols re-exported from @stdlib/io.zen. The low-level builtin is builtin.io(...). Signature: io(name, typ_or_template, checks=None, optional=False, help=None, direction=None) or io(typ_or_template, checks=None, optional=False, help=None, direction=None)
  • name: Optional explicit input name (conventionally UPPERCASE). If omitted, io() must be assigned to a top-level variable and that variable name is used.
  • typ_or_template: A net type (Net, Power, Ground, etc.), an interface factory (Spi, Uart, etc.), a net template value, or an interface template value.
  • checks: Optional check function or list of checks applied to the resolved value.
  • optional: If True, a generated net/interface is used when the parent doesn’t provide one. Default False.
  • help: Help text for documentation and signatures.
  • direction: Optional signature metadata. Must be "input" or "output" when provided.
VCC = io(Power)
GND = io(Ground)
VDD_3V3 = io(Power(voltage="3.3V"))
SPI_BUS = io(Spi("SPI"), optional=True)
SPI = io(Spi, optional=True)
DATA = io(Net)
CS = io(Net)
VIN = io(Power, direction="input")
VOUT = io(Power, direction="output")
When typ_or_template is a template value, io() derives all three of these from it:
  • the placeholder type
  • the default/template metadata
  • implicit checks that constrain any provided input
For example, io(Power(voltage="1.8V - 3.6V")) requires any supplied Power net to stay within that voltage range. Use direction for genuinely one-way ports like signal or power flow. Shared rails such as GND should usually remain plain io() declarations. When the explicit name matches the assigned variable name, omitting it is the preferred style and emits no redundancy advice. input(name, typ_or_template, ...) and output(name, typ_or_template, ...) are equivalent to io(...) with direction="input" or direction="output" respectively, and they also support omitted explicit names when assigned to top-level variables.
VIN = input(Power)
VDD = input(Power(voltage="3.3V"))
VOUT = output(Power)
CS = input(Net)

config()

Declare a typed configuration input for a module. This defines parameters that control the module’s behavior — values (not nets) provided by the parent. Signature: config(name, typ, checks=None, default=None, allowed=None, optional=None, help=None) or config(typ, checks=None, default=None, allowed=None, optional=None, help=None)
  • name: Optional explicit input name (conventionally lowercase). If omitted, config() must be assigned to a top-level variable and that variable name is used.
  • typ: Expected type — primitives (str, int, float, bool), enum, record, or physical quantity constructors.
  • checks: Optional check function or list of checks.
  • default: Default value. When provided, optional defaults to True.
  • allowed: Optional finite set of allowed values. Accepts a list, tuple, or dict (using only the keys). Supported for str, int, float, bool, enum, and physical quantity types.
  • optional: Explicit override. When True with no default, returns None.
  • help: Help text.
value = config(Resistance)
package = config(Package, default=Package("0603"))
voltage = config(Voltage, optional=True)
manufacturer = config(str, default="Acme")
output_voltage = config(
    "output_voltage",
    Voltage,
    allowed=["0.8V", "0.9V", "1.0V", "1.1V"],
    default="1.0V",
)
Values passed by the parent are automatically converted to the declared type when possible. String inputs can coerce to primitives ("true"True, "42"42, "3.3"3.3), physical quantities ("10k"Resistance("10k")), and enum variants ("0603"Package("0603")). This is why Resistor(name="R1", value="10k", package="0603", ...) works even though value expects Resistance and package expects Package. When allowed is present, both the allowed set and the provided value are normalized through that same coercion path before membership is checked, and physical values are surfaced using their canonical formatting. As with io(), repeating the assigned variable name as an explicit config() name is redundant and triggers a style advice.

Writing a Module

A module is a .zen file that declares its interface with io() and config(), then uses those values to build its circuit:
# modules/LedIndicator.zen
Resistor = Module("@stdlib/generics/Resistor.zen")
Led = Module("@stdlib/generics/Led.zen")

# Configuration
color = config(str, default="red")
r_value = config(str, default="330ohms")

# Electrical interface
VCC = io(Power)
GND = io(Ground)

# Internal net
led_anode = Net("LED_ANODE")

# Circuit
Resistor(name="R1", value=r_value, package="0603", P1=VCC, P2=led_anode)
Led(name="D1", color=color, package="0603", A=led_anode, K=GND)

# Layout
Layout(name="LedIndicator", path="layout/LedIndicator")
Instantiated by a parent board:
# boards/MainBoard/MainBoard.zen
LedIndicator = Module("./modules/LedIndicator.zen")

vcc = Power("VCC_3V3", voltage="3.3V")
gnd = Ground("GND")

LedIndicator(name="LED1", VCC=vcc, GND=gnd, color="green")
LedIndicator(name="LED2", VCC=vcc, GND=gnd, color="red")

Board(name="MainBoard", layers=4, layout_path="layout/MainBoard")

Utilities

Board and Layout

Board() configures PCB manufacturing parameters — stackup, design rules, and layout path. It is a prelude symbol.
Board(name="my_board", layers=4, layout_path="layout/my_board")
Key parameters: name, layout_path, layers (2/4/6/8/10), config (explicit BoardConfig), outer_copper_weight ("1oz" or "2oz"), copper_finish (default "ENIG"). When layers is provided, Board() selects an appropriate default stackup, netclasses, and design rules. An explicit config is merged on top. See @stdlib/board_config.zen for BoardConfig, Stackup, DesignRules, NetClass, and preset stackups. Layout() defines reusable layout blocks for modules. When writing a module, use Layout(name, path) to associate a PCB layout with the subcircuit. See @stdlib/properties.zen. Simulation(name, setup=None, modifiers=None, bom_profile=...) — Attach inline simulation setup and component modifiers to the current module. Simulation() uses the same BOM-profile hook as Layout(): by default it registers the standard house-part matcher, modifiers run before bom_profile, and bom_profile=None disables automatic house matching for simulation-only evals.

File and Path

File(path) — Resolve a path relative to the current .zen file. Raises an error if it doesn’t exist.
datasheet = File("TPS54331.pdf")
footprint = File("@kicad-footprints/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod")
Path(path, allow_not_exist=False) — Like File() but supports package paths and optional non-existence.
layout_dir = Path("layout/my_board", allow_not_exist=True)

Assertions

Three global functions for validation and diagnostics:
  • check(condition, message) — Assert a condition. Raises an error with message if condition is false.
  • error(message) — Raise an error unconditionally.
  • warn(message) — Emit a warning diagnostic.
check(voltage <= Voltage("3.6V"), "Voltage exceeds maximum rating")
warn("Using deprecated parameter")

Electrical Checks

@stdlib/checks.zen provides reusable check functions for typed inputs. For example, voltage_within(range) validates that a net with voltage metadata, or a direct Voltage value, falls within a specified range:
load("@stdlib/checks.zen", "voltage_within")
vref = config(Voltage, checks=voltage_within("1.1–3.6V"))
Template-first io() can also contribute implicit checks. A typed net template with a meaningful voltage property enforces the same containment rule automatically:
VCC = io(Power(voltage="1.1–3.6V"))
Explicit checks= still run as well, after type validation and any template-derived implicit checks.

E-Series

@stdlib/utils.zen provides functions to snap values to standard resistor/capacitor E-series: e3(), e6(), e12(), e24(), e48(), e96(), e192().
load("@stdlib/utils.zen", "e96")
r = e96(Resistance("4.8k"))  # → 4.87kΩ (nearest E96 value)

Schematic Position Comments

Zener supports persisted schematic placement metadata in trailing comment blocks. These comments are consumed by tooling and surfaced in netlist output. Do not edit these comments directly. Canonical line format:
# pcb:sch <id> x=<f64> y=<f64> rot=<f64> [mirror=<x|y>]
  • id: Position key (component or net symbol key in comment form, e.g. R1, VCC.1)
  • x, y: Schematic coordinates
  • rot: Rotation in degrees
  • mirror (optional): Mirror axis (x or y)
Examples:
# pcb:sch R1 x=100.0000 y=200.0000 rot=0
# pcb:sch U1 x=150.0000 y=200.0000 rot=90 mirror=x