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@github/diodeinc/stdlib:HEAD
These can be used directly:
# Load from stdlib
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"

Core Types

Net

A Net represents an electrical connection between component pins.
# Create a net with optional name
net1 = Net()
net2 = Net("VCC")
Type: Net
Constructor: Net(name="")
  • name (optional): String identifier for the net

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, type validation, and promotion semantics.

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)
using() Specifications: Mark fields as promotion targets for automatic type conversion
NET = using(Net("VCC"))
uart = using(Uart())

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 = using(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 = using(Power()),
    uart = using(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)

Promotion Semantics

Fields marked with using() enable automatic type promotion when passing interfaces across module boundaries: Rules:
  • Unique promotion targets: Only one using() field per type per interface - duplicate promotion targets to the same type are not allowed
  • Cross-module promotion: Automatic conversion when crossing module boundaries
  • Same-module access: Explicit field access required within same module
  • Type safety: Promotion only occurs when target type exactly matches the expected type
# Valid: Single promotion target per type
Power = interface(
    NET = using(Net("VCC")),        # Only Net promotion target
    voltage = field(Voltage, ...),
)

# Invalid: Multiple promotion targets for same type
InvalidInterface = interface(
    NET1 = using(Net("VCC")),       # Error: duplicate Net promotion target
    NET2 = using(Net("GND")),       # Error: duplicate Net promotion target
)

# Automatic promotion when passed to modules
Resistor("R1", "10kOhm", "0603", P1=power, P2=gnd)  # power promotes to power.NET

# Explicit access within same module
net = power.NET  # Must use .NET explicitly

Post-Initialization Callbacks

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

Power = interface(
    NET = using(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, field() specifications, or using() specifications

Module

Modules represent hierarchical subcircuits that can be instantiated multiple times.
# 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
)
Type: Module
Constructor: Module(path)

Built-in Functions

io(name, type, 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"))
Parameters:
  • name: String identifier for the input
  • type: Expected type (Net or interface type)
  • default: Default value if not provided by parent
  • optional: If True, returns None when not provided (unless default is specified)

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

error(msg)

Raises a runtime error with the given message.
if not condition:
    error("Condition failed")

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

add_property(name, value)

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

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