Skip to main content

Specification

Overview

Zener is a domain-specific language built on Starlark for describing PCB schematics. It adds components, symbols, nets, interfaces, physical quantities, and hierarchical circuit modules. This specification defines the Zener extensions. The Starlark specification and the starlark-rust type extensions define the underlying language.

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:
  2. Schematic modules with Module() create instantiable subcircuits:

Import paths

Import paths support local files, stdlib, and remote packages:
The toolchain supplies the virtual @stdlib package. Do not declare it in [dependencies]. Remote package URLs omit versions. Declare each version in pcb.toml so import statements remain stable across upgrades:

Dependency resolution

Remote package URLs do not include versions. Declare versions in pcb.toml, then run pcb sync to reconcile imports and hydrate dependency manifests. @stdlib is toolchain-managed and implicit. See Packages for version resolution, package manifests, and dependency commands.

Repository layouts

Zener projects use one of two repository shapes: a board repository or a registry repository.

Board repository

A board repository contains one board plus any local modules and components it owns. Create one with pcb new board <name> <repo-url>:
Use pcb new package <path> to add modules or components inside the board repository. Manifest (root pcb.toml):
  • name: Optional Diode workspace name override for board release uploads. When omitted, publish uses the first path segment of repository, such as myorg from github.com/myorg/MainBoard.
  • repository: Git remote URL (used to derive package URLs for publishing)
  • pcb-version: Required pcbc toolchain lane, such as "0.3", "0.4", or "nightly". For semver lanes, pcb selects the newest installed patch in that lane and installs one if needed. Ordinary commands never auto-upgrade across lanes; use pcb migrate to run the latest stable toolchain migrations and update this field after they succeed.
  • endpoint: Optional Diode host suffix used for workspace-scoped app/API URLs. "diode.computer" resolves to app.diode.computer and api.diode.computer.
  • [board]: Defines the repository’s buildable board with name, entry path, and a short description
Older workspace formats are no longer supported. Update legacy manifests and imports before building with the current toolchain. Only the workspace root pcb.toml may contain a [workspace] section. Board dependencies are also declared in the root manifest’s [dependencies] table.

Registry repository

A registry repository contains reusable packages but no board at the root. It uses top-level components/ and modules/ directories:
Registry manifests include [workspace] but do not include [board]:
Package manifest (e.g., modules/PowerSupply/pcb.toml):
Module and component packages omit [board] because boards and other modules instantiate them as libraries.
  • [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, and the NotConnected() open-net constructor — 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 and Ground add metadata (schematic symbols, voltage) while remaining fundamentally nets. NotConnected is a constructor for an intentionally open net, not a net type.
Net(name_or_net=None, voltage=None, impedance=None) accepts a positional-only name string or existing net to cast. Power and Ground also accept voltage. Additional connected net types (Analog, Pwm, Gpio) are available from @stdlib/interfaces.zen. NotConnected is reserved for the open-net constructor; builtin.net_type("NotConnected") is invalid. If a net constructor omits name, the assigned variable name is used when available:
If the explicit name duplicates the inferred assignment name, Zener reports style advice because the explicit name is redundant. Explicit constructor names still win. Regular nets must either have a name or be assigned where a name can be inferred:
Regular net names must be unique. Duplicate regular net names are rejected; open NotConnected() nets are exempt. NotConnected() has no source-level name. A supplied name is ignored with a warning. Downstream tools assign connection-derived names where required. 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 type annotations on io() boundaries check electrical compatibility; they do not change whether a provided value is connected or intentionally open. NotConnected() can satisfy any net-shaped io() because it is an open net value, and it remains NotConnected in the emitted netlist. Specialized connected types can be viewed as Net; plain connected Net values do not automatically promote to specialized types. Explicit constructors assert the resulting connected net type. For example, Net(NotConnected()) constructs a regular connected Net, while passing NotConnected() directly to io(Net) keeps it open. NotConnected itself is not valid where a type is expected, such as io(NotConnected).

Interfaces

Interfaces define reusable connection patterns — groups of related nets. Define custom interfaces with interface():
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:
Only values generated by the interface definition are renamed this way. Caller-provided nets or interface instances keep their existing names:
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.
Constructor: Component(**kwargs) 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) Attributes: .mpn, .manufacturer, .qualifications, .datasheet Use Part with the part parameter on Component() for primary sourcing, and in properties["alternatives"] for alternate parts:
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.
Constructor: Symbol(library, name=None)
  • library: Path to a .kicad_sym file. Supports local paths and package paths.
  • name: Symbol name within the library. Required for multi-symbol libraries; omit for single-symbol files.

Physical quantities

Physical quantities have a nominal value, optional min/max bounds, and dimensional units. Zener exposes five SI bases as builtin.Mass, builtin.Length, builtin.Time, builtin.Current, and builtin.Temperature. @stdlib/units.zen re-exports them and derives conventional electrical types such as Voltage, Resistance, Capacitance, Inductance, Impedance, and Frequency using multiplication and division. For example, Voltage is Mass * Length * Length / (Current * Time * Time * Time).
Derived types are dimensional rather than nominal: independently constructed types with the same dimensions accept the same values. For example, Voltage / Current and stdlib Resistance are the same physical type. SI base symbols are resolved in the context of the declared type, so Length("1m") is one metre. Untyped "1m" parsing retains its historical meaning of one milliohm for compatibility. 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), .spice() Operators: +, -, *, / (with unit tracking), <, >, <=, >=, == (strict equality against another PhysicalValue), unary - Use .matches(other) for 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.)". SPICE formatting: Pass a PhysicalValue directly or use .spice() to emit an ngspice-safe string (meg for mega, no unit suffix, e.g. Resistance("2MOhm")2meg).

Generic components

Prefer generic components over raw Component() where possible. Generics come with standard symbols, footprints, and automatic BOM matching to house parts.
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:
Instantiation takes a required name and passes remaining arguments as inputs to the module’s io() and config() declarations:
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, use a generated net or interface when the parent omits the input. The default is False.
  • help: Help text for documentation and signatures.
  • direction: Optional signature metadata. Must be "input" or "output" when provided.
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 only for one-way signal or power flow. Leave shared rails such as GND as 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.

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, or physical quantity constructors. record() types are not supported as module config() inputs.
  • 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.
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.

Write a module

A module is a .zen file that declares its interface with io() and config(), then uses those values to build its circuit:
Instantiated by a parent board:

Utilities

Board and layout

Board() configures PCB manufacturing parameters — stackup, design rules, and layout path. It is a prelude symbol.
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 an existing path relative to the current .zen file. The call fails if the path does not exist.
Path(path, allow_not_exist=False) — Like File() but supports package paths and optional non-existence.

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.

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:
Template-first io() can also contribute implicit checks. A typed net template with a meaningful voltage property enforces the same containment rule automatically:
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().

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:
  • 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: