ir: kill Fragment.ports

This commit is contained in:
Wanda 2024-02-11 12:07:45 +01:00 committed by Catherine
parent a725282751
commit 751e0f4b57
9 changed files with 576 additions and 666 deletions

View file

@ -401,7 +401,7 @@ class ModuleEmitter:
port_id=port_id, port_kind=flow.value, port_id=port_id, port_kind=flow.value,
name=name, attrs=self.value_attrs.get(value, {})) name=name, attrs=self.value_attrs.get(value, {}))
self.sigport_wires[name] = (wire, value) self.sigport_wires[name] = (wire, value)
if flow == _nir.ModuleNetFlow.OUTPUT: if flow == _nir.ModuleNetFlow.Output:
continue continue
# If we just emitted an input or inout port, it is driving the value. # If we just emitted an input or inout port, it is driving the value.
self.driven_sigports.add(name) self.driven_sigports.add(name)
@ -463,7 +463,7 @@ class ModuleEmitter:
for submodule_idx in self.module.submodules: for submodule_idx in self.module.submodules:
submodule = self.netlist.modules[submodule_idx] submodule = self.netlist.modules[submodule_idx]
for _name, (value, flow) in submodule.ports.items(): for _name, (value, flow) in submodule.ports.items():
if flow == _nir.ModuleNetFlow.OUTPUT: if flow == _nir.ModuleNetFlow.Output:
self.emit_driven_wire(value) self.emit_driven_wire(value)
def sigspec(self, *parts: '_nir.Net | Iterable[_nir.Net]'): def sigspec(self, *parts: '_nir.Net | Iterable[_nir.Net]'):
@ -989,10 +989,10 @@ class EmptyModuleChecker:
return module_idx in self.empty return module_idx in self.empty
def convert_fragment(fragment, name="top", *, emit_src=True): def convert_fragment(fragment, ports, name="top", *, emit_src=True, **kwargs):
assert isinstance(fragment, _ir.Fragment) assert isinstance(fragment, _ir.Fragment)
name_map = _ast.SignalDict() name_map = _ast.SignalDict()
netlist = _ir.build_netlist(fragment, name=name) netlist = _ir.build_netlist(fragment, ports=ports, name=name, **kwargs)
empty_checker = EmptyModuleChecker(netlist) empty_checker = EmptyModuleChecker(netlist)
builder = _Builder(emit_src=emit_src) builder = _Builder(emit_src=emit_src)
for module_idx, module in enumerate(netlist.modules): for module_idx, module in enumerate(netlist.modules):
@ -1011,14 +1011,18 @@ def convert(elaboratable, name="top", platform=None, *, ports=None, emit_src=Tru
if (ports is None and if (ports is None and
hasattr(elaboratable, "signature") and hasattr(elaboratable, "signature") and
isinstance(elaboratable.signature, wiring.Signature)): isinstance(elaboratable.signature, wiring.Signature)):
ports = [] ports = {}
for _path, _member, value in elaboratable.signature.flatten(elaboratable): for path, member, value in elaboratable.signature.flatten(elaboratable):
if isinstance(value, _ast.ValueCastable): if isinstance(value, _ast.ValueCastable):
value = value.as_value() value = value.as_value()
if isinstance(value, _ast.Value): if isinstance(value, _ast.Value):
ports.append(value) if member.flow == wiring.In:
dir = _ir.PortDirection.Input
else:
dir = _ir.PortDirection.Output
ports["__".join(path)] = (value, dir)
elif ports is None: elif ports is None:
raise TypeError("The `convert()` function requires a `ports=` argument") raise TypeError("The `convert()` function requires a `ports=` argument")
fragment = _ir.Fragment.get(elaboratable, platform).prepare(ports=ports, **kwargs) fragment = _ir.Fragment.get(elaboratable, platform)
il_text, _name_map = convert_fragment(fragment, name, emit_src=emit_src) il_text, _name_map = convert_fragment(fragment, ports, name, emit_src=emit_src, **kwargs)
return il_text return il_text

View file

@ -45,14 +45,18 @@ def convert(elaboratable, name="top", platform=None, *, ports=None, emit_src=Tru
if (ports is None and if (ports is None and
hasattr(elaboratable, "signature") and hasattr(elaboratable, "signature") and
isinstance(elaboratable.signature, wiring.Signature)): isinstance(elaboratable.signature, wiring.Signature)):
ports = [] ports = {}
for path, member, value in elaboratable.signature.flatten(elaboratable): for path, member, value in elaboratable.signature.flatten(elaboratable):
if isinstance(value, _ast.ValueCastable): if isinstance(value, _ast.ValueCastable):
value = value.as_value() value = value.as_value()
if isinstance(value, _ast.Value): if isinstance(value, _ast.Value):
ports.append(value) if member.flow == wiring.In:
dir = _ir.PortDirection.Input
else:
dir = _ir.PortDirection.Output
ports["__".join(path)] = (value, dir)
elif ports is None: elif ports is None:
raise TypeError("The `convert()` function requires a `ports=` argument") raise TypeError("The `convert()` function requires a `ports=` argument")
fragment = _ir.Fragment.get(elaboratable, platform).prepare(ports=ports, **kwargs) fragment = _ir.Fragment.get(elaboratable, platform)
verilog_text, name_map = convert_fragment(fragment, name, emit_src=emit_src, strip_internal_attrs=strip_internal_attrs) verilog_text, name_map = convert_fragment(fragment, ports, name, emit_src=emit_src, strip_internal_attrs=strip_internal_attrs, **kwargs)
return verilog_text return verilog_text

View file

@ -163,11 +163,11 @@ class Platform(ResourceManager, metaclass=ABCMeta):
if pin.dir == "io": if pin.dir == "io":
add_pin_fragment(pin, self.get_diff_input_output(pin, port, attrs, invert)) add_pin_fragment(pin, self.get_diff_input_output(pin, port, attrs, invert))
fragment._propagate_ports(ports=self.iter_ports(), all_undef_as_ports=False) ports = list(self.iter_ports())
return self.toolchain_prepare(fragment, name, **kwargs) return self.toolchain_prepare(fragment, ports, name, **kwargs)
@abstractmethod @abstractmethod
def toolchain_prepare(self, fragment, name, **kwargs): def toolchain_prepare(self, fragment, ports, name, **kwargs):
""" """
Convert the ``fragment`` and constraints recorded in this :class:`Platform` into Convert the ``fragment`` and constraints recorded in this :class:`Platform` into
a :class:`BuildPlan`. a :class:`BuildPlan`.
@ -290,7 +290,7 @@ class TemplatedPlatform(Platform):
continue continue
yield net_signal, port_signal, frequency yield net_signal, port_signal, frequency
def toolchain_prepare(self, fragment, name, *, emit_src=True, **kwargs): def toolchain_prepare(self, fragment, ports, name, *, emit_src=True, **kwargs):
# Restrict the name of the design to a strict alphanumeric character set. Platforms will # Restrict the name of the design to a strict alphanumeric character set. Platforms will
# interpolate the name of the design in many different contexts: filesystem paths, Python # interpolate the name of the design in many different contexts: filesystem paths, Python
# scripts, Tcl scripts, ad-hoc constraint files, and so on. It is not practical to add # scripts, Tcl scripts, ad-hoc constraint files, and so on. It is not practical to add
@ -306,7 +306,8 @@ class TemplatedPlatform(Platform):
# and to incorporate the Amaranth version into generated code. # and to incorporate the Amaranth version into generated code.
autogenerated = f"Automatically generated by Amaranth {__version__}. Do not edit." autogenerated = f"Automatically generated by Amaranth {__version__}. Do not edit."
rtlil_text, self._name_map = rtlil.convert_fragment(fragment, name=name, emit_src=emit_src) rtlil_text, self._name_map = rtlil.convert_fragment(fragment, ports=ports, name=name,
emit_src=emit_src)
# Retrieve an override specified in either the environment or as a kwarg. # Retrieve an override specified in either the environment or as a kwarg.
# expected_type parameter is used to assert the type of kwargs, passing `None` will disable # expected_type parameter is used to assert the type of kwargs, passing `None` will disable

View file

@ -1,6 +1,7 @@
from typing import Tuple from typing import Tuple
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
from functools import reduce from functools import reduce
import enum
import warnings import warnings
from .._utils import flatten, memoize from .._utils import flatten, memoize
@ -8,7 +9,10 @@ from .. import tracer, _unused
from . import _ast, _cd, _ir, _nir from . import _ast, _cd, _ir, _nir
__all__ = ["UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "Instance"] __all__ = [
"UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "Instance",
"PortDirection", "Design", "build_netlist",
]
class UnusedElaboratable(_unused.UnusedMustUse): class UnusedElaboratable(_unused.UnusedMustUse):
@ -65,7 +69,6 @@ class Fragment:
obj = new_obj obj = new_obj
def __init__(self, *, src_loc=None): def __init__(self, *, src_loc=None):
self.ports = _ast.SignalDict()
self.drivers = OrderedDict() self.drivers = OrderedDict()
self.statements = {} self.statements = {}
self.domains = OrderedDict() self.domains = OrderedDict()
@ -76,19 +79,6 @@ class Fragment:
self.src_loc = src_loc self.src_loc = src_loc
self.origins = None self.origins = None
def add_ports(self, *ports, dir):
assert dir in ("i", "o", "io")
for port in flatten(ports):
self.ports[port] = dir
def iter_ports(self, dir=None):
if dir is None:
yield from self.ports
else:
for port, port_dir in self.ports.items():
if port_dir == dir:
yield port
def add_driver(self, signal, domain="comb"): def add_driver(self, signal, domain="comb"):
assert isinstance(domain, str) assert isinstance(domain, str)
if domain not in self.drivers: if domain not in self.drivers:
@ -111,18 +101,6 @@ class Fragment:
for signal in signals: for signal in signals:
yield domain, signal yield domain, signal
def iter_signals(self):
signals = _ast.SignalSet()
signals |= self.ports.keys()
for domain, domain_signals in self.drivers.items():
if domain != "comb":
cd = self.domains[domain]
signals.add(cd.clk)
if cd.rst is not None:
signals.add(cd.rst)
signals |= domain_signals
return signals
def add_domains(self, *domains): def add_domains(self, *domains):
for domain in flatten(domains): for domain in flatten(domains):
assert isinstance(domain, _cd.ClockDomain) assert isinstance(domain, _cd.ClockDomain)
@ -169,7 +147,6 @@ class Fragment:
# Merge subfragment's everything except clock domains into this fragment. # Merge subfragment's everything except clock domains into this fragment.
# Flattening is done after clock domain propagation, so we can assume the domains # Flattening is done after clock domain propagation, so we can assume the domains
# are already the same in every involved fragment in the first place. # are already the same in every involved fragment in the first place.
self.ports.update(subfragment.ports)
for domain, signal in subfragment.iter_drivers(): for domain, signal in subfragment.iter_drivers():
self.add_driver(signal, domain) self.add_driver(signal, domain)
for domain, statements in subfragment.statements.items(): for domain, statements in subfragment.statements.items():
@ -372,277 +349,78 @@ class Fragment:
self._propagate_domains_down() self._propagate_domains_down()
return new_domains return new_domains
def _prepare_use_def_graph(self, parent, level, uses, defs, ios, top): def _prepare_ports(self, ports):
from ._mem import MemoryInstance # Normalize ports to a list.
new_ports = []
def add_uses(*sigs, self=self): if isinstance(ports, dict):
for sig in flatten(sigs): for port_name, (signal, dir) in ports.items():
if sig not in uses: new_ports.append((port_name, signal, dir))
uses[sig] = set() elif isinstance(ports, (list, tuple)):
uses[sig].add(self) for port in ports:
if isinstance(port, tuple):
def add_defs(*sigs): port_name, signal, dir = port
for sig in flatten(sigs): new_ports.append((port_name, signal, dir))
if sig not in defs:
defs[sig] = self
else: else:
assert defs[sig] is self new_ports.append((None, port, None))
else:
msg = f"`ports` must be a dict, a list or a tuple, not {ports!r}"
if isinstance(ports, _ast.Value):
msg += " (did you mean `ports=(<signal>,)`, rather than `ports=<signal>`?)"
raise TypeError(msg)
def add_io(*sigs): # Validate ports.
for sig in flatten(sigs): prenamed_ports = set()
if sig not in ios: for (port_name, signal, dir) in new_ports:
ios[sig] = self if isinstance(port_name, str):
if port_name in prenamed_ports:
raise TypeError(f"Duplicate port name {port_name!r}")
else: else:
assert ios[sig] is self prenamed_ports.add(port_name)
elif port_name is not None:
raise TypeError(f"Port name must be a string, not {port_name!r}")
if dir is not None and not isinstance(dir, PortDirection):
raise TypeError(
f"Port direction must be a `PortDirection` instance or None, not {dir!r}")
if not isinstance(signal, (_ast.Signal, _ast.ClockSignal, _ast.ResetSignal)):
raise TypeError(f"Only signals may be added as ports, not {signal!r}")
# Collect all signals we're driving (on LHS of statements), and signals we're using return new_ports
# (on RHS of statements, or in clock domains).
for stmts in self.statements.values():
for stmt in stmts:
add_uses(stmt._rhs_signals())
add_defs(stmt._lhs_signals())
for domain, _ in self.iter_sync(): def prepare(self, ports=(), *, hierarchy=("top",), legalize_assignments=False, missing_domain=lambda name: _cd.ClockDomain(name)):
cd = self.domains[domain]
add_uses(cd.clk)
if cd.rst is not None:
add_uses(cd.rst)
# Repeat for subfragments.
for subfrag, name, src_loc in self.subfragments:
if isinstance(subfrag, Instance):
for port_name, (value, dir) in subfrag.named_ports.items():
if dir == "i":
# Prioritize defs over uses.
rhs_without_outputs = value._rhs_signals() - subfrag.iter_ports(dir="o")
subfrag.add_ports(rhs_without_outputs, dir=dir)
add_uses(value._rhs_signals())
if dir == "o":
subfrag.add_ports(value._lhs_signals(), dir=dir)
add_defs(value._lhs_signals())
if dir == "io":
subfrag.add_ports(value._lhs_signals(), dir=dir)
add_io(value._lhs_signals())
elif isinstance(subfrag, MemoryInstance):
for port in subfrag._read_ports:
subfrag.add_ports(port._data._lhs_signals(), dir="o")
add_defs(port._data._lhs_signals())
for value in [port._addr, port._en]:
subfrag.add_ports(value._rhs_signals(), dir="i")
add_uses(value._rhs_signals())
for port in subfrag._write_ports:
for value in [port._addr, port._en, port._data]:
subfrag.add_ports(value._rhs_signals(), dir="i")
add_uses(value._rhs_signals())
for domain, _ in subfrag.iter_sync():
cd = subfrag.domains[domain]
add_uses(cd.clk)
if cd.rst is not None:
add_uses(cd.rst)
else:
parent[subfrag] = self
level [subfrag] = level[self] + 1
subfrag._prepare_use_def_graph(parent, level, uses, defs, ios, top)
def _propagate_ports(self, ports, all_undef_as_ports):
# Take this fragment graph:
#
# __ B (def: q, use: p r)
# /
# A (def: p, use: q r)
# \
# \_ C (def: r, use: p q)
#
# We need to consider three cases.
# 1. Signal p requires an input port in B;
# 2. Signal r requires an output port in C;
# 3. Signal r requires an output port in C and an input port in B.
#
# Adding these ports can be in general done in three steps for each signal:
# 1. Find the least common ancestor of all uses and defs.
# 2. Going upwards from the single def, add output ports.
# 3. Going upwards from all uses, add input ports.
parent = {self: None}
level = {self: 0}
uses = _ast.SignalDict()
defs = _ast.SignalDict()
ios = _ast.SignalDict()
self._prepare_use_def_graph(parent, level, uses, defs, ios, self)
ports = _ast.SignalSet(ports)
if all_undef_as_ports:
for sig in uses:
if sig in defs:
continue
ports.add(sig)
for sig in ports:
if sig not in uses:
uses[sig] = set()
uses[sig].add(self)
@memoize
def lca_of(fragu, fragv):
# Normalize fragu to be deeper than fragv.
if level[fragu] < level[fragv]:
fragu, fragv = fragv, fragu
# Find ancestor of fragu on the same level as fragv.
for _ in range(level[fragu] - level[fragv]):
fragu = parent[fragu]
# If fragv was the ancestor of fragv, we're done.
if fragu == fragv:
return fragu
# Otherwise, they are at the same level but in different branches. Step both fragu
# and fragv until we find the common ancestor.
while parent[fragu] != parent[fragv]:
fragu = parent[fragu]
fragv = parent[fragv]
return parent[fragu]
for sig in uses:
if sig in defs:
lca = reduce(lca_of, uses[sig], defs[sig])
else:
lca = reduce(lca_of, uses[sig])
for frag in uses[sig]:
if sig in defs and frag is defs[sig]:
continue
while frag != lca:
frag.add_ports(sig, dir="i")
frag = parent[frag]
if sig in defs:
frag = defs[sig]
while frag != lca:
frag.add_ports(sig, dir="o")
frag = parent[frag]
for sig in ios:
frag = ios[sig]
while frag is not None:
frag.add_ports(sig, dir="io")
frag = parent[frag]
for sig in ports:
if sig in ios:
continue
if sig in defs:
self.add_ports(sig, dir="o")
else:
self.add_ports(sig, dir="i")
def prepare(self, ports=None, missing_domain=lambda name: _cd.ClockDomain(name)):
from ._xfrm import DomainLowerer from ._xfrm import DomainLowerer
ports = self._prepare_ports(ports)
new_domains = self._propagate_domains(missing_domain) new_domains = self._propagate_domains(missing_domain)
for domain in new_domains:
ports.append((None, domain.clk, PortDirection.Input))
if domain.rst is not None:
ports.append((None, domain.rst, PortDirection.Input))
def resolve_signal(signal):
if isinstance(signal, _ast.ClockSignal):
domain = self.domains[signal.domain]
return domain.clk
elif isinstance(signal, _ast.ResetSignal):
domain = self.domains[signal.domain]
if domain.rst is None:
raise ValueError(f"Using ResetSignal for a reset-less domain {signal.domain}")
return domain.rst
else:
return signal
ports = [
(name, resolve_signal(signal), dir)
for name, signal, dir in ports
]
fragment = DomainLowerer()(self) fragment = DomainLowerer()(self)
if ports is None: if legalize_assignments:
fragment._propagate_ports(ports=(), all_undef_as_ports=True) from ._xfrm import AssignmentLegalizer
else: fragment = AssignmentLegalizer()(fragment)
if not isinstance(ports, tuple) and not isinstance(ports, list):
msg = "`ports` must be either a list or a tuple, not {!r}"\
.format(ports)
if isinstance(ports, _ast.Value):
msg += " (did you mean `ports=(<signal>,)`, rather than `ports=<signal>`?)"
raise TypeError(msg)
mapped_ports = []
# Lower late bound signals like ClockSignal() to ports.
port_lowerer = DomainLowerer(fragment.domains)
for port in ports:
if not isinstance(port, (_ast.Signal, _ast.ClockSignal, _ast.ResetSignal)):
raise TypeError("Only signals may be added as ports, not {!r}"
.format(port))
mapped_ports.append(port_lowerer.on_value(port))
# Add ports for all newly created missing clock domains, since not doing so defeats
# the purpose of domain auto-creation. (It's possible to refer to these ports before
# the domain actually exists through late binding, but it's inconvenient.)
for cd in new_domains:
mapped_ports.append(cd.clk)
if cd.rst is not None:
mapped_ports.append(cd.rst)
fragment._propagate_ports(ports=mapped_ports, all_undef_as_ports=False)
return fragment
def _assign_names_to_signals(self): # Create design and let it do the rest.
"""Assign names to signals used in this fragment. return Design(fragment, ports, hierarchy=hierarchy)
Returns
-------
SignalDict of Signal to str
A mapping from signals used in this fragment to their local names. Because names are
deduplicated using local information only, the same signal used in a different fragment
may get a different name.
"""
signal_names = _ast.SignalDict()
assigned_names = set()
def add_signal_name(signal):
if signal not in signal_names:
if signal.name not in assigned_names:
name = signal.name
else:
name = f"{signal.name}${len(assigned_names)}"
assert name not in assigned_names
signal_names[signal] = name
assigned_names.add(name)
for port in self.ports.keys():
add_signal_name(port)
for domain_name, domain_signals in self.drivers.items():
if domain_name != "comb":
domain = self.domains[domain_name]
add_signal_name(domain.clk)
if domain.rst is not None:
add_signal_name(domain.rst)
for statements in self.statements.values():
for statement in statements:
for signal in statement._lhs_signals() | statement._rhs_signals():
if not isinstance(signal, (_ast.ClockSignal, _ast.ResetSignal)):
add_signal_name(signal)
return signal_names
def _assign_names_to_fragments(self, hierarchy=("top",), *, _names=None):
"""Assign names to this fragment and its subfragments.
Subfragments may not necessarily have a name. This method assigns every such subfragment
a name, ``U$<number>``, where ``<number>`` is based on its location in the hierarchy.
Subfragment names may collide with signal names safely in Amaranth, but this may confuse
backends. This method assigns every such subfragment a name, ``<name>$U$<number>``, where
``name`` is its original name, and ``<number>`` is based on its location in the hierarchy.
Arguments
---------
hierarchy : tuple of str
Name of this fragment.
Returns
-------
dict of Fragment to tuple of str
A mapping from this fragment and its subfragments to their full hierarchical names.
"""
if _names is None:
_names = dict()
_names[self] = hierarchy
signal_names = set(self._assign_names_to_signals().values())
for subfragment_index, (subfragment, subfragment_name, subfragment_src_loc) in enumerate(self.subfragments):
if subfragment_name is None:
subfragment_name = f"U${subfragment_index}"
elif subfragment_name in signal_names:
subfragment_name = f"{subfragment_name}$U${subfragment_index}"
assert subfragment_name not in signal_names
subfragment._assign_names_to_fragments(hierarchy=(*hierarchy, subfragment_name),
_names=_names)
return _names
class Instance(Fragment): class Instance(Fragment):
@ -682,9 +460,118 @@ class Instance(Fragment):
.format(kw, arg)) .format(kw, arg))
class Design:
"""Represents a design ready for simulation or netlist building.
Returned by ``Fragment.prepare``."""
def __init__(self, fragment, ports, *, hierarchy):
self.fragment = fragment
self.ports = ports
self.hierarchy = hierarchy
# dict of Fragment to SignalDict of Signal to name
self.signal_names = {}
self.fragment_names = {}
self._assign_names_to_signals(fragment, ports)
self._assign_names_to_fragments(fragment, hierarchy)
# Use just-assigned signal names to name all unnamed ports.
top_names = self.signal_names[fragment]
self.ports = [
(name or top_names[signal], signal, dir)
for (name, signal, dir) in self.ports
]
def _assign_names_to_signals(self, fragment, ports=None):
"""Assign names to signals used in a given fragment.
The mapping is set in ``self.signal_names``. Because names are deduplicated using local
information only, the same signal used in a different fragment may get a different name.
"""
signal_names = _ast.SignalDict()
assigned_names = set()
def add_signal_name(signal):
if signal not in signal_names:
if signal.name not in assigned_names:
name = signal.name
else:
name = f"{signal.name}${len(assigned_names)}"
assert name not in assigned_names
signal_names[signal] = name
assigned_names.add(name)
if ports is not None:
# First pass: reserve names for pre-named top-level ports. If equal to the signal name, let the signal share it.
for name, signal, _dir in ports:
if name is not None:
assigned_names.add(name)
if signal.name == name:
signal_names[signal] = name
# Second pass: ensure non-pre-named top-level ports are named first.
for name, signal, _dir in ports:
if name is None:
add_signal_name(signal)
for domain_name, domain_signals in fragment.drivers.items():
if domain_name != "comb":
domain = fragment.domains[domain_name]
add_signal_name(domain.clk)
if domain.rst is not None:
add_signal_name(domain.rst)
for statements in fragment.statements.values():
for statement in statements:
for signal in statement._lhs_signals() | statement._rhs_signals():
if not isinstance(signal, (_ast.ClockSignal, _ast.ResetSignal)):
add_signal_name(signal)
self.signal_names[fragment] = signal_names
for subfragment, _name, _src_loc in fragment.subfragments:
self._assign_names_to_signals(subfragment)
def _assign_names_to_fragments(self, fragment, hierarchy):
"""Assign names to this fragment and its subfragments.
Subfragments may not necessarily have a name. This method assigns every such subfragment
a name, ``U$<number>``, where ``<number>`` is based on its location in the hierarchy.
Subfragment names may collide with signal names safely in Amaranth, but this may confuse
backends. This method assigns every such subfragment a name, ``<name>$U$<number>``, where
``name`` is its original name, and ``<number>`` is based on its location in the hierarchy.
Arguments
---------
hierarchy : tuple of str
Name of this fragment.
Returns
-------
dict of Fragment to tuple of str
A mapping from this fragment and its subfragments to their full hierarchical names.
"""
self.fragment_names[fragment] = hierarchy
signal_names = set(self.signal_names[fragment].values())
for subfragment_index, (subfragment, subfragment_name, subfragment_src_loc) in enumerate(fragment.subfragments):
if subfragment_name is None:
subfragment_name = f"U${subfragment_index}"
elif subfragment_name in signal_names:
subfragment_name = f"{subfragment_name}$U${subfragment_index}"
assert subfragment_name not in signal_names
self._assign_names_to_fragments(subfragment, hierarchy=(*hierarchy, subfragment_name))
############################################################################################### >:3 ############################################################################################### >:3
class PortDirection(enum.Enum):
Input = "input"
Output = "output"
Inout = "inout"
class NetlistDriver: class NetlistDriver:
def __init__(self, module_idx: int, signal: _ast.Signal, def __init__(self, module_idx: int, signal: _ast.Signal,
domain: '_cd.ClockDomain | None', *, src_loc): domain: '_cd.ClockDomain | None', *, src_loc):
@ -710,9 +597,9 @@ class NetlistDriver:
class NetlistEmitter: class NetlistEmitter:
def __init__(self, netlist: _nir.Netlist, fragment_names: 'dict[_ir.Fragment, str]'): def __init__(self, netlist: _nir.Netlist, design):
self.netlist = netlist self.netlist = netlist
self.fragment_names = fragment_names self.design = design
self.drivers = _ast.SignalDict() self.drivers = _ast.SignalDict()
self.rhs_cache: dict[int, Tuple[_nir.Value, bool, _ast.Value]] = {} self.rhs_cache: dict[int, Tuple[_nir.Value, bool, _ast.Value]] = {}
@ -1155,30 +1042,54 @@ class NetlistEmitter:
self.connect(port_conn, _nir.Value(output_nets[start_bit:start_bit + len(port_conn)]), self.connect(port_conn, _nir.Value(output_nets[start_bit:start_bit + len(port_conn)]),
src_loc=instance.src_loc) src_loc=instance.src_loc)
def emit_top_ports(self, fragment: _ir.Fragment, signal_names: _ast.SignalDict): def emit_top_ports(self, fragment: _ir.Fragment):
inouts = set()
for cell in self.netlist.cells:
if isinstance(cell, _nir.IOBuffer):
inouts.update(cell.pad)
if isinstance(cell, _nir.Instance):
for value in cell.ports_io.values():
inouts.update(value)
next_input_bit = 2 # 0 and 1 are reserved for constants next_input_bit = 2 # 0 and 1 are reserved for constants
top = self.netlist.top top = self.netlist.top
for signal, dir in fragment.ports.items():
assert signal not in self.netlist.signals for name, signal, dir in self.design.ports:
name = signal_names[signal] signal_value = self.emit_signal(signal)
if dir == 'i': if dir is None:
is_driven = False
is_inout = False
for net in signal_value:
if net in self.netlist.connections:
is_driven = True
if net in inouts:
is_inout = True
if is_driven:
dir = PortDirection.Output
elif is_inout:
dir = PortDirection.Inout
else:
dir = PortDirection.Input
if dir == PortDirection.Input:
top.ports_i[name] = (next_input_bit, signal.width) top.ports_i[name] = (next_input_bit, signal.width)
nets = _nir.Value( value = _nir.Value(
_nir.Net.from_cell(0, bit) _nir.Net.from_cell(0, bit)
for bit in range(next_input_bit, next_input_bit + signal.width) for bit in range(next_input_bit, next_input_bit + signal.width)
) )
next_input_bit += signal.width next_input_bit += signal.width
self.netlist.signals[signal] = nets self.connect(signal_value, value, src_loc=signal.src_loc)
elif dir == 'o': elif dir == PortDirection.Output:
top.ports_o[name] = self.emit_signal(signal) top.ports_o[name] = signal_value
elif dir == 'io': elif dir == PortDirection.Inout:
top.ports_io[name] = (next_input_bit, signal.width) top.ports_io[name] = (next_input_bit, signal.width)
nets = _nir.Value( value = _nir.Value(
_nir.Net.from_cell(0, bit) _nir.Net.from_cell(0, bit)
for bit in range(next_input_bit, next_input_bit + signal.width) for bit in range(next_input_bit, next_input_bit + signal.width)
) )
next_input_bit += signal.width next_input_bit += signal.width
self.netlist.signals[signal] = nets self.connect(signal_value, value, src_loc=signal.src_loc)
else:
raise ValueError(f"Invalid port direction {dir!r}")
def emit_drivers(self): def emit_drivers(self):
for driver in self.drivers.values(): for driver in self.drivers.values():
@ -1205,6 +1116,7 @@ class NetlistEmitter:
src_loc = driver.signal.src_loc src_loc = driver.signal.src_loc
self.connect(self.emit_signal(driver.signal), value, src_loc=src_loc) self.connect(self.emit_signal(driver.signal), value, src_loc=src_loc)
def emit_undriven(self):
# Connect all undriven signal bits to their initial values. This can only happen for entirely # Connect all undriven signal bits to their initial values. This can only happen for entirely
# undriven signals, or signals that are partially driven by instances. # undriven signals, or signals that are partially driven by instances.
for signal, value in self.netlist.signals.items(): for signal, value in self.netlist.signals.items():
@ -1215,7 +1127,7 @@ class NetlistEmitter:
def emit_fragment(self, fragment: _ir.Fragment, parent_module_idx: 'int | None', *, cell_src_loc=None): def emit_fragment(self, fragment: _ir.Fragment, parent_module_idx: 'int | None', *, cell_src_loc=None):
from . import _mem from . import _mem
fragment_name = self.fragment_names[fragment] fragment_name = self.design.fragment_names[fragment]
if isinstance(fragment, _ir.Instance): if isinstance(fragment, _ir.Instance):
assert parent_module_idx is not None assert parent_module_idx is not None
if fragment.type == "$tribuf": if fragment.type == "$tribuf":
@ -1232,10 +1144,8 @@ class NetlistEmitter:
self.emit_read_port(parent_module_idx, fragment, port, memory, write_ports) self.emit_read_port(parent_module_idx, fragment, port, memory, write_ports)
elif type(fragment) is _ir.Fragment: elif type(fragment) is _ir.Fragment:
module_idx = self.netlist.add_module(parent_module_idx, fragment_name, src_loc=fragment.src_loc, cell_src_loc=cell_src_loc) module_idx = self.netlist.add_module(parent_module_idx, fragment_name, src_loc=fragment.src_loc, cell_src_loc=cell_src_loc)
signal_names = fragment._assign_names_to_signals() signal_names = self.design.signal_names[fragment]
self.netlist.modules[module_idx].signal_names = signal_names self.netlist.modules[module_idx].signal_names = signal_names
if parent_module_idx is None:
self.emit_top_ports(fragment, signal_names)
for signal in signal_names: for signal in signal_names:
self.emit_signal(signal) self.emit_signal(signal)
for domain, stmts in fragment.statements.items(): for domain, stmts in fragment.statements.items():
@ -1245,13 +1155,14 @@ class NetlistEmitter:
self.emit_fragment(subfragment, module_idx, cell_src_loc=sub_src_loc) self.emit_fragment(subfragment, module_idx, cell_src_loc=sub_src_loc)
if parent_module_idx is None: if parent_module_idx is None:
self.emit_drivers() self.emit_drivers()
self.emit_top_ports(fragment)
self.emit_undriven()
else: else:
assert False # :nocov: assert False # :nocov:
def _emit_netlist(netlist: _nir.Netlist, fragment, hierarchy): def _emit_netlist(netlist: _nir.Netlist, design):
fragment_names = fragment._assign_names_to_fragments(hierarchy) NetlistEmitter(netlist, design).emit_fragment(design.fragment, None)
NetlistEmitter(netlist, fragment_names).emit_fragment(fragment, None)
def _compute_net_flows(netlist: _nir.Netlist): def _compute_net_flows(netlist: _nir.Netlist):
@ -1260,39 +1171,39 @@ def _compute_net_flows(netlist: _nir.Netlist):
# The rules for net flows are as follows: # The rules for net flows are as follows:
# #
# - the modules that have a given net in their net_flow form a subtree of the hierarchy # - the modules that have a given net in their net_flow form a subtree of the hierarchy
# - INTERNAL is used in the root of the subtree and nowhere else # - Internal is used in the root of the subtree and nowhere else
# - OUTPUT is used for modules that contain the definition of the net, or are on the # - Output is used for modules that contain the definition of the net, or are on the
# path from the definition to the root # path from the definition to the root
# - remaining modules have a flow of INPUT (unless the net is a top-level inout port, # - remaining modules have a flow of Input (unless the net is a top-level inout port,
# in which case it is INOUT) # in which case it is Inout)
# #
# In other words, the tree looks something like this: # In other words, the tree looks something like this:
# #
# - [no flow] <<< top # - [no flow] <<< top
# - [no flow] # - [no flow]
# - INTERNAL # - Internal
# - INPUT << use # - Input << use
# - [no flow] # - [no flow]
# - INPUT # - Input
# - INPUT << use # - Input << use
# - OUTPUT # - Output
# - INPUT << use # - Input << use
# - [no flow] # - [no flow]
# - OUTPUT << def # - Output << def
# - INPUT # - Input
# - INPUT # - Input
# - [no flow] # - [no flow]
# - [no flow] # - [no flow]
# - [no flow] # - [no flow]
# #
# This function doesn't assign the INOUT flow — that is corrected later, in compute_ports. # This function doesn't assign the Inout flow — that is corrected later, in compute_ports.
lca = {} lca = {}
# Initialize by marking the definition point of every net. # Initialize by marking the definition point of every net.
for cell_idx, cell in enumerate(netlist.cells): for cell_idx, cell in enumerate(netlist.cells):
for net in cell.output_nets(cell_idx): for net in cell.output_nets(cell_idx):
lca[net] = cell.module_idx lca[net] = cell.module_idx
netlist.modules[cell.module_idx].net_flow[net] = _nir.ModuleNetFlow.INTERNAL netlist.modules[cell.module_idx].net_flow[net] = _nir.ModuleNetFlow.Internal
# Marks a use of a net within a given module, and adjusts its netflows in all modules # Marks a use of a net within a given module, and adjusts its netflows in all modules
# as required. # as required.
@ -1309,7 +1220,7 @@ def _compute_net_flows(netlist: _nir.Netlist):
def_module = lca[net] def_module = lca[net]
# While def_module deeper than use_module, go up with def_module. # While def_module deeper than use_module, go up with def_module.
while len(modules[def_module].name) > len(modules[use_module].name): while len(modules[def_module].name) > len(modules[use_module].name):
modules[def_module].net_flow[net] = _nir.ModuleNetFlow.OUTPUT modules[def_module].net_flow[net] = _nir.ModuleNetFlow.Output
def_module = modules[def_module].parent def_module = modules[def_module].parent
# While use_module deeper than def_module, go up with use_module. # While use_module deeper than def_module, go up with use_module.
# If use_module is below def_module in the hierarchy, we may hit # If use_module is below def_module in the hierarchy, we may hit
@ -1318,19 +1229,19 @@ def _compute_net_flows(netlist: _nir.Netlist):
while len(modules[def_module].name) < len(modules[use_module].name): while len(modules[def_module].name) < len(modules[use_module].name):
if net in modules[use_module].net_flow: if net in modules[use_module].net_flow:
return return
modules[use_module].net_flow[net] = _nir.ModuleNetFlow.INPUT modules[use_module].net_flow[net] = _nir.ModuleNetFlow.Input
use_module = modules[use_module].parent use_module = modules[use_module].parent
# Now both pointers should be at the same depth within the hierarchy. # Now both pointers should be at the same depth within the hierarchy.
assert len(modules[def_module].name) == len(modules[use_module].name) assert len(modules[def_module].name) == len(modules[use_module].name)
# Move both pointers up until they meet. # Move both pointers up until they meet.
while def_module != use_module: while def_module != use_module:
modules[def_module].net_flow[net] = _nir.ModuleNetFlow.OUTPUT modules[def_module].net_flow[net] = _nir.ModuleNetFlow.Output
def_module = modules[def_module].parent def_module = modules[def_module].parent
modules[use_module].net_flow[net] = _nir.ModuleNetFlow.INPUT modules[use_module].net_flow[net] = _nir.ModuleNetFlow.Input
use_module = modules[use_module].parent use_module = modules[use_module].parent
assert len(modules[def_module].name) == len(modules[use_module].name) assert len(modules[def_module].name) == len(modules[use_module].name)
# And mark the new LCA. # And mark the new LCA.
modules[def_module].net_flow[net] = _nir.ModuleNetFlow.INTERNAL modules[def_module].net_flow[net] = _nir.ModuleNetFlow.Internal
lca[net] = def_module lca[net] = def_module
# Now mark all uses and flesh out the structure. # Now mark all uses and flesh out the structure.
@ -1373,14 +1284,17 @@ def _compute_ports(netlist: _nir.Netlist):
if value not in name_table and not name.startswith('$'): if value not in name_table and not name.startswith('$'):
name_table[value] = name name_table[value] = name
# Adjust any input flows to inout as necessary.
for (net, flow) in module.net_flow.items():
if flow == _nir.ModuleNetFlow.Input and net in inouts:
module.net_flow[net] = _nir.ModuleNetFlow.Inout
# Gather together "adjacent" nets with the same flow into ports. # Gather together "adjacent" nets with the same flow into ports.
visited = set() visited = set()
for net in sorted(module.net_flow): for net in sorted(module.net_flow):
flow = module.net_flow[net] flow = module.net_flow[net]
if flow == _nir.ModuleNetFlow.INTERNAL: if flow == _nir.ModuleNetFlow.Internal:
continue continue
if flow == _nir.ModuleNetFlow.INPUT and net in inouts:
flow = module.net_flow[net] = _nir.ModuleNetFlow.INOUT
if net in visited: if net in visited:
continue continue
# We found a net that needs a port. Keep joining the next nets output by the same # We found a net that needs a port. Keep joining the next nets output by the same
@ -1412,23 +1326,21 @@ def _compute_ports(netlist: _nir.Netlist):
for name, (start, width) in netlist.top.ports_i.items(): for name, (start, width) in netlist.top.ports_i.items():
top_module.ports[name] = ( top_module.ports[name] = (
_nir.Value(_nir.Net.from_cell(0, start + bit) for bit in range(width)), _nir.Value(_nir.Net.from_cell(0, start + bit) for bit in range(width)),
_nir.ModuleNetFlow.INPUT _nir.ModuleNetFlow.Input
) )
for name, (start, width) in netlist.top.ports_io.items(): for name, (start, width) in netlist.top.ports_io.items():
top_module.ports[name] = ( top_module.ports[name] = (
_nir.Value(_nir.Net.from_cell(0, start + bit) for bit in range(width)), _nir.Value(_nir.Net.from_cell(0, start + bit) for bit in range(width)),
_nir.ModuleNetFlow.INOUT _nir.ModuleNetFlow.Inout
) )
for name, value in netlist.top.ports_o.items(): for name, value in netlist.top.ports_o.items():
top_module.ports[name] = (value, _nir.ModuleNetFlow.OUTPUT) top_module.ports[name] = (value, _nir.ModuleNetFlow.Output)
def build_netlist(fragment, *, name="top"): def build_netlist(fragment, ports, *, name="top", **kwargs):
from ._xfrm import AssignmentLegalizer design = fragment.prepare(ports=ports, hierarchy=(name,), legalize_assignments=True, **kwargs)
fragment = AssignmentLegalizer()(fragment)
netlist = _nir.Netlist() netlist = _nir.Netlist()
_emit_netlist(netlist, fragment, hierarchy=(name,)) _emit_netlist(netlist, design)
netlist.resolve_all_nets() netlist.resolve_all_nets()
_compute_net_flows(netlist) _compute_net_flows(netlist)
_compute_ports(netlist) _compute_ports(netlist)

View file

@ -199,7 +199,7 @@ class Netlist:
self.signals[sig] = self.resolve_value(self.signals[sig]) self.signals[sig] = self.resolve_value(self.signals[sig])
def __repr__(self): def __repr__(self):
result = [] result = ["("]
for module_idx, module in enumerate(self.modules): for module_idx, module in enumerate(self.modules):
name = " ".join(repr(name) for name in module.name) name = " ".join(repr(name) for name in module.name)
ports = " ".join( ports = " ".join(
@ -209,6 +209,7 @@ class Netlist:
result.append(f"(module {module_idx} {module.parent} ({name}) {ports})") result.append(f"(module {module_idx} {module.parent} ({name}) {ports})")
for cell_idx, cell in enumerate(self.cells): for cell_idx, cell in enumerate(self.cells):
result.append(f"(cell {cell_idx} {cell.module_idx} {cell!r})") result.append(f"(cell {cell_idx} {cell.module_idx} {cell!r})")
result.append(")")
return "\n".join(result) return "\n".join(result)
def add_module(self, parent, name: str, *, src_loc=None, cell_src_loc=None): def add_module(self, parent, name: str, *, src_loc=None, cell_src_loc=None):
@ -251,21 +252,21 @@ class ModuleNetFlow(enum.Enum):
#: The net is present in the module (used in the module or needs #: The net is present in the module (used in the module or needs
#: to be routed through it between its submodules), but is not #: to be routed through it between its submodules), but is not
#: present outside its subtree and thus is not a port of this module. #: present outside its subtree and thus is not a port of this module.
INTERNAL = "internal" Internal = "internal"
#: The net is present in the module, and is not driven from #: The net is present in the module, and is not driven from
#: the module or any of its submodules. It is thus an input #: the module or any of its submodules. It is thus an input
#: port of this module. #: port of this module.
INPUT = "input" Input = "input"
#: The net is present in the module, is driven from the module or #: The net is present in the module, is driven from the module or
#: one of its submodules, and is also used outside of its subtree. #: one of its submodules, and is also used outside of its subtree.
#: It is thus an output port of this module. #: It is thus an output port of this module.
OUTPUT = "output" Output = "output"
#: The net is a special top-level inout net that is used within #: The net is a special top-level inout net that is used within
#: this module or its submodules. It is an inout port of this module. #: this module or its submodules. It is an inout port of this module.
INOUT = "inout" Inout = "inout"
class Module: class Module:
@ -1039,7 +1040,7 @@ class Instance(Cell):
items.append(f"(attr {name!r} {val!r})") items.append(f"(attr {name!r} {val!r})")
for name, val in self.ports_i.items(): for name, val in self.ports_i.items():
items.append(f"(input {name!r} {val})") items.append(f"(input {name!r} {val})")
for name, (start, width) in self.ports_i.items(): for name, (start, width) in self.ports_o.items():
items.append(f"(output {name!r} {start}:{start+width})") items.append(f"(output {name!r} {start}:{start+width})")
for name, val in self.ports_io.items(): for name, val in self.ports_io.items():
items.append(f"(inout {name!r} {val})") items.append(f"(inout {name!r} {val})")

View file

@ -209,10 +209,6 @@ class FragmentTransformer:
for subfragment, name, src_loc in fragment.subfragments: for subfragment, name, src_loc in fragment.subfragments:
new_fragment.add_subfragment(self(subfragment), name, src_loc=src_loc) new_fragment.add_subfragment(self(subfragment), name, src_loc=src_loc)
def map_ports(self, fragment, new_fragment):
for port, dir in fragment.ports.items():
new_fragment.add_ports(port, dir=dir)
def map_named_ports(self, fragment, new_fragment): def map_named_ports(self, fragment, new_fragment):
if hasattr(self, "on_value"): if hasattr(self, "on_value"):
for name, (value, dir) in fragment.named_ports.items(): for name, (value, dir) in fragment.named_ports.items():
@ -285,7 +281,6 @@ class FragmentTransformer:
new_fragment = Fragment(src_loc=fragment.src_loc) new_fragment = Fragment(src_loc=fragment.src_loc)
new_fragment.flatten = fragment.flatten new_fragment.flatten = fragment.flatten
new_fragment.attrs = OrderedDict(fragment.attrs) new_fragment.attrs = OrderedDict(fragment.attrs)
self.map_ports(fragment, new_fragment)
self.map_subfragments(fragment, new_fragment) self.map_subfragments(fragment, new_fragment)
self.map_domains(fragment, new_fragment) self.map_domains(fragment, new_fragment)
self.map_statements(fragment, new_fragment) self.map_statements(fragment, new_fragment)

View file

@ -70,8 +70,8 @@ class Simulator:
"a simulation engine name" "a simulation engine name"
.format(engine)) .format(engine))
self._fragment = Fragment.get(fragment, platform=None).prepare() self._design = Fragment.get(fragment, platform=None).prepare()
self._engine = engine(self._fragment) self._engine = engine(self._design)
self._clocked = set() self._clocked = set()
def _check_process(self, process): def _check_process(self, process):
@ -165,15 +165,15 @@ class Simulator:
in this case. in this case.
""" """
if isinstance(domain, ClockDomain): if isinstance(domain, ClockDomain):
if (domain.name in self._fragment.domains and if (domain.name in self._design.fragment.domains and
domain is not self._fragment.domains[domain.name]): domain is not self._design.fragment.domains[domain.name]):
warnings.warn("Adding a clock process that drives a clock domain object " warnings.warn("Adding a clock process that drives a clock domain object "
"named {!r}, which is distinct from an identically named domain " "named {!r}, which is distinct from an identically named domain "
"in the simulated design" "in the simulated design"
.format(domain.name), .format(domain.name),
UserWarning, stacklevel=2) UserWarning, stacklevel=2)
elif domain in self._fragment.domains: elif domain in self._design.fragment.domains:
domain = self._fragment.domains[domain] domain = self._design.fragment.domains[domain]
elif if_exists: elif if_exists:
return return
else: else:

View file

@ -36,7 +36,7 @@ class _VCDWriter:
else: else:
raise NotImplementedError raise NotImplementedError
def __init__(self, fragment, *, vcd_file, gtkw_file=None, traces=()): def __init__(self, design, *, vcd_file, gtkw_file=None, traces=()):
# Although pyvcd is a mandatory dependency, be resilient and import it as needed, so that # Although pyvcd is a mandatory dependency, be resilient and import it as needed, so that
# the simulator is still usable if it's not installed for some reason. # the simulator is still usable if it's not installed for some reason.
import vcd, vcd.gtkw import vcd, vcd.gtkw
@ -65,14 +65,14 @@ class _VCDWriter:
signal_names = SignalDict() signal_names = SignalDict()
memories = {} memories = {}
for subfragment, subfragment_name in \ for fragment, fragment_name in design.fragment_names.items():
fragment._assign_names_to_fragments(hierarchy=("bench", "top",)).items(): fragment_name = ("bench", *fragment_name)
for signal, signal_name in subfragment._assign_names_to_signals().items(): for signal, signal_name in design.signal_names[fragment].items():
if signal not in signal_names: if signal not in signal_names:
signal_names[signal] = set() signal_names[signal] = set()
signal_names[signal].add((*subfragment_name, signal_name)) signal_names[signal].add((*fragment_name, signal_name))
if isinstance(subfragment, MemoryInstance): if isinstance(fragment, MemoryInstance):
memories[subfragment._identity] = (subfragment, subfragment_name) memories[fragment._identity] = (fragment, fragment_name)
trace_names = SignalDict() trace_names = SignalDict()
assigned_names = set() assigned_names = set()
@ -403,16 +403,16 @@ class _PySimulation(BaseSimulation):
class PySimEngine(BaseEngine): class PySimEngine(BaseEngine):
def __init__(self, fragment): def __init__(self, design):
self._state = _PySimulation() self._state = _PySimulation()
self._timeline = self._state.timeline self._timeline = self._state.timeline
self._fragment = fragment self._design = design
self._processes = _FragmentCompiler(self._state)(self._fragment) self._processes = _FragmentCompiler(self._state)(self._design.fragment)
self._vcd_writers = [] self._vcd_writers = []
def add_coroutine_process(self, process, *, default_cmd): def add_coroutine_process(self, process, *, default_cmd):
self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process, self._processes.add(PyCoroProcess(self._state, self._design.fragment.domains, process,
default_cmd=default_cmd)) default_cmd=default_cmd))
def add_clock_process(self, clock, *, phase, period): def add_clock_process(self, clock, *, phase, period):
@ -462,7 +462,7 @@ class PySimEngine(BaseEngine):
@contextmanager @contextmanager
def write_vcd(self, *, vcd_file, gtkw_file, traces): def write_vcd(self, *, vcd_file, gtkw_file, traces):
vcd_writer = _VCDWriter(self._fragment, vcd_writer = _VCDWriter(self._design,
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces) vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
try: try:
self._vcd_writers.append(vcd_writer) self._vcd_writers.append(vcd_writer)

View file

@ -88,275 +88,295 @@ class FragmentPortsTestCase(FHDLTestCase):
def test_empty(self): def test_empty(self):
f = Fragment() f = Fragment()
self.assertEqual(list(f.iter_ports()), []) nl = build_netlist(f, ports=[])
self.assertRepr(nl, """
(
(module 0 None ('top'))
(cell 0 0 (top ))
)
""")
f._propagate_ports(ports=(), all_undef_as_ports=True) def test_loopback(self):
self.assertEqual(f.ports, SignalDict([]))
def test_iter_signals(self):
f = Fragment()
f.add_ports(self.s1, self.s2, dir="io")
self.assertEqual(SignalSet((self.s1, self.s2)), f.iter_signals())
def test_self_contained(self):
f = Fragment() f = Fragment()
f.add_statements( f.add_statements(
"comb", "comb",
self.c1.eq(self.s1), self.c1.eq(self.s1),
self.s1.eq(self.c1)
) )
nl = build_netlist(f, ports=[self.c1, self.s1])
self.assertRepr(nl, """
(
(module 0 None ('top') (input 's1' 0.2) (output 'c1' 0.2))
(cell 0 0 (top (output 'c1' 0.2) (input 's1' 2:3)))
)
""")
f._propagate_ports(ports=(), all_undef_as_ports=True) def test_subfragment_simple(self):
self.assertEqual(f.ports, SignalDict([])) f1 = Fragment()
f2 = Fragment()
f2.add_statements(
"comb",
self.c1.eq(~self.s1),
)
f1.add_subfragment(f2, "f2")
nl = build_netlist(f1, ports=[self.c1, self.s1])
self.assertRepr(nl, """
(
(module 0 None ('top') (input 's1' 0.2) (output 'c1' 1.0))
(module 1 0 ('top' 'f2') (input 's1' 0.2) (output 'c1' 1.0))
(cell 0 0 (top (output 'c1' 1.0) (input 's1' 2:3)))
(cell 1 1 (~ 0.2))
)
""")
def test_infer_input(self): def test_tree(self):
f = Fragment() f = Fragment()
f.add_statements( f1 = Fragment()
f.add_subfragment(f1, "f1")
f11 = Fragment()
f1.add_subfragment(f11, "f11")
f111 = Fragment()
f11.add_subfragment(f111, "f111")
f1111 = Fragment()
f111.add_subfragment(f1111, "f1111")
f12 = Fragment()
f1.add_subfragment(f12, "f12")
f13 = Fragment()
f1.add_subfragment(f13, "f13")
f131 = Fragment()
f13.add_subfragment(f131, "f131")
f2 = Fragment()
f.add_subfragment(f2, "f2")
f2.add_statements(
"comb", "comb",
self.c1.eq(self.s1) self.s2.eq(~self.s1),
) )
f131.add_statements(
"comb",
self.s3.eq(~self.s2),
Assert(~self.s1),
)
f12.add_statements(
"comb",
self.c1.eq(~self.s3),
)
f1111.add_statements(
"comb",
self.c2.eq(~self.s3),
Assert(self.s1),
)
f111.add_statements(
"comb",
self.c3.eq(~self.c2),
)
nl = build_netlist(f, ports=[self.c1, self.c2, self.c3, self.s1])
self.assertRepr(nl, """
(
(module 0 None ('top')
(input 's1' 0.2)
(output 'c1' 5.0)
(output 'c2' 2.0)
(output 'c3' 1.0))
(module 1 0 ('top' 'f1')
(input 'port$0$2' 0.2)
(output 'port$1$0' 1.0)
(output 'port$2$0' 2.0)
(output 'port$5$0' 5.0)
(input 'port$10$0' 10.0))
(module 2 1 ('top' 'f1' 'f11')
(input 'port$0$2' 0.2)
(output 'port$1$0' 1.0)
(output 'port$2$0' 2.0)
(input 'port$6$0' 6.0))
(module 3 2 ('top' 'f1' 'f11' 'f111')
(input 'port$0$2' 0.2)
(output 'c3' 1.0)
(output 'c2' 2.0)
(input 'port$6$0' 6.0))
(module 4 3 ('top' 'f1' 'f11' 'f111' 'f1111')
(input 's1' 0.2)
(output 'c2' 2.0)
(input 's3' 6.0))
(module 5 1 ('top' 'f1' 'f12')
(output 'c1' 5.0)
(input 's3' 6.0))
(module 6 1 ('top' 'f1' 'f13')
(input 'port$0$2' 0.2)
(output 'port$6$0' 6.0)
(input 'port$10$0' 10.0))
(module 7 6 ('top' 'f1' 'f13' 'f131')
(input 's1' 0.2)
(output 's3' 6.0)
(input 's2' 10.0))
(module 8 0 ('top' 'f2')
(input 's1' 0.2)
(output 's2' 10.0))
(cell 0 0 (top (output 'c1' 5.0) (output 'c2' 2.0) (output 'c3' 1.0) (input 's1' 2:3)))
(cell 1 3 (~ 2.0))
(cell 2 4 (~ 6.0))
(cell 3 4 (assignment_list 1'd0 (1 0:1 1'd1)))
(cell 4 4 (assert None 0.2 3.0))
(cell 5 5 (~ 6.0))
(cell 6 7 (~ 10.0))
(cell 7 7 (~ 0.2))
(cell 8 7 (assignment_list 1'd0 (1 0:1 1'd1)))
(cell 9 7 (assert None 7.0 8.0))
(cell 10 8 (~ 0.2))
)
""")
f._propagate_ports(ports=(), all_undef_as_ports=True) def test_port_dict(self):
self.assertEqual(f.ports, SignalDict([
(self.s1, "i")
]))
def test_request_output(self):
f = Fragment() f = Fragment()
f.add_statements( nl = build_netlist(f, ports={
"comb", "a": (self.s1, PortDirection.Output),
self.c1.eq(self.s1) "b": (self.s2, PortDirection.Input),
"c": (self.s3, PortDirection.Inout),
})
self.assertRepr(nl, """
(
(module 0 None ('top') (input 'b' 0.2) (inout 'c' 0.3) (output 'a' 1'd0))
(cell 0 0 (top (output 'a' 1'd0) (input 'b' 2:3) (inout 'c' 3:4)))
) )
""")
f._propagate_ports(ports=(self.c1,), all_undef_as_ports=True) def test_port_domain(self):
self.assertEqual(f.ports, SignalDict([
(self.s1, "i"),
(self.c1, "o")
]))
def test_input_in_subfragment(self):
f1 = Fragment()
f1.add_statements(
"comb",
self.c1.eq(self.s1)
)
f2 = Fragment()
f2.add_statements(
"comb",
self.s1.eq(0)
)
f1.add_subfragment(f2)
f1._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict())
self.assertEqual(f2.ports, SignalDict([
(self.s1, "o"),
]))
def test_input_only_in_subfragment(self):
f1 = Fragment()
f2 = Fragment()
f2.add_statements(
"comb",
self.c1.eq(self.s1)
)
f1.add_subfragment(f2)
f1._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict([
(self.s1, "i"),
]))
self.assertEqual(f2.ports, SignalDict([
(self.s1, "i"),
]))
def test_output_from_subfragment(self):
f1 = Fragment()
f1.add_statements(
"comb",
self.c1.eq(0)
)
f2 = Fragment()
f2.add_statements(
"comb",
self.c2.eq(1)
)
f1.add_subfragment(f2)
f1._propagate_ports(ports=(self.c2,), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict([
(self.c2, "o"),
]))
self.assertEqual(f2.ports, SignalDict([
(self.c2, "o"),
]))
def test_output_from_subfragment_2(self):
f1 = Fragment()
f1.add_statements(
"comb",
self.c1.eq(self.s1)
)
f2 = Fragment()
f2.add_statements(
"comb",
self.c2.eq(self.s1)
)
f1.add_subfragment(f2)
f3 = Fragment()
f3.add_statements(
"comb",
self.s1.eq(0)
)
f2.add_subfragment(f3)
f1._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f2.ports, SignalDict([
(self.s1, "o"),
]))
def test_input_output_sibling(self):
f1 = Fragment()
f2 = Fragment()
f2.add_statements(
"comb",
self.c1.eq(self.c2)
)
f1.add_subfragment(f2)
f3 = Fragment()
f3.add_statements(
"comb",
self.c2.eq(0)
)
f3.add_driver(self.c2)
f1.add_subfragment(f3)
f1._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict())
def test_output_input_sibling(self):
f1 = Fragment()
f2 = Fragment()
f2.add_statements(
"comb",
self.c2.eq(0)
)
f2.add_driver(self.c2)
f1.add_subfragment(f2)
f3 = Fragment()
f3.add_statements(
"comb",
self.c1.eq(self.c2)
)
f1.add_subfragment(f3)
f1._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict())
def test_input_cd(self):
sync = ClockDomain()
f = Fragment() f = Fragment()
f.add_statements( cd_sync = ClockDomain()
"sync", ctr = Signal(4)
self.c1.eq(self.s1) f.add_domains(cd_sync)
f.add_driver(ctr, "sync")
f.add_statements("sync", ctr.eq(ctr + 1))
nl = build_netlist(f, ports=[
ClockSignal("sync"),
ResetSignal("sync"),
ctr,
])
self.assertRepr(nl, """
(
(module 0 None ('top') (input 'clk' 0.2) (input 'rst' 0.3) (output 'ctr' 5.0:4))
(cell 0 0 (top (output 'ctr' 5.0:4) (input 'clk' 2:3) (input 'rst' 3:4)))
(cell 1 0 (+ (cat 5.0:4 1'd0) 5'd1))
(cell 2 0 (matches 0.3 1))
(cell 3 0 (priority_match 1 2.0))
(cell 4 0 (assignment_list 5.0:4 (1 0:4 1.0:4) (3.0 0:4 4'd0)))
(cell 5 0 (flipflop 4.0:4 0 pos 0.2 0))
) )
f.add_domains(sync) """)
f.add_driver(self.c1, "sync")
f._propagate_ports(ports=(), all_undef_as_ports=True) def test_port_autodomain(self):
self.assertEqual(f.ports, SignalDict([
(self.s1, "i"),
(sync.clk, "i"),
(sync.rst, "i"),
]))
def test_input_cd_reset_less(self):
sync = ClockDomain(reset_less=True)
f = Fragment() f = Fragment()
f.add_statements( ctr = Signal(4)
"sync", f.add_driver(ctr, "sync")
self.c1.eq(self.s1) f.add_statements("sync", ctr.eq(ctr + 1))
nl = build_netlist(f, ports=[ctr])
self.assertRepr(nl, """
(
(module 0 None ('top') (input 'clk' 0.2) (input 'rst' 0.3) (output 'ctr' 5.0:4))
(cell 0 0 (top (output 'ctr' 5.0:4) (input 'clk' 2:3) (input 'rst' 3:4)))
(cell 1 0 (+ (cat 5.0:4 1'd0) 5'd1))
(cell 2 0 (matches 0.3 1))
(cell 3 0 (priority_match 1 2.0))
(cell 4 0 (assignment_list 5.0:4 (1 0:4 1.0:4) (3.0 0:4 4'd0)))
(cell 5 0 (flipflop 4.0:4 0 pos 0.2 0))
) )
f.add_domains(sync) """)
f.add_driver(self.c1, "sync")
f._propagate_ports(ports=(), all_undef_as_ports=True) def test_port_partial(self):
self.assertEqual(f.ports, SignalDict([ f = Fragment()
(self.s1, "i"),
(sync.clk, "i"),
]))
def test_inout(self):
s = Signal()
f1 = Fragment() f1 = Fragment()
f2 = Instance("foo", io_x=s) f.add_subfragment(f1, "f1")
f1.add_subfragment(f2) a = Signal(4)
b = Signal(4)
c = Signal(3)
f1.add_driver(c)
f1.add_statements("comb", c.eq((a * b).shift_right(4)))
nl = build_netlist(f, ports=[a, b, c])
self.assertRepr(nl, """
(
(module 0 None ('top')
(input 'a' 0.2:6)
(input 'b' 0.6:10)
(output 'c' 1.4:7))
(module 1 0 ('top' 'f1')
(input 'a' 0.2:6)
(input 'b' 0.6:10)
(output 'c' 1.4:7))
(cell 0 0 (top
(output 'c' 1.4:7)
(input 'a' 2:6)
(input 'b' 6:10)))
(cell 1 1 (* (cat 0.2:6 4'd0) (cat 0.6:10 4'd0)))
)
""")
f1._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict([
(s, "io")
]))
def test_in_out_same_signal(self): def test_port_instance(self):
s = Signal()
f1 = Instance("foo", i_x=s, o_y=s)
f2 = Fragment()
f2.add_subfragment(f1)
f2._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f1.ports, SignalDict([
(s, "o")
]))
f3 = Instance("foo", o_y=s, i_x=s)
f4 = Fragment()
f4.add_subfragment(f3)
f4._propagate_ports(ports=(), all_undef_as_ports=True)
self.assertEqual(f3.ports, SignalDict([
(s, "o")
]))
def test_clk_rst(self):
sync = ClockDomain()
f = Fragment() f = Fragment()
f.add_domains(sync) f1 = Fragment()
f.add_subfragment(f1, "f1")
f = f.prepare(ports=(ClockSignal("sync"), ResetSignal("sync"))) a = Signal(4)
self.assertEqual(f.ports, SignalDict([ b = Signal(4)
(sync.clk, "i"), c = Signal(4)
(sync.rst, "i"), d = Signal(4)
])) f1.add_subfragment(Instance("t",
p_p = "meow",
a_a = True,
i_aa=a,
io_bb=b,
o_cc=c,
o_dd=d,
), "i")
nl = build_netlist(f, ports=[a, b, c, d])
self.assertRepr(nl, """
(
(module 0 None ('top')
(input 'a' 0.2:6)
(inout 'b' 0.6:10)
(output 'c' 1.0:4)
(output 'd' 1.4:8))
(module 1 0 ('top' 'f1')
(input 'port$0$2' 0.2:6)
(inout 'port$0$6' 0.6:10)
(output 'port$1$0' 1.0:4)
(output 'port$1$4' 1.4:8))
(cell 0 0 (top
(output 'c' 1.0:4)
(output 'd' 1.4:8)
(input 'a' 2:6)
(inout 'b' 6:10)))
(cell 1 1 (instance 't' 'i'
(param 'p' 'meow')
(attr 'a' True)
(input 'aa' 0.2:6)
(output 'cc' 0:4)
(output 'dd' 4:8)
(inout 'bb' 0.6:10)))
)
""")
def test_port_wrong(self): def test_port_wrong(self):
f = Fragment() f = Fragment()
a = Signal()
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
r"^Only signals may be added as ports, not \(const 1'd1\)$"): r"^Only signals may be added as ports, not \(const 1'd1\)$"):
f.prepare(ports=(Const(1),)) build_netlist(f, ports=(Const(1),))
with self.assertRaisesRegex(TypeError,
r"^Port name must be a string, not 1$"):
build_netlist(f, ports={1: (a, PortDirection.Input)})
with self.assertRaisesRegex(TypeError,
r"^Port direction must be a `PortDirection` instance or None, not 'i'$"):
build_netlist(f, ports={"a": (a, "i")})
def test_port_not_iterable(self): def test_port_not_iterable(self):
f = Fragment() f = Fragment()
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
r"^`ports` must be either a list or a tuple, not 1$"): r"^`ports` must be a dict, a list or a tuple, not 1$"):
f.prepare(ports=1) build_netlist(f, ports=1)
with self.assertRaisesRegex(TypeError, with self.assertRaisesRegex(TypeError,
(r"^`ports` must be either a list or a tuple, not \(const 1'd1\)" (r"^`ports` must be a dict, a list or a tuple, not \(const 1'd1\)"
r" \(did you mean `ports=\(<signal>,\)`, rather than `ports=<signal>`\?\)$")): r" \(did you mean `ports=\(<signal>,\)`, rather than `ports=<signal>`\?\)$")):
f.prepare(ports=Const(1)) build_netlist(f, ports=Const(1))
class FragmentDomainsTestCase(FHDLTestCase): class FragmentDomainsTestCase(FHDLTestCase):
def test_iter_signals(self):
cd1 = ClockDomain()
cd2 = ClockDomain(reset_less=True)
s1 = Signal()
s2 = Signal()
f = Fragment()
f.add_domains(cd1, cd2)
f.add_driver(s1, "cd1")
self.assertEqual(SignalSet((cd1.clk, cd1.rst, s1)), f.iter_signals())
f.add_driver(s2, "cd2")
self.assertEqual(SignalSet((cd1.clk, cd1.rst, cd2.clk, s1, s2)), f.iter_signals())
def test_propagate_up(self): def test_propagate_up(self):
cd = ClockDomain() cd = ClockDomain()
@ -810,49 +830,17 @@ class InstanceTestCase(FHDLTestCase):
self.assertEqual(f.type, "cpu") self.assertEqual(f.type, "cpu")
self.assertEqual(f.parameters, OrderedDict([("RESET", 0x1234)])) self.assertEqual(f.parameters, OrderedDict([("RESET", 0x1234)]))
self.assertEqual(list(f.named_ports.keys()), ["clk", "rst", "stb", "data", "pins"]) self.assertEqual(list(f.named_ports.keys()), ["clk", "rst", "stb", "data", "pins"])
self.assertEqual(f.ports, SignalDict([]))
def test_prepare(self):
self.setUp_cpu()
f = self.wrap.prepare()
sync_clk = f.domains["sync"].clk
self.assertEqual(f.ports, SignalDict([
(sync_clk, "i"),
(self.rst, "i"),
(self.pins, "io"),
]))
def test_prepare_explicit_ports(self):
self.setUp_cpu()
f = self.wrap.prepare(ports=[self.rst, self.stb])
sync_clk = f.domains["sync"].clk
sync_rst = f.domains["sync"].rst
self.assertEqual(f.ports, SignalDict([
(sync_clk, "i"),
(sync_rst, "i"),
(self.rst, "i"),
(self.stb, "o"),
(self.pins, "io"),
]))
def test_prepare_slice_in_port(self):
s = Signal(2)
f = Fragment()
f.add_subfragment(Instance("foo", o_O=s[0]))
f.add_subfragment(Instance("foo", o_O=s[1]))
fp = f.prepare(ports=[s], missing_domain=lambda name: None)
self.assertEqual(fp.ports, SignalDict([
(s, "o"),
]))
def test_prepare_attrs(self): def test_prepare_attrs(self):
self.setUp_cpu() self.setUp_cpu()
self.inst.attrs["ATTR"] = 1 self.inst.attrs["ATTR"] = 1
f = self.inst.prepare() design = self.inst.prepare()
self.assertEqual(f.attrs, OrderedDict([ self.assertEqual(design.fragment.attrs, OrderedDict([
("ATTR", 1), ("ATTR", 1),
])) ]))
class NamesTestCase(FHDLTestCase):
def test_assign_names_to_signals(self): def test_assign_names_to_signals(self):
i = Signal() i = Signal()
rst = Signal() rst = Signal()
@ -864,8 +852,6 @@ class InstanceTestCase(FHDLTestCase):
f = Fragment() f = Fragment()
f.add_domains(cd_sync := ClockDomain()) f.add_domains(cd_sync := ClockDomain())
f.add_domains(cd_sync_norst := ClockDomain(reset_less=True)) f.add_domains(cd_sync_norst := ClockDomain(reset_less=True))
f.add_ports((i, rst), dir="i")
f.add_ports((o1, o2, o3), dir="o")
f.add_statements("comb", [o1.eq(0)]) f.add_statements("comb", [o1.eq(0)])
f.add_driver(o1, domain="comb") f.add_driver(o1, domain="comb")
f.add_statements("sync", [o2.eq(i1)]) f.add_statements("sync", [o2.eq(i1)])
@ -873,8 +859,15 @@ class InstanceTestCase(FHDLTestCase):
f.add_statements("sync_norst", [o3.eq(i1)]) f.add_statements("sync_norst", [o3.eq(i1)])
f.add_driver(o3, domain="sync_norst") f.add_driver(o3, domain="sync_norst")
names = f._assign_names_to_signals() ports = {
self.assertEqual(names, SignalDict([ "i": (i, PortDirection.Input),
"rst": (rst, PortDirection.Input),
"o1": (o1, PortDirection.Output),
"o2": (o2, PortDirection.Output),
"o3": (o3, PortDirection.Output),
}
design = f.prepare(ports)
self.assertEqual(design.signal_names[design.fragment], SignalDict([
(i, "i"), (i, "i"),
(rst, "rst"), (rst, "rst"),
(o1, "o1"), (o1, "o1"),
@ -891,8 +884,8 @@ class InstanceTestCase(FHDLTestCase):
f.add_subfragment(a := Fragment()) f.add_subfragment(a := Fragment())
f.add_subfragment(b := Fragment(), name="b") f.add_subfragment(b := Fragment(), name="b")
names = f._assign_names_to_fragments() design = Design(f, ports=(), hierarchy=("top",))
self.assertEqual(names, { self.assertEqual(design.fragment_names, {
f: ("top",), f: ("top",),
a: ("top", "U$0"), a: ("top", "U$0"),
b: ("top", "b") b: ("top", "b")
@ -903,8 +896,8 @@ class InstanceTestCase(FHDLTestCase):
f.add_subfragment(a := Fragment()) f.add_subfragment(a := Fragment())
f.add_subfragment(b := Fragment(), name="b") f.add_subfragment(b := Fragment(), name="b")
names = f._assign_names_to_fragments(hierarchy=("bench", "cpu")) design = Design(f, ports=[], hierarchy=("bench", "cpu"))
self.assertEqual(names, { self.assertEqual(design.fragment_names, {
f: ("bench", "cpu",), f: ("bench", "cpu",),
a: ("bench", "cpu", "U$0"), a: ("bench", "cpu", "U$0"),
b: ("bench", "cpu", "b") b: ("bench", "cpu", "b")
@ -913,10 +906,10 @@ class InstanceTestCase(FHDLTestCase):
def test_assign_names_to_fragments_collide_with_signal(self): def test_assign_names_to_fragments_collide_with_signal(self):
f = Fragment() f = Fragment()
f.add_subfragment(a_f := Fragment(), name="a") f.add_subfragment(a_f := Fragment(), name="a")
f.add_ports((a_s := Signal(name="a"),), dir="o") a_s = Signal(name="a")
names = f._assign_names_to_fragments() design = Design(f, ports=[("a", a_s, None)], hierarchy=("top",))
self.assertEqual(names, { self.assertEqual(design.fragment_names, {
f: ("top",), f: ("top",),
a_f: ("top", "a$U$0") a_f: ("top", "a$U$0")
}) })