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. Evaluation Model
  2. Core Types
  3. Built-in Functions
  4. Module System
  5. Type System

Evaluation Model

Files as Modules

Each .zen file is a Starlark module. It can be used in two ways:
  1. Its exported symbols can be load()ed into other modules. For example, load("./MyFile.zen", "MyFunction", "MyType") will load the MyFunction and MyType symbols from the MyFile.zen module.
  2. It can be loaded as a schematic module using the Module() helper. For example, MyFile = Module("./MyFile.zen") will import MyFile.zen as a schematic module, which you can instantiate like so:
    MyFile = Module("./MyFile.zen")
    MyFile(
        name = "MyFile",
        ...
    )
    

Load Resolution

The load() and Module() statements support multiple resolution strategies:
# Local file (relative to current file)
load("./utils.zen", "helper")

# Package reference
load("@stdlib:1.2.3/math.zen", "calculate")

# GitHub repository
load("@github/user/repo:branch/path.zen", "function")

# GitLab repository
load("@gitlab/user/repo:branch/path.zen", "function")

# GitLab repository (nested groups)
Symbol(library = "@gitlab/kicad/libraries/kicad-symbols:v7.0.0/Device.kicad_sym", name = "R_US")

Default Package Aliases

Zener provides built-in package aliases for commonly used libraries:
  • @kicad-footprints@gitlab/kicad/libraries/kicad-footprints:9.0.0
  • @kicad-symbols@gitlab/kicad/libraries/kicad-symbols:9.0.0
  • @stdlib → Pinned to the toolchain version (e.g., @github/diodeinc/stdlib:v0.4.4)
The @stdlib alias is special: its version is controlled by the pcb toolchain, not by user configuration. This ensures stdlib is always compatible with your toolchain version. You don’t need to declare stdlib in your [dependencies] - it’s implicitly available. These can be used directly:
# Load from stdlib (version determined by toolchain)
load("@stdlib/units.zen", "kohm", "uF")

# Load from KiCad symbols library
R_symbol = Symbol(library = "@kicad-symbols/Device.kicad_sym", name = "R")

Custom Package Aliases

You can define custom package aliases or override the defaults in your workspace’s pcb.toml:
[packages]
# Override default version
kicad-symbols = "@gitlab/kicad/libraries/kicad-symbols:7.0.0"

# Add custom aliases
my-lib = "@github/myorg/mylib:v1.0.0"
local-lib = "./path/to/local/lib"
Note: You can override stdlib to a newer version than the toolchain default by declaring it in [dependencies], but you cannot use an older version.

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.
# Create a symbol from explicit definition
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
    ]
)

# Load from a KiCad symbol library
op_amp = Symbol(library = "./symbols/LM358.kicad_sym")

# For multi-symbol libraries, specify which symbol
mcu = Symbol(library = "./symbols/microcontrollers.kicad_sym", name = "STM32F103")

# Shorthand syntax: library path and symbol name in one string
gnd = Symbol("@kicad-symbols/power.kicad_sym:GND")
resistor = Symbol("./symbols/passives.kicad_sym:R_0402")

# For single-symbol libraries, the name can be omitted
op_amp = Symbol("./symbols/LM358.kicad_sym")
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,
    __post_init__ = callback_function,  # Optional
)

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 the provided interface instance for composition
uart = Uart(TX=Net("UART_TX"), RX=Net("UART_RX"))
power = Power(NET=Net("VDD"))
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
Power = interface(
    NET = Net("VCC", symbol = Symbol(library = "@kicad-symbols/power.kicad_sym", name = "VCC")),
    voltage = field(Voltage, unit("3.3V", Voltage)),
)

Uart = interface(
    TX = Net("UART_TX"),
    RX = Net("UART_RX"),
)

SystemInterface = interface(
    power = Power(),
    uart = Uart(),
    debug = field(bool, False),
)

# Create instances
power = Power()                                      # All defaults
vcc = Power("VCC_3V3")                               # Named instance
custom = Power("MAIN", voltage=unit("5V", Voltage))  # Named with override

system = SystemInterface("MAIN", debug=True)

Post-Initialization Callbacks

def _power_post_init(self):
    if self.voltage.value <= 0:
        error("Power voltage must be positive")

Power = interface(
    NET = Net("VCC"),
    voltage = field(Voltage, unit("3.3V", Voltage)),
    __post_init__ = _power_post_init,
)
Callbacks receive self and cannot be overridden during instantiation. Type: interface
Constructor: interface(**fields)
  • Fields can be Net instances, interface instances, or field() specifications

Module

Modules represent hierarchical subcircuits that can be instantiated multiple times. Module objects support indexing to access child components and submodules directly.
# Load a module from a file
SubCircuit = Module("./subcircuit.zen")

# Instantiate the module
SubCircuit(
    name = "power_supply",
    # ... pass inputs defined by io() and config() in the module
)

# Access child components and submodules via indexing
component = module["ComponentName"]     # Access component by name
submodule = module["SubmoduleName"]     # Access submodule by name

# Chain indexing for nested access
nested_component = module["SubmoduleName"]["ComponentName"]

# Nested path syntax (equivalent to chained indexing)
nested_component = module["SubmoduleName.ComponentName"]  # Same as above

# Check if components exist using membership operator
if "ComponentName" in module:
    component = module["ComponentName"]

# Check nested paths
if "SubmoduleName.ComponentName" in module:
    nested_component = module["SubmoduleName.ComponentName"]
Type: Module
Constructor: Module(path)
Module Indexing: module[name] supports:
  • Single names: module["ComponentName"] returns Component or Module objects
  • Nested paths: module["Sub.Component"] equivalent to module["Sub"]["Component"]
  • Deep nesting: module["A.B.C"] equivalent to module["A"]["B"]["C"]
  • Returns Component objects for leaf components, Module objects for intermediate submodules
  • Raises an error if any part of the path is not found
Module Membership: name in module supports:
  • Single names: "ComponentName" in module checks if component or submodule exists
  • Nested paths: "Sub.Component" in module equivalent to checking nested existence
  • Returns True if the path exists, False otherwise
  • Works with the same path syntax as indexing
Module Attributes:
  • module.nets: Dict mapping net names to lists of connected port tuples
  • module.components: Dict mapping component paths to component objects
Naming Conventions:
  • Component paths in module.components follow the pattern SubmoduleName.ComponentName (e.g., "BMI270.BMI270", "C1.C")
  • The first part is the submodule name, the second part is the component name within that submodule
  • Indexing supports both single names and nested paths:
    • module["BMI270"] returns the BMI270 submodule
    • module["BMI270"]["BMI270"] returns the component within the submodule (chained)
    • module["BMI270.BMI270"] also returns the component (nested path syntax)
  • All three approaches are equivalent for accessing nested components

TestBench

TestBench values represent the results of module validation tests. They are created by the TestBench() function and contain information about the tested module and check results.
# TestBench values are returned by TestBench()
result = TestBench(
    name = "MyTest",
    module = MyModule,
    checks = [check_func1, check_func2]
)

# Access TestBench properties
print(result)  # TestBench(MyTest)
Type: TestBench
Created by: TestBench() function (see Built-in Functions)
Properties accessible via the TestBench value:
  • name: The test bench identifier
  • Module evaluation status
  • Check function results

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.)

PhysicalRangeType

A PhysicalRangeType is a factory type that creates PhysicalRange instances for a specific physical unit. Each unit has its own range type (e.g., VoltageRange for volts, CurrentRange for amperes).
# Range types are created by builtin.physical_range()
VoltageRange = builtin.physical_range("V")
CurrentRange = builtin.physical_range("A")

# Range types are callable and return PhysicalRange instances
input_voltage = VoltageRange("3.3V to 5V")
load_current = CurrentRange("100mA to 500mA")
Type: PhysicalRangeType Created by: builtin.physical_range(unit) (see Built-in Functions) Standard Range Types (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 Flexibility: Range constructors accept multiple input formats for maximum flexibility:
  1. String parsing with separators:
    • En-dash: "1.1–3.6V"
    • Word “to”: "11V to 26V"
    • Left side can omit unit: "11–26V"
  2. Nominal value extraction:
    • Parenthesized suffix: "11–26 V (12 V nom.)"
    • Can be combined with string ranges or keyword override
  3. Tolerance expansion:
    • Percentage tolerance: "15V 10%" expands to 13.5–16.5 V
  4. PhysicalValue conversion:
    • Single value: VoltageRange(Voltage(15)) creates point range 15–15 V
    • Toleranced value: VoltageRange(Voltage(15, 0.1)) expands tolerance to range
  5. Keyword arguments:
    • Explicit bounds: min=11, max=26
    • With nominal: min=11, nominal=16, max=26
    • Accepts numbers or strings: min="11V", max="26V"
  6. Mixed modes:
    • String range with keyword nominal: VoltageRange("11V to 26V", nominal="16V")
Type Safety:
  • Units must be consistent across min, max, and nominal values
  • Range types are distinct: VoltageRangeCurrentRange
  • Type checking enforces unit correctness at compile time
Use Cases:
  • Component operating ranges: input_voltage = VoltageRange("2.7V to 5.5V")
  • Load specifications: output_current = CurrentRange("0A to 3A")
  • Environmental conditions: operating_temp = TemperatureRange("-40C to 85C")
  • Future: Electrical Rule Checking (ERC) - validate that supply ranges satisfy load requirements

Built-in Functions

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(name, type, checks=None, default=None, optional=False)

Declares a net or interface input for a module.
# Required net input
vcc = io("vcc", Net)

# Optional interface input with default
PowerInterface = interface(vcc = Net, gnd = Net)
power = io("power", PowerInterface, optional=True)

# 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)
  • 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(name, type, default=None, convert=None, optional=False)

Declares a configuration value input for a module.
# 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
  • 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(path)

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

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

Path(path, allow_not_exist=False)

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

# LoadSpec format support
stdlib_path = Path("@stdlib/components")
github_path = Path("@github/user/repo:branch/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(msg, suppress=False, kind=None)

Raises a runtime error with the given message. 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(condition, msg)

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

warn(msg, suppress=False, kind=None)

Emits a warning diagnostic with the given message and continues execution. 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

add_property(name, value)

Adds a property to the current module instance.
add_property("layout_group", "power_supply")
add_property("critical", True)

builtin.net(type_name, **fields)

Built-in function that creates custom typed net constructors with optional field parameters.
# Create a basic net type (equivalent to the builtin Net)
Net = builtin.net("Net")

# Create a typed net with field() specifications (with defaults)
Power = builtin.net("Power", 
    voltage=field(str, "3.3V"),
    max_current=field(int, 1000)
)

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

# Create a typed net with physical value fields
load("@stdlib/units.zen", "Voltage", "VoltageRange")
Supply = builtin.net("Supply", 
    voltage=Voltage,
    range=VoltageRange
)

# Create a typed net with enum fields
Level = enum("LOW", "HIGH")
DigitalSignal = builtin.net("DigitalSignal", level=Level)
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("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("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("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("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("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("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

builtin.physical_value(unit)

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(unit)

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(name, config, default=False)

Built-in function for registering board configurations with the layout system. 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(name, check_fn, inputs=None)

Built-in function for registering electrical validation checks that run during pcb build. 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(modifier_fn)

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

match_component(match, parts)

Stdlib helper function (from @stdlib/bom/helpers.zen) for creating component modifiers that assign MPNs based on property matching. Parameters:
  • match: Dict of property names to values (all must match)
  • parts: Tuple (mpn, manufacturer) or list of tuples (first is primary, rest are alternatives)
Example:
load("@stdlib/bom/helpers.zen", "match_component")

Board(
    name="MyBoard",
    layers=4,
    modifiers=[
        match_component(
            match={"resistance": "10k", "package": "0603"},
            parts=[
                ("ERJ-3EKF1002V", "Panasonic"),  # Primary
                ("RC0603FR-0710KL", "Yageo"),    # Alternative
            ]
        ),
    ]
)
Use to override default BOM matching or assign board-specific part numbers. Modifiers run before BOM profile.

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")

Board Configuration Types

The board configuration record types are provided by @stdlib/board_config.zen and offer comprehensive control over PCB manufacturing specifications. Available Records and Functions:
load("@stdlib/board_config.zen", 
    "Board",              # Board configuration function
    "BoardConfig",        # Main configuration container
    "DesignRules",        # Design rule constraints  
    "Stackup",            # Layer stackup definition
    "Material",           # Material properties
    "CopperLayer",        # Copper layer specification
    "DielectricLayer",    # Dielectric layer specification
    "Constraints",        # Manufacturing constraints
    "Copper",             # Copper-specific rules
    "NetClass",           # Netclass definitions
    "merge_configs",      # Configuration merging utility
    "deep_merge"          # Deep merge utility
)

Board(name, layout_path, config=None, layers=None, layout_hints=None, default=False, modifiers=None, bom_profile=add_default_bom_profile)

Stdlib function for declaring board configurations for PCB layout generation.
# Load from stdlib
load("@stdlib/board_config.zen", "Board", "BoardConfig", "DesignRules", "Stackup", "Material", "CopperLayer", "DielectricLayer")

# Simple usage with predefined layer count (recommended for most projects)
Board(
    name="MyBoard",
    layout_path="layout/",
    layers=4,
    default=True
)

# Advanced usage with custom configuration
config = BoardConfig(
    stackup=Stackup(
        materials=[
            Material(
                name="FR-4",
                vendor="Isola",
                relative_permittivity=4.21,
                loss_tangent=0.025,
                reference_frequency=1e9,
            ),
        ],
        silk_screen_color="#44805BFF",
        solder_mask_color="#191919E6",
        thickness=1.605,
        symmetric=True,
        copper_finish="ENIG",
        layers=[
            CopperLayer(thickness=0.035, role="mixed"),
            DielectricLayer(thickness=0.2, material="FR-4", form="prepreg"),
            CopperLayer(thickness=0.035, role="power"),
            DielectricLayer(thickness=1.065, material="FR-4", form="core"),
            CopperLayer(thickness=0.035, role="power"),
            DielectricLayer(thickness=0.2, material="FR-4", form="prepreg"),
            CopperLayer(thickness=0.035, role="mixed"),
        ],
    )
)

# Declare board using custom configuration
Board(
    name="MyBoard",
    layout_path="layout/",
    config=config,
    default=True
)

# BOM customization with custom part matching
load("@stdlib/bom/helpers.zen", "match_component")

Board(
    name="ProductionBoard",
    layout_path="layout/",
    layers=4,
    default=True,
    # Custom modifiers run BEFORE default BOM matching
    modifiers=[
        # Override specific components with preferred vendors
        match_component(
            match={"resistance": "10k", "package": "0603"},
            parts=[
                ("ERJ-3EKF1002V", "Panasonic"),
                ("RC0603FR-0710KL", "Yageo"),  # Alternative
            ]
        ),
        match_component(
            match={"capacitance": "100nF", "voltage": "50V", "package": "0402"},
            parts=("GCM155R71H104KE02D", "Murata")
        ),
    ]
    # bom_profile defaults to add_default_bom_profile,
    # which handles standard resistors/capacitors
)

# Disable automatic BOM matching entirely
Board(
    name="CustomBoard",
    layout_path="layout/",
    layers=4,
    bom_profile=lambda: None  # No automatic BOM matching
)
Parameters:
  • name: String identifier for the board configuration (required)
  • layout_path: Directory path where layout files will be generated (required)
  • config: BoardConfig record containing design rules and stackup (optional, mutually exclusive with layers)
  • layers: Number of layers - 2, 4, 6, or 8 (optional, mutually exclusive with config). When specified, uses predefined stackups from the stdlib:
    • layers=2: 2-layer board (1.6mm, 1oz copper, SIG/SIG)
    • layers=4: 4-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/PWR/SIG)
    • layers=6: 6-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/(SIG/PWR)/(SIG/PWR)/GND/SIG)
    • layers=8: 8-layer board (1.6mm, 1oz outer/0.5oz inner, SIG/GND/SIG/PWR/GND/SIG/GND/SIG)
  • layout_hints: Optional list of layout optimization hints
  • default: If True, this becomes the default board config for the project
  • modifiers: Optional list of component modifier functions that run before BOM profile modifiers. Use this to override BOM matching or set custom part numbers for specific components. Functions are registered only at the root module level.
  • bom_profile: Function that registers BOM matching modifiers (default: add_default_bom_profile). The default profile automatically assigns manufacturer part numbers to generic resistors and capacitors. Pass lambda: None to disable BOM matching.
Note: Either config or layers must be provided, but not both. If neither is provided, an error will be raised. Implementation: The stdlib Board() function calls the language builtin builtin.add_board_config() and sets layout properties. BoardConfig Structure:
BoardConfig = record(
    design_rules=DesignRules,     # PCB design rules and constraints  
    stackup=Stackup,              # Layer stackup and materials
    num_user_layers=int,          # Number of User.N layers (default: 4)
    spec=str | None,              # Optional path to specification file
)
Parameters:
  • design_rules: Optional PCB design rules and constraints
  • stackup: Optional layer stackup and materials configuration
  • num_user_layers: Number of User.N layers to create (default: 4)
  • spec: Optional path to a specification markdown file. Use Path() to resolve the path:
    BoardConfig(spec=Path("./docs/my-spec.md"))
    
Design Rules Examples:
# Copper constraints (from @stdlib/board_config.zen)
Copper(
    minimum_clearance=0.1,              # mm
    minimum_track_width=0.1,            # mm  
    minimum_via_diameter=0.2,           # mm
    copper_to_edge_clearance=0.3,       # mm
)

# Netclasses for different signal types  
NetClass(
    name="Power",
    clearance=0.25,                     # mm
    track_width=0.6,                    # mm
    via_diameter=0.8,                   # mm
    color="#FF0000",
)
Stackup Configuration Examples:
# Material definitions (from @stdlib/board_config.zen)
Material(
    name="FR-4",
    vendor="Isola", 
    relative_permittivity=4.21,         # Dielectric constant
    loss_tangent=0.025,                 # Loss tangent  
    reference_frequency=1e9,            # Hz
)

# Layer definitions (from @stdlib/board_config.zen)
CopperLayer(thickness=0.035, role="signal")     # "signal", "power", "mixed"
DielectricLayer(thickness=1.6, material="FR-4", form="core")  # "core", "prepreg"
Configuration Merging (Stdlib Utilities): The stdlib provides utilities for merging multiple board configurations:
load("@stdlib/board_config.zen", "merge_configs", "deep_merge")

base_config = BoardConfig(stackup=my_stackup)
tight_rules = BoardConfig(design_rules=tight_constraints)

# Merge multiple configs using stdlib utilities
final_config = merge_configs(base_config, tight_rules)

# Use built-in Board() with merged config
Board("Production", final_config, "layout/", default=True)

TestBench(name, module, checks)

Creates a test bench for validating module connectivity and properties without requiring inputs.
# Load a module to test
MyCircuit = Module("./my_circuit.zen")

# Define check functions
def verify_power_connections(module):
    """Ensure all power pins are connected to VCC"""
    vcc_connections = module.nets.get("VCC", [])
    check(len(vcc_connections) >= 2, "Need at least 2 VCC connections")

def verify_ground_plane(module):
    """Check for proper ground connections"""
    check("GND" in module.nets, "Missing GND net")
    check(len(module.nets["GND"]) > 3, "GND net needs more than 3 connections")

# Create test bench
TestBench(
    name = "PowerTest",
    module = MyCircuit,
    checks = [verify_power_connections, verify_ground_plane]
)
Parameters:
  • name: String identifier for the test bench
  • module: Module instance to test (created with Module())
  • checks: List of check functions to execute
Check Function Signature: Check functions receive a single Module argument containing circuit data:
def check_function(module: Module):
    # Access circuit data through the Module
    nets = module.nets              # Map of net names to connected port tuples
    components = module.components  # Map of component paths to component objects
    
    # Access components via indexing (new preferred method)
    component = module["ComponentName"]              # Direct component access
    submodule = module["SubmoduleName"]              # Direct submodule access
    nested = module["SubmoduleName"]["ComponentName"] # Chained access
    
    # Access component attributes
    component_pins = component.pins     # Component pin connections
    component_type = component.type     # Component type (if available)
    component_props = component.properties  # Component properties dict
    
    # Perform validation - signal failures using check() or error()
    check(condition, "Failure message")  # Throws error if condition is False
Module Contents:
  • module.nets: Maps each net name to a list of connected port tuples (e.g., {"VCC": [("U1", "VDD"), ("C1", "P1")]})
  • module.components: Maps component paths to component objects (e.g., {"U1.IC": <Component>, "C1.C": <Component>})
  • module["name"]: Direct indexing access to child components and submodules by name
Check Function Behavior:
  • Check functions should use check(condition, message) or error(message) to signal failures
  • Any unhandled error or exception in a check function is treated as a test failure
  • Check functions do not need to return any specific value
  • Use print() for informational output during testing
TestBench Behavior:
  • Evaluates the module with relaxed input requirements (missing required inputs are allowed)
  • Stores check functions for deferred execution (checks run later when using pcb test command)
  • Returns a TestBench value containing test cases and deferred checks
  • When executed by pcb test: runs each check function in order, reports failures as diagnostics with precise source location pointing to the failing check() call, and prints a success message if all checks pass
Example with Modern Indexing Syntax:
def check_power_connections(module: Module):
    """Verify power IC connections using modern indexing syntax"""
    # Access submodule and then component within it (chained syntax)
    power_ic = module["PowerIC"]["LDO"]
    
    # Equivalent using nested path syntax
    power_ic_nested = module["PowerIC.LDO"]
    
    # Both approaches return the same component
    check(power_ic.name == power_ic_nested.name, "Chained and nested path syntax should be equivalent")
    
    # Check that power pins are connected properly
    check("VCC" in power_ic.pins["VIN"].name, "LDO input not connected to VCC")
    check("GND" in power_ic.pins["GND"].name, "LDO ground not connected")
    
    print(f"✓ {power_ic.name} power connections verified")

def check_capacitor_placement(module: Module):
    """Verify bypass capacitors are properly placed"""
    # Access capacitor components by name using both syntaxes
    c1 = module["C1"]["C"]    # Chained syntax
    c2 = module["C2.C"]       # Nested path syntax
    
    # Verify they're connected to power and ground
    check("VCC" in c1.pins["P1"].name, "C1 not connected to power")
    check("GND" in c1.pins["P2"].name, "C1 not connected to ground")
    check("VCC" in c2.pins["P1"].name, "C2 not connected to power")
    check("GND" in c2.pins["P2"].name, "C2 not connected to ground")
    
    print(f"✓ Capacitors {c1.name} and {c2.name} properly placed")

def check_traditional_syntax(module: Module):
    """Example using traditional components dict access"""
    for comp_path, component in module.components.items():
        if component.type == "capacitor":
            check(hasattr(component, 'capacitance'), f"{comp_path} missing capacitance")
            print(f"✓ {comp_path} capacitance: {component.capacitance}")

def check_net_connectivity(module: Module):
    """Verify net connections using nets attribute"""
    check("VCC" in module.nets, "Missing VCC power net")
    check("GND" in module.nets, "Missing GND net")
    
    vcc_connections = len(module.nets["VCC"])
    check(vcc_connections >= 2, f"VCC only has {vcc_connections} connections, need at least 2")
    
    print(f"✓ VCC net has {vcc_connections} connections")

def check_component_presence(module: Module):
    """Use membership operator to check component existence"""
    # Check if components exist before accessing them
    if "PowerIC" in module:
        power_ic = module["PowerIC"]
        print(f"✓ Found PowerIC submodule: {power_ic}")
        
        # Check nested component existence
        if "PowerIC.LDO" in module:
            ldo = module["PowerIC.LDO"]
            print(f"✓ Found LDO component: {ldo.name}")
        else:
            error("LDO component not found in PowerIC")
    else:
        print("PowerIC not present in this module")
    
    # Check multiple components at once
    required_caps = ["C1", "C2", "C3"]
    missing_caps = [cap for cap in required_caps if cap not in module]
    
    if missing_caps:
        error(f"Missing required capacitors: {missing_caps}")
    else:
        print("✓ All required capacitors present")

TestBench(
    name = "PowerCircuitValidation",
    module = MyPowerCircuit,
    checks = [
        check_power_connections,      # Uses modern indexing syntax
        check_capacitor_placement,    # Uses chained indexing
        check_traditional_syntax,     # Uses traditional components dict
        check_net_connectivity,       # Uses nets attribute
        check_component_presence,     # Uses membership operator
    ]
)

Circuit Graph Analysis & Path Validation

Overview

Zener provides circuit graph analysis for validating module connectivity and topology. The system converts circuit schematics into searchable graphs, enabling path finding between component pins and verification of component sequences. The graph analysis operates on the public interface paradigm: path finding works between component ports (specific IC pins) and external nets (module’s io() declarations), while automatically discovering internal routing paths.

Core Concepts

Circuit Graph

Every module automatically generates a circuit graph that models component connectivity:
def analyze_circuit(module: Module):
    # Get the circuit graph from any module
    graph = module.graph()
    
    # Find paths between public interface points
    paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND", max_depth=5)

Public Interface Boundaries

Path finding operates between two types of well-defined endpoints: Component Ports: Specific pins on specific components
("ComponentName", "PinName")  # e.g., ("TPS82140", "VIN"), ("STM32", "PA0")
External Nets: Public nets exposed by the module’s io() declarations
"NetName"  # e.g., "GND_GND", "VCC_VCC", "SPI_CLK"

Path Finding API

graph.paths(start, end, max_depth=10)

Finds all simple paths between two points in the circuit:
def validate_power_supply(module: Module):
    graph = module.graph()
    
    # IC pin to external net
    input_paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND")
    
    # IC pin to IC pin (internal feedback path)
    feedback_paths = graph.paths(start=("TPS82140", "VOUT"), end=("TPS82140", "FB"))
    
    # External net to IC pin
    enable_paths = graph.paths(start="EN_EN", end=("TPS82140", "EN"))
Parameters:
  • start: Component port tuple ("Component", "Pin") or external net name
  • end: Component port tuple ("Component", "Pin") or external net name
  • max_depth: Maximum number of components to traverse (default: 10)
Returns: List of Path objects

Path Objects

Each path contains discovered connectivity information:
def analyze_path(path):
    print("Path details:")
    print("  Ports:", path.ports)        # List of (component, pin) tuples traversed
    print("  Components:", len(path.components))  # Components in the path
    print("  Nets:", path.nets)          # Net names traversed

Path Validation Methods

Basic Validation

# Count components matching predicate
resistor_count = path.count(is_resistor)

# Ensure at least one component matches
path.any(is_capacitor)

# Ensure all components match
path.all(is_passive_component)

# Ensure no components match
path.none(is_active_component)

Sequential Pattern Matching

The path.matches() method validates component sequences in order:
def validate_filter_topology(module: Module):
    graph = module.graph()
    filter_paths = graph.paths(start=("OpAmp", "OUT"), end="GND_GND")
    
    # Validate exact component sequence
    filter_paths[0].matches(
        is_resistor("1kOhm"),    # Series resistor
        is_capacitor("100nF"),   # Filter capacitor
        is_resistor("10kOhm")    # Load resistor
    )

Design Principles

Datasheet Requirements Translation

Circuit validation can directly implement datasheet requirements by mapping component pin constraints to path validation: Power Supply Decoupling
def validate_vin_decoupling(module: Module):
    """Validate VIN decoupling per datasheet Figure 8-1"""
    graph = module.graph()
    
    # Find all paths from VIN pin to ground
    vin_paths = graph.paths(start=("TPS82140", "VIN"), end="GND_GND")
    
    # Datasheet requirement: "≥10µF bulk + 100nF ceramic"
    vin_paths.any(path.matches(is_capacitor("10uF")))   # Bulk capacitor
    vin_paths.any(path.matches(is_capacitor("100nF")))  # Bypass capacitor
    
    print("✓ VIN decoupling meets datasheet requirements")
    // Reference: Datasheet §8.2.1, Figure 8-1
Feedback Networks
def validate_feedback_divider(module: Module):
    """Validate feedback network per datasheet Section 8.3"""
    graph = module.graph()
    
    # Find path from output to feedback pin
    fb_paths = graph.paths(start=("TPS82140", "VOUT"), end=("TPS82140", "FB"))
    
    # Datasheet requirement: "Precision resistor divider"
    fb_paths[0].matches(
        is_resistor(),  # Upper feedback resistor
        is_resistor()   # Lower feedback resistor
    )
    
    print("✓ Feedback divider topology validated")
    // Reference: Datasheet §8.3.2, Equation 1
Bootstrap Circuits
def validate_bootstrap_cap(module: Module):
    """Validate bootstrap capacitor per datasheet Figure 8-4"""
    graph = module.graph()
    
    # Bootstrap cap connects BOOT pin to SW pin
    boot_paths = graph.paths(start=("TPS82140", "BOOT"), end=("TPS82140", "SW"))
    
    # Datasheet requirement: "100nF ceramic, X7R dielectric"
    boot_paths[0].matches(is_capacitor("100nF", dielectric="X7R"))
    
    print("✓ Bootstrap capacitor meets specifications")
    // Reference: Datasheet §8.4.1, Figure 8-4

Public Interface Paradigm

Start/End Points: Always use the module’s “public” interface:
  • Component ports: Known IC pins from the main component
  • External nets: Public nets from io() declarations
Internal Discovery: Path finding automatically traverses internal components and nets Benefits:
  • Deterministic scope: Clear rule boundaries
  • Implementation freedom: Internal routing flexibility
  • Hierarchical composability: Rules work at any module level
  • Performance: Constrained search space

Validation Strategies

Path Existence: Verify required connections exist
# "VIN must connect to GND through decoupling"
vin_paths = graph.paths(start=("IC", "VIN"), end="GND_GND")
check(len(vin_paths) > 0, "VIN decoupling missing")
Topology Validation: Verify exact component sequences
# "Output filter must be L-C configuration"
filter_paths = graph.paths(start=("IC", "SW"), end=("IC", "VOUT"))
filter_paths[0].matches(is_inductor(), is_capacitor())
Alternative Paths: Handle multiple valid implementations
# "Either RC or LC filter acceptable"
filter_paths = graph.paths(start=("IC", "OUT"), end="LOAD_LOAD")
rc_valid = any(path.matches(is_resistor(), is_capacitor()) for path in filter_paths)
lc_valid = any(path.matches(is_inductor(), is_capacitor()) for path in filter_paths)
check(rc_valid or lc_valid, "Filter topology required")
Error Suppression: Use suppress_errors=True for path identification
# Find all paths matching a pattern without failing
matching_paths = [p for p in all_paths if p.matches(
    is_resistor(), is_capacitor(), suppress_errors=True
)]
check(len(matching_paths) > 0, "No RC filter found")

path.matches() API Reference

Syntax

path.matches(*matchers, suppress_errors=False)
Parameters:
  • *matchers: Sequential matcher functions to apply in order
  • suppress_errors: If True, returns False on validation failure instead of raising errors

Sequential Processing Model

  • Matchers consume components sequentially using cursor-based processing
  • Each matcher receives (path, cursor_index) and returns components consumed
  • Validation fails if any matcher fails or if components remain after all matchers

Built-in Matcher Functions

# Basic component type matchers
is_resistor(expected_value=None)     # Match resistor, optionally with specific value
is_capacitor(expected_value=None)    # Match capacitor, optionally with specific value
is_inductor(expected_value=None)     # Match inductor, optionally with specific value

# Navigation matchers
skip(n)                             # Skip exactly n components
skip_rest()                         # Consume all remaining components

# Quantified matchers
exactly_n_resistors(n)              # Exactly n consecutive resistors
at_least_n_capacitors(n)            # At least n consecutive capacitors
at_most_n_components(n)             # At most n components of any type

# Conditional matching  
any_of(matcher1, matcher2, ...)     # Match any of the provided matchers
skip_until(matcher)                 # Skip components until matcher succeeds
contains_somewhere(matcher)         # Matcher succeeds somewhere in remaining path

# Component properties
has_package(size)                   # Component with specific package size
name_contains(pattern)              # Component name contains pattern

Custom Matcher Functions

def custom_matcher(path, cursor):
    """Custom matcher function template"""
    if cursor >= len(path.components):
        error("path ended, expected component")
    
    component = path.components[cursor]
    # Validation logic here
    check(component.type == "resistor", "Expected resistor")
    
    return 1  # Consume 1 component, advance cursor

Module System

Module Definition

A module is defined by a .zen file that declares its inputs and creates components:
# voltage_divider.zen

# Declare inputs
vin = io("vin", Net)
vout = io("vout", Net)
gnd = io("gnd", Net)

r1_value = config("r1", str, default="10k")
r2_value = config("r2", str, default="10k")

# Define a resistor symbol (could also load from library)
resistor_symbol = Symbol(
    definition = [
        ("1", ["1"]),
        ("2", ["2"])
    ]
)

# Create components
Component(
    name = "R1",
    type = "resistor",
    footprint = "0402",
    symbol = resistor_symbol,
    pins = {"1": vin, "2": vout},
    properties = {"value": r1_value}
)

Component(
    name = "R2",
    type = "resistor",
    footprint = "0402",
    symbol = resistor_symbol,
    pins = {"1": vout, "2": gnd},
    properties = {"value": r2_value}
)

Module Instantiation

# Load the module
VDivider = Module("./voltage_divider.zen")

# Create instances
VDivider(
    name = "divider1",
    vin = Net("INPUT"),
    vout = Net("OUTPUT"),
    gnd = Net("GND"),
    r1 = "100k",
    r2 = "47k"
)