Skip to main content

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 uses a built-in KiCad 9 default:
  • version = "9.0.3"
  • symbols = "gitlab.com/kicad/libraries/kicad-symbols"
  • footprints = "gitlab.com/kicad/libraries/kicad-footprints"
  • models.KICAD9_3DMODEL_DIR = "gitlab.com/kicad/libraries/kicad-packages3D"
  • 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"
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.
  • 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, 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():
  • 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", "Voltage", "Impedance")

# Basic nets
CLK = Net("CLK")
DATA = Net("DATA", impedance=Impedance(50))  # controlled impedance

# Power and ground (prelude — no load needed)
vcc = Power("VCC_3V3", voltage=Voltage("3.3V"))
gnd = Ground("GND")  # 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. 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)
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. 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
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 (usually omit — inferred from symbol)
typenoComponent type string
propertiesnoAdditional properties dict
dnpnoDo Not Populate flag
skip_bomnoExclude from BOM
When footprint is omitted, it is inferred from the symbol’s metadata. When part is provided, its mpn and manufacturer override any scalar mpn/manufacturer parameters or symbol metadata. If no part is set and no explicit/scalar mpn is set, Component() can inherit a default part from the package manifest’s parts entries matching the symbol path. manufacturer without mpn is treated as incomplete legacy scalar sourcing and does not prevent manifest fallback; prefer part = Part(...) as the simplest and most robust way to specify sourcing.

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=[])
ParameterRequiredDescription
mpnyesManufacturer part number (non-empty string)
manufactureryesManufacturer name (non-empty string)
qualificationsnoList of qualification strings (e.g. ["AEC-Q200"])
Attributes: .mpn, .manufacturer, .qualifications 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"],
    ),
    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) Operators: +, -, *, / (with unit tracking), <, >, <=, >=, == (compare nominal), unary - 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. Signature: io(name, typ, checks=None, default=None, optional=False, help=None)
  • name: Input name (conventionally UPPERCASE).
  • typ: A net type (Net, Power, Ground, etc.) or an interface factory (Spi, Uart, etc.).
  • checks: Optional check function or list of checks applied to the resolved value.
  • default: Explicit default 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.
VCC = io("VCC", Power)
GND = io("GND", Ground)
SPI = io("SPI", Spi, optional=True)
DATA = io("DATA", 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, optional=None, help=None)
  • name: Input name (conventionally lowercase).
  • 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.
  • optional: Explicit override. When True with no default, returns None.
  • help: Help text.
value = config("value", Resistance)
package = config("package", Package, default=Package("0603"))
voltage = config("voltage", Voltage, optional=True)
Values passed by the parent are automatically converted to the declared type when possible — strings become physical quantities ("10k"Resistance("10k")), enum variants ("0603"Package("0603")), etc. This is why Resistor(name="R1", value="10k", package="0603", ...) works even though value expects Resistance and package expects Package.

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("color", str, default="red")
r_value = config("r_value", str, default="330ohms")

# Electrical interface
VCC = io("VCC", Power)
GND = io("GND", 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
load("@stdlib/units.zen", "Voltage")
LedIndicator = Module("./modules/LedIndicator.zen")

vcc = Power("VCC_3V3", voltage=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.

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 io() boundaries. For example, voltage_within(range) validates that a Power net’s voltage falls within a specified range:
load("@stdlib/checks.zen", "voltage_within")
VCC = io("VCC", Power, checks=voltage_within("1.1–3.6V"))

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