Class Fields¶
Zuspec follows the Python dataclasses model for declaring classes and class fields. The field type annotation specifies the user-visible type of the field. The initializer (eg field()) captures semantics of the field, such as the direction of an input/output port or whether the field is considered to be a post-initialization constant.
import zuspec.dataclasses as zdc
@zdc.dataclass
class MyComponent(zdc.Component):
clock : zdc.bit = zdc.input()
reset : zdc.bit = zdc.input()
data_out : zdc.u32 = zdc.output()
mem : zdc.Memory[zdc.u32] = zdc.field(size=1024)
Decorators¶
@dataclass¶
The @zdc.dataclass decorator marks a class as a Zuspec type. It extends
Python’s standard @dataclass with Zuspec-specific processing and profile
validation.
@zdc.dataclass
class MyComponent(zdc.Component):
pass
# With profile validation
from zuspec.dataclasses import profiles
@zdc.dataclass(profile=profiles.RetargetableProfile)
class HardwareModel(zdc.Component):
data : zdc.u32 = zdc.field() # Width-annotated required
Profiles control validation rules:
PythonProfile- Permissive, allows standard Python typesRetargetableProfile- Strict, requires width-annotated types for hardware
@process¶
Marks an async method as an always-running process. The process starts automatically when the component’s simulation begins.
@zdc.dataclass
class Worker(zdc.Component):
@zdc.process
async def run(self):
for i in range(10):
await self.wait(zdc.Time.ns(10))
print(f"Iteration {i}")
@sync¶
Marks a synchronous (clocked) process with deferred assignment semantics. The process executes on positive edge of clock or reset.
@zdc.dataclass
class Counter(zdc.Component):
clock : zdc.bit = zdc.input()
reset : zdc.bit = zdc.input()
count : zdc.u32 = zdc.output()
@zdc.sync(clock=lambda s: s.clock, reset=lambda s: s.reset)
def _counter_proc(self):
if self.reset:
self.count = 0
else:
self.count = self.count + 1
Deferred Assignment: In @sync processes, assignments don’t take effect
immediately. The new value is applied after the method completes but before
the next evaluation.
@comb¶
Marks a combinational process with immediate assignment semantics. The process re-evaluates whenever any input changes.
@zdc.dataclass
class Adder(zdc.Component):
a : zdc.u32 = zdc.input()
b : zdc.u32 = zdc.input()
sum : zdc.u32 = zdc.output()
@zdc.comb
def _add(self):
self.sum = self.a + self.b
@invariant¶
Marks a method as a structural invariant that must always hold.
@zdc.dataclass
class Config(zdc.Struct):
x : zdc.u8 = zdc.field(bounds=(0, 100))
y : zdc.u8 = zdc.field(bounds=(0, 100))
@zdc.invariant
def sum_constraint(self) -> bool:
return self.x + self.y <= 150
@constraint¶
Marks a method as a constraint for random variables. See Constraints for complete documentation.
@zdc.dataclass
class Packet:
length: int = zdc.rand(bounds=(64, 1500), default=64)
@zdc.constraint
def min_size(self):
self.length >= 64
@zdc.constraint.generic
def small_packet(self):
self.length < 256
Constraint methods use statement syntax where statements are implicitly ANDed.
See Constraints for details on constraint expressions, helper functions
(implies(), dist(), unique(), solve_order()), and random variables.
Field Specifiers¶
field()¶
The general-purpose field specifier with the following parameters:
zdc.field(
rand=False, # Whether field is randomizable
init=None, # Dict of init values or callable
default_factory=None, # Factory for default value
default=None, # Default value
metadata=None, # Additional metadata dict
size=None, # Size (for Memory fields)
bounds=None, # Value bounds (min, max)
width=None # Width expression for bitv types
)
Example:
@zdc.dataclass
class MyStruct(zdc.Struct):
a : int = zdc.field(default=10)
b : zdc.u8 = zdc.field(bounds=(0, 255))
mem : zdc.Memory[zdc.u32] = zdc.field(size=4096)
const()¶
Marks a field as a post-construction constant (structural type parameter). Used for configuration and parameterization.
@zdc.dataclass
class WishboneInitiator(zdc.Bundle):
DATA_WIDTH : zdc.u32 = zdc.const(default=32)
ADDR_WIDTH : zdc.u32 = zdc.const(default=32)
# Other fields can reference const values
dat_w : zdc.bitv = zdc.output(width=lambda s: s.DATA_WIDTH)
rand()¶
Marks a field as a random variable for constraint-based randomization. See Constraints for complete documentation.
@zdc.dataclass
class Transaction:
# Basic random variable
addr: int = zdc.rand(default=0)
# With value bounds
data: int = zdc.rand(bounds=(0, 255), default=0)
# Random array
buffer: int = zdc.rand(size=16, default=0)
Parameters:
bounds(tuple) -(min, max)value boundsdefault(any) - Default value when not randomizedsize(int) - Array size for vector fieldswidth(int or callable) - Bit width forbitvtypes
Random variables are constrained using @constraint decorated methods.
randc()¶
Marks a field as a random-cyclic variable that cycles through all values before repeating.
@zdc.dataclass
class TestSequence:
# Cycles through 0-15
test_id: int = zdc.randc(bounds=(0, 15), default=0)
@zdc.constraint
def valid_tests(self):
self.test_id < 12 # Only 0-11 are valid
Random-cyclic variables ensure all valid values are generated before repeating.
Parameters are the same as rand(). See Constraints for details.
input()¶
Marks a field as an input port. Input fields see the value of their
bound output with no delay. Supports optional width parameter for
bitv types.
@zdc.dataclass
class Consumer(zdc.Component):
clock : zdc.bit = zdc.input()
data : zdc.u32 = zdc.input()
# Variable-width input with lambda expression
param_data : zdc.bitv = zdc.input(width=lambda s: s.DATA_WIDTH)
Top-level inputs are bound to implicit outputs
Non-top-level inputs must be explicitly bound to outputs
output()¶
Marks a field as an output port. Bound inputs see the current value
with no delay. Supports optional width parameter for bitv types.
@zdc.dataclass
class Producer(zdc.Component):
clock : zdc.bit = zdc.input()
data : zdc.u32 = zdc.output()
# Variable-width output
result : zdc.bitv = zdc.output(width=lambda s: s.RESULT_WIDTH)
bundle()¶
Instantiates a bundle (interface) with declared directionality.
Supports kwargs parameter for passing configuration to the bundle.
@zdc.dataclass
class MyComponent(zdc.Component):
DATA_WIDTH : zdc.u32 = zdc.const(default=32)
bus : WishboneBus = zdc.bundle(
kwargs=lambda s: dict(
DATA_WIDTH=s.DATA_WIDTH,
ADDR_WIDTH=s.DATA_WIDTH))
mirror()¶
Instantiates a bundle with flipped directionality (inputs become outputs and vice versa).
@zdc.dataclass
class Responder(zdc.Component):
bus_mirror : WishboneBus = zdc.mirror()
monitor()¶
Instantiates a bundle for passive monitoring (all signals are inputs).
@zdc.dataclass
class Monitor(zdc.Component):
bus_mon : WishboneBus = zdc.monitor()
port()¶
Marks a field as an API consumer. Ports must be bound to a matching export that provides the interface implementation.
class MemIF(Protocol):
async def read(self, addr: int) -> int: ...
async def write(self, addr: int, data: int): ...
@zdc.dataclass
class Consumer(zdc.Component):
mem : MemIF = zdc.port()
export()¶
Marks a field as an API provider. Exports must be bound to implementations
of the interface methods via __bind__.
@zdc.dataclass
class Provider(zdc.Component):
mem : MemIF = zdc.export()
def __bind__(self): return {
self.mem.read : self.do_read,
self.mem.write : self.do_write,
}
async def do_read(self, addr: int) -> int:
return self._data[addr]
async def do_write(self, addr: int, data: int):
self._data[addr] = data
inst()¶
Marks a field for automatic instance construction based on annotated type.
Supports kwargs for constructor arguments and size for containers.
@zdc.dataclass
class Top(zdc.Component):
# Single instance with kwargs
counter : Counter = zdc.inst(
kwargs=lambda s: dict(WIDTH=s.COUNTER_WIDTH))
# Array of instances
workers : List[Worker] = zdc.inst(
elem_factory=Worker,
size=4)
tuple()¶
Creates a fixed-size tuple field with automatic element construction.
from typing import Tuple
@zdc.dataclass
class RegBank(zdc.Component):
regs : Tuple[zdc.Reg[zdc.u32], ...] = zdc.tuple(
size=8,
elem_factory=lambda: zdc.Reg[zdc.u32]())
Binding Helper¶
bind[Ts,Ti]¶
The bind helper class provides type-safe binding specifications for
inline field bindings.
from typing import Self
@zdc.dataclass
class Parent(zdc.Component):
clock : zdc.bit = zdc.input()
child : ChildComponent = zdc.field(bind=zdc.bind[Self, ChildComponent](
lambda s, f: {
f.clock : s.clock,
}
))
The lambda receives:
s- Handle to parent class (Self)f- Handle to field being bound (ChildComponent)
Returns a dict mapping target fields to source fields.
Execution Types¶
ExecKind¶
Enumeration of execution method kinds:
ExecKind.Comb- Combinational (RTL)ExecKind.Sync- Synchronous (RTL)ExecKind.Proc- Process (behavioral)
Exec / ExecProc / ExecSync / ExecComb¶
Internal marker types for decorated methods:
Exec- Base execution markerExecProc- Process execution marker (from@process)ExecSync- Synchronous execution marker (from@sync)ExecComb- Combinational execution marker (from@comb)
Parameterization Features¶
Zuspec supports powerful parameterization through const fields and lambda expressions. See PARAMETERIZATION_SUMMARY.md for complete details.
Width Expressions¶
Fields can reference const parameters to determine their width dynamically:
@zdc.dataclass
class ParameterizedBus(zdc.Bundle):
DATA_WIDTH : zdc.u32 = zdc.const(default=32)
# Width derived from parameter
data : zdc.bitv = zdc.output(width=lambda s: s.DATA_WIDTH)
# Computed width
byte_en : zdc.bitv = zdc.input(width=lambda s: s.DATA_WIDTH // 8)
Kwargs for Bundle Instantiation¶
Components can pass parameters to nested bundles:
@zdc.dataclass
class SystemComponent(zdc.Component):
BUS_WIDTH : zdc.u32 = zdc.const(default=64)
bus : SystemBus = zdc.bundle(
kwargs=lambda s: dict(
WIDTH=s.BUS_WIDTH,
ADDR_WIDTH=32))
This enables flexible, reusable component designs with compile-time configuration.