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
Evaluation Model
Files as Modules
Each .zen file is a Starlark module. It can be used in two ways:
- Its exported symbols can be
load()ed into other modules. For example,load("./MyFile.zen", "MyFunction", "MyType")will load theMyFunctionandMyTypesymbols from theMyFile.zenmodule. - It can be loaded as a schematic module using the
Module()helper. For example,MyFile = Module("./MyFile.zen")will importMyFile.zenas 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]) tupleslibrary: 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 numbertype: Component typeproperties: 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
Nettypes,Netinstances (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 inputtype: Expected type (Netor interface type)default: Default value if not provided by parentoptional: 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 inputtype: Expected type (str, int, float, bool, enum, or record type)default: Default value if not providedconvert: Optional conversion functionoptional: 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"
)