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. Core Types
  3. Built-in Functions

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", "Power", "Ground", "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—its version is controlled by the toolchain, ensuring compatibility. 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/stdlib" = "0.3"
"github.com/diodeinc/registry/reference/TPS54331" = "1.0"

Dependency Resolution

Dependencies are automatically resolved when you import a package. The toolchain discovers dependencies from import paths, resolves versions, downloads packages, and updates pcb.toml. 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
├── 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: Manifest format version
  • members: Glob patterns matching subdirectories that contain packages
Board manifest (e.g., boards/MainBoard/pcb.toml):
[workspace]
pcb-version = "0.3"

[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):
[workspace]
pcb-version = "0.3"

[dependencies]
"github.com/diodeinc/registry/reference/TPS54331" = "1.0"
Packages (modules, components) don’t have a [board] section—they’re libraries meant to be instantiated by boards or other modules. See Packages for the complete manifest reference.

Core Types

Net

A Net represents an electrical connection between component pins. Nets can optionally specify electrical properties like impedance, voltage range, and schematic symbols.
# Create a net with optional name
net1 = Net()
net2 = Net("VCC")

# Net with impedance (for controlled impedance routing)
load("@stdlib/units.zen", "Impedance")
clk = Net("CLK", impedance=Impedance(50))  # 50Ω single-ended

# Net with voltage range
load("@stdlib/units.zen", "VoltageRange")
vdd = Net("VDD_3V3", voltage=VoltageRange("3.0V to 3.6V"))

# Net with custom schematic symbol
vcc_sym = Symbol(library="@kicad-symbols/power.kicad_sym", name="VCC")
vcc = Net("VCC", symbol=vcc_sym)
Type: Net Constructor: Net(name="", symbol=None, voltage=None, impedance=None)
  • name (optional): String identifier for the net
  • symbol (optional): Symbol object for schematic representation
  • voltage (optional): VoltageRange specification for the net
  • impedance (optional): Impedance specification for single-ended nets (in Ohms)

Symbol

A Symbol represents a schematic symbol definition with its pins. Symbols can be created manually or loaded from KiCad symbol libraries.
# Local symbol file (in same directory as .zen file)
ic_symbol = Symbol(library="TCA9554DBR.kicad_sym")

# KiCad library symbol via @kicad-symbols alias
connector = Symbol(library="@kicad-symbols/Connector_Generic.kicad_sym", name="Conn_01x14")
gnd = Symbol("@kicad-symbols/power.kicad_sym:GND")

# Explicit pin definition (less common)
my_symbol = Symbol(
    name = "MyDevice",
    definition = [
        ("VCC", ["1", "8"]),    # VCC on pins 1 and 8
        ("GND", ["4"]),         # GND on pin 4
        ("IN", ["2"]),          # IN on pin 2
        ("OUT", ["7"])          # OUT on pin 7
    ]
)
Type: Symbol
Constructor: Symbol(library_spec=None, name=None, definition=None, library=None)
  • library_spec: (positional) String in format “library_path:symbol_name” or just “library_path” for single-symbol libraries
  • name: Symbol name (required when loading from multi-symbol library with named parameters)
  • definition: List of (signal_name, [pad_numbers]) tuples
  • library: Path to KiCad symbol library file
Note: You cannot mix the positional library_spec argument with the named library or name parameters.

Component

Components represent physical electronic parts with pins and properties.
# Using a Symbol for pin definitions
my_symbol = Symbol(
    definition = [
        ("VCC", ["1"]),
        ("GND", ["4"]),
        ("OUT", ["8"])
    ]
)

Component(
    name = "U1",                   # Required: instance name
    footprint = "SOIC-8",          # Required: PCB footprint
    symbol = my_symbol,            # Symbol defines the pins
    pins = {                       # Required: pin connections
        "VCC": vcc_net,
        "GND": gnd_net,
        "OUT": output_net
    },
    prefix = "U",                  # Optional: reference designator prefix (default: "U")
    mpn = "LM358",                 # Optional: manufacturer part number
    type = "op-amp",               # Optional: component type
    properties = {                 # Optional: additional properties
        "voltage": "5V"
    }
)
Type: Component
Constructor: Component(**kwargs)
Key parameters:
  • name: Instance name (required)
  • footprint: PCB footprint (required)
  • symbol: Symbol object defining pins (required)
  • pins: Pin connections to nets (required)
  • prefix: Reference designator prefix (default: “U”)
  • mpn: Manufacturer part number
  • type: Component type
  • properties: Additional properties dict

Interface

Interfaces define reusable connection patterns with field specifications and type validation. Interfaces can specify impedance requirements that automatically propagate to their constituent nets during layout. Differential Pair Example:
load("@stdlib/units.zen", "Impedance")

# DiffPair interface with 90Ω differential impedance
DiffPair = interface(
    P=Net(),
    N=Net(),
    impedance=field(Impedance, None),  # Optional impedance
)

# USB interface uses DiffPair with 90Ω impedance
Usb2 = interface(
    D=DiffPair(impedance=Impedance(90)),
)

# When instantiated, impedance automatically propagates to P/N nets
usb = Usb2("USB")  # USB.D.P and USB.D.N both get 90Ω differential impedance
The impedance from DiffPair interfaces is stored as differential_impedance on the P and N nets, allowing the layout system to assign appropriate netclasses for differential pair routing.

Basic Syntax

InterfaceName = interface(
    field_name = field_specification,
)

Field Types

Net Instances: Use the provided Net instance as the default template
NET = Net("VCC", symbol = Symbol(library = "@kicad-symbols/power.kicad_sym", name = "VCC"))
SDA = Net("I2C_SDA")
Interface Instances: Use interfaces for composition
uart = Uart(TX=Net("UART_TX"), RX=Net("UART_RX"))
field() Specifications: Enforce type checking with explicit defaults
voltage = field(Voltage, unit("3.3V", Voltage))
freqs = field(list[str], ["100kHz", "400kHz"])
count = field(int, 42)

Interface Instantiation

InterfaceName([name], field1=value1, field2=value2, ...)
  • Optional name: First positional argument sets the interface instance name
  • Field overrides: Named parameters override defaults
  • Type validation: Values must match field specifications

Examples

# Define interfaces for grouped signals
Uart = interface(
    TX = Net("UART_TX"),
    RX = Net("UART_RX"),
)

Spi = interface(
    CLK = Net("SPI_CLK"),
    MOSI = Net("SPI_MOSI"),
    MISO = Net("SPI_MISO"),
    CS = Net("SPI_CS"),
)

# Compose interfaces
SystemInterface = interface(
    uart = Uart(),
    spi = Spi(),
    debug = field(bool, False),
)

# Create instances
uart = Uart("DEBUG_UART")
system = SystemInterface("MAIN", debug=True)
Note: Power and Ground are net types from @stdlib/interfaces.zen, not interfaces. Use them like:
load("@stdlib/interfaces.zen", "Power", "Ground")
vcc = Power("VCC_3V3", voltage=VoltageRange("3.3V"))
gnd = Ground("GND")
Type: interface
Constructor: interface(**fields)
  • Fields can be Net instances, interface instances, or field() specifications

PhysicalValue

A PhysicalValue represents an electrical quantity with a numeric value, physical unit, and optional tolerance. Physical values support arithmetic operations with automatic unit tracking.
# PhysicalValue instances are created by unit-specific constructors
# (See builtin.physical_value() in Built-in Functions)

load("@stdlib/units.zen", "Voltage", "Current", "Resistance", "unit")

# Create physical values
supply = Voltage("3.3V")
current = Current("100mA")
resistor = Resistance("4k7")  # 4.7kΩ using resistor notation

# With tolerance
precise_voltage = Voltage("3.3V").with_tolerance("5%")  # 3.3V ±5%

# Arithmetic with automatic unit tracking
power = Voltage("3.3V") * Current("0.5A")  # 1.65W
resistance = Voltage("5V") / Current("100mA")  # 50Ω
Type: PhysicalValue Created by: Unit-specific constructors (e.g., Voltage, Current, Resistance) Properties:
  • .value - The numeric value as a float
  • .tolerance - The tolerance as a decimal fraction (e.g., 0.05 for 5%)
  • .unit - The unit string representation
Methods:
  • .with_tolerance(tolerance) - Returns a new value with updated tolerance
  • .with_value(value) - Returns a new value with updated numeric value
  • .with_unit(unit) - Returns a new value with updated unit
  • .abs() - Returns the absolute value, preserving unit and tolerance
  • .diff(other) - Returns the absolute difference between two values
  • .within(other) - Checks if this value’s tolerance range fits within another’s
Operators:
  • Arithmetic (+, -, *, /) - Operations with automatic unit tracking
  • Comparison (<, >, <=, >=, ==) - Compare physical values
  • Unary negation (-) - Negate the value

PhysicalRange

A PhysicalRange represents a bounded range of physical values with an optional nominal value, used for specifying operating ranges, tolerances, and electrical characteristics. Ranges have minimum and maximum bounds with consistent physical units.
# PhysicalRange values are created by unit-specific range constructors
# (See builtin.physical_range() in Built-in Functions)

# String parsing with range separators
voltage_range = VoltageRange("1.1–3.6V")              # 1.1–3.6 V
supply_range = VoltageRange("11V to 26V")             # 11–26 V

# With nominal value in parentheses
input_range = VoltageRange("11–26 V (12 V nom.)")     # 11–26 V (12 V nom.)

# Tolerance expansion
nominal_voltage = VoltageRange("15V 10%")             # 13.5–16.5 V

# From PhysicalValue
single_point = VoltageRange(Voltage(15))              # 15–15 V

# Keyword arguments
operating_range = VoltageRange(min=11, nominal=16, max=26)  # 11–26 V (16 V nom.)

# Mixed string/number keywords
custom_range = VoltageRange(min="11V", nominal="16", max="26V")  # 11–26 V (16 V nom.)

# Override nominal separately
with_nominal = VoltageRange("11V to 26V", nominal="16V")  # 11–26 V (16 V nom.)
Type: PhysicalRange Created by: Unit-specific range constructors (e.g., VoltageRange, CurrentRange) Properties:
  • min: Minimum value (Decimal)
  • max: Maximum value (Decimal)
  • nominal: Optional nominal/typical value (Decimal or None)
  • unit: Physical unit dimensions
Display Format:
  • With nominal: 11–26 V (12 V nom.)
  • Without nominal: 11–26 V
Methods:
  • .diff(other) - Returns the maximum possible absolute difference between two ranges
    • other: Another PhysicalRange or string (e.g., "0V", "1.7V to 2.0V")
    • Returns PhysicalValue with the worst-case difference
    • Units must match or an error is raised
    • Tolerance is always zero (like PhysicalValue.diff())
    • Useful for determining component voltage ratings (capacitors, level shifters, etc.)
Operators:
  • Addition (+) - Shift a range by a value
    • range + value returns a new range with min/max/nominal shifted by the value
    • Units must match (or value must be dimensionless)
    • The value’s tolerance is ignored (only the numeric value is used)
  • Subtraction (-) - Shift a range down by a value
    • range - value returns a new range with min/max/nominal shifted down by the value
    • Units must match (or value must be dimensionless)
  • Comparison (<, >, <=, >=) - Compare ranges using conservative semantics
    • range1 < range2 is true iff range1.max < range2.min (entire range1 is strictly below range2)
    • range1 > range2 is true iff range1.min > range2.max (entire range1 is strictly above range2)
    • For overlapping ranges, max values are compared as a tiebreaker
    • Can compare with: PhysicalRange, PhysicalValue, or string values
    • Units must be compatible or an error is raised
  • Equality (==) - Check if two ranges are identical
    • Returns true if min, max, nominal, and unit all match
    • Can compare with: PhysicalRange or string values
  • Containment (in) - Check if a value fits within the range
    • value in range returns true if value’s bounds fit entirely within range’s bounds
    • Works with PhysicalValue, PhysicalRange, or string values
  • Unary negation (-) - Negate the range
    • Returns a new range with negated min/max (swapped) and negated nominal
Examples:
# Capacitor voltage rating
vcc = VoltageRange("3.0V to 3.6V")
cap_rating = vcc.diff("0V")  # 3.6V

# Level shifter between two rails
v_high = VoltageRange("3.0V to 3.6V")
max_diff = v_high.diff("1.7V to 2.0V")  # 1.9V

# Range comparison (conservative semantics)
low_voltage = VoltageRange("1V to 2V")
high_voltage = VoltageRange("3V to 4V")
low_voltage < high_voltage   # True (2V < 3V)
low_voltage <= high_voltage  # True

# Overlapping ranges
range1 = VoltageRange("1V to 3V")
range2 = VoltageRange("2V to 4V")
range1 < range2  # False (ranges overlap, but 3V < 4V so Less by tiebreaker)

# Comparing range to value
input_range = VoltageRange("3.3V to 5V")
if input_range <= VoltageRange("3.3V"):
    # Input is entirely within 3.3V rail
    pass

# Equality
VoltageRange("1V to 2V") == VoltageRange("1V to 2V")  # True
VoltageRange("1V to 2V") == VoltageRange("1V to 2V (1.5V nom.)")  # False (different nominal)

# Range arithmetic (shifting ranges)
base_range = VoltageRange("1V to 2V")
shifted_up = base_range + Voltage("3V")    # 4V to 5V
shifted_down = base_range - Voltage("0.5V")  # 0.5V to 1.5V

# With nominal values preserved
input_range = VoltageRange("1V to 3V (2V nom.)")
offset_range = input_range + Voltage("5V")  # 6V to 8V (7V nom.)

Built-in Functions

load()

Imports symbols from another module file. Signature: load(path, *symbols)
# Load from local file
load("./utils.zen", "helper")

# Load from stdlib
load("@stdlib/units.zen", "Voltage", "Resistance")
load("@stdlib/interfaces.zen", "Power", "Ground", "Spi")

# Load multiple symbols
load("@stdlib/units.zen", "Voltage", "Current", "Resistance", "unit")
Parameters:
  • path: Module path to load from (local, @stdlib/, or package URL)
  • *symbols: Names of symbols to import from the module
See Modules and Imports for path resolution details.

Module()

Imports a .zen file as an instantiable schematic module. Modules represent hierarchical subcircuits that can be instantiated multiple times. Signature: Module(path) Defining a module (voltage_divider.zen):
# Declare IO ports
vin = io("vin", Net)
vout = io("vout", Net)
gnd = io("gnd", Net)

# Declare configuration parameters
r1_value = config("r1", str, default="10k")
r2_value = config("r2", str, default="10k")

# Load generic components
Resistor = Module("@stdlib/generics/Resistor.zen")

# Create the subcircuit
Resistor(name="R1", value=r1_value, P1=vin, P2=vout)
Resistor(name="R2", value=r2_value, P1=vout, P2=gnd)
Loading and instantiating:
VoltageDivider = Module("./voltage_divider.zen")

VoltageDivider(
    name = "divider1",
    vin = vcc,
    vout = feedback,
    gnd = gnd,
    r1 = "100k",
    r2 = "47k"
)
Parameters:
  • path: Module path to load (local, @stdlib/, or package URL)
Returns: A callable module constructor See Modules and Imports for path resolution details. For module introspection and testing, see the Testing documentation.

moved()

Signature: moved(old_path, new_path) Parameters
  • old_path: The old path that should be remapped
  • new_path: The new path to remap to
The moved() directive allows you to specify path remapping for refactoring support. When modules or components are moved or renamed, downstream artifacts (like layout files) may still reference the old paths. The moved() directive provides a mapping from old paths to new paths. moved() directives are scoped to the module where they are defined. They affect how paths from that module are resolved in downstream artifacts. If the path refers to a module, all children of that module are automatically remapped:
# This single directive handles:
# POW.PS1 -> PS1
# POW.PS1.some_resistor -> PS1.some_resistor  
# POW.PS1.inner.component -> PS1.inner.component
moved("POW.PS1", "PS1")
# Moved a power supply module from POW namespace to root
moved("POW.PS1", "PS1")

# Renamed a component
moved("OLD_COMP", "NEW_COMP")

# Moved components between modules
moved("ModuleA.ComponentX", "ModuleB.ComponentX")

# Net remapping for position comments
moved("AN_OLD_FILTERED_VCC_VCC", "FILTERED_VCC_VCC")

io()

Declares a net or interface input for a module. Signature: io(name, type, checks=None, default=None, optional=False)
# Required net input
vcc = io("vcc", Net)

# Net type input (Power, Ground from @stdlib/interfaces.zen)
load("@stdlib/interfaces.zen", "Power", "Ground")
power = io("power", Power)
gnd = io("gnd", Ground)

# With explicit default
data = io("data", Net, default=Net("DATA"))

# With validation checks (single function)
def check_voltage_within(within):
    def check_fn(power_value):
        voltage = power_value.voltage
        check(voltage.min >= within.min and voltage.max <= within.max,
              f"Voltage {voltage} not within {within}")
    return check_fn

vbat = io("VBAT", Power, check_voltage_within(VoltageRange("10–30 V")))

# Or with multiple checks as a list
vbat = io("VBAT", Power, [check_voltage_within(VoltageRange("10–30 V")), check_current()])

# Or as a named argument
vbat = io("VBAT", Power, checks=check_voltage_within(VoltageRange("10–30 V")))
Parameters:
  • name: String identifier for the input (positional). Convention: UPPERCASE (e.g., VCC, GND, SPI)
  • type: Expected type - Net or interface type (positional)
  • checks: Optional check function or list of check functions to run on the value at eval time (3rd positional or named)
  • default: Default value if not provided by parent (named)
  • optional: If True, returns None when not provided (unless default is specified) (named)

config()

Declares a configuration value input for a module. Signature: config(name, type, default=None, convert=None, optional=False)
# String configuration
prefix = config("prefix", str, default="U")

# Integer with conversion
baudrate = config("baudrate", int, convert=int)

# Enum configuration
Direction = enum("NORTH", "SOUTH", "EAST", "WEST")
heading = config("heading", Direction)

# Optional configuration
debug = config("debug", bool, optional=True)
Parameters:
  • name: String identifier for the input. Convention: lowercase (e.g., value, package, baudrate)
  • type: Expected type (str, int, float, bool, enum, or record type)
  • default: Default value if not provided
  • convert: Optional conversion function
  • optional: If True, returns None when not provided (unless default is specified)

File()

Resolves a file or directory path using the load resolver. Signature: File(path)
# Get absolute path to a file
config_path = File("./config.json")

# Works with load resolver syntax
stdlib_path = File("@stdlib/components")

Path()

Advanced path resolution supporting all LoadSpec formats with optional existence checking. Signature: Path(path, allow_not_exist=False)
# Basic path resolution (same as File)
config_path = Path("./config.json")

# LoadSpec format support
stdlib_path = Path("@stdlib/components")
github_path = Path("github.com/user/repo/path.zen")

# Allow non-existent paths (only works with local paths)
optional_file = Path("./optional.zen", allow_not_exist=True)
Parameters:
  • path: String path to resolve (supports any LoadSpec format)
  • allow_not_exist: Optional boolean (default: False). If True, allows non-existent paths. Can only be used with local path LoadSpecs, not package/GitHub/GitLab specs.

error()

Raises a runtime error with the given message. Signature: error(msg, suppress=False, kind=None) With suppress=True, the error is rendered but doesn’t stop evaluation or fail the build.
# Basic error (stops evaluation)
if not condition:
    error("Condition failed")

# Suppressed error (rendered but doesn't fail build)
error("Non-critical issue detected", suppress=True)

# Categorized error for filtering
error("Voltage out of range", suppress=True, kind="electrical.voltage")
Parameters:
  • msg: Error message (required)
  • suppress: If True, renders diagnostic but doesn’t stop evaluation or fail build (default: False)
  • kind: Optional diagnostic kind for filtering/categorization (e.g., “electrical.voltage_mismatch”)

check()

Checks a condition and raises an error if false. Signature: check(condition, msg)
check(voltage > 0, "Voltage must be positive")
check(len(pins) == 8, "Expected 8 pins")

warn()

Emits a warning diagnostic with the given message and continues execution. Signature: warn(msg, suppress=False, kind=None) Unlike error(), this does not stop evaluation.
# Basic warning
warn("Component value is outside typical range")

# Suppressed warning (won't fail build with -Dwarnings)
warn("Optimization suggestion", suppress=True)

# Categorized warning for filtering
warn("High current detected", kind="electrical.current")
warn("Trace too narrow", kind="layout.spacing")
Parameters:
  • msg: Warning message (required)
  • suppress: If True, won’t cause build failure with -Dwarnings flag (default: False)
  • kind: Optional diagnostic kind for filtering/categorization (e.g., “electrical.overvoltage”)
Diagnostic Kinds: The kind parameter enables hierarchical categorization of diagnostics for filtering at the CLI level:
# Categorize by domain
warn("Voltage exceeds spec", kind="electrical")
warn("Trace spacing too narrow", kind="layout")
warn("Missing part number", kind="bom")

# Hierarchical kinds use dot-separated segments
warn("Overvoltage detected", kind="electrical.voltage.overvoltage")
warn("Trace clearance violation", kind="layout.spacing.clearance")
Kinds can be suppressed at build time using the -S flag with hierarchical matching:
  • -S electrical suppresses all electrical.* warnings
  • -S electrical.voltage suppresses electrical.voltage.* warnings
  • -S warnings suppresses all warnings regardless of kind
  • -S errors suppresses all errors regardless of kind

builtin.net_type()

Built-in function that creates custom typed net constructors with optional field parameters. The stdlib uses this to define Power, Ground, and other net types.
# Create a typed net with field() specifications
load("@stdlib/units.zen", "VoltageRange")
MyPower = builtin.net_type("MyPower",
    voltage=VoltageRange,
    symbol=field(Symbol, default=Symbol(library="@kicad-symbols/power.kicad_sym", name="VCC"))
)

# Create a typed net with direct type constructors (no defaults)
Clock = builtin.net_type("Clock", frequency=int)

# Create a typed net with enum fields
Level = enum("LOW", "HIGH")
DigitalSignal = builtin.net_type("DigitalSignal", level=Level)
Note: Power and Ground net types are already provided by @stdlib/interfaces.zen. Parameters:
  • type_name: String name for the net type (required, positional)
  • **fields: Keyword arguments defining optional typed fields
    • Each field can be a field() spec (with default), a type constructor (str/int/float/bool), an enum type, or a physical value type
Field Specifications: Fields accept any of these specifications:
  • field(type, default) - Typed field with a default value (from starlark’s builtin field())
  • str, int, float, bool - Direct type constructors (no default)
  • EnumType - Enum type created with enum()
  • Voltage, Current, etc. - Physical value types from stdlib
Usage: All fields are optional during net instantiation - you can create a net without providing any field values:
Power = builtin.net_type("Power", voltage=field(str, "3.3V"), max_current=field(int, 1000))

# Provide all fields
vcc = Power("VCC", voltage="5V", max_current=2000)

# Provide some fields (others get defaults from field() specs)
vdd = Power("VDD", voltage="3.3V")  # max_current uses default 1000

# Provide no fields (all use defaults from field() specs)
v3v3 = Power("3V3")  # voltage="3.3V", max_current=1000

# Access field values as attributes
print(vcc.voltage)        # "5V"
print(vcc.max_current)    # 2000
Field Access:
  • Fields can be accessed as attributes on net instances: net.field_name
  • If a field was not provided and has no default, accessing it will fail
  • Fields are stored in the net’s properties and can be introspected
Type Validation:
  • Field values are validated at net instantiation time using starlark’s type system
  • Type mismatches produce clear error messages:
    Power = builtin.net_type("Power", voltage=str)
    vcc = Power("VCC", voltage=123)  # Error: Field 'voltage' has wrong type: expected str, got int
    
  • Validation works uniformly for all type specifications: builtin types, field() wrappers, enums, and custom types
Typical Use Cases:
# Power nets with voltage specifications
Power = builtin.net_type("Power", voltage=field(str, "3.3V"))
vcc_3v3 = Power("VCC_3V3", voltage="3.3V")
vcc_5v = Power("VCC_5V", voltage="5V")

# Clock signals with frequency metadata
Clock = builtin.net_type("Clock", frequency=int)
main_clk = Clock("MAIN_CLK", frequency=8000000)
rtc_clk = Clock("RTC_CLK", frequency=32768)

# Digital signals with logic level
Level = enum("1V8", "3V3", "5V")
Gpio = builtin.net_type("Gpio", level=Level)
gpio_pin = Gpio("GPIO_1", level=Level("3V3"))

# Power rails with electrical specifications
load("@stdlib/units.zen", "Voltage", "Current", "unit")
PowerRail = builtin.net_type("PowerRail", 
    nominal_voltage=Voltage,
    max_current=Current
)
main_rail = PowerRail("MAIN_RAIL",
    nominal_voltage=unit("3.3V", Voltage),
    max_current=unit("2A", Current)
)
Returns: A callable net type constructor that creates typed net instances

Net Type Promotion

When passing nets between modules, Zener supports automatic type promotion for compatible net types. This allows typed nets to be used in contexts expecting different (more general) types. Promotion Hierarchy:
NotConnected → any type (universal donor)
Power, Ground → Net (demotion to base type)
Net → nothing (cannot be promoted)
Rules:
  1. NotConnected is the universal donor: A NotConnected net can be passed wherever any other net type is expected. This is useful for leaving pins intentionally unconnected while still satisfying type requirements.
  2. Power and Ground demote to Net: Typed power and ground nets can be passed to parameters expecting a generic Net. The net retains its original type for schematic generation.
  3. Net cannot be promoted: A generic Net cannot be passed to a parameter expecting Power, Ground, or other typed nets.
  4. Nothing promotes to NotConnected: No net type can be converted to NotConnected. If a parameter expects NotConnected, only a NotConnected net can be passed.
Examples:
load("@stdlib/interfaces.zen", "Power", "Ground", "NotConnected")

# Module expecting Power input
PowerConsumer = Module("power_consumer.zen")  # has io("vcc", Power)

# NotConnected promotes to Power - valid
nc = NotConnected("NC")
PowerConsumer(name = "consumer", vcc = nc)

# Module expecting generic Net input
GenericModule = Module("generic.zen")  # has io("sig", Net)

# Power demotes to Net - valid
vcc = Power("VCC")
GenericModule(name = "mod", sig = vcc)

# NotConnected promotes to Net - valid
nc2 = NotConnected("NC2")
GenericModule(name = "mod2", sig = nc2)
Invalid promotions (will error):
# Net cannot promote to Power
sig = Net("SIG")
PowerConsumer(name = "consumer", vcc = sig)  # Error: expected Power, got Net

# Power cannot promote to NotConnected
NCModule = Module("nc_module.zen")  # has io("nc", NotConnected)
vcc = Power("VCC")
NCModule(name = "mod", nc = vcc)  # Error: expected NotConnected, got Power
Use Cases:
  • Optional pins: Use NotConnected as a default for optional module inputs that can accept any net type
  • Generic modules: Accept Net type to allow any typed net (Power, Ground, etc.) to be passed
  • Type safety: Require specific types (Power, Ground) when electrical semantics matter

builtin.physical_value()

Built-in function that creates unit-specific physical value constructor types for electrical quantities with optional tolerances.
# Create physical value constructors for different units
Voltage = builtin.physical_value("V")
Current = builtin.physical_value("A")
Resistance = builtin.physical_value("Ohm")
Capacitance = builtin.physical_value("F")
Inductance = builtin.physical_value("H")
Frequency = builtin.physical_value("Hz")
Temperature = builtin.physical_value("K")
Time = builtin.physical_value("s")
Power = builtin.physical_value("W")

# Use the constructors to create physical values
supply = Voltage("3.3V")
current = Current("100mA")
resistor = Resistance("4k7")  # 4.7kΩ using resistor notation

# With tolerance
precise_voltage = Voltage("3.3V").with_tolerance("5%")  # 3.3V ±5%
loose_resistor = Resistance("10kOhm").with_tolerance(0.1)  # ±10%
Parameters:
  • unit: String identifier for the physical unit (e.g., "V", "A", "Ohm", "F", "H", "Hz", "K", "s", "W")
Returns: A PhysicalValueType that can be called to create PhysicalValue instances Standard Usage: This builtin is typically accessed through @stdlib/units.zen, which provides pre-defined constructors:
load("@stdlib/units.zen", "Voltage", "Current", "Resistance", "unit")

# Create physical values
v = unit("3.3V", Voltage)
i = unit("100mA", Current)

# Perform calculations - units are tracked automatically
p = v * i  # Power = Voltage × Current (330mW)
r = v / i  # Resistance = Voltage / Current (33Ω)
Physical Value Methods: Physical values support several methods for manipulation:
  • .with_tolerance(tolerance) - Returns a new physical value with updated tolerance
    • tolerance: String like "5%" or decimal like 0.05
  • .with_value(value) - Returns a new physical value with updated numeric value
    • value: Numeric value (int or float)
  • .with_unit(unit) - Returns a new physical value with updated unit (for unit conversion/casting)
    • unit: String unit identifier or None for dimensionless
  • .abs() - Returns the absolute value of the physical value, preserving unit and tolerance
    • No parameters required
  • .diff(other) - Returns the absolute difference between two physical values
    • other: Another PhysicalValue or string (e.g., "5V") to compare against
    • Units must match or an error is raised
    • Always returns a positive value
    • Tolerance is dropped (consistent with subtraction behavior)
  • .within(other) - Checks if this value’s tolerance range fits completely within another’s
    • other: Another PhysicalValue or string (e.g., "3.3V")
    • Returns True if self’s range is completely contained within other’s range
    • Units must match or an error is raised
Attributes:
  • .value - The numeric value as a float
  • .tolerance - The tolerance as a decimal fraction (e.g., 0.05 for 5%)
  • .unit - The unit string representation
Mathematical Operations: Physical values support arithmetic with automatic unit tracking:
# Multiplication - units multiply dimensionally
power = Voltage("3.3V") * Current("0.5A")  # 1.65W

# Division - units divide dimensionally
resistance = Voltage("5V") / Current("100mA")  # 50Ω

# Addition - requires matching units
total = Voltage("3.3V") + Voltage("5V")  # 8.3V

# Subtraction - requires matching units
delta = Voltage("5V") - Voltage("3.3V")  # 1.7V

# Absolute value
abs_voltage = Voltage("-3.3V").abs()  # 3.3V

# Difference (always positive)
diff = Voltage("3.3V").diff(Voltage("5V"))  # 1.7V
diff_str = Voltage("3.3V").diff("5V")       # 1.7V (strings are auto-converted)

# Tolerance validation
tight = Voltage("3.3V").with_tolerance("5%")   # 3.135V - 3.465V
loose = Voltage("3.3V").with_tolerance("10%")  # 2.97V - 3.63V
tight.within(loose)  # True - 5% tolerance fits within 10%
loose.within(tight)  # False - 10% doesn't fit within 5%
Note: All arithmetic operations and methods automatically convert string arguments to physical values when possible. For example, Voltage("3.3V") + "2V" works and returns 5.3V. Tolerance Handling:
  • Multiplication/Division: Tolerance preserved only for dimensionless scaling (e.g., 2 * 3.3V±1% keeps 1%)
  • Addition/Subtraction: Tolerance is always dropped
  • .abs(): Tolerance is preserved
  • .diff(): Tolerance is dropped (consistent with subtraction)
  • .within(): Considers tolerance ranges for both values
Parsing Support: Physical value constructors accept flexible string formats:
# Basic format with units
Voltage("3.3V")
Current("100mA")

# SI prefixes: m, μ/u, n, p, k, M, G
Capacitance("100nF")
Resistance("4.7kOhm")

# Resistor notation (4k7 = 4.7kΩ)
Resistance("4k7")

# Temperature conversions
Temperature("25C")   # Converts to Kelvin
Temperature("77F")   # Converts to Kelvin

builtin.physical_range()

Built-in function that creates unit-specific range constructor types for defining bounded physical value ranges.
# Create range constructors for different units
VoltageRange = builtin.physical_range("V")
CurrentRange = builtin.physical_range("A")
ResistanceRange = builtin.physical_range("Ohm")
CapacitanceRange = builtin.physical_range("F")
InductanceRange = builtin.physical_range("H")
FrequencyRange = builtin.physical_range("Hz")
TemperatureRange = builtin.physical_range("K")
TimeRange = builtin.physical_range("s")
PowerRange = builtin.physical_range("W")

# Use the constructors to create ranges
input_voltage = VoltageRange("2.7V to 5.5V")
output_current = CurrentRange("0A to 3A")
operating_temp = TemperatureRange("-40C to 85C")

# All construction patterns are supported
supply = VoltageRange("11–26 V (12 V nom.)")           # String with nominal
tolerance = VoltageRange("15V 10%")                    # Tolerance expansion
explicit = VoltageRange(min=11, nominal=16, max=26)    # Keyword arguments
from_value = VoltageRange(Voltage(15))                 # From PhysicalValue
mixed = VoltageRange("11V to 26V", nominal="16V")      # Mixed modes
Parameters:
  • unit: String identifier for the physical unit (e.g., "V", "A", "Ohm", "F", "H", "Hz", "K", "s", "W")
Returns: A PhysicalRangeType that can be called to create PhysicalRange instances Standard Usage: This builtin is typically accessed through @stdlib/units.zen, which provides pre-defined range constructors:
load("@stdlib/units.zen", "VoltageRange", "CurrentRange", "ResistanceRange")

# Use standard constructors from stdlib
input_range = VoltageRange("3.3V to 5V")
load_range = CurrentRange("100mA to 500mA")
pullup_range = ResistanceRange("4.7kOhm to 10kOhm")
Available in stdlib/units.zen:
  • VoltageRange - Voltage ranges (V)
  • CurrentRange - Current ranges (A)
  • ResistanceRange - Resistance ranges (Ω)
  • CapacitanceRange - Capacitance ranges (F)
  • InductanceRange - Inductance ranges (H)
  • FrequencyRange - Frequency ranges (Hz)
  • TemperatureRange - Temperature ranges (K)
  • TimeRange - Time ranges (s)
  • PowerRange - Power ranges (W)
Construction Patterns: The returned range constructor accepts flexible input formats:
# String parsing with range separators
VoltageRange("1.1–3.6V")           # En-dash separator
VoltageRange("11V to 26V")         # Word "to" separator
VoltageRange("11–26V")             # Left side can omit unit

# Nominal value in parentheses
VoltageRange("11–26 V (12 V nom.)")

# Tolerance expansion
VoltageRange("15V 10%")            # Expands to 13.5–16.5 V

# From PhysicalValue
VoltageRange(Voltage(15))          # Single point: 15–15 V
VoltageRange(Voltage(15, 0.1))     # With tolerance: 13.5–16.5 V

# Keyword arguments (all must be present)
VoltageRange(min=11, max=26)
VoltageRange(min=11, nominal=16, max=26)

# Mixed string/number keywords
VoltageRange(min="11V", nominal="16", max="26V")

# Override nominal separately
VoltageRange("11V to 26V", nominal="16V")
Type Safety:
  • Units must be consistent: VoltageRange("5V to 3A") → Error (mixing V and A)
  • Range types are distinct: Cannot pass a VoltageRange where CurrentRange is expected
  • Type checking enforces correctness at compile time
Use Cases: Physical ranges are used to specify operating characteristics, constraints, and requirements:
# Component specifications
input_voltage = VoltageRange("2.7V to 5.5V")
output_current = CurrentRange("0A to 3A (1.5A nom.)")
operating_temp = TemperatureRange("-40C to 85C")

# Load requirements
supply_current = CurrentRange("10mA to 100mA")
clock_frequency = FrequencyRange("1MHz to 100MHz")

# Tolerance specifications
capacitance = CapacitanceRange("100nF 10%")  # 90nF to 110nF
resistance = ResistanceRange("10kOhm 5%")    # 9.5kΩ to 10.5kΩ

# Future: Electrical Rule Checking (ERC)
# Validate that supply ranges satisfy load requirements
# check(supply_voltage_range.contains(load_voltage_range))
See PhysicalRange in Core Types for detailed information about the returned range values.

builtin.add_board_config()

Built-in function for registering board configurations with the layout system. Signature: builtin.add_board_config(name, config, default=False) Parameters:
  • name: String identifier for the board configuration
  • config: BoardConfig object containing design rules and stackup
  • default: If True, this becomes the default board config for the project
This builtin is typically called through the stdlib Board() function rather than directly.

builtin.add_electrical_check()

Built-in function for registering electrical validation checks that run during pcb build. Signature: builtin.add_electrical_check(name, check_fn, inputs=None) Parameters:
  • name: String identifier for the check (required)
  • check_fn: Function to execute for validation (required)
  • inputs: Optional dictionary of input parameters to pass to the check function
Electrical checks use lazy evaluation - they are registered during module evaluation but execute after the design is fully evaluated. This allows checks to validate electrical properties, design rules, or other constraints across the entire design. The check function receives the module as its first argument, followed by any specified inputs as keyword arguments:
def voltage_range_check(module, min_voltage, max_voltage):
    """Check that supply voltage is within acceptable range"""
    supply = module.supply_voltage
    if supply < min_voltage or supply > max_voltage:
        error("Supply voltage {} is outside range {}-{}".format(
            supply, min_voltage, max_voltage
        ))

builtin.add_electrical_check(
    name="supply_voltage_range",
    check_fn=voltage_range_check,
    inputs={
        "min_voltage": 3.0,
        "max_voltage": 5.5,
    }
)
Check Function Signature:
def check_function(module, **kwargs):
    # Validation logic
    # Raise error() or fail assertion to indicate failure
    pass
Example - Basic Check:
def check_no_floating_nets(module):
    """Ensure all nets are connected to at least 2 pins"""
    for net in module.nets:
        if len(net.pins) < 2:
            error("Net '{}' is floating (only {} pin connected)".format(
                net.name, len(net.pins)
            ))

builtin.add_electrical_check(
    name="no_floating_nets",
    check_fn=check_no_floating_nets
)
Example - Parameterized Check:
def check_power_capacity(module, max_current):
    """Verify power supply can handle load current"""
    total_load = sum([c.max_current for c in module.components])
    if total_load > max_current:
        error("Total load current {}A exceeds supply capacity {}A".format(
            total_load, max_current
        ))

builtin.add_electrical_check(
    name="power_capacity",
    check_fn=check_power_capacity,
    inputs={"max_current": 3.0}
)
Execution Model:
  1. Checks are registered during module evaluation via builtin.add_electrical_check()
  2. Check functions and inputs are stored as frozen values
  3. During pcb build, after evaluation completes, all checks are collected from the module tree
  4. Each check executes with a fresh evaluator context
  5. Check failures generate error diagnostics that are reported through the standard diagnostic system
  6. Build fails if any electrical checks fail
Notes:
  • Checks run only during pcb build, not during pcb test (use TestBench for test-specific validation)
  • Check failures generate error-level diagnostics
  • Checks can access the entire module structure, including components, nets, interfaces, and properties
  • Multiple checks with the same name are allowed (they execute independently)

builtin.add_component_modifier()

Built-in function for registering component modifier functions that automatically run on every component created in the current module and all descendant modules. Parameters:
  • modifier_fn: Function that accepts a component and modifies it (required)
Component modifiers enable organization-wide policies by allowing parent modules to automatically modify components created in child modules. Modifiers execute in bottom-up order: child’s own modifiers first, then parent’s, then grandparent’s. This allows parent policies to override child choices. Modifier Function Signature:
def modifier_function(component):
    # Modify component properties
    # No return value required
    pass
Example - Assign Part Numbers for Generic Components:
def assign_parts(component):
    """Convert generic components to specific part numbers"""
    # Resistors
    if hasattr(component, "resistance"):
        if component.resistance == "10k":
            component.manufacturer = "Yageo"
            component.mpn = "RC0603FR-0710KL"

    # Capacitors
    if hasattr(component, "capacitance"):
        if component.capacitance == "10uF":
            component.manufacturer = "Samsung"
            component.mpn = "CL21A106KAYNNNE"

builtin.add_component_modifier(assign_parts)

# Child modules use generic components, parent assigns real parts
ChildBoard = Module("Child.zen")
ChildBoard(name="Board1")
Execution Model:
  1. Modifiers are registered during module evaluation via builtin.add_component_modifier()
  2. When a child module is instantiated, it inherits all ancestor modifiers
  3. Each component creation triggers modifiers in bottom-up order:
    • Module’s own modifiers execute first
    • Parent’s modifiers execute next
    • Grandparent’s and further ancestors follow
  4. Modifiers can read and write any component property (mpn, manufacturer, dnp, custom properties)
Notes:
  • Modifiers only apply to components created AFTER registration
  • Parent modifiers run AFTER child modifiers (can override child choices)
  • Modifiers are inherited through the entire module hierarchy
  • Common uses: vendor policies, DNP rules, property validation, debug tagging

builtin.current_module_path()

Built-in function that returns the current module’s path as a list of strings. Returns: List of strings representing the module hierarchy
  • Root module: [] (empty list)
  • Child module: ["child_name"]
  • Nested module: ["parent_name", "child_name"]
This function enables conditional logic based on module depth or position in the hierarchy. Common use case: applying different policies at the root module versus child modules. Example - Conditional BOM Modifiers at Root:
# Apply BOM modifiers only at the root module
def bom_modifier(component):
    component.bom_notes = "Production-ready"

if len(builtin.current_module_path()) == 0:
    builtin.add_component_modifier(bom_modifier)
Example - Check Module Depth:
path = builtin.current_module_path()
if len(path) > 2:
    print("Warning: deeply nested module")
See the Testing documentation for TestBench and circuit graph analysis.