mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
c680fa9c52
Signed-off-by: akhilles <akhilvelagapudi@gmail.com>
1367 lines
43 KiB
Plaintext
1367 lines
43 KiB
Plaintext
---
|
||
title: "Zener Language"
|
||
description: "Hardware Description Language for PCB design"
|
||
---
|
||
|
||
# Specification
|
||
|
||
## Overview
|
||
|
||
Zener is a domain-specific language built on top of
|
||
[Starlark](https://github.com/bazelbuild/starlark/blob/master/spec.md) 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](https://github.com/bazelbuild/starlark/blob/master/spec.md) and the
|
||
[starlark-rust types extension](https://github.com/facebook/starlark-rust/blob/main/docs/types.md).
|
||
|
||
## Table of Contents
|
||
|
||
1. [Evaluation Model](#evaluation-model)
|
||
2. [Core Types](#core-types)
|
||
3. [Built-in Functions](#built-in-functions)
|
||
4. [Module System](#module-system)
|
||
5. [Type System](#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:
|
||
```python
|
||
MyFile = Module("./MyFile.zen")
|
||
MyFile(
|
||
name = "MyFile",
|
||
...
|
||
)
|
||
```
|
||
|
||
### Load Resolution
|
||
|
||
The `load()` and `Module()` statements support multiple resolution strategies:
|
||
|
||
```python
|
||
# 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:
|
||
|
||
```python
|
||
# Load from stdlib - these are aliases to builtin physical unit types
|
||
load("@stdlib/units.zen", "Voltage", "Resistance") # Same as builtin.Voltage, builtin.Resistance
|
||
|
||
# Load from KiCad symbols library
|
||
R_symbol = Symbol(library = "@kicad-symbols/Device.kicad_sym", name = "R")
|
||
```
|
||
|
||
**Note**: The `@stdlib/units.zen` module provides convenient aliases to the language's builtin physical unit types. When you import symbols like `Voltage`, `Resistance`, etc. from `units.zen`, you're actually getting references to `builtin.Voltage`, `builtin.Resistance`, etc. The stdlib unit symbols are automatically replaced with their native builtin counterparts during module evaluation for optimal performance.
|
||
|
||
#### Custom Package Aliases
|
||
|
||
You can define custom package aliases or override the defaults in your workspace's `pcb.toml`:
|
||
|
||
```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.
|
||
|
||
```python
|
||
# 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.
|
||
|
||
```python
|
||
# 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.
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
InterfaceName = interface(
|
||
field_name = field_specification,
|
||
__post_init__ = callback_function, # Optional
|
||
)
|
||
```
|
||
|
||
#### Field Types
|
||
|
||
**Net Instances**: Use the provided Net instance as the default template
|
||
```python
|
||
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
|
||
```python
|
||
uart = Uart(TX=Net("UART_TX"), RX=Net("UART_RX"))
|
||
power = Power(NET=Net("VDD"))
|
||
```
|
||
|
||
**field() Specifications**: Enforce type checking with explicit defaults
|
||
```python
|
||
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
|
||
```python
|
||
NET = using(Net("VCC"))
|
||
uart = using(Uart())
|
||
```
|
||
|
||
#### Interface Instantiation
|
||
|
||
```python
|
||
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
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
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.
|
||
|
||
```python
|
||
# 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 Methods**:
|
||
- `module.nets()` - Returns dict mapping net names to lists of connected port tuples
|
||
- `module.components()` - Returns dict mapping component paths to component objects
|
||
- `module.graph()` - Returns CircuitGraph object for path finding and topology validation
|
||
|
||
**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
|
||
|
||
### PhysicalValue
|
||
|
||
Physical values represent electrical quantities with units and tolerances, enabling unit-aware arithmetic and dimensional analysis.
|
||
|
||
```python
|
||
# Create physical values using unit type constructors
|
||
voltage = Voltage("3.3V")
|
||
resistance = Resistance("10kOhm")
|
||
capacitance = Capacitance("100nF")
|
||
|
||
# Unit-aware arithmetic
|
||
power = voltage * voltage / resistance # Result: Power type
|
||
time_constant = resistance * capacitance # Result: Time type
|
||
|
||
# Convert units and add tolerances
|
||
voltage_mv = voltage.with_unit("mV") # Convert to millivolts
|
||
resistor = resistance.with_tolerance("5%") # Add 5% tolerance
|
||
updated = voltage.with_value(5) # Change to 5V, keep unit and tolerance
|
||
|
||
# Parse from strings with automatic unit detection
|
||
freq = Frequency("100MHz")
|
||
temp = Temperature("25°C")
|
||
|
||
# Single argument constructors
|
||
voltage_5v = Voltage(5) # 5V (numeric)
|
||
resistance_1k = Resistance("1k") # 1kΩ (string with unit)
|
||
|
||
# Dimensionless values and casting
|
||
dimensionless = voltage.with_unit(None) # Remove units
|
||
new_current = Current(dimensionless) # Cast dimensionless to current
|
||
|
||
# Comparisons
|
||
check(voltage > Voltage("2V"), "Voltage too low") # Compare with string
|
||
check(resistance == "10kOhm", "Wrong resistance") # Equality with string
|
||
sorted_voltages = sorted([v1, v2, v3]) # Sort physical values
|
||
```
|
||
|
||
**Type**: `PhysicalValue`
|
||
|
||
**Available Unit Types**:
|
||
- `Voltage` - electrical potential (V, mV, kV)
|
||
- `Current` - electrical current (A, mA, µA)
|
||
- `Resistance` - electrical resistance (Ω, kΩ, MΩ)
|
||
- `Capacitance` - electrical capacitance (F, µF, nF, pF)
|
||
- `Inductance` - electrical inductance (H, mH, µH)
|
||
- `Frequency` - frequency (Hz, kHz, MHz, GHz)
|
||
- `Time` - time duration (s, ms, µs, ns)
|
||
- `Temperature` - temperature (°C, °F, K)
|
||
- `Charge` - electrical charge (C, mC, µC)
|
||
- `Power` - electrical power (W, mW, µW)
|
||
- `Energy` - energy (J, Wh, kWh)
|
||
- `Conductance` - electrical conductance (S)
|
||
- `MagneticFlux` - magnetic flux (Wb)
|
||
|
||
**Constructor**: `UnitType(value, tolerance=None)` or `UnitType(value_only)`
|
||
|
||
- `value`: String with value and unit (e.g., "3.3V", "10kΩ"), numeric value, or existing PhysicalValue
|
||
- `tolerance`: Optional tolerance specification (e.g., "5%", "±0.1V")
|
||
|
||
**Single Argument Constructor**:
|
||
- `UnitType(5)` - Creates value with numeric 5 in the unit type (e.g., `Voltage(5)` = 5V)
|
||
- `UnitType("5V")` - Parses string with unit (e.g., `Voltage("5V")` = 5V)
|
||
- `UnitType(other_physical_value)` - Converts between compatible units or casts dimensionless values
|
||
|
||
**Methods**:
|
||
- `with_unit(unit)` - Convert to specified unit, returns new PhysicalValue
|
||
- `with_unit(None)` - Convert to dimensionless value, returns new PhysicalValue
|
||
- `with_tolerance(tolerance)` - Add/modify tolerance, returns new PhysicalValue
|
||
- `with_value(value)` - Update the numeric value while preserving unit and tolerance, returns new PhysicalValue
|
||
|
||
**Unit-Aware Operations**:
|
||
- Arithmetic operations automatically handle unit conversions and dimensional analysis
|
||
- Compatible units are automatically converted (e.g., V + mV = V)
|
||
- Dimensional analysis creates appropriate result types (e.g., V/A = Ω, R×C = s)
|
||
- Supports all electrical relationships: Ohm's law (V=I×R), power (P=V×I), time constants (τ=RC), etc.
|
||
- Invalid operations (e.g., V + Hz) produce dimensionless results
|
||
- Physical values can be mixed with strings and numbers in arithmetic operations
|
||
|
||
**Comparisons**:
|
||
- Physical values support equality (`==`, `!=`) and ordering (`<`, `>`, `<=`, `>=`) operations
|
||
- Units must match for comparisons, except dimensionless values can compare with any unit
|
||
- Tolerance is ignored in equality comparisons - only value and unit matter
|
||
- Can compare with strings (e.g., `voltage == "5V"`) if they represent compatible physical values
|
||
|
||
**Dimensionless Values**:
|
||
- Dimensionless physical values can be cast to any unit type
|
||
- Use `with_unit(None)` to convert any physical value to dimensionless
|
||
- Dimensionless results are produced by unit cancellation (e.g., R×G = 1, V/V = 1)
|
||
- Numeric literals are treated as dimensionless when mixed with physical values
|
||
|
||
**String Representation**:
|
||
Physical values maintain lossless string conversion, preserving original units and tolerances.
|
||
|
||
### MetadataContainer
|
||
|
||
Metadata containers provide typed, persistent storage that works across module boundaries. Values are validated against the container's declared type and can be accumulated over time.
|
||
|
||
```python
|
||
# Create typed metadata containers
|
||
voltage_log = metadata(Voltage)
|
||
error_log = metadata(str)
|
||
measurements = metadata(float)
|
||
|
||
# Store values with type checking
|
||
voltage_log.push(Voltage("3.3V")) # Valid
|
||
voltage_log.push("invalid") # Error: type mismatch
|
||
|
||
# Retrieve values
|
||
latest = voltage_log.get() # Most recent value or None
|
||
all_values = voltage_log.list() # All values as list
|
||
```
|
||
|
||
**Type**: `MetadataContainer`
|
||
**Created by**: `metadata()` function
|
||
|
||
**Methods**:
|
||
- `push(value)` - Add a value (must match container's type)
|
||
- `get()` - Get the most recent value, or `None` if empty
|
||
- `list()` - Get all values as a list in chronological order
|
||
|
||
**Supported Types**:
|
||
- Basic types: `str`, `int`, `float`, `bool`, `list`, `dict`
|
||
- Physical values: `Voltage`, `Current`, `Resistance`, etc.
|
||
- Not supported: enums, records, custom types
|
||
|
||
### 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.
|
||
|
||
```python
|
||
# 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](#testbenchname-module-checks))
|
||
|
||
Properties accessible via the TestBench value:
|
||
- `name`: The test bench identifier
|
||
- Module evaluation status
|
||
- Check function results
|
||
|
||
## Built-in Functions
|
||
|
||
### metadata(type)
|
||
|
||
Creates a typed metadata container for accumulating values across module boundaries.
|
||
|
||
**Parameters**:
|
||
- `type`: Type constructor (e.g., `str`, `Voltage`, `float`)
|
||
|
||
**Returns**: `MetadataContainer` with the specified type
|
||
|
||
```python
|
||
# Create containers for different types
|
||
voltage_log = metadata(Voltage)
|
||
message_log = metadata(str)
|
||
readings = metadata(float)
|
||
|
||
# Use the container methods
|
||
voltage_log.push(Voltage("5V"))
|
||
latest_reading = voltage_log.get()
|
||
all_readings = voltage_log.list()
|
||
```
|
||
|
||
**Supported Types**: Basic types (`str`, `int`, `float`, `bool`, `list`, `dict`) and PhysicalValue types. Enums and records are not supported.
|
||
|
||
### 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:
|
||
|
||
```python
|
||
# 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")
|
||
```
|
||
|
||
```python
|
||
# 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, default=None, optional=False)
|
||
|
||
Declares a net or interface input for a module.
|
||
|
||
```python
|
||
# 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.
|
||
|
||
```python
|
||
# 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.
|
||
|
||
```python
|
||
# 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.
|
||
|
||
```python
|
||
# 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)
|
||
|
||
Raises a runtime error with the given message.
|
||
|
||
```python
|
||
if not condition:
|
||
error("Condition failed")
|
||
```
|
||
|
||
### check(condition, msg)
|
||
|
||
Checks a condition and raises an error if false.
|
||
|
||
```python
|
||
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.
|
||
|
||
```python
|
||
add_property("layout_group", "power_supply")
|
||
add_property("critical", True)
|
||
```
|
||
|
||
### 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.
|
||
|
||
## 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:**
|
||
|
||
```python
|
||
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, config, layout_path, layout_hints=None, default=False)
|
||
|
||
**Stdlib function** for declaring board configurations for PCB layout generation.
|
||
|
||
```python
|
||
# Load from stdlib
|
||
load("@stdlib/board_config.zen", "Board", "BoardConfig", "DesignRules", "Stackup", "Material", "CopperLayer", "DielectricLayer")
|
||
|
||
# Define board 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 stdlib function
|
||
Board(
|
||
name="MyBoard",
|
||
config=config,
|
||
layout_path="layout/",
|
||
default=True
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `name`: String identifier for the board configuration
|
||
- `config`: `BoardConfig` record containing design rules and stackup
|
||
- `layout_path`: Directory path where layout files will be generated
|
||
- `layout_hints`: Optional list of layout optimization hints
|
||
- `default`: If True, this becomes the default board config for the project
|
||
|
||
**Implementation:** The stdlib `Board()` function calls the language builtin `builtin.add_board_config()` and sets layout properties.
|
||
|
||
**BoardConfig Structure:**
|
||
|
||
```python
|
||
BoardConfig = record(
|
||
design_rules=DesignRules, # PCB design rules and constraints
|
||
stackup=Stackup, # Layer stackup and materials
|
||
)
|
||
```
|
||
|
||
**Design Rules Examples:**
|
||
|
||
```python
|
||
# 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:**
|
||
|
||
```python
|
||
# 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:
|
||
|
||
```python
|
||
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, test_cases, checks)
|
||
|
||
Creates a test bench for validating module behavior across multiple test cases with parametric inputs.
|
||
|
||
```python
|
||
# Load a module to test
|
||
MyCircuit = Module("./my_circuit.zen")
|
||
|
||
# Define check functions that receive (module, inputs)
|
||
def verify_power_connections(module, inputs):
|
||
"""Ensure all power pins are connected to VCC"""
|
||
nets = module.nets()
|
||
vcc_connections = nets.get("VCC", [])
|
||
check(len(vcc_connections) >= 2, "Need at least 2 VCC connections")
|
||
|
||
def verify_ground_plane(module, inputs):
|
||
"""Check for proper ground connections"""
|
||
nets = module.nets()
|
||
check("GND" in nets, "Missing GND net")
|
||
check(len(nets["GND"]) > 3, "GND net needs more than 3 connections")
|
||
|
||
# Create test bench with multiple test cases
|
||
TestBench(
|
||
name = "PowerTest",
|
||
module = MyCircuit,
|
||
test_cases = {
|
||
"case_3v3": {"voltage": "3.3V", "current": "1A"},
|
||
"case_5v": {"voltage": "5V", "current": "2A"},
|
||
},
|
||
checks = [verify_power_connections, verify_ground_plane]
|
||
)
|
||
```
|
||
|
||
**Parameters:**
|
||
|
||
- `name`: String identifier for the test bench
|
||
- `module`: ModuleLoader to test (created with `Module()`)
|
||
- `test_cases`: Dict mapping case names to input parameter dicts
|
||
- `checks`: Optional list of check functions to execute for each test case
|
||
|
||
**Check Function Signature:**
|
||
|
||
Check functions receive two arguments: the evaluated module and the test case inputs:
|
||
|
||
```python
|
||
def check_function(module: Module, inputs: dict):
|
||
# Access circuit data by calling Module methods
|
||
nets = module.nets() # Returns dict of net connections
|
||
components = module.components() # Returns dict of component objects
|
||
graph = module.graph() # Returns CircuitGraph object
|
||
|
||
# Access test case parameters
|
||
voltage = inputs["voltage"]
|
||
current = inputs["current"]
|
||
|
||
# Access components via indexing
|
||
component = module["ComponentName"]
|
||
submodule = module["SubmoduleName"]
|
||
nested = module["SubmoduleName"]["ComponentName"]
|
||
|
||
# Perform validation - signal failures using check() or error()
|
||
check(condition, "Failure message")
|
||
```
|
||
|
||
**Module Methods:**
|
||
|
||
- `module.nets()` - Returns dict mapping net names to lists of connected port tuples
|
||
- Example: `{"VCC": [("U1", "VDD"), ("C1", "P1")]}`
|
||
- `module.components()` - Returns dict mapping component paths to component objects
|
||
- Example: `{"U1.IC": <Component>, "C1.C": <Component>}`
|
||
- `module.graph()` - Returns CircuitGraph for path finding and topology validation
|
||
- `module[name]` - Direct indexing access to child components and submodules
|
||
|
||
**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 once for each test case with the provided inputs
|
||
- Executes all check functions for each successful module evaluation
|
||
- Reports failures as diagnostics with precise source location
|
||
- Prints a summary message showing passed/failed checks across all test cases
|
||
- Returns a TestBench value containing results
|
||
|
||
**Example with Multiple Test Cases:**
|
||
|
||
```python
|
||
MyModule = Module("./voltage_regulator.zen")
|
||
|
||
def check_output_voltage(module, inputs):
|
||
"""Verify output voltage configuration"""
|
||
components = module.components()
|
||
regulator = components.get("U1.REG")
|
||
|
||
expected_voltage = inputs["output_voltage"]
|
||
check(regulator is not None, "Regulator component not found")
|
||
|
||
print(f"✓ Output voltage configured for {expected_voltage}")
|
||
|
||
def check_bypass_capacitors(module, inputs):
|
||
"""Verify proper decoupling"""
|
||
graph = module.graph()
|
||
|
||
# Find paths from VIN to GND through capacitors
|
||
vin_paths = graph.paths(start=("U1", "VIN"), end="GND_GND")
|
||
check(len(vin_paths) > 0, "No bypass capacitor found on VIN")
|
||
|
||
print(f"✓ Found {len(vin_paths)} bypass capacitor paths")
|
||
|
||
def check_component_existence(module, inputs):
|
||
"""Verify required components are present"""
|
||
if "U1" in module:
|
||
regulator = module["U1"]
|
||
print(f"✓ Found regulator: {regulator}")
|
||
else:
|
||
error("Regulator not found")
|
||
|
||
TestBench(
|
||
name = "VoltageRegulatorTest",
|
||
module = MyModule,
|
||
test_cases = {
|
||
"3v3_config": {
|
||
"output_voltage": "3.3V",
|
||
"input_voltage": "5V",
|
||
},
|
||
"5v_config": {
|
||
"output_voltage": "5V",
|
||
"input_voltage": "12V",
|
||
},
|
||
},
|
||
checks = [
|
||
check_output_voltage,
|
||
check_bypass_capacitors,
|
||
check_component_existence,
|
||
]
|
||
)
|
||
```
|
||
|
||
**Named Check Functions:**
|
||
|
||
You can provide custom names for check functions using tuples:
|
||
|
||
```python
|
||
TestBench(
|
||
name = "MyTest",
|
||
module = MyModule,
|
||
test_cases = {"default": {}},
|
||
checks = [
|
||
("Power Check", check_power),
|
||
("Ground Check", check_ground),
|
||
]
|
||
)
|
||
```
|
||
|
||
## 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:
|
||
|
||
```python
|
||
def analyze_circuit(module, inputs):
|
||
# Get the circuit graph from the 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
|
||
```python
|
||
("ComponentName", "PinName") # e.g., ("TPS82140", "VIN"), ("STM32", "PA0")
|
||
```
|
||
|
||
**External Nets**: Public nets exposed by the module's io() declarations
|
||
```python
|
||
"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:
|
||
|
||
```python
|
||
def validate_power_supply(module, inputs):
|
||
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:
|
||
|
||
```python
|
||
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
|
||
|
||
```python
|
||
# 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:
|
||
|
||
```python
|
||
def validate_filter_topology(module, inputs):
|
||
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**
|
||
```python
|
||
def validate_vin_decoupling(module, inputs):
|
||
"""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**
|
||
```python
|
||
def validate_feedback_divider(module, inputs):
|
||
"""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**
|
||
```python
|
||
def validate_bootstrap_cap(module, inputs):
|
||
"""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
|
||
```python
|
||
# "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
|
||
```python
|
||
# "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
|
||
```python
|
||
# "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
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
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
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
# 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"
|
||
)
|
||
```
|