Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions tests/tests_ir/backend/test_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
Bits,
BitStruct,
Dimensions,
LogicArray,
LogicBitSelect,
LogicFieldSelect,
LogicSelect,
Expand Down Expand Up @@ -259,6 +260,49 @@ def test_complex_port(self):
},
}

def test_multidimensional_port(self):
ty = Bits(
dimensions=[
Dimensions(upper=ElaboratableValue(1), lower=ElaboratableValue(0)),
Dimensions(upper=ElaboratableValue(7), lower=ElaboratableValue(0)),
]
)

top = Module(
id=Identifier(name="top"),
ports=[
Port(
name="foo",
direction=PortDirection.IN,
type=ty,
default_value=ElaboratableValue(4),
),
],
)

backend = IpCoreDescriptionBackend()

out = backend.represent(top)
[out] = backend.serialize(out)
tree = yaml.safe_load(out.content)

assert tree == {
"id": {"name": "top", "library": "libdefault", "vendor": "vendor", "version": "0.1"},
"signals": {
"in": [
{
"name": "foo",
"dimensions": [["1", "0"], ["7", "0"]],
"default": "4",
},
],
},
}

frontend = IPCoreDescriptionFrontend()
mod = frontend.parse_str(out.content)
_compare_modules(top, mod)

def test_parameters(self):
mod = Module(
id=Identifier(name="top"),
Expand Down Expand Up @@ -612,6 +656,43 @@ def test_config_output(self):
expected_obj: dict[str, str] = {name: h.to_str() for name, h in rep.items()}
assert repo_dict == expected_obj

def test_multidimensional_top_level_ports_roundtrip(self):
design_yaml = """
name: top
external:
ports:
in:
- name: in_arr
dimensions:
- [1, 0]
- [7, 0]
out:
- name: out_vec
dimensions:
- [15, 0]
"""

front = DesignDescriptionFrontend()
orig_des = front.parse_str(design_yaml)

back = DesignDescriptionBackend()
out = back.represent(orig_des.parent)
[out] = back.serialize(out)

new_des = front.parse_str(out.content)

in_arr = new_des.parent.ports.find_by_name_or_error("in_arr")
out_vec = new_des.parent.ports.find_by_name_or_error("out_vec")

assert isinstance(in_arr.type, LogicArray)
assert in_arr.type.dimensions == [
Dimensions(upper=ElaboratableValue(1), lower=ElaboratableValue(0)),
Dimensions(upper=ElaboratableValue(7), lower=ElaboratableValue(0)),
]
assert isinstance(out_vec.type, LogicArray)
assert out_vec.type.dimensions == [
Dimensions(upper=ElaboratableValue(15), lower=ElaboratableValue(0))
]

class TestDesignPositionsBackend:
def test_positions(self):
Expand Down
56 changes: 56 additions & 0 deletions tests/tests_ir/frontend/test_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,64 @@ def test_config_field(self):
assert all(type(out.config.repositories[k]) is type(rep[k]) for k in keys)
assert all(out.config.repositories[k].to_str() == rep[k].to_str() for k in keys)

def test_multidimensional_top_level_ports(self):
des = """
name: top
external:
ports:
in:
- name: in_arr
dimensions:
- [1, 0]
- [7, 0]
out:
- name: out_vec
dimensions:
- [15, 0]
"""

mod = DesignDescriptionFrontend().parse_str(des)

in_arr = mod.parent.ports.find_by_name_or_error("in_arr")
out_vec = mod.parent.ports.find_by_name_or_error("out_vec")

assert isinstance(in_arr.type, LogicArray)
assert in_arr.type.dimensions == [
Dimensions(upper=ElaboratableValue(1), lower=ElaboratableValue(0)),
Dimensions(upper=ElaboratableValue(7), lower=ElaboratableValue(0)),
]
assert isinstance(out_vec.type, LogicArray)
assert out_vec.type.dimensions == [
Dimensions(upper=ElaboratableValue(15), lower=ElaboratableValue(0))
]


class TestIPCoreDescriptionFrontend:
def test_multidimensional_signal(self):
ip = """
id:
name: top
vendor: vendor
library: libdefault
signals:
in:
- name: in_arr
dimensions:
- [1, 0]
- [7, 0]
default: 4
"""

mod = IPCoreDescriptionFrontend().parse_str(ip)
in_arr = mod.ports.find_by_name_or_error("in_arr")

assert isinstance(in_arr.type, LogicArray)
assert in_arr.type.dimensions == [
Dimensions(upper=ElaboratableValue(1), lower=ElaboratableValue(0)),
Dimensions(upper=ElaboratableValue(7), lower=ElaboratableValue(0)),
]
assert in_arr.default_value == ElaboratableValue("4")

def test_parse_on_mem_yaml(self):
ip = Path("examples/ir_examples/interconnect/ips/mem.yaml")
mod = IPCoreDescriptionFrontend().parse_file(ip)
Expand Down
20 changes: 17 additions & 3 deletions topwrap/backend/yaml/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
IPCoreInterface,
IPCoreIntfPorts,
IPCoreParameter,
IPCorePortDefinition,
IPCorePorts,
IPCoreStruct,
IPCoreStructField,
Expand All @@ -28,6 +29,7 @@
ConnectionsSection,
DesignDescription,
DesignExternalIntfs,
DesignExternalPortDefinition,
DesignExternalPorts,
DesignExternalSection,
DesignInverterPosition,
Expand Down Expand Up @@ -149,7 +151,11 @@ def _represent_signal(
raise ValueError("Trying to slice a single bit")
elif isinstance(type, Bits):
if len(type.dimensions) > 1:
raise ValueError("IP core YAML format only supports one-dimensional bit vectors")
return IPCorePortDefinition(
name=name,
dimensions=tuple((d.upper.value, d.lower.value) for d in type.dimensions),
default=default.value if default else None,
)

bound = (type.dimensions[0].upper.value, type.dimensions[0].lower.value)
else:
Expand Down Expand Up @@ -536,11 +542,19 @@ def _represent_external_ports(self, mod: Module) -> DesignExternalPorts:
outputs = []
inouts = []

def represent_port(port: Port):
if isinstance(port.type, LogicArray) and isinstance(port.type.item, Bit):
return DesignExternalPortDefinition(
name=port.name,
dimensions=[(dim.upper.value, dim.lower.value) for dim in port.type.dimensions],
)
return port.name

for port in mod.non_intf_ports():
if port.direction is PortDirection.IN:
inputs.append(port.name)
inputs.append(represent_port(port))
elif port.direction is PortDirection.OUT:
outputs.append(port.name)
outputs.append(represent_port(port))
elif port.direction is PortDirection.INOUT:
# Look for connection that this port is a part of, then from that
# find the module port it's connected to.
Expand Down
27 changes: 27 additions & 0 deletions topwrap/backend/yaml/common/ip_core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from topwrap.util import get_config, get_interface_by_id

_StrOrInt = Union[str, int]
IPCoreDimension = Tuple[_StrOrInt, _StrOrInt]


@marshmallow_dataclass.dataclass(frozen=True)
Expand All @@ -55,11 +56,26 @@ def _validate(self, self_obj: Dict[str, Any], **kwargs: Any) -> bool:
return True


@marshmallow_dataclass.dataclass(frozen=True)
class IPCorePortDefinition(MarshmallowDataclassExtensions):
name: str
dimensions: Tuple[IPCoreDimension, ...] = ext_field(tuple, inline_depth=1)
default: Optional[_StrOrInt] = ext_field(None)

@marshmallow.validates_schema
def _validate(self, self_obj: Dict[str, Any], **kwargs: Any) -> bool:
if len(self_obj["dimensions"]) == 0:
raise marshmallow.ValidationError("Signal dimensions cannot be empty")

return True


Signal = Union[
str,
Tuple[str, _StrOrInt, _StrOrInt],
Tuple[str, _StrOrInt, _StrOrInt, _StrOrInt, _StrOrInt],
IPCoreComplexSignal,
IPCorePortDefinition,
]


Expand Down Expand Up @@ -89,6 +105,17 @@ def raw(self) -> Signal:

@staticmethod
def from_sig_and_dir(sig: Signal, dir: LegacyPortDirection) -> "IPCorePort":
if isinstance(sig, IPCorePortDefinition):
upper_bound, lower_bound = sig.dimensions[0]
return IPCorePort(
name=sig.name,
direction=dir,
upper_bound=upper_bound,
lower_bound=lower_bound,
upper_slice=upper_bound,
lower_slice=lower_bound,
)

if isinstance(sig, IPCoreComplexSignal):
name = sig.name if sig.name is not None else str(sig.path)
assert name is not None
Expand Down
36 changes: 30 additions & 6 deletions topwrap/frontend/yaml/design.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from topwrap.backend.yaml.common.ip_core_schema import param_to_ir_param
from topwrap.frontend.yaml.design_schema import (
DesignDescription,
DesignExternalPortDefinition,
DesignIP,
DesignNodePosition,
DesignPositionDefinition,
Expand All @@ -36,7 +37,7 @@
ResetPolarity,
)
from topwrap.model.design import ClockDomain, Design, ModuleInstance, ResetDomain
from topwrap.model.hdl_types import Bit
from topwrap.model.hdl_types import Bit, Bits, Dimensions
from topwrap.model.interconnect import Interconnect
from topwrap.model.interface import Interface, InterfaceDefinition, InterfaceMode, InterfaceSignal
from topwrap.model.memory_map import MemoryMap as IRMemoryMap
Expand Down Expand Up @@ -136,15 +137,36 @@ def _parse_components(self, desc: DesignDescription, design: Design, source: Opt
parsed, _ = self._parse_hier(source, hdesc, hname)
design.add_component(ModuleInstance(name=hname, module=parsed.parent))

def _parse_ports(self, desc: DesignDescription) -> dict[str, tuple[PortDirection, bool]]:
# Parse external port names and determine if they are multidimensional
def _external_decl_name(self, decl: str | DesignExternalPortDefinition) -> str:
return decl if isinstance(decl, str) else decl.name

def _external_decl_type(self, decl: DesignExternalPortDefinition):
if len(decl.dimensions) == 0:
return Bit()
return Bits(
dimensions=[
Dimensions(ElaboratableValue(upper), ElaboratableValue(lower))
for upper, lower in decl.dimensions
]
)

def _parse_ports(self, desc: DesignDescription, mod: Module) -> dict[str, tuple[PortDirection, bool]]:
declared_exts = dict[str, tuple[PortDirection, bool]]()
for port, group in ((True, desc.external.ports), (False, desc.external.interfaces)):
for dir, decls in ((PortDirection.IN, group.input), (PortDirection.OUT, group.output)):
for d in decls:
if d in declared_exts:
logger.warning(f"Skipping duplicated external IO: '{d}'")
name = self._external_decl_name(d) if port else d
if name in declared_exts:
logger.warning(f"Skipping duplicated external IO: '{name}'")
continue
declared_exts[d] = (dir, port)

if port and isinstance(d, DesignExternalPortDefinition):
mod.add_port(
Port(name=name, direction=dir, type=self._external_decl_type(d))
)

declared_exts[name] = (dir, port)
return declared_exts

def _parse_connections(
Expand All @@ -163,6 +185,8 @@ def _parse_connections(

def _add_ports(self, mod: Module, declared_exts: dict[str, tuple[PortDirection, bool]]):
for name, (dir, port) in declared_exts.items():
if mod.ios.find_by_name(name) is not None:
continue
if port:
mod.add_port(Port(name=name, direction=dir, type=Bit()))
else:
Expand Down Expand Up @@ -218,7 +242,7 @@ def _parse_hier(

# Gather declarations of external ports and interfaces so that
# they can be instantiated with the inferred type later on
declared_exts = self._parse_ports(desc)
declared_exts = self._parse_ports(desc, mod)
# Parse regular connections between ports, interfaces and externals
self._parse_connections(desc, design, declared_exts)

Expand Down
13 changes: 11 additions & 2 deletions topwrap/frontend/yaml/design_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,19 @@ def path(self):
return self.file.to_path()


@marshmallow_dataclass.dataclass(frozen=True)
class DesignExternalPortDefinition(MarshmallowDataclassExtensions):
name: str
dimensions: List[Tuple[Union[str, int], Union[str, int]]] = ext_field(list, inline_depth=1)


DesignExternalPort = Union[str, DesignExternalPortDefinition]


@marshmallow_dataclass.dataclass(frozen=True)
class DesignExternalPorts(MarshmallowDataclassExtensions):
input: List[str] = ext_field(list, data_key="in")
output: List[str] = ext_field(list, data_key="out")
input: List[DesignExternalPort] = ext_field(list, data_key="in")
output: List[DesignExternalPort] = ext_field(list, data_key="out")
inout: List[Tuple[str, str]] = ext_field(list, inline_depth=1)

@cached_property
Expand Down
Loading
Loading