Files
diodeinc__pcb/docs/spec.md
d-asnaghi 9b35621c34 fix spec
2025-07-06 22:50:28 -07:00

9.7 KiB

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

Type: Symbol
Constructor: Symbol(name=None, definition=None, library=None)

  • name: Symbol name (required when loading from multi-symbol library)
  • definition: List of (signal_name, [pad_numbers]) tuples
  • library: Path to KiCad symbol library file

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 as a collection of nets or sub-interfaces.

# Define an interface type
PowerInterface = interface(
    vcc = Net,
    gnd = Net
)

# Create an interface instance
power = PowerInterface("MAIN")  # Creates MAIN_VCC and MAIN_GND nets

# Interfaces can contain other interfaces
SystemInterface = interface(
    power = PowerInterface,
    data = Net,
    clock = Net
)

# Access interface members
system = SystemInterface()
resistor = Component(
    name = "R1",
    footprint = "0402",
    pin_defs = {"1": "1", "2": "2"},
    pins = {
        "1": system.power.vcc,
        "2": system.power.gnd
    }
)

Type: interface
Constructor: interface(**fields)

  • Fields can be Net types, Net instances (templates), or other interface types/instances

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