"""Submodule defining blocks within a design."""
from __future__ import annotations
import dataclasses
import enum
from pathlib import Path
from typing import TYPE_CHECKING
from . import vhdl
from ._yamllable import ToFromYAML, ensure_existence_and_type
if TYPE_CHECKING:
from typing import Any, Self
from ._yamllable import PathLike, YAMLMapping
class DocEnum(enum.StrEnum):
def __new__(cls, value: str, doc: str | None = None) -> Self:
"""Create a new string enum."""
self = str.__new__(cls, value)
self._value_ = value
if doc is not None:
self.__doc__ = doc
return self
[docs]
@enum.unique
class Direction(DocEnum):
"""Class representing the direction of a pin."""
IN = "in", "The pin is an input."
OUT = "out", "The pin is an output."
INOUT = "inout", "The pin is an input and an output."
@classmethod
def _missing_(cls, value: Any) -> Self | None: # noqa: ANN401
"""Interpret values that don't completely match an enum value."""
if not isinstance(value, str):
return None
value = value.lower()
for member in cls:
if member.value == value:
return member
return None
def __repr__(self) -> str:
return f"{self.__class__.__name__}.{self.value.upper()}"
@dataclasses.dataclass
class Parameter(ToFromYAML):
"""Class representing a parameter on a block."""
name: str
"""The name of the parameter."""
type: str
"""The type of the parameter. This maps to the VHDL type."""
@classmethod
def from_dict(cls, dct: YAMLMapping) -> Self:
"""Create a Parameter from a dictionary."""
name = ensure_existence_and_type(dct, "name", str)
t = ensure_existence_and_type(dct, "type", str)
return cls(name=name, type=t)
def to_dict(self) -> YAMLMapping:
"""Convert a Parameter to a dictionary."""
return {
"name": self.name,
"type": self.type,
}
def to_vhdl_entity_generic_definition(
self,
prefix: str | None = None,
prefix_separator: str = "_",
) -> str:
"""Return the VHDL entity generic definition for this parameter."""
if prefix is not None and len(prefix) != 0:
return f"{prefix}{prefix_separator}{self.name}: {self.type}"
return f"{self.name}: {self.type}"
[docs]
@dataclasses.dataclass
class Pin(ToFromYAML):
"""Class representing a pin on a block."""
name: str
"""The name of the pin."""
type: str
"""The type of the pin. This maps to the VHDL type."""
direction: Direction | str
"""The direction of the pin."""
def __post_init__(self) -> None:
self.direction = Direction(self.direction)
[docs]
@classmethod
def from_dict(cls, dct: YAMLMapping) -> Self:
"""Create a Pin from a dictionary."""
name = ensure_existence_and_type(dct, "name", str)
t = ensure_existence_and_type(dct, "type", str)
direction = ensure_existence_and_type(dct, "direction", str)
return cls(name=name, type=t, direction=direction)
[docs]
def to_dict(self) -> YAMLMapping:
"""Convert a Pin to a dictionary."""
return {
"name": self.name,
"type": self.type,
"direction": str(self.direction),
}
[docs]
def to_vhdl_entity_port_definition(
self,
prefix: str | None = None,
prefix_separator: str = "_",
) -> str:
"""Return the VHDL entity port definition for this pin."""
if prefix is not None and len(prefix) != 0:
return (
f"{prefix}{prefix_separator}{self.name}: {self.direction} {self.type}"
)
return f"{self.name}: {self.direction} {self.type}"
[docs]
@dataclasses.dataclass
class Block(ToFromYAML):
"""Class representing an available block in a block design."""
name: str
"""The name of the block."""
parameters: dict[str, Parameter]
"""The parameters on the block."""
pins: dict[str, Pin]
"""The pins on the block."""
[docs]
@classmethod
def from_dict(cls, dct: YAMLMapping) -> Self:
"""Create a Block from a dictionary."""
name = ensure_existence_and_type(dct, "name", str)
parameters = ensure_existence_and_type(dct, "parameters", dict)
parameters_as_objects = {
k: Parameter.from_dict(ensure_existence_and_type(parameters, k, dict))
for k in parameters
}
pins = ensure_existence_and_type(dct, "pins", dict)
pins_as_objects = {
k: Pin.from_dict(ensure_existence_and_type(pins, k, dict)) for k in pins
}
return cls(name=name, parameters=parameters_as_objects, pins=pins_as_objects)
[docs]
@classmethod
def from_vhdl_file(cls, path: PathLike) -> Self:
"""Create a Block from a VHDL file."""
return cls.from_vhdl_string(Path(path).read_bytes())
[docs]
@classmethod
def from_vhdl_string(cls, s: str | bytes) -> Self:
"""
Create a Block from a VHDL string or bytes string.
Raises
------
RuntimeError
If `s` contains 0 or > 1 entity declarations.
"""
if isinstance(s, str):
s = bytes(s, "utf-8")
tree = vhdl.PARSER.parse(s)
captures = vhdl.ENTITY_NAME_QUERY.captures(tree.root_node)
entity_name = [
e.text.decode("utf-8")
for e in captures.get("entity.name", [])
if e.text is not None
]
if len(entity_name) != 1:
message = (
f"'{s.decode('utf-8')}' doesn't contain exactly one entity declaration"
)
raise RuntimeError(message)
captures = vhdl.ENTITY_GENERICS_QUERY.captures(tree.root_node)
names = [
n.text.decode("utf-8")
for n in captures.get("generic.name", [])
if n.text is not None
]
types = [
t.text.decode("utf-8")
for t in captures.get("generic.type", [])
if t.text is not None
]
generics = []
for name, generic_type in zip(names, types, strict=True):
generics.append({"name": name, "type": generic_type})
parameters = {
generic["name"]: Parameter(
name=generic["name"],
type=generic["type"],
)
for generic in generics
}
captures = vhdl.ENTITY_PORTS_QUERY.captures(tree.root_node)
names = [
n.text.decode("utf-8")
for n in captures.get("port.name", [])
if n.text is not None
]
directions = [
d.text.decode("utf-8")
for d in captures.get("port.direction", [])
if d.text is not None
]
types = [
t.text.decode("utf-8")
for t in captures.get("port.type", [])
if t.text is not None
]
ports = []
for name, direction, port_type in zip(names, directions, types, strict=True):
ports.append({"name": name, "direction": direction, "type": port_type})
pins = {
port["name"]: Pin(
name=port["name"],
direction=port["direction"],
type=port["type"],
)
for port in ports
}
return cls(name=entity_name[0], parameters=parameters, pins=pins)
[docs]
def to_dict(self) -> YAMLMapping:
"""Convert a Block to a dictionary."""
return {
"name": self.name,
"parameters": {k: v.to_dict() for k, v in self.parameters.items()},
"pins": {k: v.to_dict() for k, v in self.pins.items()},
}