diff --git a/pyVHDLModel/Base.py b/pyVHDLModel/Base.py index 011d4b5d8..89100c447 100644 --- a/pyVHDLModel/Base.py +++ b/pyVHDLModel/Base.py @@ -35,7 +35,7 @@ Base-classes for the VHDL language model. """ from enum import unique, Enum -from typing import Type, Tuple, Iterable, Optional as Nullable, Union, cast +from typing import Type, Tuple, Iterable, Optional as Nullable, Union, cast, TypeVar, Generic, Dict, List from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType @@ -450,3 +450,32 @@ def Expression(self) -> ExpressionUnion: @property def After(self) -> Expression: return self._after + + +T = TypeVar("T") + +@export +class Groups(Generic[T], dict): + """A typed dictionary for grouping lists of objects by name (string keys) or None for ungrouped items.""" + + def __init__(self, data: Dict[str | None, List[T]], item_type: Type[T]): + self._item_type = item_type + # Validate the input data before calling super().__init__ + for key, value in data.items(): + self._validate_key_value(key, value) + super().__init__(data) + + def _validate_key_value(self, key: str | None, value: List[T]) -> None: + """Validate key and value types.""" + if not isinstance(key, (str, type(None))): + raise TypeError("Keys must be strings or None") + if not isinstance(value, list) or not all(isinstance(item, self._item_type) for item in value): + raise TypeError(f"Values must be lists of {self._item_type.__name__}") + + def __setitem__(self, key: str | None, value: List[T]) -> None: + """Override to add type checking when setting items.""" + self._validate_key_value(key, value) + super().__setitem__(key, value) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({dict(self)!r})" diff --git a/pyVHDLModel/DesignUnit.py b/pyVHDLModel/DesignUnit.py index 62c0f65c5..d9ff5518f 100644 --- a/pyVHDLModel/DesignUnit.py +++ b/pyVHDLModel/DesignUnit.py @@ -45,8 +45,8 @@ from pyVHDLModel.Namespace import Namespace from pyVHDLModel.Regions import ConcurrentDeclarationRegionMixin from pyVHDLModel.Symbol import Symbol, PackageSymbol, EntitySymbol, LibraryReferenceSymbol -from pyVHDLModel.Interface import GenericInterfaceItemMixin, PortInterfaceItemMixin -from pyVHDLModel.Object import DeferredConstant +from pyVHDLModel.Interface import GenericInterfaceItemMixin, PortInterfaceItemMixin, PortGroups +from pyVHDLModel.Object import DeferredConstant, SignalGroups from pyVHDLModel.Concurrent import ConcurrentStatement, ConcurrentStatementsMixin @@ -581,8 +581,52 @@ def __repr__(self) -> str: return f"{lib}.{self._identifier}(body)" +class DesignUnitInterfaceMixin(metaclass=ExtendedType, mixin=True): + _genericItems: List[GenericInterfaceItemMixin] + _portItems: List[PortInterfaceItemMixin] + _portGroups: PortGroups + + def __init__( + self, + genericItems: Nullable[Iterable[GenericInterfaceItemMixin]] = None, + portItems: Nullable[Union[Iterable[PortInterfaceItemMixin], PortGroups]] = None + ) -> None: + self._genericItems = [] + if genericItems is not None: + for item in genericItems: + self._genericItems.append(item) + item._parent = self + self._portItems = [] + self._portGroups = {} + if isinstance(portItems, PortGroups): + for group in portItems.keys(): + self._portGroups[group] = [] # Initialize as empty list + for item in portItems[group]: + self._portGroups[group].append(item) # Append to the list + if item not in self._portItems: + self._portItems.append(item) + item._parent = self + elif portItems is not None: + self._portGroups[None] = [] + for item in portItems: + self._portGroups[None].append(item) + self._portItems.append(item) + item._parent = self + + @property + def GenericItems(self) -> List[GenericInterfaceItemMixin]: + return self._genericItems + + @property + def PortItems(self) -> List[PortInterfaceItemMixin]: + return self._portItems + + @property + def PortGroups(self) -> PortGroups: + return self._portGroups + @export -class Entity(PrimaryUnit, DesignUnitWithContextMixin, ConcurrentDeclarationRegionMixin, ConcurrentStatementsMixin): +class Entity(PrimaryUnit, DesignUnitWithContextMixin, ConcurrentDeclarationRegionMixin, ConcurrentStatementsMixin, DesignUnitInterfaceMixin): """ Represents an entity declaration. @@ -597,9 +641,6 @@ class Entity(PrimaryUnit, DesignUnitWithContextMixin, ConcurrentDeclarationRegio _allowBlackbox: Nullable[bool] #: Allow blackboxes for components in this package. - _genericItems: List[GenericInterfaceItemMixin] - _portItems: List[PortInterfaceItemMixin] - _architectures: Dict[str, 'Architecture'] def __init__( @@ -607,7 +648,7 @@ def __init__( identifier: str, contextItems: Nullable[Iterable[ContextUnion]] = None, genericItems: Nullable[Iterable[GenericInterfaceItemMixin]] = None, - portItems: Nullable[Iterable[PortInterfaceItemMixin]] = None, + portItems: Nullable[Union[Iterable[PortInterfaceItemMixin], PortGroups]] = None, declaredItems: Nullable[Iterable] = None, statements: Nullable[Iterable[ConcurrentStatement]] = None, documentation: Nullable[str] = None, @@ -621,19 +662,7 @@ def __init__( self._allowBlackbox = allowBlackbox - # TODO: extract to mixin - self._genericItems = [] - if genericItems is not None: - for item in genericItems: - self._genericItems.append(item) - item._parent = self - - # TODO: extract to mixin - self._portItems = [] - if portItems is not None: - for item in portItems: - self._portItems.append(item) - item._parent = self + DesignUnitInterfaceMixin.__init__(self, genericItems, portItems) self._architectures = {} @@ -653,16 +682,6 @@ def AllowBlackbox(self) -> bool: def AllowBlackbox(self, value: Nullable[bool]) -> None: self._allowBlackbox = value - # TODO: extract to mixin for generics - @property - def GenericItems(self) -> List[GenericInterfaceItemMixin]: - return self._genericItems - - # TODO: extract to mixin for ports - @property - def PortItems(self) -> List[PortInterfaceItemMixin]: - return self._portItems - @property def Architectures(self) -> Dict[str, 'Architecture']: return self._architectures @@ -704,7 +723,7 @@ def __init__( identifier: str, entity: EntitySymbol, contextItems: Nullable[Iterable[Context]] = None, - declaredItems: Nullable[Iterable] = None, + declaredItems: Nullable[Union[Iterable, SignalGroups]] = None, statements: Iterable['ConcurrentStatement'] = None, documentation: Nullable[str] = None, allowBlackbox: Nullable[bool] = None, @@ -754,7 +773,7 @@ def __repr__(self) -> str: @export -class Component(ModelEntity, NamedEntityMixin, DocumentedEntityMixin): +class Component(ModelEntity, NamedEntityMixin, DocumentedEntityMixin, DesignUnitInterfaceMixin): """ Represents a configuration declaration. @@ -770,9 +789,6 @@ class Component(ModelEntity, NamedEntityMixin, DocumentedEntityMixin): _allowBlackbox: Nullable[bool] #: Allow component to be a blackbox. _isBlackBox: Nullable[bool] #: Component is a blackbox. - _genericItems: List[GenericInterfaceItemMixin] - _portItems: List[PortInterfaceItemMixin] - _entity: Nullable[Entity] def __init__( @@ -792,19 +808,7 @@ def __init__( self._isBlackBox = None self._entity = None - # TODO: extract to mixin - self._genericItems = [] - if genericItems is not None: - for item in genericItems: - self._genericItems.append(item) - item._parent = self - - # TODO: extract to mixin - self._portItems = [] - if portItems is not None: - for item in portItems: - self._portItems.append(item) - item._parent = self + DesignUnitInterfaceMixin.__init__(self, genericItems, portItems) @property def AllowBlackbox(self) -> bool: @@ -831,14 +835,6 @@ def IsBlackbox(self) -> bool: """ return self._isBlackBox - @property - def GenericItems(self) -> List[GenericInterfaceItemMixin]: - return self._genericItems - - @property - def PortItems(self) -> List[PortInterfaceItemMixin]: - return self._portItems - @property def Entity(self) -> Nullable[Entity]: return self._entity diff --git a/pyVHDLModel/Interface.py b/pyVHDLModel/Interface.py index 9b690affb..56c8b3302 100644 --- a/pyVHDLModel/Interface.py +++ b/pyVHDLModel/Interface.py @@ -34,13 +34,13 @@ Interface items are used in generic, port and parameter declarations. """ -from typing import Iterable, Optional as Nullable +from typing import Iterable, Optional as Nullable, Dict, List from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyVHDLModel.Symbol import Symbol -from pyVHDLModel.Base import ModelEntity, DocumentedEntityMixin, ExpressionUnion, Mode, NamedEntityMixin +from pyVHDLModel.Base import ModelEntity, DocumentedEntityMixin, ExpressionUnion, Mode, NamedEntityMixin, Groups from pyVHDLModel.Object import Constant, Signal, Variable, File from pyVHDLModel.Subprogram import Procedure, Function from pyVHDLModel.Type import Type @@ -218,3 +218,11 @@ def __init__( ) -> None: super().__init__(identifiers, subtype, documentation, parent) ParameterInterfaceItemMixin.__init__(self) + + +@export +class PortGroups(Groups[PortInterfaceItemMixin]): + """A typed dictionary for grouping lists of port interface items by name (string keys) or None for ungrouped items.""" + + def __init__(self, data: Dict[str | None, List[PortInterfaceItemMixin]]): + super().__init__(data, PortInterfaceItemMixin) diff --git a/pyVHDLModel/Object.py b/pyVHDLModel/Object.py index 54cac0bc4..cec427cae 100644 --- a/pyVHDLModel/Object.py +++ b/pyVHDLModel/Object.py @@ -34,13 +34,13 @@ Objects are constants, variables, signals and files. """ -from typing import Iterable, Optional as Nullable +from typing import Iterable, Optional as Nullable, Dict, List from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyTooling.Graph import Vertex -from pyVHDLModel.Base import ModelEntity, MultipleNamedEntityMixin, DocumentedEntityMixin, ExpressionUnion +from pyVHDLModel.Base import ModelEntity, MultipleNamedEntityMixin, DocumentedEntityMixin, ExpressionUnion, Groups from pyVHDLModel.Symbol import Symbol @@ -244,3 +244,11 @@ class File(Obj): .. todo:: File object not implemented. """ + + +@export +class SignalGroups(Groups[Signal]): + """A typed dictionary for grouping lists of signal objects by name (string keys) or None for ungrouped items.""" + + def __init__(self, data: Dict[str | None, List[Signal]] = {}): + super().__init__(data, Signal) diff --git a/pyVHDLModel/Regions.py b/pyVHDLModel/Regions.py index 9754497cc..16dc3e8b8 100644 --- a/pyVHDLModel/Regions.py +++ b/pyVHDLModel/Regions.py @@ -34,16 +34,16 @@ tbd. """ -from typing import List, Dict, Iterable, Optional as Nullable +from typing import List, Dict, Iterable, Optional as Nullable, Any from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType -from pyVHDLModel.Object import Constant, SharedVariable, File, Variable, Signal +from pyVHDLModel.Base import Groups +from pyVHDLModel.Object import Constant, SharedVariable, File, Variable, Signal, SignalGroups from pyVHDLModel.Subprogram import Subprogram, Function, Procedure from pyVHDLModel.Type import Subtype, FullType - @export class ConcurrentDeclarationRegionMixin(metaclass=ExtendedType, mixin=True): # FIXME: define list prefix type e.g. via Union @@ -62,13 +62,28 @@ class ConcurrentDeclarationRegionMixin(metaclass=ExtendedType, mixin=True): _functions: Dict[str, Dict[str, Function]] #: Dictionary of all functions declared in this concurrent declaration region. _procedures: Dict[str, Dict[str, Procedure]] #: Dictionary of all procedures declared in this concurrent declaration region. + _signalGroups: SignalGroups #: Dictionary of all signal groups declared in this concurrent declaration region. + def __init__(self, declaredItems: Nullable[Iterable] = None) -> None: # TODO: extract to mixin self._declaredItems = [] # TODO: convert to dict + self._signalGroups = SignalGroups() if declaredItems is not None: for item in declaredItems: - self._declaredItems.append(item) - item._parent = self + if isinstance(item, Groups): + if isinstance(item, SignalGroups): + groups = self._signalGroups + else: + raise ValueError(f"Unsupported group type: {type(item)}") + for groupName in item.keys(): + groups[groupName] = [] + for groupItem in item[groupName]: + groups[groupName].append(groupItem) + self._declaredItems.append(groupItem) + groupItem._parent = self + else: + self._declaredItems.append(item) + item._parent = self self._types = {} self._subtypes = {} @@ -125,6 +140,10 @@ def Functions(self) -> Dict[str, Dict[str, Function]]: def Procedures(self) -> Dict[str, Dict[str, Procedure]]: return self._procedures + @readonly + def SignalGroups(self) -> SignalGroups: + return self._signalGroups + def IndexDeclaredItems(self) -> None: """ Index declared items listed in the concurrent declaration region. diff --git a/tests/unit/Instantiate.py b/tests/unit/Instantiate.py index 68c37f02c..8e9eaab6a 100644 --- a/tests/unit/Instantiate.py +++ b/tests/unit/Instantiate.py @@ -36,15 +36,16 @@ from pyTooling.Graph import Graph from pyVHDLModel import Design, Library, Document, IEEEFlavor -from pyVHDLModel.Base import Direction, Range +from pyVHDLModel.Base import Direction, Range, Mode from pyVHDLModel.Name import SelectedName, SimpleName, AllName, AttributeName -from pyVHDLModel.Object import Constant, Signal +from pyVHDLModel.Object import Constant, Signal, SignalGroups from pyVHDLModel.Symbol import LibraryReferenceSymbol, PackageReferenceSymbol, PackageMemberReferenceSymbol, SimpleSubtypeSymbol from pyVHDLModel.Symbol import AllPackageMembersReferenceSymbol, ContextReferenceSymbol, EntitySymbol from pyVHDLModel.Symbol import ArchitectureSymbol, PackageSymbol, EntityInstantiationSymbol from pyVHDLModel.Symbol import ComponentInstantiationSymbol, ConfigurationInstantiationSymbol from pyVHDLModel.Expression import IntegerLiteral, FloatingPointLiteral from pyVHDLModel.Type import Subtype, IntegerType, RealType, ArrayType, RecordType +from pyVHDLModel.Interface import PortSignalInterfaceItem, PortGroups from pyVHDLModel.DesignUnit import Package, PackageBody, Context, Entity, Architecture, Configuration @@ -397,6 +398,34 @@ def test_Entity(self) -> None: self.assertEqual(0, len(entity.DeclaredItems)) self.assertEqual(0, len(entity.Statements)) + def test_EntityWithPorts(self) -> None: + port1 = PortSignalInterfaceItem(["port1"], Mode.In, SimpleSubtypeSymbol(SimpleName("Bit"))) + port2 = PortSignalInterfaceItem(["port2"], Mode.Out, SimpleSubtypeSymbol(SimpleName("Bit"))) + entity = Entity("entity_1", portItems=[port1, port2]) + + self.assertEqual(2, len(entity.PortItems)) + self.assertEqual(port1, entity.PortItems[0]) + self.assertEqual(port2, entity.PortItems[1]) + + def test_EntityWithPortGroups(self) -> None: + group1port1 = PortSignalInterfaceItem(["port1"], Mode.In, SimpleSubtypeSymbol(SimpleName("Bit"))) + group1port2 = PortSignalInterfaceItem(["port2"], Mode.Out, SimpleSubtypeSymbol(SimpleName("Bit"))) + group2port1 = PortSignalInterfaceItem(["port3"], Mode.In, SimpleSubtypeSymbol(SimpleName("Bit"))) + group2port2 = PortSignalInterfaceItem(["port4"], Mode.Out, SimpleSubtypeSymbol(SimpleName("Bit"))) + group2port3 = PortSignalInterfaceItem(["port5"], Mode.In, SimpleSubtypeSymbol(SimpleName("Bit"))) + entity = Entity("entity_1", portItems=PortGroups({ + "group1": [group1port1, group1port2], + "group2": [group2port1, group2port2, group2port3] + })) + + self.assertEqual(5, len(entity.PortItems)) + self.assertEqual(2, len(entity.PortGroups)) + self.assertEqual(group1port1, entity.PortGroups["group1"][0]) + self.assertEqual(group1port2, entity.PortGroups["group1"][1]) + self.assertEqual(group2port1, entity.PortGroups["group2"][0]) + self.assertEqual(group2port2, entity.PortGroups["group2"][1]) + self.assertEqual(group2port3, entity.PortGroups["group2"][2]) + def test_Architecture(self) -> None: entitySymbol = EntitySymbol(SimpleName("entity_1")) architecture = Architecture("arch_1", entitySymbol, parent=None) @@ -406,6 +435,54 @@ def test_Architecture(self) -> None: self.assertEqual(0, len(architecture.DeclaredItems)) self.assertEqual(0, len(architecture.Statements)) + def test_ArchitectureWithSignals(self) -> None: + entitySymbol = EntitySymbol(SimpleName("entity_1")) + signal1 = Signal(("signal1", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + signal2 = Signal(("signal2", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + architecture = Architecture("arch_1", entitySymbol, declaredItems=[signal1, signal2]) + architecture.IndexDeclaredItems() + + self.assertIsNotNone(architecture) + self.assertEqual("arch_1", architecture.Identifier) + self.assertEqual(2, len(architecture.DeclaredItems)) + self.assertEqual(signal1, architecture.DeclaredItems[0]) + self.assertEqual(signal2, architecture.DeclaredItems[1]) + self.assertEqual(2, len(architecture.Signals)) + self.assertEqual(signal1, architecture.Signals["signal1"]) + self.assertEqual(signal2, architecture.Signals["signal2"]) + + def test_ArchitectureWithSignalGroups(self) -> None: + entitySymbol = EntitySymbol(SimpleName("entity_1")) + group1signal1 = Signal(("signal1", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + group1signal2 = Signal(("signal2", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + group2signal1 = Signal(("signal3", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + group2signal2 = Signal(("signal4", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + group2signal3 = Signal(("signal5", ), SimpleSubtypeSymbol(SimpleName("Bit"))) + architecture = Architecture("arch_1", entitySymbol, declaredItems=[SignalGroups({ + "group1": [group1signal1, group1signal2], + "group2": [group2signal1, group2signal2, group2signal3] + })]) + architecture.IndexDeclaredItems() + + self.assertEqual(5, len(architecture.DeclaredItems)) + self.assertEqual(group1signal1, architecture.DeclaredItems[0]) + self.assertEqual(group1signal2, architecture.DeclaredItems[1]) + self.assertEqual(group2signal1, architecture.DeclaredItems[2]) + self.assertEqual(group2signal2, architecture.DeclaredItems[3]) + self.assertEqual(group2signal3, architecture.DeclaredItems[4]) + self.assertEqual(5, len(architecture.Signals)) + self.assertEqual(group1signal1, architecture.Signals["signal1"]) + self.assertEqual(group1signal2, architecture.Signals["signal2"]) + self.assertEqual(group2signal1, architecture.Signals["signal3"]) + self.assertEqual(group2signal2, architecture.Signals["signal4"]) + self.assertEqual(group2signal3, architecture.Signals["signal5"]) + self.assertEqual(2, len(architecture.SignalGroups)) + self.assertEqual(group1signal1, architecture.SignalGroups["group1"][0]) + self.assertEqual(group1signal2, architecture.SignalGroups["group1"][1]) + self.assertEqual(group2signal1, architecture.SignalGroups["group2"][0]) + self.assertEqual(group2signal2, architecture.SignalGroups["group2"][1]) + self.assertEqual(group2signal3, architecture.SignalGroups["group2"][2]) + def test_Package(self) -> None: package = Package("pack_1", parent=None)