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

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)

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)
  • Executes each check function in order
  • Reports failures as diagnostics with precise source location pointing to the failing check() call
  • Prints a success message if all checks pass
  • Returns a TestBench value containing results
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"
)